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