Passed
Push — master ( a7eef0...bd92b3 )
by
unknown
28:58 queued 15:52
created

ConfigurationService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
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
namespace TYPO3\CMS\Form\Domain\Configuration;
19
20
use TYPO3\CMS\Core\Cache\CacheManager;
21
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
22
use TYPO3\CMS\Core\SingletonInterface;
23
use TYPO3\CMS\Core\Utility\ArrayUtility;
24
use TYPO3\CMS\Core\Utility\GeneralUtility;
25
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
26
use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
27
use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
28
use TYPO3\CMS\Form\Domain\Configuration\Exception\PrototypeNotFoundException;
29
use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto;
30
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AdditionalElementPropertyPathsExtractor;
31
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\ExtractorDto;
32
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\IsCreatableFormElementExtractor;
33
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\MultiValuePropertiesExtractor;
34
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\PredefinedDefaultsExtractor;
35
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\PropertyPathsExtractor;
36
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\SelectOptionsExtractor;
37
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\IsCreatablePropertyCollectionElementExtractor;
38
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\MultiValuePropertiesExtractor as CollectionMultiValuePropertiesExtractor;
39
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\PredefinedDefaultsExtractor as CollectionPredefinedDefaultsExtractor;
40
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\PropertyPathsExtractor as CollectionPropertyPathsExtractor;
41
use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\SelectOptionsExtractor as CollectionSelectOptionsExtractor;
42
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface;
43
use TYPO3\CMS\Form\Service\TranslationService;
44
45
/**
46
 * Helper for configuration settings
47
 * Scope: frontend / backend
48
 */
49
class ConfigurationService implements SingletonInterface
50
{
51
52
    /**
53
     * @var array
54
     */
55
    protected $formSettings;
56
57
    /**
58
     * @var array
59
     */
60
    protected $firstLevelCache = [];
61
62
    /**
63
     * @var TranslationService
64
     */
65
    protected $translationService;
66
67
    public function __construct(ConfigurationManagerInterface $configurationManager)
68
    {
69
        $this->formSettings = $configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_YAML_SETTINGS, 'form');
70
    }
71
72
    /**
73
     * Get the prototype configuration
74
     *
75
     * @param string $prototypeName name of the prototype to get the configuration for
76
     * @return array the prototype configuration
77
     * @throws PrototypeNotFoundException if prototype with the name $prototypeName was not found
78
     */
79
    public function getPrototypeConfiguration(string $prototypeName): array
80
    {
81
        if (!isset($this->formSettings['prototypes'][$prototypeName])) {
82
            throw new PrototypeNotFoundException(
83
                sprintf('The Prototype "%s" was not found.', $prototypeName),
84
                1475924277
85
            );
86
        }
87
        return $this->formSettings['prototypes'][$prototypeName];
88
    }
89
90
    /**
91
     * Return all prototype names which are defined within "formManager.selectablePrototypesConfiguration.*.identifier"
92
     *
93
     * @return array
94
     * @internal
95
     */
96
    public function getSelectablePrototypeNamesDefinedInFormEditorSetup(): array
97
    {
98
        $returnValue = GeneralUtility::makeInstance(
99
            ArrayProcessor::class,
100
            $this->formSettings['formManager']['selectablePrototypesConfiguration'] ?? []
101
        )->forEach(
102
            GeneralUtility::makeInstance(
103
                ArrayProcessing::class,
104
                'selectablePrototypeNames',
105
                '^([\d]+)\.identifier$',
106
                function ($_, $value) {
107
                    return $value;
108
                }
109
            )
110
        );
111
112
        return array_values($returnValue['selectablePrototypeNames'] ?? []);
113
    }
114
115
    /**
116
     * Check if a form element property is defined in the form setup.
117
     * If a form element property is defined in the form setup then it
118
     * means that the form element property can be written by the form editor.
119
     * A form element property can be written if the property path is defined within
120
     * the following form editor properties:
121
     * * formElementsDefinition.<formElementType>.formEditor.editors.<index>.propertyPath
122
     * * formElementsDefinition.<formElementType>.formEditor.editors.<index>.*.propertyPath
123
     * * formElementsDefinition.<formElementType>.formEditor.editors.<index>.additionalElementPropertyPaths
124
     * * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.additionalElementPropertyPaths
125
     * If a form editor property "templateName" is
126
     * "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor"
127
     * it means that the form editor property "propertyPath" is interpreted as a so called "multiValueProperty".
128
     * A "multiValueProperty" can contain any subproperties relative to the value from "propertyPath" which are valid.
129
     * If "formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName = Inspector-PropertyGridEditor"
130
     * and
131
     * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.propertyPath = options.xxx"
132
     * then (for example) "options.xxx.yyy" is a valid property path to write.
133
     * If you use a custom form editor "inspector editor" implementation which does not define the writable
134
     * property paths by one of the above described inspector editor properties (e.g "propertyPath") within
135
     * the form setup, you must provide the writable property paths with a hook.
136
     *
137
     * @see executeBuildFormDefinitionValidationConfigurationHooks()
138
     * @param ValidationDto $dto
139
     * @return bool
140
     * @internal
141
     */
142
    public function isFormElementPropertyDefinedInFormEditorSetup(ValidationDto $dto): bool
143
    {
144
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
145
            $dto->getPrototypeName()
146
        );
147
148
        $subConfig = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()] ?? [];
149
        return $this->isPropertyDefinedInFormEditorSetup($dto->getPropertyPath(), $subConfig);
150
    }
151
152
    /**
153
     * Check if a form elements finisher|validator property is defined in the form setup.
154
     * If a form elements finisher|validator property is defined in the form setup then it
155
     * means that the form elements finisher|validator property can be written by the form editor.
156
     * A form elements finisher|validator property can be written if the property path is defined within
157
     * the following form editor properties:
158
     * * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath
159
     * * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.*.propertyPath
160
     * If a form elements finisher|validator property "templateName" is
161
     * "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor"
162
     * it means that the form editor property "propertyPath" is interpreted as a so called "multiValueProperty".
163
     * A "multiValueProperty" can contain any subproperties relative to the value from "propertyPath" which are valid.
164
     * If "formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.templateName = Inspector-PropertyGridEditor"
165
     * and
166
     * "formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath = options.xxx"
167
     * that (for example) "options.xxx.yyy" is a valid property path to write.
168
     * If you use a custom form editor "inspector editor" implementation which not defines the writable
169
     * property paths by one of the above described inspector editor properties (e.g "propertyPath") within
170
     * the form setup, you must provide the writable property paths with a hook.
171
     *
172
     * @see executeBuildFormDefinitionValidationConfigurationHooks()
173
     * @param ValidationDto $dto
174
     * @return bool
175
     * @internal
176
     */
177
    public function isPropertyCollectionPropertyDefinedInFormEditorSetup(ValidationDto $dto): bool
178
    {
179
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
180
            $dto->getPrototypeName()
181
        );
182
        $subConfig = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()] ?? [];
183
184
        return $this->isPropertyDefinedInFormEditorSetup($dto->getPropertyPath(), $subConfig);
185
    }
186
187
    /**
188
     * If a form element editor has a property called "selectOptions"
189
     * (e.g. editors with templateName "Inspector-SingleSelectEditor" or "Inspector-MultiSelectEditor")
190
     * then only the defined values within the selectOptions are allowed to be written
191
     * by the form editor.
192
     *
193
     * @param ValidationDto $dto
194
     * @return bool
195
     * @internal
196
     */
197
    public function formElementPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup(
198
        ValidationDto $dto
199
    ): bool {
200
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
201
            $dto->getPrototypeName()
202
        );
203
204
        $propertyPath = $this->getBasePropertyPathFromMultiValueFormElementProperty($dto);
205
        return isset(
206
            $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['selectOptions'][$propertyPath]
207
        );
208
    }
209
210
    /**
211
     * Get the "selectOptions" value for a form element property from the form setup.
212
     *
213
     * @param ValidationDto $dto
214
     * @return array
215
     * @param bool $translated
216
     * @throws PropertyException
217
     * @internal
218
     */
219
    public function getAllowedValuesForFormElementPropertyFromFormEditorSetup(
220
        ValidationDto $dto,
221
        bool $translated = true
222
    ): array {
223
        if (!$this->formElementPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup($dto)) {
224
            throw new PropertyException(
225
                sprintf(
226
                    'No selectOptions found for form element type "%s" and property path "%s"',
227
                    $dto->getFormElementType(),
228
                    $dto->getPropertyPath()
229
                ),
230
                1614264312
231
            );
232
        }
233
234
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
235
            $dto->getPrototypeName()
236
        );
237
238
        $property = $translated ? 'selectOptions' : 'untranslatedSelectOptions';
239
        $propertyPath = $this->getBasePropertyPathFromMultiValueFormElementProperty($dto);
240
        return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()][$property][$propertyPath];
241
    }
242
243
    /**
244
     * If a form elements finisher|validator editor has a property called "selectOptions"
245
     * (e.g. editors with templateName "Inspector-SingleSelectEditor" or "Inspector-MultiSelectEditor")
246
     * then only the defined values within the selectOptions are allowed to be written
247
     * by the form editor.
248
     *
249
     * @param ValidationDto $dto
250
     * @return bool
251
     * @internal
252
     */
253
    public function propertyCollectionPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup(
254
        ValidationDto $dto
255
    ): bool {
256
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
257
            $dto->getPrototypeName()
258
        );
259
260
        $propertyPath = $this->getBasePropertyPathFromMultiValuePropertyCollectionElement($dto);
261
        return isset(
262
            $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['selectOptions'][$propertyPath]
263
        );
264
    }
265
266
    /**
267
     * Get the "selectOptions" value for a form elements finisher|validator property from the form setup.
268
     *
269
     * @param ValidationDto $dto
270
     * @param bool $translated
271
     * @return array
272
     * @throws PropertyException
273
     * @internal
274
     */
275
    public function getAllowedValuesForPropertyCollectionPropertyFromFormEditorSetup(
276
        ValidationDto $dto,
277
        bool $translated = true
278
    ): array {
279
        if (!$this->propertyCollectionPropertyHasLimitedAllowedValuesDefinedWithinFormEditorSetup($dto)) {
280
            throw new PropertyException(
281
                sprintf(
282
                    'No selectOptions found for property collection "%s" and identifier "%s" and property path "%s"',
283
                    $dto->getPropertyCollectionName(),
284
                    $dto->getPropertyCollectionElementIdentifier(),
285
                    $dto->getPropertyPath()
286
                ),
287
                1614264313
288
            );
289
        }
290
291
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
292
            $dto->getPrototypeName()
293
        );
294
295
        $property = $translated ? 'selectOptions' : 'untranslatedSelectOptions';
296
        $propertyPath = $this->getBasePropertyPathFromMultiValuePropertyCollectionElement($dto);
297
        return $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()][$property][$propertyPath];
298
    }
299
300
    /**
301
     * @param ValidationDto $dto
302
     * @return string
303
     */
304
    protected function getBasePropertyPathFromMultiValueFormElementProperty(
305
        ValidationDto $dto
306
    ): string {
307
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
308
            $dto->getPrototypeName()
309
        );
310
311
        $propertyPath = $dto->getPropertyPath();
312
        $multiValueProperties = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['multiValueProperties'] ?? [];
313
        foreach ($multiValueProperties as $multiValueProperty) {
314
            if (strpos($propertyPath, $multiValueProperty) === 0) {
315
                $propertyPath = $multiValueProperty;
316
                continue;
317
            }
318
        }
319
320
        return $propertyPath;
321
    }
322
323
    /**
324
     * @param ValidationDto $dto
325
     * @return string
326
     */
327
    protected function getBasePropertyPathFromMultiValuePropertyCollectionElement(
328
        ValidationDto $dto
329
    ): string {
330
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
331
            $dto->getPrototypeName()
332
        );
333
334
        $propertyPath = $dto->getPropertyPath();
335
        $multiValueProperties = $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['multiValueProperties'] ?? [];
336
        foreach ($multiValueProperties as $multiValueProperty) {
337
            if (strpos($propertyPath, $multiValueProperty) === 0) {
338
                $propertyPath = $multiValueProperty;
339
                continue;
340
            }
341
        }
342
343
        return $propertyPath;
344
    }
345
346
    /**
347
     * Check if a form element property is defined in "predefinedDefaults" in the form setup.
348
     * If a form element property is defined in the "predefinedDefaults" in the form setup then it
349
     * means that the form element property can be written by the form editor.
350
     * A form element default property is defined within the following form editor properties:
351
     * * formElementsDefinition.<formElementType>.formEditor.predefinedDefaults.<propertyPath> = "default value"
352
     *
353
     * @param ValidationDto $dto
354
     * @return bool
355
     * @internal
356
     */
357
    public function isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup(
358
        ValidationDto $dto
359
    ): bool {
360
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
361
            $dto->getPrototypeName()
362
        );
363
        return isset(
364
            $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['predefinedDefaults'][$dto->getPropertyPath()]
365
        );
366
    }
367
368
    /**
369
     * Get the "predefinedDefaults" value for a form element property from the form setup.
370
     * A form element default property is defined within the following form editor properties:
371
     * * formElementsDefinition.<formElementType>.formEditor.predefinedDefaults.<propertyPath> = "default value"
372
     *
373
     * @param ValidationDto $dto
374
     * @param bool $translated
375
     * @return mixed
376
     * @throws PropertyException
377
     * @internal
378
     */
379
    public function getFormElementPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto, bool $translated = true)
380
    {
381
        if (!$this->isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
382
            throw new PropertyException(
383
                sprintf(
384
                    'No predefinedDefaults found for form element type "%s" and property path "%s"',
385
                    $dto->getFormElementType(),
386
                    $dto->getPropertyPath()
387
                ),
388
                1528578401
389
            );
390
        }
391
392
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
393
            $dto->getPrototypeName()
394
        );
395
396
        $property = $translated ? 'predefinedDefaults' : 'untranslatedPredefinedDefaults';
397
        return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()][$property][$dto->getPropertyPath()];
398
    }
399
400
    /**
401
     * Check if a form elements finisher|validator property is defined in "predefinedDefaults" in the form setup.
402
     * If a form elements finisher|validator property is defined in "predefinedDefaults" in the form setup then it
403
     * means that the form elements finisher|validator property can be written by the form editor.
404
     * A form elements finisher|validator default property is defined within the following form editor properties:
405
     * * <validatorsDefinition|finishersDefinition>.<index>.formEditor.predefinedDefaults.<propertyPath> = "default value"
406
     *
407
     * @param ValidationDto $dto
408
     * @return bool
409
     * @internal
410
     */
411
    public function isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup(
412
        ValidationDto $dto
413
    ): bool {
414
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
415
            $dto->getPrototypeName()
416
        );
417
        return isset(
418
            $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['predefinedDefaults'][$dto->getPropertyPath()]
419
        );
420
    }
421
422
    /**
423
     * Get the "predefinedDefaults" value for a form elements finisher|validator property from the form setup.
424
     * A form elements finisher|validator default property is defined within the following form editor properties:
425
     * * <validatorsDefinition|finishersDefinition>.<index>.formEditor.predefinedDefaults.<propertyPath> = "default value"
426
     *
427
     * @param ValidationDto $dto
428
     * @param bool $translated
429
     * @return mixed
430
     * @throws PropertyException
431
     * @internal
432
     */
433
    public function getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto, bool $translated = true)
434
    {
435
        if (!$this->isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
436
            throw new PropertyException(
437
                sprintf(
438
                    'No predefinedDefaults found for property collection "%s" and identifier "%s" and property path "%s"',
439
                    $dto->getPropertyCollectionName(),
440
                    $dto->getPropertyCollectionElementIdentifier(),
441
                    $dto->getPropertyPath()
442
                ),
443
                1528578402
444
            );
445
        }
446
447
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
448
            $dto->getPrototypeName()
449
        );
450
451
        $property = $translated ? 'predefinedDefaults' : 'untranslatedPredefinedDefaults';
452
        return $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()][$property][$dto->getPropertyPath()];
453
    }
454
455
    /**
456
     * Check if the form element is creatable through the form editor.
457
     * A form element is creatable if the following properties are set:
458
     *  * formElementsDefinition.<formElementType>.formEditor.group
459
     *  * formElementsDefinition.<formElementType>.formEditor.groupSorting
460
     * And the value from "formElementsDefinition.<formElementType>.formEditor.group" is
461
     * one of the keys within "formEditor.formElementGroups"
462
     *
463
     * @param ValidationDto $dto
464
     * @return bool
465
     * @internal
466
     */
467
    public function isFormElementTypeCreatableByFormEditor(ValidationDto $dto): bool
468
    {
469
        if ($dto->getFormElementType() === 'Form') {
470
            return true;
471
        }
472
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
473
            $dto->getPrototypeName()
474
        );
475
        return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['creatable'] ?? false;
476
    }
477
478
    /**
479
     * Check if the form elements finisher|validator is creatable through the form editor.
480
     * A form elements finisher|validator is creatable if the following conditions are true:
481
     * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName = Inspector-FinishersEditor"
482
     * or
483
     * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName = Inspector-ValidatorsEditor"
484
     * and
485
     * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.selectOptions.<index>.value = <finisherIdentifier|validatorIdentifier>"
486
     *
487
     * @param ValidationDto $dto
488
     * @return bool
489
     * @internal
490
     */
491
    public function isPropertyCollectionElementIdentifierCreatableByFormEditor(ValidationDto $dto): bool
492
    {
493
        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
494
            $dto->getPrototypeName()
495
        );
496
        return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['creatable'] ?? false;
497
    }
498
499
    /**
500
     * Check if the form elements type is defined within the form setup.
501
     *
502
     * @param ValidationDto $dto
503
     * @return bool
504
     * @internal
505
     */
506
    public function isFormElementTypeDefinedInFormSetup(ValidationDto $dto): bool
507
    {
508
        $prototypeConfiguration = $this->getPrototypeConfiguration($dto->getPrototypeName());
509
        return ArrayUtility::isValidPath(
510
            $prototypeConfiguration,
511
            'formElementsDefinition.' . $dto->getFormElementType(),
512
            '.'
513
        );
514
    }
515
516
    /**
517
     * @param array $keys
518
     * @param string $prototypeName
519
     * @return array
520
     * @internal
521
     */
522
    public function getAllBackendTranslationsForTranslationKeys(array $keys, string $prototypeName): array
523
    {
524
        $translations = [];
525
        foreach ($keys as $key) {
526
            if (!is_string($key)) {
527
                continue;
528
            }
529
530
            $translations[$key] = $this->getAllBackendTranslationsForTranslationKey($key, $prototypeName);
531
        }
532
533
        return $translations;
534
    }
535
536
    /**
537
     * @param string $key
538
     * @param string $prototypeName
539
     * @return array
540
     */
541
    public function getAllBackendTranslationsForTranslationKey(string $key, string $prototypeName): array
542
    {
543
        $prototypeConfiguration = $this->getPrototypeConfiguration($prototypeName);
544
545
        return $this->getTranslationService()->translateToAllBackendLanguages(
546
            $key,
547
            [],
548
            $prototypeConfiguration['formEditor']['translationFiles'] ?? []
549
        );
550
    }
551
552
    /**
553
     * Collect all the form editor configurations which are needed to check if a
554
     * form definition property can be written or not.
555
     *
556
     * @param string $prototypeName
557
     * @return array
558
     */
559
    protected function buildFormDefinitionValidationConfigurationFromFormEditorSetup(string $prototypeName): array
560
    {
561
        $cacheKey = implode('_', ['buildFormDefinitionValidationConfigurationFromFormEditorSetup', $prototypeName]);
562
        $configuration = $this->getCacheEntry($cacheKey);
563
564
        if ($configuration === null) {
565
            $prototypeConfiguration = $this->getPrototypeConfiguration($prototypeName);
566
            $extractorDto = GeneralUtility::makeInstance(ExtractorDto::class, $prototypeConfiguration);
567
568
            GeneralUtility::makeInstance(ArrayProcessor::class, $prototypeConfiguration)->forEach(
569
                GeneralUtility::makeInstance(
570
                    ArrayProcessing::class,
571
                    'formElementPropertyPaths',
572
                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.(propertyPath|.*\.propertyPath)$',
573
                    GeneralUtility::makeInstance(PropertyPathsExtractor::class, $extractorDto)
574
                ),
575
                GeneralUtility::makeInstance(
576
                    ArrayProcessing::class,
577
                    'formElementAdditionalElementPropertyPaths',
578
                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.additionalElementPropertyPaths\.([\d]+)',
579
                    GeneralUtility::makeInstance(AdditionalElementPropertyPathsExtractor::class, $extractorDto)
580
                ),
581
                GeneralUtility::makeInstance(
582
                    ArrayProcessing::class,
583
                    'formElementRelativeMultiValueProperties',
584
                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.templateName$',
585
                    GeneralUtility::makeInstance(MultiValuePropertiesExtractor::class, $extractorDto)
586
                ),
587
                GeneralUtility::makeInstance(
588
                    ArrayProcessing::class,
589
                    'formElementSelectOptions',
590
                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.selectOptions\.([\d]+)\.value$',
591
                    GeneralUtility::makeInstance(SelectOptionsExtractor::class, $extractorDto)
592
                ),
593
                GeneralUtility::makeInstance(
594
                    ArrayProcessing::class,
595
                    'formElementPredefinedDefaults',
596
                    '^formElementsDefinition\.(.*)\.formEditor\.predefinedDefaults\.(.+)$',
597
                    GeneralUtility::makeInstance(PredefinedDefaultsExtractor::class, $extractorDto)
598
                ),
599
                GeneralUtility::makeInstance(
600
                    ArrayProcessing::class,
601
                    'formElementCreatable',
602
                    '^formElementsDefinition\.(.*)\.formEditor.group$',
603
                    GeneralUtility::makeInstance(IsCreatableFormElementExtractor::class, $extractorDto)
604
                ),
605
                GeneralUtility::makeInstance(
606
                    ArrayProcessing::class,
607
                    'propertyCollectionCreatable',
608
                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.templateName$',
609
                    GeneralUtility::makeInstance(IsCreatablePropertyCollectionElementExtractor::class, $extractorDto)
610
                ),
611
                GeneralUtility::makeInstance(
612
                    ArrayProcessing::class,
613
                    'propertyCollectionPropertyPaths',
614
                    '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.(propertyPath|.*\.propertyPath)$',
615
                    GeneralUtility::makeInstance(CollectionPropertyPathsExtractor::class, $extractorDto)
616
                ),
617
                GeneralUtility::makeInstance(
618
                    ArrayProcessing::class,
619
                    'propertyCollectionAdditionalElementPropertyPaths',
620
                    '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.additionalElementPropertyPaths\.([\d]+)',
621
                    GeneralUtility::makeInstance(AdditionalElementPropertyPathsExtractor::class, $extractorDto)
622
                ),
623
                GeneralUtility::makeInstance(
624
                    ArrayProcessing::class,
625
                    'propertyCollectionRelativeMultiValueProperties',
626
                    '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.templateName$',
627
                    GeneralUtility::makeInstance(CollectionMultiValuePropertiesExtractor::class, $extractorDto)
628
                ),
629
                GeneralUtility::makeInstance(
630
                    ArrayProcessing::class,
631
                    'propertyCollectionSelectOptions',
632
                    '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.selectOptions\.([\d]+)\.value$',
633
                    GeneralUtility::makeInstance(CollectionSelectOptionsExtractor::class, $extractorDto)
634
                ),
635
                GeneralUtility::makeInstance(
636
                    ArrayProcessing::class,
637
                    'propertyCollectionPredefinedDefaults',
638
                    '^(validatorsDefinition|finishersDefinition)\.(.*)\.formEditor\.predefinedDefaults\.(.+)$',
639
                    GeneralUtility::makeInstance(CollectionPredefinedDefaultsExtractor::class, $extractorDto)
640
                )
641
            );
642
            $configuration = $extractorDto->getResult();
643
644
            $configuration = $this->translateValues($prototypeConfiguration, $configuration);
645
646
            $configuration = $this->executeBuildFormDefinitionValidationConfigurationHooks(
647
                $prototypeName,
648
                $configuration
649
            );
650
651
            $this->setCacheEntry($cacheKey, $configuration);
652
        }
653
654
        return $configuration;
655
    }
656
657
    /**
658
     * If you use a custom form editor "inspector editor" implementation which does not define the writable
659
     * property paths by one of the described inspector editor properties (e.g "propertyPath") within
660
     * the form setup, you must provide the writable property paths with a hook.
661
     *
662
     * @see isFormElementPropertyDefinedInFormEditorSetup()
663
     * @see isPropertyCollectionPropertyDefinedInFormEditorSetup()
664
     * Connect to the hook:
665
     * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'][] = \Vendor\YourNamespace\YourClass::class;
666
     * Use the hook:
667
     * public function addAdditionalPropertyPaths(\TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto $validationDto): array
668
     * {
669
     *     $textValidationDto = $validationDto->withFormElementType('Text');
670
     *     $textValidatorsValidationDto = $textValidationDto->withPropertyCollectionName('validators');
671
     *     $dateValidationDto = $validationDto->withFormElementType('Date');
672
     *     $propertyPaths = [
673
     *         $textValidationDto->withPropertyPath('properties.my.custom.property'),
674
     *         $textValidationDto->withPropertyPath('properties.my.other.custom.property'),
675
     *         $textValidatorsValidationDto->withPropertyCollectionElementIdentifier('StringLength')->withPropertyPath('options.custom.property'),
676
     *         $textValidatorsValidationDto->withPropertyCollectionElementIdentifier('CustomValidator')->withPropertyPath('options.other.custom.property'),
677
     *         $dateValidationDto->withPropertyPath('properties.custom.property'),
678
     *         // ..
679
     *     ];
680
     *     return $propertyPaths;
681
     * }
682
     * @param string $prototypeName
683
     * @param array $configuration
684
     * @return array
685
     * @throws PropertyException
686
     */
687
    protected function executeBuildFormDefinitionValidationConfigurationHooks(
688
        string $prototypeName,
689
        array $configuration
690
    ): array {
691
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'] ?? [] as $className) {
692
            $hookObj = GeneralUtility::makeInstance($className);
693
            if (method_exists($hookObj, 'addAdditionalPropertyPaths')) {
694
                $validationDto = GeneralUtility::makeInstance(ValidationDto::class, $prototypeName);
695
                $propertyPathsFromHook = $hookObj->addAdditionalPropertyPaths($validationDto);
696
                if (!is_array($propertyPathsFromHook)) {
697
                    $message = 'Return value of "%s->addAdditionalPropertyPaths() must be type "array"';
698
                    throw new PropertyException(sprintf($message, $className), 1528633965);
699
                }
700
                $configuration = $this->addAdditionalPropertyPathsFromHook(
701
                    $className,
702
                    $prototypeName,
703
                    $propertyPathsFromHook,
704
                    $configuration
705
                );
706
            }
707
        }
708
709
        return $configuration;
710
    }
711
712
    /**
713
     * @param string $hookClassName
714
     * @param string $prototypeName
715
     * @param array $propertyPathsFromHook
716
     * @param array $configuration
717
     * @return array
718
     * @throws PropertyException
719
     */
720
    protected function addAdditionalPropertyPathsFromHook(
721
        string $hookClassName,
722
        string $prototypeName,
723
        array $propertyPathsFromHook,
724
        array $configuration
725
    ): array {
726
        foreach ($propertyPathsFromHook as $index => $validationDto) {
727
            if (!($validationDto instanceof ValidationDto)) {
728
                $message = 'Return value of "%s->addAdditionalPropertyPaths()[%s] must be an instance of "%s"';
729
                throw new PropertyException(
730
                    sprintf($message, $hookClassName, $index, ValidationDto::class),
731
                    1528633966
732
                );
733
            }
734
735
            if ($validationDto->getPrototypeName() !== $prototypeName) {
736
                $message = 'The prototype name "%s" does not match "%s" on "%s->addAdditionalPropertyPaths()[%s]';
737
                throw new PropertyException(
738
                    sprintf(
739
                        $message,
740
                        $validationDto->getPrototypeName(),
741
                        $prototypeName,
742
                        $hookClassName,
743
                        $index,
744
                        ValidationDto::class
745
                    ),
746
                    1528634966
747
                );
748
            }
749
750
            $formElementType = $validationDto->getFormElementType();
751
            if (!$this->isFormElementTypeDefinedInFormSetup($validationDto)) {
752
                $message = 'Form element type "%s" does not exist in prototype configuration "%s"';
753
                throw new PropertyException(
754
                    sprintf($message, $formElementType, $validationDto->getPrototypeName()),
755
                    1528633967
756
                );
757
            }
758
759
            if ($validationDto->hasPropertyCollectionName() &&
760
                $validationDto->hasPropertyCollectionElementIdentifier()) {
761
                $propertyCollectionName = $validationDto->getPropertyCollectionName();
762
                $propertyCollectionElementIdentifier = $validationDto->getPropertyCollectionElementIdentifier();
763
764
                if ($propertyCollectionName !== 'finishers' && $propertyCollectionName !== 'validators') {
765
                    $message = 'The property collection name "%s" for form element "%s" must be "finishers" or "validators"';
766
                    throw new PropertyException(
767
                        sprintf($message, $propertyCollectionName, $formElementType),
768
                        1528636941
769
                    );
770
                }
771
772
                $configuration['formElements'][$formElementType]['collections'][$propertyCollectionName][$propertyCollectionElementIdentifier]['additionalPropertyPaths'][]
773
                    = $validationDto->getPropertyPath();
774
            } else {
775
                $configuration['formElements'][$formElementType]['additionalPropertyPaths'][]
776
                    = $validationDto->getPropertyPath();
777
            }
778
        }
779
780
        return $configuration;
781
    }
782
783
    /**
784
     * @param string $propertyPath
785
     * @param array $subConfig
786
     * @return bool
787
     */
788
    protected function isPropertyDefinedInFormEditorSetup(string $propertyPath, array $subConfig): bool
789
    {
790
        if (empty($subConfig)) {
791
            return false;
792
        }
793
        if (
794
            in_array($propertyPath, $subConfig['propertyPaths'] ?? [], true)
795
            || in_array($propertyPath, $subConfig['additionalElementPropertyPaths'] ?? [], true)
796
            || in_array($propertyPath, $subConfig['additionalPropertyPaths'] ?? [], true)
797
        ) {
798
            return true;
799
        }
800
        foreach ($subConfig['multiValueProperties'] ?? [] as $relativeMultiValueProperty) {
801
            if (strpos($propertyPath, $relativeMultiValueProperty) === 0) {
802
                return true;
803
            }
804
        }
805
806
        return false;
807
    }
808
809
    /**
810
     * @param array $prototypeConfiguration
811
     * @param array $configuration
812
     * @return array
813
     */
814
    protected function translateValues(array $prototypeConfiguration, array $configuration): array
815
    {
816
        if (isset($configuration['formElements'])) {
817
            $configuration['formElements'] = $this->translatePredefinedDefaults(
818
                $prototypeConfiguration,
819
                $configuration['formElements']
820
            );
821
822
            $configuration['formElements'] = $this->translateSelectOptions(
823
                $prototypeConfiguration,
824
                $configuration['formElements']
825
            );
826
        }
827
828
        foreach ($configuration['collections'] ?? [] as $name => $collections) {
829
            $configuration['collections'][$name] = $this->translatePredefinedDefaults($prototypeConfiguration, $collections);
830
            $configuration['collections'][$name] = $this->translateSelectOptions($prototypeConfiguration, $configuration['collections'][$name]);
831
        }
832
        return $configuration;
833
    }
834
835
    /**
836
     * @param array $prototypeConfiguration
837
     * @param array $formElements
838
     * @return array
839
     */
840
    protected function translatePredefinedDefaults(array $prototypeConfiguration, array $formElements): array
841
    {
842
        foreach ($formElements ?? [] as $name => $formElement) {
843
            if (!isset($formElement['predefinedDefaults'])) {
844
                continue;
845
            }
846
            $formElement['untranslatedPredefinedDefaults'] = $formElement['predefinedDefaults'];
847
            $formElement['predefinedDefaults'] = $this->getTranslationService()->translateValuesRecursive(
848
                $formElement['predefinedDefaults'],
849
                $prototypeConfiguration['formEditor']['translationFiles'] ?? []
850
            );
851
            $formElements[$name] = $formElement;
852
        }
853
        return $formElements;
854
    }
855
856
    /**
857
     * @param array $prototypeConfiguration
858
     * @param array $formElements
859
     * @return array
860
     */
861
    protected function translateSelectOptions(array $prototypeConfiguration, array $formElements): array
862
    {
863
        foreach ($formElements ?? [] as $name => $formElement) {
864
            if (empty($formElement['selectOptions']) || !is_array($formElement['selectOptions'])) {
865
                continue;
866
            }
867
            $formElement['untranslatedSelectOptions'] = $formElement['selectOptions'];
868
869
            $formElement['selectOptions'] = $this->getTranslationService()->translateValuesRecursive(
870
                $formElement['selectOptions'],
871
                $prototypeConfiguration['formEditor']['translationFiles'] ?? []
872
            );
873
            $formElements[$name] = $formElement;
874
        }
875
        return $formElements;
876
    }
877
878
    /**
879
     * @param string $cacheKey
880
     * @return mixed
881
     */
882
    protected function getCacheEntry(string $cacheKey)
883
    {
884
        if (isset($this->firstLevelCache[$cacheKey])) {
885
            return $this->firstLevelCache[$cacheKey];
886
        }
887
        $cacheValue = $this->getCacheFrontend()->get('form_' . $cacheKey);
888
        return $cacheValue === false ? null : $cacheValue;
889
    }
890
891
    /**
892
     * @param string $cacheKey
893
     * @param mixed $value
894
     */
895
    protected function setCacheEntry(string $cacheKey, $value): void
896
    {
897
        $this->getCacheFrontend()->set('form_' . $cacheKey, $value);
898
        $this->firstLevelCache[$cacheKey] = $value;
899
    }
900
901
    /**
902
     * @return TranslationService
903
     */
904
    protected function getTranslationService(): TranslationService
905
    {
906
        return $this->translationService instanceof TranslationService
0 ignored issues
show
introduced by
$this->translationService is always a sub-type of TYPO3\CMS\Form\Service\TranslationService.
Loading history...
907
            ? $this->translationService
908
            : TranslationService::getInstance();
909
    }
910
911
    /**
912
     * @return FrontendInterface
913
     */
914
    protected function getCacheFrontend(): FrontendInterface
915
    {
916
        return GeneralUtility::makeInstance(CacheManager::class)->getCache('assets');
917
    }
918
}
919