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