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