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