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