‪TYPO3CMS  ‪main
LocalDriver.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\Http\Message\ResponseInterface;
37 
41 class LocalDriver extends AbstractHierarchicalFilesystemDriver implements StreamableDriverInterface
42 {
46  public const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF';
47 
51  protected string $absoluteBasePath = '/';
52 
56  protected array $supportedHashAlgorithms = ['sha1', 'md5'];
57 
62  protected ?string $baseUri = null;
63 
67  protected array $mappingFolderNameToRole = [
68  '_recycler_' => ‪FolderInterface::ROLE_RECYCLER,
70  'user_upload' => ‪FolderInterface::ROLE_USERUPLOAD,
71  ];
72 
73  public function __construct(array $configuration = [])
74  {
75  parent::__construct($configuration);
76  // The capabilities default of this driver. See Capabilities::CAPABILITY_* constants for possible values
77  $this->capabilities = new Capabilities(
82  );
83  }
84 
89  public function mergeConfigurationCapabilities(Capabilities $capabilities): Capabilities
90  {
91  $this->capabilities->and($capabilities);
92  return $this->capabilities;
93  }
94 
95  public function processConfiguration(): void
96  {
97  try {
98  $this->absoluteBasePath = $this->calculateBasePath($this->configuration);
99  } catch (InvalidConfigurationException $e) {
100  // The storage is offline, but the absolute base path requires a "/" at the end.
101  $this->absoluteBasePath = '/';
102  throw $e;
103  }
104  $this->determineBaseUrl();
105  if ($this->baseUri === null) {
106  // remove public flag
107  $this->capabilities->removeCapability(‪Capabilities::CAPABILITY_PUBLIC);
108  }
109  }
110 
115  public function initialize(): void {}
116 
121  protected function determineBaseUrl(): void
122  {
123  // only calculate baseURI if the storage does not enforce jumpUrl Script
124  if ($this->hasCapability(‪Capabilities::CAPABILITY_PUBLIC)) {
125  if (!empty($this->configuration['baseUri'])) {
126  $this->baseUri = rtrim($this->configuration['baseUri'], '/') . '/';
127  } elseif (str_starts_with($this->absoluteBasePath, ‪Environment::getPublicPath())) {
128  // use site-relative URLs
129  $temporaryBaseUri = rtrim(‪PathUtility::stripPathSitePrefix($this->absoluteBasePath), '/');
130  if ($temporaryBaseUri !== '') {
131  $uriParts = explode('/', $temporaryBaseUri);
132  $uriParts = array_map(rawurlencode(...), $uriParts);
133  $temporaryBaseUri = implode('/', $uriParts) . '/';
134  }
135  $this->baseUri = $temporaryBaseUri;
136  }
137  }
138  }
139 
143  protected function calculateBasePath(array $configuration): string
144  {
145  if (!array_key_exists('basePath', $configuration) || empty($configuration['basePath'])) {
146  throw new InvalidConfigurationException(
147  'Configuration must contain base path.',
148  1346510477
149  );
150  }
151 
152  if (!empty($configuration['pathType']) && $configuration['pathType'] === 'relative') {
153  $relativeBasePath = $configuration['basePath'];
154  $absoluteBasePath = ‪Environment::getPublicPath() . '/' . $relativeBasePath;
155  } else {
156  $absoluteBasePath = $configuration['basePath'];
157  }
158  $absoluteBasePath = $this->canonicalizeAndCheckFilePath($absoluteBasePath);
159  $absoluteBasePath = rtrim($absoluteBasePath, '/') . '/';
160  if (!$this->isAllowedAbsolutePath($absoluteBasePath)) {
161  throw new InvalidConfigurationException(
162  'Base path "' . $absoluteBasePath . '" is not within the allowed project root path or allowed lockRootPath.',
163  1704807715
164  );
165  }
166  if (!is_dir($absoluteBasePath)) {
167  throw new InvalidConfigurationException(
168  'Base path "' . $absoluteBasePath . '" does not exist or is no directory.',
169  1299233097
170  );
171  }
172  return $absoluteBasePath;
173  }
174 
183  public function getPublicUrl(string ‪$identifier): ?string
184  {
185  ‪$publicUrl = null;
186  if ($this->baseUri !== null) {
187  $uriParts = explode('/', ltrim(‪$identifier, '/'));
188  $uriParts = array_map(rawurlencode(...), $uriParts);
189  ‪$identifier = implode('/', $uriParts);
190  ‪$publicUrl = $this->baseUri . ‪$identifier;
191  }
192  return ‪$publicUrl;
193  }
194 
200  public function getRootLevelFolder(): string
201  {
202  return '/';
203  }
204 
210  public function getDefaultFolder(): string
211  {
212  ‪$identifier = '/user_upload/';
213  $createFolder = !$this->folderExists(‪$identifier);
214  if ($createFolder === true) {
215  ‪$identifier = $this->createFolder('user_upload');
216  }
217  return ‪$identifier;
218  }
219 
227  public function createFolder(string $newFolderName, string $parentFolderIdentifier = '', bool $recursive = false): string
228  {
229  $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
230  $newFolderName = trim($newFolderName, '/');
231  if ($recursive === false) {
232  $newFolderName = $this->sanitizeFileName($newFolderName);
233  $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier . $newFolderName . '/');
234  ‪GeneralUtility::mkdir($this->getAbsolutePath($newIdentifier));
235  } else {
236  $parts = ‪GeneralUtility::trimExplode('/', $newFolderName);
237  $parts = array_map($this->sanitizeFileName(...), $parts);
238  $newFolderName = implode('/', $parts);
239  $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
240  $parentFolderIdentifier . $newFolderName . '/'
241  );
242  ‪GeneralUtility::mkdir_deep($this->getAbsolutePath($newIdentifier));
243  }
244  return $newIdentifier;
245  }
246 
254  public function getFileInfoByIdentifier(string $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 anyway, 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 
280  public function getFolderInfoByIdentifier(string $folderIdentifier): array
281  {
282  $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
283 
284  if (!$this->folderExists($folderIdentifier)) {
285  throw new FolderDoesNotExistException(
286  'Folder "' . $folderIdentifier . '" does not exist.',
287  1314516810
288  );
289  }
290  $absolutePath = $this->getAbsolutePath($folderIdentifier);
291  return [
292  'identifier' => $folderIdentifier,
293  'name' => ‪PathUtility::basename($folderIdentifier),
294  'mtime' => filemtime($absolutePath),
295  'ctime' => filectime($absolutePath),
296  'storage' => $this->storageUid,
297  ];
298  }
299 
312  public function sanitizeFileName(string $fileName, string $charset = 'utf-8'): string
313  {
314  if ($charset === 'utf-8') {
315  $fileName = \Normalizer::normalize($fileName) ?: $fileName;
316  }
317 
318  // Handle UTF-8 characters
319  if (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
320  // Allow ".", "-", 0-9, a-z, A-Z and everything beyond U+C0 (latin capital letter a with grave)
321  $cleanFileName = (string)preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . ']/u', '_', trim($fileName));
322  } else {
323  $fileName = GeneralUtility::makeInstance(CharsetConverter::class)->specCharsToASCII($charset, $fileName);
324  // Replace unwanted characters with underscores
325  $cleanFileName = (string)preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . '\\xC0-\\xFF]/', '_', trim($fileName));
326  }
327  // Strip trailing dots and return
328  $cleanFileName = rtrim($cleanFileName, '.');
329  if ($cleanFileName === '') {
330  throw new InvalidFileNameException(
331  'File name ' . $fileName . ' is invalid.',
332  1320288991
333  );
334  }
335  return $cleanFileName;
336  }
337 
353  protected function getDirectoryItemList(string $folderIdentifier, int $start, int $numberOfItems, array $filterMethods, bool $includeFiles = true, bool $includeDirs = true, bool $recursive = false, string $sort = '', bool $sortRev = false): array
354  {
355  $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
356  $realPath = $this->getAbsolutePath($folderIdentifier);
357  if (!is_dir($realPath)) {
358  throw new \InvalidArgumentException(
359  'Cannot list items in directory ' . $folderIdentifier . ' - does not exist or is no directory',
360  1314349666
361  );
362  }
363 
364  $items = $this->retrieveFileAndFoldersInPath($realPath, $recursive, $includeFiles, $includeDirs, $sort, $sortRev);
365  $iterator = new \ArrayIterator($items);
366  if ($iterator->count() === 0) {
367  return [];
368  }
369 
370  // $c is the counter for how many items we still have to fetch (-1 is unlimited)
371  $c = $numberOfItems > 0 ? $numberOfItems : -1;
372  $items = [];
373  while ($iterator->valid() && ($numberOfItems === 0 || $c > 0)) {
374  // $iteratorItem is the file or folder name
375  $iteratorItem = $iterator->current();
376  // go on to the next iterator item now as we might skip this one early
377  $iterator->next();
378 
379  try {
380  if (
381  !$this->applyFilterMethodsToDirectoryItem(
382  $filterMethods,
383  $iteratorItem['name'],
384  $iteratorItem['identifier'],
385  $this->getParentFolderIdentifierOfIdentifier($iteratorItem['identifier'])
386  )
387  ) {
388  continue;
389  }
390  if ($start > 0) {
391  $start--;
392  } else {
393  // The identifier can also be an int-like string, resulting in int array keys.
394  $items[$iteratorItem['identifier']] = $iteratorItem['identifier'];
395  // Decrement item counter to make sure we only return $numberOfItems
396  // we cannot do this earlier in the method (unlike moving the iterator forward) because we only add the
397  // item here
398  --$c;
399  }
400  } catch (InvalidPathException) {
401  }
402  }
403  return $items;
404  }
405 
412  protected function applyFilterMethodsToDirectoryItem(array $filterMethods, string $itemName, string $itemIdentifier, string $parentIdentifier): bool
413  {
414  foreach ($filterMethods as $filter) {
415  if (is_callable($filter)) {
416  $result = $filter($itemName, $itemIdentifier, $parentIdentifier, [], $this);
417  // We use -1 as the "don't include“ return value, for historic reasons,
418  // as call_user_func() used to return FALSE if calling the method failed.
419  if ($result === -1) {
420  return false;
421  }
422  if ($result === false) {
423  throw new \RuntimeException(
424  'Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1],
425  1476046425
426  );
427  }
428  }
429  }
430  return true;
431  }
432 
440  public function getFileInFolder(string $fileName, string $folderIdentifier): string
441  {
442  return $this->canonicalizeAndCheckFileIdentifier($folderIdentifier . '/' . $fileName);
443  }
444 
461  public function getFilesInFolder(string $folderIdentifier, int $start = 0, int $numberOfItems = 0, bool $recursive = false, array $filenameFilterCallbacks = [], string $sort = '', bool $sortRev = false): array
462  {
463  return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $filenameFilterCallbacks, true, false, $recursive, $sort, $sortRev);
464  }
465 
473  public function countFilesInFolder(string $folderIdentifier, bool $recursive = false, array $filenameFilterCallbacks = []): int
474  {
475  return count($this->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filenameFilterCallbacks));
476  }
477 
495  public function getFoldersInFolder(string $folderIdentifier, int $start = 0, int $numberOfItems = 0, bool $recursive = false, array $folderNameFilterCallbacks = [], string $sort = '', bool $sortRev = false): array
496  {
497  return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $folderNameFilterCallbacks, false, true, $recursive, $sort, $sortRev);
498  }
499 
507  public function countFoldersInFolder(string $folderIdentifier, bool $recursive = false, array $folderNameFilterCallbacks = []): int
508  {
509  return count($this->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $folderNameFilterCallbacks));
510  }
511 
524  protected function retrieveFileAndFoldersInPath(string $path, bool $recursive = false, bool $includeFiles = true, bool $includeDirs = true, string $sort = '', bool $sortRev = false): array
525  {
526  $pathLength = strlen($this->getAbsoluteBasePath());
527  $iteratorMode = \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::FOLLOW_SYMLINKS;
528  if ($recursive) {
529  $iterator = new \RecursiveIteratorIterator(
530  new \RecursiveDirectoryIterator($path, $iteratorMode),
531  \RecursiveIteratorIterator::SELF_FIRST,
532  \RecursiveIteratorIterator::CATCH_GET_CHILD
533  );
534  } else {
535  $iterator = new \RecursiveDirectoryIterator($path, $iteratorMode);
536  }
537 
538  $directoryEntries = [];
539  while ($iterator->valid()) {
541  $entry = $iterator->current();
542  $isFile = $entry->isFile();
543  $isDirectory = !$isFile && $entry->isDir();
544  if (
545  (!$isFile && !$isDirectory) // skip non-files/non-folders
546  || ($isFile && !$includeFiles) // skip files if they are excluded
547  || ($isDirectory && !$includeDirs) // skip directories if they are excluded
548  || $entry->getFilename() === '' // skip empty entries
549  || !$entry->isReadable() // skip unreadable entries
550  ) {
551  $iterator->next();
552  continue;
553  }
554  $entryIdentifier = '/' . substr($entry->getPathname(), $pathLength);
555  $entryName = ‪PathUtility::basename($entryIdentifier);
556  if ($isDirectory) {
557  $entryIdentifier .= '/';
558  }
559  $entryArray = [
560  'identifier' => $entryIdentifier,
561  'name' => $entryName,
562  'type' => $isDirectory ? 'dir' : 'file',
563  ];
564  $directoryEntries[$entryIdentifier] = $entryArray;
565  $iterator->next();
566  }
567  return $this->sortDirectoryEntries($directoryEntries, $sort, $sortRev);
568  }
569 
582  protected function sortDirectoryEntries(array $directoryEntries, string $sort = '', bool $sortRev = false): array
583  {
584  $entriesToSort = [];
585  foreach ($directoryEntries as $entryArray) {
586  ‪$dir = pathinfo($entryArray['name'], PATHINFO_DIRNAME) . '/';
587  $fullPath = $this->getAbsoluteBasePath() . $entryArray['identifier'];
588  switch ($sort) {
589  case 'size':
590  $sortingKey = '0';
591  if ($entryArray['type'] === 'file') {
592  $sortingKey = $this->getSpecificFileInformation($fullPath, ‪$dir, 'size');
593  }
594  // Add a character for a natural order sorting
595  $sortingKey .= 's';
596  break;
597  case 'rw':
598  $perms = $this->getPermissions($entryArray['identifier']);
599  $sortingKey = ($perms['r'] ? 'R' : '')
600  . ($perms['w'] ? 'W' : '');
601  break;
602  case 'fileext':
603  $sortingKey = pathinfo($entryArray['name'], PATHINFO_EXTENSION);
604  break;
605  case 'tstamp':
606  $sortingKey = $this->getSpecificFileInformation($fullPath, ‪$dir, 'mtime');
607  // Add a character for a natural order sorting
608  $sortingKey .= 't';
609  break;
610  case 'crdate':
611  $sortingKey = $this->getSpecificFileInformation($fullPath, ‪$dir, 'ctime');
612  // Add a character for a natural order sorting
613  $sortingKey .= 'c';
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 
642  protected function extractFileInformation(string $filePath, string $containerPath, array $propertiesToExtract = []): array
643  {
644  if (empty($propertiesToExtract)) {
645  $propertiesToExtract = [
646  'size', 'atime', 'mtime', 'ctime', 'mimetype', 'name', 'extension',
647  'identifier', 'identifier_hash', 'storage', 'folder_hash',
648  ];
649  }
650  $fileInformation = [];
651  foreach ($propertiesToExtract as $property) {
652  $fileInformation[$property] = $this->getSpecificFileInformation($filePath, $containerPath, $property);
653  }
654  return $fileInformation;
655  }
656 
660  public function getSpecificFileInformation(string $fileIdentifier, string $containerPath, string $property): bool|int|string|null
661  {
662  ‪$identifier = $this->canonicalizeAndCheckFileIdentifier($containerPath . ‪PathUtility::basename($fileIdentifier));
663 
664  $fileInfo = GeneralUtility::makeInstance(FileInfo::class, $fileIdentifier);
665  return match ($property) {
666  'size' => $fileInfo->getSize(),
667  'atime' => $fileInfo->getATime(),
668  'mtime' => $fileInfo->getMTime(),
669  'ctime' => $fileInfo->getCTime(),
670  'name' => ‪PathUtility::basename($fileIdentifier),
671  'extension' => ‪PathUtility::pathinfo($fileIdentifier, PATHINFO_EXTENSION),
672  'mimetype' => (string)$fileInfo->getMimeType(),
673  'identifier' => ‪$identifier,
674  'storage' => $this->storageUid,
675  'identifier_hash' => $this->hashIdentifier(‪$identifier),
676  'folder_hash' => $this->hashIdentifier($this->getParentFolderIdentifierOfIdentifier(‪$identifier)),
677  default => throw new \InvalidArgumentException(
678  sprintf('The information "%s" is not available.', $property),
679  1476047422
680  ),
681  };
682  }
683 
687  protected function getAbsoluteBasePath(): string
688  {
689  return $this->absoluteBasePath;
690  }
691 
695  protected function getAbsolutePath(string $fileIdentifier): string
696  {
697  $relativeFilePath = ltrim($this->canonicalizeAndCheckFileIdentifier($fileIdentifier), '/');
698  return $this->absoluteBasePath . $relativeFilePath;
699  }
700 
706  public function hash(string $fileIdentifier, string $hashAlgorithm): string
707  {
708  if (!in_array($hashAlgorithm, $this->supportedHashAlgorithms, true)) {
709  throw new \InvalidArgumentException('Hash algorithm "' . $hashAlgorithm . '" is not supported.', 1304964032);
710  }
711  return match ($hashAlgorithm) {
712  'sha1' => sha1_file($this->getAbsolutePath($fileIdentifier)),
713  'md5' => md5_file($this->getAbsolutePath($fileIdentifier)),
714  default => throw new \RuntimeException('Hash algorithm ' . $hashAlgorithm . ' is not implemented.', 1329644451),
715  };
716  }
717 
729  public function addFile(string $localFilePath, string $targetFolderIdentifier, string $newFileName = '', bool $removeOriginal = true): string
730  {
731  $localFilePath = $this->canonicalizeAndCheckFilePath($localFilePath);
732  // as for the "virtual storage" for backwards-compatibility, this check always fails, as the file probably lies under public web path
733  // thus, it is not checked here
734  // @todo is check in storage
735  if (str_starts_with($localFilePath, $this->absoluteBasePath) && $this->storageUid > 0) {
736  throw new \InvalidArgumentException('Cannot add a file that is already part of this storage.', 1314778269);
737  }
738  $newFileName = $this->sanitizeFileName($newFileName !== '' ? $newFileName : ‪PathUtility::basename($localFilePath));
739  $newFileIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier) . $newFileName;
740  $targetPath = $this->getAbsolutePath($newFileIdentifier);
741 
742  if ($removeOriginal) {
743  if (is_uploaded_file($localFilePath)) {
744  $result = move_uploaded_file($localFilePath, $targetPath);
745  } else {
746  $result = rename($localFilePath, $targetPath);
747  }
748  } else {
749  $result = copy($localFilePath, $targetPath);
750  }
751  if ($result === false || !file_exists($targetPath)) {
752  throw new \RuntimeException(
753  'Adding file ' . $localFilePath . ' at ' . $newFileIdentifier . ' failed.',
754  1476046453
755  );
756  }
757  clearstatcache();
758  // Change the permissions of the file
760  return $newFileIdentifier;
761  }
762 
768  public function fileExists(string $fileIdentifier): bool
769  {
770  $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
771  return is_file($absoluteFilePath);
772  }
773 
780  public function fileExistsInFolder(string $fileName, string $folderIdentifier): bool
781  {
782  ‪$identifier = $folderIdentifier . '/' . $fileName;
783  ‪$identifier = $this->canonicalizeAndCheckFileIdentifier(‪$identifier);
784  return $this->fileExists(‪$identifier);
785  }
786 
792  public function folderExists(string $folderIdentifier): bool
793  {
794  $absoluteFilePath = $this->getAbsolutePath($folderIdentifier);
795  return is_dir($absoluteFilePath);
796  }
797 
804  public function folderExistsInFolder(string $folderName, string $folderIdentifier): bool
805  {
806  ‪$identifier = $folderIdentifier . '/' . $folderName;
807  ‪$identifier = $this->canonicalizeAndCheckFolderIdentifier(‪$identifier);
808  return $this->folderExists(‪$identifier);
809  }
810 
818  public function getFolderInFolder(string $folderName, string $folderIdentifier): string
819  {
820  return $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier . '/' . $folderName);
821  }
822 
829  public function replaceFile(string $fileIdentifier, string $localFilePath): bool
830  {
831  $filePath = $this->getAbsolutePath($fileIdentifier);
832  if (is_uploaded_file($localFilePath)) {
833  $result = move_uploaded_file($localFilePath, $filePath);
834  } else {
835  $result = rename($localFilePath, $filePath);
836  }
838  if ($result === false) {
839  throw new \RuntimeException('Replacing file ' . $fileIdentifier . ' with ' . $localFilePath . ' failed.', 1315314711);
840  }
841  return true;
842  }
843 
854  public function copyFileWithinStorage(string $fileIdentifier, string $targetFolderIdentifier, string $fileName): string
855  {
856  $sourcePath = $this->getAbsolutePath($fileIdentifier);
857  $newIdentifier = $targetFolderIdentifier . '/' . $fileName;
858  $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
859 
860  $absoluteFilePath = $this->getAbsolutePath($newIdentifier);
861  copy($sourcePath, $absoluteFilePath);
862  ‪GeneralUtility::fixPermissions($absoluteFilePath);
863  return $newIdentifier;
864  }
865 
876  public function moveFileWithinStorage(string $fileIdentifier, string $targetFolderIdentifier, string $newFileName): string
877  {
878  $sourcePath = $this->getAbsolutePath($fileIdentifier);
879  $targetIdentifier = $targetFolderIdentifier . '/' . $newFileName;
880  $targetIdentifier = $this->canonicalizeAndCheckFileIdentifier($targetIdentifier);
881  $result = rename($sourcePath, $this->getAbsolutePath($targetIdentifier));
882  if ($result === false) {
883  throw new \RuntimeException('Moving file ' . $sourcePath . ' to ' . $targetIdentifier . ' failed.', 1315314712);
884  }
885  return $targetIdentifier;
886  }
887 
891  protected function copyFileToTemporaryPath(string $fileIdentifier): string
892  {
893  $sourcePath = $this->getAbsolutePath($fileIdentifier);
894  $temporaryPath = $this->getTemporaryPathForFile($fileIdentifier);
895  $result = copy($sourcePath, $temporaryPath);
896  touch($temporaryPath, (int)filemtime($sourcePath));
897  if ($result === false) {
898  throw new \RuntimeException(
899  'Copying file "' . $fileIdentifier . '" to temporary path "' . $temporaryPath . '" failed.',
900  1320577649
901  );
902  }
903  return $temporaryPath;
904  }
905 
910  protected function recycleFileOrFolder(string $filePath, string $recycleDirectory): bool
911  {
912  $destinationFile = $recycleDirectory . '/' . ‪PathUtility::basename($filePath);
913  if (file_exists($destinationFile)) {
914  $timeStamp = \DateTimeImmutable::createFromFormat('U.u', (string)microtime(true))->format('YmdHisu');
915  $destinationFile = $recycleDirectory . '/' . $timeStamp . '_' . ‪PathUtility::basename($filePath);
916  }
917  $result = rename($filePath, $destinationFile);
918  // Update the mtime for the file, so the recycler garbage collection task knows which files to delete
919  // Using ctime() is not possible there since this is not supported on Windows
920  if ($result) {
921  touch($destinationFile);
922  }
923  return $result;
924  }
925 
931  protected function createIdentifierMap(array $filesAndFolders, string $sourceFolderIdentifier, string $targetFolderIdentifier): array
932  {
933  $identifierMap = [];
934  $identifierMap[$sourceFolderIdentifier] = $targetFolderIdentifier;
935  foreach ($filesAndFolders as $oldItem) {
936  $oldIdentifier = $oldItem['identifier'];
937  if ($oldItem['type'] === 'dir') {
938  $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
939  str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
940  );
941  } else {
942  $newIdentifier = $this->canonicalizeAndCheckFileIdentifier(
943  str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
944  );
945  }
946  if (!file_exists($this->getAbsolutePath($newIdentifier))) {
947  throw new FileOperationErrorException(
948  sprintf('File "%1$s" was not found (should have been copied/moved from "%2$s").', $newIdentifier, $oldIdentifier),
949  1330119453
950  );
951  }
952  $identifierMap[$oldIdentifier] = $newIdentifier;
953  }
954  return $identifierMap;
955  }
956 
965  public function moveFolderWithinStorage(string $sourceFolderIdentifier, string $targetFolderIdentifier, string $newFolderName): array
966  {
967  $sourcePath = $this->getAbsolutePath($sourceFolderIdentifier);
968  $relativeTargetPath = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
969  $targetPath = $this->getAbsolutePath($relativeTargetPath);
970  // get all files and folders we are going to move, to have a map for updating later.
971  $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, true);
972  $result = rename($sourcePath, $targetPath);
973  if ($result === false) {
974  throw new \RuntimeException('Moving folder ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320711817);
975  }
976  // Create a mapping from old to new identifiers
977  return $this->createIdentifierMap($filesAndFolders, $sourceFolderIdentifier, $relativeTargetPath);
978  }
979 
988  public function copyFolderWithinStorage(string $sourceFolderIdentifier, string $targetFolderIdentifier, string $newFolderName): bool
989  {
990  // This target folder path already includes the topmost level, i.e. the folder this method knows as $folderToCopy.
991  // We can thus rely on this folder being present and just create the subfolder we want to copy to.
992  $newFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
993  $sourceFolderPath = $this->getAbsolutePath($sourceFolderIdentifier);
994  $targetFolderPath = $this->getAbsolutePath($newFolderIdentifier);
995 
996  mkdir($targetFolderPath);
997  $iterator = new \RecursiveIteratorIterator(
998  new \RecursiveDirectoryIterator($sourceFolderPath),
999  \RecursiveIteratorIterator::SELF_FIRST,
1000  \RecursiveIteratorIterator::CATCH_GET_CHILD
1001  );
1002  // Rewind the iterator as this is important for some systems e.g. Windows
1003  $iterator->rewind();
1004  while ($iterator->valid()) {
1006  $current = $iterator->current();
1007  $fileName = $current->getFilename();
1008  $itemSubPath = GeneralUtility::fixWindowsFilePath((string)$iterator->getSubPathname());
1009  if ($current->isDir() && !($fileName === '..' || $fileName === '.')) {
1010  ‪GeneralUtility::mkdir($targetFolderPath . '/' . $itemSubPath);
1011  } elseif ($current->isFile()) {
1012  $copySourcePath = $sourceFolderPath . '/' . $itemSubPath;
1013  $copyTargetPath = $targetFolderPath . '/' . $itemSubPath;
1014  $result = copy($copySourcePath, $copyTargetPath);
1015  if ($result === false) {
1016  // rollback
1017  ‪GeneralUtility::rmdir($targetFolderIdentifier, true);
1018  throw new FileOperationErrorException(
1019  'Copying resource "' . $copySourcePath . '" to "' . $copyTargetPath . '" failed.',
1020  1330119452
1021  );
1022  }
1023  }
1024  $iterator->next();
1025  }
1026  ‪GeneralUtility::fixPermissions($targetFolderPath, true);
1027  return true;
1028  }
1029 
1037  public function renameFile(string $fileIdentifier, string $newName): string
1038  {
1039  // Makes sure the Path given as parameter is valid
1040  $newName = $this->sanitizeFileName($newName);
1041  $newIdentifier = rtrim(GeneralUtility::fixWindowsFilePath(‪PathUtility::dirname($fileIdentifier)), '/') . '/' . $newName;
1042  $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
1043  // The target should not exist already
1044  if ($this->fileExists($newIdentifier)) {
1045  throw new ExistingTargetFileNameException(
1046  'The target file "' . $newIdentifier . '" already exists.',
1047  1320291063
1048  );
1049  }
1050  $sourcePath = $this->getAbsolutePath($fileIdentifier);
1051  $targetPath = $this->getAbsolutePath($newIdentifier);
1052  $result = rename($sourcePath, $targetPath);
1053  if ($result === false) {
1054  throw new \RuntimeException('Renaming file ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320375115);
1055  }
1056  return $newIdentifier;
1057  }
1058 
1067  public function renameFolder(string $folderIdentifier, string $newName): array
1068  {
1069  $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
1070  $newName = $this->sanitizeFileName($newName);
1071 
1072  $newIdentifier = ‪PathUtility::dirname($folderIdentifier) . '/' . $newName;
1073  $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier($newIdentifier);
1074 
1075  $sourcePath = $this->getAbsolutePath($folderIdentifier);
1076  $targetPath = $this->getAbsolutePath($newIdentifier);
1077  // get all files and folders we are going to move, to have a map for updating later.
1078  $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, true);
1079  $result = rename($sourcePath, $targetPath);
1080  if ($result === false) {
1081  throw new \RuntimeException(sprintf('Renaming folder "%1$s" to "%2$s" failed."', $sourcePath, $targetPath), 1320375116);
1082  }
1083  try {
1084  // Create a mapping from old to new identifiers
1085  $identifierMap = $this->createIdentifierMap($filesAndFolders, $folderIdentifier, $newIdentifier);
1086  } catch (\Exception $e) {
1087  rename($targetPath, $sourcePath);
1088  throw new \RuntimeException(
1089  sprintf(
1090  'Creating filename mapping after renaming "%1$s" to "%2$s" failed. Reverted rename operation.\\n\\nOriginal error: %3$s"',
1091  $sourcePath,
1092  $targetPath,
1093  $e->getMessage()
1094  ),
1095  1334160746
1096  );
1097  }
1098  return $identifierMap;
1099  }
1100 
1108  public function deleteFile(string $fileIdentifier): bool
1109  {
1110  $filePath = $this->getAbsolutePath($fileIdentifier);
1111  $result = unlink($filePath);
1112 
1113  if ($result === false) {
1114  throw new \RuntimeException('Deletion of file ' . $fileIdentifier . ' failed.', 1320855304);
1115  }
1116  return true;
1117  }
1118 
1124  public function deleteFolder(string $folderIdentifier, bool $deleteRecursively = false): bool
1125  {
1126  $folderPath = $this->getAbsolutePath($folderIdentifier);
1127  $recycleDirectory = $this->getRecycleDirectory($folderPath);
1128  if (!empty($recycleDirectory) && $folderPath !== $recycleDirectory) {
1129  $result = $this->recycleFileOrFolder($folderPath, $recycleDirectory);
1130  } else {
1131  $result = ‪GeneralUtility::rmdir($folderPath, $deleteRecursively);
1132  }
1133  if ($result === false) {
1134  throw new FileOperationErrorException(
1135  'Deleting folder "' . $folderIdentifier . '" failed.',
1136  1330119451
1137  );
1138  }
1139  return $result;
1140  }
1141 
1148  public function isFolderEmpty(string $folderIdentifier): bool
1149  {
1150  $path = $this->getAbsolutePath($folderIdentifier);
1151  $dirHandle = opendir($path);
1152  if ($dirHandle === false) {
1153  return true;
1154  }
1155  while ($entry = readdir($dirHandle)) {
1156  if ($entry !== '.' && $entry !== '..') {
1157  closedir($dirHandle);
1158  return false;
1159  }
1160  }
1161  closedir($dirHandle);
1162  return true;
1163  }
1164 
1175  public function getFileForLocalProcessing(string $fileIdentifier, bool $writable = true): string
1176  {
1177  if ($writable === false) {
1178  return $this->getAbsolutePath($fileIdentifier);
1179  }
1180  return $this->copyFileToTemporaryPath($fileIdentifier);
1181  }
1182 
1189  public function getPermissions(string ‪$identifier): array
1190  {
1191  $path = $this->getAbsolutePath(‪$identifier);
1192  $permissionBits = fileperms($path);
1193  if ($permissionBits === false) {
1194  throw new ResourcePermissionsUnavailableException('Error while fetching permissions for ' . $path, 1319455097);
1195  }
1196  return [
1197  'r' => is_readable($path),
1198  'w' => is_writable($path),
1199  ];
1200  }
1201 
1211  public function isWithin(string $folderIdentifier, string ‪$identifier): bool
1212  {
1213  $folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier);
1214  $entryIdentifier = $this->canonicalizeAndCheckFileIdentifier(‪$identifier);
1215  if ($folderIdentifier === $entryIdentifier) {
1216  return true;
1217  }
1218  // File identifier canonicalization will not modify a single slash so
1219  // we must not append another slash in that case.
1220  if ($folderIdentifier !== '/') {
1221  $folderIdentifier .= '/';
1222  }
1223  return str_starts_with($entryIdentifier, $folderIdentifier);
1224  }
1225 
1233  public function createFile(string $fileName, string $parentFolderIdentifier): string
1234  {
1235  $fileName = $this->sanitizeFileName(ltrim($fileName, '/'));
1236  $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
1237  $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
1238  $parentFolderIdentifier . $fileName
1239  );
1240  $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
1241  $result = touch($absoluteFilePath);
1242  ‪GeneralUtility::fixPermissions($absoluteFilePath);
1243  clearstatcache();
1244  if ($result !== true) {
1245  throw new \RuntimeException('Creating file ' . $fileIdentifier . ' failed.', 1320569854);
1246  }
1247  return $fileIdentifier;
1248  }
1249 
1259  public function getFileContents(string $fileIdentifier): string
1260  {
1261  $filePath = $this->getAbsolutePath($fileIdentifier);
1262  return is_readable($filePath) ? (string)file_get_contents($filePath) : '';
1263  }
1264 
1272  public function setFileContents(string $fileIdentifier, string $contents): int
1273  {
1274  $filePath = $this->getAbsolutePath($fileIdentifier);
1275  $result = file_put_contents($filePath, $contents);
1276 
1277  // Make sure later calls to filesize() etc. return correct values.
1278  clearstatcache(true, $filePath);
1279 
1280  if ($result === false) {
1281  throw new \RuntimeException('Setting contents of file "' . $fileIdentifier . '" failed.', 1325419305);
1282  }
1283  return $result;
1284  }
1285 
1290  public function getRole(string $folderIdentifier): string
1291  {
1292  $name = ‪PathUtility::basename($folderIdentifier);
1293  return $this->mappingFolderNameToRole[$name] ?? ‪FolderInterface::ROLE_DEFAULT;
1294  }
1295 
1302  public function dumpFileContents(string ‪$identifier): void
1303  {
1304  readfile($this->getAbsolutePath($this->canonicalizeAndCheckFileIdentifier(‪$identifier)));
1305  }
1306 
1310  public function streamFile(string ‪$identifier, array $properties): ResponseInterface
1311  {
1312  $fileInfo = $this->getFileInfoByIdentifier(‪$identifier, ['name', 'mimetype', 'mtime', 'size']);
1313  $downloadName = $properties['filename_overwrite'] ?? $fileInfo['name'] ?? '';
1314  $mimeType = $properties['mimetype_overwrite'] ?? $fileInfo['mimetype'] ?? '';
1315  $contentDisposition = ($properties['as_download'] ?? false) ? 'attachment' : 'inline';
1316 
1317  $filePath = $this->getAbsolutePath($this->canonicalizeAndCheckFileIdentifier(‪$identifier));
1318 
1319  return new Response(
1320  new SelfEmittableLazyOpenStream($filePath),
1321  200,
1322  [
1323  'Content-Disposition' => $contentDisposition . '; filename="' . $downloadName . '"',
1324  'Content-Type' => $mimeType,
1325  'Content-Length' => (string)$fileInfo['size'],
1326  'Last-Modified' => gmdate('D, d M Y H:i:s', $fileInfo['mtime']) . ' GMT',
1327  // Cache-Control header is needed here to solve an issue with browser IE8 and lower
1328  // See for more information: http://support.microsoft.com/kb/323308
1329  'Cache-Control' => '',
1330  ]
1331  );
1332  }
1333 
1338  protected function getRecycleDirectory(string $path): string
1339  {
1340  $recyclerSubdirectory = array_search(‪FolderInterface::ROLE_RECYCLER, $this->mappingFolderNameToRole, true);
1341  if ($recyclerSubdirectory === false) {
1342  return '';
1343  }
1344  $rootDirectory = rtrim($this->getAbsolutePath($this->getRootLevelFolder()), '/');
1345  $searchDirectory = ‪PathUtility::dirname($path);
1346  // Check if file or folder to be deleted is inside a recycler directory
1347  if ($this->getRole($searchDirectory) === ‪FolderInterface::ROLE_RECYCLER) {
1348  $searchDirectory = ‪PathUtility::dirname($searchDirectory);
1349  // Check if file or folder to be deleted is inside the root recycler
1350  if ($searchDirectory == $rootDirectory) {
1351  return '';
1352  }
1353  $searchDirectory = ‪PathUtility::dirname($searchDirectory);
1354  }
1355  // Search for the closest recycler directory
1356  while ($searchDirectory) {
1357  $recycleDirectory = $searchDirectory . '/' . $recyclerSubdirectory;
1358  if (is_dir($recycleDirectory)) {
1359  return $recycleDirectory;
1360  }
1361  if ($searchDirectory === $rootDirectory) {
1362  return '';
1363  }
1364  $searchDirectory = ‪PathUtility::dirname($searchDirectory);
1365  }
1366 
1367  return '';
1368  }
1369 
1374  protected function isAllowedAbsolutePath(string $path): bool
1375  {
1376  return GeneralUtility::isAllowedAbsPath($path);
1377  }
1378 }
‪TYPO3\CMS\Core\Utility\GeneralUtility\mkdir
‪static bool mkdir(string $newFolder)
Definition: GeneralUtility.php:1638
‪TYPO3\CMS\Core\Utility\PathUtility\stripPathSitePrefix
‪static stripPathSitePrefix(string $path)
Definition: PathUtility.php:428
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:27
‪TYPO3\CMS\Core\Resource\Capabilities\CAPABILITY_PUBLIC
‪const CAPABILITY_PUBLIC
Definition: Capabilities.php:31
‪TYPO3\CMS\Core\Resource\Capabilities
Definition: Capabilities.php:23
‪TYPO3\CMS\Core\Resource\FolderInterface\ROLE_DEFAULT
‪const ROLE_DEFAULT
Definition: FolderInterface.php:28
‪TYPO3\CMS\Core\Core\Environment\getPublicPath
‪static getPublicPath()
Definition: Environment.php:187
‪TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException
Definition: ExistingTargetFileNameException.php:23
‪TYPO3\CMS\Core\Resource\FolderInterface\ROLE_USERUPLOAD
‪const ROLE_USERUPLOAD
Definition: FolderInterface.php:32
‪TYPO3\CMS\Core\Charset\CharsetConverter
Definition: CharsetConverter.php:54
‪TYPO3\CMS\Core\Resource\Exception\ResourcePermissionsUnavailableException
Definition: ResourcePermissionsUnavailableException.php:25
‪$dir
‪$dir
Definition: validateRstFiles.php:257
‪TYPO3\CMS\Core\Utility\GeneralUtility\mkdir_deep
‪static mkdir_deep(string $directory)
Definition: GeneralUtility.php:1654
‪TYPO3\CMS\Core\Utility\PathUtility\basename
‪static basename(string $path)
Definition: PathUtility.php:219
‪TYPO3\CMS\Core\Resource\Capabilities\CAPABILITY_HIERARCHICAL_IDENTIFIERS
‪const CAPABILITY_HIERARCHICAL_IDENTIFIERS
Definition: Capabilities.php:40
‪TYPO3\CMS\Core\Resource\FolderInterface\ROLE_TEMPORARY
‪const ROLE_TEMPORARY
Definition: FolderInterface.php:31
‪TYPO3\CMS\Webhooks\Message\$publicUrl
‪identifier readonly string readonly string $publicUrl
Definition: FileUpdatedMessage.php:36
‪TYPO3\CMS\Core\Utility\PathUtility\dirname
‪static dirname(string $path)
Definition: PathUtility.php:243
‪TYPO3\CMS\Core\Http\Response
Definition: Response.php:32
‪TYPO3\CMS\Core\Resource\Exception\InvalidConfigurationException
Definition: InvalidConfigurationException.php:23
‪TYPO3\CMS\Core\Resource\Capabilities\CAPABILITY_BROWSABLE
‪const CAPABILITY_BROWSABLE
Definition: Capabilities.php:27
‪TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException
Definition: FolderDoesNotExistException.php:21
‪TYPO3\CMS\Core\Utility\GeneralUtility\rmdir
‪static bool rmdir(string $path, bool $removeNonEmpty=false)
Definition: GeneralUtility.php:1702
‪TYPO3\CMS\Core\Utility\GeneralUtility\fixPermissions
‪static bool fixPermissions(string $path, bool $recursive=false)
Definition: GeneralUtility.php:1496
‪TYPO3\CMS\Core\Resource\Capabilities\CAPABILITY_WRITABLE
‪const CAPABILITY_WRITABLE
Definition: Capabilities.php:36
‪TYPO3\CMS\Core\Resource\Driver
Definition: AbstractDriver.php:18
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪TYPO3\CMS\Core\Resource\FolderInterface\ROLE_RECYCLER
‪const ROLE_RECYCLER
Definition: FolderInterface.php:29
‪TYPO3\CMS\Core\Resource\FolderInterface
Definition: FolderInterface.php:24
‪TYPO3\CMS\Core\Type\File\FileInfo
Definition: FileInfo.php:25
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Http\SelfEmittableLazyOpenStream
Definition: SelfEmittableLazyOpenStream.php:32
‪TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException
Definition: FileOperationErrorException.php:21
‪TYPO3\CMS\Core\Resource\Exception\InvalidPathException
Definition: InvalidPathException.php:23
‪TYPO3\CMS\Core\Resource\Exception\InvalidFileNameException
Definition: InvalidFileNameException.php:23
‪TYPO3\CMS\Core\Utility\PathUtility\pathinfo
‪static string string[] pathinfo(string $path, int $options=PATHINFO_ALL)
Definition: PathUtility.php:270
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37