[BUGFIX] Remove upper limits of imagewidth & imageheight of tt_content
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Resource / ResourceStorage.php
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
16 namespace TYPO3\CMS\Core\Resource;
17
18 use Psr\EventDispatcher\EventDispatcherInterface;
19 use Psr\Http\Message\ResponseInterface;
20 use TYPO3\CMS\Core\Core\Environment;
21 use TYPO3\CMS\Core\Database\ConnectionPool;
22 use TYPO3\CMS\Core\Http\FalDumpFileContentsDecoratorStream;
23 use TYPO3\CMS\Core\Http\Response;
24 use TYPO3\CMS\Core\Log\LogManager;
25 use TYPO3\CMS\Core\Registry;
26 use TYPO3\CMS\Core\Resource\Driver\DriverInterface;
27 use TYPO3\CMS\Core\Resource\Driver\StreamableDriverInterface;
28 use TYPO3\CMS\Core\Resource\Event\AfterFileAddedEvent;
29 use TYPO3\CMS\Core\Resource\Event\AfterFileContentsSetEvent;
30 use TYPO3\CMS\Core\Resource\Event\AfterFileCopiedEvent;
31 use TYPO3\CMS\Core\Resource\Event\AfterFileCreatedEvent;
32 use TYPO3\CMS\Core\Resource\Event\AfterFileDeletedEvent;
33 use TYPO3\CMS\Core\Resource\Event\AfterFileMovedEvent;
34 use TYPO3\CMS\Core\Resource\Event\AfterFileRenamedEvent;
35 use TYPO3\CMS\Core\Resource\Event\AfterFileReplacedEvent;
36 use TYPO3\CMS\Core\Resource\Event\AfterFolderAddedEvent;
37 use TYPO3\CMS\Core\Resource\Event\AfterFolderCopiedEvent;
38 use TYPO3\CMS\Core\Resource\Event\AfterFolderDeletedEvent;
39 use TYPO3\CMS\Core\Resource\Event\AfterFolderMovedEvent;
40 use TYPO3\CMS\Core\Resource\Event\AfterFolderRenamedEvent;
41 use TYPO3\CMS\Core\Resource\Event\BeforeFileAddedEvent;
42 use TYPO3\CMS\Core\Resource\Event\BeforeFileContentsSetEvent;
43 use TYPO3\CMS\Core\Resource\Event\BeforeFileCopiedEvent;
44 use TYPO3\CMS\Core\Resource\Event\BeforeFileCreatedEvent;
45 use TYPO3\CMS\Core\Resource\Event\BeforeFileDeletedEvent;
46 use TYPO3\CMS\Core\Resource\Event\BeforeFileMovedEvent;
47 use TYPO3\CMS\Core\Resource\Event\BeforeFileRenamedEvent;
48 use TYPO3\CMS\Core\Resource\Event\BeforeFileReplacedEvent;
49 use TYPO3\CMS\Core\Resource\Event\BeforeFolderAddedEvent;
50 use TYPO3\CMS\Core\Resource\Event\BeforeFolderCopiedEvent;
51 use TYPO3\CMS\Core\Resource\Event\BeforeFolderDeletedEvent;
52 use TYPO3\CMS\Core\Resource\Event\BeforeFolderMovedEvent;
53 use TYPO3\CMS\Core\Resource\Event\BeforeFolderRenamedEvent;
54 use TYPO3\CMS\Core\Resource\Event\GeneratePublicUrlForResourceEvent;
55 use TYPO3\CMS\Core\Resource\Event\SanitizeFileNameEvent;
56 use TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException;
57 use TYPO3\CMS\Core\Resource\Exception\ExistingTargetFolderException;
58 use TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException;
59 use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
60 use TYPO3\CMS\Core\Resource\Exception\IllegalFileExtensionException;
61 use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException;
62 use TYPO3\CMS\Core\Resource\Exception\InsufficientFileReadPermissionsException;
63 use TYPO3\CMS\Core\Resource\Exception\InsufficientFileWritePermissionsException;
64 use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
65 use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderWritePermissionsException;
66 use TYPO3\CMS\Core\Resource\Exception\InsufficientUserPermissionsException;
67 use TYPO3\CMS\Core\Resource\Exception\InvalidConfigurationException;
68 use TYPO3\CMS\Core\Resource\Exception\InvalidHashException;
69 use TYPO3\CMS\Core\Resource\Exception\InvalidTargetFolderException;
70 use TYPO3\CMS\Core\Resource\Exception\ResourcePermissionsUnavailableException;
71 use TYPO3\CMS\Core\Resource\Exception\UploadException;
72 use TYPO3\CMS\Core\Resource\Exception\UploadSizeException;
73 use TYPO3\CMS\Core\Resource\Index\FileIndexRepository;
74 use TYPO3\CMS\Core\Resource\Index\Indexer;
75 use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
76 use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
77 use TYPO3\CMS\Core\Resource\Search\Result\DriverFilteredSearchResult;
78 use TYPO3\CMS\Core\Resource\Search\Result\EmptyFileSearchResult;
79 use TYPO3\CMS\Core\Resource\Search\Result\FileSearchResult;
80 use TYPO3\CMS\Core\Resource\Search\Result\FileSearchResultInterface;
81 use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
82 use TYPO3\CMS\Core\Resource\Service\FileProcessingService;
83 use TYPO3\CMS\Core\Service\FlexFormService;
84 use TYPO3\CMS\Core\Utility\Exception\NotImplementedMethodException;
85 use TYPO3\CMS\Core\Utility\GeneralUtility;
86 use TYPO3\CMS\Core\Utility\PathUtility;
87 use TYPO3\CMS\Core\Utility\StringUtility;
88
89 /**
90 * A "mount point" inside the TYPO3 file handling.
91 *
92 * A "storage" object handles
93 * - abstraction to the driver
94 * - permissions (from the driver, and from the user, + capabilities)
95 * - an entry point for files, folders, and for most other operations
96 *
97 * == Driver entry point
98 * The driver itself, that does the actual work on the file system,
99 * is inside the storage but completely shadowed by
100 * the storage, as the storage also handles the abstraction to the
101 * driver
102 *
103 * The storage can be on the local system, but can also be on a remote
104 * system. The combination of driver + configurable capabilities (storage
105 * is read-only e.g.) allows for flexible uses.
106 *
107 *
108 * == Permission system
109 * As all requests have to run through the storage, the storage knows about the
110 * permissions of a BE/FE user, the file permissions / limitations of the driver
111 * and has some configurable capabilities.
112 * Additionally, a BE user can use "filemounts" (known from previous installations)
113 * to limit his/her work-zone to only a subset (identifier and its subfolders/subfolders)
114 * of the user itself.
115 *
116 * Check 1: "User Permissions" [is the user allowed to write a file) [is the user allowed to write a file]
117 * Check 2: "File Mounts" of the User (act as subsets / filters to the identifiers) [is the user allowed to do something in this folder?]
118 * Check 3: "Capabilities" of Storage (then: of Driver) [is the storage/driver writable?]
119 * Check 4: "File permissions" of the Driver [is the folder writable?]
120 */
121 class ResourceStorage implements ResourceStorageInterface
122 {
123 /**
124 * The storage driver instance belonging to this storage.
125 *
126 * @var Driver\DriverInterface
127 */
128 protected $driver;
129
130 /**
131 * The database record for this storage
132 *
133 * @var array
134 */
135 protected $storageRecord;
136
137 /**
138 * The configuration belonging to this storage (decoded from the configuration field).
139 *
140 * @var array
141 */
142 protected $configuration;
143
144 /**
145 * @var Service\FileProcessingService
146 */
147 protected $fileProcessingService;
148
149 /**
150 * Whether to check if file or folder is in user mounts
151 * and the action is allowed for a user
152 * Default is FALSE so that resources are accessible for
153 * front end rendering or admins.
154 *
155 * @var bool
156 */
157 protected $evaluatePermissions = false;
158
159 /**
160 * User filemounts, added as an array, and used as filters
161 *
162 * @var array
163 */
164 protected $fileMounts = [];
165
166 /**
167 * The file permissions of the user (and their group) merged together and
168 * available as an array
169 *
170 * @var array
171 */
172 protected $userPermissions = [];
173
174 /**
175 * The capabilities of this storage as defined in the storage record.
176 * Also see the CAPABILITY_* constants below
177 *
178 * @var int
179 */
180 protected $capabilities;
181
182 /**
183 * @var EventDispatcherInterface
184 */
185 protected $eventDispatcher;
186
187 /**
188 * @var Folder
189 */
190 protected $processingFolder;
191
192 /**
193 * All processing folders of this storage used in any storage
194 *
195 * @var Folder[]
196 */
197 protected $processingFolders;
198
199 /**
200 * whether this storage is online or offline in this request
201 *
202 * @var bool
203 */
204 protected $isOnline;
205
206 /**
207 * @var bool
208 */
209 protected $isDefault = false;
210
211 /**
212 * The filters used for the files and folder names.
213 *
214 * @var array
215 */
216 protected $fileAndFolderNameFilters = [];
217
218 /**
219 * Levels numbers used to generate hashed subfolders in the processing folder
220 */
221 const PROCESSING_FOLDER_LEVELS = 2;
222
223 /**
224 * Constructor for a storage object.
225 *
226 * @param Driver\DriverInterface $driver
227 * @param array $storageRecord The storage record row from the database
228 * @param EventDispatcherInterface|null $eventDispatcher
229 */
230 public function __construct(DriverInterface $driver, array $storageRecord, EventDispatcherInterface $eventDispatcher = null)
231 {
232 $this->storageRecord = $storageRecord;
233 $this->eventDispatcher = $eventDispatcher ?? GeneralUtility::getContainer()->get(EventDispatcherInterface::class);
234 if (is_array($storageRecord['configuration'] ?? null)) {
235 $this->configuration = $storageRecord['configuration'];
236 } elseif (!empty($storageRecord['configuration'] ?? '')) {
237 $this->configuration = GeneralUtility::makeInstance(FlexFormService::class)->convertFlexFormContentToArray($storageRecord['configuration']);
238 } else {
239 $this->configuration = [];
240 }
241 $this->capabilities =
242 ($this->storageRecord['is_browsable'] ?? null ? self::CAPABILITY_BROWSABLE : 0) |
243 ($this->storageRecord['is_public'] ?? null ? self::CAPABILITY_PUBLIC : 0) |
244 ($this->storageRecord['is_writable'] ?? null ? self::CAPABILITY_WRITABLE : 0) |
245 // Always let the driver decide whether to set this capability
246 self::CAPABILITY_HIERARCHICAL_IDENTIFIERS;
247
248 $this->driver = $driver;
249 $this->driver->setStorageUid($storageRecord['uid'] ?? null);
250 $this->driver->mergeConfigurationCapabilities($this->capabilities);
251 try {
252 $this->driver->processConfiguration();
253 } catch (InvalidConfigurationException $e) {
254 // Configuration error
255 $this->isOnline = false;
256
257 $message = sprintf(
258 'Failed initializing storage [%d] "%s", error: %s',
259 $this->getUid(),
260 $this->getName(),
261 $e->getMessage()
262 );
263
264 // create a dedicated logger instance because we need a logger in the constructor
265 GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class)->error($message);
266 }
267 $this->driver->initialize();
268 $this->capabilities = $this->driver->getCapabilities();
269
270 $this->isDefault = (isset($storageRecord['is_default']) && $storageRecord['is_default'] == 1);
271 $this->resetFileAndFolderNameFiltersToDefault();
272 }
273
274 /**
275 * Gets the configuration.
276 *
277 * @return array
278 */
279 public function getConfiguration()
280 {
281 return $this->configuration;
282 }
283
284 /**
285 * Sets the configuration.
286 *
287 * @param array $configuration
288 */
289 public function setConfiguration(array $configuration)
290 {
291 $this->configuration = $configuration;
292 }
293
294 /**
295 * Gets the storage record.
296 *
297 * @return array
298 */
299 public function getStorageRecord()
300 {
301 return $this->storageRecord;
302 }
303
304 /**
305 * Sets the storage that belongs to this storage.
306 *
307 * @param Driver\DriverInterface $driver
308 * @return ResourceStorage
309 */
310 public function setDriver(DriverInterface $driver)
311 {
312 $this->driver = $driver;
313 return $this;
314 }
315
316 /**
317 * Returns the driver object belonging to this storage.
318 *
319 * @return Driver\DriverInterface
320 */
321 protected function getDriver()
322 {
323 return $this->driver;
324 }
325
326 /**
327 * Returns the name of this storage.
328 *
329 * @return string
330 */
331 public function getName()
332 {
333 return $this->storageRecord['name'];
334 }
335
336 /**
337 * Returns the UID of this storage.
338 *
339 * @return int
340 */
341 public function getUid()
342 {
343 return (int)($this->storageRecord['uid'] ?? 0);
344 }
345
346 /**
347 * Tells whether there are children in this storage.
348 *
349 * @return bool
350 */
351 public function hasChildren()
352 {
353 return true;
354 }
355
356 /*********************************
357 * Capabilities
358 ********************************/
359 /**
360 * Returns the capabilities of this storage.
361 *
362 * @return int
363 * @see \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_BROWSABLE
364 * @see \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_PUBLIC
365 * @see \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_WRITABLE
366 * @see \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_HIERARCHICAL_IDENTIFIERS
367 */
368 public function getCapabilities()
369 {
370 return (int)$this->capabilities;
371 }
372
373 /**
374 * Returns TRUE if this storage has the given capability.
375 *
376 * @param int $capability A capability, as defined in a CAPABILITY_* constant
377 * @return bool
378 */
379 protected function hasCapability($capability)
380 {
381 return ($this->capabilities & $capability) == $capability;
382 }
383
384 /**
385 * Returns TRUE if this storage is publicly available. This is just a
386 * configuration option and does not mean that it really *is* public. OTOH
387 * a storage that is marked as not publicly available will trigger the file
388 * publishing mechanisms of TYPO3.
389 *
390 * @return bool
391 */
392 public function isPublic()
393 {
394 return $this->hasCapability(self::CAPABILITY_PUBLIC);
395 }
396
397 /**
398 * Returns TRUE if this storage is writable. This is determined by the
399 * driver and the storage configuration; user permissions are not taken into account.
400 *
401 * @return bool
402 */
403 public function isWritable()
404 {
405 return $this->hasCapability(self::CAPABILITY_WRITABLE);
406 }
407
408 /**
409 * Returns TRUE if this storage is browsable by a (backend) user of TYPO3.
410 *
411 * @return bool
412 */
413 public function isBrowsable()
414 {
415 return $this->isOnline() && $this->hasCapability(self::CAPABILITY_BROWSABLE);
416 }
417
418 /**
419 * Returns TRUE if this storage stores folder structure in file identifiers.
420 *
421 * @return bool
422 */
423 public function hasHierarchicalIdentifiers(): bool
424 {
425 return $this->hasCapability(self::CAPABILITY_HIERARCHICAL_IDENTIFIERS);
426 }
427
428 /**
429 * Search for files in a storage based on given restrictions
430 * and a possibly given folder.
431 *
432 * @param FileSearchDemand $searchDemand
433 * @param Folder|null $folder
434 * @param bool $useFilters Whether storage filters should be applied
435 * @return FileSearchResultInterface
436 */
437 public function searchFiles(FileSearchDemand $searchDemand, Folder $folder = null, bool $useFilters = true): FileSearchResultInterface
438 {
439 $folder = $folder ?? $this->getRootLevelFolder();
440 if (!$folder->checkActionPermission('read')) {
441 return new EmptyFileSearchResult();
442 }
443
444 return new DriverFilteredSearchResult(
445 new FileSearchResult(
446 $searchDemand->withFolder($folder)
447 ),
448 $this->driver,
449 $useFilters ? $this->getFileAndFolderNameFilters() : []
450 );
451 }
452
453 /**
454 * Returns TRUE if the identifiers used by this storage are case-sensitive.
455 *
456 * @return bool
457 */
458 public function usesCaseSensitiveIdentifiers()
459 {
460 return $this->driver->isCaseSensitiveFileSystem();
461 }
462
463 /**
464 * Returns TRUE if this storage is browsable by a (backend) user of TYPO3.
465 *
466 * @return bool
467 */
468 public function isOnline()
469 {
470 if ($this->isOnline === null) {
471 if ($this->getUid() === 0) {
472 $this->isOnline = true;
473 }
474 // the storage is not marked as online for a longer time
475 if ($this->storageRecord['is_online'] == 0) {
476 $this->isOnline = false;
477 }
478 if ($this->isOnline !== false) {
479 // all files are ALWAYS available in the frontend
480 if (TYPO3_MODE === 'FE') {
481 $this->isOnline = true;
482 } else {
483 // check if the storage is disabled temporary for now
484 $registryObject = GeneralUtility::makeInstance(Registry::class);
485 $offlineUntil = $registryObject->get('core', 'sys_file_storage-' . $this->getUid() . '-offline-until');
486 if ($offlineUntil && $offlineUntil > time()) {
487 $this->isOnline = false;
488 } else {
489 $this->isOnline = true;
490 }
491 }
492 }
493 }
494 return $this->isOnline;
495 }
496
497 /**
498 * Returns TRUE if auto extracting of metadata is enabled
499 *
500 * @return bool
501 */
502 public function autoExtractMetadataEnabled()
503 {
504 return !empty($this->storageRecord['auto_extract_metadata']);
505 }
506
507 /**
508 * Blows the "fuse" and marks the storage as offline.
509 *
510 * Can only be modified by an admin.
511 *
512 * Typically, this is only done if the configuration is wrong.
513 */
514 public function markAsPermanentlyOffline()
515 {
516 if ($this->getUid() > 0) {
517 // @todo: move this to the storage repository
518 GeneralUtility::makeInstance(ConnectionPool::class)
519 ->getConnectionForTable('sys_file_storage')
520 ->update(
521 'sys_file_storage',
522 ['is_online' => 0],
523 ['uid' => (int)$this->getUid()]
524 );
525 }
526 $this->storageRecord['is_online'] = 0;
527 $this->isOnline = false;
528 }
529
530 /**
531 * Marks this storage as offline for the next 5 minutes.
532 *
533 * Non-permanent: This typically happens for remote storages
534 * that are "flaky" and not available all the time.
535 */
536 public function markAsTemporaryOffline()
537 {
538 $registryObject = GeneralUtility::makeInstance(Registry::class);
539 $registryObject->set('core', 'sys_file_storage-' . $this->getUid() . '-offline-until', time() + 60 * 5);
540 $this->storageRecord['is_online'] = 0;
541 $this->isOnline = false;
542 }
543
544 /*********************************
545 * User Permissions / File Mounts
546 ********************************/
547 /**
548 * Adds a filemount as a "filter" for users to only work on a subset of a
549 * storage object
550 *
551 * @param string $folderIdentifier
552 * @param array $additionalData
553 *
554 * @throws Exception\FolderDoesNotExistException
555 */
556 public function addFileMount($folderIdentifier, $additionalData = [])
557 {
558 // check for the folder before we add it as a filemount
559 if ($this->driver->folderExists($folderIdentifier) === false) {
560 // if there is an error, this is important and should be handled
561 // as otherwise the user would see the whole storage without any restrictions for the filemounts
562 throw new FolderDoesNotExistException('Folder for file mount ' . $folderIdentifier . ' does not exist.', 1334427099);
563 }
564 $data = $this->driver->getFolderInfoByIdentifier($folderIdentifier);
565 $folderObject = $this->createFolderObject($data['identifier'], $data['name']);
566 // Use the canonical identifier instead of the user provided one!
567 $folderIdentifier = $folderObject->getIdentifier();
568 if (
569 !empty($this->fileMounts[$folderIdentifier])
570 && empty($this->fileMounts[$folderIdentifier]['read_only'])
571 && !empty($additionalData['read_only'])
572 ) {
573 // Do not overwrite a regular mount with a read only mount
574 return;
575 }
576 if (empty($additionalData)) {
577 $additionalData = [
578 'path' => $folderIdentifier,
579 'title' => $folderIdentifier,
580 'folder' => $folderObject
581 ];
582 } else {
583 $additionalData['folder'] = $folderObject;
584 if (!isset($additionalData['title'])) {
585 $additionalData['title'] = $folderIdentifier;
586 }
587 }
588 $this->fileMounts[$folderIdentifier] = $additionalData;
589 }
590
591 /**
592 * Returns all file mounts that are registered with this storage.
593 *
594 * @return array
595 */
596 public function getFileMounts()
597 {
598 return $this->fileMounts;
599 }
600
601 /**
602 * Checks if the given subject is within one of the registered user
603 * file mounts. If not, working with the file is not permitted for the user.
604 *
605 * @param ResourceInterface $subject file or folder
606 * @param bool $checkWriteAccess If true, it is not only checked if the subject is within the file mount but also whether it isn't a read only file mount
607 * @return bool
608 */
609 public function isWithinFileMountBoundaries($subject, $checkWriteAccess = false)
610 {
611 if (!$this->evaluatePermissions) {
612 return true;
613 }
614 $isWithinFileMount = false;
615 if (!$subject) {
616 $subject = $this->getRootLevelFolder();
617 }
618 $identifier = $subject->getIdentifier();
619
620 // Allow access to processing folder
621 if ($this->isWithinProcessingFolder($identifier)) {
622 $isWithinFileMount = true;
623 } else {
624 // Check if the identifier of the subject is within at
625 // least one of the file mounts
626 $writableFileMountAvailable = false;
627 foreach ($this->fileMounts as $fileMount) {
628 /** @var Folder $folder */
629 $folder = $fileMount['folder'];
630 if ($this->driver->isWithin($folder->getIdentifier(), $identifier)) {
631 $isWithinFileMount = true;
632 if (!$checkWriteAccess) {
633 break;
634 }
635 if (empty($fileMount['read_only'])) {
636 $writableFileMountAvailable = true;
637 break;
638 }
639 }
640 }
641 $isWithinFileMount = $checkWriteAccess ? $writableFileMountAvailable : $isWithinFileMount;
642 }
643 return $isWithinFileMount;
644 }
645
646 /**
647 * Sets whether the permissions to access or write
648 * into this storage should be checked or not.
649 *
650 * @param bool $evaluatePermissions
651 */
652 public function setEvaluatePermissions($evaluatePermissions)
653 {
654 $this->evaluatePermissions = (bool)$evaluatePermissions;
655 }
656
657 /**
658 * Gets whether the permissions to access or write
659 * into this storage should be checked or not.
660 *
661 * @return bool $evaluatePermissions
662 */
663 public function getEvaluatePermissions()
664 {
665 return $this->evaluatePermissions;
666 }
667
668 /**
669 * Sets the user permissions of the storage.
670 *
671 * @param array $userPermissions
672 */
673 public function setUserPermissions(array $userPermissions)
674 {
675 $this->userPermissions = $userPermissions;
676 }
677
678 /**
679 * Checks if the ACL settings allow for a certain action
680 * (is a user allowed to read a file or copy a folder).
681 *
682 * @param string $action
683 * @param string $type either File or Folder
684 * @return bool
685 */
686 public function checkUserActionPermission($action, $type)
687 {
688 if (!$this->evaluatePermissions) {
689 return true;
690 }
691
692 $allow = false;
693 if (!empty($this->userPermissions[strtolower($action) . ucfirst(strtolower($type))])) {
694 $allow = true;
695 }
696
697 return $allow;
698 }
699
700 /**
701 * Checks if a file operation (= action) is allowed on a
702 * File/Folder/Storage (= subject).
703 *
704 * This method, by design, does not throw exceptions or do logging.
705 * Besides the usage from other methods in this class, it is also used by
706 * the Filelist UI to check whether an action is allowed and whether action
707 * related UI elements should thus be shown (move icon, edit icon, etc.)
708 *
709 * @param string $action action, can be read, write, delete, editMeta
710 * @param FileInterface $file
711 * @return bool
712 */
713 public function checkFileActionPermission($action, FileInterface $file)
714 {
715 $isProcessedFile = $file instanceof ProcessedFile;
716 // Check 1: Allow editing meta data of a file if it is in mount boundaries of a writable file mount
717 if ($action === 'editMeta') {
718 return !$isProcessedFile && $this->isWithinFileMountBoundaries($file, true);
719 }
720 // Check 2: Does the user have permission to perform the action? e.g. "readFile"
721 if (!$isProcessedFile && $this->checkUserActionPermission($action, 'File') === false) {
722 return false;
723 }
724 // Check 3: No action allowed on files for denied file extensions
725 if (!$this->checkFileExtensionPermission($file->getName())) {
726 return false;
727 }
728 $isReadCheck = false;
729 if (in_array($action, ['read', 'copy', 'move', 'replace'], true)) {
730 $isReadCheck = true;
731 }
732 $isWriteCheck = false;
733 if (in_array($action, ['add', 'write', 'move', 'rename', 'replace', 'delete'], true)) {
734 $isWriteCheck = true;
735 }
736 // Check 4: Does the user have the right to perform the action?
737 // (= is he within the file mount borders)
738 if (!$isProcessedFile && !$this->isWithinFileMountBoundaries($file, $isWriteCheck)) {
739 return false;
740 }
741
742 $isMissing = false;
743 if (!$isProcessedFile && $file instanceof File) {
744 $isMissing = $file->isMissing();
745 }
746
747 if ($this->driver->fileExists($file->getIdentifier()) === false) {
748 $file->setMissing(true);
749 $isMissing = true;
750 }
751
752 // Check 5: Check the capabilities of the storage (and the driver)
753 if ($isWriteCheck && ($isMissing || !$this->isWritable())) {
754 return false;
755 }
756
757 // Check 6: "File permissions" of the driver (only when file isn't marked as missing)
758 if (!$isMissing) {
759 $filePermissions = $this->driver->getPermissions($file->getIdentifier());
760 if ($isReadCheck && !$filePermissions['r']) {
761 return false;
762 }
763 if ($isWriteCheck && !$filePermissions['w']) {
764 return false;
765 }
766 }
767 return true;
768 }
769
770 /**
771 * Checks if a folder operation (= action) is allowed on a Folder.
772 *
773 * This method, by design, does not throw exceptions or do logging.
774 * See the checkFileActionPermission() method above for the reasons.
775 *
776 * @param string $action
777 * @param Folder $folder
778 * @return bool
779 */
780 public function checkFolderActionPermission($action, Folder $folder = null)
781 {
782 // Check 1: Does the user have permission to perform the action? e.g. "writeFolder"
783 if ($this->checkUserActionPermission($action, 'Folder') === false) {
784 return false;
785 }
786
787 // If we do not have a folder here, we cannot do further checks
788 if ($folder === null) {
789 return true;
790 }
791
792 $isReadCheck = false;
793 if (in_array($action, ['read', 'copy'], true)) {
794 $isReadCheck = true;
795 }
796 $isWriteCheck = false;
797 if (in_array($action, ['add', 'move', 'write', 'delete', 'rename'], true)) {
798 $isWriteCheck = true;
799 }
800 // Check 2: Does the user has the right to perform the action?
801 // (= is he within the file mount borders)
802 if (!$this->isWithinFileMountBoundaries($folder, $isWriteCheck)) {
803 return false;
804 }
805 // Check 3: Check the capabilities of the storage (and the driver)
806 if ($isReadCheck && !$this->isBrowsable()) {
807 return false;
808 }
809 if ($isWriteCheck && !$this->isWritable()) {
810 return false;
811 }
812
813 // Check 4: "Folder permissions" of the driver
814 $folderPermissions = $this->driver->getPermissions($folder->getIdentifier());
815 if ($isReadCheck && !$folderPermissions['r']) {
816 return false;
817 }
818 if ($isWriteCheck && !$folderPermissions['w']) {
819 return false;
820 }
821 return true;
822 }
823
824 /**
825 * If the fileName is given, checks it against the
826 * TYPO3_CONF_VARS[BE][fileDenyPattern] + and if the file extension is allowed.
827 *
828 * @param string $fileName full filename
829 * @return bool TRUE if extension/filename is allowed
830 */
831 protected function checkFileExtensionPermission($fileName)
832 {
833 $fileName = $this->driver->sanitizeFileName($fileName);
834 return GeneralUtility::makeInstance(FileNameValidator::class)->isValid($fileName);
835 }
836
837 /**
838 * Assures read permission for given folder.
839 *
840 * @param Folder $folder If a folder is given, mountpoints are checked. If not only user folder read permissions are checked.
841 * @throws Exception\InsufficientFolderAccessPermissionsException
842 */
843 protected function assureFolderReadPermission(Folder $folder = null)
844 {
845 if (!$this->checkFolderActionPermission('read', $folder)) {
846 if ($folder === null) {
847 throw new InsufficientFolderAccessPermissionsException(
848 'You are not allowed to read folders',
849 1430657869
850 );
851 }
852 throw new InsufficientFolderAccessPermissionsException(
853 'You are not allowed to access the given folder: "' . $folder->getName() . '"',
854 1375955684
855 );
856 }
857 }
858
859 /**
860 * Assures delete permission for given folder.
861 *
862 * @param Folder $folder If a folder is given, mountpoints are checked. If not only user folder delete permissions are checked.
863 * @param bool $checkDeleteRecursively
864 * @throws Exception\InsufficientFolderAccessPermissionsException
865 * @throws Exception\InsufficientFolderWritePermissionsException
866 * @throws Exception\InsufficientUserPermissionsException
867 */
868 protected function assureFolderDeletePermission(Folder $folder, $checkDeleteRecursively)
869 {
870 // Check user permissions for recursive deletion if it is requested
871 if ($checkDeleteRecursively && !$this->checkUserActionPermission('recursivedelete', 'Folder')) {
872 throw new InsufficientUserPermissionsException('You are not allowed to delete folders recursively', 1377779423);
873 }
874 // Check user action permission
875 if (!$this->checkFolderActionPermission('delete', $folder)) {
876 throw new InsufficientFolderAccessPermissionsException(
877 'You are not allowed to delete the given folder: "' . $folder->getName() . '"',
878 1377779039
879 );
880 }
881 // Check if the user has write permissions to folders
882 // Would be good if we could check for actual write permissions in the containing folder
883 // but we cannot since we have no access to the containing folder of this file.
884 if (!$this->checkUserActionPermission('write', 'Folder')) {
885 throw new InsufficientFolderWritePermissionsException('Writing to folders is not allowed.', 1377779111);
886 }
887 }
888
889 /**
890 * Assures read permission for given file.
891 *
892 * @param FileInterface $file
893 * @throws Exception\InsufficientFileAccessPermissionsException
894 * @throws Exception\IllegalFileExtensionException
895 */
896 protected function assureFileReadPermission(FileInterface $file)
897 {
898 if (!$this->checkFileActionPermission('read', $file)) {
899 throw new InsufficientFileAccessPermissionsException(
900 'You are not allowed to access that file: "' . $file->getName() . '"',
901 1375955429
902 );
903 }
904 if (!$this->checkFileExtensionPermission($file->getName())) {
905 throw new IllegalFileExtensionException(
906 'You are not allowed to use that file extension. File: "' . $file->getName() . '"',
907 1375955430
908 );
909 }
910 }
911
912 /**
913 * Assures write permission for given file.
914 *
915 * @param FileInterface $file
916 * @throws Exception\IllegalFileExtensionException
917 * @throws Exception\InsufficientFileWritePermissionsException
918 * @throws Exception\InsufficientUserPermissionsException
919 */
920 protected function assureFileWritePermissions(FileInterface $file)
921 {
922 // Check if user is allowed to write the file and $file is writable
923 if (!$this->checkFileActionPermission('write', $file)) {
924 throw new InsufficientFileWritePermissionsException('Writing to file "' . $file->getIdentifier() . '" is not allowed.', 1330121088);
925 }
926 if (!$this->checkFileExtensionPermission($file->getName())) {
927 throw new IllegalFileExtensionException('You are not allowed to edit a file with extension "' . $file->getExtension() . '"', 1366711933);
928 }
929 }
930
931 /**
932 * Assure replace permission for given file.
933 *
934 * @param FileInterface $file
935 * @throws Exception\InsufficientFileWritePermissionsException
936 * @throws Exception\InsufficientFolderWritePermissionsException
937 */
938 protected function assureFileReplacePermissions(FileInterface $file)
939 {
940 // Check if user is allowed to replace the file and $file is writable
941 if (!$this->checkFileActionPermission('replace', $file)) {
942 throw new InsufficientFileWritePermissionsException('Replacing file "' . $file->getIdentifier() . '" is not allowed.', 1436899571);
943 }
944 // Check if parentFolder is writable for the user
945 if (!$this->checkFolderActionPermission('write', $file->getParentFolder())) {
946 throw new InsufficientFolderWritePermissionsException('You are not allowed to write to the target folder "' . $file->getIdentifier() . '"', 1436899572);
947 }
948 }
949
950 /**
951 * Assures delete permission for given file.
952 *
953 * @param FileInterface $file
954 * @throws Exception\IllegalFileExtensionException
955 * @throws Exception\InsufficientFileWritePermissionsException
956 * @throws Exception\InsufficientFolderWritePermissionsException
957 */
958 protected function assureFileDeletePermissions(FileInterface $file)
959 {
960 // Check for disallowed file extensions
961 if (!$this->checkFileExtensionPermission($file->getName())) {
962 throw new IllegalFileExtensionException('You are not allowed to delete a file with extension "' . $file->getExtension() . '"', 1377778916);
963 }
964 // Check further permissions if file is not a processed file
965 if (!$file instanceof ProcessedFile) {
966 // Check if user is allowed to delete the file and $file is writable
967 if (!$this->checkFileActionPermission('delete', $file)) {
968 throw new InsufficientFileWritePermissionsException('You are not allowed to delete the file "' . $file->getIdentifier() . '"', 1319550425);
969 }
970 // Check if the user has write permissions to folders
971 // Would be good if we could check for actual write permissions in the containing folder
972 // but we cannot since we have no access to the containing folder of this file.
973 if (!$this->checkUserActionPermission('write', 'Folder')) {
974 throw new InsufficientFolderWritePermissionsException('Writing to folders is not allowed.', 1377778702);
975 }
976 }
977 }
978
979 /**
980 * Checks if a file/user has the permission to be written to a Folder/Storage.
981 * If not, throws an exception.
982 *
983 * @param Folder $targetFolder The target folder where the file should be written
984 * @param string $targetFileName The file name which should be written into the storage
985 *
986 * @throws Exception\InsufficientFolderWritePermissionsException
987 * @throws Exception\IllegalFileExtensionException
988 * @throws Exception\InsufficientUserPermissionsException
989 */
990 protected function assureFileAddPermissions($targetFolder, $targetFileName)
991 {
992 // Check for a valid file extension
993 if (!$this->checkFileExtensionPermission($targetFileName)) {
994 throw new IllegalFileExtensionException('Extension of file name is not allowed in "' . $targetFileName . '"!', 1322120271);
995 }
996 // Makes sure the user is allowed to upload
997 if (!$this->checkUserActionPermission('add', 'File')) {
998 throw new InsufficientUserPermissionsException('You are not allowed to add files to this storage "' . $this->getUid() . '"', 1376992145);
999 }
1000 // Check if targetFolder is writable
1001 if (!$this->checkFolderActionPermission('write', $targetFolder)) {
1002 throw new InsufficientFolderWritePermissionsException('You are not allowed to write to the target folder "' . $targetFolder->getIdentifier() . '"', 1322120356);
1003 }
1004 }
1005
1006 /**
1007 * Checks if a file has the permission to be uploaded to a Folder/Storage.
1008 * If not, throws an exception.
1009 *
1010 * @param string $localFilePath the temporary file name from $_FILES['file1']['tmp_name']
1011 * @param Folder $targetFolder The target folder where the file should be uploaded
1012 * @param string $targetFileName the destination file name $_FILES['file1']['name']
1013 * @param int $uploadedFileSize
1014 *
1015 * @throws Exception\InsufficientFolderWritePermissionsException
1016 * @throws Exception\UploadException
1017 * @throws Exception\IllegalFileExtensionException
1018 * @throws Exception\UploadSizeException
1019 * @throws Exception\InsufficientUserPermissionsException
1020 */
1021 protected function assureFileUploadPermissions($localFilePath, $targetFolder, $targetFileName, $uploadedFileSize)
1022 {
1023 // Makes sure this is an uploaded file
1024 if (!is_uploaded_file($localFilePath)) {
1025 throw new UploadException('The upload has failed, no uploaded file found!', 1322110455);
1026 }
1027 // Max upload size (kb) for files.
1028 $maxUploadFileSize = GeneralUtility::getMaxUploadFileSize() * 1024;
1029 if ($maxUploadFileSize > 0 && $uploadedFileSize >= $maxUploadFileSize) {
1030 unlink($localFilePath);
1031 throw new UploadSizeException('The uploaded file exceeds the size-limit of ' . $maxUploadFileSize . ' bytes', 1322110041);
1032 }
1033 $this->assureFileAddPermissions($targetFolder, $targetFileName);
1034 }
1035
1036 /**
1037 * Checks for permissions to move a file.
1038 *
1039 * @throws \RuntimeException
1040 * @throws Exception\InsufficientFolderAccessPermissionsException
1041 * @throws Exception\InsufficientUserPermissionsException
1042 * @throws Exception\IllegalFileExtensionException
1043 * @param FileInterface $file
1044 * @param Folder $targetFolder
1045 * @param string $targetFileName
1046 */
1047 protected function assureFileMovePermissions(FileInterface $file, Folder $targetFolder, $targetFileName)
1048 {
1049 // Check if targetFolder is within this storage
1050 if ($this->getUid() !== $targetFolder->getStorage()->getUid()) {
1051 throw new \RuntimeException('The target folder is not in the same storage. Target folder given: "' . $targetFolder->getIdentifier() . '"', 1422553107);
1052 }
1053 // Check for a valid file extension
1054 if (!$this->checkFileExtensionPermission($targetFileName)) {
1055 throw new IllegalFileExtensionException('Extension of file name is not allowed in "' . $targetFileName . '"!', 1378243279);
1056 }
1057 // Check if user is allowed to move and $file is readable and writable
1058 if (!$file->getStorage()->checkFileActionPermission('move', $file)) {
1059 throw new InsufficientUserPermissionsException('You are not allowed to move files to storage "' . $this->getUid() . '"', 1319219349);
1060 }
1061 // Check if target folder is writable
1062 if (!$this->checkFolderActionPermission('write', $targetFolder)) {
1063 throw new InsufficientFolderAccessPermissionsException('You are not allowed to write to the target folder "' . $targetFolder->getIdentifier() . '"', 1319219350);
1064 }
1065 }
1066
1067 /**
1068 * Checks for permissions to rename a file.
1069 *
1070 * @param FileInterface $file
1071 * @param string $targetFileName
1072 * @throws Exception\InsufficientFileWritePermissionsException
1073 * @throws Exception\IllegalFileExtensionException
1074 * @throws Exception\InsufficientFileReadPermissionsException
1075 * @throws Exception\InsufficientUserPermissionsException
1076 */
1077 protected function assureFileRenamePermissions(FileInterface $file, $targetFileName)
1078 {
1079 // Check if file extension is allowed
1080 if (!$this->checkFileExtensionPermission($targetFileName) || !$this->checkFileExtensionPermission($file->getName())) {
1081 throw new IllegalFileExtensionException('You are not allowed to rename a file with this extension. File given: "' . $file->getName() . '"', 1371466663);
1082 }
1083 // Check if user is allowed to rename
1084 if (!$this->checkFileActionPermission('rename', $file)) {
1085 throw new InsufficientUserPermissionsException('You are not allowed to rename files. File given: "' . $file->getName() . '"', 1319219351);
1086 }
1087 // Check if the user is allowed to write to folders
1088 // Although it would be good to check, we cannot check here if the folder actually is writable
1089 // because we do not know in which folder the file resides.
1090 // So we rely on the driver to throw an exception in case the renaming failed.
1091 if (!$this->checkFolderActionPermission('write')) {
1092 throw new InsufficientFileWritePermissionsException('You are not allowed to write to folders', 1319219352);
1093 }
1094 }
1095
1096 /**
1097 * Check if a file has the permission to be copied on a File/Folder/Storage,
1098 * if not throw an exception
1099 *
1100 * @param FileInterface $file
1101 * @param Folder $targetFolder
1102 * @param string $targetFileName
1103 *
1104 * @throws Exception
1105 * @throws Exception\InsufficientFolderWritePermissionsException
1106 * @throws Exception\IllegalFileExtensionException
1107 * @throws Exception\InsufficientFileReadPermissionsException
1108 * @throws Exception\InsufficientUserPermissionsException
1109 */
1110 protected function assureFileCopyPermissions(FileInterface $file, Folder $targetFolder, $targetFileName)
1111 {
1112 // Check if targetFolder is within this storage, this should never happen
1113 if ($this->getUid() != $targetFolder->getStorage()->getUid()) {
1114 throw new Exception('The operation of the folder cannot be called by this storage "' . $this->getUid() . '"', 1319550405);
1115 }
1116 // Check if user is allowed to copy
1117 if (!$file->getStorage()->checkFileActionPermission('copy', $file)) {
1118 throw new InsufficientFileReadPermissionsException('You are not allowed to copy the file "' . $file->getIdentifier() . '"', 1319550426);
1119 }
1120 // Check if targetFolder is writable
1121 if (!$this->checkFolderActionPermission('write', $targetFolder)) {
1122 throw new InsufficientFolderWritePermissionsException('You are not allowed to write to the target folder "' . $targetFolder->getIdentifier() . '"', 1319550435);
1123 }
1124 // Check for a valid file extension
1125 if (!$this->checkFileExtensionPermission($targetFileName) || !$this->checkFileExtensionPermission($file->getName())) {
1126 throw new IllegalFileExtensionException('You are not allowed to copy a file of that type.', 1319553317);
1127 }
1128 }
1129
1130 /**
1131 * Check if a file has the permission to be copied on a File/Folder/Storage,
1132 * if not throw an exception
1133 *
1134 * @param FolderInterface $folderToCopy
1135 * @param FolderInterface $targetParentFolder
1136 *
1137 * @throws Exception
1138 * @throws Exception\InsufficientFolderWritePermissionsException
1139 * @throws Exception\IllegalFileExtensionException
1140 * @throws Exception\InsufficientFileReadPermissionsException
1141 * @throws Exception\InsufficientUserPermissionsException
1142 * @throws \RuntimeException
1143 */
1144 protected function assureFolderCopyPermissions(FolderInterface $folderToCopy, FolderInterface $targetParentFolder)
1145 {
1146 // Check if targetFolder is within this storage, this should never happen
1147 if ($this->getUid() !== $targetParentFolder->getStorage()->getUid()) {
1148 throw new Exception('The operation of the folder cannot be called by this storage "' . $this->getUid() . '"', 1377777624);
1149 }
1150 if (!$folderToCopy instanceof Folder) {
1151 throw new \RuntimeException('The folder "' . $folderToCopy->getIdentifier() . '" to copy is not of type folder.', 1384209020);
1152 }
1153 // Check if user is allowed to copy and the folder is readable
1154 if (!$folderToCopy->getStorage()->checkFolderActionPermission('copy', $folderToCopy)) {
1155 throw new InsufficientFileReadPermissionsException('You are not allowed to copy the folder "' . $folderToCopy->getIdentifier() . '"', 1377777629);
1156 }
1157 if (!$targetParentFolder instanceof Folder) {
1158 throw new \RuntimeException('The target folder "' . $targetParentFolder->getIdentifier() . '" is not of type folder.', 1384209021);
1159 }
1160 // Check if targetFolder is writable
1161 if (!$this->checkFolderActionPermission('write', $targetParentFolder)) {
1162 throw new InsufficientFolderWritePermissionsException('You are not allowed to write to the target folder "' . $targetParentFolder->getIdentifier() . '"', 1377777635);
1163 }
1164 }
1165
1166 /**
1167 * Check if a file has the permission to be copied on a File/Folder/Storage,
1168 * if not throw an exception
1169 *
1170 * @param FolderInterface $folderToMove
1171 * @param FolderInterface $targetParentFolder
1172 *
1173 * @throws \InvalidArgumentException
1174 * @throws Exception\InsufficientFolderWritePermissionsException
1175 * @throws Exception\IllegalFileExtensionException
1176 * @throws Exception\InsufficientFileReadPermissionsException
1177 * @throws Exception\InsufficientUserPermissionsException
1178 * @throws \RuntimeException
1179 */
1180 protected function assureFolderMovePermissions(FolderInterface $folderToMove, FolderInterface $targetParentFolder)
1181 {
1182 // Check if targetFolder is within this storage, this should never happen
1183 if ($this->getUid() !== $targetParentFolder->getStorage()->getUid()) {
1184 throw new \InvalidArgumentException('Cannot move a folder into a folder that does not belong to this storage.', 1325777289);
1185 }
1186 if (!$folderToMove instanceof Folder) {
1187 throw new \RuntimeException('The folder "' . $folderToMove->getIdentifier() . '" to move is not of type Folder.', 1384209022);
1188 }
1189 // Check if user is allowed to move and the folder is writable
1190 // In fact we would need to check if the parent folder of the folder to move is writable also
1191 // But as of now we cannot extract the parent folder from this folder
1192 if (!$folderToMove->getStorage()->checkFolderActionPermission('move', $folderToMove)) {
1193 throw new InsufficientFileReadPermissionsException('You are not allowed to copy the folder "' . $folderToMove->getIdentifier() . '"', 1377778045);
1194 }
1195 if (!$targetParentFolder instanceof Folder) {
1196 throw new \RuntimeException('The target folder "' . $targetParentFolder->getIdentifier() . '" is not of type Folder.', 1384209023);
1197 }
1198 // Check if targetFolder is writable
1199 if (!$this->checkFolderActionPermission('write', $targetParentFolder)) {
1200 throw new InsufficientFolderWritePermissionsException('You are not allowed to write to the target folder "' . $targetParentFolder->getIdentifier() . '"', 1377778049);
1201 }
1202 }
1203
1204 /**
1205 * Clean a fileName from not allowed characters
1206 *
1207 * @param string $fileName The name of the file to be add, If not set, the local file name is used
1208 * @param Folder $targetFolder The target folder where the file should be added
1209 *
1210 * @throws \InvalidArgumentException
1211 * @throws Exception\ExistingTargetFileNameException
1212 * @return string
1213 */
1214 public function sanitizeFileName($fileName, Folder $targetFolder = null)
1215 {
1216 $targetFolder = $targetFolder ?: $this->getDefaultFolder();
1217 $fileName = $this->driver->sanitizeFileName($fileName);
1218
1219 // The file name could be changed by an event listener
1220 $fileName = $this->eventDispatcher->dispatch(
1221 new SanitizeFileNameEvent($fileName, $targetFolder, $this, $this->driver)
1222 )->getFileName();
1223
1224 return $fileName;
1225 }
1226
1227 /********************
1228 * FILE ACTIONS
1229 ********************/
1230 /**
1231 * Moves a file from the local filesystem to this storage.
1232 *
1233 * @param string $localFilePath The file on the server's hard disk to add
1234 * @param Folder $targetFolder The target folder where the file should be added
1235 * @param string $targetFileName The name of the file to be add, If not set, the local file name is used
1236 * @param string $conflictMode a value of the DuplicationBehavior enumeration
1237 * @param bool $removeOriginal if set the original file will be removed after successful operation
1238 *
1239 * @throws \InvalidArgumentException
1240 * @throws Exception\ExistingTargetFileNameException
1241 * @return FileInterface
1242 */
1243 public function addFile($localFilePath, Folder $targetFolder, $targetFileName = '', $conflictMode = DuplicationBehavior::RENAME, $removeOriginal = true)
1244 {
1245 $localFilePath = PathUtility::getCanonicalPath($localFilePath);
1246 // File is not available locally NOR is it an uploaded file
1247 if (!is_uploaded_file($localFilePath) && !file_exists($localFilePath)) {
1248 throw new \InvalidArgumentException('File "' . $localFilePath . '" does not exist.', 1319552745);
1249 }
1250 $conflictMode = DuplicationBehavior::cast($conflictMode);
1251 $targetFileName = $this->sanitizeFileName($targetFileName ?: PathUtility::basename($localFilePath), $targetFolder);
1252
1253 $targetFileName = $this->eventDispatcher->dispatch(
1254 new BeforeFileAddedEvent($targetFileName, $localFilePath, $targetFolder, $this, $this->driver)
1255 )->getFileName();
1256
1257 $this->assureFileAddPermissions($targetFolder, $targetFileName);
1258
1259 $replaceExisting = false;
1260 if ($conflictMode->equals(DuplicationBehavior::CANCEL) && $this->driver->fileExistsInFolder($targetFileName, $targetFolder->getIdentifier())) {
1261 throw new ExistingTargetFileNameException('File "' . $targetFileName . '" already exists in folder ' . $targetFolder->getIdentifier(), 1322121068);
1262 }
1263 if ($conflictMode->equals(DuplicationBehavior::RENAME)) {
1264 $targetFileName = $this->getUniqueName($targetFolder, $targetFileName);
1265 } elseif ($conflictMode->equals(DuplicationBehavior::REPLACE) && $this->driver->fileExistsInFolder($targetFileName, $targetFolder->getIdentifier())) {
1266 $replaceExisting = true;
1267 }
1268
1269 $fileIdentifier = $this->driver->addFile($localFilePath, $targetFolder->getIdentifier(), $targetFileName, $removeOriginal);
1270 $file = $this->getFileByIdentifier($fileIdentifier);
1271
1272 if ($replaceExisting && $file instanceof File) {
1273 $this->getIndexer()->updateIndexEntry($file);
1274 }
1275
1276 $this->eventDispatcher->dispatch(
1277 new AfterFileAddedEvent($file, $targetFolder)
1278 );
1279 return $file;
1280 }
1281
1282 /**
1283 * Updates a processed file with a new file from the local filesystem.
1284 *
1285 * @param string $localFilePath
1286 * @param ProcessedFile $processedFile
1287 * @param Folder $processingFolder
1288 * @return FileInterface
1289 * @throws \InvalidArgumentException
1290 * @internal use only
1291 */
1292 public function updateProcessedFile($localFilePath, ProcessedFile $processedFile, Folder $processingFolder = null)
1293 {
1294 if (!file_exists($localFilePath)) {
1295 throw new \InvalidArgumentException('File "' . $localFilePath . '" does not exist.', 1319552746);
1296 }
1297 if ($processingFolder === null) {
1298 $processingFolder = $this->getProcessingFolder($processedFile->getOriginalFile());
1299 }
1300 $fileIdentifier = $this->driver->addFile($localFilePath, $processingFolder->getIdentifier(), $processedFile->getName());
1301 // @todo check if we have to update the processed file other then the identifier
1302 $processedFile->setIdentifier($fileIdentifier);
1303 return $processedFile;
1304 }
1305
1306 /**
1307 * Creates a (cryptographic) hash for a file.
1308 *
1309 * @param FileInterface $fileObject
1310 * @param string $hash
1311 * @throws \TYPO3\CMS\Core\Resource\Exception\InvalidHashException
1312 * @return string
1313 */
1314 public function hashFile(FileInterface $fileObject, $hash)
1315 {
1316 return $this->hashFileByIdentifier($fileObject->getIdentifier(), $hash);
1317 }
1318
1319 /**
1320 * Creates a (cryptographic) hash for a fileIdentifier.
1321 *
1322 * @param string $fileIdentifier
1323 * @param string $hash
1324 * @throws \TYPO3\CMS\Core\Resource\Exception\InvalidHashException
1325 * @return string
1326 */
1327 public function hashFileByIdentifier($fileIdentifier, $hash)
1328 {
1329 $hash = $this->driver->hash($fileIdentifier, $hash);
1330 if (!is_string($hash) || $hash === '') {
1331 throw new InvalidHashException('Hash has to be non-empty string.', 1551950301);
1332 }
1333 return $hash;
1334 }
1335
1336 /**
1337 * Hashes a file identifier, taking the case sensitivity of the file system
1338 * into account. This helps mitigating problems with case-insensitive
1339 * databases.
1340 *
1341 * @param string|FileInterface $file
1342 * @return string
1343 */
1344 public function hashFileIdentifier($file)
1345 {
1346 if (is_object($file) && $file instanceof FileInterface) {
1347 /** @var FileInterface $file */
1348 $file = $file->getIdentifier();
1349 }
1350 return $this->driver->hashIdentifier($file);
1351 }
1352
1353 /**
1354 * Returns a publicly accessible URL for a file.
1355 *
1356 * WARNING: Access to the file may be restricted by further means, e.g.
1357 * some web-based authentication. You have to take care of this yourself.
1358 *
1359 * @param ResourceInterface $resourceObject The file or folder object
1360 * @param bool $relativeToCurrentScript Determines whether the URL returned should be relative to the current script, in case it is relative at all (only for the LocalDriver)
1361 * @return string|null NULL if file is missing or deleted, the generated url otherwise
1362 */
1363 public function getPublicUrl(ResourceInterface $resourceObject, $relativeToCurrentScript = false)
1364 {
1365 $publicUrl = null;
1366 if ($this->isOnline()) {
1367 // Pre-process the public URL by an accordant event
1368 $event = new GeneratePublicUrlForResourceEvent($resourceObject, $this, $this->driver, $relativeToCurrentScript);
1369 $publicUrl = $this->eventDispatcher->dispatch($event)->getPublicUrl();
1370 if (
1371 $publicUrl === null
1372 && $resourceObject instanceof File
1373 && ($helper = OnlineMediaHelperRegistry::getInstance()->getOnlineMediaHelper($resourceObject)) !== false
1374 ) {
1375 $publicUrl = $helper->getPublicUrl($resourceObject, $relativeToCurrentScript);
1376 }
1377
1378 // If an event listener did not handle the URL generation, use the default way to determine public URL
1379 if ($publicUrl === null) {
1380 if ($this->hasCapability(self::CAPABILITY_PUBLIC)) {
1381 $publicUrl = $this->driver->getPublicUrl($resourceObject->getIdentifier());
1382 }
1383
1384 if ($publicUrl === null && $resourceObject instanceof FileInterface) {
1385 $queryParameterArray = ['eID' => 'dumpFile', 't' => ''];
1386 if ($resourceObject instanceof File) {
1387 $queryParameterArray['f'] = $resourceObject->getUid();
1388 $queryParameterArray['t'] = 'f';
1389 } elseif ($resourceObject instanceof ProcessedFile) {
1390 $queryParameterArray['p'] = $resourceObject->getUid();
1391 $queryParameterArray['t'] = 'p';
1392 }
1393
1394 $queryParameterArray['token'] = GeneralUtility::hmac(implode('|', $queryParameterArray), 'resourceStorageDumpFile');
1395 $publicUrl = GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'));
1396 $publicUrl .= '?' . http_build_query($queryParameterArray, '', '&', PHP_QUERY_RFC3986);
1397 }
1398
1399 // If requested, make the path relative to the current script in order to make it possible
1400 // to use the relative file
1401 if ($publicUrl !== null && $relativeToCurrentScript && !GeneralUtility::isValidUrl($publicUrl)) {
1402 $absolutePathToContainingFolder = PathUtility::dirname(Environment::getPublicPath() . '/' . $publicUrl);
1403 $pathPart = PathUtility::getRelativePathTo($absolutePathToContainingFolder);
1404 $filePart = substr(Environment::getPublicPath() . '/' . $publicUrl, strlen($absolutePathToContainingFolder) + 1);
1405 $publicUrl = $pathPart . $filePart;
1406 }
1407 }
1408 }
1409 return $publicUrl;
1410 }
1411
1412 /**
1413 * Passes a file to the File Processing Services and returns the resulting ProcessedFile object.
1414 *
1415 * @param FileInterface $fileObject The file object
1416 * @param string $context
1417 * @param array $configuration
1418 *
1419 * @return ProcessedFile
1420 * @throws \InvalidArgumentException
1421 */
1422 public function processFile(FileInterface $fileObject, $context, array $configuration)
1423 {
1424 if ($fileObject->getStorage() !== $this) {
1425 throw new \InvalidArgumentException('Cannot process files of foreign storage', 1353401835);
1426 }
1427 $processedFile = $this->getFileProcessingService()->processFile($fileObject, $this, $context, $configuration);
1428
1429 return $processedFile;
1430 }
1431
1432 /**
1433 * Copies a file from the storage for local processing.
1434 *
1435 * @param FileInterface $fileObject
1436 * @param bool $writable
1437 * @return string Path to local file (either original or copied to some temporary local location)
1438 */
1439 public function getFileForLocalProcessing(FileInterface $fileObject, $writable = true)
1440 {
1441 $filePath = $this->driver->getFileForLocalProcessing($fileObject->getIdentifier(), $writable);
1442 return $filePath;
1443 }
1444
1445 /**
1446 * Gets a file by identifier.
1447 *
1448 * @param string $identifier
1449 * @return FileInterface
1450 */
1451 public function getFile($identifier)
1452 {
1453 $file = $this->getFileByIdentifier($identifier);
1454 if (!$this->driver->fileExists($identifier)) {
1455 $file->setMissing(true);
1456 }
1457 return $file;
1458 }
1459
1460 /**
1461 * Gets a file object from storage by file identifier
1462 * If the file is outside of the process folder, it gets indexed and returned as file object afterwards
1463 * If the file is within processing folder, the file object will be directly returned
1464 *
1465 * @param string $fileIdentifier
1466 * @return File|ProcessedFile|null
1467 */
1468 public function getFileByIdentifier(string $fileIdentifier)
1469 {
1470 if (!$this->isWithinProcessingFolder($fileIdentifier)) {
1471 $fileData = $this->getFileIndexRepository()->findOneByStorageAndIdentifier($this, $fileIdentifier);
1472 if ($fileData === false) {
1473 return $this->getIndexer()->createIndexEntry($fileIdentifier);
1474 }
1475 return $this->getResourceFactoryInstance()->getFileObject($fileData['uid'], $fileData);
1476 }
1477 return $this->getProcessedFileRepository()->findByStorageAndIdentifier($this, $fileIdentifier);
1478 }
1479
1480 protected function getProcessedFileRepository(): ProcessedFileRepository
1481 {
1482 return GeneralUtility::makeInstance(ProcessedFileRepository::class);
1483 }
1484
1485 /**
1486 * Gets information about a file.
1487 *
1488 * @param FileInterface $fileObject
1489 * @return array
1490 * @internal
1491 */
1492 public function getFileInfo(FileInterface $fileObject)
1493 {
1494 return $this->getFileInfoByIdentifier($fileObject->getIdentifier());
1495 }
1496
1497 /**
1498 * Gets information about a file by its identifier.
1499 *
1500 * @param string $identifier
1501 * @param array $propertiesToExtract
1502 * @return array
1503 * @internal
1504 */
1505 public function getFileInfoByIdentifier($identifier, array $propertiesToExtract = [])
1506 {
1507 return $this->driver->getFileInfoByIdentifier($identifier, $propertiesToExtract);
1508 }
1509
1510 /**
1511 * Unsets the file and folder name filters, thus making this storage return unfiltered filelists.
1512 */
1513 public function unsetFileAndFolderNameFilters()
1514 {
1515 $this->fileAndFolderNameFilters = [];
1516 }
1517
1518 /**
1519 * Resets the file and folder name filters to the default values defined in the TYPO3 configuration.
1520 */
1521 public function resetFileAndFolderNameFiltersToDefault()
1522 {
1523 $this->fileAndFolderNameFilters = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['defaultFilterCallbacks'];
1524 }
1525
1526 /**
1527 * Returns the file and folder name filters used by this storage.
1528 *
1529 * @return array
1530 */
1531 public function getFileAndFolderNameFilters()
1532 {
1533 return $this->fileAndFolderNameFilters;
1534 }
1535
1536 /**
1537 * @param array $filters
1538 * @return $this
1539 */
1540 public function setFileAndFolderNameFilters(array $filters)
1541 {
1542 $this->fileAndFolderNameFilters = $filters;
1543 return $this;
1544 }
1545
1546 /**
1547 * @param callable $filter
1548 */
1549 public function addFileAndFolderNameFilter($filter)
1550 {
1551 $this->fileAndFolderNameFilters[] = $filter;
1552 }
1553
1554 /**
1555 * @param string $fileIdentifier
1556 *
1557 * @return string
1558 */
1559 public function getFolderIdentifierFromFileIdentifier($fileIdentifier)
1560 {
1561 return $this->driver->getParentFolderIdentifierOfIdentifier($fileIdentifier);
1562 }
1563
1564 /**
1565 * Get file from folder
1566 *
1567 * @param string $fileName
1568 * @param Folder $folder
1569 * @return File|ProcessedFile|null
1570 */
1571 public function getFileInFolder($fileName, Folder $folder)
1572 {
1573 $identifier = $this->driver->getFileInFolder($fileName, $folder->getIdentifier());
1574 return $this->getFileByIdentifier($identifier);
1575 }
1576
1577 /**
1578 * @param Folder $folder
1579 * @param int $start
1580 * @param int $maxNumberOfItems
1581 * @param bool $useFilters
1582 * @param bool $recursive
1583 * @param string $sort Property name used to sort the items.
1584 * Among them may be: '' (empty, no sorting), name,
1585 * fileext, size, tstamp and rw.
1586 * If a driver does not support the given property, it
1587 * should fall back to "name".
1588 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
1589 * @return File[]
1590 * @throws Exception\InsufficientFolderAccessPermissionsException
1591 */
1592 public function getFilesInFolder(Folder $folder, $start = 0, $maxNumberOfItems = 0, $useFilters = true, $recursive = false, $sort = '', $sortRev = false)
1593 {
1594 $this->assureFolderReadPermission($folder);
1595
1596 $rows = $this->getFileIndexRepository()->findByFolder($folder);
1597
1598 $filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
1599 $fileIdentifiers = array_values($this->driver->getFilesInFolder($folder->getIdentifier(), $start, $maxNumberOfItems, $recursive, $filters, $sort, $sortRev));
1600
1601 $items = [];
1602 foreach ($fileIdentifiers as $identifier) {
1603 if (isset($rows[$identifier])) {
1604 $fileObject = $this->getFileFactory()->getFileObject($rows[$identifier]['uid'], $rows[$identifier]);
1605 } else {
1606 $fileObject = $this->getFileByIdentifier($identifier);
1607 }
1608 if ($fileObject instanceof FileInterface) {
1609 $key = $fileObject->getName();
1610 while (isset($items[$key])) {
1611 $key .= 'z';
1612 }
1613 $items[$key] = $fileObject;
1614 }
1615 }
1616
1617 return $items;
1618 }
1619
1620 /**
1621 * @param string $folderIdentifier
1622 * @param bool $useFilters
1623 * @param bool $recursive
1624 * @return array
1625 */
1626 public function getFileIdentifiersInFolder($folderIdentifier, $useFilters = true, $recursive = false)
1627 {
1628 $filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
1629 return $this->driver->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filters);
1630 }
1631
1632 /**
1633 * @param Folder $folder
1634 * @param bool $useFilters
1635 * @param bool $recursive
1636 * @return int Number of files in folder
1637 * @throws Exception\InsufficientFolderAccessPermissionsException
1638 */
1639 public function countFilesInFolder(Folder $folder, $useFilters = true, $recursive = false)
1640 {
1641 $this->assureFolderReadPermission($folder);
1642 $filters = $useFilters ? $this->fileAndFolderNameFilters : [];
1643 return $this->driver->countFilesInFolder($folder->getIdentifier(), $recursive, $filters);
1644 }
1645
1646 /**
1647 * @param string $folderIdentifier
1648 * @param bool $useFilters
1649 * @param bool $recursive
1650 * @return array
1651 */
1652 public function getFolderIdentifiersInFolder($folderIdentifier, $useFilters = true, $recursive = false)
1653 {
1654 $filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
1655 return $this->driver->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $filters);
1656 }
1657
1658 /**
1659 * Returns TRUE if the specified file exists
1660 *
1661 * @param string $identifier
1662 * @return bool
1663 */
1664 public function hasFile($identifier)
1665 {
1666 // Allow if identifier is in processing folder
1667 if (!$this->isWithinProcessingFolder($identifier)) {
1668 $this->assureFolderReadPermission();
1669 }
1670 return $this->driver->fileExists($identifier);
1671 }
1672
1673 /**
1674 * Get all processing folders that live in this storage
1675 *
1676 * @return Folder[]
1677 */
1678 public function getProcessingFolders()
1679 {
1680 if ($this->processingFolders === null) {
1681 $this->processingFolders = [];
1682 $this->processingFolders[] = $this->getProcessingFolder();
1683 /** @var StorageRepository $storageRepository */
1684 $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
1685 $allStorages = $storageRepository->findAll();
1686 foreach ($allStorages as $storage) {
1687 // To circumvent the permission check of the folder, we use the factory to create it "manually" instead of directly using $storage->getProcessingFolder()
1688 // See #66695 for details
1689 [$storageUid, $processingFolderIdentifier] = array_pad(GeneralUtility::trimExplode(':', $storage->getStorageRecord()['processingfolder']), 2, null);
1690 if (empty($processingFolderIdentifier) || (int)$storageUid !== $this->getUid()) {
1691 continue;
1692 }
1693 $potentialProcessingFolder = $this->createFolderObject($processingFolderIdentifier, $processingFolderIdentifier);
1694 if ($potentialProcessingFolder->getStorage() === $this && $potentialProcessingFolder->getIdentifier() !== $this->getProcessingFolder()->getIdentifier()) {
1695 $this->processingFolders[] = $potentialProcessingFolder;
1696 }
1697 }
1698 }
1699
1700 return $this->processingFolders;
1701 }
1702
1703 /**
1704 * Returns TRUE if folder that is in current storage is set as
1705 * processing folder for one of the existing storages
1706 *
1707 * @param Folder $folder
1708 * @return bool
1709 */
1710 public function isProcessingFolder(Folder $folder)
1711 {
1712 $isProcessingFolder = false;
1713 foreach ($this->getProcessingFolders() as $processingFolder) {
1714 if ($folder->getCombinedIdentifier() === $processingFolder->getCombinedIdentifier()) {
1715 $isProcessingFolder = true;
1716 break;
1717 }
1718 }
1719 return $isProcessingFolder;
1720 }
1721
1722 /**
1723 * Checks if the queried file in the given folder exists
1724 *
1725 * @param string $fileName
1726 * @param Folder $folder
1727 * @return bool
1728 */
1729 public function hasFileInFolder($fileName, Folder $folder)
1730 {
1731 $this->assureFolderReadPermission($folder);
1732 return $this->driver->fileExistsInFolder($fileName, $folder->getIdentifier());
1733 }
1734
1735 /**
1736 * Get contents of a file object
1737 *
1738 * @param FileInterface $file
1739 *
1740 * @throws Exception\InsufficientFileReadPermissionsException
1741 * @return string
1742 */
1743 public function getFileContents($file)
1744 {
1745 $this->assureFileReadPermission($file);
1746 return $this->driver->getFileContents($file->getIdentifier());
1747 }
1748
1749 /**
1750 * Returns a PSR-7 Response which can be used to stream the requested file
1751 *
1752 * @param FileInterface $file
1753 * @param bool $asDownload If set Content-Disposition attachment is sent, inline otherwise
1754 * @param string $alternativeFilename the filename for the download (if $asDownload is set)
1755 * @param string $overrideMimeType If set this will be used as Content-Type header instead of the automatically detected mime type.
1756 * @return ResponseInterface
1757 */
1758 public function streamFile(
1759 FileInterface $file,
1760 bool $asDownload = false,
1761 string $alternativeFilename = null,
1762 string $overrideMimeType = null
1763 ): ResponseInterface {
1764 if (!$this->driver instanceof StreamableDriverInterface) {
1765 return $this->getPseudoStream($file, $asDownload, $alternativeFilename, $overrideMimeType);
1766 }
1767
1768 $properties = [
1769 'as_download' => $asDownload,
1770 'filename_overwrite' => $alternativeFilename,
1771 'mimetype_overwrite' => $overrideMimeType,
1772 ];
1773 return $this->driver->streamFile($file->getIdentifier(), $properties);
1774 }
1775
1776 /**
1777 * Wrap DriverInterface::dumpFileContents into a SelfEmittableStreamInterface
1778 *
1779 * @param FileInterface $file
1780 * @param bool $asDownload If set Content-Disposition attachment is sent, inline otherwise
1781 * @param string $alternativeFilename the filename for the download (if $asDownload is set)
1782 * @param string $overrideMimeType If set this will be used as Content-Type header instead of the automatically detected mime type.
1783 * @return ResponseInterface
1784 */
1785 protected function getPseudoStream(
1786 FileInterface $file,
1787 bool $asDownload = false,
1788 string $alternativeFilename = null,
1789 string $overrideMimeType = null
1790 ) {
1791 $downloadName = $alternativeFilename ?: $file->getName();
1792 $contentDisposition = $asDownload ? 'attachment' : 'inline';
1793
1794 $stream = new FalDumpFileContentsDecoratorStream($file->getIdentifier(), $this->driver, $file->getSize());
1795 $headers = [
1796 'Content-Disposition' => $contentDisposition . '; filename="' . $downloadName . '"',
1797 'Content-Type' => $overrideMimeType ?: $file->getMimeType(),
1798 'Content-Length' => (string)$file->getSize(),
1799 'Last-Modified' => gmdate('D, d M Y H:i:s', array_pop($this->driver->getFileInfoByIdentifier($file->getIdentifier(), ['mtime']))) . ' GMT',
1800 // Cache-Control header is needed here to solve an issue with browser IE8 and lower
1801 // See for more information: http://support.microsoft.com/kb/323308
1802 'Cache-Control' => '',
1803 ];
1804
1805 return new Response($stream, 200, $headers);
1806 }
1807
1808 /**
1809 * Set contents of a file object.
1810 *
1811 * @param AbstractFile $file
1812 * @param string $contents
1813 *
1814 * @throws \Exception|\RuntimeException
1815 * @throws Exception\InsufficientFileWritePermissionsException
1816 * @throws Exception\InsufficientUserPermissionsException
1817 * @return int The number of bytes written to the file
1818 */
1819 public function setFileContents(AbstractFile $file, $contents)
1820 {
1821 // Check if user is allowed to edit
1822 $this->assureFileWritePermissions($file);
1823 $this->eventDispatcher->dispatch(
1824 new BeforeFileContentsSetEvent($file, $contents)
1825 );
1826 // Call driver method to update the file and update file index entry afterwards
1827 $result = $this->driver->setFileContents($file->getIdentifier(), $contents);
1828 if ($file instanceof File) {
1829 $this->getIndexer()->updateIndexEntry($file);
1830 }
1831 $this->eventDispatcher->dispatch(
1832 new AfterFileContentsSetEvent($file, $contents)
1833 );
1834 return $result;
1835 }
1836
1837 /**
1838 * Creates a new file
1839 *
1840 * previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_newfile()
1841 *
1842 * @param string $fileName The name of the file to be created
1843 * @param Folder $targetFolderObject The target folder where the file should be created
1844 *
1845 * @throws Exception\IllegalFileExtensionException
1846 * @throws Exception\InsufficientFolderWritePermissionsException
1847 * @return FileInterface The file object
1848 */
1849 public function createFile($fileName, Folder $targetFolderObject)
1850 {
1851 $this->assureFileAddPermissions($targetFolderObject, $fileName);
1852 $this->eventDispatcher->dispatch(
1853 new BeforeFileCreatedEvent($fileName, $targetFolderObject)
1854 );
1855 $newFileIdentifier = $this->driver->createFile($fileName, $targetFolderObject->getIdentifier());
1856 $this->eventDispatcher->dispatch(
1857 new AfterFileCreatedEvent($newFileIdentifier, $targetFolderObject)
1858 );
1859 return $this->getFileByIdentifier($newFileIdentifier);
1860 }
1861
1862 /**
1863 * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::deleteFile()
1864 *
1865 * @param FileInterface $fileObject
1866 * @throws Exception\InsufficientFileAccessPermissionsException
1867 * @throws Exception\FileOperationErrorException
1868 * @return bool TRUE if deletion succeeded
1869 */
1870 public function deleteFile($fileObject)
1871 {
1872 $this->assureFileDeletePermissions($fileObject);
1873
1874 $this->eventDispatcher->dispatch(
1875 new BeforeFileDeletedEvent($fileObject)
1876 );
1877 $deleted = true;
1878
1879 if ($this->driver->fileExists($fileObject->getIdentifier())) {
1880 // Disable permission check to find nearest recycler and move file without errors
1881 $currentPermissions = $this->evaluatePermissions;
1882 $this->evaluatePermissions = false;
1883
1884 $recyclerFolder = $this->getNearestRecyclerFolder($fileObject);
1885 if ($recyclerFolder === null) {
1886 $result = $this->driver->deleteFile($fileObject->getIdentifier());
1887 } else {
1888 $result = $this->moveFile($fileObject, $recyclerFolder);
1889 $deleted = false;
1890 }
1891
1892 $this->evaluatePermissions = $currentPermissions;
1893
1894 if (!$result) {
1895 throw new FileOperationErrorException('Deleting the file "' . $fileObject->getIdentifier() . '\' failed.', 1329831691);
1896 }
1897 }
1898 // Mark the file object as deleted
1899 if ($deleted && $fileObject instanceof AbstractFile) {
1900 $fileObject->setDeleted();
1901 }
1902
1903 $this->eventDispatcher->dispatch(
1904 new AfterFileDeletedEvent($fileObject)
1905 );
1906
1907 return true;
1908 }
1909
1910 /**
1911 * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_copy()
1912 * copies a source file (from any location) in to the target
1913 * folder, the latter has to be part of this storage
1914 *
1915 * @param FileInterface $file
1916 * @param Folder $targetFolder
1917 * @param string $targetFileName an optional destination fileName
1918 * @param string $conflictMode a value of the DuplicationBehavior enumeration
1919 *
1920 * @throws \Exception|Exception\AbstractFileOperationException
1921 * @throws Exception\ExistingTargetFileNameException
1922 * @return FileInterface
1923 */
1924 public function copyFile(FileInterface $file, Folder $targetFolder, $targetFileName = null, $conflictMode = DuplicationBehavior::RENAME)
1925 {
1926 $conflictMode = DuplicationBehavior::cast($conflictMode);
1927 if ($targetFileName === null) {
1928 $targetFileName = $file->getName();
1929 }
1930 $sanitizedTargetFileName = $this->driver->sanitizeFileName($targetFileName);
1931 $this->assureFileCopyPermissions($file, $targetFolder, $sanitizedTargetFileName);
1932
1933 $this->eventDispatcher->dispatch(
1934 new BeforeFileCopiedEvent($file, $targetFolder)
1935 );
1936
1937 // File exists and we should abort, let's abort
1938 if ($conflictMode->equals(DuplicationBehavior::CANCEL) && $targetFolder->hasFile($sanitizedTargetFileName)) {
1939 throw new ExistingTargetFileNameException('The target file already exists.', 1320291064);
1940 }
1941 // File exists and we should find another name, let's find another one
1942 if ($conflictMode->equals(DuplicationBehavior::RENAME) && $targetFolder->hasFile($sanitizedTargetFileName)) {
1943 $sanitizedTargetFileName = $this->getUniqueName($targetFolder, $sanitizedTargetFileName);
1944 }
1945 $sourceStorage = $file->getStorage();
1946 // Call driver method to create a new file from an existing file object,
1947 // and return the new file object
1948 if ($sourceStorage === $this) {
1949 $newFileObjectIdentifier = $this->driver->copyFileWithinStorage($file->getIdentifier(), $targetFolder->getIdentifier(), $sanitizedTargetFileName);
1950 } else {
1951 $tempPath = $file->getForLocalProcessing();
1952 $newFileObjectIdentifier = $this->driver->addFile($tempPath, $targetFolder->getIdentifier(), $sanitizedTargetFileName);
1953 }
1954 $newFileObject = $this->getFileByIdentifier($newFileObjectIdentifier);
1955
1956 $this->eventDispatcher->dispatch(
1957 new AfterFileCopiedEvent($file, $targetFolder, $newFileObjectIdentifier, $newFileObject)
1958 );
1959 return $newFileObject;
1960 }
1961
1962 /**
1963 * Moves a $file into a $targetFolder
1964 * the target folder has to be part of this storage
1965 *
1966 * previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_move()
1967 *
1968 * @param FileInterface $file
1969 * @param Folder $targetFolder
1970 * @param string $targetFileName an optional destination fileName
1971 * @param string $conflictMode a value of the DuplicationBehavior enumeration
1972 *
1973 * @throws Exception\ExistingTargetFileNameException
1974 * @throws \RuntimeException
1975 * @return FileInterface
1976 */
1977 public function moveFile($file, $targetFolder, $targetFileName = null, $conflictMode = DuplicationBehavior::RENAME)
1978 {
1979 $conflictMode = DuplicationBehavior::cast($conflictMode);
1980 if ($targetFileName === null) {
1981 $targetFileName = $file->getName();
1982 }
1983 $originalFolder = $file->getParentFolder();
1984 $sanitizedTargetFileName = $this->driver->sanitizeFileName($targetFileName);
1985 $this->assureFileMovePermissions($file, $targetFolder, $sanitizedTargetFileName);
1986 if ($targetFolder->hasFile($sanitizedTargetFileName)) {
1987 // File exists and we should abort, let's abort
1988 if ($conflictMode->equals(DuplicationBehavior::RENAME)) {
1989 $sanitizedTargetFileName = $this->getUniqueName($targetFolder, $sanitizedTargetFileName);
1990 } elseif ($conflictMode->equals(DuplicationBehavior::CANCEL)) {
1991 throw new ExistingTargetFileNameException('The target file already exists', 1329850997);
1992 }
1993 }
1994 $this->eventDispatcher->dispatch(
1995 new BeforeFileMovedEvent($file, $targetFolder, $sanitizedTargetFileName)
1996 );
1997 $sourceStorage = $file->getStorage();
1998 // Call driver method to move the file and update the index entry
1999 try {
2000 if ($sourceStorage === $this) {
2001 $newIdentifier = $this->driver->moveFileWithinStorage($file->getIdentifier(), $targetFolder->getIdentifier(), $sanitizedTargetFileName);
2002 if (!$file instanceof AbstractFile) {
2003 throw new \RuntimeException('The given file is not of type AbstractFile.', 1384209025);
2004 }
2005 $file->updateProperties(['identifier' => $newIdentifier]);
2006 } else {
2007 $tempPath = $file->getForLocalProcessing();
2008 $newIdentifier = $this->driver->addFile($tempPath, $targetFolder->getIdentifier(), $sanitizedTargetFileName);
2009
2010 // Disable permission check to find nearest recycler and move file without errors
2011 $currentPermissions = $sourceStorage->evaluatePermissions;
2012 $sourceStorage->evaluatePermissions = false;
2013
2014 $recyclerFolder = $sourceStorage->getNearestRecyclerFolder($file);
2015 if ($recyclerFolder === null) {
2016 $sourceStorage->driver->deleteFile($file->getIdentifier());
2017 } else {
2018 $sourceStorage->moveFile($file, $recyclerFolder);
2019 }
2020 $sourceStorage->evaluatePermissions = $currentPermissions;
2021 if ($file instanceof File) {
2022 $file->updateProperties(['storage' => $this->getUid(), 'identifier' => $newIdentifier]);
2023 }
2024 }
2025 $this->getIndexer()->updateIndexEntry($file);
2026 } catch (\TYPO3\CMS\Core\Exception $e) {
2027 echo $e->getMessage();
2028 }
2029 $this->eventDispatcher->dispatch(
2030 new AfterFileMovedEvent($file, $targetFolder, $originalFolder)
2031 );
2032 return $file;
2033 }
2034
2035 /**
2036 * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_rename()
2037 *
2038 * @param FileInterface $file
2039 * @param string $targetFileName
2040 * @param string $conflictMode
2041 * @return FileInterface
2042 * @throws ExistingTargetFileNameException
2043 */
2044 public function renameFile($file, $targetFileName, $conflictMode = DuplicationBehavior::RENAME)
2045 {
2046 // The name should be different from the current.
2047 if ($file->getName() === $targetFileName) {
2048 return $file;
2049 }
2050 $sanitizedTargetFileName = $this->driver->sanitizeFileName($targetFileName);
2051 $this->assureFileRenamePermissions($file, $sanitizedTargetFileName);
2052 $this->eventDispatcher->dispatch(
2053 new BeforeFileRenamedEvent($file, $sanitizedTargetFileName)
2054 );
2055
2056 $conflictMode = DuplicationBehavior::cast($conflictMode);
2057
2058 // Call driver method to rename the file and update the index entry
2059 try {
2060 $newIdentifier = $this->driver->renameFile($file->getIdentifier(), $sanitizedTargetFileName);
2061 if ($file instanceof File) {
2062 $file->updateProperties(['identifier' => $newIdentifier]);
2063 }
2064 $this->getIndexer()->updateIndexEntry($file);
2065 } catch (ExistingTargetFileNameException $exception) {
2066 if ($conflictMode->equals(DuplicationBehavior::RENAME)) {
2067 $newName = $this->getUniqueName($file->getParentFolder(), $sanitizedTargetFileName);
2068 $file = $this->renameFile($file, $newName);
2069 } elseif ($conflictMode->equals(DuplicationBehavior::CANCEL)) {
2070 throw $exception;
2071 } elseif ($conflictMode->equals(DuplicationBehavior::REPLACE)) {
2072 $sourceFileIdentifier = substr($file->getCombinedIdentifier(), 0, (int)strrpos($file->getCombinedIdentifier(), '/') + 1) . $targetFileName;
2073 $sourceFile = $this->getResourceFactoryInstance()->getFileObjectFromCombinedIdentifier($sourceFileIdentifier);
2074 $file = $this->replaceFile($sourceFile, Environment::getPublicPath() . '/' . $file->getPublicUrl());
2075 }
2076 } catch (\RuntimeException $e) {
2077 }
2078
2079 $this->eventDispatcher->dispatch(
2080 new AfterFileRenamedEvent($file, $sanitizedTargetFileName)
2081 );
2082
2083 return $file;
2084 }
2085
2086 /**
2087 * Replaces a file with a local file (e.g. a freshly uploaded file)
2088 *
2089 * @param FileInterface $file
2090 * @param string $localFilePath
2091 *
2092 * @return FileInterface
2093 *
2094 * @throws Exception\IllegalFileExtensionException
2095 * @throws \InvalidArgumentException
2096 */
2097 public function replaceFile(FileInterface $file, $localFilePath)
2098 {
2099 $this->assureFileReplacePermissions($file);
2100 if (!file_exists($localFilePath)) {
2101 throw new \InvalidArgumentException('File "' . $localFilePath . '" does not exist.', 1325842622);
2102 }
2103 $this->eventDispatcher->dispatch(
2104 new BeforeFileReplacedEvent($file, $localFilePath)
2105 );
2106 $this->driver->replaceFile($file->getIdentifier(), $localFilePath);
2107 if ($file instanceof File) {
2108 $this->getIndexer()->updateIndexEntry($file);
2109 }
2110 $this->eventDispatcher->dispatch(
2111 new AfterFileReplacedEvent($file, $localFilePath)
2112 );
2113 return $file;
2114 }
2115
2116 /**
2117 * Adds an uploaded file into the Storage. Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::file_upload()
2118 *
2119 * @param array $uploadedFileData contains information about the uploaded file given by $_FILES['file1']
2120 * @param Folder $targetFolder the target folder
2121 * @param string $targetFileName the file name to be written
2122 * @param string $conflictMode a value of the DuplicationBehavior enumeration
2123 * @return FileInterface The file object
2124 */
2125 public function addUploadedFile(array $uploadedFileData, Folder $targetFolder = null, $targetFileName = null, $conflictMode = DuplicationBehavior::CANCEL)
2126 {
2127 $conflictMode = DuplicationBehavior::cast($conflictMode);
2128 $localFilePath = $uploadedFileData['tmp_name'];
2129 if ($targetFolder === null) {
2130 $targetFolder = $this->getDefaultFolder();
2131 }
2132 if ($targetFileName === null) {
2133 $targetFileName = $uploadedFileData['name'];
2134 }
2135 $targetFileName = $this->driver->sanitizeFileName($targetFileName);
2136
2137 $this->assureFileUploadPermissions($localFilePath, $targetFolder, $targetFileName, $uploadedFileData['size']);
2138 if ($this->hasFileInFolder($targetFileName, $targetFolder) && $conflictMode->equals(DuplicationBehavior::REPLACE)) {
2139 $file = $this->getFileInFolder($targetFileName, $targetFolder);
2140 $resultObject = $this->replaceFile($file, $localFilePath);
2141 } else {
2142 $resultObject = $this->addFile($localFilePath, $targetFolder, $targetFileName, (string)$conflictMode);
2143 }
2144 return $resultObject;
2145 }
2146
2147 /********************
2148 * FOLDER ACTIONS
2149 ********************/
2150 /**
2151 * Returns an array with all file objects in a folder and its subfolders, with the file identifiers as keys.
2152 * @todo check if this is a duplicate
2153 * @param Folder $folder
2154 * @return File[]
2155 */
2156 protected function getAllFileObjectsInFolder(Folder $folder)
2157 {
2158 $files = [];
2159 $folderQueue = [$folder];
2160 while (!empty($folderQueue)) {
2161 $folder = array_shift($folderQueue);
2162 foreach ($folder->getSubfolders() as $subfolder) {
2163 $folderQueue[] = $subfolder;
2164 }
2165 foreach ($folder->getFiles() as $file) {
2166 /** @var FileInterface $file */
2167 $files[$file->getIdentifier()] = $file;
2168 }
2169 }
2170
2171 return $files;
2172 }
2173
2174 /**
2175 * Moves a folder. If you want to move a folder from this storage to another
2176 * one, call this method on the target storage, otherwise you will get an exception.
2177 *
2178 * @param Folder $folderToMove The folder to move.
2179 * @param Folder $targetParentFolder The target parent folder
2180 * @param string $newFolderName
2181 * @param string $conflictMode a value of the DuplicationBehavior enumeration
2182 *
2183 * @throws \Exception|\TYPO3\CMS\Core\Exception
2184 * @throws \InvalidArgumentException
2185 * @throws InvalidTargetFolderException
2186 * @return Folder
2187 */
2188 public function moveFolder(Folder $folderToMove, Folder $targetParentFolder, $newFolderName = null, $conflictMode = DuplicationBehavior::RENAME)
2189 {
2190 // @todo add tests
2191 $this->assureFolderMovePermissions($folderToMove, $targetParentFolder);
2192 $sourceStorage = $folderToMove->getStorage();
2193 $sanitizedNewFolderName = $this->driver->sanitizeFileName($newFolderName ?: $folderToMove->getName());
2194 // @todo check if folder already exists in $targetParentFolder, handle this conflict then
2195 $this->eventDispatcher->dispatch(
2196 new BeforeFolderMovedEvent($folderToMove, $targetParentFolder, $sanitizedNewFolderName)
2197 );
2198 // Get all file objects now so we are able to update them after moving the folder
2199 $fileObjects = $this->getAllFileObjectsInFolder($folderToMove);
2200 if ($sourceStorage === $this) {
2201 if ($this->isWithinFolder($folderToMove, $targetParentFolder)) {
2202 throw new InvalidTargetFolderException(
2203 sprintf(
2204 'Cannot move folder "%s" into target folder "%s", because the target folder is already within the folder to be moved!',
2205 $folderToMove->getName(),
2206 $targetParentFolder->getName()
2207 ),
2208 1422723050
2209 );
2210 }
2211 $fileMappings = $this->driver->moveFolderWithinStorage($folderToMove->getIdentifier(), $targetParentFolder->getIdentifier(), $sanitizedNewFolderName);
2212 } else {
2213 $fileMappings = $this->moveFolderBetweenStorages($folderToMove, $targetParentFolder, $sanitizedNewFolderName);
2214 }
2215 // Update the identifier and storage of all file objects
2216 foreach ($fileObjects as $oldIdentifier => $fileObject) {
2217 $newIdentifier = $fileMappings[$oldIdentifier];
2218 $fileObject->updateProperties(['storage' => $this->getUid(), 'identifier' => $newIdentifier]);
2219 $this->getIndexer()->updateIndexEntry($fileObject);
2220 }
2221 $returnObject = $this->getFolder($fileMappings[$folderToMove->getIdentifier()]);
2222
2223 $this->eventDispatcher->dispatch(
2224 new AfterFolderMovedEvent($folderToMove, $targetParentFolder, $returnObject)
2225 );
2226 return $returnObject;
2227 }
2228
2229 /**
2230 * Moves the given folder from a different storage to the target folder in this storage.
2231 *
2232 * @param Folder $folderToMove
2233 * @param Folder $targetParentFolder
2234 * @param string $newFolderName
2235 * @throws NotImplementedMethodException
2236 */
2237 protected function moveFolderBetweenStorages(Folder $folderToMove, Folder $targetParentFolder, $newFolderName)
2238 {
2239 throw new NotImplementedMethodException('Not yet implemented', 1476046361);
2240 }
2241
2242 /**
2243 * Copies a folder.
2244 *
2245 * @param FolderInterface $folderToCopy The folder to copy
2246 * @param FolderInterface $targetParentFolder The target folder
2247 * @param string $newFolderName
2248 * @param string $conflictMode a value of the DuplicationBehavior enumeration
2249 * @return Folder The new (copied) folder object
2250 * @throws InvalidTargetFolderException
2251 */
2252 public function copyFolder(FolderInterface $folderToCopy, FolderInterface $targetParentFolder, $newFolderName = null, $conflictMode = DuplicationBehavior::RENAME)
2253 {
2254 // @todo implement the $conflictMode handling
2255 $this->assureFolderCopyPermissions($folderToCopy, $targetParentFolder);
2256 $returnObject = null;
2257 $sanitizedNewFolderName = $this->driver->sanitizeFileName($newFolderName ?: $folderToCopy->getName());
2258 if ($folderToCopy instanceof Folder && $targetParentFolder instanceof Folder) {
2259 $this->eventDispatcher->dispatch(
2260 new BeforeFolderCopiedEvent($folderToCopy, $targetParentFolder, $sanitizedNewFolderName)
2261 );
2262 }
2263 $sourceStorage = $folderToCopy->getStorage();
2264 // call driver method to move the file
2265 // that also updates the file object properties
2266 if ($sourceStorage === $this) {
2267 if ($this->isWithinFolder($folderToCopy, $targetParentFolder)) {
2268 throw new InvalidTargetFolderException(
2269 sprintf(
2270 'Cannot copy folder "%s" into target folder "%s", because the target folder is already within the folder to be copied!',
2271 $folderToCopy->getName(),
2272 $targetParentFolder->getName()
2273 ),
2274 1422723059
2275 );
2276 }
2277 $this->driver->copyFolderWithinStorage($folderToCopy->getIdentifier(), $targetParentFolder->getIdentifier(), $sanitizedNewFolderName);
2278 $returnObject = $this->getFolder($targetParentFolder->getSubfolder($sanitizedNewFolderName)->getIdentifier());
2279 } else {
2280 $this->copyFolderBetweenStorages($folderToCopy, $targetParentFolder, $sanitizedNewFolderName);
2281 }
2282 if ($folderToCopy instanceof Folder && $targetParentFolder instanceof Folder) {
2283 $this->eventDispatcher->dispatch(
2284 new AfterFolderCopiedEvent($folderToCopy, $targetParentFolder, $returnObject)
2285 );
2286 }
2287 return $returnObject;
2288 }
2289
2290 /**
2291 * Copies a folder between storages.
2292 *
2293 * @param FolderInterface $folderToCopy
2294 * @param FolderInterface $targetParentFolder
2295 * @param string $newFolderName
2296 * @throws NotImplementedMethodException
2297 */
2298 protected function copyFolderBetweenStorages(FolderInterface $folderToCopy, FolderInterface $targetParentFolder, $newFolderName)
2299 {
2300 throw new NotImplementedMethodException('Not yet implemented.', 1476046386);
2301 }
2302
2303 /**
2304 * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::folder_move()
2305 *
2306 * @param Folder $folderObject
2307 * @param string $newName
2308 * @throws \Exception
2309 * @throws \InvalidArgumentException
2310 * @return Folder
2311 */
2312 public function renameFolder($folderObject, $newName)
2313 {
2314
2315 // Renaming the folder should check if the parent folder is writable
2316 // We cannot do this however because we cannot extract the parent folder from a folder currently
2317 if (!$this->checkFolderActionPermission('rename', $folderObject)) {
2318 throw new InsufficientUserPermissionsException('You are not allowed to rename the folder "' . $folderObject->getIdentifier() . '\'', 1357811441);
2319 }
2320
2321 $sanitizedNewName = $this->driver->sanitizeFileName($newName);
2322 if ($this->driver->folderExistsInFolder($sanitizedNewName, $folderObject->getIdentifier())) {
2323 throw new \InvalidArgumentException('The folder ' . $sanitizedNewName . ' already exists in folder ' . $folderObject->getIdentifier(), 1325418870);
2324 }
2325 $this->eventDispatcher->dispatch(
2326 new BeforeFolderRenamedEvent($folderObject, $sanitizedNewName)
2327 );
2328 $fileObjects = $this->getAllFileObjectsInFolder($folderObject);
2329 $fileMappings = $this->driver->renameFolder($folderObject->getIdentifier(), $sanitizedNewName);
2330 // Update the identifier of all file objects
2331 foreach ($fileObjects as $oldIdentifier => $fileObject) {
2332 $newIdentifier = $fileMappings[$oldIdentifier];
2333 $fileObject->updateProperties(['identifier' => $newIdentifier]);
2334 $this->getIndexer()->updateIndexEntry($fileObject);
2335 }
2336 $returnObject = $this->getFolder($fileMappings[$folderObject->getIdentifier()]);
2337
2338 $this->eventDispatcher->dispatch(
2339 new AfterFolderRenamedEvent($returnObject)
2340 );
2341 return $returnObject;
2342 }
2343
2344 /**
2345 * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::folder_delete()
2346 *
2347 * @param Folder $folderObject
2348 * @param bool $deleteRecursively
2349 * @throws \RuntimeException
2350 * @throws Exception\InsufficientFolderAccessPermissionsException
2351 * @throws Exception\InsufficientUserPermissionsException
2352 * @throws Exception\FileOperationErrorException
2353 * @throws Exception\InvalidPathException
2354 * @return bool
2355 */
2356 public function deleteFolder($folderObject, $deleteRecursively = false)
2357 {
2358 $isEmpty = $this->driver->isFolderEmpty($folderObject->getIdentifier());
2359 $this->assureFolderDeletePermission($folderObject, $deleteRecursively && !$isEmpty);
2360 if (!$isEmpty && !$deleteRecursively) {
2361 throw new \RuntimeException('Could not delete folder "' . $folderObject->getIdentifier() . '" because it is not empty.', 1325952534);
2362 }
2363
2364 $this->eventDispatcher->dispatch(
2365 new BeforeFolderDeletedEvent($folderObject)
2366 );
2367
2368 foreach ($this->getFilesInFolder($folderObject, 0, 0, false, $deleteRecursively) as $file) {
2369 $this->deleteFile($file);
2370 }
2371
2372 $result = $this->driver->deleteFolder($folderObject->getIdentifier(), $deleteRecursively);
2373
2374 $this->eventDispatcher->dispatch(
2375 new AfterFolderDeletedEvent($folderObject, $result)
2376 );
2377 return $result;
2378 }
2379
2380 /**
2381 * Returns the Identifier for a folder within a given folder.
2382 *
2383 * @param string $folderName The name of the target folder
2384 * @param Folder $parentFolder
2385 * @param bool $returnInaccessibleFolderObject
2386 * @return Folder|InaccessibleFolder
2387 * @throws \Exception
2388 * @throws Exception\InsufficientFolderAccessPermissionsException
2389 */
2390 public function getFolderInFolder($folderName, Folder $parentFolder, $returnInaccessibleFolderObject = false)
2391 {
2392 $folderIdentifier = $this->driver->getFolderInFolder($folderName, $parentFolder->getIdentifier());
2393 return $this->getFolder($folderIdentifier, $returnInaccessibleFolderObject);
2394 }
2395
2396 /**
2397 * @param Folder $folder
2398 * @param int $start
2399 * @param int $maxNumberOfItems
2400 * @param bool $useFilters
2401 * @param bool $recursive
2402 * @param string $sort Property name used to sort the items.
2403 * Among them may be: '' (empty, no sorting), name,
2404 * fileext, size, tstamp and rw.
2405 * If a driver does not support the given property, it
2406 * should fall back to "name".
2407 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
2408 * @return Folder[]
2409 */
2410 public function getFoldersInFolder(Folder $folder, $start = 0, $maxNumberOfItems = 0, $useFilters = true, $recursive = false, $sort = '', $sortRev = false)
2411 {
2412 $filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
2413
2414 $folderIdentifiers = $this->driver->getFoldersInFolder($folder->getIdentifier(), $start, $maxNumberOfItems, $recursive, $filters, $sort, $sortRev);
2415
2416 // Exclude processing folders
2417 foreach ($this->getProcessingFolders() as $processingFolder) {
2418 $processingIdentifier = $processingFolder->getIdentifier();
2419 if (isset($folderIdentifiers[$processingIdentifier])) {
2420 unset($folderIdentifiers[$processingIdentifier]);
2421 }
2422 }
2423 $folders = [];
2424 foreach ($folderIdentifiers as $folderIdentifier) {
2425 $folders[$folderIdentifier] = $this->getFolder($folderIdentifier, true);
2426 }
2427 return $folders;
2428 }
2429
2430 /**
2431 * @param Folder $folder
2432 * @param bool $useFilters
2433 * @param bool $recursive
2434 * @return int Number of subfolders
2435 * @throws Exception\InsufficientFolderAccessPermissionsException
2436 */
2437 public function countFoldersInFolder(Folder $folder, $useFilters = true, $recursive = false)
2438 {
2439 $this->assureFolderReadPermission($folder);
2440 $filters = $useFilters ? $this->fileAndFolderNameFilters : [];
2441 return $this->driver->countFoldersInFolder($folder->getIdentifier(), $recursive, $filters);
2442 }
2443
2444 /**
2445 * Returns TRUE if the specified folder exists.
2446 *
2447 * @param string $identifier
2448 * @return bool
2449 */
2450 public function hasFolder($identifier)
2451 {
2452 $this->assureFolderReadPermission();
2453 return $this->driver->folderExists($identifier);
2454 }
2455
2456 /**
2457 * Checks if the given file exists in the given folder
2458 *
2459 * @param string $folderName
2460 * @param Folder $folder
2461 * @return bool
2462 */
2463 public function hasFolderInFolder($folderName, Folder $folder)
2464 {
2465 $this->assureFolderReadPermission($folder);
2466 return $this->driver->folderExistsInFolder($folderName, $folder->getIdentifier());
2467 }
2468
2469 /**
2470 * Creates a new folder.
2471 *
2472 * previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_newfolder()
2473 *
2474 * @param string $folderName The new folder name
2475 * @param Folder $parentFolder (optional) the parent folder to create the new folder inside of. If not given, the root folder is used
2476 * @return Folder
2477 * @throws Exception\ExistingTargetFolderException
2478 * @throws Exception\InsufficientFolderAccessPermissionsException
2479 * @throws Exception\InsufficientFolderWritePermissionsException
2480 * @throws \Exception
2481 */
2482 public function createFolder($folderName, Folder $parentFolder = null)
2483 {
2484 if ($parentFolder === null) {
2485 $parentFolder = $this->getRootLevelFolder();
2486 } elseif (!$this->driver->folderExists($parentFolder->getIdentifier())) {
2487 throw new \InvalidArgumentException('Parent folder "' . $parentFolder->getIdentifier() . '" does not exist.', 1325689164);
2488 }
2489 if (!$this->checkFolderActionPermission('add', $parentFolder)) {
2490 throw new InsufficientFolderWritePermissionsException('You are not allowed to create directories in the folder "' . $parentFolder->getIdentifier() . '"', 1323059807);
2491 }
2492 if ($this->driver->folderExistsInFolder($folderName, $parentFolder->getIdentifier())) {
2493 throw new ExistingTargetFolderException('Folder "' . $folderName . '" already exists.', 1423347324);
2494 }
2495
2496 $this->eventDispatcher->dispatch(
2497 new BeforeFolderAddedEvent($parentFolder, $folderName)
2498 );
2499
2500 $newFolder = $this->getDriver()->createFolder($folderName, $parentFolder->getIdentifier(), true);
2501 $newFolder = $this->getFolder($newFolder);
2502
2503 $this->eventDispatcher->dispatch(
2504 new AfterFolderAddedEvent($newFolder)
2505 );
2506
2507 return $newFolder;
2508 }
2509
2510 /**
2511 * Retrieves information about a folder
2512 *
2513 * @param Folder $folder
2514 * @return array
2515 */
2516 public function getFolderInfo(Folder $folder)
2517 {
2518 return $this->driver->getFolderInfoByIdentifier($folder->getIdentifier());
2519 }
2520
2521 /**
2522 * Returns the default folder where new files are stored if no other folder is given.
2523 *
2524 * @return Folder
2525 */
2526 public function getDefaultFolder()
2527 {
2528 return $this->getFolder($this->driver->getDefaultFolder());
2529 }
2530
2531 /**
2532 * @param string $identifier
2533 * @param bool $returnInaccessibleFolderObject
2534 *
2535 * @return Folder|InaccessibleFolder
2536 * @throws \Exception
2537 * @throws Exception\InsufficientFolderAccessPermissionsException
2538 */
2539 public function getFolder($identifier, $returnInaccessibleFolderObject = false)
2540 {
2541 $data = $this->driver->getFolderInfoByIdentifier($identifier);
2542 $folder = $this->createFolderObject($data['identifier'] ?? '', $data['name'] ?? '');
2543
2544 try {
2545 $this->assureFolderReadPermission($folder);
2546 } catch (InsufficientFolderAccessPermissionsException $e) {
2547 $folder = null;
2548 if ($returnInaccessibleFolderObject) {
2549 // if parent folder is readable return inaccessible folder object
2550 $parentPermissions = $this->driver->getPermissions($this->driver->getParentFolderIdentifierOfIdentifier($identifier));
2551 if ($parentPermissions['r']) {
2552 $folder = GeneralUtility::makeInstance(
2553 InaccessibleFolder::class,
2554 $this,
2555 $data['identifier'],
2556 $data['name']
2557 );
2558 }
2559 }
2560
2561 if ($folder === null) {
2562 throw $e;
2563 }
2564 }
2565 return $folder;
2566 }
2567
2568 /**
2569 * Returns TRUE if the specified file is in a folder that is set a processing for a storage
2570 *
2571 * @param string $identifier
2572 * @return bool
2573 */
2574 public function isWithinProcessingFolder($identifier)
2575 {
2576 $inProcessingFolder = false;
2577 foreach ($this->getProcessingFolders() as $processingFolder) {
2578 if ($this->driver->isWithin($processingFolder->getIdentifier(), $identifier)) {
2579 $inProcessingFolder = true;
2580 break;
2581 }
2582 }
2583 return $inProcessingFolder;
2584 }
2585
2586 /**
2587 * Checks if a resource (file or folder) is within the given folder
2588 *
2589 * @param Folder $folder
2590 * @param ResourceInterface $resource
2591 * @return bool
2592 * @throws \InvalidArgumentException
2593 */
2594 public function isWithinFolder(Folder $folder, ResourceInterface $resource)
2595 {
2596 if ($folder->getStorage() !== $this) {
2597 throw new \InvalidArgumentException('Given folder "' . $folder->getIdentifier() . '" is not part of this storage!', 1422709241);
2598 }
2599 if ($folder->getStorage() !== $resource->getStorage()) {
2600 return false;
2601 }
2602 return $this->driver->isWithin($folder->getIdentifier(), $resource->getIdentifier());
2603 }
2604
2605 /**
2606 * Returns the folders on the root level of the storage
2607 * or the first mount point of this storage for this user
2608 * if $respectFileMounts is set.
2609 *
2610 * @param bool $respectFileMounts
2611 * @return Folder
2612 */
2613 public function getRootLevelFolder($respectFileMounts = true)
2614 {
2615 if ($respectFileMounts && !empty($this->fileMounts)) {
2616 $mount = reset($this->fileMounts);
2617 return $mount['folder'];
2618 }
2619 return $this->createFolderObject($this->driver->getRootLevelFolder(), '');
2620 }
2621
2622 /**
2623 * Returns the destination path/fileName of a unique fileName/foldername in that path.
2624 * If $theFile exists in $theDest (directory) the file have numbers appended up to $this->maxNumber.
2625 * Hereafter a unique string will be appended.
2626 * This function is used by fx. DataHandler when files are attached to records
2627 * and needs to be uniquely named in the uploads/* folders
2628 *
2629 * @param FolderInterface $folder
2630 * @param string $theFile The input fileName to check
2631 * @param bool $dontCheckForUnique If set the fileName is returned with the path prepended without checking whether it already existed!
2632 *
2633 * @throws \RuntimeException
2634 * @return string A unique fileName inside $folder, based on $theFile.
2635 * @see \TYPO3\CMS\Core\Utility\File\BasicFileUtility::getUniqueName()
2636 */
2637 protected function getUniqueName(FolderInterface $folder, $theFile, $dontCheckForUnique = false)
2638 {
2639 $maxNumber = 99;
2640 // Fetches info about path, name, extension of $theFile
2641 $origFileInfo = PathUtility::pathinfo($theFile);
2642 // Check if the file exists and if not - return the fileName...
2643 // The destinations file
2644 $theDestFile = $origFileInfo['basename'];
2645 // If the file does NOT exist we return this fileName
2646 if (!$this->driver->fileExistsInFolder($theDestFile, $folder->getIdentifier()) || $dontCheckForUnique) {
2647 return $theDestFile;
2648 }
2649 // Well the fileName in its pure form existed. Now we try to append
2650 // numbers / unique-strings and see if we can find an available fileName
2651 // This removes _xx if appended to the file
2652 $theTempFileBody = preg_replace('/_[0-9][0-9]$/', '', $origFileInfo['filename']);
2653 $theOrigExt = $origFileInfo['extension'] ? '.' . $origFileInfo['extension'] : '';
2654 for ($a = 1; $a <= $maxNumber + 1; $a++) {
2655 // First we try to append numbers
2656 if ($a <= $maxNumber) {
2657 $insert = '_' . sprintf('%02d', $a);
2658 } else {
2659 $insert = '_' . substr(md5(StringUtility::getUniqueId()), 0, 6);
2660 }
2661 $theTestFile = $theTempFileBody . $insert . $theOrigExt;
2662 // The destinations file
2663 $theDestFile = $theTestFile;
2664 // If the file does NOT exist we return this fileName
2665 if (!$this->driver->fileExistsInFolder($theDestFile, $folder->getIdentifier())) {
2666 return $theDestFile;
2667 }
2668 }
2669 throw new \RuntimeException('Last possible name "' . $theDestFile . '" is already taken.', 1325194291);
2670 }
2671
2672 /**
2673 * @return ResourceFactory
2674 */
2675 protected function getFileFactory()
2676 {
2677 return GeneralUtility::makeInstance(ResourceFactory::class);
2678 }
2679
2680 /**
2681 * @return Index\FileIndexRepository
2682 */
2683 protected function getFileIndexRepository()
2684 {
2685 return FileIndexRepository::getInstance();
2686 }
2687
2688 /**
2689 * @return Service\FileProcessingService
2690 */
2691 protected function getFileProcessingService()
2692 {
2693 if (!$this->fileProcessingService) {
2694 $this->fileProcessingService = GeneralUtility::makeInstance(FileProcessingService::class, $this, $this->driver, $this->eventDispatcher);
2695 }
2696 return $this->fileProcessingService;
2697 }
2698
2699 /**
2700 * Gets the role of a folder.
2701 *
2702 * @param FolderInterface $folder Folder object to get the role from
2703 * @return string The role the folder has
2704 */
2705 public function getRole(FolderInterface $folder)
2706 {
2707 $folderRole = FolderInterface::ROLE_DEFAULT;
2708 $identifier = $folder->getIdentifier();
2709 if (method_exists($this->driver, 'getRole')) {
2710 $folderRole = $this->driver->getRole($folder->getIdentifier());
2711 }
2712 if (isset($this->fileMounts[$identifier])) {
2713 $folderRole = FolderInterface::ROLE_MOUNT;
2714
2715 if (!empty($this->fileMounts[$identifier]['read_only'])) {
2716 $folderRole = FolderInterface::ROLE_READONLY_MOUNT;
2717 }
2718 if ($this->fileMounts[$identifier]['user_mount']) {
2719 $folderRole = FolderInterface::ROLE_USER_MOUNT;
2720 }
2721 }
2722 if ($folder instanceof Folder && $this->isProcessingFolder($folder)) {
2723 $folderRole = FolderInterface::ROLE_PROCESSING;
2724 }
2725
2726 return $folderRole;
2727 }
2728
2729 /**
2730 * Getter function to return the folder where the files can
2731 * be processed. Does not check for access rights here.
2732 *
2733 * @param File $file Specific file you want to have the processing folder for
2734 * @return Folder
2735 */
2736 public function getProcessingFolder(File $file = null)
2737 {
2738 // If a file is given, make sure to return the processing folder of the correct storage
2739 if ($file !== null && $file->getStorage()->getUid() !== $this->getUid()) {
2740 return $file->getStorage()->getProcessingFolder($file);
2741 }
2742 if (!isset($this->processingFolder)) {
2743 $processingFolder = self::DEFAULT_ProcessingFolder;
2744 if (!empty($this->storageRecord['processingfolder'])) {
2745 $processingFolder = $this->storageRecord['processingfolder'];
2746 }
2747 try {
2748 if (strpos($processingFolder, ':') !== false) {
2749 [$storageUid, $processingFolderIdentifier] = explode(':', $processingFolder, 2);
2750 $storage = GeneralUtility::makeInstance(StorageRepository::class)->findByUid((int)$storageUid);
2751 if ($storage->hasFolder($processingFolderIdentifier)) {
2752 $this->processingFolder = $storage->getFolder($processingFolderIdentifier);
2753 } else {
2754 $rootFolder = $storage->getRootLevelFolder(false);
2755 $currentEvaluatePermissions = $storage->getEvaluatePermissions();
2756 $storage->setEvaluatePermissions(false);
2757 $this->processingFolder = $storage->createFolder(
2758 ltrim($processingFolderIdentifier, '/'),
2759 $rootFolder
2760 );
2761 $storage->setEvaluatePermissions($currentEvaluatePermissions);
2762 }
2763 } else {
2764 if ($this->driver->folderExists($processingFolder) === false) {
2765 $rootFolder = $this->getRootLevelFolder(false);
2766 try {
2767 $currentEvaluatePermissions = $this->evaluatePermissions;
2768 $this->evaluatePermissions = false;
2769 $this->processingFolder = $this->createFolder(
2770 $processingFolder,
2771 $rootFolder
2772 );
2773 $this->evaluatePermissions = $currentEvaluatePermissions;
2774 } catch (\InvalidArgumentException $e) {
2775 $this->processingFolder = GeneralUtility::makeInstance(
2776 InaccessibleFolder::class,
2777 $this,
2778 $processingFolder,
2779 $processingFolder
2780 );
2781 }
2782 } else {
2783 $data = $this->driver->getFolderInfoByIdentifier($processingFolder);
2784 $this->processingFolder = $this->createFolderObject($data['identifier'], $data['name']);
2785 }
2786 }
2787 } catch (InsufficientFolderWritePermissionsException|ResourcePermissionsUnavailableException $e) {
2788 $this->processingFolder = GeneralUtility::makeInstance(
2789 InaccessibleFolder::class,
2790 $this,
2791 $processingFolder,
2792 $processingFolder
2793 );
2794 }
2795 }
2796
2797 $processingFolder = $this->processingFolder;
2798 if (!empty($file)) {
2799 $processingFolder = $this->getNestedProcessingFolder($file, $processingFolder);
2800 }
2801 return $processingFolder;
2802 }
2803
2804 /**
2805 * Getter function to return the the file's corresponding hashed subfolder
2806 * of the processed folder
2807 *
2808 * @param File $file
2809 * @param Folder $rootProcessingFolder
2810 * @return Folder
2811 * @throws Exception\InsufficientFolderWritePermissionsException
2812 */
2813 protected function getNestedProcessingFolder(File $file, Folder $rootProcessingFolder)
2814 {
2815 $processingFolder = $rootProcessingFolder;
2816 $nestedFolderNames = $this->getNamesForNestedProcessingFolder(
2817 $file->getIdentifier(),
2818 self::PROCESSING_FOLDER_LEVELS
2819 );
2820
2821 try {
2822 foreach ($nestedFolderNames as $folderName) {
2823 if ($processingFolder->hasFolder($folderName)) {
2824 $processingFolder = $processingFolder->getSubfolder($folderName);
2825 } else {
2826 $currentEvaluatePermissions = $processingFolder->getStorage()->getEvaluatePermissions();
2827 $processingFolder->getStorage()->setEvaluatePermissions(false);
2828 $processingFolder = $processingFolder->createFolder($folderName);
2829 $processingFolder->getStorage()->setEvaluatePermissions($currentEvaluatePermissions);
2830 }
2831 }
2832 } catch (FolderDoesNotExistException $e) {
2833 }
2834
2835 return $processingFolder;
2836 }
2837
2838 /**
2839 * Generates appropriate hashed sub-folder path for a given file identifier
2840 *
2841 * @param string $fileIdentifier
2842 * @param int $levels
2843 * @return string[]
2844 */
2845 protected function getNamesForNestedProcessingFolder($fileIdentifier, $levels)
2846 {
2847 $names = [];
2848 if ($levels === 0) {
2849 return $names;
2850 }
2851 $hash = md5($fileIdentifier);
2852 for ($i = 1; $i <= $levels; $i++) {
2853 $names[] = substr($hash, $i, 1);
2854 }
2855 return $names;
2856 }
2857
2858 /**
2859 * Gets the driver Type configured for this storage.
2860 *
2861 * @return string
2862 */
2863 public function getDriverType()
2864 {
2865 return $this->storageRecord['driver'];
2866 }
2867
2868 /**
2869 * Gets the Indexer.
2870 *
2871 * @return Index\Indexer
2872 */
2873 protected function getIndexer()
2874 {
2875 return GeneralUtility::makeInstance(Indexer::class, $this);
2876 }
2877
2878 /**
2879 * @param bool $isDefault
2880 */
2881 public function setDefault($isDefault)
2882 {
2883 $this->isDefault = (bool)$isDefault;
2884 }
2885
2886 /**
2887 * @return bool
2888 */
2889 public function isDefault()
2890 {
2891 return $this->isDefault;
2892 }
2893
2894 /**
2895 * @return ResourceFactory
2896 */
2897 public function getResourceFactoryInstance(): ResourceFactory
2898 {
2899 return GeneralUtility::makeInstance(ResourceFactory::class);
2900 }
2901
2902 /**
2903 * Returns the current BE user.
2904 *
2905 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
2906 */
2907 protected function getBackendUser()
2908 {
2909 return $GLOBALS['BE_USER'];
2910 }
2911
2912 /**
2913 * Get the nearest Recycler folder for given file
2914 *
2915 * Return null if:
2916 * - There is no folder with ROLE_RECYCLER in the rootline of the given File
2917 * - File is a ProcessedFile (we don't know the concept of recycler folders for processedFiles)
2918 * - File is located in a folder with ROLE_RECYCLER
2919 *
2920 * @param FileInterface $file
2921 * @return Folder|null
2922 */
2923 protected function getNearestRecyclerFolder(FileInterface $file)
2924 {
2925 if ($file instanceof ProcessedFile) {
2926 return null;
2927 }
2928 // if the storage is not browsable we cannot fetch the parent folder of the file so no recycler handling is possible
2929 if (!$this->isBrowsable()) {
2930 return null;
2931 }
2932
2933 $recyclerFolder = null;
2934 $folder = $file->getParentFolder();
2935
2936 do {
2937 if ($folder->getRole() === FolderInterface::ROLE_RECYCLER) {
2938 break;
2939 }
2940
2941 foreach ($folder->getSubfolders() as $subFolder) {
2942 if ($subFolder->getRole() === FolderInterface::ROLE_RECYCLER) {
2943 $recyclerFolder = $subFolder;
2944 break;
2945 }
2946 }
2947
2948 $parentFolder = $folder->getParentFolder();
2949 $isFolderLoop = $folder->getIdentifier() === $parentFolder->getIdentifier();
2950 $folder = $parentFolder;
2951 } while ($recyclerFolder === null && !$isFolderLoop);
2952
2953 return $recyclerFolder;
2954 }
2955
2956 /**
2957 * Creates a folder to directly access (a part of) a storage.
2958 *
2959 * @param string $identifier The path to the folder. Might also be a simple unique string, depending on the storage driver.
2960 * @param string $name The name of the folder (e.g. the folder name)
2961 * @return Folder
2962 */
2963 protected function createFolderObject(string $identifier, string $name)
2964 {
2965 return GeneralUtility::makeInstance(Folder::class, $this, $identifier, $name);
2966 }
2967 }