330c0788ee845563e55ac23d97a33ed5a2ed5a13
[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 ' . $cleanFileName . ' 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->absoluteBasePath . $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 copy($sourcePath, $this->absoluteBasePath . $newIdentifier);
759 GeneralUtility::fixPermissions($this->absoluteBasePath . $newIdentifier);
760 return $newIdentifier;
761 }
762
763 /**
764 * Moves a file *within* the current storage.
765 * Note that this is only about an inner-storage move action, where a file is just
766 * moved to another folder in the same storage.
767 *
768 * @param string $fileIdentifier
769 * @param string $targetFolderIdentifier
770 * @param string $newFileName
771 *
772 * @return string
773 * @throws \RuntimeException
774 */
775 public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $newFileName) {
776 $sourcePath = $this->getAbsolutePath($fileIdentifier);
777 $targetIdentifier = $targetFolderIdentifier . '/' . $newFileName;
778 $targetIdentifier = $this->canonicalizeAndCheckFileIdentifier($targetIdentifier);
779 $result = rename($sourcePath, $this->getAbsolutePath($targetIdentifier));
780 if ($result === FALSE) {
781 throw new \RuntimeException('Moving file ' . $sourcePath . ' to ' . $targetIdentifier . ' failed.', 1315314712);
782 }
783 return $targetIdentifier;
784 }
785
786 /**
787 * Copies a file to a temporary path and returns that path.
788 *
789 * @param string $fileIdentifier
790 * @return string The temporary path
791 * @throws \RuntimeException
792 */
793 protected function copyFileToTemporaryPath($fileIdentifier) {
794 $sourcePath = $this->getAbsolutePath($fileIdentifier);
795 $temporaryPath = $this->getTemporaryPathForFile($fileIdentifier);
796 $result = copy($sourcePath, $temporaryPath);
797 touch($temporaryPath, filemtime($sourcePath));
798 if ($result === FALSE) {
799 throw new \RuntimeException(
800 'Copying file "' . $fileIdentifier . '" to temporary path "' . $temporaryPath . '" failed.',
801 1320577649
802 );
803 }
804 return $temporaryPath;
805 }
806
807 /**
808 * Creates a map of old and new file/folder identifiers after renaming or
809 * moving a folder. The old identifier is used as the key, the new one as the value.
810 *
811 * @param array $filesAndFolders
812 * @param string $sourceFolderIdentifier
813 * @param string $targetFolderIdentifier
814 *
815 * @return array
816 * @throws Exception\FileOperationErrorException
817 */
818 protected function createIdentifierMap(array $filesAndFolders, $sourceFolderIdentifier, $targetFolderIdentifier) {
819 $identifierMap = array();
820 $identifierMap[$sourceFolderIdentifier] = $targetFolderIdentifier;
821 foreach ($filesAndFolders as $oldItem) {
822 if ($oldItem['type'] == 'dir') {
823 $oldIdentifier = $oldItem['identifier'];
824 $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
825 str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
826 );
827 } else {
828 $oldIdentifier = $oldItem['identifier'];
829 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier(
830 str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
831 );
832 }
833 if (!file_exists($this->getAbsolutePath($newIdentifier))) {
834 throw new Exception\FileOperationErrorException(
835 sprintf('File "%1$s" was not found (should have been copied/moved from "%2$s").', $newIdentifier, $oldIdentifier),
836 1330119453
837 );
838 }
839 $identifierMap[$oldIdentifier] = $newIdentifier;
840 }
841 return $identifierMap;
842 }
843
844 /**
845 * Folder equivalent to moveFileWithinStorage().
846 *
847 * @param string $sourceFolderIdentifier
848 * @param string $targetFolderIdentifier
849 * @param string $newFolderName
850 *
851 * @return array A map of old to new file identifiers
852 * @throws \RuntimeException
853 */
854 public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName) {
855 $sourcePath = $this->getAbsolutePath($sourceFolderIdentifier);
856 $relativeTargetPath = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
857 $targetPath = $this->getAbsolutePath($relativeTargetPath);
858 // get all files and folders we are going to move, to have a map for updating later.
859 $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, TRUE);
860 $result = rename($sourcePath, $targetPath);
861 if ($result === FALSE) {
862 throw new \RuntimeException('Moving folder ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320711817);
863 }
864 // Create a mapping from old to new identifiers
865 $identifierMap = $this->createIdentifierMap($filesAndFolders, $sourceFolderIdentifier, $relativeTargetPath);
866 return $identifierMap;
867 }
868
869 /**
870 * Folder equivalent to copyFileWithinStorage().
871 *
872 * @param string $sourceFolderIdentifier
873 * @param string $targetFolderIdentifier
874 * @param string $newFolderName
875 *
876 * @return bool
877 * @throws Exception\FileOperationErrorException
878 */
879 public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName) {
880 // This target folder path already includes the topmost level, i.e. the folder this method knows as $folderToCopy.
881 // We can thus rely on this folder being present and just create the subfolder we want to copy to.
882 $newFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
883 $sourceFolderPath = $this->getAbsolutePath($sourceFolderIdentifier);
884 $targetFolderPath = $this->getAbsolutePath($newFolderIdentifier);
885
886 mkdir($targetFolderPath);
887 /** @var $iterator \RecursiveDirectoryIterator */
888 $iterator = new \RecursiveIteratorIterator(
889 new \RecursiveDirectoryIterator($sourceFolderPath),
890 \RecursiveIteratorIterator::SELF_FIRST
891 );
892 // Rewind the iterator as this is important for some systems e.g. Windows
893 $iterator->rewind();
894 while ($iterator->valid()) {
895 /** @var $current \RecursiveDirectoryIterator */
896 $current = $iterator->current();
897 $fileName = $current->getFilename();
898 $itemSubPath = GeneralUtility::fixWindowsFilePath($iterator->getSubPathname());
899 if ($current->isDir() && !($fileName === '..' || $fileName === '.')) {
900 GeneralUtility::mkdir($targetFolderPath . '/' . $itemSubPath);
901 } elseif ($current->isFile()) {
902 $result = copy($sourceFolderPath . '/' . $itemSubPath, $targetFolderPath . '/' . $itemSubPath);
903 if ($result === FALSE) {
904 // rollback
905 GeneralUtility::rmdir($targetFolderIdentifier, TRUE);
906 throw new Exception\FileOperationErrorException(
907 'Copying file "' . $sourceFolderPath . $itemSubPath . '" to "' . $targetFolderPath . $itemSubPath . '" failed.',
908 1330119452
909 );
910
911 }
912 }
913 $iterator->next();
914 }
915 GeneralUtility::fixPermissions($targetFolderPath, TRUE);
916 return TRUE;
917 }
918
919 /**
920 * Renames a file in this storage.
921 *
922 * @param string $fileIdentifier
923 * @param string $newName The target path (including the file name!)
924 * @return string The identifier of the file after renaming
925 * @throws Exception\ExistingTargetFileNameException
926 * @throws \RuntimeException
927 */
928 public function renameFile($fileIdentifier, $newName) {
929 // Makes sure the Path given as parameter is valid
930 $newName = $this->sanitizeFileName($newName);
931 $newIdentifier = rtrim(GeneralUtility::fixWindowsFilePath(PathUtility::dirname($fileIdentifier)), '/') . '/' . $newName;
932 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
933 // The target should not exist already
934 if ($this->fileExists($newIdentifier)) {
935 throw new Exception\ExistingTargetFileNameException(
936 'The target file "' . $newIdentifier . '" already exists.',
937 1320291063
938 );
939 }
940 $sourcePath = $this->getAbsolutePath($fileIdentifier);
941 $targetPath = $this->getAbsolutePath($newIdentifier);
942 $result = rename($sourcePath, $targetPath);
943 if ($result === FALSE) {
944 throw new \RuntimeException('Renaming file ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320375115);
945 }
946 return $newIdentifier;
947 }
948
949
950 /**
951 * Renames a folder in this storage.
952 *
953 * @param string $folderIdentifier
954 * @param string $newName
955 * @return array A map of old to new file identifiers of all affected files and folders
956 * @throws \RuntimeException if renaming the folder failed
957 */
958 public function renameFolder($folderIdentifier, $newName) {
959 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
960 $newName = $this->sanitizeFileName($newName);
961
962 $newIdentifier = PathUtility::dirname($folderIdentifier) . '/' . $newName;
963 $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier($newIdentifier);
964
965 $sourcePath = $this->getAbsolutePath($folderIdentifier);
966 $targetPath = $this->getAbsolutePath($newIdentifier);
967 // get all files and folders we are going to move, to have a map for updating later.
968 $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, TRUE);
969 $result = rename($sourcePath, $targetPath);
970 if ($result === FALSE) {
971 throw new \RuntimeException(sprintf('Renaming folder "%1$s" to "%2$s" failed."', $sourcePath, $targetPath), 1320375116);
972 }
973 try {
974 // Create a mapping from old to new identifiers
975 $identifierMap = $this->createIdentifierMap($filesAndFolders, $folderIdentifier, $newIdentifier);
976 } catch (\Exception $e) {
977 rename($targetPath, $sourcePath);
978 throw new \RuntimeException(
979 sprintf(
980 'Creating filename mapping after renaming "%1$s" to "%2$s" failed. Reverted rename operation.\\n\\nOriginal error: %3$s"',
981 $sourcePath, $targetPath, $e->getMessage()
982 ),
983 1334160746
984 );
985 }
986 return $identifierMap;
987 }
988
989 /**
990 * Removes a file from the filesystem. This does not check if the file is
991 * still used or if it is a bad idea to delete it for some other reason
992 * this has to be taken care of in the upper layers (e.g. the Storage)!
993 *
994 * @param string $fileIdentifier
995 * @return bool TRUE if deleting the file succeeded
996 * @throws \RuntimeException
997 */
998 public function deleteFile($fileIdentifier) {
999 $filePath = $this->getAbsolutePath($fileIdentifier);
1000 $result = unlink($filePath);
1001 if ($result === FALSE) {
1002 throw new \RuntimeException('Deletion of file ' . $fileIdentifier . ' failed.', 1320855304);
1003 }
1004 return $result;
1005 }
1006
1007 /**
1008 * Removes a folder from this storage.
1009 *
1010 * @param string $folderIdentifier
1011 * @param bool $deleteRecursively
1012 * @return bool
1013 * @throws Exception\FileOperationErrorException
1014 * @throws Exception\InvalidPathException
1015 */
1016 public function deleteFolder($folderIdentifier, $deleteRecursively = FALSE) {
1017 $folderPath = $this->getAbsolutePath($folderIdentifier);
1018 $result = GeneralUtility::rmdir($folderPath, $deleteRecursively);
1019 if ($result === FALSE) {
1020 throw new Exception\FileOperationErrorException(
1021 'Deleting folder "' . $folderIdentifier . '" failed.',
1022 1330119451
1023 );
1024 }
1025 return $result;
1026 }
1027
1028 /**
1029 * Checks if a folder contains files and (if supported) other folders.
1030 *
1031 * @param string $folderIdentifier
1032 * @return bool TRUE if there are no files and folders within $folder
1033 */
1034 public function isFolderEmpty($folderIdentifier) {
1035 $path = $this->getAbsolutePath($folderIdentifier);
1036 $dirHandle = opendir($path);
1037 while ($entry = readdir($dirHandle)) {
1038 if ($entry !== '.' && $entry !== '..') {
1039 closedir($dirHandle);
1040 return FALSE;
1041 }
1042 }
1043 closedir($dirHandle);
1044 return TRUE;
1045 }
1046
1047 /**
1048 * Returns (a local copy of) a file for processing it. This makes a copy
1049 * first when in writable mode, so if you change the file, you have to update it yourself afterwards.
1050 *
1051 * @param string $fileIdentifier
1052 * @param bool $writable Set this to FALSE if you only need the file for read operations.
1053 * This might speed up things, e.g. by using a cached local version.
1054 * Never modify the file if you have set this flag!
1055 * @return string The path to the file on the local disk
1056 */
1057 public function getFileForLocalProcessing($fileIdentifier, $writable = TRUE) {
1058 if ($writable === FALSE) {
1059 return $this->getAbsolutePath($fileIdentifier);
1060 } else {
1061 return $this->copyFileToTemporaryPath($fileIdentifier);
1062 }
1063 }
1064
1065
1066 /**
1067 * Returns the permissions of a file/folder as an array (keys r, w) of boolean flags
1068 *
1069 * @param string $identifier
1070 * @return array
1071 * @throws \RuntimeException
1072 */
1073 public function getPermissions($identifier) {
1074 $path = $this->getAbsolutePath($identifier);
1075 $permissionBits = fileperms($path);
1076 if ($permissionBits === FALSE) {
1077 throw new \RuntimeException('Error while fetching permissions for ' . $path, 1319455097);
1078 }
1079 return array(
1080 'r' => (bool)is_readable($path),
1081 'w' => (bool)is_writable($path)
1082 );
1083 }
1084
1085 /**
1086 * Checks if a given identifier is within a container, e.g. if
1087 * a file or folder is within another folder. It will also return
1088 * TRUE if both canonicalized identifiers are equal.
1089 *
1090 * @param string $folderIdentifier
1091 * @param string $identifier identifier to be checked against $folderIdentifier
1092 * @return bool TRUE if $content is within or matches $folderIdentifier
1093 */
1094 public function isWithin($folderIdentifier, $identifier) {
1095 $folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier);
1096 $entryIdentifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
1097 if ($folderIdentifier === $entryIdentifier) {
1098 return TRUE;
1099 }
1100 // File identifier canonicalization will not modify a single slash so
1101 // we must not append another slash in that case.
1102 if ($folderIdentifier !== '/') {
1103 $folderIdentifier .= '/';
1104 }
1105 return GeneralUtility::isFirstPartOfStr($entryIdentifier, $folderIdentifier);
1106 }
1107
1108 /**
1109 * Creates a new (empty) file and returns the identifier.
1110 *
1111 * @param string $fileName
1112 * @param string $parentFolderIdentifier
1113 * @return string
1114 * @throws Exception\InvalidFileNameException
1115 * @throws \RuntimeException
1116 */
1117 public function createFile($fileName, $parentFolderIdentifier) {
1118 if (!$this->isValidFilename($fileName)) {
1119 throw new Exception\InvalidFileNameException(
1120 'Invalid characters in fileName "' . $fileName . '"',
1121 1320572272
1122 );
1123 }
1124 $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
1125 $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
1126 $parentFolderIdentifier . $this->sanitizeFileName(ltrim($fileName, '/'))
1127 );
1128 $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
1129 $result = touch($absoluteFilePath);
1130 GeneralUtility::fixPermissions($absoluteFilePath);
1131 clearstatcache();
1132 if ($result !== TRUE) {
1133 throw new \RuntimeException('Creating file ' . $fileIdentifier . ' failed.', 1320569854);
1134 }
1135 return $fileIdentifier;
1136 }
1137
1138 /**
1139 * Returns the contents of a file. Beware that this requires to load the
1140 * complete file into memory and also may require fetching the file from an
1141 * external location. So this might be an expensive operation (both in terms of
1142 * processing resources and money) for large files.
1143 *
1144 * @param string $fileIdentifier
1145 * @return string The file contents
1146 */
1147 public function getFileContents($fileIdentifier) {
1148 $filePath = $this->getAbsolutePath($fileIdentifier);
1149 return file_get_contents($filePath);
1150 }
1151
1152 /**
1153 * Sets the contents of a file to the specified value.
1154 *
1155 * @param string $fileIdentifier
1156 * @param string $contents
1157 * @return int The number of bytes written to the file
1158 * @throws \RuntimeException if the operation failed
1159 */
1160 public function setFileContents($fileIdentifier, $contents) {
1161 $filePath = $this->getAbsolutePath($fileIdentifier);
1162 $result = file_put_contents($filePath, $contents);
1163
1164 // Make sure later calls to filesize() etc. return correct values.
1165 clearstatcache(TRUE, $filePath);
1166
1167 if ($result === FALSE) {
1168 throw new \RuntimeException('Setting contents of file "' . $fileIdentifier . '" failed.', 1325419305);
1169 }
1170 return $result;
1171 }
1172
1173 /**
1174 * Gets the charset conversion object.
1175 *
1176 * @return \TYPO3\CMS\Core\Charset\CharsetConverter
1177 */
1178 protected function getCharsetConversion() {
1179 if (!isset($this->charsetConversion)) {
1180 if (TYPO3_MODE === 'FE') {
1181 $this->charsetConversion = $GLOBALS['TSFE']->csConvObj;
1182 } elseif (is_object($GLOBALS['LANG'])) {
1183 // BE assumed:
1184 $this->charsetConversion = $GLOBALS['LANG']->csConvObj;
1185 } else {
1186 // The object may not exist yet, so we need to create it now. Happens in the Install Tool for example.
1187 $this->charsetConversion = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Charset\CharsetConverter::class);
1188 }
1189 }
1190 return $this->charsetConversion;
1191 }
1192
1193 /**
1194 * Returns the role of an item (currently only folders; can later be extended for files as well)
1195 *
1196 * @param string $folderIdentifier
1197 * @return string
1198 */
1199 public function getRole($folderIdentifier) {
1200 $name = PathUtility::basename($folderIdentifier);
1201 $role = $this->mappingFolderNameToRole[$name];
1202 if (empty($role)) {
1203 $role = FolderInterface::ROLE_DEFAULT;
1204 }
1205 return $role;
1206 }
1207
1208 /**
1209 * Directly output the contents of the file to the output
1210 * buffer. Should not take care of header files or flushing
1211 * buffer before. Will be taken care of by the Storage.
1212 *
1213 * @param string $identifier
1214 *
1215 * @return void
1216 */
1217 public function dumpFileContents($identifier) {
1218 readfile($this->getAbsolutePath($this->canonicalizeAndCheckFileIdentifier($identifier)), 0);
1219 }
1220
1221
1222 }