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