Passed
Push — master ( 68a314...38dcf7 )
by
unknown
17:08
created

ResourceStorage   F

Complexity

Total Complexity 403

Size/Duplication

Total Lines 2851
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 403
eloc 905
dl 0
loc 2851
rs 1.695
c 0
b 0
f 0

119 Methods

Rating   Name   Duplication   Size   Complexity  
A getEvaluatePermissions() 0 3 1
A usesCaseSensitiveIdentifiers() 0 3 1
A moveFolder() 0 39 5
C getPublicUrl() 0 47 14
A hashFile() 0 3 1
A assureFileCopyPermissions() 0 17 6
A setConfiguration() 0 3 1
A processFile() 0 8 2
A isProcessingFolder() 0 10 3
A setFileContents() 0 16 2
A setEvaluatePermissions() 0 3 1
A isBrowsable() 0 3 2
A assureFileMovePermissions() 0 17 5
A getAllFileObjectsInFolder() 0 16 4
A assureFileDeletePermissions() 0 17 5
A getFileAndFolderNameFilters() 0 3 1
A assureFolderMovePermissions() 0 21 6
A hasChildren() 0 3 1
A updateProcessedFile() 0 12 3
A getCapabilities() 0 3 1
B isWithinFileMountBoundaries() 0 35 9
A getFolderIdentifiersInFolder() 0 4 2
A getFileInfo() 0 3 1
A addUploadedFile() 0 20 5
D checkFileActionPermission() 0 55 21
A hasFileInFolder() 0 4 1
A assureFileAddPermissions() 0 13 4
B moveFile() 0 56 10
A getFileForLocalProcessing() 0 4 1
A isPublic() 0 3 1
A autoExtractMetadataEnabled() 0 3 1
A assureFileRenamePermissions() 0 16 5
A getFileIdentifiersInFolder() 0 4 2
A assureFileWritePermissions() 0 8 3
A moveFolderBetweenStorages() 0 3 1
A getFileByIdentifier() 0 10 3
A getFileContents() 0 4 1
A replaceFile() 0 17 3
A isWritable() 0 3 1
A markAsTemporaryOffline() 0 6 1
A checkUserActionPermission() 0 12 3
A sanitizeFileName() 0 11 2
B __construct() 0 42 8
A hasHierarchicalIdentifiers() 0 3 1
A assureFolderReadPermission() 0 12 3
A addFileAndFolderNameFilter() 0 3 1
B deleteFile() 0 38 6
A getProcessedFileRepository() 0 3 1
A searchFiles() 0 13 3
A assureFileReadPermission() 0 12 3
C checkFolderActionPermission() 0 42 14
A getFolderIdentifierFromFileIdentifier() 0 3 1
A assureFileUploadPermissions() 0 13 4
A streamFile() 0 16 2
B isOnline() 0 29 9
A getFile() 0 7 2
A hasCapability() 0 3 1
A getStorageRecord() 0 3 1
A getName() 0 3 1
A setDriver() 0 4 1
B copyFile() 0 36 7
A hasFile() 0 7 2
A getConfiguration() 0 3 1
A setFileAndFolderNameFilters() 0 4 1
A getFileInFolder() 0 4 1
A assureFolderCopyPermissions() 0 19 6
A getPseudoStream() 0 21 4
A unsetFileAndFolderNameFilters() 0 3 1
A getUid() 0 3 1
B addFile() 0 37 11
B renameFile() 0 40 8
A hashFileIdentifier() 0 7 3
A createFile() 0 11 1
B getProcessingFolders() 0 23 7
A hashFileByIdentifier() 0 7 3
A setUserPermissions() 0 3 1
A getFilesInFolder() 0 26 6
A checkFileExtensionPermission() 0 4 1
A getFileMounts() 0 3 1
A assureFolderDeletePermission() 0 18 5
A markAsPermanentlyOffline() 0 14 2
A resetFileAndFolderNameFiltersToDefault() 0 3 1
A countFilesInFolder() 0 5 2
A assureFileReplacePermissions() 0 9 3
A getDriver() 0 3 1
B addFileMount() 0 33 7
A getFileInfoByIdentifier() 0 3 1
B getProcessingFolder() 0 66 11
A getBackendUser() 0 3 1
A copyFolderBetweenStorages() 0 3 1
A getResourceFactoryInstance() 0 3 1
A deleteFolder() 0 22 5
B getUniqueName() 0 33 9
A getFileFactory() 0 3 1
A getFolder() 0 27 5
A createFolder() 0 26 5
A isWithinProcessingFolder() 0 10 3
A getNestedProcessingFolder() 0 23 4
A getRootLevelFolder() 0 7 3
B getNearestRecyclerFolder() 0 31 8
A getFolderInfo() 0 3 1
A getFoldersInFolder() 0 18 5
A getFileProcessingService() 0 6 2
A getFolderInFolder() 0 4 1
A getDefaultFolder() 0 3 1
B getRole() 0 22 7
A createFolderObject() 0 3 1
A countFoldersInFolder() 0 5 2
A setDefault() 0 3 1
A hasFolder() 0 4 1
A getIndexer() 0 3 1
A isWithinFolder() 0 9 3
A getFileIndexRepository() 0 3 1
A hasFolderInFolder() 0 4 1
A getNamesForNestedProcessingFolder() 0 11 3
A renameFolder() 0 30 4
C copyFolder() 0 40 13
A isDefault() 0 3 1
A getDriverType() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ResourceStorage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ResourceStorage, and based on these observations, apply Extract Interface, too.

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

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

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

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

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

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

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

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

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

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

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

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