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