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