ResourceStorage::getBackendUser()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Core\Resource;
17
18
use Psr\EventDispatcher\EventDispatcherInterface;
19
use Psr\Http\Message\ResponseInterface;
20
use Psr\Http\Message\ServerRequestInterface;
21
use TYPO3\CMS\Core\Core\Environment;
22
use TYPO3\CMS\Core\Database\ConnectionPool;
23
use TYPO3\CMS\Core\Http\ApplicationType;
24
use TYPO3\CMS\Core\Http\FalDumpFileContentsDecoratorStream;
25
use TYPO3\CMS\Core\Http\Response;
26
use TYPO3\CMS\Core\Log\LogManager;
27
use TYPO3\CMS\Core\Registry;
28
use TYPO3\CMS\Core\Resource\Driver\DriverInterface;
29
use TYPO3\CMS\Core\Resource\Driver\StreamableDriverInterface;
30
use TYPO3\CMS\Core\Resource\Event\AfterFileAddedEvent;
31
use TYPO3\CMS\Core\Resource\Event\AfterFileContentsSetEvent;
32
use TYPO3\CMS\Core\Resource\Event\AfterFileCopiedEvent;
33
use TYPO3\CMS\Core\Resource\Event\AfterFileCreatedEvent;
34
use TYPO3\CMS\Core\Resource\Event\AfterFileDeletedEvent;
35
use TYPO3\CMS\Core\Resource\Event\AfterFileMovedEvent;
36
use TYPO3\CMS\Core\Resource\Event\AfterFileRenamedEvent;
37
use TYPO3\CMS\Core\Resource\Event\AfterFileReplacedEvent;
38
use TYPO3\CMS\Core\Resource\Event\AfterFolderAddedEvent;
39
use TYPO3\CMS\Core\Resource\Event\AfterFolderCopiedEvent;
40
use TYPO3\CMS\Core\Resource\Event\AfterFolderDeletedEvent;
41
use TYPO3\CMS\Core\Resource\Event\AfterFolderMovedEvent;
42
use TYPO3\CMS\Core\Resource\Event\AfterFolderRenamedEvent;
43
use TYPO3\CMS\Core\Resource\Event\BeforeFileAddedEvent;
44
use TYPO3\CMS\Core\Resource\Event\BeforeFileContentsSetEvent;
45
use TYPO3\CMS\Core\Resource\Event\BeforeFileCopiedEvent;
46
use TYPO3\CMS\Core\Resource\Event\BeforeFileCreatedEvent;
47
use TYPO3\CMS\Core\Resource\Event\BeforeFileDeletedEvent;
48
use TYPO3\CMS\Core\Resource\Event\BeforeFileMovedEvent;
49
use TYPO3\CMS\Core\Resource\Event\BeforeFileRenamedEvent;
50
use TYPO3\CMS\Core\Resource\Event\BeforeFileReplacedEvent;
51
use TYPO3\CMS\Core\Resource\Event\BeforeFolderAddedEvent;
52
use TYPO3\CMS\Core\Resource\Event\BeforeFolderCopiedEvent;
53
use TYPO3\CMS\Core\Resource\Event\BeforeFolderDeletedEvent;
54
use TYPO3\CMS\Core\Resource\Event\BeforeFolderMovedEvent;
55
use TYPO3\CMS\Core\Resource\Event\BeforeFolderRenamedEvent;
56
use TYPO3\CMS\Core\Resource\Event\GeneratePublicUrlForResourceEvent;
57
use TYPO3\CMS\Core\Resource\Event\SanitizeFileNameEvent;
58
use TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException;
59
use TYPO3\CMS\Core\Resource\Exception\ExistingTargetFolderException;
60
use TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException;
61
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
62
use TYPO3\CMS\Core\Resource\Exception\IllegalFileExtensionException;
63
use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException;
64
use TYPO3\CMS\Core\Resource\Exception\InsufficientFileReadPermissionsException;
65
use TYPO3\CMS\Core\Resource\Exception\InsufficientFileWritePermissionsException;
66
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
67
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderWritePermissionsException;
68
use TYPO3\CMS\Core\Resource\Exception\InsufficientUserPermissionsException;
69
use TYPO3\CMS\Core\Resource\Exception\InvalidConfigurationException;
70
use TYPO3\CMS\Core\Resource\Exception\InvalidHashException;
71
use TYPO3\CMS\Core\Resource\Exception\InvalidTargetFolderException;
72
use TYPO3\CMS\Core\Resource\Exception\ResourcePermissionsUnavailableException;
73
use TYPO3\CMS\Core\Resource\Exception\UploadException;
74
use TYPO3\CMS\Core\Resource\Exception\UploadSizeException;
75
use TYPO3\CMS\Core\Resource\Index\FileIndexRepository;
76
use TYPO3\CMS\Core\Resource\Index\Indexer;
77
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
78
use TYPO3\CMS\Core\Resource\Search\FileSearchDemand;
79
use TYPO3\CMS\Core\Resource\Search\Result\DriverFilteredSearchResult;
80
use TYPO3\CMS\Core\Resource\Search\Result\EmptyFileSearchResult;
81
use TYPO3\CMS\Core\Resource\Search\Result\FileSearchResult;
82
use TYPO3\CMS\Core\Resource\Search\Result\FileSearchResultInterface;
83
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
84
use TYPO3\CMS\Core\Resource\Service\FileProcessingService;
85
use TYPO3\CMS\Core\Service\FlexFormService;
86
use TYPO3\CMS\Core\Utility\Exception\NotImplementedMethodException;
87
use TYPO3\CMS\Core\Utility\GeneralUtility;
88
use TYPO3\CMS\Core\Utility\PathUtility;
89
use TYPO3\CMS\Core\Utility\StringUtility;
90
91
/**
92
 * A "mount point" inside the TYPO3 file handling.
93
 *
94
 * A "storage" object handles
95
 * - abstraction to the driver
96
 * - permissions (from the driver, and from the user, + capabilities)
97
 * - an entry point for files, folders, and for most other operations
98
 *
99
 * == Driver entry point
100
 * The driver itself, that does the actual work on the file system,
101
 * is inside the storage but completely shadowed by
102
 * the storage, as the storage also handles the abstraction to the
103
 * driver
104
 *
105
 * The storage can be on the local system, but can also be on a remote
106
 * system. The combination of driver + configurable capabilities (storage
107
 * is read-only e.g.) allows for flexible uses.
108
 *
109
 *
110
 * == Permission system
111
 * As all requests have to run through the storage, the storage knows about the
112
 * permissions of a BE/FE user, the file permissions / limitations of the driver
113
 * and has some configurable capabilities.
114
 * Additionally, a BE user can use "filemounts" (known from previous installations)
115
 * to limit his/her work-zone to only a subset (identifier and its subfolders/subfolders)
116
 * of the user itself.
117
 *
118
 * Check 1: "User Permissions" [is the user allowed to write a file) [is the user allowed to write a file]
119
 * Check 2: "File Mounts" of the User (act as subsets / filters to the identifiers) [is the user allowed to do something in this folder?]
120
 * Check 3: "Capabilities" of Storage (then: of Driver) [is the storage/driver writable?]
121
 * Check 4: "File permissions" of the Driver [is the folder writable?]
122
 */
123
class ResourceStorage implements ResourceStorageInterface
124
{
125
    /**
126
     * The storage driver instance belonging to this storage.
127
     *
128
     * @var Driver\DriverInterface
129
     */
130
    protected $driver;
131
132
    /**
133
     * The database record for this storage
134
     *
135
     * @var array
136
     */
137
    protected $storageRecord;
138
139
    /**
140
     * The configuration belonging to this storage (decoded from the configuration field).
141
     *
142
     * @var array
143
     */
144
    protected $configuration;
145
146
    /**
147
     * @var Service\FileProcessingService
148
     */
149
    protected $fileProcessingService;
150
151
    /**
152
     * Whether to check if file or folder is in user mounts
153
     * and the action is allowed for a user
154
     * Default is FALSE so that resources are accessible for
155
     * front end rendering or admins.
156
     *
157
     * @var bool
158
     */
159
    protected $evaluatePermissions = false;
160
161
    /**
162
     * User filemounts, added as an array, and used as filters
163
     *
164
     * @var array
165
     */
166
    protected $fileMounts = [];
167
168
    /**
169
     * The file permissions of the user (and their group) merged together and
170
     * available as an array
171
     *
172
     * @var array
173
     */
174
    protected $userPermissions = [];
175
176
    /**
177
     * The capabilities of this storage as defined in the storage record.
178
     * Also see the CAPABILITY_* constants below
179
     *
180
     * @var int
181
     */
182
    protected $capabilities;
183
184
    /**
185
     * @var EventDispatcherInterface
186
     */
187
    protected $eventDispatcher;
188
189
    /**
190
     * @var Folder
191
     */
192
    protected $processingFolder;
193
194
    /**
195
     * All processing folders of this storage used in any storage
196
     *
197
     * @var Folder[]
198
     */
199
    protected $processingFolders;
200
201
    /**
202
     * whether this storage is online or offline in this request
203
     *
204
     * @var bool
205
     */
206
    protected $isOnline;
207
208
    /**
209
     * @var bool
210
     */
211
    protected $isDefault = false;
212
213
    /**
214
     * The filters used for the files and folder names.
215
     *
216
     * @var array
217
     */
218
    protected $fileAndFolderNameFilters = [];
219
220
    /**
221
     * Levels numbers used to generate hashed subfolders in the processing folder
222
     */
223
    const PROCESSING_FOLDER_LEVELS = 2;
224
225
    /**
226
     * Constructor for a storage object.
227
     *
228
     * @param Driver\DriverInterface $driver
229
     * @param array $storageRecord The storage record row from the database
230
     * @param EventDispatcherInterface|null $eventDispatcher
231
     */
232
    public function __construct(DriverInterface $driver, array $storageRecord, EventDispatcherInterface $eventDispatcher = null)
233
    {
234
        $this->storageRecord = $storageRecord;
235
        $this->eventDispatcher = $eventDispatcher ?? GeneralUtility::getContainer()->get(EventDispatcherInterface::class);
236
        if (is_array($storageRecord['configuration'] ?? null)) {
237
            $this->configuration = $storageRecord['configuration'];
238
        } elseif (!empty($storageRecord['configuration'] ?? '')) {
239
            $this->configuration = GeneralUtility::makeInstance(FlexFormService::class)->convertFlexFormContentToArray($storageRecord['configuration']);
240
        } else {
241
            $this->configuration = [];
242
        }
243
        $this->capabilities =
244
            ($this->storageRecord['is_browsable'] ?? null ? self::CAPABILITY_BROWSABLE : 0) |
245
            ($this->storageRecord['is_public'] ?? null ? self::CAPABILITY_PUBLIC : 0) |
246
            ($this->storageRecord['is_writable'] ?? null ? self::CAPABILITY_WRITABLE : 0) |
247
            // Always let the driver decide whether to set this capability
248
            self::CAPABILITY_HIERARCHICAL_IDENTIFIERS;
249
250
        $this->driver = $driver;
251
        $this->driver->setStorageUid($storageRecord['uid'] ?? null);
252
        $this->driver->mergeConfigurationCapabilities($this->capabilities);
253
        try {
254
            $this->driver->processConfiguration();
255
        } catch (InvalidConfigurationException $e) {
256
            // Configuration error
257
            $this->isOnline = false;
258
259
            $message = sprintf(
260
                'Failed initializing storage [%d] "%s", error: %s',
261
                $this->getUid(),
262
                $this->getName(),
263
                $e->getMessage()
264
            );
265
266
            // create a dedicated logger instance because we need a logger in the constructor
267
            GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class)->error($message);
268
        }
269
        $this->driver->initialize();
270
        $this->capabilities = $this->driver->getCapabilities();
271
272
        $this->isDefault = (isset($storageRecord['is_default']) && $storageRecord['is_default'] == 1);
273
        $this->resetFileAndFolderNameFiltersToDefault();
274
    }
275
276
    /**
277
     * Gets the configuration.
278
     *
279
     * @return array
280
     */
281
    public function getConfiguration()
282
    {
283
        return $this->configuration;
284
    }
285
286
    /**
287
     * Sets the configuration.
288
     *
289
     * @param array $configuration
290
     */
291
    public function setConfiguration(array $configuration)
292
    {
293
        $this->configuration = $configuration;
294
    }
295
296
    /**
297
     * Gets the storage record.
298
     *
299
     * @return array
300
     */
301
    public function getStorageRecord()
302
    {
303
        return $this->storageRecord;
304
    }
305
306
    /**
307
     * Sets the storage that belongs to this storage.
308
     *
309
     * @param Driver\DriverInterface $driver
310
     * @return ResourceStorage
311
     */
312
    public function setDriver(DriverInterface $driver)
313
    {
314
        $this->driver = $driver;
315
        return $this;
316
    }
317
318
    /**
319
     * Returns the driver object belonging to this storage.
320
     *
321
     * @return Driver\DriverInterface
322
     */
323
    protected function getDriver()
324
    {
325
        return $this->driver;
326
    }
327
328
    /**
329
     * Returns the name of this storage.
330
     *
331
     * @return string
332
     */
333
    public function getName()
334
    {
335
        return $this->storageRecord['name'];
336
    }
337
338
    /**
339
     * Returns the UID of this storage.
340
     *
341
     * @return int
342
     */
343
    public function getUid()
344
    {
345
        return (int)($this->storageRecord['uid'] ?? 0);
346
    }
347
348
    /**
349
     * Tells whether there are children in this storage.
350
     *
351
     * @return bool
352
     */
353
    public function hasChildren()
354
    {
355
        return true;
356
    }
357
358
    /*********************************
359
     * Capabilities
360
     ********************************/
361
    /**
362
     * Returns the capabilities of this storage.
363
     *
364
     * @return int
365
     * @see \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_BROWSABLE
366
     * @see \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_PUBLIC
367
     * @see \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_WRITABLE
368
     * @see \TYPO3\CMS\Core\Resource\ResourceStorageInterface::CAPABILITY_HIERARCHICAL_IDENTIFIERS
369
     */
370
    public function getCapabilities()
371
    {
372
        return (int)$this->capabilities;
373
    }
374
375
    /**
376
     * Returns TRUE if this storage has the given capability.
377
     *
378
     * @param int $capability A capability, as defined in a CAPABILITY_* constant
379
     * @return bool
380
     */
381
    protected function hasCapability($capability)
382
    {
383
        return ($this->capabilities & $capability) == $capability;
384
    }
385
386
    /**
387
     * Returns TRUE if this storage is publicly available. This is just a
388
     * configuration option and does not mean that it really *is* public. OTOH
389
     * a storage that is marked as not publicly available will trigger the file
390
     * publishing mechanisms of TYPO3.
391
     *
392
     * @return bool
393
     */
394
    public function isPublic()
395
    {
396
        return $this->hasCapability(self::CAPABILITY_PUBLIC);
397
    }
398
399
    /**
400
     * Returns TRUE if this storage is writable. This is determined by the
401
     * driver and the storage configuration; user permissions are not taken into account.
402
     *
403
     * @return bool
404
     */
405
    public function isWritable()
406
    {
407
        return $this->hasCapability(self::CAPABILITY_WRITABLE);
408
    }
409
410
    /**
411
     * Returns TRUE if this storage is browsable by a (backend) user of TYPO3.
412
     *
413
     * @return bool
414
     */
415
    public function isBrowsable()
416
    {
417
        return $this->isOnline() && $this->hasCapability(self::CAPABILITY_BROWSABLE);
418
    }
419
420
    /**
421
     * Returns TRUE if this storage stores folder structure in file identifiers.
422
     *
423
     * @return bool
424
     */
425
    public function hasHierarchicalIdentifiers(): bool
426
    {
427
        return $this->hasCapability(self::CAPABILITY_HIERARCHICAL_IDENTIFIERS);
428
    }
429
430
    /**
431
     * Search for files in a storage based on given restrictions
432
     * and a possibly given folder.
433
     *
434
     * @param FileSearchDemand $searchDemand
435
     * @param Folder|null $folder
436
     * @param bool $useFilters Whether storage filters should be applied
437
     * @return FileSearchResultInterface
438
     */
439
    public function searchFiles(FileSearchDemand $searchDemand, Folder $folder = null, bool $useFilters = true): FileSearchResultInterface
440
    {
441
        $folder = $folder ?? $this->getRootLevelFolder();
442
        if (!$folder->checkActionPermission('read')) {
443
            return new EmptyFileSearchResult();
444
        }
445
446
        return new DriverFilteredSearchResult(
447
            new FileSearchResult(
448
                $searchDemand->withFolder($folder)
449
            ),
450
            $this->driver,
451
            $useFilters ? $this->getFileAndFolderNameFilters() : []
452
        );
453
    }
454
455
    /**
456
     * Returns TRUE if the identifiers used by this storage are case-sensitive.
457
     *
458
     * @return bool
459
     */
460
    public function usesCaseSensitiveIdentifiers()
461
    {
462
        return $this->driver->isCaseSensitiveFileSystem();
463
    }
464
465
    /**
466
     * Returns TRUE if this storage is browsable by a (backend) user of TYPO3.
467
     *
468
     * @return bool
469
     */
470
    public function isOnline()
471
    {
472
        if ($this->isOnline === null) {
473
            if ($this->getUid() === 0) {
474
                $this->isOnline = true;
475
            }
476
            // the storage is not marked as online for a longer time
477
            if ($this->storageRecord['is_online'] == 0) {
478
                $this->isOnline = false;
479
            }
480
            if ($this->isOnline !== false) {
481
                if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
482
                    && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
483
                ) {
484
                    // All files are ALWAYS available in the frontend
485
                    $this->isOnline = true;
486
                } else {
487
                    // check if the storage is disabled temporary for now
488
                    $registryObject = GeneralUtility::makeInstance(Registry::class);
489
                    $offlineUntil = $registryObject->get('core', 'sys_file_storage-' . $this->getUid() . '-offline-until');
490
                    if ($offlineUntil && $offlineUntil > time()) {
491
                        $this->isOnline = false;
492
                    } else {
493
                        $this->isOnline = true;
494
                    }
495
                }
496
            }
497
        }
498
        return $this->isOnline;
499
    }
500
501
    /**
502
     * Returns TRUE if auto extracting of metadata is enabled
503
     *
504
     * @return bool
505
     */
506
    public function autoExtractMetadataEnabled()
507
    {
508
        return !empty($this->storageRecord['auto_extract_metadata']);
509
    }
510
511
    /**
512
     * Blows the "fuse" and marks the storage as offline.
513
     *
514
     * Can only be modified by an admin.
515
     *
516
     * Typically, this is only done if the configuration is wrong.
517
     */
518
    public function markAsPermanentlyOffline()
519
    {
520
        if ($this->getUid() > 0) {
521
            // @todo: move this to the storage repository
522
            GeneralUtility::makeInstance(ConnectionPool::class)
523
                ->getConnectionForTable('sys_file_storage')
524
                ->update(
525
                    'sys_file_storage',
526
                    ['is_online' => 0],
527
                    ['uid' => (int)$this->getUid()]
528
                );
529
        }
530
        $this->storageRecord['is_online'] = 0;
531
        $this->isOnline = false;
532
    }
533
534
    /**
535
     * Marks this storage as offline for the next 5 minutes.
536
     *
537
     * Non-permanent: This typically happens for remote storages
538
     * that are "flaky" and not available all the time.
539
     */
540
    public function markAsTemporaryOffline()
541
    {
542
        $registryObject = GeneralUtility::makeInstance(Registry::class);
543
        $registryObject->set('core', 'sys_file_storage-' . $this->getUid() . '-offline-until', time() + 60 * 5);
544
        $this->storageRecord['is_online'] = 0;
545
        $this->isOnline = false;
546
    }
547
548
    /*********************************
549
     * User Permissions / File Mounts
550
     ********************************/
551
    /**
552
     * Adds a filemount as a "filter" for users to only work on a subset of a
553
     * storage object
554
     *
555
     * @param string $folderIdentifier
556
     * @param array $additionalData
557
     *
558
     * @throws Exception\FolderDoesNotExistException
559
     */
560
    public function addFileMount($folderIdentifier, $additionalData = [])
561
    {
562
        // check for the folder before we add it as a filemount
563
        if ($this->driver->folderExists($folderIdentifier) === false) {
564
            // if there is an error, this is important and should be handled
565
            // as otherwise the user would see the whole storage without any restrictions for the filemounts
566
            throw new FolderDoesNotExistException('Folder for file mount ' . $folderIdentifier . ' does not exist.', 1334427099);
567
        }
568
        $data = $this->driver->getFolderInfoByIdentifier($folderIdentifier);
569
        $folderObject = $this->createFolderObject($data['identifier'], $data['name']);
570
        // Use the canonical identifier instead of the user provided one!
571
        $folderIdentifier = $folderObject->getIdentifier();
572
        if (
573
            !empty($this->fileMounts[$folderIdentifier])
574
            && empty($this->fileMounts[$folderIdentifier]['read_only'])
575
            && !empty($additionalData['read_only'])
576
        ) {
577
            // Do not overwrite a regular mount with a read only mount
578
            return;
579
        }
580
        if (empty($additionalData)) {
581
            $additionalData = [
582
                'path' => $folderIdentifier,
583
                'title' => $folderIdentifier,
584
                'folder' => $folderObject
585
            ];
586
        } else {
587
            $additionalData['folder'] = $folderObject;
588
            if (!isset($additionalData['title'])) {
589
                $additionalData['title'] = $folderIdentifier;
590
            }
591
        }
592
        $this->fileMounts[$folderIdentifier] = $additionalData;
593
    }
594
595
    /**
596
     * Returns all file mounts that are registered with this storage.
597
     *
598
     * @return array
599
     */
600
    public function getFileMounts()
601
    {
602
        return $this->fileMounts;
603
    }
604
605
    /**
606
     * Checks if the given subject is within one of the registered user
607
     * file mounts. If not, working with the file is not permitted for the user.
608
     *
609
     * @param ResourceInterface $subject file or folder
610
     * @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
611
     * @return bool
612
     */
613
    public function isWithinFileMountBoundaries($subject, $checkWriteAccess = false)
614
    {
615
        if (!$this->evaluatePermissions) {
616
            return true;
617
        }
618
        $isWithinFileMount = false;
619
        if (!$subject) {
0 ignored issues
show
introduced by
$subject is of type TYPO3\CMS\Core\Resource\ResourceInterface, thus it always evaluated to true.
Loading history...
620
            $subject = $this->getRootLevelFolder();
621
        }
622
        $identifier = $subject->getIdentifier();
623
624
        // Allow access to processing folder
625
        if ($this->isWithinProcessingFolder($identifier)) {
626
            $isWithinFileMount = true;
627
        } else {
628
            // Check if the identifier of the subject is within at
629
            // least one of the file mounts
630
            $writableFileMountAvailable = false;
631
            foreach ($this->fileMounts as $fileMount) {
632
                /** @var Folder $folder */
633
                $folder = $fileMount['folder'];
634
                if ($this->driver->isWithin($folder->getIdentifier(), $identifier)) {
635
                    $isWithinFileMount = true;
636
                    if (!$checkWriteAccess) {
637
                        break;
638
                    }
639
                    if (empty($fileMount['read_only'])) {
640
                        $writableFileMountAvailable = true;
641
                        break;
642
                    }
643
                }
644
            }
645
            $isWithinFileMount = $checkWriteAccess ? $writableFileMountAvailable : $isWithinFileMount;
646
        }
647
        return $isWithinFileMount;
648
    }
649
650
    /**
651
     * Sets whether the permissions to access or write
652
     * into this storage should be checked or not.
653
     *
654
     * @param bool $evaluatePermissions
655
     */
656
    public function setEvaluatePermissions($evaluatePermissions)
657
    {
658
        $this->evaluatePermissions = (bool)$evaluatePermissions;
659
    }
660
661
    /**
662
     * Gets whether the permissions to access or write
663
     * into this storage should be checked or not.
664
     *
665
     * @return bool $evaluatePermissions
666
     */
667
    public function getEvaluatePermissions()
668
    {
669
        return $this->evaluatePermissions;
670
    }
671
672
    /**
673
     * Sets the user permissions of the storage.
674
     *
675
     * @param array $userPermissions
676
     */
677
    public function setUserPermissions(array $userPermissions)
678
    {
679
        $this->userPermissions = $userPermissions;
680
    }
681
682
    /**
683
     * Checks if the ACL settings allow for a certain action
684
     * (is a user allowed to read a file or copy a folder).
685
     *
686
     * @param string $action
687
     * @param string $type either File or Folder
688
     * @return bool
689
     */
690
    public function checkUserActionPermission($action, $type)
691
    {
692
        if (!$this->evaluatePermissions) {
693
            return true;
694
        }
695
696
        $allow = false;
697
        if (!empty($this->userPermissions[strtolower($action) . ucfirst(strtolower($type))])) {
698
            $allow = true;
699
        }
700
701
        return $allow;
702
    }
703
704
    /**
705
     * Checks if a file operation (= action) is allowed on a
706
     * File/Folder/Storage (= subject).
707
     *
708
     * This method, by design, does not throw exceptions or do logging.
709
     * Besides the usage from other methods in this class, it is also used by
710
     * the Filelist UI to check whether an action is allowed and whether action
711
     * related UI elements should thus be shown (move icon, edit icon, etc.)
712
     *
713
     * @param string $action action, can be read, write, delete, editMeta
714
     * @param FileInterface $file
715
     * @return bool
716
     */
717
    public function checkFileActionPermission($action, FileInterface $file)
718
    {
719
        $isProcessedFile = $file instanceof ProcessedFile;
720
        // Check 1: Allow editing meta data of a file if it is in mount boundaries of a writable file mount
721
        if ($action === 'editMeta') {
722
            return !$isProcessedFile && $this->isWithinFileMountBoundaries($file, true);
723
        }
724
        // Check 2: Does the user have permission to perform the action? e.g. "readFile"
725
        if (!$isProcessedFile && $this->checkUserActionPermission($action, 'File') === false) {
726
            return false;
727
        }
728
        // Check 3: No action allowed on files for denied file extensions
729
        if (!$this->checkFileExtensionPermission($file->getName())) {
730
            return false;
731
        }
732
        $isReadCheck = false;
733
        if (in_array($action, ['read', 'copy', 'move', 'replace'], true)) {
734
            $isReadCheck = true;
735
        }
736
        $isWriteCheck = false;
737
        if (in_array($action, ['add', 'write', 'move', 'rename', 'replace', 'delete'], true)) {
738
            $isWriteCheck = true;
739
        }
740
        // Check 4: Does the user have the right to perform the action?
741
        // (= is he within the file mount borders)
742
        if (!$isProcessedFile && !$this->isWithinFileMountBoundaries($file, $isWriteCheck)) {
743
            return false;
744
        }
745
746
        $isMissing = false;
747
        if (!$isProcessedFile && $file instanceof File) {
748
            $isMissing = $file->isMissing();
749
        }
750
751
        if ($this->driver->fileExists($file->getIdentifier()) === false) {
752
            $file->setMissing(true);
0 ignored issues
show
Bug introduced by
The method setMissing() does not exist on TYPO3\CMS\Core\Resource\FileInterface. It seems like you code against a sub-type of TYPO3\CMS\Core\Resource\FileInterface such as TYPO3\CMS\Core\Resource\File. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

752
            $file->/** @scrutinizer ignore-call */ 
753
                   setMissing(true);
Loading history...
753
            $isMissing = true;
754
        }
755
756
        // Check 5: Check the capabilities of the storage (and the driver)
757
        if ($isWriteCheck && ($isMissing || !$this->isWritable())) {
758
            return false;
759
        }
760
761
        // Check 6: "File permissions" of the driver (only when file isn't marked as missing)
762
        if (!$isMissing) {
763
            $filePermissions = $this->driver->getPermissions($file->getIdentifier());
764
            if ($isReadCheck && !$filePermissions['r']) {
765
                return false;
766
            }
767
            if ($isWriteCheck && !$filePermissions['w']) {
768
                return false;
769
            }
770
        }
771
        return true;
772
    }
773
774
    /**
775
     * Checks if a folder operation (= action) is allowed on a Folder.
776
     *
777
     * This method, by design, does not throw exceptions or do logging.
778
     * See the checkFileActionPermission() method above for the reasons.
779
     *
780
     * @param string $action
781
     * @param Folder $folder
782
     * @return bool
783
     */
784
    public function checkFolderActionPermission($action, Folder $folder = null)
785
    {
786
        // Check 1: Does the user have permission to perform the action? e.g. "writeFolder"
787
        if ($this->checkUserActionPermission($action, 'Folder') === false) {
788
            return false;
789
        }
790
791
        // If we do not have a folder here, we cannot do further checks
792
        if ($folder === null) {
793
            return true;
794
        }
795
796
        $isReadCheck = false;
797
        if (in_array($action, ['read', 'copy'], true)) {
798
            $isReadCheck = true;
799
        }
800
        $isWriteCheck = false;
801
        if (in_array($action, ['add', 'move', 'write', 'delete', 'rename'], true)) {
802
            $isWriteCheck = true;
803
        }
804
        // Check 2: Does the user has the right to perform the action?
805
        // (= is he within the file mount borders)
806
        if (!$this->isWithinFileMountBoundaries($folder, $isWriteCheck)) {
807
            return false;
808
        }
809
        // Check 3: Check the capabilities of the storage (and the driver)
810
        if ($isReadCheck && !$this->isBrowsable()) {
811
            return false;
812
        }
813
        if ($isWriteCheck && !$this->isWritable()) {
814
            return false;
815
        }
816
817
        // Check 4: "Folder permissions" of the driver
818
        $folderPermissions = $this->driver->getPermissions($folder->getIdentifier());
819
        if ($isReadCheck && !$folderPermissions['r']) {
820
            return false;
821
        }
822
        if ($isWriteCheck && !$folderPermissions['w']) {
823
            return false;
824
        }
825
        return true;
826
    }
827
828
    /**
829
     * If the fileName is given, checks it against the
830
     * TYPO3_CONF_VARS[BE][fileDenyPattern] + and if the file extension is allowed.
831
     *
832
     * @param string $fileName full filename
833
     * @return bool TRUE if extension/filename is allowed
834
     */
835
    protected function checkFileExtensionPermission($fileName)
836
    {
837
        $fileName = $this->driver->sanitizeFileName($fileName);
838
        return GeneralUtility::makeInstance(FileNameValidator::class)->isValid($fileName);
839
    }
840
841
    /**
842
     * Assures read permission for given folder.
843
     *
844
     * @param Folder $folder If a folder is given, mountpoints are checked. If not only user folder read permissions are checked.
845
     * @throws Exception\InsufficientFolderAccessPermissionsException
846
     */
847
    protected function assureFolderReadPermission(Folder $folder = null)
848
    {
849
        if (!$this->checkFolderActionPermission('read', $folder)) {
850
            if ($folder === null) {
851
                throw new InsufficientFolderAccessPermissionsException(
852
                    'You are not allowed to read folders',
853
                    1430657869
854
                );
855
            }
856
            throw new InsufficientFolderAccessPermissionsException(
857
                'You are not allowed to access the given folder: "' . $folder->getName() . '"',
858
                1375955684
859
            );
860
        }
861
    }
862
863
    /**
864
     * Assures delete permission for given folder.
865
     *
866
     * @param Folder $folder If a folder is given, mountpoints are checked. If not only user folder delete permissions are checked.
867
     * @param bool $checkDeleteRecursively
868
     * @throws Exception\InsufficientFolderAccessPermissionsException
869
     * @throws Exception\InsufficientFolderWritePermissionsException
870
     * @throws Exception\InsufficientUserPermissionsException
871
     */
872
    protected function assureFolderDeletePermission(Folder $folder, $checkDeleteRecursively)
873
    {
874
        // Check user permissions for recursive deletion if it is requested
875
        if ($checkDeleteRecursively && !$this->checkUserActionPermission('recursivedelete', 'Folder')) {
876
            throw new InsufficientUserPermissionsException('You are not allowed to delete folders recursively', 1377779423);
877
        }
878
        // Check user action permission
879
        if (!$this->checkFolderActionPermission('delete', $folder)) {
880
            throw new InsufficientFolderAccessPermissionsException(
881
                'You are not allowed to delete the given folder: "' . $folder->getName() . '"',
882
                1377779039
883
            );
884
        }
885
        // Check if the user has write permissions to folders
886
        // Would be good if we could check for actual write permissions in the containing folder
887
        // but we cannot since we have no access to the containing folder of this file.
888
        if (!$this->checkUserActionPermission('write', 'Folder')) {
889
            throw new InsufficientFolderWritePermissionsException('Writing to folders is not allowed.', 1377779111);
890
        }
891
    }
892
893
    /**
894
     * Assures read permission for given file.
895
     *
896
     * @param FileInterface $file
897
     * @throws Exception\InsufficientFileAccessPermissionsException
898
     * @throws Exception\IllegalFileExtensionException
899
     */
900
    protected function assureFileReadPermission(FileInterface $file)
901
    {
902
        if (!$this->checkFileActionPermission('read', $file)) {
903
            throw new InsufficientFileAccessPermissionsException(
904
                'You are not allowed to access that file: "' . $file->getName() . '"',
905
                1375955429
906
            );
907
        }
908
        if (!$this->checkFileExtensionPermission($file->getName())) {
909
            throw new IllegalFileExtensionException(
910
                'You are not allowed to use that file extension. File: "' . $file->getName() . '"',
911
                1375955430
912
            );
913
        }
914
    }
915
916
    /**
917
     * Assures write permission for given file.
918
     *
919
     * @param FileInterface $file
920
     * @throws Exception\IllegalFileExtensionException
921
     * @throws Exception\InsufficientFileWritePermissionsException
922
     * @throws Exception\InsufficientUserPermissionsException
923
     */
924
    protected function assureFileWritePermissions(FileInterface $file)
925
    {
926
        // Check if user is allowed to write the file and $file is writable
927
        if (!$this->checkFileActionPermission('write', $file)) {
928
            throw new InsufficientFileWritePermissionsException('Writing to file "' . $file->getIdentifier() . '" is not allowed.', 1330121088);
929
        }
930
        if (!$this->checkFileExtensionPermission($file->getName())) {
931
            throw new IllegalFileExtensionException('You are not allowed to edit a file with extension "' . $file->getExtension() . '"', 1366711933);
932
        }
933
    }
934
935
    /**
936
     * Assure replace permission for given file.
937
     *
938
     * @param FileInterface $file
939
     * @throws Exception\InsufficientFileWritePermissionsException
940
     * @throws Exception\InsufficientFolderWritePermissionsException
941
     */
942
    protected function assureFileReplacePermissions(FileInterface $file)
943
    {
944
        // Check if user is allowed to replace the file and $file is writable
945
        if (!$this->checkFileActionPermission('replace', $file)) {
946
            throw new InsufficientFileWritePermissionsException('Replacing file "' . $file->getIdentifier() . '" is not allowed.', 1436899571);
947
        }
948
        // Check if parentFolder is writable for the user
949
        if (!$this->checkFolderActionPermission('write', $file->getParentFolder())) {
950
            throw new InsufficientFolderWritePermissionsException('You are not allowed to write to the target folder "' . $file->getIdentifier() . '"', 1436899572);
951
        }
952
    }
953
954
    /**
955
     * Assures delete permission for given file.
956
     *
957
     * @param FileInterface $file
958
     * @throws Exception\IllegalFileExtensionException
959
     * @throws Exception\InsufficientFileWritePermissionsException
960
     * @throws Exception\InsufficientFolderWritePermissionsException
961
     */
962
    protected function assureFileDeletePermissions(FileInterface $file)
963
    {
964
        // Check for disallowed file extensions
965
        if (!$this->checkFileExtensionPermission($file->getName())) {
966
            throw new IllegalFileExtensionException('You are not allowed to delete a file with extension "' . $file->getExtension() . '"', 1377778916);
967
        }
968
        // Check further permissions if file is not a processed file
969
        if (!$file instanceof ProcessedFile) {
970
            // Check if user is allowed to delete the file and $file is writable
971
            if (!$this->checkFileActionPermission('delete', $file)) {
972
                throw new InsufficientFileWritePermissionsException('You are not allowed to delete the file "' . $file->getIdentifier() . '"', 1319550425);
973
            }
974
            // Check if the user has write permissions to folders
975
            // Would be good if we could check for actual write permissions in the containing folder
976
            // but we cannot since we have no access to the containing folder of this file.
977
            if (!$this->checkUserActionPermission('write', 'Folder')) {
978
                throw new InsufficientFolderWritePermissionsException('Writing to folders is not allowed.', 1377778702);
979
            }
980
        }
981
    }
982
983
    /**
984
     * Checks if a file/user has the permission to be written to a Folder/Storage.
985
     * If not, throws an exception.
986
     *
987
     * @param Folder $targetFolder The target folder where the file should be written
988
     * @param string $targetFileName The file name which should be written into the storage
989
     *
990
     * @throws Exception\InsufficientFolderWritePermissionsException
991
     * @throws Exception\IllegalFileExtensionException
992
     * @throws Exception\InsufficientUserPermissionsException
993
     */
994
    protected function assureFileAddPermissions($targetFolder, $targetFileName)
995
    {
996
        // Check for a valid file extension
997
        if (!$this->checkFileExtensionPermission($targetFileName)) {
998
            throw new IllegalFileExtensionException('Extension of file name is not allowed in "' . $targetFileName . '"!', 1322120271);
999
        }
1000
        // Makes sure the user is allowed to upload
1001
        if (!$this->checkUserActionPermission('add', 'File')) {
1002
            throw new InsufficientUserPermissionsException('You are not allowed to add files to this storage "' . $this->getUid() . '"', 1376992145);
1003
        }
1004
        // Check if targetFolder is writable
1005
        if (!$this->checkFolderActionPermission('write', $targetFolder)) {
1006
            throw new InsufficientFolderWritePermissionsException('You are not allowed to write to the target folder "' . $targetFolder->getIdentifier() . '"', 1322120356);
1007
        }
1008
    }
1009
1010
    /**
1011
     * Checks if a file has the permission to be uploaded to a Folder/Storage.
1012
     * If not, throws an exception.
1013
     *
1014
     * @param string $localFilePath the temporary file name from $_FILES['file1']['tmp_name']
1015
     * @param Folder $targetFolder The target folder where the file should be uploaded
1016
     * @param string $targetFileName the destination file name $_FILES['file1']['name']
1017
     * @param int $uploadedFileSize
1018
     *
1019
     * @throws Exception\InsufficientFolderWritePermissionsException
1020
     * @throws Exception\UploadException
1021
     * @throws Exception\IllegalFileExtensionException
1022
     * @throws Exception\UploadSizeException
1023
     * @throws Exception\InsufficientUserPermissionsException
1024
     */
1025
    protected function assureFileUploadPermissions($localFilePath, $targetFolder, $targetFileName, $uploadedFileSize)
1026
    {
1027
        // Makes sure this is an uploaded file
1028
        if (!is_uploaded_file($localFilePath)) {
1029
            throw new UploadException('The upload has failed, no uploaded file found!', 1322110455);
1030
        }
1031
        // Max upload size (kb) for files.
1032
        $maxUploadFileSize = GeneralUtility::getMaxUploadFileSize() * 1024;
1033
        if ($maxUploadFileSize > 0 && $uploadedFileSize >= $maxUploadFileSize) {
1034
            unlink($localFilePath);
1035
            throw new UploadSizeException('The uploaded file exceeds the size-limit of ' . $maxUploadFileSize . ' bytes', 1322110041);
1036
        }
1037
        $this->assureFileAddPermissions($targetFolder, $targetFileName);
1038
    }
1039
1040
    /**
1041
     * Checks for permissions to move a file.
1042
     *
1043
     * @throws \RuntimeException
1044
     * @throws Exception\InsufficientFolderAccessPermissionsException
1045
     * @throws Exception\InsufficientUserPermissionsException
1046
     * @throws Exception\IllegalFileExtensionException
1047
     * @param FileInterface $file
1048
     * @param Folder $targetFolder
1049
     * @param string $targetFileName
1050
     */
1051
    protected function assureFileMovePermissions(FileInterface $file, Folder $targetFolder, $targetFileName)
1052
    {
1053
        // Check if targetFolder is within this storage
1054
        if ($this->getUid() !== $targetFolder->getStorage()->getUid()) {
1055
            throw new \RuntimeException('The target folder is not in the same storage. Target folder given: "' . $targetFolder->getIdentifier() . '"', 1422553107);
1056
        }
1057
        // Check for a valid file extension
1058
        if (!$this->checkFileExtensionPermission($targetFileName)) {
1059
            throw new IllegalFileExtensionException('Extension of file name is not allowed in "' . $targetFileName . '"!', 1378243279);
1060
        }
1061
        // Check if user is allowed to move and $file is readable and writable
1062
        if (!$file->getStorage()->checkFileActionPermission('move', $file)) {
1063
            throw new InsufficientUserPermissionsException('You are not allowed to move files to storage "' . $this->getUid() . '"', 1319219349);
1064
        }
1065
        // Check if target folder is writable
1066
        if (!$this->checkFolderActionPermission('write', $targetFolder)) {
1067
            throw new InsufficientFolderAccessPermissionsException('You are not allowed to write to the target folder "' . $targetFolder->getIdentifier() . '"', 1319219350);
1068
        }
1069
    }
1070
1071
    /**
1072
     * Checks for permissions to rename a file.
1073
     *
1074
     * @param FileInterface $file
1075
     * @param string $targetFileName
1076
     * @throws Exception\InsufficientFileWritePermissionsException
1077
     * @throws Exception\IllegalFileExtensionException
1078
     * @throws Exception\InsufficientFileReadPermissionsException
1079
     * @throws Exception\InsufficientUserPermissionsException
1080
     */
1081
    protected function assureFileRenamePermissions(FileInterface $file, $targetFileName)
1082
    {
1083
        // Check if file extension is allowed
1084
        if (!$this->checkFileExtensionPermission($targetFileName) || !$this->checkFileExtensionPermission($file->getName())) {
1085
            throw new IllegalFileExtensionException('You are not allowed to rename a file with this extension. File given: "' . $file->getName() . '"', 1371466663);
1086
        }
1087
        // Check if user is allowed to rename
1088
        if (!$this->checkFileActionPermission('rename', $file)) {
1089
            throw new InsufficientUserPermissionsException('You are not allowed to rename files. File given: "' . $file->getName() . '"', 1319219351);
1090
        }
1091
        // Check if the user is allowed to write to folders
1092
        // Although it would be good to check, we cannot check here if the folder actually is writable
1093
        // because we do not know in which folder the file resides.
1094
        // So we rely on the driver to throw an exception in case the renaming failed.
1095
        if (!$this->checkFolderActionPermission('write')) {
1096
            throw new InsufficientFileWritePermissionsException('You are not allowed to write to folders', 1319219352);
1097
        }
1098
    }
1099
1100
    /**
1101
     * Check if a file has the permission to be copied on a File/Folder/Storage,
1102
     * if not throw an exception
1103
     *
1104
     * @param FileInterface $file
1105
     * @param Folder $targetFolder
1106
     * @param string $targetFileName
1107
     *
1108
     * @throws Exception
1109
     * @throws Exception\InsufficientFolderWritePermissionsException
1110
     * @throws Exception\IllegalFileExtensionException
1111
     * @throws Exception\InsufficientFileReadPermissionsException
1112
     * @throws Exception\InsufficientUserPermissionsException
1113
     */
1114
    protected function assureFileCopyPermissions(FileInterface $file, Folder $targetFolder, $targetFileName)
1115
    {
1116
        // Check if targetFolder is within this storage, this should never happen
1117
        if ($this->getUid() != $targetFolder->getStorage()->getUid()) {
1118
            throw new Exception('The operation of the folder cannot be called by this storage "' . $this->getUid() . '"', 1319550405);
1119
        }
1120
        // Check if user is allowed to copy
1121
        if (!$file->getStorage()->checkFileActionPermission('copy', $file)) {
1122
            throw new InsufficientFileReadPermissionsException('You are not allowed to copy the file "' . $file->getIdentifier() . '"', 1319550426);
1123
        }
1124
        // Check if targetFolder is writable
1125
        if (!$this->checkFolderActionPermission('write', $targetFolder)) {
1126
            throw new InsufficientFolderWritePermissionsException('You are not allowed to write to the target folder "' . $targetFolder->getIdentifier() . '"', 1319550435);
1127
        }
1128
        // Check for a valid file extension
1129
        if (!$this->checkFileExtensionPermission($targetFileName) || !$this->checkFileExtensionPermission($file->getName())) {
1130
            throw new IllegalFileExtensionException('You are not allowed to copy a file of that type.', 1319553317);
1131
        }
1132
    }
1133
1134
    /**
1135
     * Check if a file has the permission to be copied on a File/Folder/Storage,
1136
     * if not throw an exception
1137
     *
1138
     * @param FolderInterface $folderToCopy
1139
     * @param FolderInterface $targetParentFolder
1140
     *
1141
     * @throws Exception
1142
     * @throws Exception\InsufficientFolderWritePermissionsException
1143
     * @throws Exception\IllegalFileExtensionException
1144
     * @throws Exception\InsufficientFileReadPermissionsException
1145
     * @throws Exception\InsufficientUserPermissionsException
1146
     * @throws \RuntimeException
1147
     */
1148
    protected function assureFolderCopyPermissions(FolderInterface $folderToCopy, FolderInterface $targetParentFolder)
1149
    {
1150
        // Check if targetFolder is within this storage, this should never happen
1151
        if ($this->getUid() !== $targetParentFolder->getStorage()->getUid()) {
1152
            throw new Exception('The operation of the folder cannot be called by this storage "' . $this->getUid() . '"', 1377777624);
1153
        }
1154
        if (!$folderToCopy instanceof Folder) {
1155
            throw new \RuntimeException('The folder "' . $folderToCopy->getIdentifier() . '" to copy is not of type folder.', 1384209020);
1156
        }
1157
        // Check if user is allowed to copy and the folder is readable
1158
        if (!$folderToCopy->getStorage()->checkFolderActionPermission('copy', $folderToCopy)) {
1159
            throw new InsufficientFileReadPermissionsException('You are not allowed to copy the folder "' . $folderToCopy->getIdentifier() . '"', 1377777629);
1160
        }
1161
        if (!$targetParentFolder instanceof Folder) {
1162
            throw new \RuntimeException('The target folder "' . $targetParentFolder->getIdentifier() . '" is not of type folder.', 1384209021);
1163
        }
1164
        // Check if targetFolder is writable
1165
        if (!$this->checkFolderActionPermission('write', $targetParentFolder)) {
1166
            throw new InsufficientFolderWritePermissionsException('You are not allowed to write to the target folder "' . $targetParentFolder->getIdentifier() . '"', 1377777635);
1167
        }
1168
    }
1169
1170
    /**
1171
     * Check if a file has the permission to be copied on a File/Folder/Storage,
1172
     * if not throw an exception
1173
     *
1174
     * @param FolderInterface $folderToMove
1175
     * @param FolderInterface $targetParentFolder
1176
     *
1177
     * @throws \InvalidArgumentException
1178
     * @throws Exception\InsufficientFolderWritePermissionsException
1179
     * @throws Exception\IllegalFileExtensionException
1180
     * @throws Exception\InsufficientFileReadPermissionsException
1181
     * @throws Exception\InsufficientUserPermissionsException
1182
     * @throws \RuntimeException
1183
     */
1184
    protected function assureFolderMovePermissions(FolderInterface $folderToMove, FolderInterface $targetParentFolder)
1185
    {
1186
        // Check if targetFolder is within this storage, this should never happen
1187
        if ($this->getUid() !== $targetParentFolder->getStorage()->getUid()) {
1188
            throw new \InvalidArgumentException('Cannot move a folder into a folder that does not belong to this storage.', 1325777289);
1189
        }
1190
        if (!$folderToMove instanceof Folder) {
1191
            throw new \RuntimeException('The folder "' . $folderToMove->getIdentifier() . '" to move is not of type Folder.', 1384209022);
1192
        }
1193
        // Check if user is allowed to move and the folder is writable
1194
        // In fact we would need to check if the parent folder of the folder to move is writable also
1195
        // But as of now we cannot extract the parent folder from this folder
1196
        if (!$folderToMove->getStorage()->checkFolderActionPermission('move', $folderToMove)) {
1197
            throw new InsufficientFileReadPermissionsException('You are not allowed to copy the folder "' . $folderToMove->getIdentifier() . '"', 1377778045);
1198
        }
1199
        if (!$targetParentFolder instanceof Folder) {
1200
            throw new \RuntimeException('The target folder "' . $targetParentFolder->getIdentifier() . '" is not of type Folder.', 1384209023);
1201
        }
1202
        // Check if targetFolder is writable
1203
        if (!$this->checkFolderActionPermission('write', $targetParentFolder)) {
1204
            throw new InsufficientFolderWritePermissionsException('You are not allowed to write to the target folder "' . $targetParentFolder->getIdentifier() . '"', 1377778049);
1205
        }
1206
    }
1207
1208
    /**
1209
     * Clean a fileName from not allowed characters
1210
     *
1211
     * @param string $fileName The name of the file to be add, If not set, the local file name is used
1212
     * @param Folder $targetFolder The target folder where the file should be added
1213
     *
1214
     * @throws \InvalidArgumentException
1215
     * @throws Exception\ExistingTargetFileNameException
1216
     * @return string
1217
     */
1218
    public function sanitizeFileName($fileName, Folder $targetFolder = null)
1219
    {
1220
        $targetFolder = $targetFolder ?: $this->getDefaultFolder();
1221
        $fileName = $this->driver->sanitizeFileName($fileName);
1222
1223
        // The file name could be changed by an event listener
1224
        $fileName = $this->eventDispatcher->dispatch(
1225
            new SanitizeFileNameEvent($fileName, $targetFolder, $this, $this->driver)
1226
        )->getFileName();
1227
1228
        return $fileName;
1229
    }
1230
1231
    /********************
1232
     * FILE ACTIONS
1233
     ********************/
1234
    /**
1235
     * Moves a file from the local filesystem to this storage.
1236
     *
1237
     * @param string $localFilePath The file on the server's hard disk to add
1238
     * @param Folder $targetFolder The target folder where the file should be added
1239
     * @param string $targetFileName The name of the file to be add, If not set, the local file name is used
1240
     * @param string $conflictMode a value of the DuplicationBehavior enumeration
1241
     * @param bool $removeOriginal if set the original file will be removed after successful operation
1242
     *
1243
     * @throws \InvalidArgumentException
1244
     * @throws Exception\ExistingTargetFileNameException
1245
     * @return FileInterface
1246
     */
1247
    public function addFile($localFilePath, Folder $targetFolder, $targetFileName = '', $conflictMode = DuplicationBehavior::RENAME, $removeOriginal = true)
1248
    {
1249
        $localFilePath = PathUtility::getCanonicalPath($localFilePath);
1250
        // File is not available locally NOR is it an uploaded file
1251
        if (!is_uploaded_file($localFilePath) && !file_exists($localFilePath)) {
1252
            throw new \InvalidArgumentException('File "' . $localFilePath . '" does not exist.', 1319552745);
1253
        }
1254
        $conflictMode = DuplicationBehavior::cast($conflictMode);
1255
        $targetFileName = $this->sanitizeFileName($targetFileName ?: PathUtility::basename($localFilePath), $targetFolder);
1256
1257
        $targetFileName = $this->eventDispatcher->dispatch(
1258
            new BeforeFileAddedEvent($targetFileName, $localFilePath, $targetFolder, $this, $this->driver)
1259
        )->getFileName();
1260
1261
        $this->assureFileAddPermissions($targetFolder, $targetFileName);
1262
1263
        $replaceExisting = false;
1264
        if ($conflictMode->equals(DuplicationBehavior::CANCEL) && $this->driver->fileExistsInFolder($targetFileName, $targetFolder->getIdentifier())) {
1265
            throw new ExistingTargetFileNameException('File "' . $targetFileName . '" already exists in folder ' . $targetFolder->getIdentifier(), 1322121068);
1266
        }
1267
        if ($conflictMode->equals(DuplicationBehavior::RENAME)) {
1268
            $targetFileName = $this->getUniqueName($targetFolder, $targetFileName);
1269
        } elseif ($conflictMode->equals(DuplicationBehavior::REPLACE) && $this->driver->fileExistsInFolder($targetFileName, $targetFolder->getIdentifier())) {
1270
            $replaceExisting = true;
1271
        }
1272
1273
        $fileIdentifier = $this->driver->addFile($localFilePath, $targetFolder->getIdentifier(), $targetFileName, $removeOriginal);
1274
        $file = $this->getFileByIdentifier($fileIdentifier);
1275
1276
        if ($replaceExisting && $file instanceof File) {
1277
            $this->getIndexer()->updateIndexEntry($file);
1278
        }
1279
1280
        $this->eventDispatcher->dispatch(
1281
            new AfterFileAddedEvent($file, $targetFolder)
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type null; however, parameter $file of TYPO3\CMS\Core\Resource\...dedEvent::__construct() does only seem to accept TYPO3\CMS\Core\Resource\FileInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1281
            new AfterFileAddedEvent(/** @scrutinizer ignore-type */ $file, $targetFolder)
Loading history...
1282
        );
1283
        return $file;
1284
    }
1285
1286
    /**
1287
     * Updates a processed file with a new file from the local filesystem.
1288
     *
1289
     * @param string $localFilePath
1290
     * @param ProcessedFile $processedFile
1291
     * @param Folder $processingFolder
1292
     * @return FileInterface
1293
     * @throws \InvalidArgumentException
1294
     * @internal use only
1295
     */
1296
    public function updateProcessedFile($localFilePath, ProcessedFile $processedFile, Folder $processingFolder = null)
1297
    {
1298
        if (!file_exists($localFilePath)) {
1299
            throw new \InvalidArgumentException('File "' . $localFilePath . '" does not exist.', 1319552746);
1300
        }
1301
        if ($processingFolder === null) {
1302
            $processingFolder = $this->getProcessingFolder($processedFile->getOriginalFile());
1303
        }
1304
        $fileIdentifier = $this->driver->addFile($localFilePath, $processingFolder->getIdentifier(), $processedFile->getName());
1305
        // @todo check if we have to update the processed file other then the identifier
1306
        $processedFile->setIdentifier($fileIdentifier);
1307
        return $processedFile;
1308
    }
1309
1310
    /**
1311
     * Creates a (cryptographic) hash for a file.
1312
     *
1313
     * @param FileInterface $fileObject
1314
     * @param string $hash
1315
     * @throws \TYPO3\CMS\Core\Resource\Exception\InvalidHashException
1316
     * @return string
1317
     */
1318
    public function hashFile(FileInterface $fileObject, $hash)
1319
    {
1320
        return $this->hashFileByIdentifier($fileObject->getIdentifier(), $hash);
1321
    }
1322
1323
    /**
1324
     * Creates a (cryptographic) hash for a fileIdentifier.
1325
     *
1326
     * @param string $fileIdentifier
1327
     * @param string $hash
1328
     * @throws \TYPO3\CMS\Core\Resource\Exception\InvalidHashException
1329
     * @return string
1330
     */
1331
    public function hashFileByIdentifier($fileIdentifier, $hash)
1332
    {
1333
        $hash = $this->driver->hash($fileIdentifier, $hash);
1334
        if (!is_string($hash) || $hash === '') {
0 ignored issues
show
introduced by
The condition is_string($hash) is always true.
Loading history...
1335
            throw new InvalidHashException('Hash has to be non-empty string.', 1551950301);
1336
        }
1337
        return $hash;
1338
    }
1339
1340
    /**
1341
     * Hashes a file identifier, taking the case sensitivity of the file system
1342
     * into account. This helps mitigating problems with case-insensitive
1343
     * databases.
1344
     *
1345
     * @param string|FileInterface $file
1346
     * @return string
1347
     */
1348
    public function hashFileIdentifier($file)
1349
    {
1350
        if (is_object($file) && $file instanceof FileInterface) {
1351
            /** @var FileInterface $file */
1352
            $file = $file->getIdentifier();
1353
        }
1354
        return $this->driver->hashIdentifier($file);
1355
    }
1356
1357
    /**
1358
     * Returns a publicly accessible URL for a file.
1359
     *
1360
     * WARNING: Access to the file may be restricted by further means, e.g.
1361
     * some web-based authentication. You have to take care of this yourself.
1362
     *
1363
     * @param ResourceInterface $resourceObject The file or folder object
1364
     * @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). Deprecated since TYPO3 v11, will be removed in TYPO3 v12.0
1365
     * @return string|null NULL if file is missing or deleted, the generated url otherwise
1366
     */
1367
    public function getPublicUrl(ResourceInterface $resourceObject, $relativeToCurrentScript = false)
1368
    {
1369
        if ($relativeToCurrentScript !== false) {
1370
            trigger_error('FAL API usage with "getPublicUrl" returning a relative path will be removed in TYPO3 v12.0.', E_USER_DEPRECATED);
1371
        }
1372
        $publicUrl = null;
1373
        if ($this->isOnline()) {
1374
            // Pre-process the public URL by an accordant event
1375
            $event = new GeneratePublicUrlForResourceEvent($resourceObject, $this, $this->driver, $relativeToCurrentScript);
1376
            $publicUrl = $this->eventDispatcher->dispatch($event)->getPublicUrl();
1377
            if (
1378
                $publicUrl === null
1379
                && $resourceObject instanceof File
1380
                && ($helper = OnlineMediaHelperRegistry::getInstance()->getOnlineMediaHelper($resourceObject)) !== false
0 ignored issues
show
introduced by
The condition $helper = TYPO3\CMS\Core...sourceObject) !== false is always false.
Loading history...
1381
            ) {
1382
                $publicUrl = $helper->getPublicUrl($resourceObject, $relativeToCurrentScript);
1383
            }
1384
1385
            // If an event listener did not handle the URL generation, use the default way to determine public URL
1386
            if ($publicUrl === null) {
1387
                if ($this->hasCapability(self::CAPABILITY_PUBLIC)) {
1388
                    $publicUrl = $this->driver->getPublicUrl($resourceObject->getIdentifier());
1389
                }
1390
1391
                if ($publicUrl === null && $resourceObject instanceof FileInterface) {
1392
                    $queryParameterArray = ['eID' => 'dumpFile', 't' => ''];
1393
                    if ($resourceObject instanceof File) {
1394
                        $queryParameterArray['f'] = $resourceObject->getUid();
1395
                        $queryParameterArray['t'] = 'f';
1396
                    } elseif ($resourceObject instanceof ProcessedFile) {
1397
                        $queryParameterArray['p'] = $resourceObject->getUid();
1398
                        $queryParameterArray['t'] = 'p';
1399
                    }
1400
1401
                    $queryParameterArray['token'] = GeneralUtility::hmac(implode('|', $queryParameterArray), 'resourceStorageDumpFile');
1402
                    $publicUrl = GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'));
1403
                    $publicUrl .= '?' . http_build_query($queryParameterArray, '', '&', PHP_QUERY_RFC3986);
1404
                }
1405
1406
                // If requested, make the path relative to the current script in order to make it possible
1407
                // to use the relative file
1408
                if ($publicUrl !== null && $relativeToCurrentScript && !GeneralUtility::isValidUrl($publicUrl)) {
1409
                    $absolutePathToContainingFolder = PathUtility::dirname(Environment::getPublicPath() . '/' . $publicUrl);
1410
                    $pathPart = PathUtility::getRelativePathTo($absolutePathToContainingFolder);
1411
                    $filePart = substr(Environment::getPublicPath() . '/' . $publicUrl, strlen($absolutePathToContainingFolder) + 1);
1412
                    $publicUrl = $pathPart . $filePart;
1413
                }
1414
            }
1415
        }
1416
        return $publicUrl;
1417
    }
1418
1419
    /**
1420
     * Passes a file to the File Processing Services and returns the resulting ProcessedFile object.
1421
     *
1422
     * @param FileInterface $fileObject The file object
1423
     * @param string $context
1424
     * @param array $configuration
1425
     *
1426
     * @return ProcessedFile
1427
     * @throws \InvalidArgumentException
1428
     */
1429
    public function processFile(FileInterface $fileObject, $context, array $configuration)
1430
    {
1431
        if ($fileObject->getStorage() !== $this) {
1432
            throw new \InvalidArgumentException('Cannot process files of foreign storage', 1353401835);
1433
        }
1434
        $processedFile = $this->getFileProcessingService()->processFile($fileObject, $this, $context, $configuration);
1435
1436
        return $processedFile;
1437
    }
1438
1439
    /**
1440
     * Copies a file from the storage for local processing.
1441
     *
1442
     * @param FileInterface $fileObject
1443
     * @param bool $writable
1444
     * @return string Path to local file (either original or copied to some temporary local location)
1445
     */
1446
    public function getFileForLocalProcessing(FileInterface $fileObject, $writable = true)
1447
    {
1448
        $filePath = $this->driver->getFileForLocalProcessing($fileObject->getIdentifier(), $writable);
1449
        return $filePath;
1450
    }
1451
1452
    /**
1453
     * Gets a file by identifier.
1454
     *
1455
     * @param string $identifier
1456
     * @return FileInterface
1457
     */
1458
    public function getFile($identifier)
1459
    {
1460
        $file = $this->getFileByIdentifier($identifier);
1461
        if (!$this->driver->fileExists($identifier)) {
1462
            $file->setMissing(true);
0 ignored issues
show
Bug introduced by
The method setMissing() does not exist on TYPO3\CMS\Core\Resource\ProcessedFile. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1462
            $file->/** @scrutinizer ignore-call */ 
1463
                   setMissing(true);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1463
        }
1464
        return $file;
1465
    }
1466
1467
    /**
1468
     * Gets a file object from storage by file identifier
1469
     * If the file is outside of the process folder, it gets indexed and returned as file object afterwards
1470
     * If the file is within processing folder, the file object will be directly returned
1471
     *
1472
     * @param string $fileIdentifier
1473
     * @return File|ProcessedFile|null
1474
     */
1475
    public function getFileByIdentifier(string $fileIdentifier)
1476
    {
1477
        if (!$this->isWithinProcessingFolder($fileIdentifier)) {
1478
            $fileData = $this->getFileIndexRepository()->findOneByStorageAndIdentifier($this, $fileIdentifier);
1479
            if ($fileData === false) {
1480
                return $this->getIndexer()->createIndexEntry($fileIdentifier);
1481
            }
1482
            return $this->getResourceFactoryInstance()->getFileObject($fileData['uid'], $fileData);
1483
        }
1484
        return $this->getProcessedFileRepository()->findByStorageAndIdentifier($this, $fileIdentifier);
1485
    }
1486
1487
    protected function getProcessedFileRepository(): ProcessedFileRepository
1488
    {
1489
        return GeneralUtility::makeInstance(ProcessedFileRepository::class);
1490
    }
1491
1492
    /**
1493
     * Gets information about a file.
1494
     *
1495
     * @param FileInterface $fileObject
1496
     * @return array
1497
     * @internal
1498
     */
1499
    public function getFileInfo(FileInterface $fileObject)
1500
    {
1501
        return $this->getFileInfoByIdentifier($fileObject->getIdentifier());
1502
    }
1503
1504
    /**
1505
     * Gets information about a file by its identifier.
1506
     *
1507
     * @param string $identifier
1508
     * @param array $propertiesToExtract
1509
     * @return array
1510
     * @internal
1511
     */
1512
    public function getFileInfoByIdentifier($identifier, array $propertiesToExtract = [])
1513
    {
1514
        return $this->driver->getFileInfoByIdentifier($identifier, $propertiesToExtract);
1515
    }
1516
1517
    /**
1518
     * Unsets the file and folder name filters, thus making this storage return unfiltered filelists.
1519
     */
1520
    public function unsetFileAndFolderNameFilters()
1521
    {
1522
        $this->fileAndFolderNameFilters = [];
1523
    }
1524
1525
    /**
1526
     * Resets the file and folder name filters to the default values defined in the TYPO3 configuration.
1527
     */
1528
    public function resetFileAndFolderNameFiltersToDefault()
1529
    {
1530
        $this->fileAndFolderNameFilters = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['defaultFilterCallbacks'];
1531
    }
1532
1533
    /**
1534
     * Returns the file and folder name filters used by this storage.
1535
     *
1536
     * @return array
1537
     */
1538
    public function getFileAndFolderNameFilters()
1539
    {
1540
        return $this->fileAndFolderNameFilters;
1541
    }
1542
1543
    /**
1544
     * @param array $filters
1545
     * @return $this
1546
     */
1547
    public function setFileAndFolderNameFilters(array $filters)
1548
    {
1549
        $this->fileAndFolderNameFilters = $filters;
1550
        return $this;
1551
    }
1552
1553
    /**
1554
     * @param callable $filter
1555
     */
1556
    public function addFileAndFolderNameFilter($filter)
1557
    {
1558
        $this->fileAndFolderNameFilters[] = $filter;
1559
    }
1560
1561
    /**
1562
     * @param string $fileIdentifier
1563
     *
1564
     * @return string
1565
     */
1566
    public function getFolderIdentifierFromFileIdentifier($fileIdentifier)
1567
    {
1568
        return $this->driver->getParentFolderIdentifierOfIdentifier($fileIdentifier);
1569
    }
1570
1571
    /**
1572
     * Get file from folder
1573
     *
1574
     * @param string $fileName
1575
     * @param Folder $folder
1576
     * @return File|ProcessedFile|null
1577
     */
1578
    public function getFileInFolder($fileName, Folder $folder)
1579
    {
1580
        $identifier = $this->driver->getFileInFolder($fileName, $folder->getIdentifier());
1581
        return $this->getFileByIdentifier($identifier);
1582
    }
1583
1584
    /**
1585
     * @param Folder $folder
1586
     * @param int $start
1587
     * @param int $maxNumberOfItems
1588
     * @param bool $useFilters
1589
     * @param bool $recursive
1590
     * @param string $sort Property name used to sort the items.
1591
     *                     Among them may be: '' (empty, no sorting), name,
1592
     *                     fileext, size, tstamp and rw.
1593
     *                     If a driver does not support the given property, it
1594
     *                     should fall back to "name".
1595
     * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
1596
     * @return File[]
1597
     * @throws Exception\InsufficientFolderAccessPermissionsException
1598
     */
1599
    public function getFilesInFolder(Folder $folder, $start = 0, $maxNumberOfItems = 0, $useFilters = true, $recursive = false, $sort = '', $sortRev = false)
1600
    {
1601
        $this->assureFolderReadPermission($folder);
1602
1603
        $rows = $this->getFileIndexRepository()->findByFolder($folder);
1604
1605
        $filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1606
        $fileIdentifiers = array_values($this->driver->getFilesInFolder($folder->getIdentifier(), $start, $maxNumberOfItems, $recursive, $filters, $sort, $sortRev));
1607
1608
        $items = [];
1609
        foreach ($fileIdentifiers as $identifier) {
1610
            if (isset($rows[$identifier])) {
1611
                $fileObject = $this->getFileFactory()->getFileObject($rows[$identifier]['uid'], $rows[$identifier]);
1612
            } else {
1613
                $fileObject = $this->getFileByIdentifier($identifier);
1614
            }
1615
            if ($fileObject instanceof FileInterface) {
1616
                $key = $fileObject->getName();
1617
                while (isset($items[$key])) {
1618
                    $key .= 'z';
1619
                }
1620
                $items[$key] = $fileObject;
1621
            }
1622
        }
1623
1624
        return $items;
1625
    }
1626
1627
    /**
1628
     * @param string $folderIdentifier
1629
     * @param bool $useFilters
1630
     * @param bool $recursive
1631
     * @return array
1632
     */
1633
    public function getFileIdentifiersInFolder($folderIdentifier, $useFilters = true, $recursive = false)
1634
    {
1635
        $filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1636
        return $this->driver->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filters);
1637
    }
1638
1639
    /**
1640
     * @param Folder $folder
1641
     * @param bool $useFilters
1642
     * @param bool $recursive
1643
     * @return int Number of files in folder
1644
     * @throws Exception\InsufficientFolderAccessPermissionsException
1645
     */
1646
    public function countFilesInFolder(Folder $folder, $useFilters = true, $recursive = false)
1647
    {
1648
        $this->assureFolderReadPermission($folder);
1649
        $filters = $useFilters ? $this->fileAndFolderNameFilters : [];
1650
        return $this->driver->countFilesInFolder($folder->getIdentifier(), $recursive, $filters);
1651
    }
1652
1653
    /**
1654
     * @param string $folderIdentifier
1655
     * @param bool $useFilters
1656
     * @param bool $recursive
1657
     * @return array
1658
     */
1659
    public function getFolderIdentifiersInFolder($folderIdentifier, $useFilters = true, $recursive = false)
1660
    {
1661
        $filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1662
        return $this->driver->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $filters);
1663
    }
1664
1665
    /**
1666
     * Returns TRUE if the specified file exists
1667
     *
1668
     * @param string $identifier
1669
     * @return bool
1670
     */
1671
    public function hasFile($identifier)
1672
    {
1673
        // Allow if identifier is in processing folder
1674
        if (!$this->isWithinProcessingFolder($identifier)) {
1675
            $this->assureFolderReadPermission();
1676
        }
1677
        return $this->driver->fileExists($identifier);
1678
    }
1679
1680
    /**
1681
     * Get all processing folders that live in this storage
1682
     *
1683
     * @return Folder[]
1684
     */
1685
    public function getProcessingFolders()
1686
    {
1687
        if ($this->processingFolders === null) {
1688
            $this->processingFolders = [];
1689
            $this->processingFolders[] = $this->getProcessingFolder();
1690
            /** @var StorageRepository $storageRepository */
1691
            $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
1692
            $allStorages = $storageRepository->findAll();
1693
            foreach ($allStorages as $storage) {
1694
                // To circumvent the permission check of the folder, we use the factory to create it "manually" instead of directly using $storage->getProcessingFolder()
1695
                // See #66695 for details
1696
                [$storageUid, $processingFolderIdentifier] = array_pad(GeneralUtility::trimExplode(':', $storage->getStorageRecord()['processingfolder']), 2, null);
1697
                if (empty($processingFolderIdentifier) || (int)$storageUid !== $this->getUid()) {
1698
                    continue;
1699
                }
1700
                $potentialProcessingFolder = $this->createFolderObject($processingFolderIdentifier, $processingFolderIdentifier);
1701
                if ($potentialProcessingFolder->getStorage() === $this && $potentialProcessingFolder->getIdentifier() !== $this->getProcessingFolder()->getIdentifier()) {
1702
                    $this->processingFolders[] = $potentialProcessingFolder;
1703
                }
1704
            }
1705
        }
1706
1707
        return $this->processingFolders;
1708
    }
1709
1710
    /**
1711
     * Returns TRUE if folder that is in current storage  is set as
1712
     * processing folder for one of the existing storages
1713
     *
1714
     * @param Folder $folder
1715
     * @return bool
1716
     */
1717
    public function isProcessingFolder(Folder $folder)
1718
    {
1719
        $isProcessingFolder = false;
1720
        foreach ($this->getProcessingFolders() as $processingFolder) {
1721
            if ($folder->getCombinedIdentifier() === $processingFolder->getCombinedIdentifier()) {
1722
                $isProcessingFolder = true;
1723
                break;
1724
            }
1725
        }
1726
        return $isProcessingFolder;
1727
    }
1728
1729
    /**
1730
     * Checks if the queried file in the given folder exists
1731
     *
1732
     * @param string $fileName
1733
     * @param Folder $folder
1734
     * @return bool
1735
     */
1736
    public function hasFileInFolder($fileName, Folder $folder)
1737
    {
1738
        $this->assureFolderReadPermission($folder);
1739
        return $this->driver->fileExistsInFolder($fileName, $folder->getIdentifier());
1740
    }
1741
1742
    /**
1743
     * Get contents of a file object
1744
     *
1745
     * @param FileInterface $file
1746
     *
1747
     * @throws Exception\InsufficientFileReadPermissionsException
1748
     * @return string
1749
     */
1750
    public function getFileContents($file)
1751
    {
1752
        $this->assureFileReadPermission($file);
1753
        return $this->driver->getFileContents($file->getIdentifier());
1754
    }
1755
1756
    /**
1757
     * Returns a PSR-7 Response which can be used to stream the requested file
1758
     *
1759
     * @param FileInterface $file
1760
     * @param bool $asDownload If set Content-Disposition attachment is sent, inline otherwise
1761
     * @param string $alternativeFilename the filename for the download (if $asDownload is set)
1762
     * @param string $overrideMimeType If set this will be used as Content-Type header instead of the automatically detected mime type.
1763
     * @return ResponseInterface
1764
     */
1765
    public function streamFile(
1766
        FileInterface $file,
1767
        bool $asDownload = false,
1768
        string $alternativeFilename = null,
1769
        string $overrideMimeType = null
1770
    ): ResponseInterface {
1771
        if (!$this->driver instanceof StreamableDriverInterface) {
1772
            return $this->getPseudoStream($file, $asDownload, $alternativeFilename, $overrideMimeType);
1773
        }
1774
1775
        $properties = [
1776
            'as_download' => $asDownload,
1777
            'filename_overwrite' => $alternativeFilename,
1778
            'mimetype_overwrite' => $overrideMimeType,
1779
        ];
1780
        return $this->driver->streamFile($file->getIdentifier(), $properties);
1781
    }
1782
1783
    /**
1784
     * Wrap DriverInterface::dumpFileContents into a SelfEmittableStreamInterface
1785
     *
1786
     * @param FileInterface $file
1787
     * @param bool $asDownload If set Content-Disposition attachment is sent, inline otherwise
1788
     * @param string $alternativeFilename the filename for the download (if $asDownload is set)
1789
     * @param string $overrideMimeType If set this will be used as Content-Type header instead of the automatically detected mime type.
1790
     * @return ResponseInterface
1791
     */
1792
    protected function getPseudoStream(
1793
        FileInterface $file,
1794
        bool $asDownload = false,
1795
        string $alternativeFilename = null,
1796
        string $overrideMimeType = null
1797
    ) {
1798
        $downloadName = $alternativeFilename ?: $file->getName();
1799
        $contentDisposition = $asDownload ? 'attachment' : 'inline';
1800
1801
        $stream = new FalDumpFileContentsDecoratorStream($file->getIdentifier(), $this->driver, $file->getSize());
1802
        $fileInfo = $this->driver->getFileInfoByIdentifier($file->getIdentifier(), ['mtime']);
1803
        $headers = [
1804
            'Content-Disposition' => $contentDisposition . '; filename="' . $downloadName . '"',
1805
            'Content-Type' => $overrideMimeType ?: $file->getMimeType(),
1806
            'Content-Length' => (string)$file->getSize(),
1807
            'Last-Modified' => gmdate('D, d M Y H:i:s', array_pop($fileInfo)) . ' GMT',
1808
            // Cache-Control header is needed here to solve an issue with browser IE8 and lower
1809
            // See for more information: http://support.microsoft.com/kb/323308
1810
            'Cache-Control' => '',
1811
        ];
1812
1813
        return new Response($stream, 200, $headers);
1814
    }
1815
1816
    /**
1817
     * Set contents of a file object.
1818
     *
1819
     * @param AbstractFile $file
1820
     * @param string $contents
1821
     *
1822
     * @throws \Exception|\RuntimeException
1823
     * @throws Exception\InsufficientFileWritePermissionsException
1824
     * @throws Exception\InsufficientUserPermissionsException
1825
     * @return int The number of bytes written to the file
1826
     */
1827
    public function setFileContents(AbstractFile $file, $contents)
1828
    {
1829
        // Check if user is allowed to edit
1830
        $this->assureFileWritePermissions($file);
1831
        $this->eventDispatcher->dispatch(
1832
            new BeforeFileContentsSetEvent($file, $contents)
1833
        );
1834
        // Call driver method to update the file and update file index entry afterwards
1835
        $result = $this->driver->setFileContents($file->getIdentifier(), $contents);
1836
        if ($file instanceof File) {
1837
            $this->getIndexer()->updateIndexEntry($file);
1838
        }
1839
        $this->eventDispatcher->dispatch(
1840
            new AfterFileContentsSetEvent($file, $contents)
1841
        );
1842
        return $result;
1843
    }
1844
1845
    /**
1846
     * Creates a new file
1847
     *
1848
     * previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_newfile()
1849
     *
1850
     * @param string $fileName The name of the file to be created
1851
     * @param Folder $targetFolderObject The target folder where the file should be created
1852
     *
1853
     * @throws Exception\IllegalFileExtensionException
1854
     * @throws Exception\InsufficientFolderWritePermissionsException
1855
     * @return FileInterface The file object
1856
     */
1857
    public function createFile($fileName, Folder $targetFolderObject)
1858
    {
1859
        $this->assureFileAddPermissions($targetFolderObject, $fileName);
1860
        $this->eventDispatcher->dispatch(
1861
            new BeforeFileCreatedEvent($fileName, $targetFolderObject)
1862
        );
1863
        $newFileIdentifier = $this->driver->createFile($fileName, $targetFolderObject->getIdentifier());
1864
        $this->eventDispatcher->dispatch(
1865
            new AfterFileCreatedEvent($newFileIdentifier, $targetFolderObject)
1866
        );
1867
        return $this->getFileByIdentifier($newFileIdentifier);
1868
    }
1869
1870
    /**
1871
     * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::deleteFile()
1872
     *
1873
     * @param FileInterface $fileObject
1874
     * @throws Exception\InsufficientFileAccessPermissionsException
1875
     * @throws Exception\FileOperationErrorException
1876
     * @return bool TRUE if deletion succeeded
1877
     */
1878
    public function deleteFile($fileObject)
1879
    {
1880
        $this->assureFileDeletePermissions($fileObject);
1881
1882
        $this->eventDispatcher->dispatch(
1883
            new BeforeFileDeletedEvent($fileObject)
1884
        );
1885
        $deleted = true;
1886
1887
        if ($this->driver->fileExists($fileObject->getIdentifier())) {
1888
            // Disable permission check to find nearest recycler and move file without errors
1889
            $currentPermissions = $this->evaluatePermissions;
1890
            $this->evaluatePermissions = false;
1891
1892
            $recyclerFolder = $this->getNearestRecyclerFolder($fileObject);
1893
            if ($recyclerFolder === null) {
1894
                $result = $this->driver->deleteFile($fileObject->getIdentifier());
1895
            } else {
1896
                $result = $this->moveFile($fileObject, $recyclerFolder);
1897
                $deleted = false;
1898
            }
1899
1900
            $this->evaluatePermissions = $currentPermissions;
1901
1902
            if (!$result) {
1903
                throw new FileOperationErrorException('Deleting the file "' . $fileObject->getIdentifier() . '\' failed.', 1329831691);
1904
            }
1905
        }
1906
        // Mark the file object as deleted
1907
        if ($deleted && $fileObject instanceof AbstractFile) {
1908
            $fileObject->setDeleted();
1909
        }
1910
1911
        $this->eventDispatcher->dispatch(
1912
            new AfterFileDeletedEvent($fileObject)
1913
        );
1914
1915
        return true;
1916
    }
1917
1918
    /**
1919
     * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_copy()
1920
     * copies a source file (from any location) in to the target
1921
     * folder, the latter has to be part of this storage
1922
     *
1923
     * @param FileInterface $file
1924
     * @param Folder $targetFolder
1925
     * @param string $targetFileName an optional destination fileName
1926
     * @param string $conflictMode a value of the DuplicationBehavior enumeration
1927
     *
1928
     * @throws \Exception|Exception\AbstractFileOperationException
1929
     * @throws Exception\ExistingTargetFileNameException
1930
     * @return FileInterface
1931
     */
1932
    public function copyFile(FileInterface $file, Folder $targetFolder, $targetFileName = null, $conflictMode = DuplicationBehavior::RENAME)
1933
    {
1934
        $conflictMode = DuplicationBehavior::cast($conflictMode);
1935
        if ($targetFileName === null) {
1936
            $targetFileName = $file->getName();
1937
        }
1938
        $sanitizedTargetFileName = $this->driver->sanitizeFileName($targetFileName);
1939
        $this->assureFileCopyPermissions($file, $targetFolder, $sanitizedTargetFileName);
1940
1941
        $this->eventDispatcher->dispatch(
1942
            new BeforeFileCopiedEvent($file, $targetFolder)
1943
        );
1944
1945
        // File exists and we should abort, let's abort
1946
        if ($conflictMode->equals(DuplicationBehavior::CANCEL) && $targetFolder->hasFile($sanitizedTargetFileName)) {
1947
            throw new ExistingTargetFileNameException('The target file already exists.', 1320291064);
1948
        }
1949
        // File exists and we should find another name, let's find another one
1950
        if ($conflictMode->equals(DuplicationBehavior::RENAME) && $targetFolder->hasFile($sanitizedTargetFileName)) {
1951
            $sanitizedTargetFileName = $this->getUniqueName($targetFolder, $sanitizedTargetFileName);
1952
        }
1953
        $sourceStorage = $file->getStorage();
1954
        // Call driver method to create a new file from an existing file object,
1955
        // and return the new file object
1956
        if ($sourceStorage === $this) {
1957
            $newFileObjectIdentifier = $this->driver->copyFileWithinStorage($file->getIdentifier(), $targetFolder->getIdentifier(), $sanitizedTargetFileName);
1958
        } else {
1959
            $tempPath = $file->getForLocalProcessing();
1960
            $newFileObjectIdentifier = $this->driver->addFile($tempPath, $targetFolder->getIdentifier(), $sanitizedTargetFileName);
1961
        }
1962
        $newFileObject = $this->getFileByIdentifier($newFileObjectIdentifier);
1963
1964
        $this->eventDispatcher->dispatch(
1965
            new AfterFileCopiedEvent($file, $targetFolder, $newFileObjectIdentifier, $newFileObject)
1966
        );
1967
        return $newFileObject;
1968
    }
1969
1970
    /**
1971
     * Moves a $file into a $targetFolder
1972
     * the target folder has to be part of this storage
1973
     *
1974
     * previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_move()
1975
     *
1976
     * @param FileInterface $file
1977
     * @param Folder $targetFolder
1978
     * @param string $targetFileName an optional destination fileName
1979
     * @param string $conflictMode a value of the DuplicationBehavior enumeration
1980
     *
1981
     * @throws Exception\ExistingTargetFileNameException
1982
     * @throws \RuntimeException
1983
     * @return FileInterface
1984
     */
1985
    public function moveFile($file, $targetFolder, $targetFileName = null, $conflictMode = DuplicationBehavior::RENAME)
1986
    {
1987
        $conflictMode = DuplicationBehavior::cast($conflictMode);
1988
        if ($targetFileName === null) {
1989
            $targetFileName = $file->getName();
1990
        }
1991
        $originalFolder = $file->getParentFolder();
1992
        $sanitizedTargetFileName = $this->driver->sanitizeFileName($targetFileName);
1993
        $this->assureFileMovePermissions($file, $targetFolder, $sanitizedTargetFileName);
1994
        if ($targetFolder->hasFile($sanitizedTargetFileName)) {
1995
            // File exists and we should abort, let's abort
1996
            if ($conflictMode->equals(DuplicationBehavior::RENAME)) {
1997
                $sanitizedTargetFileName = $this->getUniqueName($targetFolder, $sanitizedTargetFileName);
1998
            } elseif ($conflictMode->equals(DuplicationBehavior::CANCEL)) {
1999
                throw new ExistingTargetFileNameException('The target file already exists', 1329850997);
2000
            }
2001
        }
2002
        $this->eventDispatcher->dispatch(
2003
            new BeforeFileMovedEvent($file, $targetFolder, $sanitizedTargetFileName)
2004
        );
2005
        $sourceStorage = $file->getStorage();
2006
        // Call driver method to move the file and update the index entry
2007
        try {
2008
            if ($sourceStorage === $this) {
2009
                $newIdentifier = $this->driver->moveFileWithinStorage($file->getIdentifier(), $targetFolder->getIdentifier(), $sanitizedTargetFileName);
2010
                if (!$file instanceof AbstractFile) {
2011
                    throw new \RuntimeException('The given file is not of type AbstractFile.', 1384209025);
2012
                }
2013
                $file->updateProperties(['identifier' => $newIdentifier]);
2014
            } else {
2015
                $tempPath = $file->getForLocalProcessing();
2016
                $newIdentifier = $this->driver->addFile($tempPath, $targetFolder->getIdentifier(), $sanitizedTargetFileName);
2017
2018
                // Disable permission check to find nearest recycler and move file without errors
2019
                $currentPermissions = $sourceStorage->evaluatePermissions;
2020
                $sourceStorage->evaluatePermissions = false;
2021
2022
                $recyclerFolder = $sourceStorage->getNearestRecyclerFolder($file);
2023
                if ($recyclerFolder === null) {
2024
                    $sourceStorage->driver->deleteFile($file->getIdentifier());
2025
                } else {
2026
                    $sourceStorage->moveFile($file, $recyclerFolder);
2027
                }
2028
                $sourceStorage->evaluatePermissions = $currentPermissions;
2029
                if ($file instanceof File) {
2030
                    $file->updateProperties(['storage' => $this->getUid(), 'identifier' => $newIdentifier]);
2031
                }
2032
            }
2033
            $this->getIndexer()->updateIndexEntry($file);
2034
        } catch (\TYPO3\CMS\Core\Exception $e) {
2035
            echo $e->getMessage();
2036
        }
2037
        $this->eventDispatcher->dispatch(
2038
            new AfterFileMovedEvent($file, $targetFolder, $originalFolder)
2039
        );
2040
        return $file;
2041
    }
2042
2043
    /**
2044
     * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_rename()
2045
     *
2046
     * @param FileInterface $file
2047
     * @param string $targetFileName
2048
     * @param string $conflictMode
2049
     * @return FileInterface
2050
     * @throws ExistingTargetFileNameException
2051
     */
2052
    public function renameFile($file, $targetFileName, $conflictMode = DuplicationBehavior::RENAME)
2053
    {
2054
        // The name should be different from the current.
2055
        if ($file->getName() === $targetFileName) {
2056
            return $file;
2057
        }
2058
        $sanitizedTargetFileName = $this->driver->sanitizeFileName($targetFileName);
2059
        $this->assureFileRenamePermissions($file, $sanitizedTargetFileName);
2060
        $this->eventDispatcher->dispatch(
2061
            new BeforeFileRenamedEvent($file, $sanitizedTargetFileName)
2062
        );
2063
2064
        $conflictMode = DuplicationBehavior::cast($conflictMode);
2065
2066
        // Call driver method to rename the file and update the index entry
2067
        try {
2068
            $newIdentifier = $this->driver->renameFile($file->getIdentifier(), $sanitizedTargetFileName);
2069
            if ($file instanceof File) {
2070
                $file->updateProperties(['identifier' => $newIdentifier]);
2071
            }
2072
            $this->getIndexer()->updateIndexEntry($file);
2073
        } catch (ExistingTargetFileNameException $exception) {
2074
            if ($conflictMode->equals(DuplicationBehavior::RENAME)) {
2075
                $newName = $this->getUniqueName($file->getParentFolder(), $sanitizedTargetFileName);
2076
                $file = $this->renameFile($file, $newName);
2077
            } elseif ($conflictMode->equals(DuplicationBehavior::CANCEL)) {
2078
                throw $exception;
2079
            } elseif ($conflictMode->equals(DuplicationBehavior::REPLACE)) {
2080
                $sourceFileIdentifier = substr($file->getCombinedIdentifier(), 0, (int)strrpos($file->getCombinedIdentifier(), '/') + 1) . $targetFileName;
0 ignored issues
show
Bug introduced by
The method getCombinedIdentifier() does not exist on TYPO3\CMS\Core\Resource\FileInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to TYPO3\CMS\Core\Resource\FileInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2080
                $sourceFileIdentifier = substr($file->/** @scrutinizer ignore-call */ getCombinedIdentifier(), 0, (int)strrpos($file->getCombinedIdentifier(), '/') + 1) . $targetFileName;
Loading history...
2081
                $sourceFile = $this->getResourceFactoryInstance()->getFileObjectFromCombinedIdentifier($sourceFileIdentifier);
2082
                $file = $this->replaceFile($sourceFile, Environment::getPublicPath() . '/' . $file->getPublicUrl());
0 ignored issues
show
Bug introduced by
It seems like $sourceFile can also be of type null; however, parameter $file of TYPO3\CMS\Core\Resource\...eStorage::replaceFile() does only seem to accept TYPO3\CMS\Core\Resource\FileInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2082
                $file = $this->replaceFile(/** @scrutinizer ignore-type */ $sourceFile, Environment::getPublicPath() . '/' . $file->getPublicUrl());
Loading history...
2083
            }
2084
        } catch (\RuntimeException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2085
        }
2086
2087
        $this->eventDispatcher->dispatch(
2088
            new AfterFileRenamedEvent($file, $sanitizedTargetFileName)
2089
        );
2090
2091
        return $file;
2092
    }
2093
2094
    /**
2095
     * Replaces a file with a local file (e.g. a freshly uploaded file)
2096
     *
2097
     * @param FileInterface $file
2098
     * @param string $localFilePath
2099
     *
2100
     * @return FileInterface
2101
     *
2102
     * @throws Exception\IllegalFileExtensionException
2103
     * @throws \InvalidArgumentException
2104
     */
2105
    public function replaceFile(FileInterface $file, $localFilePath)
2106
    {
2107
        $this->assureFileReplacePermissions($file);
2108
        if (!file_exists($localFilePath)) {
2109
            throw new \InvalidArgumentException('File "' . $localFilePath . '" does not exist.', 1325842622);
2110
        }
2111
        $this->eventDispatcher->dispatch(
2112
            new BeforeFileReplacedEvent($file, $localFilePath)
2113
        );
2114
        $this->driver->replaceFile($file->getIdentifier(), $localFilePath);
2115
        if ($file instanceof File) {
2116
            $this->getIndexer()->updateIndexEntry($file);
2117
        }
2118
        $this->eventDispatcher->dispatch(
2119
            new AfterFileReplacedEvent($file, $localFilePath)
2120
        );
2121
        return $file;
2122
    }
2123
2124
    /**
2125
     * Adds an uploaded file into the Storage. Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::file_upload()
2126
     *
2127
     * @param array $uploadedFileData contains information about the uploaded file given by $_FILES['file1']
2128
     * @param Folder $targetFolder the target folder
2129
     * @param string $targetFileName the file name to be written
2130
     * @param string $conflictMode a value of the DuplicationBehavior enumeration
2131
     * @return FileInterface The file object
2132
     */
2133
    public function addUploadedFile(array $uploadedFileData, Folder $targetFolder = null, $targetFileName = null, $conflictMode = DuplicationBehavior::CANCEL)
2134
    {
2135
        $conflictMode = DuplicationBehavior::cast($conflictMode);
2136
        $localFilePath = $uploadedFileData['tmp_name'];
2137
        if ($targetFolder === null) {
2138
            $targetFolder = $this->getDefaultFolder();
2139
        }
2140
        if ($targetFileName === null) {
2141
            $targetFileName = $uploadedFileData['name'];
2142
        }
2143
        $targetFileName = $this->driver->sanitizeFileName($targetFileName);
2144
2145
        $this->assureFileUploadPermissions($localFilePath, $targetFolder, $targetFileName, $uploadedFileData['size']);
2146
        if ($this->hasFileInFolder($targetFileName, $targetFolder) && $conflictMode->equals(DuplicationBehavior::REPLACE)) {
2147
            $file = $this->getFileInFolder($targetFileName, $targetFolder);
2148
            $resultObject = $this->replaceFile($file, $localFilePath);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type null; however, parameter $file of TYPO3\CMS\Core\Resource\...eStorage::replaceFile() does only seem to accept TYPO3\CMS\Core\Resource\FileInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2148
            $resultObject = $this->replaceFile(/** @scrutinizer ignore-type */ $file, $localFilePath);
Loading history...
2149
        } else {
2150
            $resultObject = $this->addFile($localFilePath, $targetFolder, $targetFileName, (string)$conflictMode);
2151
        }
2152
        return $resultObject;
2153
    }
2154
2155
    /********************
2156
     * FOLDER ACTIONS
2157
     ********************/
2158
    /**
2159
     * Returns an array with all file objects in a folder and its subfolders, with the file identifiers as keys.
2160
     * @todo check if this is a duplicate
2161
     * @param Folder $folder
2162
     * @return File[]
2163
     */
2164
    protected function getAllFileObjectsInFolder(Folder $folder)
2165
    {
2166
        $files = [];
2167
        $folderQueue = [$folder];
2168
        while (!empty($folderQueue)) {
2169
            $folder = array_shift($folderQueue);
2170
            foreach ($folder->getSubfolders() as $subfolder) {
2171
                $folderQueue[] = $subfolder;
2172
            }
2173
            foreach ($folder->getFiles() as $file) {
2174
                /** @var FileInterface $file */
2175
                $files[$file->getIdentifier()] = $file;
2176
            }
2177
        }
2178
2179
        return $files;
2180
    }
2181
2182
    /**
2183
     * Moves a folder. If you want to move a folder from this storage to another
2184
     * one, call this method on the target storage, otherwise you will get an exception.
2185
     *
2186
     * @param Folder $folderToMove The folder to move.
2187
     * @param Folder $targetParentFolder The target parent folder
2188
     * @param string $newFolderName
2189
     * @param string $conflictMode a value of the DuplicationBehavior enumeration
2190
     *
2191
     * @throws \Exception|\TYPO3\CMS\Core\Exception
2192
     * @throws \InvalidArgumentException
2193
     * @throws InvalidTargetFolderException
2194
     * @return Folder
2195
     */
2196
    public function moveFolder(Folder $folderToMove, Folder $targetParentFolder, $newFolderName = null, $conflictMode = DuplicationBehavior::RENAME)
2197
    {
2198
        // @todo add tests
2199
        $this->assureFolderMovePermissions($folderToMove, $targetParentFolder);
2200
        $sourceStorage = $folderToMove->getStorage();
2201
        $sanitizedNewFolderName = $this->driver->sanitizeFileName($newFolderName ?: $folderToMove->getName());
2202
        // @todo check if folder already exists in $targetParentFolder, handle this conflict then
2203
        $this->eventDispatcher->dispatch(
2204
            new BeforeFolderMovedEvent($folderToMove, $targetParentFolder, $sanitizedNewFolderName)
2205
        );
2206
        // Get all file objects now so we are able to update them after moving the folder
2207
        $fileObjects = $this->getAllFileObjectsInFolder($folderToMove);
2208
        if ($sourceStorage === $this) {
2209
            if ($this->isWithinFolder($folderToMove, $targetParentFolder)) {
2210
                throw new InvalidTargetFolderException(
2211
                    sprintf(
2212
                        'Cannot move folder "%s" into target folder "%s", because the target folder is already within the folder to be moved!',
2213
                        $folderToMove->getName(),
2214
                        $targetParentFolder->getName()
2215
                    ),
2216
                    1422723050
2217
                );
2218
            }
2219
            $fileMappings = $this->driver->moveFolderWithinStorage($folderToMove->getIdentifier(), $targetParentFolder->getIdentifier(), $sanitizedNewFolderName);
2220
        } else {
2221
            $fileMappings = $this->moveFolderBetweenStorages($folderToMove, $targetParentFolder, $sanitizedNewFolderName);
2222
        }
2223
        // Update the identifier and storage of all file objects
2224
        foreach ($fileObjects as $oldIdentifier => $fileObject) {
2225
            $newIdentifier = $fileMappings[$oldIdentifier];
2226
            $fileObject->updateProperties(['storage' => $this->getUid(), 'identifier' => $newIdentifier]);
2227
            $this->getIndexer()->updateIndexEntry($fileObject);
2228
        }
2229
        $returnObject = $this->getFolder($fileMappings[$folderToMove->getIdentifier()]);
2230
2231
        $this->eventDispatcher->dispatch(
2232
            new AfterFolderMovedEvent($folderToMove, $targetParentFolder, $returnObject)
2233
        );
2234
        return $returnObject;
2235
    }
2236
2237
    /**
2238
     * Moves the given folder from a different storage to the target folder in this storage.
2239
     *
2240
     * @param Folder $folderToMove
2241
     * @param Folder $targetParentFolder
2242
     * @param string $newFolderName
2243
     * @throws NotImplementedMethodException
2244
     */
2245
    protected function moveFolderBetweenStorages(Folder $folderToMove, Folder $targetParentFolder, $newFolderName)
2246
    {
2247
        throw new NotImplementedMethodException('Not yet implemented', 1476046361);
2248
    }
2249
2250
    /**
2251
     * Copies a folder.
2252
     *
2253
     * @param FolderInterface $folderToCopy The folder to copy
2254
     * @param FolderInterface $targetParentFolder The target folder
2255
     * @param string $newFolderName
2256
     * @param string $conflictMode a value of the DuplicationBehavior enumeration
2257
     * @return Folder The new (copied) folder object
2258
     * @throws InvalidTargetFolderException
2259
     */
2260
    public function copyFolder(FolderInterface $folderToCopy, FolderInterface $targetParentFolder, $newFolderName = null, $conflictMode = DuplicationBehavior::RENAME)
2261
    {
2262
        $conflictMode = DuplicationBehavior::cast($conflictMode);
2263
        $this->assureFolderCopyPermissions($folderToCopy, $targetParentFolder);
2264
        $returnObject = null;
2265
        $sanitizedNewFolderName = $this->driver->sanitizeFileName($newFolderName ?: $folderToCopy->getName());
2266
        if ($folderToCopy instanceof Folder && $targetParentFolder instanceof Folder) {
0 ignored issues
show
introduced by
$targetParentFolder is always a sub-type of TYPO3\CMS\Core\Resource\Folder.
Loading history...
2267
            $this->eventDispatcher->dispatch(
2268
                new BeforeFolderCopiedEvent($folderToCopy, $targetParentFolder, $sanitizedNewFolderName)
2269
            );
2270
        }
2271
        if ($conflictMode->equals(DuplicationBehavior::CANCEL) && ($targetParentFolder->hasFolder($sanitizedNewFolderName) || $targetParentFolder->hasFile($sanitizedNewFolderName))) {
2272
            throw new InvalidTargetFolderException(
2273
                sprintf(
2274
                    'Cannot copy folder "%s" into target folder "%s", because there is already a folder or file with that name in the target folder!',
2275
                    $sanitizedNewFolderName,
2276
                    $targetParentFolder->getIdentifier()
2277
                ),
2278
                1422723059
2279
            );
2280
        }
2281
        // Folder exists and we should find another name, let's find another one
2282
        if ($conflictMode->equals(DuplicationBehavior::RENAME) && ($targetParentFolder->hasFolder($sanitizedNewFolderName) || $targetParentFolder->hasFile($sanitizedNewFolderName))) {
2283
            $sanitizedNewFolderName = $this->getUniqueName($targetParentFolder, $sanitizedNewFolderName);
2284
        }
2285
        $sourceStorage = $folderToCopy->getStorage();
2286
        // call driver method to move the file
2287
        // that also updates the file object properties
2288
        if ($sourceStorage === $this) {
2289
            $this->driver->copyFolderWithinStorage($folderToCopy->getIdentifier(), $targetParentFolder->getIdentifier(), $sanitizedNewFolderName);
2290
            $returnObject = $this->getFolder($targetParentFolder->getSubfolder($sanitizedNewFolderName)->getIdentifier());
2291
        } else {
2292
            $this->copyFolderBetweenStorages($folderToCopy, $targetParentFolder, $sanitizedNewFolderName);
2293
        }
2294
        if ($folderToCopy instanceof Folder && $targetParentFolder instanceof Folder) {
0 ignored issues
show
introduced by
$targetParentFolder is always a sub-type of TYPO3\CMS\Core\Resource\Folder.
Loading history...
2295
            $this->eventDispatcher->dispatch(
2296
                new AfterFolderCopiedEvent($folderToCopy, $targetParentFolder, $returnObject)
2297
            );
2298
        }
2299
        return $returnObject;
2300
    }
2301
2302
    /**
2303
     * Copies a folder between storages.
2304
     *
2305
     * @param FolderInterface $folderToCopy
2306
     * @param FolderInterface $targetParentFolder
2307
     * @param string $newFolderName
2308
     * @throws NotImplementedMethodException
2309
     */
2310
    protected function copyFolderBetweenStorages(FolderInterface $folderToCopy, FolderInterface $targetParentFolder, $newFolderName)
2311
    {
2312
        throw new NotImplementedMethodException('Not yet implemented.', 1476046386);
2313
    }
2314
2315
    /**
2316
     * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::folder_move()
2317
     *
2318
     * @param Folder $folderObject
2319
     * @param string $newName
2320
     * @throws \Exception
2321
     * @throws \InvalidArgumentException
2322
     * @return Folder
2323
     */
2324
    public function renameFolder($folderObject, $newName)
2325
    {
2326
2327
        // Renaming the folder should check if the parent folder is writable
2328
        // We cannot do this however because we cannot extract the parent folder from a folder currently
2329
        if (!$this->checkFolderActionPermission('rename', $folderObject)) {
2330
            throw new InsufficientUserPermissionsException('You are not allowed to rename the folder "' . $folderObject->getIdentifier() . '\'', 1357811441);
2331
        }
2332
2333
        $sanitizedNewName = $this->driver->sanitizeFileName($newName);
2334
        if ($this->driver->folderExistsInFolder($sanitizedNewName, $folderObject->getIdentifier())) {
2335
            throw new \InvalidArgumentException('The folder ' . $sanitizedNewName . ' already exists in folder ' . $folderObject->getIdentifier(), 1325418870);
2336
        }
2337
        $this->eventDispatcher->dispatch(
2338
            new BeforeFolderRenamedEvent($folderObject, $sanitizedNewName)
2339
        );
2340
        $fileObjects = $this->getAllFileObjectsInFolder($folderObject);
2341
        $fileMappings = $this->driver->renameFolder($folderObject->getIdentifier(), $sanitizedNewName);
2342
        // Update the identifier of all file objects
2343
        foreach ($fileObjects as $oldIdentifier => $fileObject) {
2344
            $newIdentifier = $fileMappings[$oldIdentifier];
2345
            $fileObject->updateProperties(['identifier' => $newIdentifier]);
2346
            $this->getIndexer()->updateIndexEntry($fileObject);
2347
        }
2348
        $returnObject = $this->getFolder($fileMappings[$folderObject->getIdentifier()]);
2349
2350
        $this->eventDispatcher->dispatch(
2351
            new AfterFolderRenamedEvent($returnObject, $folderObject)
2352
        );
2353
        return $returnObject;
2354
    }
2355
2356
    /**
2357
     * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::folder_delete()
2358
     *
2359
     * @param Folder $folderObject
2360
     * @param bool $deleteRecursively
2361
     * @throws \RuntimeException
2362
     * @throws Exception\InsufficientFolderAccessPermissionsException
2363
     * @throws Exception\InsufficientUserPermissionsException
2364
     * @throws Exception\FileOperationErrorException
2365
     * @throws Exception\InvalidPathException
2366
     * @return bool
2367
     */
2368
    public function deleteFolder($folderObject, $deleteRecursively = false)
2369
    {
2370
        $isEmpty = $this->driver->isFolderEmpty($folderObject->getIdentifier());
2371
        $this->assureFolderDeletePermission($folderObject, $deleteRecursively && !$isEmpty);
2372
        if (!$isEmpty && !$deleteRecursively) {
2373
            throw new \RuntimeException('Could not delete folder "' . $folderObject->getIdentifier() . '" because it is not empty.', 1325952534);
2374
        }
2375
2376
        $this->eventDispatcher->dispatch(
2377
            new BeforeFolderDeletedEvent($folderObject)
2378
        );
2379
2380
        foreach ($this->getFilesInFolder($folderObject, 0, 0, false, $deleteRecursively) as $file) {
2381
            $this->deleteFile($file);
2382
        }
2383
2384
        $result = $this->driver->deleteFolder($folderObject->getIdentifier(), $deleteRecursively);
2385
2386
        $this->eventDispatcher->dispatch(
2387
            new AfterFolderDeletedEvent($folderObject, $result)
2388
        );
2389
        return $result;
2390
    }
2391
2392
    /**
2393
     * Returns the Identifier for a folder within a given folder.
2394
     *
2395
     * @param string $folderName The name of the target folder
2396
     * @param Folder $parentFolder
2397
     * @param bool $returnInaccessibleFolderObject
2398
     * @return Folder|InaccessibleFolder
2399
     * @throws \Exception
2400
     * @throws Exception\InsufficientFolderAccessPermissionsException
2401
     */
2402
    public function getFolderInFolder($folderName, Folder $parentFolder, $returnInaccessibleFolderObject = false)
2403
    {
2404
        $folderIdentifier = $this->driver->getFolderInFolder($folderName, $parentFolder->getIdentifier());
2405
        return $this->getFolder($folderIdentifier, $returnInaccessibleFolderObject);
2406
    }
2407
2408
    /**
2409
     * @param Folder $folder
2410
     * @param int $start
2411
     * @param int $maxNumberOfItems
2412
     * @param bool $useFilters
2413
     * @param bool $recursive
2414
     * @param string $sort Property name used to sort the items.
2415
     *                     Among them may be: '' (empty, no sorting), name,
2416
     *                     fileext, size, tstamp and rw.
2417
     *                     If a driver does not support the given property, it
2418
     *                     should fall back to "name".
2419
     * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
2420
     * @return Folder[]
2421
     */
2422
    public function getFoldersInFolder(Folder $folder, $start = 0, $maxNumberOfItems = 0, $useFilters = true, $recursive = false, $sort = '', $sortRev = false)
2423
    {
2424
        $filters = $useFilters == true ? $this->fileAndFolderNameFilters : [];
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
2425
2426
        $folderIdentifiers = $this->driver->getFoldersInFolder($folder->getIdentifier(), $start, $maxNumberOfItems, $recursive, $filters, $sort, $sortRev);
2427
2428
        // Exclude processing folders
2429
        foreach ($this->getProcessingFolders() as $processingFolder) {
2430
            $processingIdentifier = $processingFolder->getIdentifier();
2431
            if (isset($folderIdentifiers[$processingIdentifier])) {
2432
                unset($folderIdentifiers[$processingIdentifier]);
2433
            }
2434
        }
2435
        $folders = [];
2436
        foreach ($folderIdentifiers as $folderIdentifier) {
2437
            $folders[$folderIdentifier] = $this->getFolder($folderIdentifier, true);
2438
        }
2439
        return $folders;
2440
    }
2441
2442
    /**
2443
     * @param Folder  $folder
2444
     * @param bool $useFilters
2445
     * @param bool $recursive
2446
     * @return int Number of subfolders
2447
     * @throws Exception\InsufficientFolderAccessPermissionsException
2448
     */
2449
    public function countFoldersInFolder(Folder $folder, $useFilters = true, $recursive = false)
2450
    {
2451
        $this->assureFolderReadPermission($folder);
2452
        $filters = $useFilters ? $this->fileAndFolderNameFilters : [];
2453
        return $this->driver->countFoldersInFolder($folder->getIdentifier(), $recursive, $filters);
2454
    }
2455
2456
    /**
2457
     * Returns TRUE if the specified folder exists.
2458
     *
2459
     * @param string $identifier
2460
     * @return bool
2461
     */
2462
    public function hasFolder($identifier)
2463
    {
2464
        $this->assureFolderReadPermission();
2465
        return $this->driver->folderExists($identifier);
2466
    }
2467
2468
    /**
2469
     * Checks if the given file exists in the given folder
2470
     *
2471
     * @param string $folderName
2472
     * @param Folder $folder
2473
     * @return bool
2474
     */
2475
    public function hasFolderInFolder($folderName, Folder $folder)
2476
    {
2477
        $this->assureFolderReadPermission($folder);
2478
        return $this->driver->folderExistsInFolder($folderName, $folder->getIdentifier());
2479
    }
2480
2481
    /**
2482
     * Creates a new folder.
2483
     *
2484
     * previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_newfolder()
2485
     *
2486
     * @param string $folderName The new folder name
2487
     * @param Folder $parentFolder (optional) the parent folder to create the new folder inside of. If not given, the root folder is used
2488
     * @return Folder
2489
     * @throws Exception\ExistingTargetFolderException
2490
     * @throws Exception\InsufficientFolderAccessPermissionsException
2491
     * @throws Exception\InsufficientFolderWritePermissionsException
2492
     * @throws \Exception
2493
     */
2494
    public function createFolder($folderName, Folder $parentFolder = null)
2495
    {
2496
        if ($parentFolder === null) {
2497
            $parentFolder = $this->getRootLevelFolder();
2498
        } elseif (!$this->driver->folderExists($parentFolder->getIdentifier())) {
2499
            throw new \InvalidArgumentException('Parent folder "' . $parentFolder->getIdentifier() . '" does not exist.', 1325689164);
2500
        }
2501
        if (!$this->checkFolderActionPermission('add', $parentFolder)) {
2502
            throw new InsufficientFolderWritePermissionsException('You are not allowed to create directories in the folder "' . $parentFolder->getIdentifier() . '"', 1323059807);
2503
        }
2504
        if ($this->driver->folderExistsInFolder($folderName, $parentFolder->getIdentifier())) {
2505
            throw new ExistingTargetFolderException('Folder "' . $folderName . '" already exists.', 1423347324);
2506
        }
2507
2508
        $this->eventDispatcher->dispatch(
2509
            new BeforeFolderAddedEvent($parentFolder, $folderName)
2510
        );
2511
2512
        $newFolder = $this->getDriver()->createFolder($folderName, $parentFolder->getIdentifier(), true);
2513
        $newFolder = $this->getFolder($newFolder);
2514
2515
        $this->eventDispatcher->dispatch(
2516
            new AfterFolderAddedEvent($newFolder)
2517
        );
2518
2519
        return $newFolder;
2520
    }
2521
2522
    /**
2523
     * Retrieves information about a folder
2524
     *
2525
     * @param Folder $folder
2526
     * @return array
2527
     */
2528
    public function getFolderInfo(Folder $folder)
2529
    {
2530
        return $this->driver->getFolderInfoByIdentifier($folder->getIdentifier());
2531
    }
2532
2533
    /**
2534
     * Returns the default folder where new files are stored if no other folder is given.
2535
     *
2536
     * @return Folder
2537
     */
2538
    public function getDefaultFolder()
2539
    {
2540
        return $this->getFolder($this->driver->getDefaultFolder());
2541
    }
2542
2543
    /**
2544
     * @param string $identifier
2545
     * @param bool $returnInaccessibleFolderObject
2546
     *
2547
     * @return Folder|InaccessibleFolder
2548
     * @throws \Exception
2549
     * @throws Exception\InsufficientFolderAccessPermissionsException
2550
     */
2551
    public function getFolder($identifier, $returnInaccessibleFolderObject = false)
2552
    {
2553
        $data = $this->driver->getFolderInfoByIdentifier($identifier);
2554
        $folder = $this->createFolderObject($data['identifier'] ?? '', $data['name'] ?? '');
2555
2556
        try {
2557
            $this->assureFolderReadPermission($folder);
2558
        } catch (InsufficientFolderAccessPermissionsException $e) {
2559
            $folder = null;
2560
            if ($returnInaccessibleFolderObject) {
2561
                // if parent folder is readable return inaccessible folder object
2562
                $parentPermissions = $this->driver->getPermissions($this->driver->getParentFolderIdentifierOfIdentifier($identifier));
2563
                if ($parentPermissions['r']) {
2564
                    $folder = GeneralUtility::makeInstance(
2565
                        InaccessibleFolder::class,
2566
                        $this,
2567
                        $data['identifier'],
2568
                        $data['name']
2569
                    );
2570
                }
2571
            }
2572
2573
            if ($folder === null) {
2574
                throw $e;
2575
            }
2576
        }
2577
        return $folder;
2578
    }
2579
2580
    /**
2581
     * Returns TRUE if the specified file is in a folder that is set a processing for a storage
2582
     *
2583
     * @param string $identifier
2584
     * @return bool
2585
     */
2586
    public function isWithinProcessingFolder($identifier)
2587
    {
2588
        $inProcessingFolder = false;
2589
        foreach ($this->getProcessingFolders() as $processingFolder) {
2590
            if ($this->driver->isWithin($processingFolder->getIdentifier(), $identifier)) {
2591
                $inProcessingFolder = true;
2592
                break;
2593
            }
2594
        }
2595
        return $inProcessingFolder;
2596
    }
2597
2598
    /**
2599
     * Checks if a resource (file or folder) is within the given folder
2600
     *
2601
     * @param Folder $folder
2602
     * @param ResourceInterface $resource
2603
     * @return bool
2604
     * @throws \InvalidArgumentException
2605
     */
2606
    public function isWithinFolder(Folder $folder, ResourceInterface $resource)
2607
    {
2608
        if ($folder->getStorage() !== $this) {
2609
            throw new \InvalidArgumentException('Given folder "' . $folder->getIdentifier() . '" is not part of this storage!', 1422709241);
2610
        }
2611
        if ($folder->getStorage() !== $resource->getStorage()) {
2612
            return false;
2613
        }
2614
        return $this->driver->isWithin($folder->getIdentifier(), $resource->getIdentifier());
2615
    }
2616
2617
    /**
2618
     * Returns the folders on the root level of the storage
2619
     * or the first mount point of this storage for this user
2620
     * if $respectFileMounts is set.
2621
     *
2622
     * @param bool $respectFileMounts
2623
     * @return Folder
2624
     */
2625
    public function getRootLevelFolder($respectFileMounts = true)
2626
    {
2627
        if ($respectFileMounts && !empty($this->fileMounts)) {
2628
            $mount = reset($this->fileMounts);
2629
            return $mount['folder'];
2630
        }
2631
        return $this->createFolderObject($this->driver->getRootLevelFolder(), '');
2632
    }
2633
2634
    /**
2635
     * Returns the destination path/fileName of a unique fileName/foldername in that path.
2636
     * If $theFile exists in $theDest (directory) the file have numbers appended up to $this->maxNumber.
2637
     * Hereafter a unique string will be appended.
2638
     * This function is used by fx. DataHandler when files are attached to records
2639
     * and needs to be uniquely named in the uploads/* folders
2640
     *
2641
     * @param FolderInterface $folder
2642
     * @param string $theFile The input fileName to check
2643
     * @param bool $dontCheckForUnique If set the fileName is returned with the path prepended without checking whether it already existed!
2644
     *
2645
     * @throws \RuntimeException
2646
     * @return string A unique fileName inside $folder, based on $theFile.
2647
     * @see \TYPO3\CMS\Core\Utility\File\BasicFileUtility::getUniqueName()
2648
     */
2649
    protected function getUniqueName(FolderInterface $folder, $theFile, $dontCheckForUnique = false)
2650
    {
2651
        $maxNumber = 99;
2652
        // Fetches info about path, name, extension of $theFile
2653
        $origFileInfo = PathUtility::pathinfo($theFile);
2654
        // Check if the file exists and if not - return the fileName...
2655
        // The destinations file
2656
        $theDestFile = $origFileInfo['basename'];
2657
        // If the file does NOT exist we return this fileName
2658
        if ($dontCheckForUnique || (!$this->driver->fileExistsInFolder($theDestFile, $folder->getIdentifier()) && !$this->driver->folderExistsInFolder($theDestFile, $folder->getIdentifier()))) {
2659
            return $theDestFile;
2660
        }
2661
        // Well the fileName in its pure form existed. Now we try to append
2662
        // numbers / unique-strings and see if we can find an available fileName
2663
        // This removes _xx if appended to the file
2664
        $theTempFileBody = preg_replace('/_[0-9][0-9]$/', '', $origFileInfo['filename']);
2665
        $theOrigExt = $origFileInfo['extension'] ? '.' . $origFileInfo['extension'] : '';
2666
        for ($a = 1; $a <= $maxNumber + 1; $a++) {
2667
            // First we try to append numbers
2668
            if ($a <= $maxNumber) {
2669
                $insert = '_' . sprintf('%02d', $a);
2670
            } else {
2671
                $insert = '_' . substr(md5(StringUtility::getUniqueId()), 0, 6);
2672
            }
2673
            $theTestFile = $theTempFileBody . $insert . $theOrigExt;
2674
            // The destinations file
2675
            $theDestFile = $theTestFile;
2676
            // If the file does NOT exist we return this fileName
2677
            if (!$this->driver->fileExistsInFolder($theDestFile, $folder->getIdentifier()) && !$this->driver->folderExistsInFolder($theDestFile, $folder->getIdentifier())) {
2678
                return $theDestFile;
2679
            }
2680
        }
2681
        throw new \RuntimeException('Last possible name "' . $theDestFile . '" is already taken.', 1325194291);
2682
    }
2683
2684
    /**
2685
     * @return ResourceFactory
2686
     */
2687
    protected function getFileFactory()
2688
    {
2689
        return GeneralUtility::makeInstance(ResourceFactory::class);
2690
    }
2691
2692
    /**
2693
     * @return Index\FileIndexRepository
2694
     */
2695
    protected function getFileIndexRepository()
2696
    {
2697
        return FileIndexRepository::getInstance();
2698
    }
2699
2700
    /**
2701
     * @return Service\FileProcessingService
2702
     */
2703
    protected function getFileProcessingService()
2704
    {
2705
        if (!$this->fileProcessingService) {
2706
            $this->fileProcessingService = GeneralUtility::makeInstance(FileProcessingService::class, $this, $this->driver, $this->eventDispatcher);
2707
        }
2708
        return $this->fileProcessingService;
2709
    }
2710
2711
    /**
2712
     * Gets the role of a folder.
2713
     *
2714
     * @param FolderInterface $folder Folder object to get the role from
2715
     * @return string The role the folder has
2716
     */
2717
    public function getRole(FolderInterface $folder)
2718
    {
2719
        $folderRole = FolderInterface::ROLE_DEFAULT;
2720
        $identifier = $folder->getIdentifier();
2721
        if (method_exists($this->driver, 'getRole')) {
2722
            $folderRole = $this->driver->getRole($folder->getIdentifier());
2723
        }
2724
        if (isset($this->fileMounts[$identifier])) {
2725
            $folderRole = FolderInterface::ROLE_MOUNT;
2726
2727
            if (!empty($this->fileMounts[$identifier]['read_only'])) {
2728
                $folderRole = FolderInterface::ROLE_READONLY_MOUNT;
2729
            }
2730
            if ($this->fileMounts[$identifier]['user_mount']) {
2731
                $folderRole = FolderInterface::ROLE_USER_MOUNT;
2732
            }
2733
        }
2734
        if ($folder instanceof Folder && $this->isProcessingFolder($folder)) {
2735
            $folderRole = FolderInterface::ROLE_PROCESSING;
2736
        }
2737
2738
        return $folderRole;
2739
    }
2740
2741
    /**
2742
     * Getter function to return the folder where the files can
2743
     * be processed. Does not check for access rights here.
2744
     *
2745
     * @param File $file Specific file you want to have the processing folder for
2746
     * @return Folder
2747
     */
2748
    public function getProcessingFolder(File $file = null)
2749
    {
2750
        // If a file is given, make sure to return the processing folder of the correct storage
2751
        if ($file !== null && $file->getStorage()->getUid() !== $this->getUid()) {
2752
            return $file->getStorage()->getProcessingFolder($file);
2753
        }
2754
        if (!isset($this->processingFolder)) {
2755
            $processingFolder = self::DEFAULT_ProcessingFolder;
2756
            if (!empty($this->storageRecord['processingfolder'])) {
2757
                $processingFolder = $this->storageRecord['processingfolder'];
2758
            }
2759
            try {
2760
                if (strpos($processingFolder, ':') !== false) {
2761
                    [$storageUid, $processingFolderIdentifier] = explode(':', $processingFolder, 2);
2762
                    $storage = GeneralUtility::makeInstance(StorageRepository::class)->findByUid((int)$storageUid);
2763
                    if ($storage->hasFolder($processingFolderIdentifier)) {
2764
                        $this->processingFolder = $storage->getFolder($processingFolderIdentifier);
2765
                    } else {
2766
                        $rootFolder = $storage->getRootLevelFolder(false);
2767
                        $currentEvaluatePermissions = $storage->getEvaluatePermissions();
2768
                        $storage->setEvaluatePermissions(false);
2769
                        $this->processingFolder = $storage->createFolder(
2770
                            ltrim($processingFolderIdentifier, '/'),
2771
                            $rootFolder
2772
                        );
2773
                        $storage->setEvaluatePermissions($currentEvaluatePermissions);
2774
                    }
2775
                } else {
2776
                    if ($this->driver->folderExists($processingFolder) === false) {
2777
                        $rootFolder = $this->getRootLevelFolder(false);
2778
                        try {
2779
                            $currentEvaluatePermissions = $this->evaluatePermissions;
2780
                            $this->evaluatePermissions = false;
2781
                            $this->processingFolder = $this->createFolder(
2782
                                $processingFolder,
2783
                                $rootFolder
2784
                            );
2785
                            $this->evaluatePermissions = $currentEvaluatePermissions;
2786
                        } catch (\InvalidArgumentException $e) {
2787
                            $this->processingFolder = GeneralUtility::makeInstance(
2788
                                InaccessibleFolder::class,
2789
                                $this,
2790
                                $processingFolder,
2791
                                $processingFolder
2792
                            );
2793
                        }
2794
                    } else {
2795
                        $data = $this->driver->getFolderInfoByIdentifier($processingFolder);
2796
                        $this->processingFolder = $this->createFolderObject($data['identifier'], $data['name']);
2797
                    }
2798
                }
2799
            } catch (InsufficientFolderWritePermissionsException|ResourcePermissionsUnavailableException $e) {
2800
                $this->processingFolder = GeneralUtility::makeInstance(
2801
                    InaccessibleFolder::class,
2802
                    $this,
2803
                    $processingFolder,
2804
                    $processingFolder
2805
                );
2806
            }
2807
        }
2808
2809
        $processingFolder = $this->processingFolder;
2810
        if (!empty($file)) {
2811
            $processingFolder = $this->getNestedProcessingFolder($file, $processingFolder);
2812
        }
2813
        return $processingFolder;
2814
    }
2815
2816
    /**
2817
     * Getter function to return the the file's corresponding hashed subfolder
2818
     * of the processed folder
2819
     *
2820
     * @param File $file
2821
     * @param Folder $rootProcessingFolder
2822
     * @return Folder
2823
     * @throws Exception\InsufficientFolderWritePermissionsException
2824
     */
2825
    protected function getNestedProcessingFolder(File $file, Folder $rootProcessingFolder)
2826
    {
2827
        $processingFolder = $rootProcessingFolder;
2828
        $nestedFolderNames = $this->getNamesForNestedProcessingFolder(
2829
            $file->getIdentifier(),
2830
            self::PROCESSING_FOLDER_LEVELS
2831
        );
2832
2833
        try {
2834
            foreach ($nestedFolderNames as $folderName) {
2835
                if ($processingFolder->hasFolder($folderName)) {
2836
                    $processingFolder = $processingFolder->getSubfolder($folderName);
2837
                } else {
2838
                    $currentEvaluatePermissions = $processingFolder->getStorage()->getEvaluatePermissions();
2839
                    $processingFolder->getStorage()->setEvaluatePermissions(false);
2840
                    $processingFolder = $processingFolder->createFolder($folderName);
2841
                    $processingFolder->getStorage()->setEvaluatePermissions($currentEvaluatePermissions);
2842
                }
2843
            }
2844
        } catch (FolderDoesNotExistException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2845
        }
2846
2847
        return $processingFolder;
2848
    }
2849
2850
    /**
2851
     * Generates appropriate hashed sub-folder path for a given file identifier
2852
     *
2853
     * @param string $fileIdentifier
2854
     * @param int $levels
2855
     * @return string[]
2856
     */
2857
    protected function getNamesForNestedProcessingFolder($fileIdentifier, $levels)
2858
    {
2859
        $names = [];
2860
        if ($levels === 0) {
2861
            return $names;
2862
        }
2863
        $hash = md5($fileIdentifier);
2864
        for ($i = 1; $i <= $levels; $i++) {
2865
            $names[] = substr($hash, $i, 1);
2866
        }
2867
        return $names;
2868
    }
2869
2870
    /**
2871
     * Gets the driver Type configured for this storage.
2872
     *
2873
     * @return string
2874
     */
2875
    public function getDriverType()
2876
    {
2877
        return $this->storageRecord['driver'];
2878
    }
2879
2880
    /**
2881
     * Gets the Indexer.
2882
     *
2883
     * @return Index\Indexer
2884
     */
2885
    protected function getIndexer()
2886
    {
2887
        return GeneralUtility::makeInstance(Indexer::class, $this);
2888
    }
2889
2890
    /**
2891
     * @param bool $isDefault
2892
     */
2893
    public function setDefault($isDefault)
2894
    {
2895
        $this->isDefault = (bool)$isDefault;
2896
    }
2897
2898
    /**
2899
     * @return bool
2900
     */
2901
    public function isDefault()
2902
    {
2903
        return $this->isDefault;
2904
    }
2905
2906
    /**
2907
     * @return ResourceFactory
2908
     */
2909
    public function getResourceFactoryInstance(): ResourceFactory
2910
    {
2911
        return GeneralUtility::makeInstance(ResourceFactory::class);
2912
    }
2913
2914
    /**
2915
     * Returns the current BE user.
2916
     *
2917
     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
2918
     */
2919
    protected function getBackendUser()
2920
    {
2921
        return $GLOBALS['BE_USER'];
2922
    }
2923
2924
    /**
2925
     * Get the nearest Recycler folder for given file
2926
     *
2927
     * Return null if:
2928
     *  - There is no folder with ROLE_RECYCLER in the rootline of the given File
2929
     *  - File is a ProcessedFile (we don't know the concept of recycler folders for processedFiles)
2930
     *  - File is located in a folder with ROLE_RECYCLER
2931
     *
2932
     * @param FileInterface $file
2933
     * @return Folder|null
2934
     */
2935
    protected function getNearestRecyclerFolder(FileInterface $file)
2936
    {
2937
        if ($file instanceof ProcessedFile) {
2938
            return null;
2939
        }
2940
        // if the storage is not browsable we cannot fetch the parent folder of the file so no recycler handling is possible
2941
        if (!$this->isBrowsable()) {
2942
            return null;
2943
        }
2944
2945
        $recyclerFolder = null;
2946
        $folder = $file->getParentFolder();
2947
2948
        do {
2949
            if ($folder->getRole() === FolderInterface::ROLE_RECYCLER) {
0 ignored issues
show
Bug introduced by
The method getRole() does not exist on TYPO3\CMS\Core\Resource\FolderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to TYPO3\CMS\Core\Resource\FolderInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2949
            if ($folder->/** @scrutinizer ignore-call */ getRole() === FolderInterface::ROLE_RECYCLER) {
Loading history...
2950
                break;
2951
            }
2952
2953
            foreach ($folder->getSubfolders() as $subFolder) {
2954
                if ($subFolder->getRole() === FolderInterface::ROLE_RECYCLER) {
2955
                    $recyclerFolder = $subFolder;
2956
                    break;
2957
                }
2958
            }
2959
2960
            $parentFolder = $folder->getParentFolder();
2961
            $isFolderLoop = $folder->getIdentifier() === $parentFolder->getIdentifier();
2962
            $folder = $parentFolder;
2963
        } while ($recyclerFolder === null && !$isFolderLoop);
2964
2965
        return $recyclerFolder;
2966
    }
2967
2968
    /**
2969
     * Creates a folder to directly access (a part of) a storage.
2970
     *
2971
     * @param string $identifier The path to the folder. Might also be a simple unique string, depending on the storage driver.
2972
     * @param string $name The name of the folder (e.g. the folder name)
2973
     * @return Folder
2974
     */
2975
    protected function createFolderObject(string $identifier, string $name)
2976
    {
2977
        return GeneralUtility::makeInstance(Folder::class, $this, $identifier, $name);
2978
    }
2979
}
2980