[TASK] Move and Namespace classes
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Resource / Driver / LocalDriver.php
1 <?php
2 namespace TYPO3\CMS\Core\Resource\Driver;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2011 Andreas Wolf <andreas.wolf@ikt-werk.de>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 * A copy is found in the textfile GPL.txt and important notices to the license
19 * from the author is found in LICENSE.txt distributed with these scripts.
20 *
21 *
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29 /**
30 * Driver for the local file system
31 *
32 * @author Andreas Wolf <andreas.wolf@ikt-werk.de>
33 * @package TYPO3
34 * @subpackage t3lib
35 */
36 class LocalDriver extends \TYPO3\CMS\Core\Resource\Driver\AbstractDriver {
37
38 /**
39 * The absolute base path. It always contains a trailing slash.
40 *
41 * @var string
42 */
43 protected $absoluteBasePath;
44
45 /**
46 * A list of all supported hash algorithms, written all lower case.
47 *
48 * @var array
49 */
50 protected $supportedHashAlgorithms = array('sha1', 'md5');
51
52 /**
53 * The base URL that points to this driver's storage. As long is this
54 * is not set, it is assumed that this folder is not publicly available
55 *
56 * @var string
57 */
58 protected $baseUri;
59
60 /**
61 * @var \TYPO3\CMS\Core\Charset\CharsetConverter
62 */
63 protected $charsetConversion;
64
65 /**
66 * Checks if a configuration is valid for this storage.
67 *
68 * @param array $configuration The configuration
69 * @return void
70 * @throws \TYPO3\CMS\Core\Resource\Exception\InvalidConfigurationException
71 */
72 static public function verifyConfiguration(array $configuration) {
73 self::calculateBasePath($configuration);
74 }
75
76 /**
77 * Processes the configuration for this driver.
78 *
79 * @return void
80 */
81 public function processConfiguration() {
82 $this->absoluteBasePath = $this->calculateBasePath($this->configuration);
83 }
84
85 /**
86 * Initializes this object. This is called by the storage after the driver
87 * has been attached.
88 *
89 * @return void
90 */
91 public function initialize() {
92 $this->determineBaseUrl();
93 // The capabilities of this driver. See CAPABILITY_* constants for possible values
94 $this->capabilities = (\TYPO3\CMS\Core\Resource\ResourceStorage::CAPABILITY_BROWSABLE | \TYPO3\CMS\Core\Resource\ResourceStorage::CAPABILITY_PUBLIC) | \TYPO3\CMS\Core\Resource\ResourceStorage::CAPABILITY_WRITABLE;
95 }
96
97 /**
98 * Determines the base URL for this driver, from the configuration or
99 * the TypoScript frontend object
100 *
101 * @return void
102 */
103 protected function determineBaseUrl() {
104 if (\TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($this->absoluteBasePath, PATH_site)) {
105 // use site-relative URLs
106 // TODO add unit test
107 $this->baseUri = substr($this->absoluteBasePath, strlen(PATH_site));
108 } elseif (isset($this->configuration['baseUri']) && \TYPO3\CMS\Core\Utility\GeneralUtility::isValidUrl($this->configuration['baseUri'])) {
109 $this->baseUri = rtrim($this->configuration['baseUri'], '/') . '/';
110 } else {
111
112 }
113 }
114
115 /**
116 * Calculates the absolute path to this drivers storage location.
117 *
118 * @throws RuntimeException
119 * @param array $configuration
120 * @return string
121 */
122 protected function calculateBasePath(array $configuration) {
123 if ($configuration['pathType'] === 'relative') {
124 $relativeBasePath = $configuration['basePath'];
125 $absoluteBasePath = PATH_site . $relativeBasePath;
126 } else {
127 $absoluteBasePath = $configuration['basePath'];
128 }
129 $absoluteBasePath = rtrim($absoluteBasePath, '/') . '/';
130 if (!is_dir($absoluteBasePath)) {
131 throw new \TYPO3\CMS\Core\Resource\Exception\InvalidConfigurationException(('Base path "' . $absoluteBasePath) . '" does not exist or is no directory.', 1299233097);
132 }
133 return $absoluteBasePath;
134 }
135
136 /**
137 * Returns the public URL to a file. For the local driver, this will always
138 * return a path relative to PATH_site.
139 *
140 * @param \TYPO3\CMS\Core\Resource\ResourceInterface $fileOrFolder
141 * @param bool $relativeToCurrentScript Determines whether the URL returned should be relative to the current script, in case it is relative at all (only for the LocalDriver)
142 * @return string
143 */
144 public function getPublicUrl(\TYPO3\CMS\Core\Resource\ResourceInterface $fileOrFolder, $relativeToCurrentScript = FALSE) {
145 if ($this->configuration['pathType'] === 'relative' && rtrim($this->configuration['basePath'], '/') !== '') {
146 $publicUrl = (rtrim($this->configuration['basePath'], '/') . '/') . ltrim($fileOrFolder->getIdentifier(), '/');
147 } elseif (isset($this->baseUri)) {
148 $publicUrl = $this->baseUri . ltrim($fileOrFolder->getIdentifier(), '/');
149 } else {
150 throw new \TYPO3\CMS\Core\Resource\Exception('Public URL of file cannot be determined', 1329765518);
151 }
152 // If requested, make the path relative to the current script in order to make it possible
153 // to use the relative file
154 if ($relativeToCurrentScript) {
155 $publicUrl = \TYPO3\CMS\Core\Utility\PathUtility::getRelativePathTo(dirname((PATH_site . $publicUrl))) . basename($publicUrl);
156 }
157 return $publicUrl;
158 }
159
160 /**
161 * Returns the root level folder of the storage.
162 *
163 * @return \TYPO3\CMS\Core\Resource\Folder
164 */
165 public function getRootLevelFolder() {
166 if (!$this->rootLevelFolder) {
167 $this->rootLevelFolder = \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->createFolderObject($this->storage, '/', '');
168 }
169 return $this->rootLevelFolder;
170 }
171
172 /**
173 * Returns the default folder new files should be put into.
174 *
175 * @return \TYPO3\CMS\Core\Resource\Folder
176 */
177 public function getDefaultFolder() {
178 if (!$this->defaultLevelFolder) {
179 if (!file_exists(($this->absoluteBasePath . '_temp_/'))) {
180 mkdir($this->absoluteBasePath . '_temp_/');
181 }
182 $this->defaultLevelFolder = \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->createFolderObject($this->storage, '/_temp_/', '');
183 }
184 return $this->defaultLevelFolder;
185 }
186
187 /**
188 * Creates a folder.
189 *
190 * @param string $newFolderName
191 * @param \TYPO3\CMS\Core\Resource\Folder $parentFolder
192 * @return \TYPO3\CMS\Core\Resource\Folder The new (created) folder object
193 */
194 public function createFolder($newFolderName, \TYPO3\CMS\Core\Resource\Folder $parentFolder) {
195 $newFolderName = trim($this->sanitizeFileName($newFolderName), '/');
196 $newFolderPath = $this->getAbsolutePath($parentFolder) . $newFolderName;
197 \TYPO3\CMS\Core\Utility\GeneralUtility::mkdir($newFolderPath);
198 return \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->createFolderObject($this->storage, $parentFolder->getIdentifier() . $newFolderName, $newFolderName);
199 }
200
201 /**
202 * Returns information about a file.
203 *
204 * @param string $fileIdentifier In the case of the LocalDriver, this is the (relative) path to the file.
205 * @return array
206 */
207 public function getFileInfoByIdentifier($fileIdentifier) {
208 // Makes sure the Path given as parameter is valid
209 $this->checkFilePath($fileIdentifier);
210 $dirPath = dirname($fileIdentifier);
211 if ($dirPath !== '' && $dirPath !== '/') {
212 $dirPath = ('/' . trim($dirPath, '/')) . '/';
213 }
214 $absoluteFilePath = $this->absoluteBasePath . ltrim($fileIdentifier, '/');
215 // don't use $this->fileExists() because we need the absolute path to the file anyways, so we can directly
216 // use PHP's filesystem method.
217 if (!file_exists($absoluteFilePath)) {
218 throw new \InvalidArgumentException(('File ' . $fileIdentifier) . ' does not exist.', 1314516809);
219 }
220 return $this->extractFileInformation($absoluteFilePath, $dirPath);
221 }
222
223 /**
224 * Wrapper for t3lib_div::validPathStr()
225 *
226 * @param string $theFile Filepath to evaluate
227 * @return boolean TRUE if no '/', '..' or '\' is in the $theFile
228 * @see t3lib_div::validPathStr()
229 */
230 protected function isPathValid($theFile) {
231 return \TYPO3\CMS\Core\Utility\GeneralUtility::validPathStr($theFile);
232 }
233
234 /**
235 * Returns a string where any character not matching [.a-zA-Z0-9_-] is
236 * substituted by '_'
237 * Trailing dots are removed
238 *
239 * previously in t3lib_basicFileFunctions::cleanFileName()
240 *
241 * @param string $fileName Input string, typically the body of a fileName
242 * @param string $charset Charset of the a fileName (defaults to current charset; depending on context)
243 * @return string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed
244 */
245 protected function sanitizeFileName($fileName, $charset = '') {
246 // Handle UTF-8 characters
247 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
248 // Allow ".", "-", 0-9, a-z, A-Z and everything beyond U+C0 (latin capital letter a with grave)
249 $cleanFileName = preg_replace('/[\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF]/u', '_', trim($fileName));
250 } else {
251 // Define character set
252 if (!$charset) {
253 if (TYPO3_MODE === 'FE') {
254 $charset = $GLOBALS['TSFE']->renderCharset;
255 } elseif (is_object($GLOBALS['LANG'])) {
256 // BE assumed:
257 $charset = $GLOBALS['LANG']->charSet;
258 } else {
259 // best guess
260 $charset = 'utf-8';
261 }
262 }
263 // If a charset was found, convert fileName
264 if ($charset) {
265 $fileName = $this->getCharsetConversion()->specCharsToASCII($charset, $fileName);
266 }
267 // Replace unwanted characters by underscores
268 $cleanFileName = preg_replace('/[^.[:alnum:]_-]/', '_', trim($fileName));
269 }
270 // Strip trailing dots and return
271 $cleanFileName = preg_replace('/\\.*$/', '', $cleanFileName);
272 if (!$cleanFileName) {
273 throw new \TYPO3\CMS\Core\Resource\Exception\InvalidFileNameException(('File name ' . $cleanFileName) . ' is invalid.', 1320288991);
274 }
275 return $cleanFileName;
276 }
277
278 /**
279 * Generic wrapper for extracting a list of items from a path. The
280 * extraction itself is done by the given handler method
281 *
282 * @param string $path
283 * @param integer $start The position to start the listing; if not set, start from the beginning
284 * @param integer $numberOfItems The number of items to list; if set to zero, all items are returned
285 * @param array $filterMethods The filter methods used to filter the directory items
286 * @param string $itemHandlerMethod The method (in this class) that handles the single iterator elements.
287 * @param array $itemRows
288 * @return array
289 */
290 // TODO add unit tests
291 protected function getDirectoryItemList($path, $start, $numberOfItems, array $filterMethods, $itemHandlerMethod, $itemRows = array()) {
292 $realPath = rtrim(($this->absoluteBasePath . trim($path, '/')), '/') . '/';
293 if (!is_dir($realPath)) {
294 throw new \InvalidArgumentException(('Cannot list items in directory ' . $path) . ' - does not exist or is no directory', 1314349666);
295 }
296 if ($start > 0) {
297 $start--;
298 }
299 // Fetch the files and folders and sort them by name; we have to do
300 // this here because the directory iterator does return them in
301 // an arbitrary order
302 $items = $this->getFileAndFoldernamesInPath($realPath);
303 natcasesort($items);
304 $iterator = new \ArrayIterator($items);
305 if ($iterator->count() == 0) {
306 return array();
307 }
308 $iterator->seek($start);
309 if ($path !== '' && $path !== '/') {
310 $path = ('/' . trim($path, '/')) . '/';
311 }
312 // $c is the counter for how many items we still have to fetch (-1 is unlimited)
313 $c = $numberOfItems > 0 ? $numberOfItems : -1;
314 $items = array();
315 while ($iterator->valid() && ($numberOfItems == 0 || $c > 0)) {
316 // $iteratorItem is the file or folder name
317 $iteratorItem = $iterator->current();
318 // go on to the next iterator item now as we might skip this one early
319 $iterator->next();
320 $identifier = $path . $iteratorItem;
321 if ($this->applyFilterMethodsToDirectoryItem($filterMethods, $iteratorItem, $identifier, $path) === FALSE) {
322 continue;
323 }
324 if (isset($itemRows[$identifier])) {
325 list($key, $item) = $this->{$itemHandlerMethod}($iteratorItem, $path, $itemRows[$identifier]);
326 } else {
327 list($key, $item) = $this->{$itemHandlerMethod}($iteratorItem, $path);
328 }
329 if (empty($item)) {
330 continue;
331 }
332 $items[$key] = $item;
333 // Decrement item counter to make sure we only return $numberOfItems
334 // we cannot do this earlier in the method (unlike moving the iterator forward) because we only add the
335 // item here
336 --$c;
337 }
338 return $items;
339 }
340
341 /**
342 * Handler for items in a file list.
343 *
344 * @param string $fileName
345 * @param string $path
346 * @param array $fileRow The pre-loaded file row
347 * @return array
348 */
349 protected function getFileList_itemCallback($fileName, $path, array $fileRow = array()) {
350 $filePath = $this->getAbsolutePath($path . $fileName);
351 if (!is_file($filePath)) {
352 return array('', array());
353 }
354 // TODO add unit test for existing file row case
355 if (!empty($fileRow) && filemtime($filePath) <= $fileRow['modification_date']) {
356 return array($fileName, $fileRow);
357 } else {
358 return array($fileName, $this->extractFileInformation($filePath, $path));
359 }
360 }
361
362 /**
363 * Handler for items in a directory listing.
364 *
365 * @param string $folderName The folder's name
366 * @param string $parentPath The path to the folder's parent folder
367 * @param array $folderRow [optional]
368 * @return array
369 */
370 protected function getFolderList_itemCallback($folderName, $parentPath, array $folderRow = array()) {
371 $folderPath = $this->getAbsolutePath($parentPath . $folderName);
372 if (!is_dir($folderPath)) {
373 return array('', array());
374 }
375 // also don't show hidden files
376 if ((($folderName === '..' || $folderName === '.') || $folderName === '') || \TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($folderName, '.') === TRUE) {
377 return array('', array());
378 }
379 // remove the trailing slash from the folder name (the trailing slash comes from the DirectoryIterator)
380 $folderName = substr($folderName, 0, -1);
381 return array($folderName, $this->extractFolderInformation($folderPath, $parentPath));
382 }
383
384 /**
385 * Returns a list with the names of all files and folders in a path, optionally recursive.
386 * Folder names have a trailing slash.
387 *
388 * @param string $path The absolute path
389 * @param bool $recursive If TRUE, recursively fetches files and folders
390 * @return array
391 */
392 protected function getFileAndFoldernamesInPath($path, $recursive = FALSE) {
393 if ($recursive) {
394 $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \FilesystemIterator::CURRENT_AS_FILEINFO));
395 } else {
396 $iterator = new \RecursiveDirectoryIterator($path, \FilesystemIterator::CURRENT_AS_FILEINFO);
397 }
398 $directoryEntries = array();
399 while ($iterator->valid()) {
400 /** @var $entry SplFileInfo */
401 $entry = $iterator->current();
402 // skip non-files/non-folders, and empty entries
403 if (!$entry->isFile() && !$entry->isDir() || $entry->getFilename() == '') {
404 $iterator->next();
405 continue;
406 }
407 // skip the pseudo-directories "." and ".."
408 if ($entry->getFilename() == '..' || $entry->getFilename() == '.') {
409 $iterator->next();
410 continue;
411 }
412 $entryPath = substr($entry->getPathname(), strlen($path));
413 if ($entry->isDir()) {
414 $entryPath .= '/';
415 }
416 $directoryEntries[] = $entryPath;
417 $iterator->next();
418 }
419 return $directoryEntries;
420 }
421
422 /**
423 * Extracts information about a file from the filesystem.
424 *
425 * @param string $filePath The absolute path to the file
426 * @param string $containerPath The relative path to the file's container
427 * @return array
428 */
429 protected function extractFileInformation($filePath, $containerPath) {
430 $fileName = basename($filePath);
431 $fileMimeInformation = new \finfo(FILEINFO_MIME_TYPE);
432 $fileInformation = array(
433 'size' => filesize($filePath),
434 'atime' => fileatime($filePath),
435 'mtime' => filemtime($filePath),
436 'ctime' => filectime($filePath),
437 'mimetype' => $fileMimeInformation->file($filePath),
438 'name' => $fileName,
439 'identifier' => $containerPath . $fileName,
440 'storage' => $this->storage->getUid()
441 );
442 return $fileInformation;
443 }
444
445 /**
446 * Extracts information about a folder from the filesystem.
447 *
448 * @param string $folderPath The absolute path to the folder
449 * @param string $containerPath The relative path to the folder's container inside the storage (must end with a trailing slash)
450 * @return array
451 */
452 protected function extractFolderInformation($folderPath, $containerPath) {
453 $folderName = basename($folderPath);
454 $folderInformation = array(
455 'ctime' => filectime($folderPath),
456 'mtime' => filemtime($folderPath),
457 'name' => $folderName,
458 'identifier' => ($containerPath . $folderName) . '/',
459 'storage' => $this->storage->getUid()
460 );
461 return $folderInformation;
462 }
463
464 /**
465 * Returns the absolute path of the folder this driver operates on.
466 *
467 * @return string
468 */
469 public function getAbsoluteBasePath() {
470 return $this->absoluteBasePath;
471 }
472
473 /**
474 * Returns the absolute path of a file or folder.
475 *
476 * @param t3lib_file_FileInterface|t3lib_file_Folder|string $file
477 * @return string
478 */
479 public function getAbsolutePath($file) {
480 if ($file instanceof \TYPO3\CMS\Core\Resource\FileInterface) {
481 $path = $this->absoluteBasePath . ltrim($file->getIdentifier(), '/');
482 } elseif ($file instanceof \TYPO3\CMS\Core\Resource\Folder) {
483 // We can assume a trailing slash here because it is added by the folder object on construction.
484 $path = $this->absoluteBasePath . ltrim($file->getIdentifier(), '/');
485 } elseif (is_string($file)) {
486 $path = $this->absoluteBasePath . ltrim($file, '/');
487 } else {
488 throw new \RuntimeException(('Type "' . gettype($file)) . '" is not supported.', 1325191178);
489 }
490 return $path;
491 }
492
493 /**
494 * Returns metadata of a file (size, times, mimetype)
495 *
496 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
497 * @return array
498 */
499 public function getLowLevelFileInfo(\TYPO3\CMS\Core\Resource\FileInterface $file) {
500 // TODO define which data should be returned
501 // TODO write unit test
502 // TODO cache this info. Registry?
503 $filePath = $this->getAbsolutePath($file);
504 $fileStat = stat($filePath);
505 $fileInfo = new \finfo();
506 $stat = array(
507 'size' => filesize($filePath),
508 'atime' => $fileStat['atime'],
509 'mtime' => $fileStat['mtime'],
510 'ctime' => $fileStat['ctime'],
511 'nlink' => $fileStat['nlink'],
512 'type' => $fileInfo->file($filePath, FILEINFO_MIME_TYPE),
513 'mimetype' => $fileInfo->file($filePath, FILEINFO_MIME_TYPE)
514 );
515 return $stat;
516 }
517
518 /**
519 * Creates a (cryptographic) hash for a file.
520 *
521 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
522 * @param string $hashAlgorithm The hash algorithm to use
523 * @return string
524 */
525 public function hash(\TYPO3\CMS\Core\Resource\FileInterface $file, $hashAlgorithm) {
526 if (!in_array($hashAlgorithm, $this->getSupportedHashAlgorithms())) {
527 throw new \InvalidArgumentException(('Hash algorithm "' . $hashAlgorithm) . '" is not supported.', 1304964032);
528 }
529 switch ($hashAlgorithm) {
530 case 'sha1':
531 $hash = sha1_file($this->getAbsolutePath($file));
532 break;
533 case 'md5':
534 $hash = md5_file($this->getAbsolutePath($file));
535 break;
536 default:
537 throw new \RuntimeException(('Hash algorithm ' . $hashAlgorithm) . ' is not implemented.', 1329644451);
538 }
539 return $hash;
540 }
541
542 /**
543 * Adds a file from the local server hard disk to a given path in TYPO3s virtual file system.
544 *
545 * This assumes that the local file exists, so no further check is done here!
546 *
547 * @param string $localFilePath
548 * @param \TYPO3\CMS\Core\Resource\Folder $targetFolder
549 * @param string $fileName The name to add the file under
550 * @param \TYPO3\CMS\Core\Resource\AbstractFile $updateFileObject File object to update (instead of creating a new object). With this parameter, this function can be used to "populate" a dummy file object with a real file underneath.
551 * @todo t3lib_file_File $updateFileObject should be t3lib_file_FileInterface, but indexer logic is only in t3lib_file_File
552 * @return \TYPO3\CMS\Core\Resource\FileInterface
553 */
554 public function addFile($localFilePath, \TYPO3\CMS\Core\Resource\Folder $targetFolder, $fileName, \TYPO3\CMS\Core\Resource\AbstractFile $updateFileObject = NULL) {
555 // as for the "virtual storage" for backwards-compatibility, this check always fails, as the file probably lies under PATH_site
556 // thus, it is not checked here
557 if (\TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($localFilePath, $this->absoluteBasePath) && $this->storage->getUid() > 0) {
558 throw new \InvalidArgumentException('Cannot add a file that is already part of this storage.', 1314778269);
559 }
560 $relativeTargetPath = ltrim($targetFolder->getIdentifier(), '/');
561 $relativeTargetPath .= $fileName ? $fileName : basename($localFilePath);
562 $targetPath = $this->absoluteBasePath . $relativeTargetPath;
563 if (is_uploaded_file($localFilePath)) {
564 $moveResult = move_uploaded_file($localFilePath, $targetPath);
565 } else {
566 $moveResult = rename($localFilePath, $targetPath);
567 }
568 if ($moveResult !== TRUE) {
569 throw new \RuntimeException(((('Moving file ' . $localFilePath) . ' to ') . $targetPath) . ' failed.', 1314803096);
570 }
571 clearstatcache();
572 // Change the permissions of the file
573 \TYPO3\CMS\Core\Utility\GeneralUtility::fixPermissions($targetPath);
574 $fileInfo = $this->getFileInfoByIdentifier($relativeTargetPath);
575 if ($updateFileObject) {
576 $updateFileObject->updateProperties($fileInfo);
577 return $updateFileObject;
578 } else {
579 $fileObject = $this->getFileObject($fileInfo);
580 return $fileObject;
581 }
582 }
583
584 /**
585 * Checks if a resource exists - does not care for the type (file or folder).
586 *
587 * @param $identifier
588 * @return boolean
589 */
590 public function resourceExists($identifier) {
591 $absoluteResourcePath = $this->absoluteBasePath . ltrim($identifier, '/');
592 return file_exists($absoluteResourcePath);
593 }
594
595 /**
596 * Checks if a file exists.
597 *
598 * @param string $identifier
599 * @return boolean
600 */
601 public function fileExists($identifier) {
602 $absoluteFilePath = $this->absoluteBasePath . ltrim($identifier, '/');
603 return is_file($absoluteFilePath);
604 }
605
606 /**
607 * Checks if a file inside a storage folder exists
608 *
609 * @param string $fileName
610 * @param \TYPO3\CMS\Core\Resource\Folder $folder
611 * @return boolean
612 */
613 public function fileExistsInFolder($fileName, \TYPO3\CMS\Core\Resource\Folder $folder) {
614 $identifier = ltrim($folder->getIdentifier(), '/') . $fileName;
615 return $this->fileExists($identifier);
616 }
617
618 /**
619 * Checks if a folder exists.
620 *
621 * @param string $identifier
622 * @return boolean
623 */
624 public function folderExists($identifier) {
625 $absoluteFilePath = $this->absoluteBasePath . ltrim($identifier, '/');
626 return is_dir($absoluteFilePath);
627 }
628
629 /**
630 * Checks if a file inside a storage folder exists.
631 *
632 * @param string $folderName
633 * @param \TYPO3\CMS\Core\Resource\Folder $folder
634 * @return boolean
635 */
636 public function folderExistsInFolder($folderName, \TYPO3\CMS\Core\Resource\Folder $folder) {
637 $identifier = $folder->getIdentifier() . $folderName;
638 return $this->folderExists($identifier);
639 }
640
641 /**
642 * Returns a folder within the given folder.
643 *
644 * @param string $name The name of the folder to get
645 * @param \TYPO3\CMS\Core\Resource\Folder $parentFolder
646 * @return \TYPO3\CMS\Core\Resource\Folder
647 */
648 public function getFolderInFolder($name, \TYPO3\CMS\Core\Resource\Folder $parentFolder) {
649 $folderIdentifier = ($parentFolder->getIdentifier() . $name) . '/';
650 return $this->getFolder($folderIdentifier);
651 }
652
653 /**
654 * Replaces the contents (and file-specific metadata) of a file object with a local file.
655 *
656 * @param \TYPO3\CMS\Core\Resource\AbstractFile $file
657 * @param string $localFilePath
658 * @return boolean TRUE if the operation succeeded
659 */
660 public function replaceFile(\TYPO3\CMS\Core\Resource\AbstractFile $file, $localFilePath) {
661 $filePath = $this->getAbsolutePath($file);
662 $result = rename($localFilePath, $filePath);
663 if ($result === FALSE) {
664 throw new \RuntimeException(((('Replacing file ' . $filePath) . ' with ') . $localFilePath) . ' failed.', 1315314711);
665 }
666 $fileInfo = $this->getFileInfoByIdentifier($file->getIdentifier());
667 $file->updateProperties($fileInfo);
668 // TODO update index
669 return $result;
670 }
671
672 /**
673 * Adds a file at the specified location. This should only be used internally.
674 *
675 * @param string $localFilePath
676 * @param \TYPO3\CMS\Core\Resource\Folder $targetFolder
677 * @param string $targetFileName
678 * @return boolean TRUE if adding the file succeeded
679 */
680 public function addFileRaw($localFilePath, \TYPO3\CMS\Core\Resource\Folder $targetFolder, $targetFileName) {
681 $fileIdentifier = $targetFolder->getIdentifier() . $targetFileName;
682 $absoluteFilePath = $this->absoluteBasePath . $fileIdentifier;
683 $result = copy($localFilePath, $absoluteFilePath);
684 if ($result === FALSE || !file_exists($absoluteFilePath)) {
685 throw new \RuntimeException(((('Adding file ' . $localFilePath) . ' at ') . $fileIdentifier) . ' failed.');
686 }
687 return $fileIdentifier;
688 }
689
690 /**
691 * Deletes a file without access and usage checks. This should only be used internally.
692 *
693 * This accepts an identifier instead of an object because we might want to delete files that have no object
694 * associated with (or we don't want to create an object for) them - e.g. when moving a file to another storage.
695 *
696 * @param string $identifier
697 * @return bool TRUE if removing the file succeeded
698 */
699 public function deleteFileRaw($identifier) {
700 $targetPath = $this->absoluteBasePath . ltrim($identifier, '/');
701 $result = unlink($targetPath);
702 if ($result === FALSE || file_exists($targetPath)) {
703 throw new \RuntimeException(('Deleting file ' . $identifier) . ' failed.', 1320381534);
704 }
705 return TRUE;
706 }
707
708 /**
709 * Copies a file *within* the current storage.
710 * Note that this is only about an intra-storage move action, where a file is just
711 * moved to another folder in the same storage.
712 *
713 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
714 * @param \TYPO3\CMS\Core\Resource\Folder $targetFolder
715 * @param string $fileName
716 * @return \TYPO3\CMS\Core\Resource\FileInterface The new (copied) file object.
717 */
718 public function copyFileWithinStorage(\TYPO3\CMS\Core\Resource\FileInterface $file, \TYPO3\CMS\Core\Resource\Folder $targetFolder, $fileName) {
719 // TODO add unit test
720 $sourcePath = $this->getAbsolutePath($file);
721 $targetPath = ltrim($targetFolder->getIdentifier(), '/') . $fileName;
722 copy($sourcePath, $this->absoluteBasePath . $targetPath);
723 return $this->getFile($targetPath);
724 }
725
726 /**
727 * Moves a file *within* the current storage.
728 * Note that this is only about an intra-storage move action, where a file is just
729 * moved to another folder in the same storage.
730 *
731 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
732 * @param \TYPO3\CMS\Core\Resource\Folder $targetFolder
733 * @param string $fileName
734 * @return boolean
735 */
736 public function moveFileWithinStorage(\TYPO3\CMS\Core\Resource\FileInterface $file, \TYPO3\CMS\Core\Resource\Folder $targetFolder, $fileName) {
737 $sourcePath = $this->getAbsolutePath($file);
738 $targetIdentifier = $targetFolder->getIdentifier() . $fileName;
739 $result = rename($sourcePath, $this->absoluteBasePath . $targetIdentifier);
740 if ($result === FALSE) {
741 throw new \RuntimeException(((('Moving file ' . $sourcePath) . ' to ') . $targetIdentifier) . ' failed.', 1315314712);
742 }
743 return $targetIdentifier;
744 }
745
746 /**
747 * Copies a file to a temporary path and returns that path.
748 *
749 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
750 * @return string The temporary path
751 */
752 public function copyFileToTemporaryPath(\TYPO3\CMS\Core\Resource\FileInterface $file) {
753 $sourcePath = $this->getAbsolutePath($file);
754 $temporaryPath = $this->getTemporaryPathForFile($file);
755 $result = copy($sourcePath, $temporaryPath);
756 if ($result === FALSE) {
757 throw new \RuntimeException(('Copying file ' . $file->getIdentifier()) . ' to temporary path failed.', 1320577649);
758 }
759 return $temporaryPath;
760 }
761
762 /**
763 * Creates a map of old and new file/folder identifiers after renaming or
764 * moving a folder. The old identifier is used as the key, the new one as the value.
765 *
766 * @param array $filesAndFolders
767 * @param string $relativeSourcePath
768 * @param string $relativeTargetPath
769 * @return array
770 */
771 protected function createIdentifierMap(array $filesAndFolders, $relativeSourcePath, $relativeTargetPath) {
772 $identifierMap = array();
773 $identifierMap[$relativeSourcePath] = $relativeTargetPath;
774 foreach ($filesAndFolders as $oldSubIdentifier) {
775 $oldIdentifier = $relativeSourcePath . $oldSubIdentifier;
776 $newIdentifier = $relativeTargetPath . $oldSubIdentifier;
777 if (!$this->resourceExists($newIdentifier)) {
778 throw new \TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException(sprintf('File "%1$s" was not found (should have been copied/moved from "%2$s").', $newIdentifier, $oldIdentifier), 1330119453);
779 }
780 $identifierMap[$oldIdentifier] = $newIdentifier;
781 }
782 return $identifierMap;
783 }
784
785 /**
786 * Folder equivalent to moveFileWithinStorage().
787 *
788 * @param \TYPO3\CMS\Core\Resource\Folder $folderToMove
789 * @param \TYPO3\CMS\Core\Resource\Folder $targetFolder
790 * @param string $newFolderName
791 * @return array A map of old to new file identifiers
792 */
793 public function moveFolderWithinStorage(\TYPO3\CMS\Core\Resource\Folder $folderToMove, \TYPO3\CMS\Core\Resource\Folder $targetFolder, $newFolderName) {
794 $relativeSourcePath = $folderToMove->getIdentifier();
795 $sourcePath = $this->getAbsolutePath($relativeSourcePath);
796 $relativeTargetPath = ($targetFolder->getIdentifier() . $newFolderName) . '/';
797 $targetPath = $this->getAbsolutePath($relativeTargetPath);
798 // get all files and folders we are going to move, to have a map for updating later.
799 $filesAndFolders = $this->getFileAndFoldernamesInPath($sourcePath, TRUE);
800 $result = rename($sourcePath, $targetPath);
801 if ($result === FALSE) {
802 throw new \RuntimeException(((('Moving folder ' . $sourcePath) . ' to ') . $targetPath) . ' failed.', 1320711817);
803 }
804 // Create a mapping from old to new identifiers
805 $identifierMap = $this->createIdentifierMap($filesAndFolders, $relativeSourcePath, $relativeTargetPath);
806 return $identifierMap;
807 }
808
809 /**
810 * Folder equivalent to copyFileWithinStorage().
811 *
812 * @param \TYPO3\CMS\Core\Resource\Folder $folderToCopy
813 * @param \TYPO3\CMS\Core\Resource\Folder $targetFolder
814 * @param string $newFolderName
815 * @return boolean
816 */
817 public function copyFolderWithinStorage(\TYPO3\CMS\Core\Resource\Folder $folderToCopy, \TYPO3\CMS\Core\Resource\Folder $targetFolder, $newFolderName) {
818 // This target folder path already includes the topmost level, i.e. the folder this method knows as $folderToCopy.
819 // We can thus rely on this folder being present and just create the subfolder we want to copy to.
820 $targetFolderPath = ($this->getAbsolutePath($targetFolder) . $newFolderName) . '/';
821 mkdir($targetFolderPath);
822 $sourceFolderPath = $this->getAbsolutePath($folderToCopy);
823 /** @var $iterator RecursiveDirectoryIterator */
824 $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($sourceFolderPath));
825 while ($iterator->valid()) {
826 /** @var $current RecursiveDirectoryIterator */
827 $current = $iterator->current();
828 $itemSubPath = $iterator->getSubPathname();
829 if ($current->isDir() && !($itemSubPath === '..' || $itemSubPath === '.')) {
830 mkdir($targetFolderPath . $itemSubPath);
831 } elseif ($current->isFile()) {
832 $result = copy($sourceFolderPath . $itemSubPath, $targetFolderPath . $itemSubPath);
833 if ($result === FALSE) {
834 throw new \TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException(((((('Copying file "' . $sourceFolderPath) . $itemSubPath) . '" to "') . $targetFolderPath) . $itemSubPath) . '" failed.', 1330119452);
835 }
836 }
837 $iterator->next();
838 }
839 return TRUE;
840 }
841
842 /**
843 * Renames a file in this storage.
844 *
845 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
846 * @param string $newName The target path (including the file name!)
847 * @return string The identifier of the file after renaming
848 */
849 public function renameFile(\TYPO3\CMS\Core\Resource\FileInterface $file, $newName) {
850 // Makes sure the Path given as parameter is valid
851 $newName = $this->sanitizeFileName($newName);
852 $newIdentifier = (rtrim(dirname($file->getIdentifier()), '/') . '/') . $newName;
853 // The target should not exist already
854 if ($this->fileExists($newIdentifier)) {
855 throw new \TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException('The target file already exists.', 1320291063);
856 }
857 $sourcePath = $this->getAbsolutePath($file);
858 $targetPath = ($this->absoluteBasePath . '/') . ltrim($newIdentifier, '/');
859 $result = rename($sourcePath, $targetPath);
860 if ($result === FALSE) {
861 throw new \RuntimeException(((('Renaming file ' . $sourcePath) . ' to ') . $targetPath) . ' failed.', 1320375115);
862 }
863 return $newIdentifier;
864 }
865
866 /**
867 * Makes sure the Path given as parameter is valid
868 *
869 * @param string $filePath The file path (including the file name!)
870 * @return void
871 */
872 protected function checkFilePath($filePath) {
873 // filePath must be valid
874 if (!$this->isPathValid($filePath)) {
875 throw new \TYPO3\CMS\Core\Resource\Exception\InvalidPathException(('File ' . $filePath) . ' is not valid (".." and "//" is not allowed in path).', 1320286857);
876 }
877 }
878
879 /**
880 * Renames a folder in this storage.
881 *
882 * @param \TYPO3\CMS\Core\Resource\Folder $folder
883 * @param string $newName The target path (including the file name!)
884 * @return array A map of old to new file identifiers
885 * @throws RuntimeException if renaming the folder failed
886 */
887 public function renameFolder(\TYPO3\CMS\Core\Resource\Folder $folder, $newName) {
888 // Makes sure the path given as parameter is valid
889 $newName = $this->sanitizeFileName($newName);
890 $relativeSourcePath = $folder->getIdentifier();
891 $sourcePath = $this->getAbsolutePath($relativeSourcePath);
892 $relativeTargetPath = ((rtrim(dirname($relativeSourcePath), '/') . '/') . $newName) . '/';
893 $targetPath = $this->getAbsolutePath($relativeTargetPath);
894 // get all files and folders we are going to move, to have a map for updating later.
895 $filesAndFolders = $this->getFileAndFoldernamesInPath($sourcePath, TRUE);
896 $result = rename($sourcePath, $targetPath);
897 if ($result === FALSE) {
898 throw new \RuntimeException(sprintf('Renaming folder "%1$s" to "%2$s" failed."', $sourcePath, $targetPath), 1320375116);
899 }
900 try {
901 // Create a mapping from old to new identifiers
902 $identifierMap = $this->createIdentifierMap($filesAndFolders, $relativeSourcePath, $relativeTargetPath);
903 } catch (\Exception $e) {
904 rename($targetPath, $sourcePath);
905 throw new \RuntimeException(sprintf('Creating filename mapping after renaming "%1$s" to "%2$s" failed. Reverted rename operation.\\n\\nOriginal error: %3$s"', $sourcePath, $targetPath, $e->getMessage()), 1334160746);
906 }
907 return $identifierMap;
908 }
909
910 /**
911 * Removes a file from this storage.
912 *
913 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
914 * @return boolean TRUE if deleting the file succeeded
915 */
916 public function deleteFile(\TYPO3\CMS\Core\Resource\FileInterface $file) {
917 $filePath = $this->getAbsolutePath($file);
918 $result = unlink($filePath);
919 if ($result === FALSE) {
920 throw new \RuntimeException(('Deletion of file ' . $file->getIdentifier()) . ' failed.', 1320855304);
921 }
922 return $result;
923 }
924
925 /**
926 * Removes a folder from this storage.
927 *
928 * @param \TYPO3\CMS\Core\Resource\Folder $folder
929 * @param bool $deleteRecursively
930 * @return boolean
931 */
932 public function deleteFolder(\TYPO3\CMS\Core\Resource\Folder $folder, $deleteRecursively = FALSE) {
933 $folderPath = $this->getAbsolutePath($folder);
934 $result = \TYPO3\CMS\Core\Utility\GeneralUtility::rmdir($folderPath, $deleteRecursively);
935 if ($result === FALSE) {
936 throw new \TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException(('Deleting folder "' . $folder->getIdentifier()) . '" failed.', 1330119451);
937 }
938 return $result;
939 }
940
941 /**
942 * Checks if a folder contains files and (if supported) other folders.
943 *
944 * @param \TYPO3\CMS\Core\Resource\Folder $folder
945 * @return boolean TRUE if there are no files and folders within $folder
946 */
947 public function isFolderEmpty(\TYPO3\CMS\Core\Resource\Folder $folder) {
948 $path = $this->getAbsolutePath($folder);
949 $dirHandle = opendir($path);
950 while ($entry = readdir($dirHandle)) {
951 if ($entry !== '.' && $entry !== '..') {
952 closedir($dirHandle);
953 return FALSE;
954 }
955 }
956 return TRUE;
957 }
958
959 /**
960 * Returns a (local copy of) a file for processing it. This makes a copy
961 * first when in writable mode, so if you change the file,
962 * you have to update it yourself afterwards.
963 *
964 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
965 * @param boolean $writable Set this to FALSE if you only need the file for read operations. This might speed up things, e.g. by using a cached local version. Never modify the file if you have set this flag!
966 * @return string The path to the file on the local disk
967 */
968 public function getFileForLocalProcessing(\TYPO3\CMS\Core\Resource\FileInterface $file, $writable = TRUE) {
969 if ($writable === FALSE) {
970 // TODO check if this is ok or introduce additional measures against file changes
971 return $this->getAbsolutePath($file);
972 } else {
973 // TODO check if this might also serve as a dump basic implementation in the abstract driver.
974 return $this->copyFileToTemporaryPath($file);
975 }
976 }
977
978 /**
979 * Returns the permissions of a file as an array (keys r, w) of boolean flags
980 *
981 * @param \TYPO3\CMS\Core\Resource\FileInterface $file The file object to check
982 * @return array
983 * @throws RuntimeException If fetching the permissions failed
984 */
985 public function getFilePermissions(\TYPO3\CMS\Core\Resource\FileInterface $file) {
986 $filePath = $this->getAbsolutePath($file);
987 return $this->getPermissions($filePath);
988 }
989
990 /**
991 * Returns the permissions of a folder as an array (keys r, w) of boolean flags
992 *
993 * @param \TYPO3\CMS\Core\Resource\Folder $folder
994 * @return array
995 * @throws RuntimeException If fetching the permissions failed
996 */
997 public function getFolderPermissions(\TYPO3\CMS\Core\Resource\Folder $folder) {
998 $folderPath = $this->getAbsolutePath($folder);
999 return $this->getPermissions($folderPath);
1000 }
1001
1002 /**
1003 * Helper function to unify access to permission information
1004 *
1005 * @param string $path
1006 * @return array
1007 * @throws RuntimeException If fetching the permissions failed
1008 */
1009 protected function getPermissions($path) {
1010 $permissionBits = fileperms($path);
1011 if ($permissionBits === FALSE) {
1012 throw new \RuntimeException('Error while fetching permissions for ' . $path, 1319455097);
1013 }
1014 return array(
1015 'r' => (bool) is_readable($path),
1016 'w' => (bool) is_writable($path)
1017 );
1018 }
1019
1020 /**
1021 * Checks if a given object or identifier is within a container, e.g. if
1022 * a file or folder is within another folder.
1023 * This can e.g. be used to check for webmounts.
1024 *
1025 * @param \TYPO3\CMS\Core\Resource\Folder $container
1026 * @param mixed $content An object or an identifier to check
1027 * @return bool TRUE if $content is within $container, always FALSE if $container is not within this storage
1028 */
1029 public function isWithin(\TYPO3\CMS\Core\Resource\Folder $container, $content) {
1030 if ($container->getStorage() != $this->storage) {
1031 return FALSE;
1032 }
1033 if ($content instanceof \TYPO3\CMS\Core\Resource\FileInterface || $content instanceof \TYPO3\CMS\Core\Resource\Folder) {
1034 $content = $container->getIdentifier();
1035 }
1036 $folderPath = $container->getIdentifier();
1037 $content = '/' . ltrim($content, '/');
1038 return \TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($content, $folderPath);
1039 }
1040
1041 /**
1042 * Creates a new file and returns the matching file object for it.
1043 *
1044 * @param string $fileName
1045 * @param \TYPO3\CMS\Core\Resource\Folder $parentFolder
1046 * @return \TYPO3\CMS\Core\Resource\File
1047 */
1048 public function createFile($fileName, \TYPO3\CMS\Core\Resource\Folder $parentFolder) {
1049 if (!$this->isValidFilename($fileName)) {
1050 throw new \TYPO3\CMS\Core\Resource\Exception\InvalidFileNameException(('Invalid characters in fileName "' . $fileName) . '"', 1320572272);
1051 }
1052 $filePath = $parentFolder->getIdentifier() . ltrim($fileName, '/');
1053 // TODO set permissions of new file
1054 $result = touch($this->absoluteBasePath . $filePath);
1055 clearstatcache();
1056 if ($result !== TRUE) {
1057 throw new \RuntimeException(('Creating file ' . $filePath) . ' failed.', 1320569854);
1058 }
1059 $fileInfo = $this->getFileInfoByIdentifier($filePath);
1060 return $this->getFileObject($fileInfo);
1061 }
1062
1063 /**
1064 * Returns the contents of a file. Beware that this requires to load the
1065 * complete file into memory and also may require fetching the file from an
1066 * external location. So this might be an expensive operation (both in terms of
1067 * processing resources and money) for large files.
1068 *
1069 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
1070 * @return string The file contents
1071 */
1072 public function getFileContents(\TYPO3\CMS\Core\Resource\FileInterface $file) {
1073 $filePath = $this->getAbsolutePath($file);
1074 return file_get_contents($filePath);
1075 }
1076
1077 /**
1078 * Sets the contents of a file to the specified value.
1079 *
1080 * @param \TYPO3\CMS\Core\Resource\FileInterface $file
1081 * @param string $contents
1082 * @return integer The number of bytes written to the file
1083 * @throws RuntimeException if the operation failed
1084 */
1085 public function setFileContents(\TYPO3\CMS\Core\Resource\FileInterface $file, $contents) {
1086 $filePath = $this->getAbsolutePath($file);
1087 $result = file_put_contents($filePath, $contents);
1088 if ($result === FALSE) {
1089 throw new \RuntimeException(('Setting contents of file "' . $file->getIdentifier()) . '" failed.', 1325419305);
1090 }
1091 return $result;
1092 }
1093
1094 /**
1095 * Gets the charset conversion object.
1096 *
1097 * @return \TYPO3\CMS\Core\Charset\CharsetConverter
1098 */
1099 protected function getCharsetConversion() {
1100 if (!isset($this->charsetConversion)) {
1101 if (TYPO3_MODE === 'FE') {
1102 $this->charsetConversion = $GLOBALS['TSFE']->csConvObj;
1103 } elseif (is_object($GLOBALS['LANG'])) {
1104 // BE assumed:
1105 $this->charsetConversion = $GLOBALS['LANG']->csConvObj;
1106 } else {
1107 // The object may not exist yet, so we need to create it now. Happens in the Install Tool for example.
1108 $this->charsetConversion = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Charset\\CharsetConverter');
1109 }
1110 }
1111 return $this->charsetConversion;
1112 }
1113
1114 }
1115
1116
1117 ?>