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