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