36704f0b89e414cee9e5bdb1822d05d4ce40f7a3
[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\Configuration\Loader\FalYamlFileLoader;
21 use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader\Configuration as LoadConfiguration;
22 use TYPO3\CMS\Core\Configuration\Writer\Exception\FileWriteException;
23 use TYPO3\CMS\Core\Configuration\Writer\YamlFileWriter;
24 use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
25 use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
26 use TYPO3\CMS\Core\Resource\File;
27 use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
28 use TYPO3\CMS\Core\Resource\Folder;
29 use TYPO3\CMS\Core\Resource\ResourceStorage;
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31 use TYPO3\CMS\Core\Utility\PathUtility;
32 use TYPO3\CMS\Extbase\Object\ObjectManager;
33 use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface;
34 use TYPO3\CMS\Form\Mvc\Persistence\Exception\NoUniqueIdentifierException;
35 use TYPO3\CMS\Form\Mvc\Persistence\Exception\NoUniquePersistenceIdentifierException;
36 use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
37
38 /**
39 * Concrete implementation of the FormPersistenceManagerInterface
40 *
41 * Scope: frontend / backend
42 */
43 class FormPersistenceManager implements FormPersistenceManagerInterface
44 {
45
46 /**
47 * @var \TYPO3\CMS\Core\Resource\StorageRepository
48 */
49 protected $storageRepository;
50
51 /**
52 * @var array
53 */
54 protected $formSettings;
55
56 /**
57 * @param \TYPO3\CMS\Core\Resource\StorageRepository $storageRepository
58 * @internal
59 */
60 public function injectStorageRepository(\TYPO3\CMS\Core\Resource\StorageRepository $storageRepository)
61 {
62 $this->storageRepository = $storageRepository;
63 }
64
65 /**
66 * @internal
67 */
68 public function initializeObject()
69 {
70 $this->formSettings = GeneralUtility::makeInstance(ObjectManager::class)
71 ->get(ConfigurationManagerInterface::class)
72 ->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_YAML_SETTINGS, 'form');
73 }
74
75 /**
76 * Load the array formDefinition identified by $persistenceIdentifier, and return it.
77 * Only files with the extension .yaml are loaded.
78 *
79 * @param string $persistenceIdentifier
80 * @param LoadConfiguration $loadConfiguration
81 * @return array
82 * @throws PersistenceManagerException
83 * @internal
84 */
85 public function load(string $persistenceIdentifier, LoadConfiguration $loadConfiguration = null): array
86 {
87 if (pathinfo($persistenceIdentifier, PATHINFO_EXTENSION) !== 'yaml') {
88 throw new PersistenceManagerException(sprintf('The file "%s" could not be loaded.', $persistenceIdentifier), 1477679819);
89 }
90
91 if (strpos($persistenceIdentifier, 'EXT:') === 0) {
92 if (!array_key_exists(pathinfo($persistenceIdentifier, PATHINFO_DIRNAME) . '/', $this->getAccessibleExtensionFolders())) {
93 throw new PersistenceManagerException(sprintf('The file "%s" could not be loaded.', $persistenceIdentifier), 1484071985);
94 }
95 $file = $persistenceIdentifier;
96 } else {
97 $file = $this->getFileByIdentifier($persistenceIdentifier);
98 }
99
100 try {
101 $yaml = GeneralUtility::makeInstance(ObjectManager::class)
102 ->get(FalYamlFileLoader::class, $loadConfiguration)
103 ->load($file);
104 } catch (\Exception $e) {
105 $yaml = [
106 'identifier' => $file->getCombinedIdentifier(),
107 'label' => $e->getMessage(),
108 'error' => [
109 'code' => $e->getCode(),
110 'message' => $e->getMessage()
111 ],
112 'invalid' => true,
113 ];
114 }
115
116 return $yaml;
117 }
118
119 /**
120 * Save the array form representation identified by $persistenceIdentifier.
121 * Only files with the extension .yaml are saved.
122 * If the formDefinition is located within a EXT: resource, save is only
123 * allowed if the configuration path
124 * TYPO3.CMS.Form.persistenceManager.allowSaveToExtensionPaths
125 * is set to true.
126 *
127 * @param string $persistenceIdentifier
128 * @param array $formDefinition
129 * @throws PersistenceManagerException
130 * @internal
131 */
132 public function save(string $persistenceIdentifier, array $formDefinition)
133 {
134 if (pathinfo($persistenceIdentifier, PATHINFO_EXTENSION) !== 'yaml') {
135 throw new PersistenceManagerException(sprintf('The file "%s" could not be saved.', $persistenceIdentifier), 1477679820);
136 }
137
138 if (strpos($persistenceIdentifier, 'EXT:') === 0) {
139 if (!$this->formSettings['persistenceManager']['allowSaveToExtensionPaths']) {
140 throw new PersistenceManagerException('Save to extension paths is not allowed.', 1477680881);
141 }
142 if (!array_key_exists(pathinfo($persistenceIdentifier, PATHINFO_DIRNAME) . '/', $this->getAccessibleExtensionFolders())) {
143 throw new PersistenceManagerException(sprintf('The file "%s" could not be saved.', $persistenceIdentifier), 1484073571);
144 }
145 $fileToSave = $persistenceIdentifier;
146 } else {
147 $fileToSave = $this->getOrCreateFile($persistenceIdentifier);
148 }
149
150 try {
151 GeneralUtility::makeInstance(ObjectManager::class)
152 ->get(YamlFileWriter::class)
153 ->save($fileToSave, $formDefinition);
154 } catch (FileWriteException $e) {
155 throw new PersistenceManagerException(sprintf(
156 'The file "%s" could not be saved: %s',
157 $persistenceIdentifier,
158 $e->getMessage()
159 ), 1512582637, $e);
160 }
161 }
162
163 /**
164 * Delete the form representation identified by $persistenceIdentifier.
165 * Only files with the extension .yaml are removed.
166 *
167 * @param string $persistenceIdentifier
168 * @throws PersistenceManagerException
169 * @internal
170 */
171 public function delete(string $persistenceIdentifier)
172 {
173 if (pathinfo($persistenceIdentifier, PATHINFO_EXTENSION) !== 'yaml') {
174 throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1472239534);
175 }
176 if (!$this->exists($persistenceIdentifier)) {
177 throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1472239535);
178 }
179 if (strpos($persistenceIdentifier, 'EXT:') === 0) {
180 if (!$this->formSettings['persistenceManager']['allowDeleteFromExtensionPaths']) {
181 throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1472239536);
182 }
183 if (!array_key_exists(pathinfo($persistenceIdentifier, PATHINFO_DIRNAME) . '/', $this->getAccessibleExtensionFolders())) {
184 throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1484073878);
185 }
186 $fileToDelete = GeneralUtility::getFileAbsFileName($persistenceIdentifier);
187 unlink($fileToDelete);
188 } else {
189 list($storageUid, $fileIdentifier) = explode(':', $persistenceIdentifier, 2);
190 $storage = $this->getStorageByUid((int)$storageUid);
191 $file = $storage->getFile($fileIdentifier);
192 if (!$storage->checkFileActionPermission('delete', $file)) {
193 throw new PersistenceManagerException(sprintf('No delete access to file "%s".', $persistenceIdentifier), 1472239516);
194 }
195 $storage->deleteFile($file);
196 }
197 }
198
199 /**
200 * Check whether a form with the specified $persistenceIdentifier exists
201 *
202 * @param string $persistenceIdentifier
203 * @return bool TRUE if a form with the given $persistenceIdentifier can be loaded, otherwise FALSE
204 * @internal
205 */
206 public function exists(string $persistenceIdentifier): bool
207 {
208 $exists = false;
209 if (pathinfo($persistenceIdentifier, PATHINFO_EXTENSION) === 'yaml') {
210 if (strpos($persistenceIdentifier, 'EXT:') === 0) {
211 if (array_key_exists(pathinfo($persistenceIdentifier, PATHINFO_DIRNAME) . '/', $this->getAccessibleExtensionFolders())) {
212 $exists = file_exists(GeneralUtility::getFileAbsFileName($persistenceIdentifier));
213 }
214 } else {
215 list($storageUid, $fileIdentifier) = explode(':', $persistenceIdentifier, 2);
216 $storage = $this->getStorageByUid((int)$storageUid);
217 $exists = $storage->hasFile($fileIdentifier);
218 }
219 }
220 return $exists;
221 }
222
223 /**
224 * List all form definitions which can be loaded through this form persistence
225 * manager.
226 *
227 * Returns an associative array with each item containing the keys 'name' (the human-readable name of the form)
228 * and 'persistenceIdentifier' (the unique identifier for the Form Persistence Manager e.g. the path to the saved form definition).
229 *
230 * @return array in the format [['name' => 'Form 01', 'persistenceIdentifier' => 'path1'], [ .... ]]
231 * @internal
232 */
233 public function listForms(): array
234 {
235 $fileExtensionFilter = GeneralUtility::makeInstance(FileExtensionFilter::class);
236 $fileExtensionFilter->setAllowedFileExtensions(['yaml']);
237
238 $identifiers = [];
239 $forms = [];
240 /** @var \TYPO3\CMS\Core\Resource\Folder $folder */
241 foreach ($this->getAccessibleFormStorageFolders() as $folder) {
242 $storage = $folder->getStorage();
243 $storage->addFileAndFolderNameFilter([$fileExtensionFilter, 'filterFileList']);
244
245 // TODO: deprecated since TYPO3 v9, will be removed in TYPO3 v10
246 $formReadOnly = false;
247 if ($folder->getCombinedIdentifier() === '1:/user_upload/') {
248 $formReadOnly = true;
249 }
250
251 $files = $folder->getFiles(0, 0, Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS, true);
252 foreach ($files as $file) {
253 $persistenceIdentifier = $storage->getUid() . ':' . $file->getIdentifier();
254
255 $form = $this->load($persistenceIdentifier);
256 $forms[] = [
257 'identifier' => $form['identifier'],
258 'name' => $form['label'] ?? $form['identifier'],
259 'persistenceIdentifier' => $persistenceIdentifier,
260 'readOnly' => $formReadOnly,
261 'removable' => true,
262 'location' => 'storage',
263 'duplicateIdentifier' => false,
264 'invalid' => $form['invalid'],
265 'error' => $form['error'],
266 ];
267 $identifiers[$form['identifier']]++;
268 }
269 $storage->resetFileAndFolderNameFiltersToDefault();
270 }
271
272 foreach ($this->getAccessibleExtensionFolders() as $relativePath => $fullPath) {
273 $relativePath = rtrim($relativePath, '/') . '/';
274 foreach (new \DirectoryIterator($fullPath) as $fileInfo) {
275 if ($fileInfo->getExtension() !== 'yaml') {
276 continue;
277 }
278 $form = $this->load($relativePath . $fileInfo->getFilename());
279 $forms[] = [
280 'identifier' => $form['identifier'],
281 'name' => $form['label'] ?? $form['identifier'],
282 'persistenceIdentifier' => $relativePath . $fileInfo->getFilename(),
283 'readOnly' => $this->formSettings['persistenceManager']['allowSaveToExtensionPaths'] ? false: true,
284 'removable' => $this->formSettings['persistenceManager']['allowDeleteFromExtensionPaths'] ? true: false,
285 'location' => 'extension',
286 'duplicateIdentifier' => false,
287 'invalid' => $form['invalid'],
288 'error' => $form['error'],
289 ];
290 $identifiers[$form['identifier']]++;
291 }
292 }
293
294 foreach ($identifiers as $identifier => $count) {
295 if ($count > 1) {
296 foreach ($forms as &$formDefinition) {
297 if ($formDefinition['identifier'] === $identifier) {
298 $formDefinition['duplicateIdentifier'] = true;
299 }
300 }
301 }
302 }
303
304 return $forms;
305 }
306
307 /**
308 * Return a list of all accessible file mountpoints for the
309 * current backend user.
310 *
311 * Only registered mountpoints from
312 * TYPO3.CMS.Form.persistenceManager.allowedFileMounts
313 * are listet.
314 *
315 * @return Folder[]
316 * @internal
317 */
318 public function getAccessibleFormStorageFolders(): array
319 {
320 $storageFolders = [];
321 if (
322 !isset($this->formSettings['persistenceManager']['allowedFileMounts'])
323 || !is_array($this->formSettings['persistenceManager']['allowedFileMounts'])
324 || empty($this->formSettings['persistenceManager']['allowedFileMounts'])
325 ) {
326 return $storageFolders;
327 }
328
329 foreach ($this->formSettings['persistenceManager']['allowedFileMounts'] as $allowedFileMount) {
330 list($storageUid, $fileMountIdentifier) = explode(':', $allowedFileMount, 2);
331 $fileMountIdentifier = rtrim($fileMountIdentifier, '/') . '/';
332
333 try {
334 $storage = $this->getStorageByUid((int)$storageUid);
335 } catch (PersistenceManagerException $e) {
336 continue;
337 }
338
339 try {
340 $folder = $storage->getFolder($fileMountIdentifier);
341 } catch (FolderDoesNotExistException $e) {
342 $storage->createFolder($fileMountIdentifier);
343 continue;
344 } catch (InsufficientFolderAccessPermissionsException $e) {
345 continue;
346 }
347 $storageFolders[$allowedFileMount] = $folder;
348 }
349 return $storageFolders;
350 }
351
352 /**
353 * Return a list of all accessible extension folders
354 *
355 * Only registered mountpoints from
356 * TYPO3.CMS.Form.persistenceManager.allowedExtensionPaths
357 * are listet.
358 *
359 * @return array
360 * @internal
361 */
362 public function getAccessibleExtensionFolders(): array
363 {
364 $extensionFolders = [];
365 if (
366 !isset($this->formSettings['persistenceManager']['allowedExtensionPaths'])
367 || !is_array($this->formSettings['persistenceManager']['allowedExtensionPaths'])
368 || empty($this->formSettings['persistenceManager']['allowedExtensionPaths'])
369 ) {
370 return $extensionFolders;
371 }
372
373 foreach ($this->formSettings['persistenceManager']['allowedExtensionPaths'] as $allowedExtensionPath) {
374 if (strpos($allowedExtensionPath, 'EXT:') !== 0) {
375 continue;
376 }
377
378 $allowedExtensionFullPath = GeneralUtility::getFileAbsFileName($allowedExtensionPath);
379 if (!file_exists($allowedExtensionFullPath)) {
380 continue;
381 }
382 $allowedExtensionPath = rtrim($allowedExtensionPath, '/') . '/';
383 $extensionFolders[$allowedExtensionPath] = $allowedExtensionFullPath;
384 }
385 return $extensionFolders;
386 }
387
388 /**
389 * This takes a form identifier and returns a unique persistence identifier for it.
390 * By default this is just similar to the identifier. But if a form with the same persistence identifier already
391 * exists a suffix is appended until the persistence identifier is unique.
392 *
393 * @param string $formIdentifier lowerCamelCased form identifier
394 * @param string $savePath
395 * @return string unique form persistence identifier
396 * @throws NoUniquePersistenceIdentifierException
397 * @internal
398 */
399 public function getUniquePersistenceIdentifier(string $formIdentifier, string $savePath): string
400 {
401 $savePath = rtrim($savePath, '/') . '/';
402 $formPersistenceIdentifier = $savePath . $formIdentifier . '.yaml';
403 if (!$this->exists($formPersistenceIdentifier)) {
404 return $formPersistenceIdentifier;
405 }
406 for ($attempts = 1; $attempts < 100; $attempts++) {
407 $formPersistenceIdentifier = $savePath . sprintf('%s_%d', $formIdentifier, $attempts) . '.yaml';
408 if (!$this->exists($formPersistenceIdentifier)) {
409 return $formPersistenceIdentifier;
410 }
411 }
412 $formPersistenceIdentifier = $savePath . sprintf('%s_%d', $formIdentifier, time()) . '.yaml';
413 if (!$this->exists($formPersistenceIdentifier)) {
414 return $formPersistenceIdentifier;
415 }
416
417 throw new NoUniquePersistenceIdentifierException(
418 sprintf('Could not find a unique persistence identifier for form identifier "%s" after %d attempts', $formIdentifier, $attempts),
419 1476010403
420 );
421 }
422
423 /**
424 * This takes a form identifier and returns a unique identifier for it.
425 * If a formDefinition with the same identifier already exists a suffix is
426 * appended until the identifier is unique.
427 *
428 * @param string $identifier
429 * @return string unique form identifier
430 * @throws NoUniqueIdentifierException
431 * @internal
432 */
433 public function getUniqueIdentifier(string $identifier): string
434 {
435 $originalIdentifier = $identifier;
436 if ($this->checkForDuplicateIdentifier($identifier)) {
437 for ($attempts = 1; $attempts < 100; $attempts++) {
438 $identifier = sprintf('%s_%d', $originalIdentifier, $attempts);
439 if (!$this->checkForDuplicateIdentifier($identifier)) {
440 return $identifier;
441 }
442 }
443 $identifier = $originalIdentifier . '_' . time();
444 if ($this->checkForDuplicateIdentifier($identifier)) {
445 throw new NoUniqueIdentifierException(
446 sprintf('Could not find a unique identifier for form identifier "%s" after %d attempts', $identifier, $attempts),
447 1477688567
448 );
449 }
450 }
451 return $identifier;
452 }
453
454 /**
455 * Check if a identifier is already used by a formDefintion.
456 *
457 * @param string $identifier
458 * @return bool
459 * @internal
460 */
461 public function checkForDuplicateIdentifier(string $identifier): bool
462 {
463 $identifierUsed = false;
464 foreach ($this->listForms() as $formDefinition) {
465 if ($formDefinition['identifier'] === $identifier) {
466 $identifierUsed = true;
467 break;
468 }
469 }
470 return $identifierUsed;
471 }
472
473 /**
474 * Returns a File object for a given $persistenceIdentifier
475 *
476 * @param string $persistenceIdentifier
477 * @return File
478 * @throws PersistenceManagerException
479 */
480 protected function getFileByIdentifier(string $persistenceIdentifier): File
481 {
482 list($storageUid, $fileIdentifier) = explode(':', $persistenceIdentifier, 2);
483 $storage = $this->getStorageByUid((int)$storageUid);
484 $file = $storage->getFile($fileIdentifier);
485 if (!$storage->checkFileActionPermission('read', $file)) {
486 throw new PersistenceManagerException(sprintf('No read access to file "%s".', $persistenceIdentifier), 1471630578);
487 }
488 return $file;
489 }
490
491 /**
492 * Returns a File object for a given $persistenceIdentifier.
493 * If no file for this identifier exists a new object will be
494 * created.
495 *
496 * @param string $persistenceIdentifier
497 * @return File
498 * @throws PersistenceManagerException
499 */
500 protected function getOrCreateFile(string $persistenceIdentifier): File
501 {
502 list($storageUid, $fileIdentifier) = explode(':', $persistenceIdentifier, 2);
503 $storage = $this->getStorageByUid((int)$storageUid);
504 $pathinfo = PathUtility::pathinfo($fileIdentifier);
505
506 if (!$storage->hasFolder($pathinfo['dirname'])) {
507 throw new PersistenceManagerException(sprintf('Could not create folder "%s".', $pathinfo['dirname']), 1471630579);
508 }
509
510 try {
511 $folder = $storage->getFolder($pathinfo['dirname']);
512 } catch (InsufficientFolderAccessPermissionsException $e) {
513 throw new PersistenceManagerException(sprintf('No read access to folder "%s".', $pathinfo['dirname']), 1512583307);
514 }
515
516 if (!$storage->checkFolderActionPermission('write', $folder)) {
517 throw new PersistenceManagerException(sprintf('No write access to folder "%s".', $pathinfo['dirname']), 1471630580);
518 }
519
520 if (!$storage->hasFile($fileIdentifier)) {
521 $file = $folder->createFile($pathinfo['basename']);
522 } else {
523 $file = $storage->getFile($fileIdentifier);
524 }
525 return $file;
526 }
527
528 /**
529 * Returns a ResourceStorage for a given uid
530 *
531 * @param int $storageUid
532 * @return ResourceStorage
533 * @throws PersistenceManagerException
534 */
535 protected function getStorageByUid(int $storageUid): ResourceStorage
536 {
537 $storage = $this->storageRepository->findByUid($storageUid);
538 if (
539 !$storage instanceof ResourceStorage
540 || !$storage->isBrowsable()
541 ) {
542 throw new PersistenceManagerException(sprintf('Could not access storage with uid "%d".', $storageUid), 1471630581);
543 }
544 return $storage;
545 }
546 }