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