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