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