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