Passed
Push — master ( 57b5b6...1bd009 )
by
unknown
47:04 queued 32:28
created

FormPersistenceManager   F

Complexity

Total Complexity 142

Size/Duplication

Total Lines 889
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 142
eloc 361
dl 0
loc 889
rs 2
c 0
b 0
f 0

33 Methods

Rating   Name   Duplication   Size   Complexity  
A isFileWithinAccessibleFormStorageFolders() 0 6 2
A isAccessibleFormStorageFolder() 0 4 1
A looksLikeAFormDefinition() 0 3 3
A hasValidFileExtension() 0 3 1
A injectResourceFactory() 0 3 1
A pathIsIntendedAsFileMountPath() 0 12 4
A generateErrorsIfFormDefinitionIsValidButHasInvalidFileExtension() 0 7 3
A pathIsIntendedAsExtensionPath() 0 3 1
A retrieveYamlFilesFromExtensionFolders() 0 14 4
A isFileWithinAccessibleExtensionFolders() 0 6 2
C getAccessibleFormStorageFolders() 0 60 12
A retrieveYamlFilesFromStorageFolders() 0 25 2
A load() 0 25 3
A injectFilePersistenceSlot() 0 3 1
A initializeObject() 0 6 1
A loadMetaData() 0 29 4
B getAccessibleExtensionFolders() 0 33 8
B retrieveFileByPersistenceIdentifier() 0 30 7
B delete() 0 26 7
A getUniquePersistenceIdentifier() 0 21 5
A injectStorageRepository() 0 3 1
A checkForDuplicateIdentifier() 0 10 3
A injectYamlSource() 0 3 1
A exists() 0 15 4
A isAccessibleExtensionFolder() 0 4 1
A getUniqueIdentifier() 0 19 5
C listForms() 0 61 13
B extractMetaDataFromCouldBeFormDefinition() 0 23 7
A sortForms() 0 17 6
A getOrCreateFile() 0 30 5
C isAllowedPersistencePath() 0 37 15
A save() 0 27 6
A getStorageByUid() 0 10 3

How to fix   Complexity   

Complex Class

Complex classes like FormPersistenceManager 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 FormPersistenceManager, 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
/*
19
 * Inspired by and partially taken from the Neos.Form package (www.neos.io)
20
 */
21
22
namespace TYPO3\CMS\Form\Mvc\Persistence;
23
24
use TYPO3\CMS\Core\Cache\CacheManager;
25
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
26
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
27
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
28
use TYPO3\CMS\Core\Resource\File;
29
use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
30
use TYPO3\CMS\Core\Resource\Folder;
31
use TYPO3\CMS\Core\Resource\ResourceFactory;
32
use TYPO3\CMS\Core\Resource\ResourceStorage;
33
use TYPO3\CMS\Core\Resource\StorageRepository;
34
use TYPO3\CMS\Core\Utility\GeneralUtility;
35
use TYPO3\CMS\Core\Utility\MathUtility;
36
use TYPO3\CMS\Core\Utility\PathUtility;
37
use TYPO3\CMS\Core\Utility\StringUtility;
38
use TYPO3\CMS\Extbase\Object\ObjectManager;
39
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface;
40
use TYPO3\CMS\Form\Mvc\Configuration\Exception\FileWriteException;
41
use TYPO3\CMS\Form\Mvc\Configuration\Exception\NoSuchFileException;
42
use TYPO3\CMS\Form\Mvc\Configuration\YamlSource;
43
use TYPO3\CMS\Form\Mvc\Persistence\Exception\NoUniqueIdentifierException;
44
use TYPO3\CMS\Form\Mvc\Persistence\Exception\NoUniquePersistenceIdentifierException;
45
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
46
use TYPO3\CMS\Form\Slot\FilePersistenceSlot;
47
48
/**
49
 * Concrete implementation of the FormPersistenceManagerInterface
50
 *
51
 * Scope: frontend / backend
52
 */
53
class FormPersistenceManager implements FormPersistenceManagerInterface
54
{
55
    const FORM_DEFINITION_FILE_EXTENSION = '.form.yaml';
56
57
    /**
58
     * @var \TYPO3\CMS\Form\Mvc\Configuration\YamlSource
59
     */
60
    protected $yamlSource;
61
62
    /**
63
     * @var \TYPO3\CMS\Core\Resource\StorageRepository
64
     */
65
    protected $storageRepository;
66
67
    /**
68
     * @var array
69
     */
70
    protected $formSettings;
71
72
    /**
73
     * @var FilePersistenceSlot
74
     */
75
    protected $filePersistenceSlot;
76
77
    /**
78
     * @var FrontendInterface
79
     */
80
    protected $runtimeCache;
81
82
    /**
83
     * @var ResourceFactory
84
     */
85
    protected $resourceFactory;
86
87
    /**
88
     * @param \TYPO3\CMS\Form\Mvc\Configuration\YamlSource $yamlSource
89
     * @internal
90
     */
91
    public function injectYamlSource(YamlSource $yamlSource)
92
    {
93
        $this->yamlSource = $yamlSource;
94
    }
95
96
    /**
97
     * @param \TYPO3\CMS\Core\Resource\StorageRepository $storageRepository
98
     * @internal
99
     */
100
    public function injectStorageRepository(StorageRepository $storageRepository)
101
    {
102
        $this->storageRepository = $storageRepository;
103
    }
104
105
    /**
106
     * @param \TYPO3\CMS\Form\Slot\FilePersistenceSlot $filePersistenceSlot
107
     */
108
    public function injectFilePersistenceSlot(FilePersistenceSlot $filePersistenceSlot)
109
    {
110
        $this->filePersistenceSlot = $filePersistenceSlot;
111
    }
112
113
    /**
114
     * @param \TYPO3\CMS\Core\Resource\ResourceFactory $resourceFactory
115
     */
116
    public function injectResourceFactory(ResourceFactory $resourceFactory)
117
    {
118
        $this->resourceFactory = $resourceFactory;
119
    }
120
121
    /**
122
     * @internal
123
     */
124
    public function initializeObject()
125
    {
126
        $this->formSettings = GeneralUtility::makeInstance(ObjectManager::class)
127
            ->get(ConfigurationManagerInterface::class)
128
            ->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_YAML_SETTINGS, 'form');
129
        $this->runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
130
    }
131
132
    /**
133
     * Load the array formDefinition identified by $persistenceIdentifier, and return it.
134
     * Only files with the extension .yaml or .form.yaml are loaded.
135
     *
136
     * @param string $persistenceIdentifier
137
     * @return array
138
     * @internal
139
     */
140
    public function load(string $persistenceIdentifier): array
141
    {
142
        $cacheKey = 'formLoad' . md5($persistenceIdentifier);
143
144
        $yaml = $this->runtimeCache->get($cacheKey);
145
        if ($yaml !== false) {
146
            return $yaml;
147
        }
148
149
        $file = $this->retrieveFileByPersistenceIdentifier($persistenceIdentifier);
150
151
        try {
152
            $yaml = $this->yamlSource->load([$file]);
153
            $this->generateErrorsIfFormDefinitionIsValidButHasInvalidFileExtension($yaml, $persistenceIdentifier);
154
        } catch (\Exception $e) {
155
            $yaml = [
156
                'type' => 'Form',
157
                'identifier' => $persistenceIdentifier,
158
                'label' => $e->getMessage(),
159
                'invalid' => true,
160
            ];
161
        }
162
        $this->runtimeCache->set($cacheKey, $yaml);
163
164
        return $yaml;
165
    }
166
167
    /**
168
     * Save the array form representation identified by $persistenceIdentifier.
169
     * Only files with the extension .form.yaml are saved.
170
     * If the formDefinition is located within an EXT: resource, save is only
171
     * allowed if the configuration path
172
     * TYPO3.CMS.Form.persistenceManager.allowSaveToExtensionPaths
173
     * is set to true.
174
     *
175
     * @param string $persistenceIdentifier
176
     * @param array $formDefinition
177
     * @throws PersistenceManagerException
178
     * @internal
179
     */
180
    public function save(string $persistenceIdentifier, array $formDefinition)
181
    {
182
        if (!$this->hasValidFileExtension($persistenceIdentifier)) {
183
            throw new PersistenceManagerException(sprintf('The file "%s" could not be saved.', $persistenceIdentifier), 1477679820);
184
        }
185
186
        if ($this->pathIsIntendedAsExtensionPath($persistenceIdentifier)) {
187
            if (!$this->formSettings['persistenceManager']['allowSaveToExtensionPaths']) {
188
                throw new PersistenceManagerException('Save to extension paths is not allowed.', 1477680881);
189
            }
190
            if (!$this->isFileWithinAccessibleExtensionFolders($persistenceIdentifier)) {
191
                $message = sprintf('The file "%s" could not be saved. Please check your configuration option "persistenceManager.allowedExtensionPaths"', $persistenceIdentifier);
192
                throw new PersistenceManagerException($message, 1484073571);
193
            }
194
            $fileToSave = GeneralUtility::getFileAbsFileName($persistenceIdentifier);
195
        } else {
196
            $fileToSave = $this->getOrCreateFile($persistenceIdentifier);
197
        }
198
199
        try {
200
            $this->yamlSource->save($fileToSave, $formDefinition);
201
        } catch (FileWriteException $e) {
202
            throw new PersistenceManagerException(sprintf(
203
                'The file "%s" could not be saved: %s',
204
                $persistenceIdentifier,
205
                $e->getMessage()
206
            ), 1512582637, $e);
207
        }
208
    }
209
210
    /**
211
     * Delete the form representation identified by $persistenceIdentifier.
212
     * Only files with the extension .form.yaml are removed.
213
     *
214
     * @param string $persistenceIdentifier
215
     * @throws PersistenceManagerException
216
     * @internal
217
     */
218
    public function delete(string $persistenceIdentifier)
219
    {
220
        if (!$this->hasValidFileExtension($persistenceIdentifier)) {
221
            throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1472239534);
222
        }
223
        if (!$this->exists($persistenceIdentifier)) {
224
            throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1472239535);
225
        }
226
        if ($this->pathIsIntendedAsExtensionPath($persistenceIdentifier)) {
227
            if (!$this->formSettings['persistenceManager']['allowDeleteFromExtensionPaths']) {
228
                throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1472239536);
229
            }
230
            if (!$this->isFileWithinAccessibleExtensionFolders($persistenceIdentifier)) {
231
                $message = sprintf('The file "%s" could not be removed. Please check your configuration option "persistenceManager.allowedExtensionPaths"', $persistenceIdentifier);
232
                throw new PersistenceManagerException($message, 1484073878);
233
            }
234
            $fileToDelete = GeneralUtility::getFileAbsFileName($persistenceIdentifier);
235
            unlink($fileToDelete);
236
        } else {
237
            [$storageUid, $fileIdentifier] = explode(':', $persistenceIdentifier, 2);
238
            $storage = $this->getStorageByUid((int)$storageUid);
239
            $file = $storage->getFile($fileIdentifier);
240
            if (!$storage->checkFileActionPermission('delete', $file)) {
241
                throw new PersistenceManagerException(sprintf('No delete access to file "%s".', $persistenceIdentifier), 1472239516);
242
            }
243
            $storage->deleteFile($file);
244
        }
245
    }
246
247
    /**
248
     * Check whether a form with the specified $persistenceIdentifier exists
249
     *
250
     * @param string $persistenceIdentifier
251
     * @return bool TRUE if a form with the given $persistenceIdentifier can be loaded, otherwise FALSE
252
     * @internal
253
     */
254
    public function exists(string $persistenceIdentifier): bool
255
    {
256
        $exists = false;
257
        if ($this->hasValidFileExtension($persistenceIdentifier)) {
258
            if ($this->pathIsIntendedAsExtensionPath($persistenceIdentifier)) {
259
                if ($this->isFileWithinAccessibleExtensionFolders($persistenceIdentifier)) {
260
                    $exists = file_exists(GeneralUtility::getFileAbsFileName($persistenceIdentifier));
261
                }
262
            } else {
263
                [$storageUid, $fileIdentifier] = explode(':', $persistenceIdentifier, 2);
264
                $storage = $this->getStorageByUid((int)$storageUid);
265
                $exists = $storage->hasFile($fileIdentifier);
266
            }
267
        }
268
        return $exists;
269
    }
270
271
    /**
272
     * List all form definitions which can be loaded through this form persistence
273
     * manager.
274
     *
275
     * Returns an associative array with each item containing the keys 'name' (the human-readable name of the form)
276
     * and 'persistenceIdentifier' (the unique identifier for the Form Persistence Manager e.g. the path to the saved form definition).
277
     *
278
     * @return array in the format [['name' => 'Form 01', 'persistenceIdentifier' => 'path1'], [ .... ]]
279
     * @internal
280
     */
281
    public function listForms(): array
282
    {
283
        $identifiers = [];
284
        $forms = [];
285
286
        foreach ($this->retrieveYamlFilesFromStorageFolders() as $file) {
287
            $form = $this->loadMetaData($file);
288
289
            if (!$this->looksLikeAFormDefinition($form)) {
290
                continue;
291
            }
292
293
            $persistenceIdentifier = $file->getCombinedIdentifier();
294
            if ($this->hasValidFileExtension($persistenceIdentifier)) {
295
                $forms[] = [
296
                    'identifier' => $form['identifier'],
297
                    'name' => $form['label'] ?? $form['identifier'],
298
                    'persistenceIdentifier' => $persistenceIdentifier,
299
                    'readOnly' => false,
300
                    'removable' => true,
301
                    'location' => 'storage',
302
                    'duplicateIdentifier' => false,
303
                    'invalid' => $form['invalid'],
304
                    'fileUid' => $form['fileUid'],
305
                ];
306
                $identifiers[$form['identifier']]++;
307
            }
308
        }
309
310
        foreach ($this->retrieveYamlFilesFromExtensionFolders() as $fullPath => $fileName) {
311
            $form = $this->loadMetaData($fullPath);
312
313
            if ($this->looksLikeAFormDefinition($form)) {
314
                if ($this->hasValidFileExtension($fileName)) {
315
                    $forms[] = [
316
                        'identifier' => $form['identifier'],
317
                        'name' => $form['label'] ?? $form['identifier'],
318
                        'persistenceIdentifier' => $fullPath,
319
                        'readOnly' => $this->formSettings['persistenceManager']['allowSaveToExtensionPaths'] ? false: true,
320
                        'removable' => $this->formSettings['persistenceManager']['allowDeleteFromExtensionPaths'] ? true: false,
321
                        'location' => 'extension',
322
                        'duplicateIdentifier' => false,
323
                        'invalid' => $form['invalid'],
324
                        'fileUid' => $form['fileUid'],
325
                    ];
326
                    $identifiers[$form['identifier']]++;
327
                }
328
            }
329
        }
330
331
        foreach ($identifiers as $identifier => $count) {
332
            if ($count > 1) {
333
                foreach ($forms as &$formDefinition) {
334
                    if ($formDefinition['identifier'] === $identifier) {
335
                        $formDefinition['duplicateIdentifier'] = true;
336
                    }
337
                }
338
            }
339
        }
340
341
        return $this->sortForms($forms);
342
    }
343
344
    /**
345
     * Retrieves yaml files from storage folders for further processing.
346
     * At this time it's not determined yet, whether these files contain form data.
347
     *
348
     * @return File[]
349
     * @internal
350
     */
351
    public function retrieveYamlFilesFromStorageFolders(): array
352
    {
353
        $filesFromStorageFolders = [];
354
355
        $fileExtensionFilter = GeneralUtility::makeInstance(FileExtensionFilter::class);
356
        $fileExtensionFilter->setAllowedFileExtensions(['yaml']);
357
358
        foreach ($this->getAccessibleFormStorageFolders() as $folder) {
359
            $storage = $folder->getStorage();
360
            $storage->addFileAndFolderNameFilter([
361
                $fileExtensionFilter,
362
                'filterFileList'
363
            ]);
364
365
            $files = $folder->getFiles(
366
                0,
367
                0,
368
                Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS,
369
                true
370
            );
371
            $filesFromStorageFolders = array_merge($filesFromStorageFolders, array_values($files));
372
            $storage->resetFileAndFolderNameFiltersToDefault();
373
        }
374
375
        return $filesFromStorageFolders;
376
    }
377
378
    /**
379
     * Retrieves yaml files from extension folders for further processing.
380
     * At this time it's not determined yet, whether these files contain form data.
381
     *
382
     * @return array<string, string>
383
     * @internal
384
     */
385
    public function retrieveYamlFilesFromExtensionFolders(): array
386
    {
387
        $filesFromExtensionFolders = [];
388
389
        foreach ($this->getAccessibleExtensionFolders() as $relativePath => $fullPath) {
390
            foreach (new \DirectoryIterator($fullPath) as $fileInfo) {
391
                if ($fileInfo->getExtension() !== 'yaml') {
392
                    continue;
393
                }
394
                $filesFromExtensionFolders[$relativePath . $fileInfo->getFilename()] = $fileInfo->getFilename();
395
            }
396
        }
397
398
        return $filesFromExtensionFolders;
399
    }
400
401
    /**
402
     * Return a list of all accessible file mountpoints for the
403
     * current backend user.
404
     *
405
     * Only registered mountpoints from
406
     * TYPO3.CMS.Form.persistenceManager.allowedFileMounts
407
     * are listed.
408
     *
409
     * @return Folder[]
410
     * @internal
411
     */
412
    public function getAccessibleFormStorageFolders(): array
413
    {
414
        $storageFolders = [];
415
416
        if (
417
            !isset($this->formSettings['persistenceManager']['allowedFileMounts'])
418
            || !is_array($this->formSettings['persistenceManager']['allowedFileMounts'])
419
            || empty($this->formSettings['persistenceManager']['allowedFileMounts'])
420
        ) {
421
            return $storageFolders;
422
        }
423
424
        foreach ($this->formSettings['persistenceManager']['allowedFileMounts'] as $allowedFileMount) {
425
            $allowedFileMount = rtrim($allowedFileMount, '/') . '/';
426
            // $fileMountPath is like "/form_definitions/" or "/group_homes/1/form_definitions/"
427
            [$storageUid, $fileMountPath] = explode(':', $allowedFileMount, 2);
428
429
            try {
430
                $storage = $this->getStorageByUid((int)$storageUid);
431
            } catch (PersistenceManagerException $e) {
432
                continue;
433
            }
434
435
            $isStorageFileMount = false;
436
            $parentFolder = $storage->getRootLevelFolder(false);
437
438
            foreach ($storage->getFileMounts() as $storageFileMount) {
439
                /** @var \TYPO3\CMS\Core\Resource\Folder */
440
                $storageFileMountFolder = $storageFileMount['folder'];
441
442
                // Normally should use ResourceStorage::isWithinFolder() to check if the configured file mount path is within a storage file mount but this requires a valid Folder object and thus a directory which already exists. And the folder could simply not exist yet.
443
                if (StringUtility::beginsWith($fileMountPath, $storageFileMountFolder->getIdentifier())) {
444
                    $isStorageFileMount = true;
445
                    $parentFolder = $storageFileMountFolder;
446
                }
447
            }
448
449
            // Get storage folder object, create it if missing
450
            try {
451
                $fileMountFolder = $storage->getFolder($fileMountPath);
452
            } catch (InsufficientFolderAccessPermissionsException $e) {
453
                continue;
454
            } catch (FolderDoesNotExistException $e) {
455
                if ($isStorageFileMount) {
456
                    $fileMountPath = substr(
457
                        $fileMountPath,
458
                        strlen($parentFolder->getIdentifier())
459
                    );
460
                }
461
462
                try {
463
                    $fileMountFolder = $storage->createFolder($fileMountPath, $parentFolder);
464
                } catch (InsufficientFolderAccessPermissionsException $e) {
465
                    continue;
466
                }
467
            }
468
469
            $storageFolders[$allowedFileMount] = $fileMountFolder;
470
        }
471
        return $storageFolders;
472
    }
473
474
    /**
475
     * Return a list of all accessible extension folders
476
     *
477
     * Only registered mountpoints from
478
     * TYPO3.CMS.Form.persistenceManager.allowedExtensionPaths
479
     * are listed.
480
     *
481
     * @return array
482
     * @internal
483
     */
484
    public function getAccessibleExtensionFolders(): array
485
    {
486
        $extensionFolders = $this->runtimeCache->get('formAccessibleExtensionFolders');
487
488
        if ($extensionFolders !== false) {
489
            return $extensionFolders;
490
        }
491
492
        $extensionFolders = [];
493
        if (
494
            !isset($this->formSettings['persistenceManager']['allowedExtensionPaths'])
495
            || !is_array($this->formSettings['persistenceManager']['allowedExtensionPaths'])
496
            || empty($this->formSettings['persistenceManager']['allowedExtensionPaths'])
497
        ) {
498
            $this->runtimeCache->set('formAccessibleExtensionFolders', $extensionFolders);
499
            return $extensionFolders;
500
        }
501
502
        foreach ($this->formSettings['persistenceManager']['allowedExtensionPaths'] as $allowedExtensionPath) {
503
            if (!$this->pathIsIntendedAsExtensionPath($allowedExtensionPath)) {
504
                continue;
505
            }
506
507
            $allowedExtensionFullPath = GeneralUtility::getFileAbsFileName($allowedExtensionPath);
508
            if (!file_exists($allowedExtensionFullPath)) {
509
                continue;
510
            }
511
            $allowedExtensionPath = rtrim($allowedExtensionPath, '/') . '/';
512
            $extensionFolders[$allowedExtensionPath] = $allowedExtensionFullPath;
513
        }
514
515
        $this->runtimeCache->set('formAccessibleExtensionFolders', $extensionFolders);
516
        return $extensionFolders;
517
    }
518
519
    /**
520
     * This takes a form identifier and returns a unique persistence identifier for it.
521
     * By default this is just similar to the identifier. But if a form with the same persistence identifier already
522
     * exists a suffix is appended until the persistence identifier is unique.
523
     *
524
     * @param string $formIdentifier lowerCamelCased form identifier
525
     * @param string $savePath
526
     * @return string unique form persistence identifier
527
     * @throws NoUniquePersistenceIdentifierException
528
     * @internal
529
     */
530
    public function getUniquePersistenceIdentifier(string $formIdentifier, string $savePath): string
531
    {
532
        $savePath = rtrim($savePath, '/') . '/';
533
        $formPersistenceIdentifier = $savePath . $formIdentifier . self::FORM_DEFINITION_FILE_EXTENSION;
534
        if (!$this->exists($formPersistenceIdentifier)) {
535
            return $formPersistenceIdentifier;
536
        }
537
        for ($attempts = 1; $attempts < 100; $attempts++) {
538
            $formPersistenceIdentifier = $savePath . sprintf('%s_%d', $formIdentifier, $attempts) . self::FORM_DEFINITION_FILE_EXTENSION;
539
            if (!$this->exists($formPersistenceIdentifier)) {
540
                return $formPersistenceIdentifier;
541
            }
542
        }
543
        $formPersistenceIdentifier = $savePath . sprintf('%s_%d', $formIdentifier, time()) . self::FORM_DEFINITION_FILE_EXTENSION;
544
        if (!$this->exists($formPersistenceIdentifier)) {
545
            return $formPersistenceIdentifier;
546
        }
547
548
        throw new NoUniquePersistenceIdentifierException(
549
            sprintf('Could not find a unique persistence identifier for form identifier "%s" after %d attempts', $formIdentifier, $attempts),
550
            1476010403
551
        );
552
    }
553
554
    /**
555
     * This takes a form identifier and returns a unique identifier for it.
556
     * If a formDefinition with the same identifier already exists a suffix is
557
     * appended until the identifier is unique.
558
     *
559
     * @param string $identifier
560
     * @return string unique form identifier
561
     * @throws NoUniqueIdentifierException
562
     * @internal
563
     */
564
    public function getUniqueIdentifier(string $identifier): string
565
    {
566
        $originalIdentifier = $identifier;
567
        if ($this->checkForDuplicateIdentifier($identifier)) {
568
            for ($attempts = 1; $attempts < 100; $attempts++) {
569
                $identifier = sprintf('%s_%d', $originalIdentifier, $attempts);
570
                if (!$this->checkForDuplicateIdentifier($identifier)) {
571
                    return $identifier;
572
                }
573
            }
574
            $identifier = $originalIdentifier . '_' . time();
575
            if ($this->checkForDuplicateIdentifier($identifier)) {
576
                throw new NoUniqueIdentifierException(
577
                    sprintf('Could not find a unique identifier for form identifier "%s" after %d attempts', $identifier, $attempts),
578
                    1477688567
579
                );
580
            }
581
        }
582
        return  $identifier;
583
    }
584
585
    /**
586
     * Check if an identifier is already used by a formDefinition.
587
     *
588
     * @param string $identifier
589
     * @return bool
590
     * @internal
591
     */
592
    public function checkForDuplicateIdentifier(string $identifier): bool
593
    {
594
        $identifierUsed = false;
595
        foreach ($this->listForms() as $formDefinition) {
596
            if ($formDefinition['identifier'] === $identifier) {
597
                $identifierUsed = true;
598
                break;
599
            }
600
        }
601
        return $identifierUsed;
602
    }
603
604
    /**
605
     * Check if a persistence path or if a persistence identifier path
606
     * is configured within the form setup
607
     * (TYPO3.CMS.Form.persistenceManager.allowedExtensionPaths / TYPO3.CMS.Form.persistenceManager.allowedFileMounts).
608
     * If the input is a persistence identifier an additional check for a
609
     * valid file extension will be performed.
610
     * .
611
     * @param string $persistencePath
612
     * @return bool
613
     * @internal
614
     */
615
    public function isAllowedPersistencePath(string $persistencePath): bool
616
    {
617
        $pathinfo = PathUtility::pathinfo($persistencePath);
618
        $persistencePathIsFile = isset($pathinfo['extension']);
619
620
        if (
621
            $persistencePathIsFile
622
            && $this->pathIsIntendedAsExtensionPath($persistencePath)
623
            && $this->hasValidFileExtension($persistencePath)
624
            && $this->isFileWithinAccessibleExtensionFolders($persistencePath)
625
        ) {
626
            return true;
627
        }
628
        if (
629
            $persistencePathIsFile
630
            && $this->pathIsIntendedAsFileMountPath($persistencePath)
631
            && $this->hasValidFileExtension($persistencePath)
632
            && $this->isFileWithinAccessibleFormStorageFolders($persistencePath)
633
        ) {
634
            return true;
635
        }
636
        if (
637
            !$persistencePathIsFile
638
            && $this->pathIsIntendedAsExtensionPath($persistencePath)
639
            && $this->isAccessibleExtensionFolder($persistencePath)
640
        ) {
641
            return true;
642
        }
643
        if (
644
            !$persistencePathIsFile
645
            && $this->pathIsIntendedAsFileMountPath($persistencePath)
646
            && $this->isAccessibleFormStorageFolder($persistencePath)
647
        ) {
648
            return true;
649
        }
650
651
        return false;
652
    }
653
654
    /**
655
     * @param string $path
656
     * @return bool
657
     */
658
    protected function pathIsIntendedAsExtensionPath(string $path): bool
659
    {
660
        return strpos($path, 'EXT:') === 0;
661
    }
662
663
    /**
664
     * @param string $path
665
     * @return bool
666
     */
667
    protected function pathIsIntendedAsFileMountPath(string $path): bool
668
    {
669
        if (empty($path)) {
670
            return false;
671
        }
672
673
        [$storageUid, $pathIdentifier] = explode(':', $path, 2);
674
        if (empty($storageUid) || empty($pathIdentifier)) {
675
            return false;
676
        }
677
678
        return MathUtility::canBeInterpretedAsInteger($storageUid);
679
    }
680
681
    /**
682
     * Returns a File object for a given $persistenceIdentifier.
683
     * If no file for this identifier exists a new object will be
684
     * created.
685
     *
686
     * @param string $persistenceIdentifier
687
     * @return File
688
     * @throws PersistenceManagerException
689
     */
690
    protected function getOrCreateFile(string $persistenceIdentifier): File
691
    {
692
        [$storageUid, $fileIdentifier] = explode(':', $persistenceIdentifier, 2);
693
        $storage = $this->getStorageByUid((int)$storageUid);
694
        $pathinfo = PathUtility::pathinfo($fileIdentifier);
695
696
        if (!$storage->hasFolder($pathinfo['dirname'])) {
697
            throw new PersistenceManagerException(sprintf('Could not create folder "%s".', $pathinfo['dirname']), 1471630579);
698
        }
699
700
        try {
701
            $folder = $storage->getFolder($pathinfo['dirname']);
702
        } catch (InsufficientFolderAccessPermissionsException $e) {
703
            throw new PersistenceManagerException(sprintf('No read access to folder "%s".', $pathinfo['dirname']), 1512583307);
704
        }
705
706
        if (!$storage->checkFolderActionPermission('write', $folder)) {
707
            throw new PersistenceManagerException(sprintf('No write access to folder "%s".', $pathinfo['dirname']), 1471630580);
708
        }
709
710
        if (!$storage->hasFile($fileIdentifier)) {
711
            $this->filePersistenceSlot->allowInvocation(
712
                FilePersistenceSlot::COMMAND_FILE_CREATE,
713
                $folder->getCombinedIdentifier() . $pathinfo['basename']
714
            );
715
            $file = $folder->createFile($pathinfo['basename']);
716
        } else {
717
            $file = $storage->getFile($fileIdentifier);
718
        }
719
        return $file;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $file could return the type TYPO3\CMS\Core\Resource\ProcessedFile which is incompatible with the type-hinted return TYPO3\CMS\Core\Resource\File. Consider adding an additional type-check to rule them out.
Loading history...
720
    }
721
722
    /**
723
     * Returns a ResourceStorage for a given uid
724
     *
725
     * @param int $storageUid
726
     * @return ResourceStorage
727
     * @throws PersistenceManagerException
728
     */
729
    protected function getStorageByUid(int $storageUid): ResourceStorage
730
    {
731
        $storage = $this->storageRepository->findByUid($storageUid);
732
        if (
733
            !$storage instanceof ResourceStorage
734
            || !$storage->isBrowsable()
735
        ) {
736
            throw new PersistenceManagerException(sprintf('Could not access storage with uid "%d".', $storageUid), 1471630581);
737
        }
738
        return $storage;
739
    }
740
741
    /**
742
     * @param string|File $persistenceIdentifier
743
     * @return array
744
     * @throws NoSuchFileException
745
     */
746
    protected function loadMetaData($persistenceIdentifier): array
747
    {
748
        if ($persistenceIdentifier instanceof File) {
749
            $file = $persistenceIdentifier;
750
            $persistenceIdentifier = $file->getCombinedIdentifier();
751
        } else {
752
            $file = $this->retrieveFileByPersistenceIdentifier($persistenceIdentifier);
753
        }
754
755
        try {
756
            $rawYamlContent = $file->getContents();
757
758
            if ($rawYamlContent === false) {
0 ignored issues
show
introduced by
The condition $rawYamlContent === false is always false.
Loading history...
759
                throw new NoSuchFileException(sprintf('YAML file "%s" could not be loaded', $persistenceIdentifier), 1524684462);
760
            }
761
762
            $yaml = $this->extractMetaDataFromCouldBeFormDefinition($rawYamlContent);
763
            $this->generateErrorsIfFormDefinitionIsValidButHasInvalidFileExtension($yaml, $persistenceIdentifier);
764
            $yaml['fileUid'] = $file->getUid();
765
        } catch (\Exception $e) {
766
            $yaml = [
767
                'type' => 'Form',
768
                'identifier' => $persistenceIdentifier,
769
                'label' => $e->getMessage(),
770
                'invalid' => true,
771
            ];
772
        }
773
774
        return $yaml;
775
    }
776
777
    /**
778
     * @param string $maybeRawFormDefinition
779
     * @return array
780
     */
781
    protected function extractMetaDataFromCouldBeFormDefinition(string $maybeRawFormDefinition): array
782
    {
783
        $metaDataProperties = ['identifier', 'type', 'label', 'prototypeName'];
784
        $metaData = [];
785
        foreach (explode(LF, $maybeRawFormDefinition) as $line) {
786
            if (empty($line) || $line[0] === ' ') {
787
                continue;
788
            }
789
790
            [$key, $value] = explode(':', $line);
791
            if (
792
                empty($key)
793
                || empty($value)
794
                || !in_array($key, $metaDataProperties, true)
795
            ) {
796
                continue;
797
            }
798
799
            $value = trim($value, " '\"\r");
800
            $metaData[$key] = $value;
801
        }
802
803
        return $metaData;
804
    }
805
806
    /**
807
     * @param array $formDefinition
808
     * @param string $persistenceIdentifier
809
     * @throws PersistenceManagerException
810
     */
811
    protected function generateErrorsIfFormDefinitionIsValidButHasInvalidFileExtension(array $formDefinition, string $persistenceIdentifier): void
812
    {
813
        if (
814
            $this->looksLikeAFormDefinition($formDefinition)
815
            && !$this->hasValidFileExtension($persistenceIdentifier)
816
        ) {
817
            throw new PersistenceManagerException(sprintf('Form definition "%s" does not end with ".form.yaml".', $persistenceIdentifier), 1531160649);
818
        }
819
    }
820
821
    /**
822
     * @param string $persistenceIdentifier
823
     * @return File
824
     * @throws PersistenceManagerException
825
     * @throws NoSuchFileException
826
     */
827
    protected function retrieveFileByPersistenceIdentifier(string $persistenceIdentifier): File
828
    {
829
        if (pathinfo($persistenceIdentifier, PATHINFO_EXTENSION) !== 'yaml') {
830
            throw new PersistenceManagerException(sprintf('The file "%s" could not be loaded.', $persistenceIdentifier), 1477679819);
831
        }
832
833
        if (
834
            $this->pathIsIntendedAsExtensionPath($persistenceIdentifier)
835
            && !$this->isFileWithinAccessibleExtensionFolders($persistenceIdentifier)
836
        ) {
837
            $message = sprintf('The file "%s" could not be loaded. Please check your configuration option "persistenceManager.allowedExtensionPaths"', $persistenceIdentifier);
838
            throw new PersistenceManagerException($message, 1484071985);
839
        }
840
841
        try {
842
            $file = $this->resourceFactory->retrieveFileOrFolderObject($persistenceIdentifier);
843
        } catch (\Exception $e) {
844
            // Top level catch to ensure useful following exception handling, because FAL throws top level exceptions.
845
            $file = null;
846
        }
847
848
        if ($file === null) {
849
            throw new NoSuchFileException(sprintf('YAML file "%s" could not be loaded', $persistenceIdentifier), 1524684442);
850
        }
851
852
        if (!$file->getStorage()->checkFileActionPermission('read', $file)) {
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type TYPO3\CMS\Core\Resource\Folder; however, parameter $file of TYPO3\CMS\Core\Resource\...kFileActionPermission() 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

852
        if (!$file->getStorage()->checkFileActionPermission('read', /** @scrutinizer ignore-type */ $file)) {
Loading history...
853
            throw new PersistenceManagerException(sprintf('No read access to file "%s".', $persistenceIdentifier), 1471630578);
854
        }
855
856
        return $file;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $file could return the type TYPO3\CMS\Core\Resource\Folder which is incompatible with the type-hinted return TYPO3\CMS\Core\Resource\File. Consider adding an additional type-check to rule them out.
Loading history...
857
    }
858
859
    /**
860
     * @param string $fileName
861
     * @return bool
862
     */
863
    protected function hasValidFileExtension(string $fileName): bool
864
    {
865
        return StringUtility::endsWith($fileName, self::FORM_DEFINITION_FILE_EXTENSION);
866
    }
867
868
    /**
869
     * @param string $fileName
870
     * @return bool
871
     */
872
    protected function isFileWithinAccessibleExtensionFolders(string $fileName): bool
873
    {
874
        $pathInfo = PathUtility::pathinfo($fileName, PATHINFO_DIRNAME);
875
        $pathInfo = is_string($pathInfo) ? $pathInfo : '';
0 ignored issues
show
introduced by
The condition is_string($pathInfo) is always true.
Loading history...
876
        $dirName = rtrim($pathInfo, '/') . '/';
877
        return array_key_exists($dirName, $this->getAccessibleExtensionFolders());
878
    }
879
880
    /**
881
     * @param string $fileName
882
     * @return bool
883
     */
884
    protected function isFileWithinAccessibleFormStorageFolders(string $fileName): bool
885
    {
886
        $pathInfo = PathUtility::pathinfo($fileName, PATHINFO_DIRNAME);
887
        $pathInfo = is_string($pathInfo) ? $pathInfo : '';
0 ignored issues
show
introduced by
The condition is_string($pathInfo) is always true.
Loading history...
888
        $dirName = rtrim($pathInfo, '/') . '/';
889
        return array_key_exists($dirName, $this->getAccessibleFormStorageFolders());
890
    }
891
892
    /**
893
     * @param string $folderName
894
     * @return bool
895
     */
896
    protected function isAccessibleExtensionFolder(string $folderName): bool
897
    {
898
        $folderName = rtrim($folderName, '/') . '/';
899
        return array_key_exists($folderName, $this->getAccessibleExtensionFolders());
900
    }
901
902
    /**
903
     * @param string $folderName
904
     * @return bool
905
     */
906
    protected function isAccessibleFormStorageFolder(string $folderName): bool
907
    {
908
        $folderName = rtrim($folderName, '/') . '/';
909
        return array_key_exists($folderName, $this->getAccessibleFormStorageFolders());
910
    }
911
912
    /**
913
     * @param array $data
914
     * @return bool
915
     */
916
    protected function looksLikeAFormDefinition(array $data): bool
917
    {
918
        return isset($data['identifier'], $data['type']) && !empty($data['identifier']) && trim($data['type']) === 'Form';
919
    }
920
921
    /**
922
     * @param array $forms
923
     * @return array
924
     */
925
    protected function sortForms(array $forms): array
926
    {
927
        $keys = $this->formSettings['persistenceManager']['sortByKeys'] ?? ['name', 'fileUid'];
928
        $ascending = $this->formSettings['persistenceManager']['sortAscending'] ?? true;
929
930
        usort($forms, function (array $a, array $b) use ($keys) {
931
            foreach ($keys as $key) {
932
                if (isset($a[$key]) && isset($b[$key])) {
933
                    $diff = strcasecmp((string)$a[$key], (string)$b[$key]);
934
                    if ($diff) {
935
                        return $diff;
936
                    }
937
                }
938
            }
939
        });
940
941
        return ($ascending) ? $forms : array_reverse($forms);
942
    }
943
}
944