‪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  {
117  }
118 
123  protected function determineBaseUrl(): void
124  {
125  // only calculate baseURI if the storage does not enforce jumpUrl Script
126  if ($this->hasCapability(‪Capabilities::CAPABILITY_PUBLIC)) {
127  if (!empty($this->configuration['baseUri'])) {
128  $this->baseUri = rtrim($this->configuration['baseUri'], '/') . '/';
129  } elseif (str_starts_with($this->absoluteBasePath, ‪Environment::getPublicPath())) {
130  // use site-relative URLs
131  $temporaryBaseUri = rtrim(‪PathUtility::stripPathSitePrefix($this->absoluteBasePath), '/');
132  if ($temporaryBaseUri !== '') {
133  $uriParts = explode('/', $temporaryBaseUri);
134  $uriParts = array_map('rawurlencode', $uriParts);
135  $temporaryBaseUri = implode('/', $uriParts) . '/';
136  }
137  $this->baseUri = $temporaryBaseUri;
138  }
139  }
140  }
141 
145  protected function calculateBasePath(array $configuration): string
146  {
147  if (!array_key_exists('basePath', $configuration) || empty($configuration['basePath'])) {
148  throw new InvalidConfigurationException(
149  'Configuration must contain base path.',
150  1346510477
151  );
152  }
153 
154  if (!empty($configuration['pathType']) && $configuration['pathType'] === 'relative') {
155  $relativeBasePath = $configuration['basePath'];
156  $absoluteBasePath = ‪Environment::getPublicPath() . '/' . $relativeBasePath;
157  } else {
158  $absoluteBasePath = $configuration['basePath'];
159  }
160  $absoluteBasePath = $this->canonicalizeAndCheckFilePath($absoluteBasePath);
161  $absoluteBasePath = rtrim($absoluteBasePath, '/') . '/';
162  if (!is_dir($absoluteBasePath)) {
163  throw new InvalidConfigurationException(
164  'Base path "' . $absoluteBasePath . '" does not exist or is no directory.',
165  1299233097
166  );
167  }
168  return $absoluteBasePath;
169  }
170 
179  public function getPublicUrl(string ‪$identifier): ?string
180  {
181  ‪$publicUrl = null;
182  if ($this->baseUri !== null) {
183  $uriParts = explode('/', ltrim(‪$identifier, '/'));
184  $uriParts = array_map('rawurlencode', $uriParts);
185  ‪$identifier = implode('/', $uriParts);
186  ‪$publicUrl = $this->baseUri . ‪$identifier;
187  }
188  return ‪$publicUrl;
189  }
190 
196  public function getRootLevelFolder(): string
197  {
198  return '/';
199  }
200 
206  public function getDefaultFolder(): string
207  {
208  ‪$identifier = '/user_upload/';
209  $createFolder = !$this->folderExists(‪$identifier);
210  if ($createFolder === true) {
211  ‪$identifier = $this->createFolder('user_upload');
212  }
213  return ‪$identifier;
214  }
215 
223  public function createFolder(string $newFolderName, string $parentFolderIdentifier = '', bool $recursive = false): string
224  {
225  $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
226  $newFolderName = trim($newFolderName, '/');
227  if ($recursive === false) {
228  $newFolderName = $this->sanitizeFileName($newFolderName);
229  $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier . $newFolderName . '/');
230  ‪GeneralUtility::mkdir($this->getAbsolutePath($newIdentifier));
231  } else {
232  $parts = GeneralUtility::trimExplode('/', $newFolderName);
233  $parts = array_map([$this, 'sanitizeFileName'], $parts);
234  $newFolderName = implode('/', $parts);
235  $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
236  $parentFolderIdentifier . $newFolderName . '/'
237  );
238  ‪GeneralUtility::mkdir_deep($this->getAbsolutePath($newIdentifier));
239  }
240  return $newIdentifier;
241  }
242 
250  public function getFileInfoByIdentifier(string $fileIdentifier, array $propertiesToExtract = []): array
251  {
252  $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
253  // don't use $this->fileExists() because we need the absolute path to the file anyway, so we can directly
254  // use PHP's filesystem method.
255  if (!file_exists($absoluteFilePath) || !is_file($absoluteFilePath)) {
256  throw new \InvalidArgumentException('File ' . $fileIdentifier . ' does not exist.', 1314516809);
257  }
258 
259  $dirPath = ‪PathUtility::dirname($fileIdentifier);
260  $dirPath = $this->canonicalizeAndCheckFolderIdentifier($dirPath);
261  return $this->extractFileInformation($absoluteFilePath, $dirPath, $propertiesToExtract);
262  }
263 
276  public function getFolderInfoByIdentifier(string $folderIdentifier): array
277  {
278  $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
279 
280  if (!$this->folderExists($folderIdentifier)) {
281  throw new FolderDoesNotExistException(
282  'Folder "' . $folderIdentifier . '" does not exist.',
283  1314516810
284  );
285  }
286  $absolutePath = $this->getAbsolutePath($folderIdentifier);
287  return [
288  'identifier' => $folderIdentifier,
289  'name' => ‪PathUtility::basename($folderIdentifier),
290  'mtime' => filemtime($absolutePath),
291  'ctime' => filectime($absolutePath),
292  'storage' => $this->storageUid,
293  ];
294  }
295 
308  public function sanitizeFileName(string $fileName, string $charset = 'utf-8'): string
309  {
310  if ($charset === 'utf-8') {
311  $fileName = \Normalizer::normalize($fileName) ?: $fileName;
312  }
313 
314  // Handle UTF-8 characters
315  if (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
316  // Allow ".", "-", 0-9, a-z, A-Z and everything beyond U+C0 (latin capital letter a with grave)
317  $cleanFileName = (string)preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . ']/u', '_', trim($fileName));
318  } else {
319  $fileName = GeneralUtility::makeInstance(CharsetConverter::class)->specCharsToASCII($charset, $fileName);
320  // Replace unwanted characters with underscores
321  $cleanFileName = (string)preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . '\\xC0-\\xFF]/', '_', trim($fileName));
322  }
323  // Strip trailing dots and return
324  $cleanFileName = rtrim($cleanFileName, '.');
325  if ($cleanFileName === '') {
326  throw new InvalidFileNameException(
327  'File name ' . $fileName . ' is invalid.',
328  1320288991
329  );
330  }
331  return $cleanFileName;
332  }
333 
349  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
350  {
351  $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
352  $realPath = $this->getAbsolutePath($folderIdentifier);
353  if (!is_dir($realPath)) {
354  throw new \InvalidArgumentException(
355  'Cannot list items in directory ' . $folderIdentifier . ' - does not exist or is no directory',
356  1314349666
357  );
358  }
359 
360  $items = $this->retrieveFileAndFoldersInPath($realPath, $recursive, $includeFiles, $includeDirs, $sort, $sortRev);
361  $iterator = new \ArrayIterator($items);
362  if ($iterator->count() === 0) {
363  return [];
364  }
365 
366  // $c is the counter for how many items we still have to fetch (-1 is unlimited)
367  $c = $numberOfItems > 0 ? $numberOfItems : - 1;
368  $items = [];
369  while ($iterator->valid() && ($numberOfItems === 0 || $c > 0)) {
370  // $iteratorItem is the file or folder name
371  $iteratorItem = $iterator->current();
372  // go on to the next iterator item now as we might skip this one early
373  $iterator->next();
374 
375  try {
376  if (
377  !$this->applyFilterMethodsToDirectoryItem(
378  $filterMethods,
379  $iteratorItem['name'],
380  $iteratorItem['identifier'],
381  $this->getParentFolderIdentifierOfIdentifier($iteratorItem['identifier'])
382  )
383  ) {
384  continue;
385  }
386  if ($start > 0) {
387  $start--;
388  } else {
389  // The identifier can also be an int-like string, resulting in int array keys.
390  $items[$iteratorItem['identifier']] = $iteratorItem['identifier'];
391  // Decrement item counter to make sure we only return $numberOfItems
392  // we cannot do this earlier in the method (unlike moving the iterator forward) because we only add the
393  // item here
394  --$c;
395  }
396  } catch (InvalidPathException) {
397  }
398  }
399  return $items;
400  }
401 
408  protected function applyFilterMethodsToDirectoryItem(array $filterMethods, string $itemName, string $itemIdentifier, string $parentIdentifier): bool
409  {
410  foreach ($filterMethods as $filter) {
411  if (is_callable($filter)) {
412  $result = $filter($itemName, $itemIdentifier, $parentIdentifier, [], $this);
413  // We use -1 as the "don't include“ return value, for historic reasons,
414  // as call_user_func() used to return FALSE if calling the method failed.
415  if ($result === -1) {
416  return false;
417  }
418  if ($result === false) {
419  throw new \RuntimeException(
420  'Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1],
421  1476046425
422  );
423  }
424  }
425  }
426  return true;
427  }
428 
436  public function getFileInFolder(string $fileName, string $folderIdentifier): string
437  {
438  return $this->canonicalizeAndCheckFileIdentifier($folderIdentifier . '/' . $fileName);
439  }
440 
457  public function getFilesInFolder(string $folderIdentifier, int $start = 0, int $numberOfItems = 0, bool $recursive = false, array $filenameFilterCallbacks = [], string $sort = '', bool $sortRev = false): array
458  {
459  return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $filenameFilterCallbacks, true, false, $recursive, $sort, $sortRev);
460  }
461 
469  public function countFilesInFolder(string $folderIdentifier, bool $recursive = false, array $filenameFilterCallbacks = []): int
470  {
471  return count($this->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filenameFilterCallbacks));
472  }
473 
491  public function getFoldersInFolder(string $folderIdentifier, int $start = 0, int $numberOfItems = 0, bool $recursive = false, array $folderNameFilterCallbacks = [], string $sort = '', bool $sortRev = false): array
492  {
493  return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $folderNameFilterCallbacks, false, true, $recursive, $sort, $sortRev);
494  }
495 
503  public function countFoldersInFolder(string $folderIdentifier, bool $recursive = false, array $folderNameFilterCallbacks = []): int
504  {
505  return count($this->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $folderNameFilterCallbacks));
506  }
507 
520  protected function retrieveFileAndFoldersInPath(string $path, bool $recursive = false, bool $includeFiles = true, bool $includeDirs = true, string $sort = '', bool $sortRev = false): array
521  {
522  $pathLength = strlen($this->getAbsoluteBasePath());
523  $iteratorMode = \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::FOLLOW_SYMLINKS;
524  if ($recursive) {
525  $iterator = new \RecursiveIteratorIterator(
526  new \RecursiveDirectoryIterator($path, $iteratorMode),
527  \RecursiveIteratorIterator::SELF_FIRST,
528  \RecursiveIteratorIterator::CATCH_GET_CHILD
529  );
530  } else {
531  $iterator = new \RecursiveDirectoryIterator($path, $iteratorMode);
532  }
533 
534  $directoryEntries = [];
535  while ($iterator->valid()) {
537  $entry = $iterator->current();
538  $isFile = $entry->isFile();
539  $isDirectory = !$isFile && $entry->isDir();
540  if (
541  (!$isFile && !$isDirectory) // skip non-files/non-folders
542  || ($isFile && !$includeFiles) // skip files if they are excluded
543  || ($isDirectory && !$includeDirs) // skip directories if they are excluded
544  || $entry->getFilename() === '' // skip empty entries
545  || !$entry->isReadable() // skip unreadable entries
546  ) {
547  $iterator->next();
548  continue;
549  }
550  $entryIdentifier = '/' . substr($entry->getPathname(), $pathLength);
551  $entryName = ‪PathUtility::basename($entryIdentifier);
552  if ($isDirectory) {
553  $entryIdentifier .= '/';
554  }
555  $entryArray = [
556  'identifier' => $entryIdentifier,
557  'name' => $entryName,
558  'type' => $isDirectory ? 'dir' : 'file',
559  ];
560  $directoryEntries[$entryIdentifier] = $entryArray;
561  $iterator->next();
562  }
563  return $this->sortDirectoryEntries($directoryEntries, $sort, $sortRev);
564  }
565 
578  protected function sortDirectoryEntries(array $directoryEntries, string $sort = '', bool $sortRev = false): array
579  {
580  $entriesToSort = [];
581  foreach ($directoryEntries as $entryArray) {
582  ‪$dir = pathinfo($entryArray['name'], PATHINFO_DIRNAME) . '/';
583  $fullPath = $this->getAbsoluteBasePath() . $entryArray['identifier'];
584  switch ($sort) {
585  case 'size':
586  $sortingKey = '0';
587  if ($entryArray['type'] === 'file') {
588  $sortingKey = $this->getSpecificFileInformation($fullPath, ‪$dir, 'size');
589  }
590  // Add a character for a natural order sorting
591  $sortingKey .= 's';
592  break;
593  case 'rw':
594  $perms = $this->getPermissions($entryArray['identifier']);
595  $sortingKey = ($perms['r'] ? 'R' : '')
596  . ($perms['w'] ? 'W' : '');
597  break;
598  case 'fileext':
599  $sortingKey = pathinfo($entryArray['name'], PATHINFO_EXTENSION);
600  break;
601  case 'tstamp':
602  $sortingKey = $this->getSpecificFileInformation($fullPath, ‪$dir, 'mtime');
603  // Add a character for a natural order sorting
604  $sortingKey .= 't';
605  break;
606  case 'crdate':
607  $sortingKey = $this->getSpecificFileInformation($fullPath, ‪$dir, 'ctime');
608  // Add a character for a natural order sorting
609  $sortingKey .= 'c';
610  break;
611  case 'name':
612  case 'file':
613  default:
614  $sortingKey = $entryArray['name'];
615  }
616  $i = 0;
617  while (isset($entriesToSort[$sortingKey . $i])) {
618  $i++;
619  }
620  $entriesToSort[$sortingKey . $i] = $entryArray;
621  }
622  uksort($entriesToSort, 'strnatcasecmp');
623 
624  if ($sortRev) {
625  $entriesToSort = array_reverse($entriesToSort);
626  }
627 
628  return $entriesToSort;
629  }
630 
638  protected function extractFileInformation(string $filePath, string $containerPath, array $propertiesToExtract = []): array
639  {
640  if (empty($propertiesToExtract)) {
641  $propertiesToExtract = [
642  'size', 'atime', 'mtime', 'ctime', 'mimetype', 'name', 'extension',
643  'identifier', 'identifier_hash', 'storage', 'folder_hash',
644  ];
645  }
646  $fileInformation = [];
647  foreach ($propertiesToExtract as $property) {
648  $fileInformation[$property] = $this->getSpecificFileInformation($filePath, $containerPath, $property);
649  }
650  return $fileInformation;
651  }
652 
656  public function getSpecificFileInformation(string $fileIdentifier, string $containerPath, string $property): bool|int|string|null
657  {
658  ‪$identifier = $this->canonicalizeAndCheckFileIdentifier($containerPath . ‪PathUtility::basename($fileIdentifier));
659 
660  $fileInfo = GeneralUtility::makeInstance(FileInfo::class, $fileIdentifier);
661  return match ($property) {
662  'size' => $fileInfo->getSize(),
663  'atime' => $fileInfo->getATime(),
664  'mtime' => $fileInfo->getMTime(),
665  'ctime' => $fileInfo->getCTime(),
666  'name' => ‪PathUtility::basename($fileIdentifier),
667  'extension' => ‪PathUtility::pathinfo($fileIdentifier, PATHINFO_EXTENSION),
668  'mimetype' => (string)$fileInfo->getMimeType(),
669  'identifier' => ‪$identifier,
670  'storage' => $this->storageUid,
671  'identifier_hash' => $this->hashIdentifier(‪$identifier),
672  'folder_hash' => $this->hashIdentifier($this->getParentFolderIdentifierOfIdentifier(‪$identifier)),
673  default => throw new \InvalidArgumentException(
674  sprintf('The information "%s" is not available.', $property),
675  1476047422
676  ),
677  };
678  }
679 
683  protected function getAbsoluteBasePath(): string
684  {
685  return $this->absoluteBasePath;
686  }
687 
691  protected function getAbsolutePath(string $fileIdentifier): string
692  {
693  $relativeFilePath = ltrim($this->canonicalizeAndCheckFileIdentifier($fileIdentifier), '/');
694  return $this->absoluteBasePath . $relativeFilePath;
695  }
696 
702  public function hash(string $fileIdentifier, string $hashAlgorithm): string
703  {
704  if (!in_array($hashAlgorithm, $this->supportedHashAlgorithms, true)) {
705  throw new \InvalidArgumentException('Hash algorithm "' . $hashAlgorithm . '" is not supported.', 1304964032);
706  }
707  return match ($hashAlgorithm) {
708  'sha1' => sha1_file($this->getAbsolutePath($fileIdentifier)),
709  'md5' => md5_file($this->getAbsolutePath($fileIdentifier)),
710  default => throw new \RuntimeException('Hash algorithm ' . $hashAlgorithm . ' is not implemented.', 1329644451),
711  };
712  }
713 
725  public function addFile(string $localFilePath, string $targetFolderIdentifier, string $newFileName = '', bool $removeOriginal = true): string
726  {
727  $localFilePath = $this->canonicalizeAndCheckFilePath($localFilePath);
728  // as for the "virtual storage" for backwards-compatibility, this check always fails, as the file probably lies under public web path
729  // thus, it is not checked here
730  // @todo is check in storage
731  if (str_starts_with($localFilePath, $this->absoluteBasePath) && $this->storageUid > 0) {
732  throw new \InvalidArgumentException('Cannot add a file that is already part of this storage.', 1314778269);
733  }
734  $newFileName = $this->sanitizeFileName($newFileName !== '' ? $newFileName : ‪PathUtility::basename($localFilePath));
735  $newFileIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier) . $newFileName;
736  $targetPath = $this->getAbsolutePath($newFileIdentifier);
737 
738  if ($removeOriginal) {
739  if (is_uploaded_file($localFilePath)) {
740  $result = move_uploaded_file($localFilePath, $targetPath);
741  } else {
742  $result = rename($localFilePath, $targetPath);
743  }
744  } else {
745  $result = copy($localFilePath, $targetPath);
746  }
747  if ($result === false || !file_exists($targetPath)) {
748  throw new \RuntimeException(
749  'Adding file ' . $localFilePath . ' at ' . $newFileIdentifier . ' failed.',
750  1476046453
751  );
752  }
753  clearstatcache();
754  // Change the permissions of the file
756  return $newFileIdentifier;
757  }
758 
764  public function fileExists(string $fileIdentifier): bool
765  {
766  $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
767  return is_file($absoluteFilePath);
768  }
769 
776  public function fileExistsInFolder(string $fileName, string $folderIdentifier): bool
777  {
778  ‪$identifier = $folderIdentifier . '/' . $fileName;
779  ‪$identifier = $this->canonicalizeAndCheckFileIdentifier(‪$identifier);
780  return $this->fileExists(‪$identifier);
781  }
782 
788  public function folderExists(string $folderIdentifier): bool
789  {
790  $absoluteFilePath = $this->getAbsolutePath($folderIdentifier);
791  return is_dir($absoluteFilePath);
792  }
793 
800  public function folderExistsInFolder(string $folderName, string $folderIdentifier): bool
801  {
802  ‪$identifier = $folderIdentifier . '/' . $folderName;
803  ‪$identifier = $this->canonicalizeAndCheckFolderIdentifier(‪$identifier);
804  return $this->folderExists(‪$identifier);
805  }
806 
814  public function getFolderInFolder(string $folderName, string $folderIdentifier): string
815  {
816  return $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier . '/' . $folderName);
817  }
818 
825  public function replaceFile(string $fileIdentifier, string $localFilePath): bool
826  {
827  $filePath = $this->getAbsolutePath($fileIdentifier);
828  if (is_uploaded_file($localFilePath)) {
829  $result = move_uploaded_file($localFilePath, $filePath);
830  } else {
831  $result = rename($localFilePath, $filePath);
832  }
834  if ($result === false) {
835  throw new \RuntimeException('Replacing file ' . $fileIdentifier . ' with ' . $localFilePath . ' failed.', 1315314711);
836  }
837  return true;
838  }
839 
850  public function copyFileWithinStorage(string $fileIdentifier, string $targetFolderIdentifier, string $fileName): string
851  {
852  $sourcePath = $this->getAbsolutePath($fileIdentifier);
853  $newIdentifier = $targetFolderIdentifier . '/' . $fileName;
854  $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
855 
856  $absoluteFilePath = $this->getAbsolutePath($newIdentifier);
857  copy($sourcePath, $absoluteFilePath);
858  ‪GeneralUtility::fixPermissions($absoluteFilePath);
859  return $newIdentifier;
860  }
861 
872  public function moveFileWithinStorage(string $fileIdentifier, string $targetFolderIdentifier, string $newFileName): string
873  {
874  $sourcePath = $this->getAbsolutePath($fileIdentifier);
875  $targetIdentifier = $targetFolderIdentifier . '/' . $newFileName;
876  $targetIdentifier = $this->canonicalizeAndCheckFileIdentifier($targetIdentifier);
877  $result = rename($sourcePath, $this->getAbsolutePath($targetIdentifier));
878  if ($result === false) {
879  throw new \RuntimeException('Moving file ' . $sourcePath . ' to ' . $targetIdentifier . ' failed.', 1315314712);
880  }
881  return $targetIdentifier;
882  }
883 
887  protected function copyFileToTemporaryPath(string $fileIdentifier): string
888  {
889  $sourcePath = $this->getAbsolutePath($fileIdentifier);
890  $temporaryPath = $this->getTemporaryPathForFile($fileIdentifier);
891  $result = copy($sourcePath, $temporaryPath);
892  touch($temporaryPath, (int)filemtime($sourcePath));
893  if ($result === false) {
894  throw new \RuntimeException(
895  'Copying file "' . $fileIdentifier . '" to temporary path "' . $temporaryPath . '" failed.',
896  1320577649
897  );
898  }
899  return $temporaryPath;
900  }
901 
906  protected function recycleFileOrFolder(string $filePath, string $recycleDirectory): bool
907  {
908  $destinationFile = $recycleDirectory . '/' . ‪PathUtility::basename($filePath);
909  if (file_exists($destinationFile)) {
910  $timeStamp = \DateTimeImmutable::createFromFormat('U.u', (string)microtime(true))->format('YmdHisu');
911  $destinationFile = $recycleDirectory . '/' . $timeStamp . '_' . ‪PathUtility::basename($filePath);
912  }
913  $result = rename($filePath, $destinationFile);
914  // Update the mtime for the file, so the recycler garbage collection task knows which files to delete
915  // Using ctime() is not possible there since this is not supported on Windows
916  if ($result) {
917  touch($destinationFile);
918  }
919  return $result;
920  }
921 
927  protected function createIdentifierMap(array $filesAndFolders, string $sourceFolderIdentifier, string $targetFolderIdentifier): array
928  {
929  $identifierMap = [];
930  $identifierMap[$sourceFolderIdentifier] = $targetFolderIdentifier;
931  foreach ($filesAndFolders as $oldItem) {
932  $oldIdentifier = $oldItem['identifier'];
933  if ($oldItem['type'] === 'dir') {
934  $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
935  str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
936  );
937  } else {
938  $newIdentifier = $this->canonicalizeAndCheckFileIdentifier(
939  str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
940  );
941  }
942  if (!file_exists($this->getAbsolutePath($newIdentifier))) {
943  throw new FileOperationErrorException(
944  sprintf('File "%1$s" was not found (should have been copied/moved from "%2$s").', $newIdentifier, $oldIdentifier),
945  1330119453
946  );
947  }
948  $identifierMap[$oldIdentifier] = $newIdentifier;
949  }
950  return $identifierMap;
951  }
952 
961  public function moveFolderWithinStorage(string $sourceFolderIdentifier, string $targetFolderIdentifier, string $newFolderName): array
962  {
963  $sourcePath = $this->getAbsolutePath($sourceFolderIdentifier);
964  $relativeTargetPath = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
965  $targetPath = $this->getAbsolutePath($relativeTargetPath);
966  // get all files and folders we are going to move, to have a map for updating later.
967  $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, true);
968  $result = rename($sourcePath, $targetPath);
969  if ($result === false) {
970  throw new \RuntimeException('Moving folder ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320711817);
971  }
972  // Create a mapping from old to new identifiers
973  return $this->createIdentifierMap($filesAndFolders, $sourceFolderIdentifier, $relativeTargetPath);
974  }
975 
984  public function copyFolderWithinStorage(string $sourceFolderIdentifier, string $targetFolderIdentifier, string $newFolderName): bool
985  {
986  // This target folder path already includes the topmost level, i.e. the folder this method knows as $folderToCopy.
987  // We can thus rely on this folder being present and just create the subfolder we want to copy to.
988  $newFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
989  $sourceFolderPath = $this->getAbsolutePath($sourceFolderIdentifier);
990  $targetFolderPath = $this->getAbsolutePath($newFolderIdentifier);
991 
992  mkdir($targetFolderPath);
993  $iterator = new \RecursiveIteratorIterator(
994  new \RecursiveDirectoryIterator($sourceFolderPath),
995  \RecursiveIteratorIterator::SELF_FIRST,
996  \RecursiveIteratorIterator::CATCH_GET_CHILD
997  );
998  // Rewind the iterator as this is important for some systems e.g. Windows
999  $iterator->rewind();
1000  while ($iterator->valid()) {
1002  $current = $iterator->current();
1003  $fileName = $current->getFilename();
1004  $itemSubPath = GeneralUtility::fixWindowsFilePath((string)$iterator->getSubPathname());
1005  if ($current->isDir() && !($fileName === '..' || $fileName === '.')) {
1006  ‪GeneralUtility::mkdir($targetFolderPath . '/' . $itemSubPath);
1007  } elseif ($current->isFile()) {
1008  $copySourcePath = $sourceFolderPath . '/' . $itemSubPath;
1009  $copyTargetPath = $targetFolderPath . '/' . $itemSubPath;
1010  $result = copy($copySourcePath, $copyTargetPath);
1011  if ($result === false) {
1012  // rollback
1013  ‪GeneralUtility::rmdir($targetFolderIdentifier, true);
1014  throw new FileOperationErrorException(
1015  'Copying resource "' . $copySourcePath . '" to "' . $copyTargetPath . '" failed.',
1016  1330119452
1017  );
1018  }
1019  }
1020  $iterator->next();
1021  }
1022  ‪GeneralUtility::fixPermissions($targetFolderPath, true);
1023  return true;
1024  }
1025 
1033  public function renameFile(string $fileIdentifier, string $newName): string
1034  {
1035  // Makes sure the Path given as parameter is valid
1036  $newName = $this->sanitizeFileName($newName);
1037  $newIdentifier = rtrim(GeneralUtility::fixWindowsFilePath(‪PathUtility::dirname($fileIdentifier)), '/') . '/' . $newName;
1038  $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
1039  // The target should not exist already
1040  if ($this->fileExists($newIdentifier)) {
1041  throw new ExistingTargetFileNameException(
1042  'The target file "' . $newIdentifier . '" already exists.',
1043  1320291063
1044  );
1045  }
1046  $sourcePath = $this->getAbsolutePath($fileIdentifier);
1047  $targetPath = $this->getAbsolutePath($newIdentifier);
1048  $result = rename($sourcePath, $targetPath);
1049  if ($result === false) {
1050  throw new \RuntimeException('Renaming file ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320375115);
1051  }
1052  return $newIdentifier;
1053  }
1054 
1063  public function renameFolder(string $folderIdentifier, string $newName): array
1064  {
1065  $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
1066  $newName = $this->sanitizeFileName($newName);
1067 
1068  $newIdentifier = ‪PathUtility::dirname($folderIdentifier) . '/' . $newName;
1069  $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier($newIdentifier);
1070 
1071  $sourcePath = $this->getAbsolutePath($folderIdentifier);
1072  $targetPath = $this->getAbsolutePath($newIdentifier);
1073  // get all files and folders we are going to move, to have a map for updating later.
1074  $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, true);
1075  $result = rename($sourcePath, $targetPath);
1076  if ($result === false) {
1077  throw new \RuntimeException(sprintf('Renaming folder "%1$s" to "%2$s" failed."', $sourcePath, $targetPath), 1320375116);
1078  }
1079  try {
1080  // Create a mapping from old to new identifiers
1081  $identifierMap = $this->createIdentifierMap($filesAndFolders, $folderIdentifier, $newIdentifier);
1082  } catch (\Exception $e) {
1083  rename($targetPath, $sourcePath);
1084  throw new \RuntimeException(
1085  sprintf(
1086  'Creating filename mapping after renaming "%1$s" to "%2$s" failed. Reverted rename operation.\\n\\nOriginal error: %3$s"',
1087  $sourcePath,
1088  $targetPath,
1089  $e->getMessage()
1090  ),
1091  1334160746
1092  );
1093  }
1094  return $identifierMap;
1095  }
1096 
1104  public function deleteFile(string $fileIdentifier): bool
1105  {
1106  $filePath = $this->getAbsolutePath($fileIdentifier);
1107  $result = unlink($filePath);
1108 
1109  if ($result === false) {
1110  throw new \RuntimeException('Deletion of file ' . $fileIdentifier . ' failed.', 1320855304);
1111  }
1112  return true;
1113  }
1114 
1120  public function deleteFolder(string $folderIdentifier, bool $deleteRecursively = false): bool
1121  {
1122  $folderPath = $this->getAbsolutePath($folderIdentifier);
1123  $recycleDirectory = $this->getRecycleDirectory($folderPath);
1124  if (!empty($recycleDirectory) && $folderPath !== $recycleDirectory) {
1125  $result = $this->recycleFileOrFolder($folderPath, $recycleDirectory);
1126  } else {
1127  $result = ‪GeneralUtility::rmdir($folderPath, $deleteRecursively);
1128  }
1129  if ($result === false) {
1130  throw new FileOperationErrorException(
1131  'Deleting folder "' . $folderIdentifier . '" failed.',
1132  1330119451
1133  );
1134  }
1135  return $result;
1136  }
1137 
1144  public function isFolderEmpty(string $folderIdentifier): bool
1145  {
1146  $path = $this->getAbsolutePath($folderIdentifier);
1147  $dirHandle = opendir($path);
1148  if ($dirHandle === false) {
1149  return true;
1150  }
1151  while ($entry = readdir($dirHandle)) {
1152  if ($entry !== '.' && $entry !== '..') {
1153  closedir($dirHandle);
1154  return false;
1155  }
1156  }
1157  closedir($dirHandle);
1158  return true;
1159  }
1160 
1171  public function getFileForLocalProcessing(string $fileIdentifier, bool $writable = true): string
1172  {
1173  if ($writable === false) {
1174  return $this->getAbsolutePath($fileIdentifier);
1175  }
1176  return $this->copyFileToTemporaryPath($fileIdentifier);
1177  }
1178 
1185  public function getPermissions(string ‪$identifier): array
1186  {
1187  $path = $this->getAbsolutePath(‪$identifier);
1188  $permissionBits = fileperms($path);
1189  if ($permissionBits === false) {
1190  throw new ResourcePermissionsUnavailableException('Error while fetching permissions for ' . $path, 1319455097);
1191  }
1192  return [
1193  'r' => is_readable($path),
1194  'w' => is_writable($path),
1195  ];
1196  }
1197 
1207  public function isWithin(string $folderIdentifier, string ‪$identifier): bool
1208  {
1209  $folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier);
1210  $entryIdentifier = $this->canonicalizeAndCheckFileIdentifier(‪$identifier);
1211  if ($folderIdentifier === $entryIdentifier) {
1212  return true;
1213  }
1214  // File identifier canonicalization will not modify a single slash so
1215  // we must not append another slash in that case.
1216  if ($folderIdentifier !== '/') {
1217  $folderIdentifier .= '/';
1218  }
1219  return str_starts_with($entryIdentifier, $folderIdentifier);
1220  }
1221 
1229  public function createFile(string $fileName, string $parentFolderIdentifier): string
1230  {
1231  $fileName = $this->sanitizeFileName(ltrim($fileName, '/'));
1232  $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
1233  $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
1234  $parentFolderIdentifier . $fileName
1235  );
1236  $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
1237  $result = touch($absoluteFilePath);
1238  ‪GeneralUtility::fixPermissions($absoluteFilePath);
1239  clearstatcache();
1240  if ($result !== true) {
1241  throw new \RuntimeException('Creating file ' . $fileIdentifier . ' failed.', 1320569854);
1242  }
1243  return $fileIdentifier;
1244  }
1245 
1255  public function getFileContents(string $fileIdentifier): string
1256  {
1257  $filePath = $this->getAbsolutePath($fileIdentifier);
1258  return is_readable($filePath) ? (string)file_get_contents($filePath) : '';
1259  }
1260 
1268  public function setFileContents(string $fileIdentifier, string $contents): int
1269  {
1270  $filePath = $this->getAbsolutePath($fileIdentifier);
1271  $result = file_put_contents($filePath, $contents);
1272 
1273  // Make sure later calls to filesize() etc. return correct values.
1274  clearstatcache(true, $filePath);
1275 
1276  if ($result === false) {
1277  throw new \RuntimeException('Setting contents of file "' . $fileIdentifier . '" failed.', 1325419305);
1278  }
1279  return $result;
1280  }
1281 
1286  public function getRole(string $folderIdentifier): string
1287  {
1288  $name = ‪PathUtility::basename($folderIdentifier);
1289  return $this->mappingFolderNameToRole[$name] ?? ‪FolderInterface::ROLE_DEFAULT;
1290  }
1291 
1298  public function dumpFileContents(string ‪$identifier): void
1299  {
1300  readfile($this->getAbsolutePath($this->canonicalizeAndCheckFileIdentifier(‪$identifier)));
1301  }
1302 
1306  public function streamFile(string ‪$identifier, array $properties): ResponseInterface
1307  {
1308  $fileInfo = $this->getFileInfoByIdentifier(‪$identifier, ['name', 'mimetype', 'mtime', 'size']);
1309  $downloadName = $properties['filename_overwrite'] ?? $fileInfo['name'] ?? '';
1310  $mimeType = $properties['mimetype_overwrite'] ?? $fileInfo['mimetype'] ?? '';
1311  $contentDisposition = ($properties['as_download'] ?? false) ? 'attachment' : 'inline';
1312 
1313  $filePath = $this->getAbsolutePath($this->canonicalizeAndCheckFileIdentifier(‪$identifier));
1314 
1315  return new Response(
1316  new SelfEmittableLazyOpenStream($filePath),
1317  200,
1318  [
1319  'Content-Disposition' => $contentDisposition . '; filename="' . $downloadName . '"',
1320  'Content-Type' => $mimeType,
1321  'Content-Length' => (string)$fileInfo['size'],
1322  'Last-Modified' => gmdate('D, d M Y H:i:s', $fileInfo['mtime']) . ' GMT',
1323  // Cache-Control header is needed here to solve an issue with browser IE8 and lower
1324  // See for more information: http://support.microsoft.com/kb/323308
1325  'Cache-Control' => '',
1326  ]
1327  );
1328  }
1329 
1334  protected function getRecycleDirectory(string $path): string
1335  {
1336  $recyclerSubdirectory = array_search(‪FolderInterface::ROLE_RECYCLER, $this->mappingFolderNameToRole, true);
1337  if ($recyclerSubdirectory === false) {
1338  return '';
1339  }
1340  $rootDirectory = rtrim($this->getAbsolutePath($this->getRootLevelFolder()), '/');
1341  $searchDirectory = ‪PathUtility::dirname($path);
1342  // Check if file or folder to be deleted is inside a recycler directory
1343  if ($this->getRole($searchDirectory) === ‪FolderInterface::ROLE_RECYCLER) {
1344  $searchDirectory = ‪PathUtility::dirname($searchDirectory);
1345  // Check if file or folder to be deleted is inside the root recycler
1346  if ($searchDirectory == $rootDirectory) {
1347  return '';
1348  }
1349  $searchDirectory = ‪PathUtility::dirname($searchDirectory);
1350  }
1351  // Search for the closest recycler directory
1352  while ($searchDirectory) {
1353  $recycleDirectory = $searchDirectory . '/' . $recyclerSubdirectory;
1354  if (is_dir($recycleDirectory)) {
1355  return $recycleDirectory;
1356  }
1357  if ($searchDirectory === $rootDirectory) {
1358  return '';
1359  }
1360  $searchDirectory = ‪PathUtility::dirname($searchDirectory);
1361  }
1362 
1363  return '';
1364  }
1365 }
‪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:24
‪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:26
‪$dir
‪$dir
Definition: validateRstFiles.php:257
‪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:24
‪TYPO3\CMS\Core\Resource\Capabilities\CAPABILITY_BROWSABLE
‪const CAPABILITY_BROWSABLE
Definition: Capabilities.php:27
‪TYPO3\CMS\Core\Utility\GeneralUtility\fixPermissions
‪static mixed fixPermissions($path, $recursive=false)
Definition: GeneralUtility.php:1479
‪TYPO3\CMS\Core\Utility\GeneralUtility\mkdir_deep
‪static mkdir_deep($directory)
Definition: GeneralUtility.php:1638
‪TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException
Definition: FolderDoesNotExistException.php:22
‪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\rmdir
‪static bool rmdir($path, $removeNonEmpty=false)
Definition: GeneralUtility.php:1691
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:51
‪TYPO3\CMS\Core\Http\SelfEmittableLazyOpenStream
Definition: SelfEmittableLazyOpenStream.php:32
‪TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException
Definition: FileOperationErrorException.php:22
‪TYPO3\CMS\Core\Utility\GeneralUtility\mkdir
‪static bool mkdir($newFolder)
Definition: GeneralUtility.php:1621
‪TYPO3\CMS\Core\Resource\Exception\InvalidPathException
Definition: InvalidPathException.php:24
‪TYPO3\CMS\Core\Resource\Exception\InvalidFileNameException
Definition: InvalidFileNameException.php:24
‪TYPO3\CMS\Core\Utility\PathUtility\pathinfo
‪static string string[] pathinfo(string $path, int $options=PATHINFO_ALL)
Definition: PathUtility.php:270
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37