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