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