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