[BUGFIX] FAL: Allow filenames like "0"
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Resource / Driver / LocalDriver.php
1 <?php
2 namespace TYPO3\CMS\Core\Resource\Driver;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Resource\FolderInterface;
18 use TYPO3\CMS\Core\Resource\ResourceStorage;
19 use TYPO3\CMS\Core\Type\File\FileInfo;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21 use TYPO3\CMS\Core\Utility\PathUtility;
22 use TYPO3\CMS\Core\Resource\Exception;
23
24 /**
25 * Driver for the local file system
26 *
27 */
28 class LocalDriver extends AbstractHierarchicalFilesystemDriver {
29
30 /**
31 * @var string
32 */
33 const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF';
34
35 /**
36 * The absolute base path. It always contains a trailing slash.
37 *
38 * @var string
39 */
40 protected $absoluteBasePath;
41
42 /**
43 * A list of all supported hash algorithms, written all lower case.
44 *
45 * @var array
46 */
47 protected $supportedHashAlgorithms = array('sha1', 'md5');
48
49 /**
50 * The base URL that points to this driver's storage. As long is this
51 * is not set, it is assumed that this folder is not publicly available
52 *
53 * @var string
54 */
55 protected $baseUri = NULL;
56
57 /**
58 * @var \TYPO3\CMS\Core\Charset\CharsetConverter
59 */
60 protected $charsetConversion;
61
62 /** @var array */
63 protected $mappingFolderNameToRole = array(
64 '_recycler_' => FolderInterface::ROLE_RECYCLER,
65 '_temp_' => FolderInterface::ROLE_TEMPORARY,
66 'user_upload' => FolderInterface::ROLE_USERUPLOAD,
67 );
68
69 /**
70 * @param array $configuration
71 */
72 public function __construct(array $configuration = array()) {
73 parent::__construct($configuration);
74 // The capabilities default of this driver. See CAPABILITY_* constants for possible values
75 $this->capabilities =
76 ResourceStorage::CAPABILITY_BROWSABLE
77 | ResourceStorage::CAPABILITY_PUBLIC
78 | ResourceStorage::CAPABILITY_WRITABLE;
79 }
80
81 /**
82 * Merges the capabilites merged by the user at the storage
83 * configuration into the actual capabilities of the driver
84 * and returns the result.
85 *
86 * @param int $capabilities
87 *
88 * @return int
89 */
90 public function mergeConfigurationCapabilities($capabilities) {
91 $this->capabilities &= $capabilities;
92 return $this->capabilities;
93 }
94
95
96 /**
97 * Processes the configuration for this driver.
98 *
99 * @return void
100 */
101 public function processConfiguration() {
102 $this->absoluteBasePath = $this->calculateBasePath($this->configuration);
103 $this->determineBaseUrl();
104 if ($this->baseUri === NULL) {
105 // remove public flag
106 $this->capabilities &= ~ResourceStorage::CAPABILITY_PUBLIC;
107 }
108 }
109
110 /**
111 * Initializes this object. This is called by the storage after the driver
112 * has been attached.
113 *
114 * @return void
115 */
116 public function initialize() {
117 }
118
119 /**
120 * Determines the base URL for this driver, from the configuration or
121 * the TypoScript frontend object
122 *
123 * @return void
124 */
125 protected function determineBaseUrl() {
126 // only calculate baseURI if the storage does not enforce jumpUrl Script
127 if ($this->hasCapability(ResourceStorage::CAPABILITY_PUBLIC)) {
128 if (GeneralUtility::isFirstPartOfStr($this->absoluteBasePath, PATH_site)) {
129 // use site-relative URLs
130 $temporaryBaseUri = rtrim(PathUtility::stripPathSitePrefix($this->absoluteBasePath), '/');
131 if ($temporaryBaseUri !== '') {
132 $uriParts = explode('/', $temporaryBaseUri);
133 $uriParts = array_map('rawurlencode', $uriParts);
134 $temporaryBaseUri = implode('/', $uriParts) . '/';
135 }
136 $this->baseUri = $temporaryBaseUri;
137 } elseif (isset($this->configuration['baseUri']) && GeneralUtility::isValidUrl($this->configuration['baseUri'])) {
138 $this->baseUri = rtrim($this->configuration['baseUri'], '/') . '/';
139 }
140 }
141 }
142
143 /**
144 * Calculates the absolute path to this drivers storage location.
145 *
146 * @throws Exception\InvalidConfigurationException
147 * @param array $configuration
148 * @return string
149 */
150 protected function calculateBasePath(array $configuration) {
151 if (!array_key_exists('basePath', $configuration) || empty($configuration['basePath'])) {
152 throw new Exception\InvalidConfigurationException(
153 'Configuration must contain base path.',
154 1346510477
155 );
156 }
157
158 if ($configuration['pathType'] === 'relative') {
159 $relativeBasePath = $configuration['basePath'];
160 $absoluteBasePath = PATH_site . $relativeBasePath;
161 } else {
162 $absoluteBasePath = $configuration['basePath'];
163 }
164 $absoluteBasePath = $this->canonicalizeAndCheckFilePath($absoluteBasePath);
165 $absoluteBasePath = rtrim($absoluteBasePath, '/') . '/';
166 if (!is_dir($absoluteBasePath)) {
167 throw new Exception\InvalidConfigurationException(
168 'Base path "' . $absoluteBasePath . '" does not exist or is no directory.',
169 1299233097
170 );
171 }
172 return $absoluteBasePath;
173 }
174
175 /**
176 * Returns the public URL to a file.
177 * For the local driver, this will always return a path relative to PATH_site.
178 *
179 * @param string $identifier
180 * @return string
181 * @throws \TYPO3\CMS\Core\Resource\Exception
182 */
183 public function getPublicUrl($identifier) {
184 $publicUrl = NULL;
185 if ($this->baseUri !== NULL) {
186 $uriParts = explode('/', ltrim($identifier, '/'));
187 $uriParts = array_map('rawurlencode', $uriParts);
188 $identifier = implode('/', $uriParts);
189 $publicUrl = $this->baseUri . $identifier;
190 }
191 return $publicUrl;
192 }
193
194 /**
195 * Returns the Identifier of the root level folder of the storage.
196 *
197 * @return string
198 */
199 public function getRootLevelFolder() {
200 return '/';
201 }
202
203 /**
204 * Returns identifier of the default folder new files should be put into.
205 *
206 * @return string
207 */
208 public function getDefaultFolder() {
209 $identifier = '/user_upload/';
210 $createFolder = !$this->folderExists($identifier);
211 if ($createFolder === TRUE) {
212 $identifier = $this->createFolder('user_upload');
213 }
214 return $identifier;
215 }
216
217 /**
218 * Creates a folder, within a parent folder.
219 * If no parent folder is given, a rootlevel folder will be created
220 *
221 * @param string $newFolderName
222 * @param string $parentFolderIdentifier
223 * @param bool $recursive
224 * @return string the Identifier of the new folder
225 */
226 public function createFolder($newFolderName, $parentFolderIdentifier = '', $recursive = FALSE) {
227 $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
228 $newFolderName = trim($newFolderName, '/');
229 if ($recursive == FALSE) {
230 $newFolderName = $this->sanitizeFileName($newFolderName);
231 $newIdentifier = $parentFolderIdentifier . $newFolderName . '/';
232 GeneralUtility::mkdir($this->getAbsolutePath($newIdentifier));
233 } else {
234 $parts = GeneralUtility::trimExplode('/', $newFolderName);
235 $parts = array_map(array($this, 'sanitizeFileName'), $parts);
236 $newFolderName = implode('/', $parts);
237 $newIdentifier = $parentFolderIdentifier . $newFolderName . '/';
238 GeneralUtility::mkdir_deep($this->getAbsolutePath($parentFolderIdentifier) . '/', $newFolderName);
239 }
240 return $newIdentifier;
241 }
242
243 /**
244 * Returns information about a file.
245 *
246 * @param string $fileIdentifier In the case of the LocalDriver, this is the (relative) path to the file.
247 * @param array $propertiesToExtract Array of properties which should be extracted, if empty all will be extracted
248 * @return array
249 * @throws \InvalidArgumentException
250 */
251 public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtract = array()) {
252 $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
253 // don't use $this->fileExists() because we need the absolute path to the file anyways, so we can directly
254 // use PHP's filesystem method.
255 if (!file_exists($absoluteFilePath) || !is_file($absoluteFilePath)) {
256 throw new \InvalidArgumentException('File ' . $fileIdentifier . ' does not exist.', 1314516809);
257 }
258
259 $dirPath = PathUtility::dirname($fileIdentifier);
260 $dirPath = $this->canonicalizeAndCheckFolderIdentifier($dirPath);
261 return $this->extractFileInformation($absoluteFilePath, $dirPath, $propertiesToExtract);
262 }
263
264 /**
265 * Returns information about a folder.
266 *
267 * @param string $folderIdentifier In the case of the LocalDriver, this is the (relative) path to the file.
268 * @return array
269 * @throws Exception\FolderDoesNotExistException
270 */
271 public function getFolderInfoByIdentifier($folderIdentifier) {
272 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
273
274 if (!$this->folderExists($folderIdentifier)) {
275 throw new Exception\FolderDoesNotExistException(
276 'Folder "' . $folderIdentifier . '" does not exist.',
277 1314516810
278 );
279 }
280 return array(
281 'identifier' => $folderIdentifier,
282 'name' => PathUtility::basename($folderIdentifier),
283 'storage' => $this->storageUid
284 );
285 }
286
287 /**
288 * Returns a string where any character not matching [.a-zA-Z0-9_-] is
289 * substituted by '_'
290 * Trailing dots are removed
291 *
292 * Previously in \TYPO3\CMS\Core\Utility\File\BasicFileUtility::cleanFileName()
293 *
294 * @param string $fileName Input string, typically the body of a fileName
295 * @param string $charset Charset of the a fileName (defaults to current charset; depending on context)
296 * @return string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed
297 * @throws Exception\InvalidFileNameException
298 */
299 public function sanitizeFileName($fileName, $charset = '') {
300 // Handle UTF-8 characters
301 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
302 // Allow ".", "-", 0-9, a-z, A-Z and everything beyond U+C0 (latin capital letter a with grave)
303 $cleanFileName = preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . ']/u', '_', trim($fileName));
304 } else {
305 // Define character set
306 if (!$charset) {
307 if (TYPO3_MODE === 'FE') {
308 $charset = $GLOBALS['TSFE']->renderCharset;
309 } else {
310 // default for Backend
311 $charset = 'utf-8';
312 }
313 }
314 // If a charset was found, convert fileName
315 if ($charset) {
316 $fileName = $this->getCharsetConversion()->specCharsToASCII($charset, $fileName);
317 }
318 // Replace unwanted characters by underscores
319 $cleanFileName = preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . '\\xC0-\\xFF]/', '_', trim($fileName));
320 }
321 // Strip trailing dots and return
322 $cleanFileName = rtrim($cleanFileName, '.');
323 if ($cleanFileName === '') {
324 throw new Exception\InvalidFileNameException(
325 'File name ' . $fileName . ' is invalid.',
326 1320288991
327 );
328 }
329 return $cleanFileName;
330 }
331
332 /**
333 * Generic wrapper for extracting a list of items from a path.
334 *
335 * @param string $folderIdentifier
336 * @param int $start The position to start the listing; if not set, start from the beginning
337 * @param int $numberOfItems The number of items to list; if set to zero, all items are returned
338 * @param array $filterMethods The filter methods used to filter the directory items
339 * @param bool $includeFiles
340 * @param bool $includeDirs
341 * @param bool $recursive
342 *
343 * @return array
344 * @throws \InvalidArgumentException
345 */
346 protected function getDirectoryItemList($folderIdentifier, $start = 0, $numberOfItems = 0, array $filterMethods, $includeFiles = TRUE, $includeDirs = TRUE, $recursive = FALSE) {
347 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
348 $realPath = $this->getAbsolutePath($folderIdentifier);
349 if (!is_dir($realPath)) {
350 throw new \InvalidArgumentException(
351 'Cannot list items in directory ' . $folderIdentifier . ' - does not exist or is no directory',
352 1314349666
353 );
354 }
355
356 if ($start > 0) {
357 $start--;
358 }
359
360 // Fetch the files and folders and sort them by name; we have to do
361 // this here because the directory iterator does return them in
362 // an arbitrary order
363 $items = $this->retrieveFileAndFoldersInPath($realPath, $recursive, $includeFiles, $includeDirs);
364 uksort(
365 $items,
366 array(\TYPO3\CMS\Core\Utility\ResourceUtility::class, 'recursiveFileListSortingHelper')
367 );
368
369 $iterator = new \ArrayIterator($items);
370 if ($iterator->count() === 0) {
371 return array();
372 }
373 $iterator->seek($start);
374
375 // $c is the counter for how many items we still have to fetch (-1 is unlimited)
376 $c = $numberOfItems > 0 ? $numberOfItems : - 1;
377 $items = array();
378 while ($iterator->valid() && ($numberOfItems === 0 || $c > 0)) {
379 // $iteratorItem is the file or folder name
380 $iteratorItem = $iterator->current();
381 // go on to the next iterator item now as we might skip this one early
382 $iterator->next();
383
384 if (
385 !$this->applyFilterMethodsToDirectoryItem(
386 $filterMethods,
387 $iteratorItem['name'],
388 $iteratorItem['identifier'],
389 $this->getParentFolderIdentifierOfIdentifier($iteratorItem['identifier'])
390 )
391 ) {
392 continue;
393 }
394
395
396 $items[$iteratorItem['identifier']] = $iteratorItem['identifier'];
397 // Decrement item counter to make sure we only return $numberOfItems
398 // we cannot do this earlier in the method (unlike moving the iterator forward) because we only add the
399 // item here
400 --$c;
401 }
402 return $items;
403 }
404
405 /**
406 * Applies a set of filter methods to a file name to find out if it should be used or not. This is e.g. used by
407 * directory listings.
408 *
409 * @param array $filterMethods The filter methods to use
410 * @param string $itemName
411 * @param string $itemIdentifier
412 * @param string $parentIdentifier
413 * @throws \RuntimeException
414 * @return bool
415 */
416 protected function applyFilterMethodsToDirectoryItem(array $filterMethods, $itemName, $itemIdentifier, $parentIdentifier) {
417 foreach ($filterMethods as $filter) {
418 if (is_array($filter)) {
419 $result = call_user_func($filter, $itemName, $itemIdentifier, $parentIdentifier, array(), $this);
420 // We have to use -1 as the „don't include“ return value, as call_user_func() will return FALSE
421 // If calling the method succeeded and thus we can't use that as a return value.
422 if ($result === -1) {
423 return FALSE;
424 } elseif ($result === FALSE) {
425 throw new \RuntimeException('Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1]);
426 }
427 }
428 }
429 return TRUE;
430 }
431
432 /**
433 * Returns a list of files inside the specified path
434 *
435 * @param string $folderIdentifier
436 * @param int $start
437 * @param int $numberOfItems
438 * @param bool $recursive
439 * @param array $filenameFilterCallbacks The method callbacks to use for filtering the items
440 *
441 * @return array of FileIdentifiers
442 */
443 public function getFilesInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = FALSE, array $filenameFilterCallbacks = array()) {
444 return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $filenameFilterCallbacks, TRUE, FALSE, $recursive);
445 }
446
447 /**
448 * Returns a list of folders inside the specified path
449 *
450 * @param string $folderIdentifier
451 * @param int $start
452 * @param int $numberOfItems
453 * @param bool $recursive
454 * @param array $folderNameFilterCallbacks The method callbacks to use for filtering the items
455 *
456 * @return array of Folder Identifier
457 */
458 public function getFoldersInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = FALSE, array $folderNameFilterCallbacks = array()) {
459 return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $folderNameFilterCallbacks, FALSE, TRUE, $recursive);
460 }
461
462 /**
463 * Returns a list with the names of all files and folders in a path, optionally recursive.
464 *
465 * @param string $path The absolute path
466 * @param bool $recursive If TRUE, recursively fetches files and folders
467 * @param bool $includeFiles
468 * @param bool $includeDirs
469 * @return array
470 */
471 protected function retrieveFileAndFoldersInPath($path, $recursive = FALSE, $includeFiles = TRUE, $includeDirs = TRUE) {
472 $pathLength = strlen($this->getAbsoluteBasePath());
473 $iteratorMode = \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::FOLLOW_SYMLINKS;
474 if ($recursive) {
475 $iterator = new \RecursiveIteratorIterator(
476 new \RecursiveDirectoryIterator($path, $iteratorMode),
477 \RecursiveIteratorIterator::SELF_FIRST
478 );
479 } else {
480 $iterator = new \RecursiveDirectoryIterator($path, $iteratorMode);
481 }
482
483 $directoryEntries = array();
484 while ($iterator->valid()) {
485 /** @var $entry \SplFileInfo */
486 $entry = $iterator->current();
487 // skip non-files/non-folders, and empty entries
488 if ((!$entry->isFile() && !$entry->isDir()) || $entry->getFilename() == '' ||
489 ($entry->isFile() && !$includeFiles) || ($entry->isDir() && !$includeDirs)) {
490 $iterator->next();
491 continue;
492 }
493 $entryIdentifier = '/' . substr($entry->getPathname(), $pathLength);
494 $entryName = PathUtility::basename($entryIdentifier);
495 if ($entry->isDir()) {
496 $entryIdentifier .= '/';
497 }
498 $entryArray = array(
499 'identifier' => $entryIdentifier,
500 'name' => $entryName,
501 'type' => $entry->isDir() ? 'dir' : 'file'
502 );
503 $directoryEntries[$entryIdentifier] = $entryArray;
504 $iterator->next();
505 }
506 return $directoryEntries;
507 }
508
509 /**
510 * Extracts information about a file from the filesystem.
511 *
512 * @param string $filePath The absolute path to the file
513 * @param string $containerPath The relative path to the file's container
514 * @param array $propertiesToExtract array of properties which should be returned, if empty all will be extracted
515 * @return array
516 */
517 protected function extractFileInformation($filePath, $containerPath, array $propertiesToExtract = array()) {
518 if (count($propertiesToExtract) === 0) {
519 $propertiesToExtract = array(
520 'size', 'atime', 'atime', 'mtime', 'ctime', 'mimetype', 'name',
521 'identifier', 'identifier_hash', 'storage', 'folder_hash'
522 );
523 }
524 $fileInformation = array();
525 foreach ($propertiesToExtract as $property) {
526 $fileInformation[$property] = $this->getSpecificFileInformation($filePath, $containerPath, $property);
527 }
528 return $fileInformation;
529 }
530
531
532 /**
533 * Extracts a specific FileInformation from the FileSystems.
534 *
535 * @param string $fileIdentifier
536 * @param string $containerPath
537 * @param string $property
538 *
539 * @return bool|int|string
540 * @throws \InvalidArgumentException
541 */
542 public function getSpecificFileInformation($fileIdentifier, $containerPath, $property) {
543 $identifier = $this->canonicalizeAndCheckFileIdentifier($containerPath . PathUtility::basename($fileIdentifier));
544
545 $fileInfo = GeneralUtility::makeInstance(FileInfo::class, $fileIdentifier);
546 switch ($property) {
547 case 'size':
548 return $fileInfo->getSize();
549 case 'atime':
550 return $fileInfo->getATime();
551 case 'mtime':
552 return $fileInfo->getMTime();
553 case 'ctime':
554 return $fileInfo->getCTime();
555 case 'name':
556 return PathUtility::basename($fileIdentifier);
557 case 'mimetype':
558 return $fileInfo->getMimeType();
559 case 'identifier':
560 return $identifier;
561 case 'storage':
562 return $this->storageUid;
563 case 'identifier_hash':
564 return $this->hashIdentifier($identifier);
565 case 'folder_hash':
566 return $this->hashIdentifier($this->getParentFolderIdentifierOfIdentifier($identifier));
567 default:
568 throw new \InvalidArgumentException(sprintf('The information "%s" is not available.', $property));
569 }
570 }
571
572 /**
573 * Returns the absolute path of the folder this driver operates on.
574 *
575 * @return string
576 */
577 protected function getAbsoluteBasePath() {
578 return $this->absoluteBasePath;
579 }
580
581 /**
582 * Returns the absolute path of a file or folder.
583 *
584 * @param string $fileIdentifier
585 * @return string
586 * @throws Exception\InvalidPathException
587 */
588 protected function getAbsolutePath($fileIdentifier) {
589 $relativeFilePath = ltrim($this->canonicalizeAndCheckFileIdentifier($fileIdentifier), '/');
590 $path = $this->absoluteBasePath . $relativeFilePath;
591 return $path;
592 }
593
594 /**
595 * Creates a (cryptographic) hash for a file.
596 *
597 * @param string $fileIdentifier
598 * @param string $hashAlgorithm The hash algorithm to use
599 * @return string
600 * @throws \RuntimeException
601 * @throws \InvalidArgumentException
602 */
603 public function hash($fileIdentifier, $hashAlgorithm) {
604 if (!in_array($hashAlgorithm, $this->supportedHashAlgorithms)) {
605 throw new \InvalidArgumentException('Hash algorithm "' . $hashAlgorithm . '" is not supported.', 1304964032);
606 }
607 switch ($hashAlgorithm) {
608 case 'sha1':
609 $hash = sha1_file($this->getAbsolutePath($fileIdentifier));
610 break;
611 case 'md5':
612 $hash = md5_file($this->getAbsolutePath($fileIdentifier));
613 break;
614 default:
615 throw new \RuntimeException('Hash algorithm ' . $hashAlgorithm . ' is not implemented.', 1329644451);
616 }
617 return $hash;
618 }
619
620 /**
621 * Adds a file from the local server hard disk to a given path in TYPO3s virtual file system.
622 * This assumes that the local file exists, so no further check is done here!
623 * After a successful the original file must not exist anymore.
624 *
625 * @param string $localFilePath (within PATH_site)
626 * @param string $targetFolderIdentifier
627 * @param string $newFileName optional, if not given original name is used
628 * @param bool $removeOriginal if set the original file will be removed after successful operation
629 * @return string the identifier of the new file
630 * @throws \RuntimeException
631 * @throws \InvalidArgumentException
632 */
633 public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = TRUE) {
634 $localFilePath = $this->canonicalizeAndCheckFilePath($localFilePath);
635 // as for the "virtual storage" for backwards-compatibility, this check always fails, as the file probably lies under PATH_site
636 // thus, it is not checked here
637 // @todo is check in storage
638 if (GeneralUtility::isFirstPartOfStr($localFilePath, $this->absoluteBasePath) && $this->storageUid > 0) {
639 throw new \InvalidArgumentException('Cannot add a file that is already part of this storage.', 1314778269);
640 }
641 $newFileName = $this->sanitizeFileName($newFileName !== '' ? $newFileName : PathUtility::basename($localFilePath));
642 $newFileIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier) . $newFileName;
643 $targetPath = $this->getAbsolutePath($newFileIdentifier);
644
645 if ($removeOriginal) {
646 if (is_uploaded_file($localFilePath)) {
647 $result = move_uploaded_file($localFilePath, $targetPath);
648 } else {
649 $result = rename($localFilePath, $targetPath);
650 }
651 } else {
652 $result = copy($localFilePath, $targetPath);
653 }
654 if ($result === FALSE || !file_exists($targetPath)) {
655 throw new \RuntimeException('Adding file ' . $localFilePath . ' at ' . $newFileIdentifier . ' failed.');
656 }
657 clearstatcache();
658 // Change the permissions of the file
659 GeneralUtility::fixPermissions($targetPath);
660 return $newFileIdentifier;
661 }
662
663 /**
664 * Checks if a file exists.
665 *
666 * @param string $fileIdentifier
667 *
668 * @return bool
669 */
670 public function fileExists($fileIdentifier) {
671 $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
672 return is_file($absoluteFilePath);
673 }
674
675 /**
676 * Checks if a file inside a folder exists
677 *
678 * @param string $fileName
679 * @param string $folderIdentifier
680 * @return bool
681 */
682 public function fileExistsInFolder($fileName, $folderIdentifier) {
683 $identifier = $folderIdentifier . '/' . $fileName;
684 $identifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
685 return $this->fileExists($identifier);
686 }
687
688 /**
689 * Checks if a folder exists.
690 *
691 * @param string $folderIdentifier
692 *
693 * @return bool
694 */
695 public function folderExists($folderIdentifier) {
696 $absoluteFilePath = $this->getAbsolutePath($folderIdentifier);
697 return is_dir($absoluteFilePath);
698 }
699
700 /**
701 * Checks if a folder inside a folder exists.
702 *
703 * @param string $folderName
704 * @param string $folderIdentifier
705 * @return bool
706 */
707 public function folderExistsInFolder($folderName, $folderIdentifier) {
708 $identifier = $folderIdentifier . '/' . $folderName;
709 $identifier = $this->canonicalizeAndCheckFolderIdentifier($identifier);
710 return $this->folderExists($identifier);
711 }
712
713 /**
714 * Returns the Identifier for a folder within a given folder.
715 *
716 * @param string $folderName The name of the target folder
717 * @param string $folderIdentifier
718 *
719 * @return string
720 */
721 public function getFolderInFolder($folderName, $folderIdentifier) {
722 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier . '/' . $folderName);
723 return $folderIdentifier;
724 }
725
726 /**
727 * Replaces the contents (and file-specific metadata) of a file object with a local file.
728 *
729 * @param string $fileIdentifier
730 * @param string $localFilePath
731 * @return bool TRUE if the operation succeeded
732 * @throws \RuntimeException
733 */
734 public function replaceFile($fileIdentifier, $localFilePath) {
735 $filePath = $this->getAbsolutePath($fileIdentifier);
736 $result = rename($localFilePath, $filePath);
737 if ($result === FALSE) {
738 throw new \RuntimeException('Replacing file ' . $fileIdentifier . ' with ' . $localFilePath . ' failed.', 1315314711);
739 }
740 return $result;
741 }
742
743 /**
744 * Copies a file *within* the current storage.
745 * Note that this is only about an intra-storage copy action, where a file is just
746 * copied to another folder in the same storage.
747 *
748 * @param string $fileIdentifier
749 * @param string $targetFolderIdentifier
750 * @param string $fileName
751 * @return string the Identifier of the new file
752 */
753 public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $fileName) {
754 $sourcePath = $this->getAbsolutePath($fileIdentifier);
755 $newIdentifier = $targetFolderIdentifier . '/' . $fileName;
756 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
757
758 $absoluteFilePath = $this->getAbsolutePath($newIdentifier);
759 copy($sourcePath, $absoluteFilePath);
760 GeneralUtility::fixPermissions($absoluteFilePath);
761 return $newIdentifier;
762 }
763
764 /**
765 * Moves a file *within* the current storage.
766 * Note that this is only about an inner-storage move action, where a file is just
767 * moved to another folder in the same storage.
768 *
769 * @param string $fileIdentifier
770 * @param string $targetFolderIdentifier
771 * @param string $newFileName
772 *
773 * @return string
774 * @throws \RuntimeException
775 */
776 public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $newFileName) {
777 $sourcePath = $this->getAbsolutePath($fileIdentifier);
778 $targetIdentifier = $targetFolderIdentifier . '/' . $newFileName;
779 $targetIdentifier = $this->canonicalizeAndCheckFileIdentifier($targetIdentifier);
780 $result = rename($sourcePath, $this->getAbsolutePath($targetIdentifier));
781 if ($result === FALSE) {
782 throw new \RuntimeException('Moving file ' . $sourcePath . ' to ' . $targetIdentifier . ' failed.', 1315314712);
783 }
784 return $targetIdentifier;
785 }
786
787 /**
788 * Copies a file to a temporary path and returns that path.
789 *
790 * @param string $fileIdentifier
791 * @return string The temporary path
792 * @throws \RuntimeException
793 */
794 protected function copyFileToTemporaryPath($fileIdentifier) {
795 $sourcePath = $this->getAbsolutePath($fileIdentifier);
796 $temporaryPath = $this->getTemporaryPathForFile($fileIdentifier);
797 $result = copy($sourcePath, $temporaryPath);
798 touch($temporaryPath, filemtime($sourcePath));
799 if ($result === FALSE) {
800 throw new \RuntimeException(
801 'Copying file "' . $fileIdentifier . '" to temporary path "' . $temporaryPath . '" failed.',
802 1320577649
803 );
804 }
805 return $temporaryPath;
806 }
807
808 /**
809 * Creates a map of old and new file/folder identifiers after renaming or
810 * moving a folder. The old identifier is used as the key, the new one as the value.
811 *
812 * @param array $filesAndFolders
813 * @param string $sourceFolderIdentifier
814 * @param string $targetFolderIdentifier
815 *
816 * @return array
817 * @throws Exception\FileOperationErrorException
818 */
819 protected function createIdentifierMap(array $filesAndFolders, $sourceFolderIdentifier, $targetFolderIdentifier) {
820 $identifierMap = array();
821 $identifierMap[$sourceFolderIdentifier] = $targetFolderIdentifier;
822 foreach ($filesAndFolders as $oldItem) {
823 if ($oldItem['type'] == 'dir') {
824 $oldIdentifier = $oldItem['identifier'];
825 $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
826 str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
827 );
828 } else {
829 $oldIdentifier = $oldItem['identifier'];
830 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier(
831 str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
832 );
833 }
834 if (!file_exists($this->getAbsolutePath($newIdentifier))) {
835 throw new Exception\FileOperationErrorException(
836 sprintf('File "%1$s" was not found (should have been copied/moved from "%2$s").', $newIdentifier, $oldIdentifier),
837 1330119453
838 );
839 }
840 $identifierMap[$oldIdentifier] = $newIdentifier;
841 }
842 return $identifierMap;
843 }
844
845 /**
846 * Folder equivalent to moveFileWithinStorage().
847 *
848 * @param string $sourceFolderIdentifier
849 * @param string $targetFolderIdentifier
850 * @param string $newFolderName
851 *
852 * @return array A map of old to new file identifiers
853 * @throws \RuntimeException
854 */
855 public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName) {
856 $sourcePath = $this->getAbsolutePath($sourceFolderIdentifier);
857 $relativeTargetPath = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
858 $targetPath = $this->getAbsolutePath($relativeTargetPath);
859 // get all files and folders we are going to move, to have a map for updating later.
860 $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, TRUE);
861 $result = rename($sourcePath, $targetPath);
862 if ($result === FALSE) {
863 throw new \RuntimeException('Moving folder ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320711817);
864 }
865 // Create a mapping from old to new identifiers
866 $identifierMap = $this->createIdentifierMap($filesAndFolders, $sourceFolderIdentifier, $relativeTargetPath);
867 return $identifierMap;
868 }
869
870 /**
871 * Folder equivalent to copyFileWithinStorage().
872 *
873 * @param string $sourceFolderIdentifier
874 * @param string $targetFolderIdentifier
875 * @param string $newFolderName
876 *
877 * @return bool
878 * @throws Exception\FileOperationErrorException
879 */
880 public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName) {
881 // This target folder path already includes the topmost level, i.e. the folder this method knows as $folderToCopy.
882 // We can thus rely on this folder being present and just create the subfolder we want to copy to.
883 $newFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
884 $sourceFolderPath = $this->getAbsolutePath($sourceFolderIdentifier);
885 $targetFolderPath = $this->getAbsolutePath($newFolderIdentifier);
886
887 mkdir($targetFolderPath);
888 /** @var $iterator \RecursiveDirectoryIterator */
889 $iterator = new \RecursiveIteratorIterator(
890 new \RecursiveDirectoryIterator($sourceFolderPath),
891 \RecursiveIteratorIterator::SELF_FIRST
892 );
893 // Rewind the iterator as this is important for some systems e.g. Windows
894 $iterator->rewind();
895 while ($iterator->valid()) {
896 /** @var $current \RecursiveDirectoryIterator */
897 $current = $iterator->current();
898 $fileName = $current->getFilename();
899 $itemSubPath = GeneralUtility::fixWindowsFilePath($iterator->getSubPathname());
900 if ($current->isDir() && !($fileName === '..' || $fileName === '.')) {
901 GeneralUtility::mkdir($targetFolderPath . '/' . $itemSubPath);
902 } elseif ($current->isFile()) {
903 $result = copy($sourceFolderPath . '/' . $itemSubPath, $targetFolderPath . '/' . $itemSubPath);
904 if ($result === FALSE) {
905 // rollback
906 GeneralUtility::rmdir($targetFolderIdentifier, TRUE);
907 throw new Exception\FileOperationErrorException(
908 'Copying file "' . $sourceFolderPath . $itemSubPath . '" to "' . $targetFolderPath . $itemSubPath . '" failed.',
909 1330119452
910 );
911
912 }
913 }
914 $iterator->next();
915 }
916 GeneralUtility::fixPermissions($targetFolderPath, TRUE);
917 return TRUE;
918 }
919
920 /**
921 * Renames a file in this storage.
922 *
923 * @param string $fileIdentifier
924 * @param string $newName The target path (including the file name!)
925 * @return string The identifier of the file after renaming
926 * @throws Exception\ExistingTargetFileNameException
927 * @throws \RuntimeException
928 */
929 public function renameFile($fileIdentifier, $newName) {
930 // Makes sure the Path given as parameter is valid
931 $newName = $this->sanitizeFileName($newName);
932 $newIdentifier = rtrim(GeneralUtility::fixWindowsFilePath(PathUtility::dirname($fileIdentifier)), '/') . '/' . $newName;
933 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
934 // The target should not exist already
935 if ($this->fileExists($newIdentifier)) {
936 throw new Exception\ExistingTargetFileNameException(
937 'The target file "' . $newIdentifier . '" already exists.',
938 1320291063
939 );
940 }
941 $sourcePath = $this->getAbsolutePath($fileIdentifier);
942 $targetPath = $this->getAbsolutePath($newIdentifier);
943 $result = rename($sourcePath, $targetPath);
944 if ($result === FALSE) {
945 throw new \RuntimeException('Renaming file ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320375115);
946 }
947 return $newIdentifier;
948 }
949
950
951 /**
952 * Renames a folder in this storage.
953 *
954 * @param string $folderIdentifier
955 * @param string $newName
956 * @return array A map of old to new file identifiers of all affected files and folders
957 * @throws \RuntimeException if renaming the folder failed
958 */
959 public function renameFolder($folderIdentifier, $newName) {
960 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
961 $newName = $this->sanitizeFileName($newName);
962
963 $newIdentifier = PathUtility::dirname($folderIdentifier) . '/' . $newName;
964 $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier($newIdentifier);
965
966 $sourcePath = $this->getAbsolutePath($folderIdentifier);
967 $targetPath = $this->getAbsolutePath($newIdentifier);
968 // get all files and folders we are going to move, to have a map for updating later.
969 $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, TRUE);
970 $result = rename($sourcePath, $targetPath);
971 if ($result === FALSE) {
972 throw new \RuntimeException(sprintf('Renaming folder "%1$s" to "%2$s" failed."', $sourcePath, $targetPath), 1320375116);
973 }
974 try {
975 // Create a mapping from old to new identifiers
976 $identifierMap = $this->createIdentifierMap($filesAndFolders, $folderIdentifier, $newIdentifier);
977 } catch (\Exception $e) {
978 rename($targetPath, $sourcePath);
979 throw new \RuntimeException(
980 sprintf(
981 'Creating filename mapping after renaming "%1$s" to "%2$s" failed. Reverted rename operation.\\n\\nOriginal error: %3$s"',
982 $sourcePath, $targetPath, $e->getMessage()
983 ),
984 1334160746
985 );
986 }
987 return $identifierMap;
988 }
989
990 /**
991 * Removes a file from the filesystem. This does not check if the file is
992 * still used or if it is a bad idea to delete it for some other reason
993 * this has to be taken care of in the upper layers (e.g. the Storage)!
994 *
995 * @param string $fileIdentifier
996 * @return bool TRUE if deleting the file succeeded
997 * @throws \RuntimeException
998 */
999 public function deleteFile($fileIdentifier) {
1000 $filePath = $this->getAbsolutePath($fileIdentifier);
1001 $result = unlink($filePath);
1002 if ($result === FALSE) {
1003 throw new \RuntimeException('Deletion of file ' . $fileIdentifier . ' failed.', 1320855304);
1004 }
1005 return $result;
1006 }
1007
1008 /**
1009 * Removes a folder from this storage.
1010 *
1011 * @param string $folderIdentifier
1012 * @param bool $deleteRecursively
1013 * @return bool
1014 * @throws Exception\FileOperationErrorException
1015 * @throws Exception\InvalidPathException
1016 */
1017 public function deleteFolder($folderIdentifier, $deleteRecursively = FALSE) {
1018 $folderPath = $this->getAbsolutePath($folderIdentifier);
1019 $result = GeneralUtility::rmdir($folderPath, $deleteRecursively);
1020 if ($result === FALSE) {
1021 throw new Exception\FileOperationErrorException(
1022 'Deleting folder "' . $folderIdentifier . '" failed.',
1023 1330119451
1024 );
1025 }
1026 return $result;
1027 }
1028
1029 /**
1030 * Checks if a folder contains files and (if supported) other folders.
1031 *
1032 * @param string $folderIdentifier
1033 * @return bool TRUE if there are no files and folders within $folder
1034 */
1035 public function isFolderEmpty($folderIdentifier) {
1036 $path = $this->getAbsolutePath($folderIdentifier);
1037 $dirHandle = opendir($path);
1038 while ($entry = readdir($dirHandle)) {
1039 if ($entry !== '.' && $entry !== '..') {
1040 closedir($dirHandle);
1041 return FALSE;
1042 }
1043 }
1044 closedir($dirHandle);
1045 return TRUE;
1046 }
1047
1048 /**
1049 * Returns (a local copy of) a file for processing it. This makes a copy
1050 * first when in writable mode, so if you change the file, you have to update it yourself afterwards.
1051 *
1052 * @param string $fileIdentifier
1053 * @param bool $writable Set this to FALSE if you only need the file for read operations.
1054 * This might speed up things, e.g. by using a cached local version.
1055 * Never modify the file if you have set this flag!
1056 * @return string The path to the file on the local disk
1057 */
1058 public function getFileForLocalProcessing($fileIdentifier, $writable = TRUE) {
1059 if ($writable === FALSE) {
1060 return $this->getAbsolutePath($fileIdentifier);
1061 } else {
1062 return $this->copyFileToTemporaryPath($fileIdentifier);
1063 }
1064 }
1065
1066
1067 /**
1068 * Returns the permissions of a file/folder as an array (keys r, w) of boolean flags
1069 *
1070 * @param string $identifier
1071 * @return array
1072 * @throws \RuntimeException
1073 */
1074 public function getPermissions($identifier) {
1075 $path = $this->getAbsolutePath($identifier);
1076 $permissionBits = fileperms($path);
1077 if ($permissionBits === FALSE) {
1078 throw new \RuntimeException('Error while fetching permissions for ' . $path, 1319455097);
1079 }
1080 return array(
1081 'r' => (bool)is_readable($path),
1082 'w' => (bool)is_writable($path)
1083 );
1084 }
1085
1086 /**
1087 * Checks if a given identifier is within a container, e.g. if
1088 * a file or folder is within another folder. It will also return
1089 * TRUE if both canonicalized identifiers are equal.
1090 *
1091 * @param string $folderIdentifier
1092 * @param string $identifier identifier to be checked against $folderIdentifier
1093 * @return bool TRUE if $content is within or matches $folderIdentifier
1094 */
1095 public function isWithin($folderIdentifier, $identifier) {
1096 $folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier);
1097 $entryIdentifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
1098 if ($folderIdentifier === $entryIdentifier) {
1099 return TRUE;
1100 }
1101 // File identifier canonicalization will not modify a single slash so
1102 // we must not append another slash in that case.
1103 if ($folderIdentifier !== '/') {
1104 $folderIdentifier .= '/';
1105 }
1106 return GeneralUtility::isFirstPartOfStr($entryIdentifier, $folderIdentifier);
1107 }
1108
1109 /**
1110 * Creates a new (empty) file and returns the identifier.
1111 *
1112 * @param string $fileName
1113 * @param string $parentFolderIdentifier
1114 * @return string
1115 * @throws Exception\InvalidFileNameException
1116 * @throws \RuntimeException
1117 */
1118 public function createFile($fileName, $parentFolderIdentifier) {
1119 if (!$this->isValidFilename($fileName)) {
1120 throw new Exception\InvalidFileNameException(
1121 'Invalid characters in fileName "' . $fileName . '"',
1122 1320572272
1123 );
1124 }
1125 $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
1126 $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
1127 $parentFolderIdentifier . $this->sanitizeFileName(ltrim($fileName, '/'))
1128 );
1129 $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
1130 $result = touch($absoluteFilePath);
1131 GeneralUtility::fixPermissions($absoluteFilePath);
1132 clearstatcache();
1133 if ($result !== TRUE) {
1134 throw new \RuntimeException('Creating file ' . $fileIdentifier . ' failed.', 1320569854);
1135 }
1136 return $fileIdentifier;
1137 }
1138
1139 /**
1140 * Returns the contents of a file. Beware that this requires to load the
1141 * complete file into memory and also may require fetching the file from an
1142 * external location. So this might be an expensive operation (both in terms of
1143 * processing resources and money) for large files.
1144 *
1145 * @param string $fileIdentifier
1146 * @return string The file contents
1147 */
1148 public function getFileContents($fileIdentifier) {
1149 $filePath = $this->getAbsolutePath($fileIdentifier);
1150 return file_get_contents($filePath);
1151 }
1152
1153 /**
1154 * Sets the contents of a file to the specified value.
1155 *
1156 * @param string $fileIdentifier
1157 * @param string $contents
1158 * @return int The number of bytes written to the file
1159 * @throws \RuntimeException if the operation failed
1160 */
1161 public function setFileContents($fileIdentifier, $contents) {
1162 $filePath = $this->getAbsolutePath($fileIdentifier);
1163 $result = file_put_contents($filePath, $contents);
1164
1165 // Make sure later calls to filesize() etc. return correct values.
1166 clearstatcache(TRUE, $filePath);
1167
1168 if ($result === FALSE) {
1169 throw new \RuntimeException('Setting contents of file "' . $fileIdentifier . '" failed.', 1325419305);
1170 }
1171 return $result;
1172 }
1173
1174 /**
1175 * Gets the charset conversion object.
1176 *
1177 * @return \TYPO3\CMS\Core\Charset\CharsetConverter
1178 */
1179 protected function getCharsetConversion() {
1180 if (!isset($this->charsetConversion)) {
1181 if (TYPO3_MODE === 'FE') {
1182 $this->charsetConversion = $GLOBALS['TSFE']->csConvObj;
1183 } elseif (is_object($GLOBALS['LANG'])) {
1184 // BE assumed:
1185 $this->charsetConversion = $GLOBALS['LANG']->csConvObj;
1186 } else {
1187 // The object may not exist yet, so we need to create it now. Happens in the Install Tool for example.
1188 $this->charsetConversion = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Charset\CharsetConverter::class);
1189 }
1190 }
1191 return $this->charsetConversion;
1192 }
1193
1194 /**
1195 * Returns the role of an item (currently only folders; can later be extended for files as well)
1196 *
1197 * @param string $folderIdentifier
1198 * @return string
1199 */
1200 public function getRole($folderIdentifier) {
1201 $name = PathUtility::basename($folderIdentifier);
1202 $role = $this->mappingFolderNameToRole[$name];
1203 if (empty($role)) {
1204 $role = FolderInterface::ROLE_DEFAULT;
1205 }
1206 return $role;
1207 }
1208
1209 /**
1210 * Directly output the contents of the file to the output
1211 * buffer. Should not take care of header files or flushing
1212 * buffer before. Will be taken care of by the Storage.
1213 *
1214 * @param string $identifier
1215 *
1216 * @return void
1217 */
1218 public function dumpFileContents($identifier) {
1219 readfile($this->getAbsolutePath($this->canonicalizeAndCheckFileIdentifier($identifier)), 0);
1220 }
1221
1222
1223 }