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