[SECURITY] Filter disallowed properties in form editor
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Domain / Configuration / ConfigurationService.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Form\Domain\Configuration;
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\Cache\CacheManager;
19 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
20 use TYPO3\CMS\Core\SingletonInterface;
21 use TYPO3\CMS\Core\Utility\ArrayUtility;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Extbase\Object\ObjectManager;
24 use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
25 use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
26 use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
27 use TYPO3\CMS\Form\Domain\Configuration\Exception\PrototypeNotFoundException;
28 use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto;
29 use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AdditionalElementPropertyPathsExtractor;
30 use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\ExtractorDto;
31 use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\IsCreatableFormElementExtractor;
32 use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\MultiValuePropertiesExtractor;
33 use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\PredefinedDefaultsExtractor;
34 use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\PropertyPathsExtractor;
35 use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\IsCreatablePropertyCollectionElementExtractor;
36 use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\MultiValuePropertiesExtractor as CollectionMultiValuePropertiesExtractor;
37 use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\PredefinedDefaultsExtractor as CollectionPredefinedDefaultsExtractor;
38 use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\PropertyPathsExtractor as CollectionPropertyPathsExtractor;
39 use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface;
40 use TYPO3\CMS\Form\Service\TranslationService;
41
42 /**
43 * Helper for configuration settings
44 * Scope: frontend / backend
45 */
46 class ConfigurationService implements SingletonInterface
47 {
48
49 /**
50 * @var array
51 */
52 protected $formSettings;
53
54 /**
55 * @var array
56 */
57 protected $firstLevelCache = [];
58
59 /**
60 * @var TranslationService
61 */
62 protected $translationService;
63
64 /**
65 * @internal
66 */
67 public function initializeObject(): void
68 {
69 $this->formSettings = GeneralUtility::makeInstance(ObjectManager::class)
70 ->get(ConfigurationManagerInterface::class)
71 ->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_YAML_SETTINGS, 'form');
72 }
73
74 /**
75 * Get the prototype configuration
76 *
77 * @param string $prototypeName name of the prototype to get the configuration for
78 * @return array the prototype configuration
79 * @throws PrototypeNotFoundException if prototype with the name $prototypeName was not found
80 * @api
81 */
82 public function getPrototypeConfiguration(string $prototypeName): array
83 {
84 if (!isset($this->formSettings['prototypes'][$prototypeName])) {
85 throw new PrototypeNotFoundException(
86 sprintf('The Prototype "%s" was not found.', $prototypeName),
87 1475924277
88 );
89 }
90 return $this->formSettings['prototypes'][$prototypeName];
91 }
92
93 /**
94 * Return all prototype names which are defined within "formManager.selectablePrototypesConfiguration.*.identifier"
95 *
96 * @return array
97 * @internal
98 */
99 public function getSelectablePrototypeNamesDefinedInFormEditorSetup(): array
100 {
101 $returnValue = GeneralUtility::makeInstance(
102 ArrayProcessor::class,
103 $this->formSettings['formManager']['selectablePrototypesConfiguration'] ?? []
104 )->forEach(
105 GeneralUtility::makeInstance(
106 ArrayProcessing::class,
107 'selectablePrototypeNames',
108 '^([\d]+)\.identifier$',
109 function ($_, $value) {
110 return $value;
111 }
112 )
113 );
114
115 return array_values($returnValue['selectablePrototypeNames'] ?? []);
116 }
117
118 /**
119 * Check if a form element property is defined in the form setup.
120 * If a form element property is defined in the form setup then it
121 * means that the form element property can be written by the form editor.
122 * A form element property can be written if the property path is defined within
123 * the following form editor properties:
124 * * formElementsDefinition.<formElementType>.formEditor.editors.<index>.propertyPath
125 * * formElementsDefinition.<formElementType>.formEditor.editors.<index>.*.propertyPath
126 * * formElementsDefinition.<formElementType>.formEditor.editors.<index>.additionalElementPropertyPaths
127 * * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.additionalElementPropertyPaths
128 * If a form editor property "templateName" is
129 * "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor"
130 * it means that the form editor property "propertyPath" is interpreted as a so called "multiValueProperty".
131 * A "multiValueProperty" can contain any subproperties relative to the value from "propertyPath" which are valid.
132 * If "formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName = Inspector-PropertyGridEditor"
133 * and
134 * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.propertyPath = options.xxx"
135 * then (for example) "options.xxx.yyy" is a valid property path to write.
136 * If you use a custom form editor "inspector editor" implementation which does not define the writable
137 * property paths by one of the above described inspector editor properties (e.g "propertyPath") within
138 * the form setup, you must provide the writable property paths with a hook.
139 *
140 * @see $this->executeBuildFormDefinitionValidationConfigurationHooks()
141 * @param ValidationDto $dto
142 * @return bool
143 * @internal
144 */
145 public function isFormElementPropertyDefinedInFormEditorSetup(ValidationDto $dto): bool
146 {
147 $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
148 $dto->getPrototypeName()
149 );
150
151 $subConfig = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()] ?? [];
152 return $this->isPropertyDefinedInFormEditorSetup($dto->getPropertyPath(), $subConfig);
153 }
154
155 /**
156 * Check if a form elements finisher|validator property is defined in the form setup.
157 * If a form elements finisher|validator property is defined in the form setup then it
158 * means that the form elements finisher|validator property can be written by the form editor.
159 * A form elements finisher|validator property can be written if the property path is defined within
160 * the following form editor properties:
161 * * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath
162 * * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.*.propertyPath
163 * If a form elements finisher|validator property "templateName" is
164 * "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor"
165 * it means that the form editor property "propertyPath" is interpreted as a so called "multiValueProperty".
166 * A "multiValueProperty" can contain any subproperties relative to the value from "propertyPath" which are valid.
167 * If "formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.templateName = Inspector-PropertyGridEditor"
168 * and
169 * "formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath = options.xxx"
170 * that (for example) "options.xxx.yyy" is a valid property path to write.
171 * If you use a custom form editor "inspector editor" implementation which not defines the writable
172 * property paths by one of the above described inspector editor properties (e.g "propertyPath") within
173 * the form setup, you must provide the writable property paths with a hook.
174 *
175 * @see $this->executeBuildFormDefinitionValidationConfigurationHooks()
176 * @param ValidationDto $dto
177 * @return bool
178 * @internal
179 */
180 public function isPropertyCollectionPropertyDefinedInFormEditorSetup(ValidationDto $dto): bool
181 {
182 $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
183 $dto->getPrototypeName()
184 );
185 $subConfig = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()] ?? [];
186
187 return $this->isPropertyDefinedInFormEditorSetup($dto->getPropertyPath(), $subConfig);
188 }
189
190 /**
191 * Check if a form element property is defined in "predefinedDefaults" in the form setup.
192 * If a form element property is defined in the "predefinedDefaults" in the form setup then it
193 * means that the form element property can be written by the form editor.
194 * A form element default property is defined within the following form editor properties:
195 * * formElementsDefinition.<formElementType>.formEditor.predefinedDefaults.<propertyPath> = "default value"
196 *
197 * @param ValidationDto $dto
198 * @return bool
199 * @internal
200 */
201 public function isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup(
202 ValidationDto $dto
203 ): bool {
204 $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
205 $dto->getPrototypeName()
206 );
207 return isset(
208 $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['predefinedDefaults'][$dto->getPropertyPath()]
209 );
210 }
211
212 /**
213 * Get the "predefinedDefaults" value for a form element property from the form setup.
214 * A form element default property is defined within the following form editor properties:
215 * * formElementsDefinition.<formElementType>.formEditor.predefinedDefaults.<propertyPath> = "default value"
216 *
217 * @param ValidationDto $dto
218 * @return mixed
219 * @throws PropertyException
220 * @internal
221 */
222 public function getFormElementPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto)
223 {
224 if (!$this->isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
225 throw new PropertyException(
226 sprintf(
227 'No predefinedDefaults found for form element type "%s" and property path "%s"',
228 $dto->getFormElementType(),
229 $dto->getPropertyPath()
230 ),
231 1528578401
232 );
233 }
234
235 $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
236 $dto->getPrototypeName()
237 );
238 return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['predefinedDefaults'][$dto->getPropertyPath()];
239 }
240
241 /**
242 * Check if a form elements finisher|validator property is defined in "predefinedDefaults" in the form setup.
243 * If a form elements finisher|validator property is defined in "predefinedDefaults" in the form setup then it
244 * means that the form elements finisher|validator property can be written by the form editor.
245 * A form elements finisher|validator default property is defined within the following form editor properties:
246 * * <validatorsDefinition|finishersDefinition>.<index>.formEditor.predefinedDefaults.<propertyPath> = "default value"
247 *
248 * @param ValidationDto $dto
249 * @return bool
250 * @internal
251 */
252 public function isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup(
253 ValidationDto $dto
254 ): bool {
255 $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
256 $dto->getPrototypeName()
257 );
258 return isset(
259 $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['predefinedDefaults'][$dto->getPropertyPath()]
260 );
261 }
262
263 /**
264 * Get the "predefinedDefaults" value for a form elements finisher|validator property from the form setup.
265 * A form elements finisher|validator default property is defined within the following form editor properties:
266 * * <validatorsDefinition|finishersDefinition>.<index>.formEditor.predefinedDefaults.<propertyPath> = "default value"
267 *
268 * @param ValidationDto $dto
269 * @return mixed
270 * @throws PropertyException
271 * @internal
272 */
273 public function getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto)
274 {
275 if (!$this->isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
276 throw new PropertyException(
277 sprintf(
278 'No predefinedDefaults found for property collection "%s" and identifier "%s" and property path "%s"',
279 $dto->getPropertyCollectionName(),
280 $dto->getPropertyCollectionElementIdentifier(),
281 $dto->getPropertyPath()
282 ),
283 1528578402
284 );
285 }
286
287 $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
288 $dto->getPrototypeName()
289 );
290 return $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['predefinedDefaults'][$dto->getPropertyPath()];
291 }
292
293 /**
294 * Check if the form element is creatable through the form editor.
295 * A form element is creatable if the following properties are set:
296 * * formElementsDefinition.<formElementType>.formEditor.group
297 * * formElementsDefinition.<formElementType>.formEditor.groupSorting
298 * And the value from "formElementsDefinition.<formElementType>.formEditor.group" is
299 * one of the keys within "formEditor.formElementGroups"
300 *
301 * @param ValidationDto $dto
302 * @return bool
303 * @internal
304 */
305 public function isFormElementTypeCreatableByFormEditor(ValidationDto $dto): bool
306 {
307 if ($dto->getFormElementType() === 'Form') {
308 return true;
309 }
310 $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
311 $dto->getPrototypeName()
312 );
313 return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['creatable'] ?? false;
314 }
315
316 /**
317 * Check if the form elements finisher|validator is creatable through the form editor.
318 * A form elements finisher|validator is creatable if the following conditions are true:
319 * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName = Inspector-FinishersEditor"
320 * or
321 * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName = Inspector-ValidatorsEditor"
322 * and
323 * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.selectOptions.<index>.value = <finisherIdentifier|validatorIdentifier>"
324 *
325 * @param ValidationDto $dto
326 * @return bool
327 * @internal
328 */
329 public function isPropertyCollectionElementIdentifierCreatableByFormEditor(ValidationDto $dto): bool
330 {
331 $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
332 $dto->getPrototypeName()
333 );
334 return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['creatable'] ?? false;
335 }
336
337 /**
338 * Check if the form elements type is defined within the form setup.
339 *
340 * @param ValidationDto $dto
341 * @return bool
342 * @internal
343 */
344 public function isFormElementTypeDefinedInFormSetup(ValidationDto $dto): bool
345 {
346 $prototypeConfiguration = $this->getPrototypeConfiguration($dto->getPrototypeName());
347 return ArrayUtility::isValidPath(
348 $prototypeConfiguration,
349 'formElementsDefinition.' . $dto->getFormElementType(),
350 '.'
351 );
352 }
353
354 /**
355 * Collect all the form editor configurations which are needed to check if a
356 * form definition property can be written or not.
357 *
358 * @param string $prototypeName
359 * @return array
360 */
361 protected function buildFormDefinitionValidationConfigurationFromFormEditorSetup(string $prototypeName): array
362 {
363 $cacheKey = implode('_', ['buildFormDefinitionValidationConfigurationFromFormEditorSetup', $prototypeName]);
364 $configuration = $this->getCacheEntry($cacheKey);
365
366 if ($configuration === null) {
367 $prototypeConfiguration = $this->getPrototypeConfiguration($prototypeName);
368 $extractorDto = GeneralUtility::makeInstance(ExtractorDto::class, $prototypeConfiguration);
369
370 GeneralUtility::makeInstance(ArrayProcessor::class, $prototypeConfiguration)->forEach(
371 GeneralUtility::makeInstance(
372 ArrayProcessing::class,
373 'formElementPropertyPaths',
374 '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.(propertyPath|.*\.propertyPath)$',
375 GeneralUtility::makeInstance(PropertyPathsExtractor::class, $extractorDto)
376 ),
377
378 GeneralUtility::makeInstance(
379 ArrayProcessing::class,
380 'formElementAdditionalElementPropertyPaths',
381 '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.additionalElementPropertyPaths\.([\d]+)',
382 GeneralUtility::makeInstance(AdditionalElementPropertyPathsExtractor::class, $extractorDto)
383 ),
384
385 GeneralUtility::makeInstance(
386 ArrayProcessing::class,
387 'formElementRelativeMultiValueProperties',
388 '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.templateName$',
389 GeneralUtility::makeInstance(MultiValuePropertiesExtractor::class, $extractorDto)
390 ),
391
392 GeneralUtility::makeInstance(
393 ArrayProcessing::class,
394 'formElementPredefinedDefaults',
395 '^formElementsDefinition\.(.*)\.formEditor\.predefinedDefaults\.(.+)$',
396 GeneralUtility::makeInstance(PredefinedDefaultsExtractor::class, $extractorDto)
397 ),
398
399 GeneralUtility::makeInstance(
400 ArrayProcessing::class,
401 'formElementCreatable',
402 '^formElementsDefinition\.(.*)\.formEditor.group$',
403 GeneralUtility::makeInstance(IsCreatableFormElementExtractor::class, $extractorDto)
404 ),
405
406 GeneralUtility::makeInstance(
407 ArrayProcessing::class,
408 'propertyCollectionCreatable',
409 '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.templateName$',
410 GeneralUtility::makeInstance(IsCreatablePropertyCollectionElementExtractor::class, $extractorDto)
411 ),
412
413 GeneralUtility::makeInstance(
414 ArrayProcessing::class,
415 'propertyCollectionPropertyPaths',
416 '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.(propertyPath|.*\.propertyPath)$',
417 GeneralUtility::makeInstance(CollectionPropertyPathsExtractor::class, $extractorDto)
418 ),
419
420 GeneralUtility::makeInstance(
421 ArrayProcessing::class,
422 'propertyCollectionAdditionalElementPropertyPaths',
423 '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.additionalElementPropertyPaths\.([\d]+)',
424 GeneralUtility::makeInstance(AdditionalElementPropertyPathsExtractor::class, $extractorDto)
425 ),
426
427 GeneralUtility::makeInstance(
428 ArrayProcessing::class,
429 'propertyCollectionRelativeMultiValueProperties',
430 '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.templateName$',
431 GeneralUtility::makeInstance(CollectionMultiValuePropertiesExtractor::class, $extractorDto)
432 ),
433
434 GeneralUtility::makeInstance(
435 ArrayProcessing::class,
436 'propertyCollectionPredefinedDefaults',
437 '^(validatorsDefinition|finishersDefinition)\.(.*)\.formEditor\.predefinedDefaults\.(.+)$',
438 GeneralUtility::makeInstance(CollectionPredefinedDefaultsExtractor::class, $extractorDto)
439 )
440 );
441 $configuration = $extractorDto->getResult();
442
443 $configuration = $this->translateValues($prototypeConfiguration, $configuration);
444
445 $configuration = $this->executeBuildFormDefinitionValidationConfigurationHooks(
446 $prototypeName,
447 $configuration
448 );
449
450 $this->setCacheEntry($cacheKey, $configuration);
451 }
452
453 return $configuration;
454 }
455
456 /**
457 * If you use a custom form editor "inspector editor" implementation which does not define the writable
458 * property paths by one of the described inspector editor properties (e.g "propertyPath") within
459 * the form setup, you must provide the writable property paths with a hook.
460 *
461 * @see $this->isFormElementPropertyDefinedInFormEditorSetup()
462 * @see $this->isPropertyCollectionPropertyDefinedInFormEditorSetup()
463 * Connect to the hook:
464 * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'][] = \Vendor\YourNamespace\YourClass::class;
465 * Use the hook:
466 * public function addAdditionalPropertyPaths(\TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto $validationDto): array
467 * {
468 * $textValidationDto = $validationDto->withFormElementType('Text');
469 * $textValidatorsValidationDto = $textValidationDto->withPropertyCollectionName('validators');
470 * $dateValidationDto = $validationDto->withFormElementType('Date');
471 * $propertyPaths = [
472 * $textValidationDto->withPropertyPath('properties.my.custom.property'),
473 * $textValidationDto->withPropertyPath('properties.my.other.custom.property'),
474 * $textValidatorsValidationDto->withPropertyCollectionElementIdentifier('StringLength')->withPropertyPath('options.custom.property'),
475 * $textValidatorsValidationDto->withPropertyCollectionElementIdentifier('CustomValidator')->withPropertyPath('options.other.custom.property'),
476 * $dateValidationDto->withPropertyPath('properties.custom.property'),
477 * // ..
478 * ];
479 * return $propertyPaths;
480 * }
481 * @param string $prototypeName
482 * @param array $configuration
483 * @return array
484 * @throws PropertyException
485 */
486 protected function executeBuildFormDefinitionValidationConfigurationHooks(
487 string $prototypeName,
488 array $configuration
489 ): array {
490 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'] ?? [] as $className) {
491 $hookObj = GeneralUtility::makeInstance($className);
492 if (method_exists($hookObj, 'addAdditionalPropertyPaths')) {
493 $validationDto = GeneralUtility::makeInstance(ValidationDto::class, $prototypeName);
494 $propertyPathsFromHook = $hookObj->addAdditionalPropertyPaths($validationDto);
495 if (!is_array($propertyPathsFromHook)) {
496 $message = 'Return value of "%s->addAdditionalPropertyPaths() must be type "array"';
497 throw new PropertyException(sprintf($message, $className), 1528633966);
498 }
499 $configuration = $this->addAdditionalPropertyPathsFromHook(
500 $className,
501 $prototypeName,
502 $propertyPathsFromHook,
503 $configuration
504 );
505 }
506 }
507
508 return $configuration;
509 }
510
511 /**
512 * @param string $hookClassName
513 * @param string $prototypeName
514 * @param array $propertyPathsFromHook
515 * @param array $configuration
516 * @return array
517 * @throws PropertyException
518 */
519 protected function addAdditionalPropertyPathsFromHook(
520 string $hookClassName,
521 string $prototypeName,
522 array $propertyPathsFromHook,
523 array $configuration
524 ): array {
525 foreach ($propertyPathsFromHook as $index => $validationDto) {
526 if (!($validationDto instanceof ValidationDto)) {
527 $message = 'Return value of "%s->addAdditionalPropertyPaths()[%s] must be an instance of "%s"';
528 throw new PropertyException(
529 sprintf($message, $hookClassName, $index, ValidationDto::class),
530 1528633966
531 );
532 }
533
534 if ($validationDto->getPrototypeName() !== $prototypeName) {
535 $message = 'The prototype name "%s" does not match "%s" on "%s->addAdditionalPropertyPaths()[%s]';
536 throw new PropertyException(
537 sprintf(
538 $message,
539 $validationDto->getPrototypeName(),
540 $prototypeName,
541 $hookClassName,
542 $index,
543 ValidationDto::class
544 ),
545 1528634966
546 );
547 }
548
549 $formElementType = $validationDto->getFormElementType();
550 if (!$this->isFormElementTypeDefinedInFormSetup($validationDto)) {
551 $message = 'Form element type "%s" does not exists in prototype configuration "%s"';
552 throw new PropertyException(
553 sprintf($message, $formElementType, $validationDto->getPrototypeName()),
554 1528633967
555 );
556 }
557
558 if ($validationDto->hasPropertyCollectionName() &&
559 $validationDto->hasPropertyCollectionElementIdentifier()) {
560 $propertyCollectionName = $validationDto->getPropertyCollectionName();
561 $propertyCollectionElementIdentifier = $validationDto->getPropertyCollectionElementIdentifier();
562
563 if ($propertyCollectionName !== 'finishers' && $propertyCollectionName !== 'validators') {
564 $message = 'The property collection name "%s" for form element "%s" must be "finishers" or "validators"';
565 throw new PropertyException(
566 sprintf($message, $propertyCollectionName, $formElementType),
567 1528636941
568 );
569 }
570
571 $configuration['formElements'][$formElementType]['collections'][$propertyCollectionName][$propertyCollectionElementIdentifier]['additionalPropertyPaths'][]
572 = $validationDto->getPropertyPath();
573 } else {
574 $configuration['formElements'][$formElementType]['additionalPropertyPaths'][]
575 = $validationDto->getPropertyPath();
576 }
577 }
578
579 return $configuration;
580 }
581
582 /**
583 * @param string $propertyPath
584 * @param array $subConfig
585 * @return bool
586 */
587 protected function isPropertyDefinedInFormEditorSetup(string $propertyPath, array $subConfig): bool
588 {
589 if (empty($subConfig)) {
590 return false;
591 }
592 if (
593 in_array($propertyPath, $subConfig['propertyPaths'] ?? [], true)
594 || in_array($propertyPath, $subConfig['additionalElementPropertyPaths'] ?? [], true)
595 || in_array($propertyPath, $subConfig['additionalPropertyPaths'] ?? [], true)
596 ) {
597 return true;
598 }
599 foreach ($subConfig['multiValueProperties'] ?? [] as $relativeMultiValueProperty) {
600 if (strpos($propertyPath, $relativeMultiValueProperty) === 0) {
601 return true;
602 }
603 }
604
605 return false;
606 }
607
608 /**
609 * @param array $prototypeConfiguration
610 * @param array $configuration
611 * @return array
612 */
613 protected function translateValues(array $prototypeConfiguration, array $configuration): array
614 {
615 if (isset($configuration['formElements'])) {
616 $configuration['formElements'] = $this->translatePredefinedDefaults(
617 $prototypeConfiguration,
618 $configuration['formElements']
619 );
620 }
621
622 foreach ($configuration['collections'] ?? [] as $name => $collections) {
623 $configuration['collections'][$name] = $this->translatePredefinedDefaults($prototypeConfiguration, $collections);
624 }
625 return $configuration;
626 }
627
628 /**
629 * @param array $prototypeConfiguration
630 * @param array $configuration
631 * @return array
632 */
633 protected function translatePredefinedDefaults(array $prototypeConfiguration, array $formElements): array
634 {
635 foreach ($formElements ?? [] as $name => $formElement) {
636 if (!isset($formElement['predefinedDefaults'])) {
637 continue;
638 }
639 $formElement['predefinedDefaults'] = $this->getTranslationService()->translateValuesRecursive(
640 $formElement['predefinedDefaults'],
641 $prototypeConfiguration['formEditor']['translationFile'] ?? null
642 );
643 $formElements[$name] = $formElement;
644 }
645 return $formElements;
646 }
647
648 /**
649 * @param string $cacheKey
650 * @return mixed
651 */
652 protected function getCacheEntry(string $cacheKey)
653 {
654 if (isset($this->firstLevelCache[$cacheKey])) {
655 return $this->firstLevelCache[$cacheKey];
656 }
657 return $this->getCacheFrontend()->has('form_' . $cacheKey)
658 ? $this->getCacheFrontend()->get('form_' . $cacheKey)
659 : null;
660 }
661
662 /**
663 * @param string $cacheKey
664 * @param mixed $value
665 */
666 protected function setCacheEntry(string $cacheKey, $value): void
667 {
668 $this->getCacheFrontend()->set('form_' . $cacheKey, $value);
669 $this->firstLevelCache[$cacheKey] = $value;
670 }
671
672 /**
673 * @return TranslationService
674 */
675 protected function getTranslationService(): TranslationService
676 {
677 return $this->translationService instanceof TranslationService
678 ? $this->translationService
679 : TranslationService::getInstance();
680 }
681
682 /**
683 * @return FrontendInterface
684 */
685 protected function getCacheFrontend(): FrontendInterface
686 {
687 return GeneralUtility::makeInstance(CacheManager::class)->getCache('assets');
688 }
689 }