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