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