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