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