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