Passed
Push — master ( a7eef0...bd92b3 )
by
unknown
28:58 queued 15:52
created

FormPersistenceManager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

799
        if (!$file->getStorage()->checkFileActionPermission('read', /** @scrutinizer ignore-type */ $file)) {
Loading history...
800
            throw new PersistenceManagerException(sprintf('No read access to file "%s".', $persistenceIdentifier), 1471630578);
801
        }
802
803
        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...
804
    }
805
806
    /**
807
     * @param string $fileName
808
     * @return bool
809
     */
810
    protected function hasValidFileExtension(string $fileName): bool
811
    {
812
        return StringUtility::endsWith($fileName, self::FORM_DEFINITION_FILE_EXTENSION);
813
    }
814
815
    /**
816
     * @param string $fileName
817
     * @return bool
818
     */
819
    protected function isFileWithinAccessibleExtensionFolders(string $fileName): bool
820
    {
821
        $pathInfo = PathUtility::pathinfo($fileName, PATHINFO_DIRNAME);
822
        $pathInfo = is_string($pathInfo) ? $pathInfo : '';
0 ignored issues
show
introduced by
The condition is_string($pathInfo) is always true.
Loading history...
823
        $dirName = rtrim($pathInfo, '/') . '/';
824
        return array_key_exists($dirName, $this->getAccessibleExtensionFolders());
825
    }
826
827
    /**
828
     * @param string $fileName
829
     * @return bool
830
     */
831
    protected function isFileWithinAccessibleFormStorageFolders(string $fileName): bool
832
    {
833
        $pathInfo = PathUtility::pathinfo($fileName, PATHINFO_DIRNAME);
834
        $pathInfo = is_string($pathInfo) ? $pathInfo : '';
0 ignored issues
show
introduced by
The condition is_string($pathInfo) is always true.
Loading history...
835
        $dirName = rtrim($pathInfo, '/') . '/';
836
        return array_key_exists($dirName, $this->getAccessibleFormStorageFolders());
837
    }
838
839
    /**
840
     * @param string $folderName
841
     * @return bool
842
     */
843
    protected function isAccessibleExtensionFolder(string $folderName): bool
844
    {
845
        $folderName = rtrim($folderName, '/') . '/';
846
        return array_key_exists($folderName, $this->getAccessibleExtensionFolders());
847
    }
848
849
    /**
850
     * @param string $folderName
851
     * @return bool
852
     */
853
    protected function isAccessibleFormStorageFolder(string $folderName): bool
854
    {
855
        $folderName = rtrim($folderName, '/') . '/';
856
        return array_key_exists($folderName, $this->getAccessibleFormStorageFolders());
857
    }
858
859
    /**
860
     * @param array $data
861
     * @return bool
862
     */
863
    protected function looksLikeAFormDefinition(array $data): bool
864
    {
865
        return isset($data['identifier'], $data['type']) && !empty($data['identifier']) && trim($data['type']) === 'Form';
866
    }
867
868
    /**
869
     * @param array $forms
870
     * @return array
871
     */
872
    protected function sortForms(array $forms): array
873
    {
874
        $keys = $this->formSettings['persistenceManager']['sortByKeys'] ?? ['name', 'fileUid'];
875
        $ascending = $this->formSettings['persistenceManager']['sortAscending'] ?? true;
876
877
        usort($forms, function (array $a, array $b) use ($keys) {
878
            foreach ($keys as $key) {
879
                if (isset($a[$key]) && isset($b[$key])) {
880
                    $diff = strcasecmp((string)$a[$key], (string)$b[$key]);
881
                    if ($diff) {
882
                        return $diff;
883
                    }
884
                }
885
            }
886
        });
887
888
        return ($ascending) ? $forms : array_reverse($forms);
889
    }
890
}
891