StorageRepository   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 484
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 67
eloc 198
c 0
b 0
f 0
dl 0
loc 484
rs 3.04

18 Methods

Rating   Name   Duplication   Size   Complexity  
B initializeLocalCache() 0 42 7
A getDefaultStorage() 0 9 3
A findByUid() 0 7 3
A __construct() 0 4 1
A findByCombinedIdentifier() 0 4 2
A fetchRecordDataByUid() 0 8 2
A flush() 0 5 1
A testCaseSensitivity() 0 22 5
A createLocalStorage() 0 42 3
A findAll() 0 13 3
A findByStorageType() 0 16 4
B getStorageObject() 0 47 8
A convertFlexFormDataToConfigurationArray() 0 6 2
A createStorageObject() 0 9 3
A createFromRecord() 0 3 1
A getDriverObject() 0 6 1
A initializeLocalStorageCache() 0 23 6
C findBestMatchingStorageByLocalPath() 0 40 12

How to fix   Complexity   

Complex Class

Complex classes like StorageRepository 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 StorageRepository, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Core\Resource;
19
20
use Psr\EventDispatcher\EventDispatcherInterface;
21
use Psr\Log\LoggerAwareInterface;
22
use Psr\Log\LoggerAwareTrait;
23
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
24
use TYPO3\CMS\Core\Core\Environment;
25
use TYPO3\CMS\Core\Database\ConnectionPool;
26
use TYPO3\CMS\Core\Resource\Driver\DriverInterface;
27
use TYPO3\CMS\Core\Resource\Driver\DriverRegistry;
28
use TYPO3\CMS\Core\Resource\Event\AfterResourceStorageInitializationEvent;
29
use TYPO3\CMS\Core\Resource\Event\BeforeResourceStorageInitializationEvent;
30
use TYPO3\CMS\Core\Service\FlexFormService;
31
use TYPO3\CMS\Core\Utility\GeneralUtility;
32
use TYPO3\CMS\Core\Utility\PathUtility;
33
34
/**
35
 * Repository for accessing the file storages
36
 */
37
class StorageRepository implements LoggerAwareInterface
38
{
39
    use LoggerAwareTrait;
40
41
    /**
42
     * @var array|null
43
     */
44
    protected $storageRowCache;
45
46
    /**
47
     * @var array<int, LocalPath>|null
48
     */
49
    protected $localDriverStorageCache;
50
51
    /**
52
     * @var string
53
     */
54
    protected $table = 'sys_file_storage';
55
56
    /**
57
     * @var DriverRegistry
58
     */
59
    protected $driverRegistry;
60
61
    /**
62
     * @var EventDispatcherInterface
63
     */
64
    protected $eventDispatcher;
65
66
    /**
67
     * @var ResourceStorage[]|null
68
     */
69
    protected $storageInstances;
70
71
    public function __construct(EventDispatcherInterface $eventDispatcher, DriverRegistry $driverRegistry)
72
    {
73
        $this->eventDispatcher = $eventDispatcher;
74
        $this->driverRegistry = $driverRegistry;
75
    }
76
77
    /**
78
     * Returns the Default Storage
79
     *
80
     * The Default Storage is considered to be the replacement for the fileadmin/ construct.
81
     * It is automatically created with the setting fileadminDir from install tool.
82
     * getDefaultStorage->getDefaultFolder() will get you fileadmin/user_upload/ in a standard
83
     * TYPO3 installation.
84
     *
85
     * @return ResourceStorage|null
86
     */
87
    public function getDefaultStorage(): ?ResourceStorage
88
    {
89
        $allStorages = $this->findAll();
90
        foreach ($allStorages as $storage) {
91
            if ($storage->isDefault()) {
92
                return $storage;
93
            }
94
        }
95
        return null;
96
    }
97
98
    public function findByUid(int $uid): ?ResourceStorage
99
    {
100
        $this->initializeLocalCache();
101
        if (isset($this->storageRowCache[$uid]) || $uid === 0) {
102
            return $this->getStorageObject($uid, $this->storageRowCache[$uid] ?? []);
103
        }
104
        return null;
105
    }
106
107
    /**
108
     * Gets a storage object from a combined identifier
109
     *
110
     * @param string $identifier An identifier of the form [storage uid]:[object identifier]
111
     * @return ResourceStorage|null
112
     */
113
    public function findByCombinedIdentifier(string $identifier): ?ResourceStorage
114
    {
115
        $parts = GeneralUtility::trimExplode(':', $identifier);
116
        return count($parts) === 2 ? $this->findByUid((int)$parts[0]) : null;
117
    }
118
119
    /**
120
     * @param int $uid
121
     * @return array
122
     */
123
    protected function fetchRecordDataByUid(int $uid): array
124
    {
125
        $this->initializeLocalCache();
126
        if (!isset($this->storageRowCache[$uid])) {
127
            throw new \InvalidArgumentException(sprintf('No storage found with uid "%d".', $uid), 1599235454);
128
        }
129
130
        return $this->storageRowCache[$uid];
131
    }
132
133
    /**
134
     * Initializes the Storage
135
     */
136
    protected function initializeLocalCache()
137
    {
138
        if ($this->storageRowCache === null) {
139
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
140
                ->getQueryBuilderForTable($this->table);
141
142
            $result = $queryBuilder
143
                ->select('*')
144
                ->from($this->table)
145
                ->orderBy('name')
146
                ->execute();
147
148
            $this->storageRowCache = [];
149
            while ($row = $result->fetch()) {
150
                if (!empty($row['uid'])) {
151
                    $this->storageRowCache[$row['uid']] = $row;
152
                }
153
            }
154
155
            // if no storage is created before or the user has not access to a storage
156
            // $this->storageRowCache would have the value array()
157
            // so check if there is any record. If no record is found, create the fileadmin/ storage
158
            // selecting just one row is enough
159
160
            if ($this->storageRowCache === []) {
161
                $connection = GeneralUtility::makeInstance(ConnectionPool::class)
162
                    ->getConnectionForTable($this->table);
163
164
                $storageObjectsCount = $connection->count('uid', $this->table, []);
165
166
                if ($storageObjectsCount === 0) {
167
                    if ($this->createLocalStorage(
168
                        rtrim($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'] ?? 'fileadmin', '/'),
169
                        $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'],
170
                        'relative',
171
                        'This is the local fileadmin/ directory. This storage mount has been created automatically by TYPO3.',
172
                        true
173
                    ) > 0) {
174
                        // clear Cache to force reloading of storages
175
                        $this->flush();
176
                        // call self for initialize Cache
177
                        $this->initializeLocalCache();
178
                    }
179
                }
180
            }
181
        }
182
    }
183
184
    /**
185
     * Flush the internal storage caches to force reloading of storages with the next fetch.
186
     *
187
     * @internal
188
     */
189
    public function flush(): void
190
    {
191
        $this->storageRowCache = null;
192
        $this->storageInstances = null;
193
        $this->localDriverStorageCache = null;
194
    }
195
196
    /**
197
     * Finds storages by type, i.e. the driver used
198
     *
199
     * @param string $storageType
200
     * @return ResourceStorage[]
201
     */
202
    public function findByStorageType($storageType)
203
    {
204
        $this->initializeLocalCache();
205
206
        $storageObjects = [];
207
        foreach ($this->storageRowCache as $storageRow) {
208
            if ($storageRow['driver'] !== $storageType) {
209
                continue;
210
            }
211
            if ($this->driverRegistry->driverExists($storageRow['driver'])) {
212
                $storageObjects[] = $this->getStorageObject($storageRow['uid'], $storageRow);
213
            } else {
214
                $this->logger->warning('Could not instantiate storage "{name}" because of missing driver.', ['name' => $storageRow['name']]);
0 ignored issues
show
Bug introduced by
The method warning() does not exist on null. ( Ignorable by Annotation )

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

214
                $this->logger->/** @scrutinizer ignore-call */ 
215
                               warning('Could not instantiate storage "{name}" because of missing driver.', ['name' => $storageRow['name']]);

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...
215
            }
216
        }
217
        return $storageObjects;
218
    }
219
220
    /**
221
     * Returns a list of mountpoints that are available in the VFS.
222
     * In case no storage exists this automatically created a storage for fileadmin/
223
     *
224
     * @return ResourceStorage[]
225
     */
226
    public function findAll()
227
    {
228
        $this->initializeLocalCache();
229
230
        $storageObjects = [];
231
        foreach ($this->storageRowCache as $storageRow) {
232
            if ($this->driverRegistry->driverExists($storageRow['driver'])) {
233
                $storageObjects[] = $this->getStorageObject($storageRow['uid'], $storageRow);
234
            } else {
235
                $this->logger->warning('Could not instantiate storage "{name}" because of missing driver.', ['name' => $storageRow['name']]);
236
            }
237
        }
238
        return $storageObjects;
239
    }
240
241
    /**
242
     * Create the initial local storage base e.g. for the fileadmin/ directory.
243
     *
244
     * @param string $name
245
     * @param string $basePath
246
     * @param string $pathType
247
     * @param string $description
248
     * @param bool $default set to default storage
249
     * @return int uid of the inserted record
250
     */
251
    public function createLocalStorage($name, $basePath, $pathType, $description = '', $default = false)
252
    {
253
        $caseSensitive = $this->testCaseSensitivity($pathType === 'relative' ? Environment::getPublicPath() . '/' . $basePath : $basePath);
254
        // create the FlexForm for the driver configuration
255
        $flexFormData = [
256
            'data' => [
257
                'sDEF' => [
258
                    'lDEF' => [
259
                        'basePath' => ['vDEF' => rtrim($basePath, '/') . '/'],
260
                        'pathType' => ['vDEF' => $pathType],
261
                        'caseSensitive' => ['vDEF' => $caseSensitive]
262
                    ]
263
                ]
264
            ]
265
        ];
266
267
        $flexFormXml = GeneralUtility::makeInstance(FlexFormTools::class)->flexArray2Xml($flexFormData, true);
268
269
        // create the record
270
        $field_values = [
271
            'pid' => 0,
272
            'tstamp' => $GLOBALS['EXEC_TIME'],
273
            'crdate' => $GLOBALS['EXEC_TIME'],
274
            'name' => $name,
275
            'description' => $description,
276
            'driver' => 'Local',
277
            'configuration' => $flexFormXml,
278
            'is_online' => 1,
279
            'is_browsable' => 1,
280
            'is_public' => 1,
281
            'is_writable' => 1,
282
            'is_default' => $default ? 1 : 0
283
        ];
284
285
        $dbConnection = GeneralUtility::makeInstance(ConnectionPool::class)
286
            ->getConnectionForTable($this->table);
287
        $dbConnection->insert($this->table, $field_values);
288
289
        // Flush local resourceStorage cache so the storage can be accessed during the same request right away
290
        $this->flush();
291
292
        return (int)$dbConnection->lastInsertId($this->table);
293
    }
294
295
    /**
296
     * Test if the local filesystem is case sensitive
297
     *
298
     * @param string $absolutePath
299
     * @return bool
300
     */
301
    protected function testCaseSensitivity($absolutePath)
302
    {
303
        $caseSensitive = true;
304
        $path = rtrim($absolutePath, '/') . '/aAbB';
305
        $testFileExists = @file_exists($path);
306
307
        // create test file
308
        if (!$testFileExists) {
309
            touch($path);
310
        }
311
312
        // do the actual sensitivity check
313
        if (@file_exists(strtoupper($path)) && @file_exists(strtolower($path))) {
314
            $caseSensitive = false;
315
        }
316
317
        // clean filesystem
318
        if (!$testFileExists) {
319
            unlink($path);
320
        }
321
322
        return $caseSensitive;
323
    }
324
325
    /**
326
     * Creates an instance of the storage from given UID. The $recordData can
327
     * be supplied to increase performance.
328
     *
329
     * @param int $uid The uid of the storage to instantiate.
330
     * @param array $recordData The record row from database.
331
     * @param mixed|null $fileIdentifier Identifier for a file. Used for auto-detection of a storage, but only if $uid === 0 (Local default storage) is used
332
     * @throws \InvalidArgumentException
333
     * @return ResourceStorage
334
     */
335
    public function getStorageObject($uid, array $recordData = [], &$fileIdentifier = null): ResourceStorage
336
    {
337
        if (!is_numeric($uid)) {
0 ignored issues
show
introduced by
The condition is_numeric($uid) is always true.
Loading history...
338
            throw new \InvalidArgumentException('The UID of storage has to be numeric. UID given: "' . $uid . '"', 1314085991);
339
        }
340
        $uid = (int)$uid;
341
        if ($uid === 0 && $fileIdentifier !== null) {
342
            $uid = $this->findBestMatchingStorageByLocalPath($fileIdentifier);
343
        }
344
        if (empty($this->storageInstances[$uid])) {
345
            $storageConfiguration = null;
346
            /** @var BeforeResourceStorageInitializationEvent $event */
347
            $event = $this->eventDispatcher->dispatch(new BeforeResourceStorageInitializationEvent($uid, $recordData, $fileIdentifier));
348
            $recordData = $event->getRecord();
349
            $uid = $event->getStorageUid();
350
            $fileIdentifier = $event->getFileIdentifier();
351
            // If the built-in storage with UID=0 is requested:
352
            if ($uid === 0) {
353
                $recordData = [
354
                    'uid' => 0,
355
                    'pid' => 0,
356
                    'name' => 'Fallback Storage',
357
                    'description' => 'Internal storage, mounting the main TYPO3_site directory.',
358
                    'driver' => 'Local',
359
                    'processingfolder' => 'typo3temp/assets/_processed_/',
360
                    // legacy code
361
                    'configuration' => '',
362
                    'is_online' => true,
363
                    'is_browsable' => true,
364
                    'is_public' => true,
365
                    'is_writable' => true,
366
                    'is_default' => false,
367
                ];
368
                $storageConfiguration = [
369
                    'basePath' => '/',
370
                    'pathType' => 'relative'
371
                ];
372
            } elseif ($recordData === [] || (int)$recordData['uid'] !== $uid) {
373
                $recordData = $this->fetchRecordDataByUid($uid);
374
            }
375
            $storageObject = $this->createStorageObject($recordData, $storageConfiguration);
376
            $storageObject = $this->eventDispatcher
377
                ->dispatch(new AfterResourceStorageInitializationEvent($storageObject))
378
                ->getStorage();
379
            $this->storageInstances[$uid] = $storageObject;
380
        }
381
        return $this->storageInstances[$uid];
382
    }
383
384
    /**
385
     * Checks whether a file resides within a real storage in local file system.
386
     * If no match is found, uid 0 is returned which is a fallback storage pointing to fileadmin in public web path.
387
     *
388
     * The file identifier is adapted accordingly to match the new storage's base path.
389
     *
390
     * @param string $localPath
391
     * @return int
392
     */
393
    protected function findBestMatchingStorageByLocalPath(&$localPath): int
394
    {
395
        if ($this->localDriverStorageCache === null) {
396
            $this->initializeLocalStorageCache();
397
        }
398
        // normalize path information (`//`, `../`)
399
        $localPath = PathUtility::getCanonicalPath($localPath);
400
        if ($localPath[0] !== '/') {
401
            $localPath = '/' . $localPath;
402
        }
403
        $bestMatchStorageUid = 0;
404
        $bestMatchLength = 0;
405
        foreach ($this->localDriverStorageCache as $storageUid => $basePath) {
406
            // try to match (resolved) relative base-path
407
            if ($basePath->getRelative() !== null
408
                && null !== $commonPrefix = PathUtility::getCommonPrefix([$basePath->getRelative(), $localPath])
409
            ) {
410
                $matchLength = strlen($commonPrefix);
411
                $basePathLength = strlen($basePath->getRelative());
412
                if ($matchLength >= $basePathLength && $matchLength > $bestMatchLength) {
413
                    $bestMatchStorageUid = $storageUid;
414
                    $bestMatchLength = $matchLength;
415
                }
416
            }
417
            // try to match (resolved) absolute base-path
418
            if (null !== $commonPrefix = PathUtility::getCommonPrefix([$basePath->getAbsolute(), $localPath])) {
419
                $matchLength = strlen($commonPrefix);
420
                $basePathLength = strlen($basePath->getAbsolute());
421
                if ($matchLength >= $basePathLength && $matchLength > $bestMatchLength) {
422
                    $bestMatchStorageUid = $storageUid;
423
                    $bestMatchLength = $matchLength;
424
                }
425
            }
426
        }
427
        if ($bestMatchLength > 0) {
428
            // $commonPrefix always has trailing slash, which needs to be excluded
429
            // (commonPrefix: /some/path/, localPath: /some/path/file.png --> /file.png; keep leading slash)
430
            $localPath = substr($localPath, $bestMatchLength - 1);
431
        }
432
        return $bestMatchStorageUid;
433
    }
434
435
    /**
436
     * Creates an array mapping all uids to the basePath of storages using the "local" driver.
437
     */
438
    protected function initializeLocalStorageCache(): void
439
    {
440
        $this->localDriverStorageCache = [
441
            // implicit legacy storage in project's public path
442
            0 => new LocalPath('/', LocalPath::TYPE_RELATIVE)
443
        ];
444
        $storageObjects = $this->findByStorageType('Local');
445
        foreach ($storageObjects as $localStorage) {
446
            $configuration = $localStorage->getConfiguration();
447
            if (!isset($configuration['basePath']) || !isset($configuration['pathType'])) {
448
                continue;
449
            }
450
            if ($configuration['pathType'] === 'relative') {
451
                $pathType = LocalPath::TYPE_RELATIVE;
452
            } elseif ($configuration['pathType'] === 'absolute') {
453
                $pathType = LocalPath::TYPE_ABSOLUTE;
454
            } else {
455
                continue;
456
            }
457
            $this->localDriverStorageCache[$localStorage->getUid()] = GeneralUtility::makeInstance(
458
                LocalPath::class,
459
                $configuration['basePath'],
460
                $pathType
461
            );
462
        }
463
    }
464
465
    /**
466
     * Creates a storage object from a storage database row.
467
     *
468
     * @param array $storageRecord
469
     * @param array|null $storageConfiguration Storage configuration (if given, this won't be extracted from the FlexForm value but the supplied array used instead)
470
     * @return ResourceStorage
471
     * @internal this method is only public for having access to ResourceFactory->createStorageObject(). In TYPO3 v12 this method can be changed to protected again.
472
     */
473
    public function createStorageObject(array $storageRecord, array $storageConfiguration = null): ResourceStorage
474
    {
475
        if (!$storageConfiguration && !empty($storageRecord['configuration'])) {
476
            $storageConfiguration = $this->convertFlexFormDataToConfigurationArray($storageRecord['configuration']);
477
        }
478
        $driverType = $storageRecord['driver'];
479
        $driverObject = $this->getDriverObject($driverType, (array)$storageConfiguration);
480
        $storageRecord['configuration'] = $storageConfiguration;
481
        return GeneralUtility::makeInstance(ResourceStorage::class, $driverObject, $storageRecord, $this->eventDispatcher);
482
    }
483
484
    /**
485
     * Converts a flexform data string to a flat array with key value pairs
486
     *
487
     * @param string $flexFormData
488
     * @return array Array with key => value pairs of the field data in the FlexForm
489
     */
490
    protected function convertFlexFormDataToConfigurationArray(string $flexFormData): array
491
    {
492
        if ($flexFormData) {
493
            return GeneralUtility::makeInstance(FlexFormService::class)->convertFlexFormContentToArray($flexFormData);
494
        }
495
        return [];
496
    }
497
498
    /**
499
     * Creates a driver object for a specified storage object.
500
     *
501
     * @param string $driverIdentificationString The driver class (or identifier) to use.
502
     * @param array $driverConfiguration The configuration of the storage
503
     * @return DriverInterface
504
     */
505
    protected function getDriverObject(string $driverIdentificationString, array $driverConfiguration): DriverInterface
506
    {
507
        $driverClass = $this->driverRegistry->getDriverClass($driverIdentificationString);
508
        /** @var DriverInterface $driverObject */
509
        $driverObject = GeneralUtility::makeInstance($driverClass, $driverConfiguration);
510
        return $driverObject;
511
    }
512
513
    /**
514
     * @param array $storageRecord
515
     * @return ResourceStorage
516
     * @internal
517
     */
518
    public function createFromRecord(array $storageRecord): ResourceStorage
519
    {
520
        return $this->createStorageObject($storageRecord);
521
    }
522
}
523