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