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