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