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