TYPO3 CMS  TYPO3_8-7
FormPersistenceManager.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types = 1);
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 
40 
47 {
48  const FORM_DEFINITION_FILE_EXTENSION = '.form.yaml';
49 
53  protected $yamlSource;
54 
58  protected $storageRepository;
59 
63  protected $formSettings;
64 
69 
73  protected $runtimeCache;
74 
78  protected $resourceFactory;
79 
84  public function injectYamlSource(\TYPO3\CMS\Form\Mvc\Configuration\YamlSource $yamlSource)
85  {
86  $this->yamlSource = $yamlSource;
87  }
88 
93  public function injectStorageRepository(\TYPO3\CMS\Core\Resource\StorageRepository $storageRepository)
94  {
95  $this->storageRepository = $storageRepository;
96  }
97 
102  {
103  $this->filePersistenceSlot = $filePersistenceSlot;
104  }
105 
109  public function injectResourceFactory(\TYPO3\CMS\Core\Resource\ResourceFactory $resourceFactory)
110  {
111  $this->resourceFactory = $resourceFactory;
112  }
113 
117  public function initializeObject()
118  {
119  $this->formSettings = GeneralUtility::makeInstance(ObjectManager::class)
120  ->get(ConfigurationManagerInterface::class)
122  $this->runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
123  }
124 
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 
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 
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 
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 
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  } else {
301  $forms[] = [
302  'identifier' => $form['identifier'],
303  'name' => $form['label'] ?? $form['identifier'],
304  'persistenceIdentifier' => $persistenceIdentifier,
305  'readOnly' => true,
306  'removable' => false,
307  'location' => 'storage',
308  'duplicateIdentifier' => false,
309  'invalid' => false,
310  'deprecatedFileExtension' => true,
311  'fileUid' => $form['fileUid'],
312  ];
313  }
314  }
315 
316  foreach ($this->retrieveYamlFilesFromExtensionFolders() as $fullPath => $fileName) {
317  $form = $this->loadMetaData($fullPath);
318 
319  if ($this->looksLikeAFormDefinition($form)) {
320  if ($this->hasValidFileExtension($fileName)) {
321  $forms[] = [
322  'identifier' => $form['identifier'],
323  'name' => $form['label'] ?? $form['identifier'],
324  'persistenceIdentifier' => $fullPath,
325  'readOnly' => $this->formSettings['persistenceManager']['allowSaveToExtensionPaths'] ? false: true,
326  'removable' => $this->formSettings['persistenceManager']['allowDeleteFromExtensionPaths'] ? true: false,
327  'location' => 'extension',
328  'duplicateIdentifier' => false,
329  'invalid' => $form['invalid'],
330  'fileUid' => $form['fileUid'],
331  ];
332  $identifiers[$form['identifier']]++;
333  } else {
334  $forms[] = [
335  'identifier' => $form['identifier'],
336  'name' => $form['label'] ?? $form['identifier'],
337  'persistenceIdentifier' => $fullPath,
338  'readOnly' => true,
339  'removable' => false,
340  'location' => 'extension',
341  'duplicateIdentifier' => false,
342  'invalid' => false,
343  'deprecatedFileExtension' => true,
344  'fileUid' => $form['fileUid'],
345  ];
346  }
347  }
348  }
349 
350  foreach ($identifiers as $identifier => $count) {
351  if ($count > 1) {
352  foreach ($forms as &$formDefinition) {
353  if ($formDefinition['identifier'] === $identifier) {
354  $formDefinition['duplicateIdentifier'] = true;
355  }
356  }
357  }
358  }
359 
360  return $forms;
361  }
362 
370  public function retrieveYamlFilesFromStorageFolders(): array
371  {
372  $filesFromStorageFolders = [];
373 
374  $fileExtensionFilter = GeneralUtility::makeInstance(FileExtensionFilter::class);
375  $fileExtensionFilter->setAllowedFileExtensions(['yaml']);
376 
377  foreach ($this->getAccessibleFormStorageFolders() as $folder) {
378  $storage = $folder->getStorage();
379  $storage->addFileAndFolderNameFilter([
380  $fileExtensionFilter,
381  'filterFileList'
382  ]);
383 
384  $files = $folder->getFiles(
385  0,
386  0,
388  true
389  );
390  $filesFromStorageFolders = $filesFromStorageFolders + $files;
391  $storage->resetFileAndFolderNameFiltersToDefault();
392  }
393 
394  return $filesFromStorageFolders;
395  }
396 
404  public function retrieveYamlFilesFromExtensionFolders(): array
405  {
406  $filesFromExtensionFolders = [];
407 
408  foreach ($this->getAccessibleExtensionFolders() as $relativePath => $fullPath) {
409  foreach (new \DirectoryIterator($fullPath) as $fileInfo) {
410  if ($fileInfo->getExtension() !== 'yaml') {
411  continue;
412  }
413  $filesFromExtensionFolders[$relativePath . $fileInfo->getFilename()] = $fileInfo->getFilename();
414  }
415  }
416 
417  return $filesFromExtensionFolders;
418  }
419 
431  public function getAccessibleFormStorageFolders(): array
432  {
433  $storageFolders = [];
434  if (
435  !isset($this->formSettings['persistenceManager']['allowedFileMounts'])
436  || !is_array($this->formSettings['persistenceManager']['allowedFileMounts'])
437  || empty($this->formSettings['persistenceManager']['allowedFileMounts'])
438  ) {
439  return $storageFolders;
440  }
441 
442  foreach ($this->formSettings['persistenceManager']['allowedFileMounts'] as $allowedFileMount) {
443  list($storageUid, $fileMountIdentifier) = explode(':', $allowedFileMount, 2);
444  $fileMountIdentifier = rtrim($fileMountIdentifier, '/') . '/';
445 
446  try {
447  $storage = $this->getStorageByUid((int)$storageUid);
448  } catch (PersistenceManagerException $e) {
449  continue;
450  }
451 
452  try {
453  $folder = $storage->getFolder($fileMountIdentifier);
454  } catch (FolderDoesNotExistException $e) {
455  continue;
457  continue;
458  }
459  $storageFolders[$allowedFileMount] = $folder;
460  }
461  return $storageFolders;
462  }
463 
474  public function getAccessibleExtensionFolders(): array
475  {
476  $extensionFolders = $this->runtimeCache->get('formAccessibleExtensionFolders');
477 
478  if ($extensionFolders !== false) {
479  return $extensionFolders;
480  }
481 
482  $extensionFolders = [];
483  if (
484  !isset($this->formSettings['persistenceManager']['allowedExtensionPaths'])
485  || !is_array($this->formSettings['persistenceManager']['allowedExtensionPaths'])
486  || empty($this->formSettings['persistenceManager']['allowedExtensionPaths'])
487  ) {
488  $this->runtimeCache->set('formAccessibleExtensionFolders', $extensionFolders);
489  return $extensionFolders;
490  }
491 
492  foreach ($this->formSettings['persistenceManager']['allowedExtensionPaths'] as $allowedExtensionPath) {
493  if (strpos($allowedExtensionPath, 'EXT:') !== 0) {
494  continue;
495  }
496 
497  $allowedExtensionFullPath = GeneralUtility::getFileAbsFileName($allowedExtensionPath);
498  if (!file_exists($allowedExtensionFullPath)) {
499  continue;
500  }
501  $allowedExtensionPath = rtrim($allowedExtensionPath, '/') . '/';
502  $extensionFolders[$allowedExtensionPath] = $allowedExtensionFullPath;
503  }
504 
505  $this->runtimeCache->set('formAccessibleExtensionFolders', $extensionFolders);
506  return $extensionFolders;
507  }
508 
520  public function getUniquePersistenceIdentifier(string $formIdentifier, string $savePath): string
521  {
522  $savePath = rtrim($savePath, '/') . '/';
523  $formPersistenceIdentifier = $savePath . $formIdentifier . self::FORM_DEFINITION_FILE_EXTENSION;
524  if (!$this->exists($formPersistenceIdentifier)) {
525  return $formPersistenceIdentifier;
526  }
527  for ($attempts = 1; $attempts < 100; $attempts++) {
528  $formPersistenceIdentifier = $savePath . sprintf('%s_%d', $formIdentifier, $attempts) . self::FORM_DEFINITION_FILE_EXTENSION;
529  if (!$this->exists($formPersistenceIdentifier)) {
530  return $formPersistenceIdentifier;
531  }
532  }
533  $formPersistenceIdentifier = $savePath . sprintf('%s_%d', $formIdentifier, time()) . self::FORM_DEFINITION_FILE_EXTENSION;
534  if (!$this->exists($formPersistenceIdentifier)) {
535  return $formPersistenceIdentifier;
536  }
537 
539  sprintf('Could not find a unique persistence identifier for form identifier "%s" after %d attempts', $formIdentifier, $attempts),
540  1476010403
541  );
542  }
543 
554  public function getUniqueIdentifier(string $identifier): string
555  {
556  $originalIdentifier = $identifier;
557  if ($this->checkForDuplicateIdentifier($identifier)) {
558  for ($attempts = 1; $attempts < 100; $attempts++) {
559  $identifier = sprintf('%s_%d', $originalIdentifier, $attempts);
560  if (!$this->checkForDuplicateIdentifier($identifier)) {
561  return $identifier;
562  }
563  }
564  $identifier = $originalIdentifier . '_' . time();
565  if ($this->checkForDuplicateIdentifier($identifier)) {
566  throw new NoUniqueIdentifierException(
567  sprintf('Could not find a unique identifier for form identifier "%s" after %d attempts', $identifier, $attempts),
568  1477688567
569  );
570  }
571  }
572  return $identifier;
573  }
574 
582  public function checkForDuplicateIdentifier(string $identifier): bool
583  {
584  $identifierUsed = false;
585  foreach ($this->listForms() as $formDefinition) {
586  if ($formDefinition['identifier'] === $identifier) {
587  $identifierUsed = true;
588  break;
589  }
590  }
591  return $identifierUsed;
592  }
593 
603  protected function getOrCreateFile(string $persistenceIdentifier): File
604  {
605  list($storageUid, $fileIdentifier) = explode(':', $persistenceIdentifier, 2);
606  $storage = $this->getStorageByUid((int)$storageUid);
607  $pathinfo = PathUtility::pathinfo($fileIdentifier);
608 
609  if (!$storage->hasFolder($pathinfo['dirname'])) {
610  throw new PersistenceManagerException(sprintf('Could not create folder "%s".', $pathinfo['dirname']), 1471630579);
611  }
612 
613  try {
614  $folder = $storage->getFolder($pathinfo['dirname']);
616  throw new PersistenceManagerException(sprintf('No read access to folder "%s".', $pathinfo['dirname']), 1512583307);
617  }
618 
619  if (!$storage->checkFolderActionPermission('write', $folder)) {
620  throw new PersistenceManagerException(sprintf('No write access to folder "%s".', $pathinfo['dirname']), 1471630580);
621  }
622 
623  if (!$storage->hasFile($fileIdentifier)) {
624  $this->filePersistenceSlot->allowInvocation(
626  $folder->getCombinedIdentifier() . $pathinfo['basename']
627  );
628  $file = $folder->createFile($pathinfo['basename']);
629  } else {
630  $file = $storage->getFile($fileIdentifier);
631  }
632  return $file;
633  }
634 
642  protected function getStorageByUid(int $storageUid): ResourceStorage
643  {
644  $storage = $this->storageRepository->findByUid($storageUid);
645  if (
646  !$storage instanceof ResourceStorage
647  || !$storage->isBrowsable()
648  ) {
649  throw new PersistenceManagerException(sprintf('Could not access storage with uid "%d".', $storageUid), 1471630581);
650  }
651  return $storage;
652  }
653 
659  protected function loadMetaData($persistenceIdentifier): array
660  {
661  if ($persistenceIdentifier instanceof File) {
662  $file = $persistenceIdentifier;
663  $persistenceIdentifier = $file->getCombinedIdentifier();
664  } else {
665  $file = $this->retrieveFileByPersistenceIdentifier($persistenceIdentifier);
666  }
667 
668  try {
669  $rawYamlContent = $file->getContents();
670 
671  if ($rawYamlContent === false) {
672  throw new NoSuchFileException(sprintf('YAML file "%s" could not be loaded', $persistenceIdentifier), 1524684462);
673  }
674 
675  $yaml = $this->extractMetaDataFromCouldBeFormDefinition($rawYamlContent);
676  $this->generateErrorsIfFormDefinitionIsValidButHasInvalidFileExtension($yaml, $persistenceIdentifier);
677  $yaml['fileUid'] = $file->getUid();
678  } catch (\Exception $e) {
679  $yaml = [
680  'type' => 'Form',
681  'identifier' => $persistenceIdentifier,
682  'label' => $e->getMessage(),
683  'invalid' => true,
684  ];
685  }
686 
687  return $yaml;
688  }
689 
694  protected function extractMetaDataFromCouldBeFormDefinition(string $maybeRawFormDefinition): array
695  {
696  $metaDataProperties = ['identifier', 'type', 'label', 'prototypeName'];
697  $metaData = [];
698  foreach (explode(LF, $maybeRawFormDefinition) as $line) {
699  if (empty($line) || $line[0] === ' ') {
700  continue;
701  }
702 
703  list($key, $value) = explode(':', $line);
704  if (
705  empty($key)
706  || empty($value)
707  || !in_array($key, $metaDataProperties, true)
708  ) {
709  continue;
710  }
711 
712  $value = trim($value, ' \'"');
713  $metaData[$key] = $value;
714  }
715 
716  return $metaData;
717  }
718 
724  protected function generateErrorsIfFormDefinitionIsValidButHasInvalidFileExtension(array $formDefinition, string $persistenceIdentifier)
725  {
726  if (
727  $this->looksLikeAFormDefinition($formDefinition)
728  && !$this->hasValidFileExtension($persistenceIdentifier)
729  && strpos($persistenceIdentifier, 'EXT:') !== 0
730  ) {
731  throw new PersistenceManagerException(sprintf('Form definition "%s" does not end with ".form.yaml".', $persistenceIdentifier), 1531160649);
732  }
733  }
734 
741  protected function retrieveFileByPersistenceIdentifier(string $persistenceIdentifier): File
742  {
743  if (pathinfo($persistenceIdentifier, PATHINFO_EXTENSION) !== 'yaml') {
744  throw new PersistenceManagerException(sprintf('The file "%s" could not be loaded.', $persistenceIdentifier), 1477679819);
745  }
746 
747  if (
748  strpos($persistenceIdentifier, 'EXT:') === 0
749  && !$this->isFileWithinAccessibleExtensionFolders($persistenceIdentifier)
750  ) {
751  $message = sprintf('The file "%s" could not be loaded. Please check your configuration option "persistenceManager.allowedExtensionPaths"', $persistenceIdentifier);
752  throw new PersistenceManagerException($message, 1484071985);
753  }
754 
755  try {
756  $file = $this->resourceFactory->retrieveFileOrFolderObject($persistenceIdentifier);
757  } catch (\Exception $e) {
758  // Top level catch to ensure useful following exception handling, because FAL throws top level exceptions.
759  $file = null;
760  }
761 
762  if ($file === null) {
763  throw new NoSuchFileException(sprintf('YAML file "%s" could not be loaded', $persistenceIdentifier), 1524684442);
764  }
765 
766  if (!$file->getStorage()->checkFileActionPermission('read', $file)) {
767  throw new PersistenceManagerException(sprintf('No read access to file "%s".', $persistenceIdentifier), 1471630578);
768  }
769 
770  return $file;
771  }
772 
777  protected function hasValidFileExtension(string $fileName): bool
778  {
779  return StringUtility::endsWith($fileName, self::FORM_DEFINITION_FILE_EXTENSION);
780  }
781 
786  protected function isFileWithinAccessibleExtensionFolders(string $fileName): bool
787  {
788  $dirName = rtrim(PathUtility::pathinfo($fileName, PATHINFO_DIRNAME), '/') . '/';
789  return array_key_exists($dirName, $this->getAccessibleExtensionFolders());
790  }
791 
796  protected function looksLikeAFormDefinition(array $data): bool
797  {
798  return isset($data['identifier'], $data['type']) && !empty($data['identifier']) && $data['type'] === 'Form';
799  }
800 }
save(string $persistenceIdentifier, array $formDefinition)
injectFilePersistenceSlot(\TYPO3\CMS\Form\Slot\FilePersistenceSlot $filePersistenceSlot)
generateErrorsIfFormDefinitionIsValidButHasInvalidFileExtension(array $formDefinition, string $persistenceIdentifier)
static getFileAbsFileName($filename, $_=null, $_2=null)
injectResourceFactory(\TYPO3\CMS\Core\Resource\ResourceFactory $resourceFactory)
static makeInstance($className,... $constructorArguments)
injectStorageRepository(\TYPO3\CMS\Core\Resource\StorageRepository $storageRepository)
extractMetaDataFromCouldBeFormDefinition(string $maybeRawFormDefinition)
static pathinfo($path, $options=null)
injectYamlSource(\TYPO3\CMS\Form\Mvc\Configuration\YamlSource $yamlSource)
const FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS
Definition: Folder.php:68
getUniquePersistenceIdentifier(string $formIdentifier, string $savePath)
static endsWith($haystack, $needle)