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