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