Completed
Push — master ( 7a0b3c...09a41b )
by
unknown
19:44
created

ResourceStorage::getFileByIdentifier()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

1455
            $file->/** @scrutinizer ignore-call */ 
1456
                   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...
1456
        }
1457
        return $file;
1458
    }
1459
1460
    /**
1461
     * Gets a file object from storage by file identifier
1462
     * If the file is outside of the process folder, it gets indexed and returned as file object afterwards
1463
     * If the file is within processing folder, the file object will be directly returned
1464
     *
1465
     * @param string $fileIdentifier
1466
     * @return File|ProcessedFile|null
1467
     */
1468
    public function getFileByIdentifier(string $fileIdentifier)
1469
    {
1470
        if (!$this->isWithinProcessingFolder($fileIdentifier)) {
1471
            $fileData = $this->getFileIndexRepository()->findOneByStorageAndIdentifier($this, $fileIdentifier);
1472
            if ($fileData === false) {
1473
                return $this->getIndexer()->createIndexEntry($fileIdentifier);
1474
            }
1475
            return $this->getResourceFactoryInstance()->getFileObject($fileData['uid'], $fileData);
1476
        }
1477
        return $this->getProcessedFileRepository()->findByStorageAndIdentifier($this, $fileIdentifier);
1478
    }
1479
1480
    protected function getProcessedFileRepository(): ProcessedFileRepository
1481
    {
1482
        return GeneralUtility::makeInstance(ProcessedFileRepository::class);
1483
    }
1484
1485
    /**
1486
     * Gets information about a file.
1487
     *
1488
     * @param FileInterface $fileObject
1489
     * @return array
1490
     * @internal
1491
     */
1492
    public function getFileInfo(FileInterface $fileObject)
1493
    {
1494
        return $this->getFileInfoByIdentifier($fileObject->getIdentifier());
1495
    }
1496
1497
    /**
1498
     * Gets information about a file by its identifier.
1499
     *
1500
     * @param string $identifier
1501
     * @param array $propertiesToExtract
1502
     * @return array
1503
     * @internal
1504
     */
1505
    public function getFileInfoByIdentifier($identifier, array $propertiesToExtract = [])
1506
    {
1507
        return $this->driver->getFileInfoByIdentifier($identifier, $propertiesToExtract);
1508
    }
1509
1510
    /**
1511
     * Unsets the file and folder name filters, thus making this storage return unfiltered filelists.
1512
     */
1513
    public function unsetFileAndFolderNameFilters()
1514
    {
1515
        $this->fileAndFolderNameFilters = [];
1516
    }
1517
1518
    /**
1519
     * Resets the file and folder name filters to the default values defined in the TYPO3 configuration.
1520
     */
1521
    public function resetFileAndFolderNameFiltersToDefault()
1522
    {
1523
        $this->fileAndFolderNameFilters = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['defaultFilterCallbacks'];
1524
    }
1525
1526
    /**
1527
     * Returns the file and folder name filters used by this storage.
1528
     *
1529
     * @return array
1530
     */
1531
    public function getFileAndFolderNameFilters()
1532
    {
1533
        return $this->fileAndFolderNameFilters;
1534
    }
1535
1536
    /**
1537
     * @param array $filters
1538
     * @return $this
1539
     */
1540
    public function setFileAndFolderNameFilters(array $filters)
1541
    {
1542
        $this->fileAndFolderNameFilters = $filters;
1543
        return $this;
1544
    }
1545
1546
    /**
1547
     * @param callable $filter
1548
     */
1549
    public function addFileAndFolderNameFilter($filter)
1550
    {
1551
        $this->fileAndFolderNameFilters[] = $filter;
1552
    }
1553
1554
    /**
1555
     * @param string $fileIdentifier
1556
     *
1557
     * @return string
1558
     */
1559
    public function getFolderIdentifierFromFileIdentifier($fileIdentifier)
1560
    {
1561
        return $this->driver->getParentFolderIdentifierOfIdentifier($fileIdentifier);
1562
    }
1563
1564
    /**
1565
     * Get file from folder
1566
     *
1567
     * @param string $fileName
1568
     * @param Folder $folder
1569
     * @return File|ProcessedFile|null
1570
     */
1571
    public function getFileInFolder($fileName, Folder $folder)
1572
    {
1573
        $identifier = $this->driver->getFileInFolder($fileName, $folder->getIdentifier());
1574
        return $this->getFileByIdentifier($identifier);
1575
    }
1576
1577
    /**
1578
     * @param Folder $folder
1579
     * @param int $start
1580
     * @param int $maxNumberOfItems
1581
     * @param bool $useFilters
1582
     * @param bool $recursive
1583
     * @param string $sort Property name used to sort the items.
1584
     *                     Among them may be: '' (empty, no sorting), name,
1585
     *                     fileext, size, tstamp and rw.
1586
     *                     If a driver does not support the given property, it
1587
     *                     should fall back to "name".
1588
     * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
1589
     * @return File[]
1590
     * @throws Exception\InsufficientFolderAccessPermissionsException
1591
     */
1592
    public function getFilesInFolder(Folder $folder, $start = 0, $maxNumberOfItems = 0, $useFilters = true, $recursive = false, $sort = '', $sortRev = false)
1593
    {
1594
        $this->assureFolderReadPermission($folder);
1595
1596
        $rows = $this->getFileIndexRepository()->findByFolder($folder);
1597
1598
        $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...
1599
        $fileIdentifiers = array_values($this->driver->getFilesInFolder($folder->getIdentifier(), $start, $maxNumberOfItems, $recursive, $filters, $sort, $sortRev));
1600
1601
        $items = [];
1602
        foreach ($fileIdentifiers as $identifier) {
1603
            if (isset($rows[$identifier])) {
1604
                $fileObject = $this->getFileFactory()->getFileObject($rows[$identifier]['uid'], $rows[$identifier]);
1605
            } else {
1606
                $fileObject = $this->getFileByIdentifier($identifier);
1607
            }
1608
            if ($fileObject instanceof FileInterface) {
1609
                $key = $fileObject->getName();
1610
                while (isset($items[$key])) {
1611
                    $key .= 'z';
1612
                }
1613
                $items[$key] = $fileObject;
1614
            }
1615
        }
1616
1617
        return $items;
1618
    }
1619
1620
    /**
1621
     * @param string $folderIdentifier
1622
     * @param bool $useFilters
1623
     * @param bool $recursive
1624
     * @return array
1625
     */
1626
    public function getFileIdentifiersInFolder($folderIdentifier, $useFilters = true, $recursive = false)
1627
    {
1628
        $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...
1629
        return $this->driver->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filters);
1630
    }
1631
1632
    /**
1633
     * @param Folder $folder
1634
     * @param bool $useFilters
1635
     * @param bool $recursive
1636
     * @return int Number of files in folder
1637
     * @throws Exception\InsufficientFolderAccessPermissionsException
1638
     */
1639
    public function countFilesInFolder(Folder $folder, $useFilters = true, $recursive = false)
1640
    {
1641
        $this->assureFolderReadPermission($folder);
1642
        $filters = $useFilters ? $this->fileAndFolderNameFilters : [];
1643
        return $this->driver->countFilesInFolder($folder->getIdentifier(), $recursive, $filters);
1644
    }
1645
1646
    /**
1647
     * @param string $folderIdentifier
1648
     * @param bool $useFilters
1649
     * @param bool $recursive
1650
     * @return array
1651
     */
1652
    public function getFolderIdentifiersInFolder($folderIdentifier, $useFilters = true, $recursive = false)
1653
    {
1654
        $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...
1655
        return $this->driver->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $filters);
1656
    }
1657
1658
    /**
1659
     * Returns TRUE if the specified file exists
1660
     *
1661
     * @param string $identifier
1662
     * @return bool
1663
     */
1664
    public function hasFile($identifier)
1665
    {
1666
        // Allow if identifier is in processing folder
1667
        if (!$this->isWithinProcessingFolder($identifier)) {
1668
            $this->assureFolderReadPermission();
1669
        }
1670
        return $this->driver->fileExists($identifier);
1671
    }
1672
1673
    /**
1674
     * Get all processing folders that live in this storage
1675
     *
1676
     * @return Folder[]
1677
     */
1678
    public function getProcessingFolders()
1679
    {
1680
        if ($this->processingFolders === null) {
1681
            $this->processingFolders = [];
1682
            $this->processingFolders[] = $this->getProcessingFolder();
1683
            /** @var StorageRepository $storageRepository */
1684
            $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
1685
            $allStorages = $storageRepository->findAll();
1686
            foreach ($allStorages as $storage) {
1687
                // To circumvent the permission check of the folder, we use the factory to create it "manually" instead of directly using $storage->getProcessingFolder()
1688
                // See #66695 for details
1689
                [$storageUid, $processingFolderIdentifier] = array_pad(GeneralUtility::trimExplode(':', $storage->getStorageRecord()['processingfolder']), 2, null);
1690
                if (empty($processingFolderIdentifier) || (int)$storageUid !== $this->getUid()) {
1691
                    continue;
1692
                }
1693
                $potentialProcessingFolder = $this->createFolderObject($processingFolderIdentifier, $processingFolderIdentifier);
1694
                if ($potentialProcessingFolder->getStorage() === $this && $potentialProcessingFolder->getIdentifier() !== $this->getProcessingFolder()->getIdentifier()) {
1695
                    $this->processingFolders[] = $potentialProcessingFolder;
1696
                }
1697
            }
1698
        }
1699
1700
        return $this->processingFolders;
1701
    }
1702
1703
    /**
1704
     * Returns TRUE if folder that is in current storage  is set as
1705
     * processing folder for one of the existing storages
1706
     *
1707
     * @param Folder $folder
1708
     * @return bool
1709
     */
1710
    public function isProcessingFolder(Folder $folder)
1711
    {
1712
        $isProcessingFolder = false;
1713
        foreach ($this->getProcessingFolders() as $processingFolder) {
1714
            if ($folder->getCombinedIdentifier() === $processingFolder->getCombinedIdentifier()) {
1715
                $isProcessingFolder = true;
1716
                break;
1717
            }
1718
        }
1719
        return $isProcessingFolder;
1720
    }
1721
1722
    /**
1723
     * Checks if the queried file in the given folder exists
1724
     *
1725
     * @param string $fileName
1726
     * @param Folder $folder
1727
     * @return bool
1728
     */
1729
    public function hasFileInFolder($fileName, Folder $folder)
1730
    {
1731
        $this->assureFolderReadPermission($folder);
1732
        return $this->driver->fileExistsInFolder($fileName, $folder->getIdentifier());
1733
    }
1734
1735
    /**
1736
     * Get contents of a file object
1737
     *
1738
     * @param FileInterface $file
1739
     *
1740
     * @throws Exception\InsufficientFileReadPermissionsException
1741
     * @return string
1742
     */
1743
    public function getFileContents($file)
1744
    {
1745
        $this->assureFileReadPermission($file);
1746
        return $this->driver->getFileContents($file->getIdentifier());
1747
    }
1748
1749
    /**
1750
     * Returns a PSR-7 Response which can be used to stream the requested file
1751
     *
1752
     * @param FileInterface $file
1753
     * @param bool $asDownload If set Content-Disposition attachment is sent, inline otherwise
1754
     * @param string $alternativeFilename the filename for the download (if $asDownload is set)
1755
     * @param string $overrideMimeType If set this will be used as Content-Type header instead of the automatically detected mime type.
1756
     * @return ResponseInterface
1757
     */
1758
    public function streamFile(
1759
        FileInterface $file,
1760
        bool $asDownload = false,
1761
        string $alternativeFilename = null,
1762
        string $overrideMimeType = null
1763
    ): ResponseInterface {
1764
        if (!$this->driver instanceof StreamableDriverInterface) {
1765
            return $this->getPseudoStream($file, $asDownload, $alternativeFilename, $overrideMimeType);
1766
        }
1767
1768
        $properties = [
1769
            'as_download' => $asDownload,
1770
            'filename_overwrite' => $alternativeFilename,
1771
            'mimetype_overwrite' => $overrideMimeType,
1772
        ];
1773
        return $this->driver->streamFile($file->getIdentifier(), $properties);
1774
    }
1775
1776
    /**
1777
     * Wrap DriverInterface::dumpFileContents into a SelfEmittableStreamInterface
1778
     *
1779
     * @param FileInterface $file
1780
     * @param bool $asDownload If set Content-Disposition attachment is sent, inline otherwise
1781
     * @param string $alternativeFilename the filename for the download (if $asDownload is set)
1782
     * @param string $overrideMimeType If set this will be used as Content-Type header instead of the automatically detected mime type.
1783
     * @return ResponseInterface
1784
     */
1785
    protected function getPseudoStream(
1786
        FileInterface $file,
1787
        bool $asDownload = false,
1788
        string $alternativeFilename = null,
1789
        string $overrideMimeType = null
1790
    ) {
1791
        $downloadName = $alternativeFilename ?: $file->getName();
1792
        $contentDisposition = $asDownload ? 'attachment' : 'inline';
1793
1794
        $stream = new FalDumpFileContentsDecoratorStream($file->getIdentifier(), $this->driver, $file->getSize());
1795
        $headers = [
1796
            'Content-Disposition' => $contentDisposition . '; filename="' . $downloadName . '"',
1797
            'Content-Type' => $overrideMimeType ?: $file->getMimeType(),
1798
            'Content-Length' => (string)$file->getSize(),
1799
            'Last-Modified' => gmdate('D, d M Y H:i:s', array_pop($this->driver->getFileInfoByIdentifier($file->getIdentifier(), ['mtime']))) . ' GMT',
0 ignored issues
show
Bug introduced by
$this->driver->getFileIn...fier(), array('mtime')) cannot be passed to array_pop() as the parameter $array expects a reference. ( Ignorable by Annotation )

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

1799
            'Last-Modified' => gmdate('D, d M Y H:i:s', array_pop(/** @scrutinizer ignore-type */ $this->driver->getFileInfoByIdentifier($file->getIdentifier(), ['mtime']))) . ' GMT',
Loading history...
1800
            // Cache-Control header is needed here to solve an issue with browser IE8 and lower
1801
            // See for more information: http://support.microsoft.com/kb/323308
1802
            'Cache-Control' => '',
1803
        ];
1804
1805
        return new Response($stream, 200, $headers);
1806
    }
1807
1808
    /**
1809
     * Set contents of a file object.
1810
     *
1811
     * @param AbstractFile $file
1812
     * @param string $contents
1813
     *
1814
     * @throws \Exception|\RuntimeException
1815
     * @throws Exception\InsufficientFileWritePermissionsException
1816
     * @throws Exception\InsufficientUserPermissionsException
1817
     * @return int The number of bytes written to the file
1818
     */
1819
    public function setFileContents(AbstractFile $file, $contents)
1820
    {
1821
        // Check if user is allowed to edit
1822
        $this->assureFileWritePermissions($file);
1823
        $this->eventDispatcher->dispatch(
1824
            new BeforeFileContentsSetEvent($file, $contents)
1825
        );
1826
        // Call driver method to update the file and update file index entry afterwards
1827
        $result = $this->driver->setFileContents($file->getIdentifier(), $contents);
1828
        if ($file instanceof File) {
1829
            $this->getIndexer()->updateIndexEntry($file);
1830
        }
1831
        $this->eventDispatcher->dispatch(
1832
            new AfterFileContentsSetEvent($file, $contents)
1833
        );
1834
        return $result;
1835
    }
1836
1837
    /**
1838
     * Creates a new file
1839
     *
1840
     * previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_newfile()
1841
     *
1842
     * @param string $fileName The name of the file to be created
1843
     * @param Folder $targetFolderObject The target folder where the file should be created
1844
     *
1845
     * @throws Exception\IllegalFileExtensionException
1846
     * @throws Exception\InsufficientFolderWritePermissionsException
1847
     * @return FileInterface The file object
1848
     */
1849
    public function createFile($fileName, Folder $targetFolderObject)
1850
    {
1851
        $this->assureFileAddPermissions($targetFolderObject, $fileName);
1852
        $this->eventDispatcher->dispatch(
1853
            new BeforeFileCreatedEvent($fileName, $targetFolderObject)
1854
        );
1855
        $newFileIdentifier = $this->driver->createFile($fileName, $targetFolderObject->getIdentifier());
1856
        $this->eventDispatcher->dispatch(
1857
            new AfterFileCreatedEvent($newFileIdentifier, $targetFolderObject)
1858
        );
1859
        return $this->getFileByIdentifier($newFileIdentifier);
1860
    }
1861
1862
    /**
1863
     * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::deleteFile()
1864
     *
1865
     * @param FileInterface $fileObject
1866
     * @throws Exception\InsufficientFileAccessPermissionsException
1867
     * @throws Exception\FileOperationErrorException
1868
     * @return bool TRUE if deletion succeeded
1869
     */
1870
    public function deleteFile($fileObject)
1871
    {
1872
        $this->assureFileDeletePermissions($fileObject);
1873
1874
        $this->eventDispatcher->dispatch(
1875
            new BeforeFileDeletedEvent($fileObject)
1876
        );
1877
        $deleted = true;
1878
1879
        if ($this->driver->fileExists($fileObject->getIdentifier())) {
1880
            // Disable permission check to find nearest recycler and move file without errors
1881
            $currentPermissions = $this->evaluatePermissions;
1882
            $this->evaluatePermissions = false;
1883
1884
            $recyclerFolder = $this->getNearestRecyclerFolder($fileObject);
1885
            if ($recyclerFolder === null) {
1886
                $result = $this->driver->deleteFile($fileObject->getIdentifier());
1887
            } else {
1888
                $result = $this->moveFile($fileObject, $recyclerFolder);
1889
                $deleted = false;
1890
            }
1891
1892
            $this->evaluatePermissions = $currentPermissions;
1893
1894
            if (!$result) {
1895
                throw new FileOperationErrorException('Deleting the file "' . $fileObject->getIdentifier() . '\' failed.', 1329831691);
1896
            }
1897
        }
1898
        // Mark the file object as deleted
1899
        if ($deleted && $fileObject instanceof AbstractFile) {
1900
            $fileObject->setDeleted();
1901
        }
1902
1903
        $this->eventDispatcher->dispatch(
1904
            new AfterFileDeletedEvent($fileObject)
1905
        );
1906
1907
        return true;
1908
    }
1909
1910
    /**
1911
     * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_copy()
1912
     * copies a source file (from any location) in to the target
1913
     * folder, the latter has to be part of this storage
1914
     *
1915
     * @param FileInterface $file
1916
     * @param Folder $targetFolder
1917
     * @param string $targetFileName an optional destination fileName
1918
     * @param string $conflictMode a value of the DuplicationBehavior enumeration
1919
     *
1920
     * @throws \Exception|Exception\AbstractFileOperationException
1921
     * @throws Exception\ExistingTargetFileNameException
1922
     * @return FileInterface
1923
     */
1924
    public function copyFile(FileInterface $file, Folder $targetFolder, $targetFileName = null, $conflictMode = DuplicationBehavior::RENAME)
1925
    {
1926
        $conflictMode = DuplicationBehavior::cast($conflictMode);
1927
        if ($targetFileName === null) {
1928
            $targetFileName = $file->getName();
1929
        }
1930
        $sanitizedTargetFileName = $this->driver->sanitizeFileName($targetFileName);
1931
        $this->assureFileCopyPermissions($file, $targetFolder, $sanitizedTargetFileName);
1932
1933
        $this->eventDispatcher->dispatch(
1934
            new BeforeFileCopiedEvent($file, $targetFolder)
1935
        );
1936
1937
        // File exists and we should abort, let's abort
1938
        if ($conflictMode->equals(DuplicationBehavior::CANCEL) && $targetFolder->hasFile($sanitizedTargetFileName)) {
1939
            throw new ExistingTargetFileNameException('The target file already exists.', 1320291064);
1940
        }
1941
        // File exists and we should find another name, let's find another one
1942
        if ($conflictMode->equals(DuplicationBehavior::RENAME) && $targetFolder->hasFile($sanitizedTargetFileName)) {
1943
            $sanitizedTargetFileName = $this->getUniqueName($targetFolder, $sanitizedTargetFileName);
1944
        }
1945
        $sourceStorage = $file->getStorage();
1946
        // Call driver method to create a new file from an existing file object,
1947
        // and return the new file object
1948
        if ($sourceStorage === $this) {
1949
            $newFileObjectIdentifier = $this->driver->copyFileWithinStorage($file->getIdentifier(), $targetFolder->getIdentifier(), $sanitizedTargetFileName);
1950
        } else {
1951
            $tempPath = $file->getForLocalProcessing();
1952
            $newFileObjectIdentifier = $this->driver->addFile($tempPath, $targetFolder->getIdentifier(), $sanitizedTargetFileName);
1953
        }
1954
        $newFileObject = $this->getFileByIdentifier($newFileObjectIdentifier);
1955
1956
        $this->eventDispatcher->dispatch(
1957
            new AfterFileCopiedEvent($file, $targetFolder, $newFileObjectIdentifier, $newFileObject)
1958
        );
1959
        return $newFileObject;
1960
    }
1961
1962
    /**
1963
     * Moves a $file into a $targetFolder
1964
     * the target folder has to be part of this storage
1965
     *
1966
     * previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_move()
1967
     *
1968
     * @param FileInterface $file
1969
     * @param Folder $targetFolder
1970
     * @param string $targetFileName an optional destination fileName
1971
     * @param string $conflictMode a value of the DuplicationBehavior enumeration
1972
     *
1973
     * @throws Exception\ExistingTargetFileNameException
1974
     * @throws \RuntimeException
1975
     * @return FileInterface
1976
     */
1977
    public function moveFile($file, $targetFolder, $targetFileName = null, $conflictMode = DuplicationBehavior::RENAME)
1978
    {
1979
        $conflictMode = DuplicationBehavior::cast($conflictMode);
1980
        if ($targetFileName === null) {
1981
            $targetFileName = $file->getName();
1982
        }
1983
        $originalFolder = $file->getParentFolder();
1984
        $sanitizedTargetFileName = $this->driver->sanitizeFileName($targetFileName);
1985
        $this->assureFileMovePermissions($file, $targetFolder, $sanitizedTargetFileName);
1986
        if ($targetFolder->hasFile($sanitizedTargetFileName)) {
1987
            // File exists and we should abort, let's abort
1988
            if ($conflictMode->equals(DuplicationBehavior::RENAME)) {
1989
                $sanitizedTargetFileName = $this->getUniqueName($targetFolder, $sanitizedTargetFileName);
1990
            } elseif ($conflictMode->equals(DuplicationBehavior::CANCEL)) {
1991
                throw new ExistingTargetFileNameException('The target file already exists', 1329850997);
1992
            }
1993
        }
1994
        $this->eventDispatcher->dispatch(
1995
            new BeforeFileMovedEvent($file, $targetFolder, $sanitizedTargetFileName)
1996
        );
1997
        $sourceStorage = $file->getStorage();
1998
        // Call driver method to move the file and update the index entry
1999
        try {
2000
            if ($sourceStorage === $this) {
2001
                $newIdentifier = $this->driver->moveFileWithinStorage($file->getIdentifier(), $targetFolder->getIdentifier(), $sanitizedTargetFileName);
2002
                if (!$file instanceof AbstractFile) {
2003
                    throw new \RuntimeException('The given file is not of type AbstractFile.', 1384209025);
2004
                }
2005
                $file->updateProperties(['identifier' => $newIdentifier]);
2006
            } else {
2007
                $tempPath = $file->getForLocalProcessing();
2008
                $newIdentifier = $this->driver->addFile($tempPath, $targetFolder->getIdentifier(), $sanitizedTargetFileName);
2009
2010
                // Disable permission check to find nearest recycler and move file without errors
2011
                $currentPermissions = $sourceStorage->evaluatePermissions;
2012
                $sourceStorage->evaluatePermissions = false;
2013
2014
                $recyclerFolder = $sourceStorage->getNearestRecyclerFolder($file);
2015
                if ($recyclerFolder === null) {
2016
                    $sourceStorage->driver->deleteFile($file->getIdentifier());
2017
                } else {
2018
                    $sourceStorage->moveFile($file, $recyclerFolder);
2019
                }
2020
                $sourceStorage->evaluatePermissions = $currentPermissions;
2021
                if ($file instanceof File) {
2022
                    $file->updateProperties(['storage' => $this->getUid(), 'identifier' => $newIdentifier]);
2023
                }
2024
            }
2025
            $this->getIndexer()->updateIndexEntry($file);
2026
        } catch (\TYPO3\CMS\Core\Exception $e) {
2027
            echo $e->getMessage();
2028
        }
2029
        $this->eventDispatcher->dispatch(
2030
            new AfterFileMovedEvent($file, $targetFolder, $originalFolder)
2031
        );
2032
        return $file;
2033
    }
2034
2035
    /**
2036
     * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_rename()
2037
     *
2038
     * @param FileInterface $file
2039
     * @param string $targetFileName
2040
     * @param string $conflictMode
2041
     * @return FileInterface
2042
     * @throws ExistingTargetFileNameException
2043
     */
2044
    public function renameFile($file, $targetFileName, $conflictMode = DuplicationBehavior::RENAME)
2045
    {
2046
        // The name should be different from the current.
2047
        if ($file->getName() === $targetFileName) {
2048
            return $file;
2049
        }
2050
        $sanitizedTargetFileName = $this->driver->sanitizeFileName($targetFileName);
2051
        $this->assureFileRenamePermissions($file, $sanitizedTargetFileName);
2052
        $this->eventDispatcher->dispatch(
2053
            new BeforeFileRenamedEvent($file, $sanitizedTargetFileName)
2054
        );
2055
2056
        $conflictMode = DuplicationBehavior::cast($conflictMode);
2057
2058
        // Call driver method to rename the file and update the index entry
2059
        try {
2060
            $newIdentifier = $this->driver->renameFile($file->getIdentifier(), $sanitizedTargetFileName);
2061
            if ($file instanceof File) {
2062
                $file->updateProperties(['identifier' => $newIdentifier]);
2063
            }
2064
            $this->getIndexer()->updateIndexEntry($file);
2065
        } catch (ExistingTargetFileNameException $exception) {
2066
            if ($conflictMode->equals(DuplicationBehavior::RENAME)) {
2067
                $newName = $this->getUniqueName($file->getParentFolder(), $sanitizedTargetFileName);
2068
                $file = $this->renameFile($file, $newName);
2069
            } elseif ($conflictMode->equals(DuplicationBehavior::CANCEL)) {
2070
                throw $exception;
2071
            } elseif ($conflictMode->equals(DuplicationBehavior::REPLACE)) {
2072
                $sourceFileIdentifier = substr($file->getCombinedIdentifier(), 0, 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

2072
                $sourceFileIdentifier = substr($file->/** @scrutinizer ignore-call */ getCombinedIdentifier(), 0, strrpos($file->getCombinedIdentifier(), '/') + 1) . $targetFileName;
Loading history...
2073
                $sourceFile = $this->getResourceFactoryInstance()->getFileObjectFromCombinedIdentifier($sourceFileIdentifier);
2074
                $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

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

2140
            $resultObject = $this->replaceFile(/** @scrutinizer ignore-type */ $file, $localFilePath);
Loading history...
2141
        } else {
2142
            $resultObject = $this->addFile($localFilePath, $targetFolder, $targetFileName, (string)$conflictMode);
2143
        }
2144
        return $resultObject;
2145
    }
2146
2147
    /********************
2148
     * FOLDER ACTIONS
2149
     ********************/
2150
    /**
2151
     * Returns an array with all file objects in a folder and its subfolders, with the file identifiers as keys.
2152
     * @todo check if this is a duplicate
2153
     * @param Folder $folder
2154
     * @return File[]
2155
     */
2156
    protected function getAllFileObjectsInFolder(Folder $folder)
2157
    {
2158
        $files = [];
2159
        $folderQueue = [$folder];
2160
        while (!empty($folderQueue)) {
2161
            $folder = array_shift($folderQueue);
2162
            foreach ($folder->getSubfolders() as $subfolder) {
2163
                $folderQueue[] = $subfolder;
2164
            }
2165
            foreach ($folder->getFiles() as $file) {
2166
                /** @var FileInterface $file */
2167
                $files[$file->getIdentifier()] = $file;
2168
            }
2169
        }
2170
2171
        return $files;
2172
    }
2173
2174
    /**
2175
     * Moves a folder. If you want to move a folder from this storage to another
2176
     * one, call this method on the target storage, otherwise you will get an exception.
2177
     *
2178
     * @param Folder $folderToMove The folder to move.
2179
     * @param Folder $targetParentFolder The target parent folder
2180
     * @param string $newFolderName
2181
     * @param string $conflictMode a value of the DuplicationBehavior enumeration
2182
     *
2183
     * @throws \Exception|\TYPO3\CMS\Core\Exception
2184
     * @throws \InvalidArgumentException
2185
     * @throws InvalidTargetFolderException
2186
     * @return Folder
2187
     */
2188
    public function moveFolder(Folder $folderToMove, Folder $targetParentFolder, $newFolderName = null, $conflictMode = DuplicationBehavior::RENAME)
2189
    {
2190
        // @todo add tests
2191
        $originalFolder = $folderToMove->getParentFolder();
0 ignored issues
show
Unused Code introduced by
The assignment to $originalFolder is dead and can be removed.
Loading history...
2192
        $this->assureFolderMovePermissions($folderToMove, $targetParentFolder);
2193
        $sourceStorage = $folderToMove->getStorage();
2194
        $returnObject = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $returnObject is dead and can be removed.
Loading history...
2195
        $sanitizedNewFolderName = $this->driver->sanitizeFileName($newFolderName ?: $folderToMove->getName());
2196
        // @todo check if folder already exists in $targetParentFolder, handle this conflict then
2197
        $this->eventDispatcher->dispatch(
2198
            new BeforeFolderMovedEvent($folderToMove, $targetParentFolder, $sanitizedNewFolderName)
2199
        );
2200
        // Get all file objects now so we are able to update them after moving the folder
2201
        $fileObjects = $this->getAllFileObjectsInFolder($folderToMove);
2202
        if ($sourceStorage === $this) {
2203
            if ($this->isWithinFolder($folderToMove, $targetParentFolder)) {
2204
                throw new InvalidTargetFolderException(
2205
                    sprintf(
2206
                        'Cannot move folder "%s" into target folder "%s", because the target folder is already within the folder to be moved!',
2207
                        $folderToMove->getName(),
2208
                        $targetParentFolder->getName()
2209
                    ),
2210
                    1422723050
2211
                );
2212
            }
2213
            $fileMappings = $this->driver->moveFolderWithinStorage($folderToMove->getIdentifier(), $targetParentFolder->getIdentifier(), $sanitizedNewFolderName);
2214
        } else {
2215
            $fileMappings = $this->moveFolderBetweenStorages($folderToMove, $targetParentFolder, $sanitizedNewFolderName);
2216
        }
2217
        // Update the identifier and storage of all file objects
2218
        foreach ($fileObjects as $oldIdentifier => $fileObject) {
2219
            $newIdentifier = $fileMappings[$oldIdentifier];
2220
            $fileObject->updateProperties(['storage' => $this->getUid(), 'identifier' => $newIdentifier]);
2221
            $this->getIndexer()->updateIndexEntry($fileObject);
2222
        }
2223
        $returnObject = $this->getFolder($fileMappings[$folderToMove->getIdentifier()]);
2224
2225
        $this->eventDispatcher->dispatch(
2226
            new AfterFolderMovedEvent($folderToMove, $targetParentFolder, $returnObject)
2227
        );
2228
        return $returnObject;
2229
    }
2230
2231
    /**
2232
     * Moves the given folder from a different storage to the target folder in this storage.
2233
     *
2234
     * @param Folder $folderToMove
2235
     * @param Folder $targetParentFolder
2236
     * @param string $newFolderName
2237
     * @throws NotImplementedMethodException
2238
     */
2239
    protected function moveFolderBetweenStorages(Folder $folderToMove, Folder $targetParentFolder, $newFolderName)
2240
    {
2241
        throw new NotImplementedMethodException('Not yet implemented', 1476046361);
2242
    }
2243
2244
    /**
2245
     * Copies a folder.
2246
     *
2247
     * @param FolderInterface $folderToCopy The folder to copy
2248
     * @param FolderInterface $targetParentFolder The target folder
2249
     * @param string $newFolderName
2250
     * @param string $conflictMode a value of the DuplicationBehavior enumeration
2251
     * @return Folder The new (copied) folder object
2252
     * @throws InvalidTargetFolderException
2253
     */
2254
    public function copyFolder(FolderInterface $folderToCopy, FolderInterface $targetParentFolder, $newFolderName = null, $conflictMode = DuplicationBehavior::RENAME)
2255
    {
2256
        // @todo implement the $conflictMode handling
2257
        $this->assureFolderCopyPermissions($folderToCopy, $targetParentFolder);
2258
        $returnObject = null;
2259
        $sanitizedNewFolderName = $this->driver->sanitizeFileName($newFolderName ?: $folderToCopy->getName());
2260
        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...
2261
            $this->eventDispatcher->dispatch(
2262
                new BeforeFolderCopiedEvent($folderToCopy, $targetParentFolder, $sanitizedNewFolderName)
2263
            );
2264
        }
2265
        $sourceStorage = $folderToCopy->getStorage();
2266
        // call driver method to move the file
2267
        // that also updates the file object properties
2268
        if ($sourceStorage === $this) {
2269
            if ($this->isWithinFolder($folderToCopy, $targetParentFolder)) {
2270
                throw new InvalidTargetFolderException(
2271
                    sprintf(
2272
                        'Cannot copy folder "%s" into target folder "%s", because the target folder is already within the folder to be copied!',
2273
                        $folderToCopy->getName(),
2274
                        $targetParentFolder->getName()
2275
                    ),
2276
                    1422723059
2277
                );
2278
            }
2279
            $this->driver->copyFolderWithinStorage($folderToCopy->getIdentifier(), $targetParentFolder->getIdentifier(), $sanitizedNewFolderName);
2280
            $returnObject = $this->getFolder($targetParentFolder->getSubfolder($sanitizedNewFolderName)->getIdentifier());
2281
        } else {
2282
            $this->copyFolderBetweenStorages($folderToCopy, $targetParentFolder, $sanitizedNewFolderName);
2283
        }
2284
        $this->eventDispatcher->dispatch(
2285
            new AfterFolderCopiedEvent($folderToCopy, $targetParentFolder, $returnObject)
2286
        );
2287
        return $returnObject;
2288
    }
2289
2290
    /**
2291
     * Copies a folder between storages.
2292
     *
2293
     * @param Folder $folderToCopy
2294
     * @param Folder $targetParentFolder
2295
     * @param string $newFolderName
2296
     * @throws NotImplementedMethodException
2297
     */
2298
    protected function copyFolderBetweenStorages(Folder $folderToCopy, Folder $targetParentFolder, $newFolderName)
2299
    {
2300
        throw new NotImplementedMethodException('Not yet implemented.', 1476046386);
2301
    }
2302
2303
    /**
2304
     * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::folder_move()
2305
     *
2306
     * @param Folder $folderObject
2307
     * @param string $newName
2308
     * @throws \Exception
2309
     * @throws \InvalidArgumentException
2310
     * @return Folder
2311
     */
2312
    public function renameFolder($folderObject, $newName)
2313
    {
2314
2315
        // Renaming the folder should check if the parent folder is writable
2316
        // We cannot do this however because we cannot extract the parent folder from a folder currently
2317
        if (!$this->checkFolderActionPermission('rename', $folderObject)) {
2318
            throw new InsufficientUserPermissionsException('You are not allowed to rename the folder "' . $folderObject->getIdentifier() . '\'', 1357811441);
2319
        }
2320
2321
        $sanitizedNewName = $this->driver->sanitizeFileName($newName);
2322
        $returnObject = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $returnObject is dead and can be removed.
Loading history...
2323
        if ($this->driver->folderExistsInFolder($sanitizedNewName, $folderObject->getIdentifier())) {
2324
            throw new \InvalidArgumentException('The folder ' . $sanitizedNewName . ' already exists in folder ' . $folderObject->getIdentifier(), 1325418870);
2325
        }
2326
        $this->eventDispatcher->dispatch(
2327
            new BeforeFolderRenamedEvent($folderObject, $sanitizedNewName)
2328
        );
2329
        $fileObjects = $this->getAllFileObjectsInFolder($folderObject);
2330
        $fileMappings = $this->driver->renameFolder($folderObject->getIdentifier(), $sanitizedNewName);
2331
        // Update the identifier of all file objects
2332
        foreach ($fileObjects as $oldIdentifier => $fileObject) {
2333
            $newIdentifier = $fileMappings[$oldIdentifier];
2334
            $fileObject->updateProperties(['identifier' => $newIdentifier]);
2335
            $this->getIndexer()->updateIndexEntry($fileObject);
2336
        }
2337
        $returnObject = $this->getFolder($fileMappings[$folderObject->getIdentifier()]);
2338
2339
        $this->eventDispatcher->dispatch(
2340
            new AfterFolderRenamedEvent($returnObject)
2341
        );
2342
        return $returnObject;
2343
    }
2344
2345
    /**
2346
     * Previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::folder_delete()
2347
     *
2348
     * @param Folder $folderObject
2349
     * @param bool $deleteRecursively
2350
     * @throws \RuntimeException
2351
     * @throws Exception\InsufficientFolderAccessPermissionsException
2352
     * @throws Exception\InsufficientUserPermissionsException
2353
     * @throws Exception\FileOperationErrorException
2354
     * @throws Exception\InvalidPathException
2355
     * @return bool
2356
     */
2357
    public function deleteFolder($folderObject, $deleteRecursively = false)
2358
    {
2359
        $isEmpty = $this->driver->isFolderEmpty($folderObject->getIdentifier());
2360
        $this->assureFolderDeletePermission($folderObject, $deleteRecursively && !$isEmpty);
2361
        if (!$isEmpty && !$deleteRecursively) {
2362
            throw new \RuntimeException('Could not delete folder "' . $folderObject->getIdentifier() . '" because it is not empty.', 1325952534);
2363
        }
2364
2365
        $this->eventDispatcher->dispatch(
2366
            new BeforeFolderDeletedEvent($folderObject)
2367
        );
2368
2369
        foreach ($this->getFilesInFolder($folderObject, 0, 0, false, $deleteRecursively) as $file) {
2370
            $this->deleteFile($file);
2371
        }
2372
2373
        $result = $this->driver->deleteFolder($folderObject->getIdentifier(), $deleteRecursively);
2374
2375
        $this->eventDispatcher->dispatch(
2376
            new AfterFolderDeletedEvent($folderObject, $result)
2377
        );
2378
        return $result;
2379
    }
2380
2381
    /**
2382
     * Returns the Identifier for a folder within a given folder.
2383
     *
2384
     * @param string $folderName The name of the target folder
2385
     * @param Folder $parentFolder
2386
     * @param bool $returnInaccessibleFolderObject
2387
     * @return Folder|InaccessibleFolder
2388
     * @throws \Exception
2389
     * @throws Exception\InsufficientFolderAccessPermissionsException
2390
     */
2391
    public function getFolderInFolder($folderName, Folder $parentFolder, $returnInaccessibleFolderObject = false)
2392
    {
2393
        $folderIdentifier = $this->driver->getFolderInFolder($folderName, $parentFolder->getIdentifier());
2394
        return $this->getFolder($folderIdentifier, $returnInaccessibleFolderObject);
2395
    }
2396
2397
    /**
2398
     * @param Folder $folder
2399
     * @param int $start
2400
     * @param int $maxNumberOfItems
2401
     * @param bool $useFilters
2402
     * @param bool $recursive
2403
     * @param string $sort Property name used to sort the items.
2404
     *                     Among them may be: '' (empty, no sorting), name,
2405
     *                     fileext, size, tstamp and rw.
2406
     *                     If a driver does not support the given property, it
2407
     *                     should fall back to "name".
2408
     * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
2409
     * @return Folder[]
2410
     */
2411
    public function getFoldersInFolder(Folder $folder, $start = 0, $maxNumberOfItems = 0, $useFilters = true, $recursive = false, $sort = '', $sortRev = false)
2412
    {
2413
        $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...
2414
2415
        $folderIdentifiers = $this->driver->getFoldersInFolder($folder->getIdentifier(), $start, $maxNumberOfItems, $recursive, $filters, $sort, $sortRev);
2416
2417
        // Exclude processing folders
2418
        foreach ($this->getProcessingFolders() as $processingFolder) {
2419
            $processingIdentifier = $processingFolder->getIdentifier();
2420
            if (isset($folderIdentifiers[$processingIdentifier])) {
2421
                unset($folderIdentifiers[$processingIdentifier]);
2422
            }
2423
        }
2424
        $folders = [];
2425
        foreach ($folderIdentifiers as $folderIdentifier) {
2426
            $folders[$folderIdentifier] = $this->getFolder($folderIdentifier, true);
2427
        }
2428
        return $folders;
2429
    }
2430
2431
    /**
2432
     * @param Folder  $folder
2433
     * @param bool $useFilters
2434
     * @param bool $recursive
2435
     * @return int Number of subfolders
2436
     * @throws Exception\InsufficientFolderAccessPermissionsException
2437
     */
2438
    public function countFoldersInFolder(Folder $folder, $useFilters = true, $recursive = false)
2439
    {
2440
        $this->assureFolderReadPermission($folder);
2441
        $filters = $useFilters ? $this->fileAndFolderNameFilters : [];
2442
        return $this->driver->countFoldersInFolder($folder->getIdentifier(), $recursive, $filters);
2443
    }
2444
2445
    /**
2446
     * Returns TRUE if the specified folder exists.
2447
     *
2448
     * @param string $identifier
2449
     * @return bool
2450
     */
2451
    public function hasFolder($identifier)
2452
    {
2453
        $this->assureFolderReadPermission();
2454
        return $this->driver->folderExists($identifier);
2455
    }
2456
2457
    /**
2458
     * Checks if the given file exists in the given folder
2459
     *
2460
     * @param string $folderName
2461
     * @param Folder $folder
2462
     * @return bool
2463
     */
2464
    public function hasFolderInFolder($folderName, Folder $folder)
2465
    {
2466
        $this->assureFolderReadPermission($folder);
2467
        return $this->driver->folderExistsInFolder($folderName, $folder->getIdentifier());
2468
    }
2469
2470
    /**
2471
     * Creates a new folder.
2472
     *
2473
     * previously in \TYPO3\CMS\Core\Utility\File\ExtendedFileUtility::func_newfolder()
2474
     *
2475
     * @param string $folderName The new folder name
2476
     * @param Folder $parentFolder (optional) the parent folder to create the new folder inside of. If not given, the root folder is used
2477
     * @return Folder
2478
     * @throws Exception\ExistingTargetFolderException
2479
     * @throws Exception\InsufficientFolderAccessPermissionsException
2480
     * @throws Exception\InsufficientFolderWritePermissionsException
2481
     * @throws \Exception
2482
     */
2483
    public function createFolder($folderName, Folder $parentFolder = null)
2484
    {
2485
        if ($parentFolder === null) {
2486
            $parentFolder = $this->getRootLevelFolder();
2487
        } elseif (!$this->driver->folderExists($parentFolder->getIdentifier())) {
2488
            throw new \InvalidArgumentException('Parent folder "' . $parentFolder->getIdentifier() . '" does not exist.', 1325689164);
2489
        }
2490
        if (!$this->checkFolderActionPermission('add', $parentFolder)) {
2491
            throw new InsufficientFolderWritePermissionsException('You are not allowed to create directories in the folder "' . $parentFolder->getIdentifier() . '"', 1323059807);
2492
        }
2493
        if ($this->driver->folderExistsInFolder($folderName, $parentFolder->getIdentifier())) {
2494
            throw new ExistingTargetFolderException('Folder "' . $folderName . '" already exists.', 1423347324);
2495
        }
2496
2497
        $this->eventDispatcher->dispatch(
2498
            new BeforeFolderAddedEvent($parentFolder, $folderName)
2499
        );
2500
2501
        $newFolder = $this->getDriver()->createFolder($folderName, $parentFolder->getIdentifier(), true);
2502
        $newFolder = $this->getFolder($newFolder);
2503
2504
        $this->eventDispatcher->dispatch(
2505
            new AfterFolderAddedEvent($newFolder)
2506
        );
2507
2508
        return $newFolder;
2509
    }
2510
2511
    /**
2512
     * Retrieves information about a folder
2513
     *
2514
     * @param Folder $folder
2515
     * @return array
2516
     */
2517
    public function getFolderInfo(Folder $folder)
2518
    {
2519
        return $this->driver->getFolderInfoByIdentifier($folder->getIdentifier());
2520
    }
2521
2522
    /**
2523
     * Returns the default folder where new files are stored if no other folder is given.
2524
     *
2525
     * @return Folder
2526
     */
2527
    public function getDefaultFolder()
2528
    {
2529
        return $this->getFolder($this->driver->getDefaultFolder());
2530
    }
2531
2532
    /**
2533
     * @param string $identifier
2534
     * @param bool $returnInaccessibleFolderObject
2535
     *
2536
     * @return Folder|InaccessibleFolder
2537
     * @throws \Exception
2538
     * @throws Exception\InsufficientFolderAccessPermissionsException
2539
     */
2540
    public function getFolder($identifier, $returnInaccessibleFolderObject = false)
2541
    {
2542
        $data = $this->driver->getFolderInfoByIdentifier($identifier);
2543
        $folder = $this->createFolderObject($data['identifier'] ?? '', $data['name'] ?? '');
2544
2545
        try {
2546
            $this->assureFolderReadPermission($folder);
2547
        } catch (InsufficientFolderAccessPermissionsException $e) {
2548
            $folder = null;
2549
            if ($returnInaccessibleFolderObject) {
2550
                // if parent folder is readable return inaccessible folder object
2551
                $parentPermissions = $this->driver->getPermissions($this->driver->getParentFolderIdentifierOfIdentifier($identifier));
2552
                if ($parentPermissions['r']) {
2553
                    $folder = GeneralUtility::makeInstance(
2554
                        InaccessibleFolder::class,
2555
                        $this,
2556
                        $data['identifier'],
2557
                        $data['name']
2558
                    );
2559
                }
2560
            }
2561
2562
            if ($folder === null) {
2563
                throw $e;
2564
            }
2565
        }
2566
        return $folder;
2567
    }
2568
2569
    /**
2570
     * Returns TRUE if the specified file is in a folder that is set a processing for a storage
2571
     *
2572
     * @param string $identifier
2573
     * @return bool
2574
     */
2575
    public function isWithinProcessingFolder($identifier)
2576
    {
2577
        $inProcessingFolder = false;
2578
        foreach ($this->getProcessingFolders() as $processingFolder) {
2579
            if ($this->driver->isWithin($processingFolder->getIdentifier(), $identifier)) {
2580
                $inProcessingFolder = true;
2581
                break;
2582
            }
2583
        }
2584
        return $inProcessingFolder;
2585
    }
2586
2587
    /**
2588
     * Checks if a resource (file or folder) is within the given folder
2589
     *
2590
     * @param Folder $folder
2591
     * @param ResourceInterface $resource
2592
     * @return bool
2593
     * @throws \InvalidArgumentException
2594
     */
2595
    public function isWithinFolder(Folder $folder, ResourceInterface $resource)
2596
    {
2597
        if ($folder->getStorage() !== $this) {
2598
            throw new \InvalidArgumentException('Given folder "' . $folder->getIdentifier() . '" is not part of this storage!', 1422709241);
2599
        }
2600
        if ($folder->getStorage() !== $resource->getStorage()) {
2601
            return false;
2602
        }
2603
        return $this->driver->isWithin($folder->getIdentifier(), $resource->getIdentifier());
2604
    }
2605
2606
    /**
2607
     * Returns the folders on the root level of the storage
2608
     * or the first mount point of this storage for this user
2609
     * if $respectFileMounts is set.
2610
     *
2611
     * @param bool $respectFileMounts
2612
     * @return Folder
2613
     */
2614
    public function getRootLevelFolder($respectFileMounts = true)
2615
    {
2616
        if ($respectFileMounts && !empty($this->fileMounts)) {
2617
            $mount = reset($this->fileMounts);
2618
            return $mount['folder'];
2619
        }
2620
        return $this->createFolderObject($this->driver->getRootLevelFolder(), '');
2621
    }
2622
2623
    /**
2624
     * Returns the destination path/fileName of a unique fileName/foldername in that path.
2625
     * If $theFile exists in $theDest (directory) the file have numbers appended up to $this->maxNumber.
2626
     * Hereafter a unique string will be appended.
2627
     * This function is used by fx. DataHandler when files are attached to records
2628
     * and needs to be uniquely named in the uploads/* folders
2629
     *
2630
     * @param FolderInterface $folder
2631
     * @param string $theFile The input fileName to check
2632
     * @param bool $dontCheckForUnique If set the fileName is returned with the path prepended without checking whether it already existed!
2633
     *
2634
     * @throws \RuntimeException
2635
     * @return string A unique fileName inside $folder, based on $theFile.
2636
     * @see \TYPO3\CMS\Core\Utility\File\BasicFileUtility::getUniqueName()
2637
     */
2638
    protected function getUniqueName(FolderInterface $folder, $theFile, $dontCheckForUnique = false)
2639
    {
2640
        $maxNumber = 99;
2641
        // Fetches info about path, name, extension of $theFile
2642
        $origFileInfo = PathUtility::pathinfo($theFile);
2643
        // Check if the file exists and if not - return the fileName...
2644
        // The destinations file
2645
        $theDestFile = $origFileInfo['basename'];
2646
        // If the file does NOT exist we return this fileName
2647
        if (!$this->driver->fileExistsInFolder($theDestFile, $folder->getIdentifier()) || $dontCheckForUnique) {
2648
            return $theDestFile;
2649
        }
2650
        // Well the fileName in its pure form existed. Now we try to append
2651
        // numbers / unique-strings and see if we can find an available fileName
2652
        // This removes _xx if appended to the file
2653
        $theTempFileBody = preg_replace('/_[0-9][0-9]$/', '', $origFileInfo['filename']);
2654
        $theOrigExt = $origFileInfo['extension'] ? '.' . $origFileInfo['extension'] : '';
2655
        for ($a = 1; $a <= $maxNumber + 1; $a++) {
2656
            // First we try to append numbers
2657
            if ($a <= $maxNumber) {
2658
                $insert = '_' . sprintf('%02d', $a);
2659
            } else {
2660
                $insert = '_' . substr(md5(StringUtility::getUniqueId()), 0, 6);
2661
            }
2662
            $theTestFile = $theTempFileBody . $insert . $theOrigExt;
2663
            // The destinations file
2664
            $theDestFile = $theTestFile;
2665
            // If the file does NOT exist we return this fileName
2666
            if (!$this->driver->fileExistsInFolder($theDestFile, $folder->getIdentifier())) {
2667
                return $theDestFile;
2668
            }
2669
        }
2670
        throw new \RuntimeException('Last possible name "' . $theDestFile . '" is already taken.', 1325194291);
2671
    }
2672
2673
    /**
2674
     * @return ResourceFactory
2675
     */
2676
    protected function getFileFactory()
2677
    {
2678
        return GeneralUtility::makeInstance(ResourceFactory::class);
2679
    }
2680
2681
    /**
2682
     * @return Index\FileIndexRepository
2683
     */
2684
    protected function getFileIndexRepository()
2685
    {
2686
        return FileIndexRepository::getInstance();
2687
    }
2688
2689
    /**
2690
     * @return Service\FileProcessingService
2691
     */
2692
    protected function getFileProcessingService()
2693
    {
2694
        if (!$this->fileProcessingService) {
2695
            $this->fileProcessingService = GeneralUtility::makeInstance(FileProcessingService::class, $this, $this->driver, $this->eventDispatcher);
2696
        }
2697
        return $this->fileProcessingService;
2698
    }
2699
2700
    /**
2701
     * Gets the role of a folder.
2702
     *
2703
     * @param FolderInterface $folder Folder object to get the role from
2704
     * @return string The role the folder has
2705
     */
2706
    public function getRole(FolderInterface $folder)
2707
    {
2708
        $folderRole = FolderInterface::ROLE_DEFAULT;
2709
        $identifier = $folder->getIdentifier();
2710
        if (method_exists($this->driver, 'getRole')) {
2711
            $folderRole = $this->driver->getRole($folder->getIdentifier());
2712
        }
2713
        if (isset($this->fileMounts[$identifier])) {
2714
            $folderRole = FolderInterface::ROLE_MOUNT;
2715
2716
            if (!empty($this->fileMounts[$identifier]['read_only'])) {
2717
                $folderRole = FolderInterface::ROLE_READONLY_MOUNT;
2718
            }
2719
            if ($this->fileMounts[$identifier]['user_mount']) {
2720
                $folderRole = FolderInterface::ROLE_USER_MOUNT;
2721
            }
2722
        }
2723
        if ($folder instanceof Folder && $this->isProcessingFolder($folder)) {
2724
            $folderRole = FolderInterface::ROLE_PROCESSING;
2725
        }
2726
2727
        return $folderRole;
2728
    }
2729
2730
    /**
2731
     * Getter function to return the folder where the files can
2732
     * be processed. Does not check for access rights here.
2733
     *
2734
     * @param File $file Specific file you want to have the processing folder for
2735
     * @return Folder
2736
     */
2737
    public function getProcessingFolder(File $file = null)
2738
    {
2739
        // If a file is given, make sure to return the processing folder of the correct storage
2740
        if ($file !== null && $file->getStorage()->getUid() !== $this->getUid()) {
2741
            return $file->getStorage()->getProcessingFolder($file);
2742
        }
2743
        if (!isset($this->processingFolder)) {
2744
            $processingFolder = self::DEFAULT_ProcessingFolder;
2745
            if (!empty($this->storageRecord['processingfolder'])) {
2746
                $processingFolder = $this->storageRecord['processingfolder'];
2747
            }
2748
            try {
2749
                if (strpos($processingFolder, ':') !== false) {
2750
                    [$storageUid, $processingFolderIdentifier] = explode(':', $processingFolder, 2);
2751
                    $storage = GeneralUtility::makeInstance(StorageRepository::class)->findByUid((int)$storageUid);
2752
                    if ($storage->hasFolder($processingFolderIdentifier)) {
2753
                        $this->processingFolder = $storage->getFolder($processingFolderIdentifier);
2754
                    } else {
2755
                        $rootFolder = $storage->getRootLevelFolder(false);
2756
                        $currentEvaluatePermissions = $storage->getEvaluatePermissions();
2757
                        $storage->setEvaluatePermissions(false);
2758
                        $this->processingFolder = $storage->createFolder(
2759
                            ltrim($processingFolderIdentifier, '/'),
2760
                            $rootFolder
2761
                        );
2762
                        $storage->setEvaluatePermissions($currentEvaluatePermissions);
2763
                    }
2764
                } else {
2765
                    if ($this->driver->folderExists($processingFolder) === false) {
2766
                        $rootFolder = $this->getRootLevelFolder(false);
2767
                        try {
2768
                            $currentEvaluatePermissions = $this->evaluatePermissions;
2769
                            $this->evaluatePermissions = false;
2770
                            $this->processingFolder = $this->createFolder(
2771
                                $processingFolder,
2772
                                $rootFolder
2773
                            );
2774
                            $this->evaluatePermissions = $currentEvaluatePermissions;
2775
                        } catch (\InvalidArgumentException $e) {
2776
                            $this->processingFolder = GeneralUtility::makeInstance(
2777
                                InaccessibleFolder::class,
2778
                                $this,
2779
                                $processingFolder,
2780
                                $processingFolder
2781
                            );
2782
                        }
2783
                    } else {
2784
                        $data = $this->driver->getFolderInfoByIdentifier($processingFolder);
2785
                        $this->processingFolder = $this->createFolderObject($data['identifier'], $data['name']);
2786
                    }
2787
                }
2788
            } catch (InsufficientFolderWritePermissionsException|ResourcePermissionsUnavailableException $e) {
2789
                $this->processingFolder = GeneralUtility::makeInstance(
2790
                    InaccessibleFolder::class,
2791
                    $this,
2792
                    $processingFolder,
2793
                    $processingFolder
2794
                );
2795
            }
2796
        }
2797
2798
        $processingFolder = $this->processingFolder;
2799
        if (!empty($file)) {
2800
            $processingFolder = $this->getNestedProcessingFolder($file, $processingFolder);
2801
        }
2802
        return $processingFolder;
2803
    }
2804
2805
    /**
2806
     * Getter function to return the the file's corresponding hashed subfolder
2807
     * of the processed folder
2808
     *
2809
     * @param File $file
2810
     * @param Folder $rootProcessingFolder
2811
     * @return Folder
2812
     * @throws Exception\InsufficientFolderWritePermissionsException
2813
     */
2814
    protected function getNestedProcessingFolder(File $file, Folder $rootProcessingFolder)
2815
    {
2816
        $processingFolder = $rootProcessingFolder;
2817
        $nestedFolderNames = $this->getNamesForNestedProcessingFolder(
2818
            $file->getIdentifier(),
2819
            self::PROCESSING_FOLDER_LEVELS
2820
        );
2821
2822
        try {
2823
            foreach ($nestedFolderNames as $folderName) {
2824
                if ($processingFolder->hasFolder($folderName)) {
2825
                    $processingFolder = $processingFolder->getSubfolder($folderName);
2826
                } else {
2827
                    $currentEvaluatePermissions = $processingFolder->getStorage()->getEvaluatePermissions();
2828
                    $processingFolder->getStorage()->setEvaluatePermissions(false);
2829
                    $processingFolder = $processingFolder->createFolder($folderName);
2830
                    $processingFolder->getStorage()->setEvaluatePermissions($currentEvaluatePermissions);
2831
                }
2832
            }
2833
        } catch (FolderDoesNotExistException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2834
        }
2835
2836
        return $processingFolder;
2837
    }
2838
2839
    /**
2840
     * Generates appropriate hashed sub-folder path for a given file identifier
2841
     *
2842
     * @param string $fileIdentifier
2843
     * @param int $levels
2844
     * @return string[]
2845
     */
2846
    protected function getNamesForNestedProcessingFolder($fileIdentifier, $levels)
2847
    {
2848
        $names = [];
2849
        if ($levels === 0) {
2850
            return $names;
2851
        }
2852
        $hash = md5($fileIdentifier);
2853
        for ($i = 1; $i <= $levels; $i++) {
2854
            $names[] = substr($hash, $i, 1);
2855
        }
2856
        return $names;
2857
    }
2858
2859
    /**
2860
     * Gets the driver Type configured for this storage.
2861
     *
2862
     * @return string
2863
     */
2864
    public function getDriverType()
2865
    {
2866
        return $this->storageRecord['driver'];
2867
    }
2868
2869
    /**
2870
     * Gets the Indexer.
2871
     *
2872
     * @return Index\Indexer
2873
     */
2874
    protected function getIndexer()
2875
    {
2876
        return GeneralUtility::makeInstance(Indexer::class, $this);
2877
    }
2878
2879
    /**
2880
     * @param bool $isDefault
2881
     */
2882
    public function setDefault($isDefault)
2883
    {
2884
        $this->isDefault = (bool)$isDefault;
2885
    }
2886
2887
    /**
2888
     * @return bool
2889
     */
2890
    public function isDefault()
2891
    {
2892
        return $this->isDefault;
2893
    }
2894
2895
    /**
2896
     * @return ResourceFactory
2897
     */
2898
    public function getResourceFactoryInstance(): ResourceFactory
2899
    {
2900
        return GeneralUtility::makeInstance(ResourceFactory::class);
2901
    }
2902
2903
    /**
2904
     * Returns the current BE user.
2905
     *
2906
     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
2907
     */
2908
    protected function getBackendUser()
2909
    {
2910
        return $GLOBALS['BE_USER'];
2911
    }
2912
2913
    /**
2914
     * Get the nearest Recycler folder for given file
2915
     *
2916
     * Return null if:
2917
     *  - There is no folder with ROLE_RECYCLER in the rootline of the given File
2918
     *  - File is a ProcessedFile (we don't know the concept of recycler folders for processedFiles)
2919
     *  - File is located in a folder with ROLE_RECYCLER
2920
     *
2921
     * @param FileInterface $file
2922
     * @return Folder|null
2923
     */
2924
    protected function getNearestRecyclerFolder(FileInterface $file)
2925
    {
2926
        if ($file instanceof ProcessedFile) {
2927
            return null;
2928
        }
2929
        // if the storage is not browsable we cannot fetch the parent folder of the file so no recycler handling is possible
2930
        if (!$this->isBrowsable()) {
2931
            return null;
2932
        }
2933
2934
        $recyclerFolder = null;
2935
        $folder = $file->getParentFolder();
2936
2937
        do {
2938
            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

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