[SECURITY] Filter disallowed properties in form editor 62/57562/2
authorRalf Zimmermann <ralf.zimmermann@tritum.de>
Thu, 12 Jul 2018 09:36:23 +0000 (11:36 +0200)
committerOliver Hader <oliver.hader@typo3.org>
Thu, 12 Jul 2018 09:36:31 +0000 (11:36 +0200)
The form editor save and preview actions now check the submitted
form definition against configured possibilities within the form
editor setup.

Releases: master, 8.7
Resolves: #85044
Security-Commit: f4a1a09378ed286f3744d6a72f09bfa11a6ba87e
Security-Bulletin: TYPO3-CORE-SA-2018-003
Change-Id: Ibf6083ab98b9fe73effe217380f555892c9c6bb0
Reviewed-on: https://review.typo3.org/57562
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
45 files changed:
typo3/sysext/core/Documentation/Changelog/8.7.x/Important-85044-FilterDisallowedPropertiesInFormEditor.rst [new file with mode: 0644]
typo3/sysext/form/Classes/Controller/FormEditorController.php
typo3/sysext/form/Classes/Domain/Configuration/ArrayProcessing/ArrayProcessing.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/ArrayProcessing/ArrayProcessor.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/ConfigurationService.php
typo3/sysext/form/Classes/Domain/Configuration/Exception/ArrayProcessorException.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/Exception/PropertyException.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AbstractConverter.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataConverter.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataToFormElementPropertyConverter.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataToPropertyCollectionElementConverter.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/ConverterDto.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/ConverterInterface.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/RemoveHmacDataConverter.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/AbstractValidator.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CollectionBasedValidator.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CreatableFormElementPropertiesValidator.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CreatablePropertyCollectionElementPropertiesValidator.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ElementBasedValidator.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/FormElementHmacDataValidator.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/PropertyCollectionElementHmacDataValidator.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ValidationDto.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ValidatorInterface.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinitionConversionService.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FormDefinitionValidationService.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/AbstractExtractor.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/AdditionalElementPropertyPathsExtractor.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/ExtractorDto.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/ExtractorInterface.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/IsCreatableFormElementExtractor.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/MultiValuePropertiesExtractor.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/PredefinedDefaultsExtractor.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/PropertyPathsExtractor.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/IsCreatablePropertyCollectionElementExtractor.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/MultiValuePropertiesExtractor.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/PredefinedDefaultsExtractor.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/PropertyPathsExtractor.php [new file with mode: 0644]
typo3/sysext/form/Classes/Mvc/Property/TypeConverter/FormDefinitionArrayConverter.php
typo3/sysext/form/Tests/Unit/Controller/FormEditorControllerTest.php
typo3/sysext/form/Tests/Unit/Domain/Configuration/ConfigurationServiceTest.php
typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinition/Validators/CreatableFormElementPropertiesValidatorTest.php [new file with mode: 0644]
typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinition/Validators/CreatablePropertyCollectionElementPropertiesValidatorTest.php [new file with mode: 0644]
typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinitionConversionServiceTest.php [new file with mode: 0644]
typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinitionValidationServiceTest.php [new file with mode: 0644]
typo3/sysext/form/Tests/Unit/Mvc/Property/TypeConverter/FormDefinitionArrayConverterTest.php

diff --git a/typo3/sysext/core/Documentation/Changelog/8.7.x/Important-85044-FilterDisallowedPropertiesInFormEditor.rst b/typo3/sysext/core/Documentation/Changelog/8.7.x/Important-85044-FilterDisallowedPropertiesInFormEditor.rst
new file mode 100644 (file)
index 0000000..8f57e0e
--- /dev/null
@@ -0,0 +1,109 @@
+.. include:: ../../Includes.txt
+
+===============================================================
+Important: #85044 - Filter disallowed properties in form editor
+===============================================================
+
+See :issue:`85044`
+
+Description
+===========
+
+The form editor save and preview actions now check the submitted form definition against configured possibilities within the form editor setup.
+
+If a form element property is defined in the form editor setup then it means that the form element property can be written by the form editor.
+A form element property can be written if the property path is defined within the following form editor properties:
+
+* :yaml:`formElementsDefinition.<formElementType>.formEditor.editors.<index>.propertyPath`
+* :yaml:`formElementsDefinition.<formElementType>.formEditor.editors.<index>.*.propertyPath`
+* :yaml:`formElementsDefinition.<formElementType>.formEditor.editors.<index>.additionalElementPropertyPaths`
+* :yaml:`formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.additionalElementPropertyPaths`
+
+If a form editor property :yaml:`templateName` is "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor"
+it means that the form editor property :yaml:`propertyPath` is interpreted as a so called "multiValueProperty".
+A "multiValueProperty" can contain any subproperties relative to the value from :yaml:`propertyPath` which are valid.
+If :yaml:`formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName` = "Inspector-PropertyGridEditor" and :yaml:`formElementsDefinition.<formElementType>.formEditor.editors.<index>.propertyPath` = "options.xxx"
+then (for example) "options.xxx.yyy" is a valid property path to write.
+
+If a form elements finisher|validator property is defined in the form editor setup then it means that the form elements finisher|validator property can be written by the form editor.
+A form elements finisher|validator property can be written if the property path is defined within the following form editor properties:
+
+* :yaml:`formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath`
+* :yaml:`formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.*.propertyPath`
+
+If a form elements finisher|validator property :yaml:`templateName` is "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor"
+it means that the form editor property :yaml:`propertyPath` is interpreted as a so called "multiValueProperty".
+A "multiValueProperty" can contain any subproperties relative to the value from :yaml:`propertyPath` which are valid.
+If :yaml:`formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.templateName` = "Inspector-PropertyGridEditor"
+and :yaml:`formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath` = "options.xxx"
+that (for example) "options.xxx.yyy" is a valid property path to write.
+
+If you use a custom form editor JavaScript "inspector editor" implementation (see https://docs.typo3.org/typo3cms/extensions/form/Concepts/FormEditor/Index.html#inspector)
+which does not define the writable property paths by one of the above described inspector editor properties (e.g :yaml:`propertyPath`) within the form setup,
+you must provide the writable property paths with a hook. Otherwise the editor will fail when saving.
+
+
+Connect to the hook:
+
+.. code-block:: yaml
+
+    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'][] = \Vendor\YourNamespace\YourClass::class;
+
+Use the hook:
+
+The hook must return an array with a set of ValidationDto objects.
+
+.. code-block:: yaml
+
+    /**
+     * @param \TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto $validationDto
+     * @return array
+     */
+    public function addAdditionalPropertyPaths(\TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto $validationDto): array
+    {
+        // Create a ValidationDto object for the form element type "Form" (:yaml:`formElementsDefinition.<formElementType>`).
+        $formValidationDto = $validationDto->withFormElementType('Form');
+        // Create a ValidationDto object for the finishers for the form element type "Form".
+        $formFinishersValidationDto = $formValidationDto->withPropertyCollectionName('finishers');
+
+        // Create a ValidationDto object for the form element type "Text" (:yaml:`formElementsDefinition.<formElementType>`).
+        $textValidationDto = $validationDto->withFormElementType('Text');
+        // Create a ValidationDto object for the validators for the form element type "Text".
+        $textValidatorsValidationDto = $textValidationDto->withPropertyCollectionName('validators');
+
+        // Create a ValidationDto object for the form element type "Date" (:yaml:`formElementsDefinition.<formElementType>`).
+        $dateValidationDto = $validationDto->withFormElementType('Date');
+
+        $propertyPaths = [
+            // Register the property :yaml:`renderingOptions.my.custom.property` for the form element type "Form".
+            // This property can now be written by the form editor.
+            $formValidationDto->withPropertyPath('renderingOptions.my.custom.property'),
+
+            // Register the property :yaml:`options.custom.property` for the finisher "MyCustomFinisher" for the form element type "Form".
+            // "MyCustomFinisher" must be equal to the identifier property from
+            // your custom inspector editor (:yaml:`formElementsDefinition.Form.formEditor.propertyCollections.finishers.<index>.editors.<index>.identifier`)
+            // This property can now be written by the form editor.
+            $formFinishersValidationDto->withPropertyCollectionElementIdentifier('MyCustomFinisher')->withPropertyPath('options.custom.property'),
+
+            // Register the properties :yaml:`properties.my.custom.property` and :yaml:`properties.my.other.custom.property` for the form element type "Text".
+            // This property can now be written by the form editor.
+            $textValidationDto->withPropertyPath('properties.my.custom.property'),
+            $textValidationDto->withPropertyPath('properties.my.other.custom.property'),
+
+            // Register the property :yaml:`options.custom.property` for the validator "CustomValidator" for the form element type "Text".
+            // "CustomValidator" must be equal to the identifier property from
+            // your custom inspector editor (:yaml:`formElementsDefinition.Text.formEditor.propertyCollections.validators.<index>.editors.<index>.identifier`)
+            // This property can now be written by the form editor.
+            $textValidatorsValidationDto->withPropertyCollectionElementIdentifier('CustomValidator')->withPropertyPath('options.custom.property'),
+
+            $textValidatorsValidationDto->withPropertyCollectionElementIdentifier('AnotherCustomValidator')->withPropertyPath('options.other.custom.property'),
+
+            $dateValidationDto->withPropertyPath('properties.custom.property'),
+            // ..
+        ];
+
+        return $propertyPaths;
+    }
+
+
+.. index:: Backend, ext:form
index bb1c1e5..3b53a80 100644 (file)
@@ -28,8 +28,10 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Mvc\View\JsonView;
 use TYPO3\CMS\Fluid\View\TemplateView;
 use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionConversionService;
 use TYPO3\CMS\Form\Domain\Exception\RenderingException;
 use TYPO3\CMS\Form\Domain\Factory\ArrayFormFactory;
+use TYPO3\CMS\Form\Exception;
 use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
 use TYPO3\CMS\Form\Service\TranslationService;
 use TYPO3\CMS\Form\Type\FormDefinitionArray;
@@ -75,14 +77,21 @@ class FormEditorController extends AbstractBackendController
             throw new PersistenceManagerException('Edit a extension formDefinition is not allowed.', 1478265661);
         }
 
+        $configurationService = $this->objectManager->get(ConfigurationService::class);
         $formDefinition = $this->formPersistenceManager->load($formPersistenceIdentifier);
-        $formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
-        $prototypeName = $prototypeName ?: $formDefinition['prototypeName'] ?? 'standard';
 
-        $formDefinition['prototypeName'] = $prototypeName;
-        $formDefinition = $this->filterEmptyArrays($formDefinition);
+        if ($prototypeName === null) {
+            $prototypeName = $formDefinition['prototypeName'] ?? 'standard';
+        } else {
+            // Loading a form definition with another prototype is currently not implemented but is planned in the future.
+            // This safety check is a preventive measure.
+            $selectablePrototypeNames = $configurationService->getSelectablePrototypeNamesDefinedInFormEditorSetup();
+            if (!in_array($prototypeName, $selectablePrototypeNames, true)) {
+                throw new Exception(sprintf('The prototype name "%s" is not configured within "formManager.selectablePrototypesConfiguration" ', $prototypeName), 1528625039);
+            }
+        }
 
-        $configurationService = $this->objectManager->get(ConfigurationService::class);
+        $formDefinition['prototypeName'] = $prototypeName;
         $this->prototypeConfiguration = $configurationService->getPrototypeConfiguration($prototypeName);
 
         $formDefinition = $this->transformFormDefinitionForFormEditor($formDefinition);
@@ -153,7 +162,7 @@ class FormEditorController extends AbstractBackendController
     public function saveFormAction(string $formPersistenceIdentifier, FormDefinitionArray $formDefinition)
     {
         $formDefinition = $formDefinition->getArrayCopy();
-        $formDefinition = $this->filterEmptyArrays($formDefinition);
+
         foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'] ?? [] as $className) {
             $hookObj = GeneralUtility::makeInstance($className);
             if (method_exists($hookObj, 'beforeFormSave')) {
@@ -170,6 +179,10 @@ class FormEditorController extends AbstractBackendController
 
         try {
             $this->formPersistenceManager->save($formPersistenceIdentifier, $formDefinition);
+            $configurationService = $this->objectManager->get(ConfigurationService::class);
+            $this->prototypeConfiguration = $configurationService->getPrototypeConfiguration($formDefinition['prototypeName']);
+            $formDefinition = $this->transformFormDefinitionForFormEditor($formDefinition);
+            $response['formDefinition'] = $formDefinition;
         } catch (PersistenceManagerException $e) {
             $response = [
                 'status' => 'error',
@@ -178,8 +191,6 @@ class FormEditorController extends AbstractBackendController
             ];
         }
 
-        $response['formDefinition'] = $formDefinition;
-
         $this->view->assign('response', $response);
         // saveFormAction uses the extbase JsonView::class.
         // That's why we have to set the view variables in this way.
@@ -201,9 +212,10 @@ class FormEditorController extends AbstractBackendController
     public function renderFormPageAction(FormDefinitionArray $formDefinition, int $pageIndex, string $prototypeName = null): string
     {
         $prototypeName = $prototypeName ?: $formDefinition['prototypeName'] ?? 'standard';
+        $formDefinition = $formDefinition->getArrayCopy();
 
         $formFactory = $this->objectManager->get(ArrayFormFactory::class);
-        $formDefinition = $formFactory->build($formDefinition->getArrayCopy(), $prototypeName);
+        $formDefinition = $formFactory->build($formDefinition, $prototypeName);
         $formDefinition->setRenderingOption('previewMode', true);
         $form = $formDefinition->bind($this->request, $this->response);
         $form->setCurrentSiteLanguage($this->buildFakeSiteLanguage(0, 0));
@@ -458,6 +470,7 @@ class FormEditorController extends AbstractBackendController
     }
 
     /**
+     * @todo move this to FormDefinitionConversionService
      * @param array $formDefinition
      * @return array
      */
@@ -475,7 +488,16 @@ class FormEditorController extends AbstractBackendController
             }
         }
 
-        return $this->transformMultiValueElementsForFormEditor($formDefinition, $multiValueProperties);
+        $formDefinition = $this->filterEmptyArrays($formDefinition);
+
+        // @todo: replace with rte parsing
+        $formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
+        $formDefinition = $this->transformMultiValueElementsForFormEditor($formDefinition, $multiValueProperties);
+
+        $formDefinitionConversionService = $this->getFormDefinitionConversionService();
+        $formDefinition = $formDefinitionConversionService->addHmacData($formDefinition);
+
+        return $formDefinition;
     }
 
     /**
@@ -574,6 +596,14 @@ class FormEditorController extends AbstractBackendController
     }
 
     /**
+     * @return FormDefinitionConversionService
+     */
+    protected function getFormDefinitionConversionService(): FormDefinitionConversionService
+    {
+        return GeneralUtility::makeInstance(FormDefinitionConversionService::class);
+    }
+
+    /**
      * Returns the current BE user.
      *
      * @return BackendUserAuthentication
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/ArrayProcessing/ArrayProcessing.php b/typo3/sysext/form/Classes/Domain/Configuration/ArrayProcessing/ArrayProcessing.php
new file mode 100644 (file)
index 0000000..b9daf34
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Helper for array processing
+ *
+ * Scope: frontend / backend
+ */
+class ArrayProcessing
+{
+
+    /**
+     * @var string
+     */
+    protected $identifier;
+
+    /**
+     * @var string
+     */
+    protected $expression;
+
+    /**
+     * @var callable
+     */
+    protected $processor;
+
+    /**
+     * @param string $identifier
+     * @param string $expression
+     * @param callable $processor
+     */
+    public function __construct(string $identifier, string $expression, callable $processor)
+    {
+        $this->identifier = $identifier;
+        $this->expression = $expression;
+        $this->processor = $processor;
+    }
+
+    /**
+     * @return string
+     */
+    public function getIdentifier(): string
+    {
+        return $this->identifier;
+    }
+
+    /**
+     * @return string
+     */
+    public function getExpression(): string
+    {
+        return $this->expression;
+    }
+
+    /**
+     * @return callable
+     */
+    public function getProcessor(): callable
+    {
+        return $this->processor;
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/ArrayProcessing/ArrayProcessor.php b/typo3/sysext/form/Classes/Domain/Configuration/ArrayProcessing/ArrayProcessor.php
new file mode 100644 (file)
index 0000000..f7c220e
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Form\Domain\Configuration\Exception\ArrayProcessorException;
+
+/**
+ * Helper for array processing
+ *
+ * Scope: frontend / backend
+ */
+class ArrayProcessor
+{
+
+    /**
+     * @var array
+     */
+    protected $data;
+
+    /**
+     * @param array $data
+     */
+    public function __construct(array $data)
+    {
+        $this->data = ArrayUtility::flatten($data);
+    }
+
+    /**
+     * @param ArrayProcessing[] $processings
+     * @return array
+     */
+    public function forEach(...$processings): array
+    {
+        $result = [];
+
+        $processings = $this->getValidProcessings($processings);
+        foreach ($this->data as $key => $value) {
+            foreach ($processings as $processing) {
+                // explicitly escaping non-escaped '#' which is used
+                // as PCRE delimiter in the following processing
+                $expression = preg_replace(
+                    '/(?<!\\\\)#/',
+                    '\\#',
+                    $processing->getExpression()
+                );
+
+                if (preg_match('#' . $expression . '#', $key, $matches)) {
+                    $identifier = $processing->getIdentifier();
+                    $processor = $processing->getProcessor();
+                    $result[$identifier] = $result[$identifier] ?? [];
+                    $result[$identifier][$key] = $processor($key, $value, $matches);
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * @param array $allProcessings
+     * @return ArrayProcessing[]
+     * @throws ArrayProcessorException
+     */
+    protected function getValidProcessings(array $allProcessings): array
+    {
+        $validProcessings = [];
+        $identifiers = [];
+        foreach ($allProcessings as $processing) {
+            if ($processing instanceof ArrayProcessing) {
+                if (in_array($processing->getIdentifier(), $identifiers, true)) {
+                    throw new ArrayProcessorException(
+                        'ArrayProcessing identifier must be unique.',
+                        1528638085
+                    );
+                }
+                $identifiers[] = $processing->getIdentifier();
+                $validProcessings[] = $processing;
+            }
+        }
+        return $validProcessings;
+    }
+}
index c9576dd..af34d68 100644 (file)
@@ -15,17 +15,35 @@ namespace TYPO3\CMS\Form\Domain\Configuration;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Object\ObjectManager;
+use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
+use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
+use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
 use TYPO3\CMS\Form\Domain\Configuration\Exception\PrototypeNotFoundException;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AdditionalElementPropertyPathsExtractor;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\ExtractorDto;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\IsCreatableFormElementExtractor;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\MultiValuePropertiesExtractor;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\PredefinedDefaultsExtractor;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement\PropertyPathsExtractor;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\IsCreatablePropertyCollectionElementExtractor;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\MultiValuePropertiesExtractor as CollectionMultiValuePropertiesExtractor;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\PredefinedDefaultsExtractor as CollectionPredefinedDefaultsExtractor;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement\PropertyPathsExtractor as CollectionPropertyPathsExtractor;
 use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface;
+use TYPO3\CMS\Form\Service\TranslationService;
 
 /**
  * Helper for configuration settings
- *
  * Scope: frontend / backend
  */
-class ConfigurationService
+class ConfigurationService implements SingletonInterface
 {
 
     /**
@@ -34,9 +52,19 @@ class ConfigurationService
     protected $formSettings;
 
     /**
+     * @var array
+     */
+    protected $firstLevelCache = [];
+
+    /**
+     * @var TranslationService
+     */
+    protected $translationService;
+
+    /**
      * @internal
      */
-    public function initializeObject()
+    public function initializeObject(): void
     {
         $this->formSettings = GeneralUtility::makeInstance(ObjectManager::class)
             ->get(ConfigurationManagerInterface::class)
@@ -54,8 +82,608 @@ class ConfigurationService
     public function getPrototypeConfiguration(string $prototypeName): array
     {
         if (!isset($this->formSettings['prototypes'][$prototypeName])) {
-            throw new PrototypeNotFoundException(sprintf('The Prototype "%s" was not found.', $prototypeName), 1475924277);
+            throw new PrototypeNotFoundException(
+                sprintf('The Prototype "%s" was not found.', $prototypeName),
+                1475924277
+            );
         }
         return $this->formSettings['prototypes'][$prototypeName];
     }
+
+    /**
+     * Return all prototype names which are defined within "formManager.selectablePrototypesConfiguration.*.identifier"
+     *
+     * @return array
+     * @internal
+     */
+    public function getSelectablePrototypeNamesDefinedInFormEditorSetup(): array
+    {
+        $returnValue = GeneralUtility::makeInstance(
+            ArrayProcessor::class,
+            $this->formSettings['formManager']['selectablePrototypesConfiguration'] ?? []
+        )->forEach(
+            GeneralUtility::makeInstance(
+                ArrayProcessing::class,
+                'selectablePrototypeNames',
+                '^([\d]+)\.identifier$',
+                function ($_, $value) {
+                    return $value;
+                }
+            )
+        );
+
+        return array_values($returnValue['selectablePrototypeNames'] ?? []);
+    }
+
+    /**
+     * Check if a form element property is defined in the form setup.
+     * If a form element property is defined in the form setup then it
+     * means that the form element property can be written by the form editor.
+     * A form element property can be written if the property path is defined within
+     * the following form editor properties:
+     * * formElementsDefinition.<formElementType>.formEditor.editors.<index>.propertyPath
+     * * formElementsDefinition.<formElementType>.formEditor.editors.<index>.*.propertyPath
+     * * formElementsDefinition.<formElementType>.formEditor.editors.<index>.additionalElementPropertyPaths
+     * * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.additionalElementPropertyPaths
+     * If a form editor property "templateName" is
+     * "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor"
+     * it means that the form editor property "propertyPath" is interpreted as a so called "multiValueProperty".
+     * A "multiValueProperty" can contain any subproperties relative to the value from "propertyPath" which are valid.
+     * If "formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName = Inspector-PropertyGridEditor"
+     * and
+     * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.propertyPath = options.xxx"
+     * then (for example) "options.xxx.yyy" is a valid property path to write.
+     * If you use a custom form editor "inspector editor" implementation which does not define the writable
+     * property paths by one of the above described inspector editor properties (e.g "propertyPath") within
+     * the form setup, you must provide the writable property paths with a hook.
+     *
+     * @see $this->executeBuildFormDefinitionValidationConfigurationHooks()
+     * @param ValidationDto $dto
+     * @return bool
+     * @internal
+     */
+    public function isFormElementPropertyDefinedInFormEditorSetup(ValidationDto $dto): bool
+    {
+        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
+            $dto->getPrototypeName()
+        );
+
+        $subConfig = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()] ?? [];
+        return $this->isPropertyDefinedInFormEditorSetup($dto->getPropertyPath(), $subConfig);
+    }
+
+    /**
+     * Check if a form elements finisher|validator property is defined in the form setup.
+     * If a form elements finisher|validator property is defined in the form setup then it
+     * means that the form elements finisher|validator property can be written by the form editor.
+     * A form elements finisher|validator property can be written if the property path is defined within
+     * the following form editor properties:
+     * * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath
+     * * formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.*.propertyPath
+     * If a form elements finisher|validator property "templateName" is
+     * "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor"
+     * it means that the form editor property "propertyPath" is interpreted as a so called "multiValueProperty".
+     * A "multiValueProperty" can contain any subproperties relative to the value from "propertyPath" which are valid.
+     * If "formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.templateName = Inspector-PropertyGridEditor"
+     * and
+     * "formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath = options.xxx"
+     * that (for example) "options.xxx.yyy" is a valid property path to write.
+     * If you use a custom form editor "inspector editor" implementation which not defines the writable
+     * property paths by one of the above described inspector editor properties (e.g "propertyPath") within
+     * the form setup, you must provide the writable property paths with a hook.
+     *
+     * @see $this->executeBuildFormDefinitionValidationConfigurationHooks()
+     * @param ValidationDto $dto
+     * @return bool
+     * @internal
+     */
+    public function isPropertyCollectionPropertyDefinedInFormEditorSetup(ValidationDto $dto): bool
+    {
+        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
+            $dto->getPrototypeName()
+        );
+        $subConfig = $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()] ?? [];
+
+        return $this->isPropertyDefinedInFormEditorSetup($dto->getPropertyPath(), $subConfig);
+    }
+
+    /**
+     * Check if a form element property is defined in "predefinedDefaults" in the form setup.
+     * If a form element property is defined in the "predefinedDefaults" in the form setup then it
+     * means that the form element property can be written by the form editor.
+     * A form element default property is defined within the following form editor properties:
+     * * formElementsDefinition.<formElementType>.formEditor.predefinedDefaults.<propertyPath> = "default value"
+     *
+     * @param ValidationDto $dto
+     * @return bool
+     * @internal
+     */
+    public function isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup(
+        ValidationDto $dto
+    ): bool {
+        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
+            $dto->getPrototypeName()
+        );
+        return isset(
+            $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['predefinedDefaults'][$dto->getPropertyPath()]
+        );
+    }
+
+    /**
+     * Get the "predefinedDefaults" value for a form element property from the form setup.
+     * A form element default property is defined within the following form editor properties:
+     * * formElementsDefinition.<formElementType>.formEditor.predefinedDefaults.<propertyPath> = "default value"
+     *
+     * @param ValidationDto $dto
+     * @return mixed
+     * @throws PropertyException
+     * @internal
+     */
+    public function getFormElementPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto)
+    {
+        if (!$this->isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
+            throw new PropertyException(
+                sprintf(
+                    'No predefinedDefaults found for form element type "%s" and property path "%s"',
+                    $dto->getFormElementType(),
+                    $dto->getPropertyPath()
+                ),
+                1528578401
+            );
+        }
+
+        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
+            $dto->getPrototypeName()
+        );
+        return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['predefinedDefaults'][$dto->getPropertyPath()];
+    }
+
+    /**
+     * Check if a form elements finisher|validator property is defined in "predefinedDefaults" in the form setup.
+     * If a form elements finisher|validator property is defined in "predefinedDefaults" in the form setup then it
+     * means that the form elements finisher|validator property can be written by the form editor.
+     * A form elements finisher|validator default property is defined within the following form editor properties:
+     * * <validatorsDefinition|finishersDefinition>.<index>.formEditor.predefinedDefaults.<propertyPath> = "default value"
+     *
+     * @param ValidationDto $dto
+     * @return bool
+     * @internal
+     */
+    public function isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup(
+        ValidationDto $dto
+    ): bool {
+        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
+            $dto->getPrototypeName()
+        );
+        return isset(
+            $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['predefinedDefaults'][$dto->getPropertyPath()]
+        );
+    }
+
+    /**
+     * Get the "predefinedDefaults" value for a form elements finisher|validator property from the form setup.
+     * A form elements finisher|validator default property is defined within the following form editor properties:
+     * * <validatorsDefinition|finishersDefinition>.<index>.formEditor.predefinedDefaults.<propertyPath> = "default value"
+     *
+     * @param ValidationDto $dto
+     * @return mixed
+     * @throws PropertyException
+     * @internal
+     */
+    public function getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto)
+    {
+        if (!$this->isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
+            throw new PropertyException(
+                sprintf(
+                    'No predefinedDefaults found for property collection "%s" and identifier "%s" and property path "%s"',
+                    $dto->getPropertyCollectionName(),
+                    $dto->getPropertyCollectionElementIdentifier(),
+                    $dto->getPropertyPath()
+                ),
+                1528578402
+            );
+        }
+
+        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
+            $dto->getPrototypeName()
+        );
+        return $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['predefinedDefaults'][$dto->getPropertyPath()];
+    }
+
+    /**
+     * Check if the form element is creatable through the form editor.
+     * A form element is creatable if the following properties are set:
+     *  * formElementsDefinition.<formElementType>.formEditor.group
+     *  * formElementsDefinition.<formElementType>.formEditor.groupSorting
+     * And the value from "formElementsDefinition.<formElementType>.formEditor.group" is
+     * one of the keys within "formEditor.formElementGroups"
+     *
+     * @param ValidationDto $dto
+     * @return bool
+     * @internal
+     */
+    public function isFormElementTypeCreatableByFormEditor(ValidationDto $dto): bool
+    {
+        if ($dto->getFormElementType() === 'Form') {
+            return true;
+        }
+        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
+            $dto->getPrototypeName()
+        );
+        return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['creatable'] ?? false;
+    }
+
+    /**
+     * Check if the form elements finisher|validator is creatable through the form editor.
+     * A form elements finisher|validator is creatable if the following conditions are true:
+     * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName = Inspector-FinishersEditor"
+     * or
+     * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName = Inspector-ValidatorsEditor"
+     * and
+     * "formElementsDefinition.<formElementType>.formEditor.editors.<index>.selectOptions.<index>.value = <finisherIdentifier|validatorIdentifier>"
+     *
+     * @param ValidationDto $dto
+     * @return bool
+     * @internal
+     */
+    public function isPropertyCollectionElementIdentifierCreatableByFormEditor(ValidationDto $dto): bool
+    {
+        $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
+            $dto->getPrototypeName()
+        );
+        return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['creatable'] ?? false;
+    }
+
+    /**
+     * Check if the form elements type is defined within the form setup.
+     *
+     * @param ValidationDto $dto
+     * @return bool
+     * @internal
+     */
+    public function isFormElementTypeDefinedInFormSetup(ValidationDto $dto): bool
+    {
+        $prototypeConfiguration = $this->getPrototypeConfiguration($dto->getPrototypeName());
+        return ArrayUtility::isValidPath(
+            $prototypeConfiguration,
+            'formElementsDefinition.' . $dto->getFormElementType(),
+            '.'
+        );
+    }
+
+    /**
+     * Collect all the form editor configurations which are needed to check if a
+     * form definition property can be written or not.
+     *
+     * @param string $prototypeName
+     * @return array
+     */
+    protected function buildFormDefinitionValidationConfigurationFromFormEditorSetup(string $prototypeName): array
+    {
+        $cacheKey = implode('_', ['buildFormDefinitionValidationConfigurationFromFormEditorSetup', $prototypeName]);
+        $configuration = $this->getCacheEntry($cacheKey);
+
+        if ($configuration === null) {
+            $prototypeConfiguration = $this->getPrototypeConfiguration($prototypeName);
+            $extractorDto = GeneralUtility::makeInstance(ExtractorDto::class, $prototypeConfiguration);
+
+            GeneralUtility::makeInstance(ArrayProcessor::class, $prototypeConfiguration)->forEach(
+                GeneralUtility::makeInstance(
+                    ArrayProcessing::class,
+                    'formElementPropertyPaths',
+                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.(propertyPath|.*\.propertyPath)$',
+                    GeneralUtility::makeInstance(PropertyPathsExtractor::class, $extractorDto)
+                ),
+
+                GeneralUtility::makeInstance(
+                    ArrayProcessing::class,
+                    'formElementAdditionalElementPropertyPaths',
+                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.additionalElementPropertyPaths\.([\d]+)',
+                    GeneralUtility::makeInstance(AdditionalElementPropertyPathsExtractor::class, $extractorDto)
+                ),
+
+                GeneralUtility::makeInstance(
+                    ArrayProcessing::class,
+                    'formElementRelativeMultiValueProperties',
+                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.templateName$',
+                    GeneralUtility::makeInstance(MultiValuePropertiesExtractor::class, $extractorDto)
+                ),
+
+                GeneralUtility::makeInstance(
+                    ArrayProcessing::class,
+                    'formElementPredefinedDefaults',
+                    '^formElementsDefinition\.(.*)\.formEditor\.predefinedDefaults\.(.+)$',
+                    GeneralUtility::makeInstance(PredefinedDefaultsExtractor::class, $extractorDto)
+                ),
+
+                GeneralUtility::makeInstance(
+                    ArrayProcessing::class,
+                    'formElementCreatable',
+                    '^formElementsDefinition\.(.*)\.formEditor.group$',
+                    GeneralUtility::makeInstance(IsCreatableFormElementExtractor::class, $extractorDto)
+                ),
+
+                GeneralUtility::makeInstance(
+                    ArrayProcessing::class,
+                    'propertyCollectionCreatable',
+                    '^formElementsDefinition\.(.*)\.formEditor\.editors\.([\d]+)\.templateName$',
+                    GeneralUtility::makeInstance(IsCreatablePropertyCollectionElementExtractor::class, $extractorDto)
+                ),
+
+                GeneralUtility::makeInstance(
+                    ArrayProcessing::class,
+                    'propertyCollectionPropertyPaths',
+                    '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.(propertyPath|.*\.propertyPath)$',
+                    GeneralUtility::makeInstance(CollectionPropertyPathsExtractor::class, $extractorDto)
+                ),
+
+                GeneralUtility::makeInstance(
+                    ArrayProcessing::class,
+                    'propertyCollectionAdditionalElementPropertyPaths',
+                    '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.additionalElementPropertyPaths\.([\d]+)',
+                    GeneralUtility::makeInstance(AdditionalElementPropertyPathsExtractor::class, $extractorDto)
+                ),
+
+                GeneralUtility::makeInstance(
+                    ArrayProcessing::class,
+                    'propertyCollectionRelativeMultiValueProperties',
+                    '^formElementsDefinition\.(.*)\.formEditor\.propertyCollections\.(finishers|validators)\.([\d]+)\.editors\.([\d]+)\.templateName$',
+                    GeneralUtility::makeInstance(CollectionMultiValuePropertiesExtractor::class, $extractorDto)
+                ),
+
+                GeneralUtility::makeInstance(
+                    ArrayProcessing::class,
+                    'propertyCollectionPredefinedDefaults',
+                    '^(validatorsDefinition|finishersDefinition)\.(.*)\.formEditor\.predefinedDefaults\.(.+)$',
+                    GeneralUtility::makeInstance(CollectionPredefinedDefaultsExtractor::class, $extractorDto)
+                )
+            );
+            $configuration = $extractorDto->getResult();
+
+            $configuration = $this->translateValues($prototypeConfiguration, $configuration);
+
+            $configuration = $this->executeBuildFormDefinitionValidationConfigurationHooks(
+                $prototypeName,
+                $configuration
+            );
+
+            $this->setCacheEntry($cacheKey, $configuration);
+        }
+
+        return $configuration;
+    }
+
+    /**
+     * If you use a custom form editor "inspector editor" implementation which does not define the writable
+     * property paths by one of the described inspector editor properties (e.g "propertyPath") within
+     * the form setup, you must provide the writable property paths with a hook.
+     *
+     * @see $this->isFormElementPropertyDefinedInFormEditorSetup()
+     * @see $this->isPropertyCollectionPropertyDefinedInFormEditorSetup()
+     * Connect to the hook:
+     * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'][] = \Vendor\YourNamespace\YourClass::class;
+     * Use the hook:
+     * public function addAdditionalPropertyPaths(\TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto $validationDto): array
+     * {
+     *     $textValidationDto = $validationDto->withFormElementType('Text');
+     *     $textValidatorsValidationDto = $textValidationDto->withPropertyCollectionName('validators');
+     *     $dateValidationDto = $validationDto->withFormElementType('Date');
+     *     $propertyPaths = [
+     *         $textValidationDto->withPropertyPath('properties.my.custom.property'),
+     *         $textValidationDto->withPropertyPath('properties.my.other.custom.property'),
+     *         $textValidatorsValidationDto->withPropertyCollectionElementIdentifier('StringLength')->withPropertyPath('options.custom.property'),
+     *         $textValidatorsValidationDto->withPropertyCollectionElementIdentifier('CustomValidator')->withPropertyPath('options.other.custom.property'),
+     *         $dateValidationDto->withPropertyPath('properties.custom.property'),
+     *         // ..
+     *     ];
+     *     return $propertyPaths;
+     * }
+     * @param string $prototypeName
+     * @param array $configuration
+     * @return array
+     * @throws PropertyException
+     */
+    protected function executeBuildFormDefinitionValidationConfigurationHooks(
+        string $prototypeName,
+        array $configuration
+    ): array {
+        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'] ?? [] as $className) {
+            $hookObj = GeneralUtility::makeInstance($className);
+            if (method_exists($hookObj, 'addAdditionalPropertyPaths')) {
+                $validationDto = GeneralUtility::makeInstance(ValidationDto::class, $prototypeName);
+                $propertyPathsFromHook = $hookObj->addAdditionalPropertyPaths($validationDto);
+                if (!is_array($propertyPathsFromHook)) {
+                    $message = 'Return value of "%s->addAdditionalPropertyPaths() must be type "array"';
+                    throw new PropertyException(sprintf($message, $className), 1528633966);
+                }
+                $configuration = $this->addAdditionalPropertyPathsFromHook(
+                    $className,
+                    $prototypeName,
+                    $propertyPathsFromHook,
+                    $configuration
+                );
+            }
+        }
+
+        return $configuration;
+    }
+
+    /**
+     * @param string $hookClassName
+     * @param string $prototypeName
+     * @param array $propertyPathsFromHook
+     * @param array $configuration
+     * @return array
+     * @throws PropertyException
+     */
+    protected function addAdditionalPropertyPathsFromHook(
+        string $hookClassName,
+        string $prototypeName,
+        array $propertyPathsFromHook,
+        array $configuration
+    ): array {
+        foreach ($propertyPathsFromHook as $index => $validationDto) {
+            if (!($validationDto instanceof ValidationDto)) {
+                $message = 'Return value of "%s->addAdditionalPropertyPaths()[%s] must be an instance of "%s"';
+                throw new PropertyException(
+                    sprintf($message, $hookClassName, $index, ValidationDto::class),
+                    1528633966
+                );
+            }
+
+            if ($validationDto->getPrototypeName() !== $prototypeName) {
+                $message = 'The prototype name "%s" does not match "%s" on "%s->addAdditionalPropertyPaths()[%s]';
+                throw new PropertyException(
+                    sprintf(
+                        $message,
+                        $validationDto->getPrototypeName(),
+                        $prototypeName,
+                        $hookClassName,
+                        $index,
+                        ValidationDto::class
+                    ),
+                    1528634966
+                );
+            }
+
+            $formElementType = $validationDto->getFormElementType();
+            if (!$this->isFormElementTypeDefinedInFormSetup($validationDto)) {
+                $message = 'Form element type "%s" does not exists in prototype configuration "%s"';
+                throw new PropertyException(
+                    sprintf($message, $formElementType, $validationDto->getPrototypeName()),
+                    1528633967
+                );
+            }
+
+            if ($validationDto->hasPropertyCollectionName() &&
+                $validationDto->hasPropertyCollectionElementIdentifier()) {
+                $propertyCollectionName = $validationDto->getPropertyCollectionName();
+                $propertyCollectionElementIdentifier = $validationDto->getPropertyCollectionElementIdentifier();
+
+                if ($propertyCollectionName !== 'finishers' && $propertyCollectionName !== 'validators') {
+                    $message = 'The property collection name "%s" for form element "%s" must be "finishers" or "validators"';
+                    throw new PropertyException(
+                        sprintf($message, $propertyCollectionName, $formElementType),
+                        1528636941
+                    );
+                }
+
+                $configuration['formElements'][$formElementType]['collections'][$propertyCollectionName][$propertyCollectionElementIdentifier]['additionalPropertyPaths'][]
+                    = $validationDto->getPropertyPath();
+            } else {
+                $configuration['formElements'][$formElementType]['additionalPropertyPaths'][]
+                    = $validationDto->getPropertyPath();
+            }
+        }
+
+        return $configuration;
+    }
+
+    /**
+     * @param string $propertyPath
+     * @param array $subConfig
+     * @return bool
+     */
+    protected function isPropertyDefinedInFormEditorSetup(string $propertyPath, array $subConfig): bool
+    {
+        if (empty($subConfig)) {
+            return false;
+        }
+        if (
+            in_array($propertyPath, $subConfig['propertyPaths'] ?? [], true)
+            || in_array($propertyPath, $subConfig['additionalElementPropertyPaths'] ?? [], true)
+            || in_array($propertyPath, $subConfig['additionalPropertyPaths'] ?? [], true)
+        ) {
+            return true;
+        }
+        foreach ($subConfig['multiValueProperties'] ?? [] as $relativeMultiValueProperty) {
+            if (strpos($propertyPath, $relativeMultiValueProperty) === 0) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @param array $prototypeConfiguration
+     * @param array $configuration
+     * @return array
+     */
+    protected function translateValues(array $prototypeConfiguration, array $configuration): array
+    {
+        if (isset($configuration['formElements'])) {
+            $configuration['formElements'] = $this->translatePredefinedDefaults(
+                $prototypeConfiguration,
+                $configuration['formElements']
+            );
+        }
+
+        foreach ($configuration['collections'] ?? [] as $name => $collections) {
+            $configuration['collections'][$name] = $this->translatePredefinedDefaults($prototypeConfiguration, $collections);
+        }
+        return $configuration;
+    }
+
+    /**
+     * @param array $prototypeConfiguration
+     * @param array $configuration
+     * @return array
+     */
+    protected function translatePredefinedDefaults(array $prototypeConfiguration, array $formElements): array
+    {
+        foreach ($formElements ?? [] as $name => $formElement) {
+            if (!isset($formElement['predefinedDefaults'])) {
+                continue;
+            }
+            $formElement['predefinedDefaults'] = $this->getTranslationService()->translateValuesRecursive(
+                $formElement['predefinedDefaults'],
+                $prototypeConfiguration['formEditor']['translationFile'] ?? null
+            );
+            $formElements[$name] = $formElement;
+        }
+        return $formElements;
+    }
+
+    /**
+     * @param string $cacheKey
+     * @return mixed
+     */
+    protected function getCacheEntry(string $cacheKey)
+    {
+        if (isset($this->firstLevelCache[$cacheKey])) {
+            return $this->firstLevelCache[$cacheKey];
+        }
+        return $this->getCacheFrontend()->has('form_' . $cacheKey)
+            ? $this->getCacheFrontend()->get('form_' . $cacheKey)
+            : null;
+    }
+
+    /**
+     * @param string $cacheKey
+     * @param mixed $value
+     */
+    protected function setCacheEntry(string $cacheKey, $value): void
+    {
+        $this->getCacheFrontend()->set('form_' . $cacheKey, $value);
+        $this->firstLevelCache[$cacheKey] = $value;
+    }
+
+    /**
+     * @return TranslationService
+     */
+    protected function getTranslationService(): TranslationService
+    {
+        return $this->translationService instanceof TranslationService
+            ? $this->translationService
+            : TranslationService::getInstance();
+    }
+
+    /**
+     * @return FrontendInterface
+     */
+    protected function getCacheFrontend(): FrontendInterface
+    {
+        return GeneralUtility::makeInstance(CacheManager::class)->getCache('assets');
+    }
 }
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/Exception/ArrayProcessorException.php b/typo3/sysext/form/Classes/Domain/Configuration/Exception/ArrayProcessorException.php
new file mode 100644 (file)
index 0000000..0f448db
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\Exception;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Form\Domain\Exception;
+
+/**
+ * @internal
+ */
+class ArrayProcessorException extends Exception
+{
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/Exception/PropertyException.php b/typo3/sysext/form/Classes/Domain/Configuration/Exception/PropertyException.php
new file mode 100644 (file)
index 0000000..de8dd3c
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\Exception;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Form\Domain\Exception;
+
+/**
+ * This exception is thrown if a form setup property was not found.
+ *
+ * @api
+ */
+class PropertyException extends Exception
+{
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AbstractConverter.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AbstractConverter.php
new file mode 100644 (file)
index 0000000..90b5bcb
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * @internal
+ */
+abstract class AbstractConverter implements ConverterInterface
+{
+
+    /**
+     * @var string
+     */
+    protected $sessionToken;
+
+    /**
+     * @var ConverterDto
+     */
+    protected $converterDto;
+
+    /**
+     * @param ConverterDto $converterDto
+     * @param string $sessionToken
+     */
+    public function __construct(ConverterDto $converterDto, string $sessionToken = '')
+    {
+        $this->converterDto = $converterDto;
+        $this->sessionToken = $sessionToken;
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataConverter.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataConverter.php
new file mode 100644 (file)
index 0000000..4578e4d
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
+use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
+
+/**
+ * @internal
+ */
+class AddHmacDataConverter extends AbstractConverter
+{
+
+    /**
+     * Add a new value "_orig_<propertyName>" as a sibling of the property key.
+     * "_orig_<propertyName>" is an array which contains the property value
+     * and a hmac hash for the property value.
+     * "_orig_<propertyName>" will be used to validate the form definition on saving.
+     * @see \TYPO3\CMS\Form\Domain\Configuration\FormDefinitionValidationService::validateFormDefinitionProperties()
+     *
+     * @param string $key
+     * @param mixed $value
+     */
+    public function __invoke(string $key, $value): void
+    {
+        $formDefinition = $this->converterDto->getFormDefinition();
+
+        $renderablePathParts = explode('.', $key);
+        array_pop($renderablePathParts);
+
+        if (count($renderablePathParts) > 1) {
+            $renderablePath = implode('.', $renderablePathParts);
+            $currentFormElement = ArrayUtility::getValueByPath($formDefinition, $renderablePath, '.');
+        } else {
+            $currentFormElement = $formDefinition;
+        }
+
+        $propertyCollectionElements = $currentFormElement['finishers'] ?? $currentFormElement['validators'] ?? [];
+        $propertyCollectionName = $currentFormElement['type'] === 'Form' ? 'finishers' : 'validators';
+        unset($currentFormElement['renderables'], $currentFormElement['finishers'], $currentFormElement['validators']);
+
+        $this->converterDto
+            ->setRenderablePathParts($renderablePathParts)
+            ->setFormElementIdentifier($value);
+
+        GeneralUtility::makeInstance(ArrayProcessor::class, $currentFormElement)->forEach(
+            GeneralUtility::makeInstance(
+                ArrayProcessing::class,
+                'addHmacData',
+                '^(?!(.*\._label|.*\._value)$).*',
+                GeneralUtility::makeInstance(
+                    AddHmacDataToFormElementPropertyConverter::class,
+                    $this->converterDto,
+                    $this->sessionToken
+                )
+            )
+        );
+
+        $this->converterDto->setPropertyCollectionName($propertyCollectionName);
+        foreach ($propertyCollectionElements as $propertyCollectionIndex => $propertyCollectionElement) {
+            $this->converterDto
+                ->setPropertyCollectionIndex((int)$propertyCollectionIndex)
+                ->setPropertyCollectionElementIdentifier($propertyCollectionElement['identifier']);
+
+            GeneralUtility::makeInstance(ArrayProcessor::class, $propertyCollectionElement)->forEach(
+                GeneralUtility::makeInstance(
+                    ArrayProcessing::class,
+                    'addHmacData',
+                    '^(?!(.*\._label|.*\._value)$).*',
+                    GeneralUtility::makeInstance(
+                        AddHmacDataToPropertyCollectionElementConverter::class,
+                        $this->converterDto,
+                        $this->sessionToken
+                    )
+                )
+            );
+        }
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataToFormElementPropertyConverter.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataToFormElementPropertyConverter.php
new file mode 100644 (file)
index 0000000..9d43981
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * @internal
+ */
+class AddHmacDataToFormElementPropertyConverter extends AbstractConverter
+{
+
+    /**
+     * @param string $key
+     * @param mixed $value
+     */
+    public function __invoke(string $key, $value): void
+    {
+        $formDefinition = $this->converterDto->getFormDefinition();
+
+        $propertyPathParts = explode('.', $key);
+        $lastKeySegment = array_pop($propertyPathParts);
+        $propertyPathParts[] = '_orig_' . $lastKeySegment;
+
+        $hmacValuePath = implode('.', array_merge($this->converterDto->getRenderablePathParts(), $propertyPathParts));
+        $hmacValue = [
+            'value' => $value,
+            'hmac' => GeneralUtility::hmac(serialize([$this->converterDto->getFormElementIdentifier(), $key, $value]), $this->sessionToken)
+        ];
+
+        $formDefinition = ArrayUtility::setValueByPath($formDefinition, $hmacValuePath, $hmacValue, '.');
+
+        $this->converterDto->setFormDefinition($formDefinition);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataToPropertyCollectionElementConverter.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataToPropertyCollectionElementConverter.php
new file mode 100644 (file)
index 0000000..267d4c1
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * @internal
+ */
+class AddHmacDataToPropertyCollectionElementConverter extends AbstractConverter
+{
+
+    /**
+     * @param string $key
+     * @param mixed $value
+     */
+    public function __invoke(string $key, $value): void
+    {
+        $formDefinition = $this->converterDto->getFormDefinition();
+
+        $propertyPathParts = explode('.', $key);
+        $lastKeySegment = array_pop($propertyPathParts);
+        $propertyPathParts[] = '_orig_' . $lastKeySegment;
+
+        $hmacValuePath = implode('.', array_merge(
+            $this->converterDto->getRenderablePathParts(),
+            [$this->converterDto->getPropertyCollectionName(), $this->converterDto->getPropertyCollectionIndex()],
+            $propertyPathParts
+        ));
+
+        $hmacValue = [
+            'value' => $value,
+            'hmac' => GeneralUtility::hmac(
+                serialize([
+                    $this->converterDto->getFormElementIdentifier(),
+                    $this->converterDto->getPropertyCollectionName(),
+                    $this->converterDto->getPropertyCollectionElementIdentifier(),
+                    $key,
+                    $value
+                ]),
+                $this->sessionToken
+            )
+        ];
+
+        $formDefinition = ArrayUtility::setValueByPath($formDefinition, $hmacValuePath, $hmacValue, '.');
+
+        $this->converterDto->setFormDefinition($formDefinition);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/ConverterDto.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/ConverterDto.php
new file mode 100644 (file)
index 0000000..d5bb7d6
--- /dev/null
@@ -0,0 +1,169 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * @internal
+ */
+class ConverterDto
+{
+
+    /**
+     * @var array
+     */
+    protected $formDefinition = [];
+
+    /**
+     * @var array
+     */
+    protected $renderablePathParts = [];
+
+    /**
+     * @var string
+     */
+    protected $formElementIdentifier = '';
+
+    /**
+     * @var int
+     */
+    protected $propertyCollectionIndex = 0;
+
+    /**
+     * @var string
+     */
+    protected $propertyCollectionName = '';
+
+    /**
+     * @var string
+     */
+    protected $propertyCollectionElementIdentifier = '';
+
+    /**
+     * @param array $formDefinition
+     */
+    public function __construct(array $formDefinition)
+    {
+        $this->formDefinition = $formDefinition;
+    }
+
+    /**
+     * @return array
+     */
+    public function getFormDefinition(): array
+    {
+        return $this->formDefinition;
+    }
+
+    /**
+     * @param array $formDefinition
+     * @return ConverterDto
+     */
+    public function setFormDefinition(array $formDefinition): ConverterDto
+    {
+        $this->formDefinition = $formDefinition;
+        return $this;
+    }
+
+    /**
+     * @return array
+     */
+    public function getRenderablePathParts(): array
+    {
+        return $this->renderablePathParts;
+    }
+
+    /**
+     * @param array $renderablePathParts
+     * @return ConverterDto
+     */
+    public function setRenderablePathParts(array $renderablePathParts): ConverterDto
+    {
+        $this->renderablePathParts = $renderablePathParts;
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getFormElementIdentifier(): string
+    {
+        return $this->formElementIdentifier;
+    }
+
+    /**
+     * @param string $formElementIdentifier
+     * @return ConverterDto
+     */
+    public function setFormElementIdentifier(string $formElementIdentifier): ConverterDto
+    {
+        $this->formElementIdentifier = $formElementIdentifier;
+        return $this;
+    }
+
+    /**
+     * @return int
+     */
+    public function getPropertyCollectionIndex(): int
+    {
+        return $this->propertyCollectionIndex;
+    }
+
+    /**
+     * @param int $propertyCollectionIndex
+     * @return ConverterDto
+     */
+    public function setPropertyCollectionIndex(int $propertyCollectionIndex): ConverterDto
+    {
+        $this->propertyCollectionIndex = $propertyCollectionIndex;
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPropertyCollectionName(): string
+    {
+        return $this->propertyCollectionName;
+    }
+
+    /**
+     * @param string $propertyCollectionName
+     * @return ConverterDto
+     */
+    public function setPropertyCollectionName(string $propertyCollectionName): ConverterDto
+    {
+        $this->propertyCollectionName = $propertyCollectionName;
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPropertyCollectionElementIdentifier(): string
+    {
+        return $this->propertyCollectionElementIdentifier;
+    }
+
+    /**
+     * @param string $propertyCollectionElementIdentifier
+     * @return ConverterDto
+     */
+    public function setPropertyCollectionElementIdentifier(string $propertyCollectionElementIdentifier): ConverterDto
+    {
+        $this->propertyCollectionElementIdentifier = $propertyCollectionElementIdentifier;
+        return $this;
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/ConverterInterface.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/ConverterInterface.php
new file mode 100644 (file)
index 0000000..c07d1db
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * @internal
+ */
+interface ConverterInterface
+{
+
+    /**
+     * @param ConverterDto $converterDto
+     * @param string $sessionToken
+     */
+    public function __construct(ConverterDto $converterDto, string $sessionToken = '');
+
+    /**
+     * @param string $key
+     * @param mixed $value
+     */
+    public function __invoke(string $key, $value);
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/RemoveHmacDataConverter.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/RemoveHmacDataConverter.php
new file mode 100644 (file)
index 0000000..a7b8481
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+
+/**
+ * @internal
+ */
+class RemoveHmacDataConverter extends AbstractConverter
+{
+
+    /**
+     * Remove the hmac data ("_orig_<propertyName>") for the corresponding property.
+     *
+     * @param string $key
+     * @param mixed $value
+     */
+    public function __invoke(string $key, $value): void
+    {
+        $formDefinition = $this->converterDto->getFormDefinition();
+
+        $propertyPathParts = explode('.', $key);
+        array_pop($propertyPathParts);
+        $propertyPath = implode('.', $propertyPathParts);
+        $formDefinition = ArrayUtility::removeByPath($formDefinition, $propertyPath, '.');
+
+        $this->converterDto->setFormDefinition($formDefinition);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/AbstractValidator.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/AbstractValidator.php
new file mode 100644 (file)
index 0000000..24e8fd6
--- /dev/null
@@ -0,0 +1,102 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Object\ObjectManager;
+use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionValidationService;
+
+/**
+ * @internal
+ */
+abstract class AbstractValidator implements ValidatorInterface
+{
+
+    /**
+     * @var ConfigurationService
+     */
+    protected $configurationService;
+
+    /**
+     * @var array
+     */
+    protected $currentElement;
+
+    /**
+     * @var string
+     */
+    protected $sessionToken;
+
+    /**
+     * @var ValidationDto
+     */
+    protected $validationDto;
+
+    /**
+     * @param array $currentElement
+     * @param string $sessionToken
+     * @param ValidationDto $validationDto
+     */
+    public function __construct(array $currentElement, string $sessionToken, ValidationDto $validationDto)
+    {
+        $this->currentElement = $currentElement;
+        $this->sessionToken = $sessionToken;
+        $this->validationDto = $validationDto;
+    }
+
+    /**
+     * Builds the path in which the hmac value is expected based on the property path.
+     *
+     * @param string $propertyPath
+     * @return string
+     */
+    protected function buildHmacDataPath(string $propertyPath): string
+    {
+        $pathParts = explode('.', $propertyPath);
+        $lastPathSegment = array_pop($pathParts);
+        $pathParts[] = '_orig_' . $lastPathSegment;
+
+        return implode('.', $pathParts);
+    }
+
+    /**
+     * @return FormDefinitionValidationService
+     */
+    protected function getFormDefinitionValidationService(): FormDefinitionValidationService
+    {
+        return GeneralUtility::makeInstance(FormDefinitionValidationService::class);
+    }
+
+    /**
+     * @return ConfigurationService
+     */
+    protected function getConfigurationService(): ConfigurationService
+    {
+        if (!($this->configurationService instanceof ConfigurationService)) {
+            $this->configurationService = $this->getObjectManager()->get(ConfigurationService::class);
+        }
+        return $this->configurationService;
+    }
+
+    /**
+     * @return ObjectManager
+     */
+    protected function getObjectManager(): ObjectManager
+    {
+        return GeneralUtility::makeInstance(ObjectManager::class);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CollectionBasedValidator.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CollectionBasedValidator.php
new file mode 100644 (file)
index 0000000..3360469
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
+
+/**
+ * @internal
+ */
+abstract class CollectionBasedValidator extends AbstractValidator
+{
+
+    /**
+     * Throws an exception if value from a property collection property
+     * does not match its hmac hash or if there is no hmac hash
+     * available for the value.
+     *
+     * @param array $currentElement
+     * @param mixed $value
+     * @param string $sessionToken
+     * @param ValidationDto $dto
+     * @throws PropertyException
+     */
+    public function validatePropertyCollectionElementPropertyValueByHmacData(
+        array $currentElement,
+        $value,
+        string $sessionToken,
+        ValidationDto $dto
+    ): void {
+        $hmacDataPath = $this->buildHmacDataPath($dto->getPropertyPath());
+        if (ArrayUtility::isValidPath($currentElement, $hmacDataPath, '.')) {
+            $hmacData = ArrayUtility::getValueByPath($currentElement, $hmacDataPath, '.');
+
+            $hmacContent = [
+                $dto->getFormElementIdentifier(),
+                $dto->getPropertyCollectionName(),
+                $dto->getPropertyCollectionElementIdentifier(),
+                $dto->getPropertyPath()
+            ];
+
+            if (!$this->getFormDefinitionValidationService()->isPropertyValueEqualToHistoricalValue($hmacContent, $value, $hmacData, $sessionToken)) {
+                $message = 'The value "%s" of property "%s" (form element "%s" / "%s.%s") is not equal to the historical value "%s" #1528591586';
+                throw new PropertyException(
+                    sprintf(
+                        $message,
+                        $value,
+                        $dto->getPropertyPath(),
+                        $dto->getFormElementIdentifier(),
+                        $dto->getPropertyCollectionName(),
+                        $dto->getPropertyCollectionElementIdentifier(),
+                        $hmacData['value'] ?? ''
+                    ),
+                    1528591586
+                );
+            }
+        } else {
+            $message = 'No hmac found for property "%s" (form element "%s" / "%s.%s") #1528591585';
+            throw new PropertyException(
+                sprintf(
+                    $message,
+                    $dto->getPropertyPath(),
+                    $dto->getFormElementIdentifier(),
+                    $dto->getPropertyCollectionName(),
+                    $dto->getPropertyCollectionElementIdentifier()
+                ),
+                1528591585
+            );
+        }
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CreatableFormElementPropertiesValidator.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CreatableFormElementPropertiesValidator.php
new file mode 100644 (file)
index 0000000..f23c0bc
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
+
+/**
+ * @internal
+ */
+class CreatableFormElementPropertiesValidator extends ElementBasedValidator
+{
+
+    /**
+     * Checks if the form element property is defined within the form editor setup
+     * or if the property is definied within the "predefinedDefaults" in the form editor setup
+     * and the property value matches the predefined value
+     * or if there is a valid hmac hash for the value.
+     *
+     * @param string $key
+     * @param mixed $value
+     */
+    public function __invoke(string $key, $value)
+    {
+        $dto = $this->validationDto->withPropertyPath($key);
+
+        if (!$this->getConfigurationService()->isFormElementPropertyDefinedInFormEditorSetup($dto)) {
+            if ($this->getConfigurationService()->isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
+                $predefinedDefaultValue = $this->getConfigurationService()->getFormElementPredefinedDefaultValueFromFormEditorSetup($dto);
+                if ($value !== $predefinedDefaultValue) {
+                    $message = 'The value "%s" of property "%s" (form element "%s") is not equal to the default value "%s" #1528588035';
+                    throw new PropertyException(
+                        sprintf(
+                            $message,
+                            $value,
+                            $dto->getPropertyPath(),
+                            $dto->getFormElementIdentifier(),
+                            $predefinedDefaultValue
+                        ),
+                        1528588035
+                    );
+                }
+            } else {
+                $this->validateFormElementPropertyValueByHmacData(
+                    $this->currentElement,
+                    $value,
+                    $this->sessionToken,
+                    $dto
+                );
+            }
+        }
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CreatablePropertyCollectionElementPropertiesValidator.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CreatablePropertyCollectionElementPropertiesValidator.php
new file mode 100644 (file)
index 0000000..79fe883
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
+
+/**
+ * @internal
+ */
+class CreatablePropertyCollectionElementPropertiesValidator extends CollectionBasedValidator
+{
+
+    /**
+     * Checks if the property collection element property is defined
+     * within the form editor setup or if the property is definied within
+     * the "predefinedDefaults" in the form editor setup
+     * and the property value matches the predefined value
+     * or if there is a valid hmac hash for the value.
+     *
+     * @param string $key
+     * @param mixed $value
+     */
+    public function __invoke(string $key, $value)
+    {
+        $dto = $this->validationDto->withPropertyPath($key);
+
+        if (!$this->getConfigurationService()->isPropertyCollectionPropertyDefinedInFormEditorSetup($dto)) {
+            if ($this->getConfigurationService()->isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
+                $this->validatePropertyCollectionElementPredefinedDefaultValue($value, $dto);
+            } else {
+                $this->validatePropertyCollectionElementPropertyValueByHmacData(
+                    $this->currentElement,
+                    $value,
+                    $this->sessionToken,
+                    $dto
+                );
+            }
+        }
+    }
+
+    /**
+     * Throws an exception if the value from a property collection property
+     * does not match the default value from the form editor setup.
+     *
+     * @param mixed $value
+     * @param ValidationDto $dto
+     * @throws PropertyException
+     */
+    protected function validatePropertyCollectionElementPredefinedDefaultValue(
+        $value,
+        ValidationDto $dto
+    ): void {
+        $predefinedDefaultValue = $this->getConfigurationService()->getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup($dto);
+        if ($value !== $predefinedDefaultValue) {
+            $message = 'The value "%s" of property "%s" (form element "%s" / "%s.%s") is not equal to the default value "%s" #1528591502';
+            throw new PropertyException(
+                sprintf(
+                    $message,
+                    $value,
+                    $dto->getPropertyPath(),
+                    $dto->getFormElementIdentifier(),
+                    $dto->getPropertyCollectionName(),
+                    $dto->getPropertyCollectionElementIdentifier(),
+                    $predefinedDefaultValue
+                ),
+                1528591502
+            );
+        }
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ElementBasedValidator.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ElementBasedValidator.php
new file mode 100644 (file)
index 0000000..7551385
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
+
+/**
+ * @internal
+ */
+abstract class ElementBasedValidator extends AbstractValidator
+{
+
+    /**
+     * Throws an exception if value from a form element property
+     * does not match its hmac hash or if there is no hmac hash
+     * available for the value.
+     *
+     * @param array $currentElement
+     * @param mixed $value
+     * @param string $sessionToken
+     * @param ValidationDto $dto
+     * @throws PropertyException
+     */
+    public function validateFormElementPropertyValueByHmacData(
+        array $currentElement,
+        $value,
+        string $sessionToken,
+        ValidationDto $dto
+    ): void {
+        $hmacDataPath = $this->buildHmacDataPath($dto->getPropertyPath());
+        if (ArrayUtility::isValidPath($currentElement, $hmacDataPath, '.')) {
+            $hmacData = ArrayUtility::getValueByPath($currentElement, $hmacDataPath, '.');
+
+            $hmacContent = [$dto->getFormElementIdentifier(), $dto->getPropertyPath()];
+            if (!$this->getFormDefinitionValidationService()->isPropertyValueEqualToHistoricalValue($hmacContent, $value, $hmacData, $sessionToken)) {
+                $message = 'The value "%s" of property "%s" (form element "%s") is not equal to the historical value "%s" #1528588036';
+                throw new PropertyException(
+                    sprintf(
+                        $message,
+                        $value,
+                        $dto->getPropertyPath(),
+                        $dto->getFormElementIdentifier(),
+                        $hmacData['value'] ?? ''
+                    ),
+                    1528588036
+                );
+            }
+        } else {
+            $message = 'No hmac found for property "%s" (form element "%s") #1528588037';
+            throw new PropertyException(
+                sprintf($message, $dto->getPropertyPath(), $dto->getFormElementIdentifier()),
+                1528588037
+            );
+        }
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/FormElementHmacDataValidator.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/FormElementHmacDataValidator.php
new file mode 100644 (file)
index 0000000..ab9e8cf
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * @internal
+ */
+class FormElementHmacDataValidator extends ElementBasedValidator
+{
+
+    /**
+     * Checks if the form element property value matches to its hmac hash.
+     *
+     * @param string $key
+     * @param mixed $value
+     */
+    public function __invoke(string $key, $value): void
+    {
+        $dto = $this->validationDto->withPropertyPath($key);
+        $this->validateFormElementPropertyValueByHmacData(
+            $this->currentElement,
+            $value,
+            $this->sessionToken,
+            $dto
+        );
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/PropertyCollectionElementHmacDataValidator.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/PropertyCollectionElementHmacDataValidator.php
new file mode 100644 (file)
index 0000000..fd9501b
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * @internal
+ */
+class PropertyCollectionElementHmacDataValidator extends CollectionBasedValidator
+{
+
+    /**
+     * Checks if the property collection element values matches to its hmac hash.
+     *
+     * @param string $key
+     * @param mixed $value
+     */
+    public function __invoke(string $key, $value): void
+    {
+        $dto = $this->validationDto->withPropertyPath($key)->withPropertyCollectionElementIdentifier(
+            $this->currentElement['identifier']
+        );
+        $this->validatePropertyCollectionElementPropertyValueByHmacData(
+            $this->currentElement,
+            $value,
+            $this->sessionToken,
+            $dto
+        );
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ValidationDto.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ValidationDto.php
new file mode 100644 (file)
index 0000000..19178d7
--- /dev/null
@@ -0,0 +1,229 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * @api
+ */
+class ValidationDto
+{
+
+    /**
+     * @var string
+     */
+    protected $prototypeName;
+
+    /**
+     * @var string
+     */
+    protected $formElementType;
+
+    /**
+     * @var string
+     */
+    protected $formElementIdentifier;
+
+    /**
+     * @var string
+     */
+    protected $propertyPath;
+
+    /**
+     * @var string
+     */
+    protected $propertyCollectionName;
+
+    /**
+     * @var string
+     */
+    protected $propertyCollectionElementIdentifier;
+
+    /**
+     * @param string $prototypeName
+     * @param string $formElementType
+     * @param string $formElementIdentifier
+     * @param string $propertyPath
+     * @param string $propertyCollectionName
+     * @param string $propertyCollectionElementIdentifier
+     */
+    public function __construct(
+        string $prototypeName = null,
+        string $formElementType = null,
+        string $formElementIdentifier = null,
+        string $propertyPath = null,
+        string $propertyCollectionName = null,
+        string $propertyCollectionElementIdentifier = null
+    ) {
+        $this->prototypeName = $prototypeName;
+        $this->formElementType = $formElementType;
+        $this->formElementIdentifier = $formElementIdentifier;
+        $this->propertyPath = $propertyPath;
+        $this->propertyCollectionName = $propertyCollectionName;
+        $this->propertyCollectionElementIdentifier = $propertyCollectionElementIdentifier;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPrototypeName(): string
+    {
+        return $this->prototypeName;
+    }
+
+    /**
+     * @return string
+     */
+    public function getFormElementType(): string
+    {
+        return $this->formElementType;
+    }
+
+    /**
+     * @return string
+     */
+    public function getFormElementIdentifier(): string
+    {
+        return $this->formElementIdentifier;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPropertyPath(): string
+    {
+        return $this->propertyPath;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPropertyCollectionName(): string
+    {
+        return $this->propertyCollectionName;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPropertyCollectionElementIdentifier(): string
+    {
+        return $this->propertyCollectionElementIdentifier;
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasPrototypeName(): bool
+    {
+        return !empty($this->prototypeName);
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasFormElementType(): bool
+    {
+        return !empty($this->formElementType);
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasFormElementIdentifier(): bool
+    {
+        return !empty($this->formElementIdentifier);
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasPropertyPath(): bool
+    {
+        return !empty($this->propertyPath);
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasPropertyCollectionName(): bool
+    {
+        return !empty($this->propertyCollectionName);
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasPropertyCollectionElementIdentifier(): bool
+    {
+        return !empty($this->propertyCollectionElementIdentifier);
+    }
+
+    /**
+     * @param string $prototypeName
+     * @return ValidationDto
+     */
+    public function withPrototypeName(string $prototypeName): ValidationDto
+    {
+        return GeneralUtility::makeInstance(self::class, $prototypeName, $this->formElementType, $this->formElementIdentifier, $this->propertyPath, $this->propertyCollectionName, $this->propertyCollectionElementIdentifier);
+    }
+
+    /**
+     * @param string $formElementType
+     * @return ValidationDto
+     */
+    public function withFormElementType(string $formElementType): ValidationDto
+    {
+        return GeneralUtility::makeInstance(self::class, $this->prototypeName, $formElementType, $this->formElementIdentifier, $this->propertyPath, $this->propertyCollectionName, $this->propertyCollectionElementIdentifier);
+    }
+
+    /**
+     * @param string $formElementIdentifier
+     * @return ValidationDto
+     */
+    public function withFormElementIdentifier(string $formElementIdentifier): ValidationDto
+    {
+        return GeneralUtility::makeInstance(self::class, $this->prototypeName, $this->formElementType, $formElementIdentifier, $this->propertyPath, $this->propertyCollectionName, $this->propertyCollectionElementIdentifier);
+    }
+
+    /**
+     * @param string $propertyPath
+     * @return ValidationDto
+     */
+    public function withPropertyPath(string $propertyPath): ValidationDto
+    {
+        return GeneralUtility::makeInstance(self::class, $this->prototypeName, $this->formElementType, $this->formElementIdentifier, $propertyPath, $this->propertyCollectionName, $this->propertyCollectionElementIdentifier);
+    }
+
+    /**
+     * @param string $propertyCollectionName
+     * @return ValidationDto
+     */
+    public function withPropertyCollectionName(string $propertyCollectionName): ValidationDto
+    {
+        return GeneralUtility::makeInstance(self::class, $this->prototypeName, $this->formElementType, $this->formElementIdentifier, $this->propertyPath, $propertyCollectionName, $this->propertyCollectionElementIdentifier);
+    }
+
+    /**
+     * @param string $propertyCollectionElementIdentifier
+     * @return ValidationDto
+     */
+    public function withPropertyCollectionElementIdentifier(string $propertyCollectionElementIdentifier): ValidationDto
+    {
+        return GeneralUtility::makeInstance(self::class, $this->prototypeName, $this->formElementType, $this->formElementIdentifier, $this->propertyPath, $this->propertyCollectionName, $propertyCollectionElementIdentifier);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ValidatorInterface.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ValidatorInterface.php
new file mode 100644 (file)
index 0000000..1d790c7
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * @internal
+ */
+interface ValidatorInterface
+{
+
+    /**
+     * @param array $currentElement
+     * @param string $sessionToken
+     * @param ValidationDto $validationDto
+     */
+    public function __construct(array $currentElement, string $sessionToken, ValidationDto $validationDto);
+
+    /**
+     * @param string $key
+     * @param mixed $value
+     */
+    public function __invoke(string $key, $value);
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinitionConversionService.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinitionConversionService.php
new file mode 100644 (file)
index 0000000..2054070
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
+use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters\AddHmacDataConverter;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters\ConverterDto;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Converters\RemoveHmacDataConverter;
+
+/**
+ * @internal
+ */
+class FormDefinitionConversionService implements SingletonInterface
+{
+
+    /**
+     * Add a new value "_orig_<propertyName>" for each scalar property value
+     * within the form definition as a sibling of the property key.
+     * "_orig_<propertyName>" is an array which contains the property value
+     * and a hmac hash for the property value.
+     * "_orig_<propertyName>" will be used to validate the form definition on saving.
+     * @see \TYPO3\CMS\Form\Domain\Configuration\FormDefinitionValidationService::validateFormDefinitionProperties()
+     *
+     * @param array $formDefinition
+     * @return array
+     */
+    public function addHmacData(array $formDefinition): array
+    {
+        // Extend the hmac hashing key with a "per form editor session" unique key.
+        $sessionToken = $this->generateSessionToken();
+        $this->persistSessionToken($sessionToken);
+
+        $converterDto = GeneralUtility::makeInstance(ConverterDto::class, $formDefinition);
+
+        GeneralUtility::makeInstance(ArrayProcessor::class, $formDefinition)->forEach(
+            GeneralUtility::makeInstance(
+                ArrayProcessing::class,
+                'addHmacData',
+                '(^identifier$|renderables\.([\d]+).\identifier$)',
+                GeneralUtility::makeInstance(
+                    AddHmacDataConverter::class,
+                    $converterDto,
+                    $sessionToken
+                )
+            )
+        );
+
+        return $converterDto->getFormDefinition();
+    }
+
+    /**
+     * Remove the "_orig_<propertyName>" values from the form definition.
+     *
+     * @param array $formDefinition
+     * @return array
+     */
+    public function removeHmacData(array $formDefinition): array
+    {
+        $converterDto = GeneralUtility::makeInstance(ConverterDto::class, $formDefinition);
+
+        GeneralUtility::makeInstance(ArrayProcessor::class, $formDefinition)->forEach(
+            GeneralUtility::makeInstance(
+                ArrayProcessing::class,
+                'removeHmacData',
+                '(_orig_.*|.*\._orig_.*)\.hmac',
+                GeneralUtility::makeInstance(
+                    RemoveHmacDataConverter::class,
+                    $converterDto
+                )
+            )
+        );
+
+        return $converterDto->getFormDefinition();
+    }
+
+    /**
+     */
+    protected function persistSessionToken(string $sessionToken)
+    {
+        $this->getBackendUser()->setAndSaveSessionData('extFormProtectionSessionToken', $sessionToken);
+    }
+
+    /**
+     * Generates the random token which is used in the hash for the form tokens.
+     *
+     * @return string
+     */
+    protected function generateSessionToken()
+    {
+        return GeneralUtility::makeInstance(Random::class)->generateRandomHexString(64);
+    }
+
+    /**
+     * @return BackendUserAuthentication
+     */
+    protected function getBackendUser(): BackendUserAuthentication
+    {
+        return $GLOBALS['BE_USER'];
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FormDefinitionValidationService.php b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinitionValidationService.php
new file mode 100644 (file)
index 0000000..a6e87c1
--- /dev/null
@@ -0,0 +1,386 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Object\ObjectManager;
+use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessing;
+use TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing\ArrayProcessor;
+use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\CreatableFormElementPropertiesValidator;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\CreatablePropertyCollectionElementPropertiesValidator;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\FormElementHmacDataValidator;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\PropertyCollectionElementHmacDataValidator;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto;
+
+/**
+ * @internal
+ */
+class FormDefinitionValidationService implements SingletonInterface
+{
+
+    /**
+     * @var ConfigurationService
+     */
+    protected $configurationService;
+
+    /**
+     * Validate the form definition properties using the form setup.
+     * Pseudo workflow:
+     * Is the form element type creatable by the form editor?
+     *   YES
+     *     foreach(form element properties) (without finishers|validators)
+     *       is the form element property defined in the form setup (can be manipulated)?
+     *         YES
+     *           valid!
+     *         NO
+     *           is the form element property defined in "predefinedDefaults" in the form setup (cannot be manipulated but should be written)?
+     *             YES
+     *               is the form element property value equals to the value defined in "predefinedDefaults" in the form setup?
+     *                 YES
+     *                   valid!
+     *                 NO
+     *                   invalid! throw exception
+     *             NO
+     *               is there a hmac hash available for the form element property value (cannot be manipulated but should be written)?
+     *                 YES
+     *                   is the form element property value equals the historical value (and is the historical value valid)?
+     *                     YES
+     *                       valid!
+     *                     NO
+     *                       invalid! throw exception
+     *                 NO
+     *                   invalid! throw exception
+     *     foreach(form elements finishers|validators)
+     *       is the form elements finisher|validator creatable by the form editor?
+     *         YES
+     *           foreach(form elements finisher|validator properties)
+     *             is the form elements finisher|validator property defined in the form setup (can be manipulated)?
+     *               YES
+     *                 valid!
+     *               NO
+     *                 is the form elements finisher|validator property defined in "predefinedDefaults" in the form setup (cannot be manipulated but should be written)?
+     *                   YES
+     *                     is the form elements finisher|validator property value equals to the value defined in "predefinedDefaults" in the form setup?
+     *                       YES
+     *                         valid!
+     *                       NO
+     *                         invalid! throw exception
+     *                   NO
+     *                     is there a hmac hash available for the form elements finisher|validator property value (can not be manipulated but should be written)?
+     *                       YES
+     *                         is the form elements finisher|validator property value equals the historical value (and is the historical value valid)?
+     *                           YES
+     *                             valid!
+     *                           NO
+     *                             invalid! throw exception
+     *                       NO
+     *                         invalid! throw exception
+     *         NO
+     *           foreach(form elements finisher|validator properties)
+     *             is there a hmac hash available for the form elements finisher|validator property value (can not be manipulated but should be written)?
+     *               YES
+     *                 is the form elements finisher|validator property value equals the historical value (and is the historical value valid)?
+     *                   YES
+     *                     valid!
+     *                   NO
+     *                     invalid! throw exception
+     *               NO
+     *                 invalid! throw exception
+     *   NO
+     *     foreach(form element properties) (without finishers|validators)
+     *       is there a hmac hash available for the form element property value (cannot be manipulated but should be written)?
+     *         YES
+     *           is the form element property value equals the historical value (and is the historical value valid)?
+     *             YES
+     *               valid!
+     *             NO
+     *               invalid! throw exception
+     *         NO
+     *           invalid! throw exception
+     *     foreach(form elements finisher|validator properties)
+     *       is there a hmac hash available for the form elements finisher|validator property value (can not be manipulated but should be written)?
+     *         YES
+     *           is the form elements finisher|validator property value equals the historical value (and is the historical value valid)?
+     *             YES
+     *               valid!
+     *             NO
+     *               invalid! throw exception
+     *         NO
+     *           invalid! throw exception
+     *
+     * @param array $currentFormElement
+     * @param string $prototypeName
+     * @param string $sessionToken
+     * @throws PropertyException
+     */
+    public function validateFormDefinitionProperties(
+        array $currentFormElement,
+        string $prototypeName,
+        string $sessionToken
+    ): void {
+        $renderables = $currentFormElement['renderables'] ?? [];
+        $propertyCollectionElements = $currentFormElement['finishers'] ?? $currentFormElement['validators'] ?? [];
+        $propertyCollectionName = $currentFormElement['type'] === 'Form' ? 'finishers' : 'validators';
+        unset($currentFormElement['renderables'], $currentFormElement['finishers'], $currentFormElement['validators']);
+
+        $validationDto = GeneralUtility::makeInstance(
+            ValidationDto::class,
+            $prototypeName,
+            $currentFormElement['type'],
+            $currentFormElement['identifier'],
+            null,
+            $propertyCollectionName
+        );
+
+        if ($this->getConfigurationService()->isFormElementTypeCreatableByFormEditor($validationDto)) {
+            $this->validateAllPropertyValuesFromCreatableFormElement(
+                $currentFormElement,
+                $sessionToken,
+                $validationDto
+            );
+
+            foreach ($propertyCollectionElements as $propertyCollectionElement) {
+                $validationDto = $validationDto->withPropertyCollectionElementIdentifier(
+                    $propertyCollectionElement['identifier']
+                );
+
+                if ($this->getConfigurationService()->isPropertyCollectionElementIdentifierCreatableByFormEditor($validationDto)) {
+                    $this->validateAllPropertyValuesFromCreatablePropertyCollectionElement(
+                        $propertyCollectionElement,
+                        $sessionToken,
+                        $validationDto
+                    );
+                } else {
+                    $this->validateAllPropertyCollectionElementValuesByHmac(
+                        $propertyCollectionElement,
+                        $sessionToken,
+                        $validationDto
+                    );
+                }
+            }
+        } else {
+            $this->validateAllFormElementPropertyValuesByHmac($currentFormElement, $sessionToken, $validationDto);
+
+            foreach ($propertyCollectionElements as $propertyCollectionElement) {
+                $this->validateAllPropertyCollectionElementValuesByHmac(
+                    $propertyCollectionElement,
+                    $sessionToken,
+                    $validationDto
+                );
+            }
+        }
+
+        foreach ($renderables as $renderable) {
+            $this->validateFormDefinitionProperties($renderable, $prototypeName, $sessionToken);
+        }
+    }
+
+    /**
+     * Returns TRUE if a property value is equals to the historical value
+     * and FALSE if not.
+     * "Historical values" means values which are available within the form definition
+     * while the form editor is loaded and the values which are available after a
+     * successful validation of the form definition on a save operation.
+     * The value must be equal to the historical value if the property key for the value
+     * is not defined within the form setup.
+     * This means that the property can not be changed by the form editor but we want to keep the value
+     * in its original state.
+     * If this is not the case (return value is FALSE), an exception must be thrown.
+     *
+     * @param array $hmacContent
+     * @param mixed $propertyValue
+     * @param array $hmacData
+     * @param string $sessionToken
+     * @return bool
+     * @throws PropertyException
+     */
+    public function isPropertyValueEqualToHistoricalValue(
+        array $hmacContent,
+        $propertyValue,
+        array $hmacData,
+        string $sessionToken
+    ): bool {
+        $this->checkHmacDataIntegrity($hmacData, $hmacContent, $sessionToken);
+        $hmacContent[] = $propertyValue;
+
+        $expectedHash = GeneralUtility::hmac(serialize($hmacContent), $sessionToken);
+        return hash_equals($expectedHash, $hmacData['hmac']);
+    }
+
+    /**
+     * Compares the historical value and the hmac hash to ensure the integrity
+     * of the data.
+     * An exception will be thrown if the value is modified.
+     *
+     * @param array $hmacData
+     * @param array $hmacContent
+     * @param string $sessionToken
+     * @throws PropertyException
+     */
+    protected function checkHmacDataIntegrity(array $hmacData, array $hmacContent, string $sessionToken)
+    {
+        $hmac = $hmacData['hmac'] ?? null;
+        if (empty($hmac)) {
+            throw new PropertyException('Hmac must not be empty. #1528538222', 1528538222);
+        }
+
+        $hmacContent[] = $hmacData['value'] ?? '';
+        $expectedHash = GeneralUtility::hmac(serialize($hmacContent), $sessionToken);
+
+        if (!hash_equals($expectedHash, $hmac)) {
+            throw new PropertyException('Unauthorized modification of historical data. #1528538252', 1528538252);
+        }
+    }
+
+    /**
+     * Walk through all form element properties and checks
+     * if the values matches to their hmac hashes.
+     *
+     * @param array $currentElement
+     * @param string $sessionToken
+     * @param ValidationDto $validationDto
+     */
+    protected function validateAllFormElementPropertyValuesByHmac(
+        array $currentElement,
+        $sessionToken,
+        ValidationDto $validationDto
+    ): void {
+        GeneralUtility::makeInstance(ArrayProcessor::class, $currentElement)->forEach(
+            GeneralUtility::makeInstance(
+                ArrayProcessing::class,
+                'validateProperties',
+                '^(?!(_orig_.*|.*\._orig_.*)$).*',
+                GeneralUtility::makeInstance(
+                    FormElementHmacDataValidator::class,
+                    $currentElement,
+                    $sessionToken,
+                    $validationDto
+                )
+            )
+        );
+    }
+
+    /**
+     * Walk through all property collection properties and checks
+     * if the values matches to their hmac hashes.
+     *
+     * @param array $currentElement
+     * @param string $sessionToken
+     * @param ValidationDto $validationDto
+     */
+    protected function validateAllPropertyCollectionElementValuesByHmac(
+        array $currentElement,
+        $sessionToken,
+        ValidationDto $validationDto
+    ): void {
+        GeneralUtility::makeInstance(ArrayProcessor::class, $currentElement)->forEach(
+            GeneralUtility::makeInstance(
+                ArrayProcessing::class,
+                'validateProperties',
+                '^(?!(_orig_.*|.*\._orig_.*)$).*',
+                GeneralUtility::makeInstance(
+                    PropertyCollectionElementHmacDataValidator::class,
+                    $currentElement,
+                    $sessionToken,
+                    $validationDto
+                )
+            )
+        );
+    }
+
+    /**
+     * Walk through all form element properties and checks
+     * if the property is defined within the form editor setup
+     * or if the property is definied within the "predefinedDefaults" in the form editor setup
+     * and the property value matches the predefined value
+     * or if there is a valid hmac hash for the value.
+     *
+     * @param array $currentElement
+     * @param string $sessionToken
+     * @param ValidationDto $validationDto
+     */
+    protected function validateAllPropertyValuesFromCreatableFormElement(
+        array $currentElement,
+        $sessionToken,
+        ValidationDto $validationDto
+    ): void {
+        GeneralUtility::makeInstance(ArrayProcessor::class, $currentElement)->forEach(
+            GeneralUtility::makeInstance(
+                ArrayProcessing::class,
+                'validateProperties',
+                '^(?!(_orig_.*|.*\._orig_.*|type|identifier)$).*',
+                GeneralUtility::makeInstance(
+                    CreatableFormElementPropertiesValidator::class,
+                    $currentElement,
+                    $sessionToken,
+                    $validationDto
+                )
+            )
+        );
+    }
+
+    /**
+     * Walk through all property collection properties and checks
+     * if the property is defined within the form editor setup
+     * or if the property is definied within the "predefinedDefaults" in the form editor setup
+     * and the property value matches the predefined value
+     * or if there is a valid hmac hash for the value.
+     *
+     * @param array $currentElement
+     * @param string $sessionToken
+     * @param ValidationDto $validationDto
+     */
+    protected function validateAllPropertyValuesFromCreatablePropertyCollectionElement(
+        array $currentElement,
+        $sessionToken,
+        ValidationDto $validationDto
+    ): void {
+        GeneralUtility::makeInstance(ArrayProcessor::class, $currentElement)->forEach(
+            GeneralUtility::makeInstance(
+                ArrayProcessing::class,
+                'validateProperties',
+                '^(?!(_orig_.*|.*\._orig_.*|identifier)$).*',
+                GeneralUtility::makeInstance(
+                    CreatablePropertyCollectionElementPropertiesValidator::class,
+                    $currentElement,
+                    $sessionToken,
+                    $validationDto
+                )
+            )
+        );
+    }
+
+    /**
+     * @return ConfigurationService
+     */
+    protected function getConfigurationService(): ConfigurationService
+    {
+        if (!($this->configurationService instanceof ConfigurationService)) {
+            $this->configurationService = $this->getObjectManager()->get(ConfigurationService::class);
+        }
+        return $this->configurationService;
+    }
+
+    /**
+     * @return ObjectManager
+     */
+    protected function getObjectManager(): ObjectManager
+    {
+        return GeneralUtility::makeInstance(ObjectManager::class);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/AbstractExtractor.php b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/AbstractExtractor.php
new file mode 100644 (file)
index 0000000..c4ac1e1
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * @internal
+ */
+abstract class AbstractExtractor implements ExtractorInterface
+{
+
+    /**
+     * @var ExtractorDto
+     */
+    protected $extractorDto;
+
+    /**
+     * @param ExtractorDto $extractorDto
+     */
+    public function __construct(ExtractorDto $extractorDto)
+    {
+        $this->extractorDto = $extractorDto;
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/AdditionalElementPropertyPathsExtractor.php b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/AdditionalElementPropertyPathsExtractor.php
new file mode 100644 (file)
index 0000000..4ff4d7c
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * @internal
+ */
+class AdditionalElementPropertyPathsExtractor extends AbstractExtractor
+{
+
+    /**
+     * @param string $_
+     * @param mixed $value
+     * @param array $matches
+     */
+    public function __invoke(string $_, $value, array $matches)
+    {
+        [, $formElementType] = $matches;
+
+        $result = $this->extractorDto->getResult();
+        $result['formElements'][$formElementType]['additionalElementPropertyPaths'][] = $value;
+        $this->extractorDto->setResult($result);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/ExtractorDto.php b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/ExtractorDto.php
new file mode 100644 (file)
index 0000000..9049de2
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * @internal
+ */
+class ExtractorDto
+{
+
+    /**
+     * @var array
+     */
+    protected $prototypeConfiguration;
+
+    /**
+     * @var array
+     */
+    protected $result = [];
+
+    /**
+     * @param array $prototypeConfiguration
+     */
+    public function __construct(array $prototypeConfiguration)
+    {
+        $this->prototypeConfiguration = $prototypeConfiguration;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPrototypeConfiguration(): array
+    {
+        return $this->prototypeConfiguration;
+    }
+
+    /**
+     * @return string
+     */
+    public function getResult(): array
+    {
+        return $this->result;
+    }
+
+    /**
+     * @return string
+     * @return ExtractorDto
+     */
+    public function setResult(array $result): ExtractorDto
+    {
+        $this->result = $result;
+        return $this;
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/ExtractorInterface.php b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/ExtractorInterface.php
new file mode 100644 (file)
index 0000000..cb7e12d
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * @internal
+ */
+interface ExtractorInterface
+{
+
+    /**
+     * @param ExtractorDto $extractorDto
+     */
+    public function __construct(ExtractorDto $extractorDto);
+
+    /**
+     * @param string $key
+     * @param mixed $value
+     * @param array $matches
+     */
+    public function __invoke(string $key, $value, array $matches);
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/IsCreatableFormElementExtractor.php b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/IsCreatableFormElementExtractor.php
new file mode 100644 (file)
index 0000000..576cc8e
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
+
+/**
+ * @internal
+ */
+class IsCreatableFormElementExtractor extends AbstractExtractor
+{
+
+    /**
+     * @param string $_
+     * @param mixed $value
+     * @param array $matches
+     */
+    public function __invoke(string $_, $value, array $matches)
+    {
+        [, $formElementType] = $matches;
+
+        $formElementGroup = $value;
+
+        $result = $this->extractorDto->getResult();
+
+        if (!ArrayUtility::isValidPath(
+            $this->extractorDto->getPrototypeConfiguration(),
+            'formElementsDefinition.' . $formElementType . '.formEditor.groupSorting',
+            '.'
+        )) {
+            $result['formElements'][$formElementType]['creatable'] = false;
+            $this->extractorDto->setResult($result);
+            return;
+        }
+
+        $formElementGroups = array_keys(
+            ArrayUtility::getValueByPath($this->extractorDto->getPrototypeConfiguration(), 'formEditor.formElementGroups', '.')
+        );
+
+        $result['formElements'][$formElementType]['creatable'] = in_array(
+            $formElementGroup,
+            $formElementGroups,
+            true
+        );
+
+        $this->extractorDto->setResult($result);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/MultiValuePropertiesExtractor.php b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/MultiValuePropertiesExtractor.php
new file mode 100644 (file)
index 0000000..d0e6f2f
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
+
+/**
+ * @internal
+ */
+class MultiValuePropertiesExtractor extends AbstractExtractor
+{
+
+    /**
+     * @param string $_
+     * @param mixed $value
+     * @param array $matches
+     */
+    public function __invoke(string $_, $value, array $matches)
+    {
+        [, $formElementType, $formEditorIndex] = $matches;
+
+        if (
+            $value !== 'Inspector-PropertyGridEditor'
+            && $value !== 'Inspector-MultiSelectEditor'
+            && $value !== 'Inspector-ValidationErrorMessageEditor'
+        ) {
+            return;
+        }
+
+        $propertyPath = implode(
+            '.',
+            [
+                'formElementsDefinition',
+                $formElementType,
+                'formEditor',
+                'editors',
+                $formEditorIndex,
+                'propertyPath',
+            ]
+        );
+
+        $result = $this->extractorDto->getResult();
+        $result['formElements'][$formElementType]['multiValueProperties'][] = ArrayUtility::getValueByPath(
+            $this->extractorDto->getPrototypeConfiguration(),
+            $propertyPath,
+            '.'
+        );
+        $this->extractorDto->setResult($result);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/PredefinedDefaultsExtractor.php b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/PredefinedDefaultsExtractor.php
new file mode 100644 (file)
index 0000000..768b8f4
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
+
+/**
+ * @internal
+ */
+class PredefinedDefaultsExtractor extends AbstractExtractor
+{
+
+    /**
+     * @param string $_
+     * @param mixed $value
+     * @param array $matches
+     */
+    public function __invoke(string $_, $value, array $matches)
+    {
+        [, $formElementType, $propertyPath] = $matches;
+
+        $result = $this->extractorDto->getResult();
+        $result['formElements'][$formElementType]['predefinedDefaults'][$propertyPath] = $value;
+        $this->extractorDto->setResult($result);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/PropertyPathsExtractor.php b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/PropertyPathsExtractor.php
new file mode 100644 (file)
index 0000000..632c476
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\FormElement;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
+
+/**
+ * @internal
+ */
+class PropertyPathsExtractor extends AbstractExtractor
+{
+
+    /**
+     * @param string $_
+     * @param mixed $value
+     * @param array $matches
+     */
+    public function __invoke(string $_, $value, array $matches)
+    {
+        $formElementPropertyPaths = $this->getPropertyPaths($value, $matches);
+
+        $result = $this->extractorDto->getResult();
+        $result = array_merge_recursive($result, ['formElements' => $formElementPropertyPaths]);
+        $this->extractorDto->setResult($result);
+    }
+
+    /**
+     * @param string $value
+     * @param array $matches
+     * @return array
+     */
+    protected function getPropertyPaths(string $value, array $matches): array
+    {
+        [, $formElementType, $formEditorIndex] = $matches;
+
+        $paths[$formElementType]['propertyPaths'] = [];
+        $templateNamePath = implode(
+            '.',
+            [
+                'formElementsDefinition',
+                $formElementType,
+                'formEditor',
+                'editors',
+                $formEditorIndex,
+                'templateName',
+            ]
+        );
+        $templateName = ArrayUtility::getValueByPath(
+            $this->extractorDto->getPrototypeConfiguration(),
+            $templateNamePath,
+            '.'
+        );
+
+        // Special processing of "Inspector-GridColumnViewPortConfigurationEditor" inspector editors.
+        // Expand the property path which contains a "{@viewPortIdentifier}" placeholder
+        // to X property paths which contain all available placeholder replacements.
+        if ($templateName === 'Inspector-GridColumnViewPortConfigurationEditor') {
+            $viewPortsPath = implode(
+                '.',
+                [
+                    'formElementsDefinition',
+                    $formElementType,
+                    'formEditor',
+                    'editors',
+                    $formEditorIndex,
+                    'configurationOptions',
+                    'viewPorts',
+                ]
+            );
+            $viewPorts = ArrayUtility::getValueByPath($this->extractorDto->getPrototypeConfiguration(), $viewPortsPath, '.');
+            foreach ($viewPorts as $viewPort) {
+                $viewPortIdentifier = $viewPort['viewPortIdentifier'];
+                $propertyPath = str_replace('{@viewPortIdentifier}', $viewPortIdentifier, $value);
+                $paths[$formElementType]['propertyPaths'][] = $propertyPath;
+            }
+        } else {
+            $paths[$formElementType]['propertyPaths'][] = $value;
+        }
+        return $paths;
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/IsCreatablePropertyCollectionElementExtractor.php b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/IsCreatablePropertyCollectionElementExtractor.php
new file mode 100644 (file)
index 0000000..94bca6b
--- /dev/null
@@ -0,0 +1,104 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
+
+/**
+ * @internal
+ */
+class IsCreatablePropertyCollectionElementExtractor extends AbstractExtractor
+{
+
+    /**
+     * @param string $_
+     * @param mixed $value
+     * @param array $matches
+     */
+    public function __invoke(string $_, $value, array $matches)
+    {
+        [, $formElementType, $formEditorIndex] = $matches;
+
+        if (
+            $value !== 'Inspector-FinishersEditor'
+            && $value !== 'Inspector-ValidatorsEditor'
+            && $value !== 'Inspector-RequiredValidatorEditor'
+        ) {
+            return;
+        }
+
+        $propertyCollectionName = $value === 'Inspector-FinishersEditor' ? 'finishers' : 'validators';
+
+        $result = $this->extractorDto->getResult();
+
+        if (
+            $value === 'Inspector-FinishersEditor'
+            || $value === 'Inspector-ValidatorsEditor'
+        ) {
+            $selectOptionsPath = implode(
+                '.',
+                [
+                    'formElementsDefinition',
+                    $formElementType,
+                    'formEditor',
+                    'editors',
+                    $formEditorIndex,
+                    'selectOptions',
+                ]
+            );
+            if (!ArrayUtility::isValidPath($this->extractorDto->getPrototypeConfiguration(), $selectOptionsPath, '.')) {
+                return;
+            }
+            $selectOptions = ArrayUtility::getValueByPath(
+                $this->extractorDto->getPrototypeConfiguration(),
+                $selectOptionsPath,
+                '.'
+            );
+            foreach ($selectOptions as $selectOption) {
+                $validatorIdentifier = $selectOption['value'] ?? '';
+                if (empty($validatorIdentifier)) {
+                    continue;
+                }
+
+                $result['formElements'][$formElementType]['collections'][$propertyCollectionName][$validatorIdentifier]['creatable'] = true;
+            }
+        } else {
+            $validatorIdentifierPath = implode(
+                '.',
+                [
+                    'formElementsDefinition',
+                    $formElementType,
+                    'formEditor',
+                    'editors',
+                    $formEditorIndex,
+                    'validatorIdentifier',
+                ]
+            );
+            if (!ArrayUtility::isValidPath($this->extractorDto->getPrototypeConfiguration(), $validatorIdentifierPath, '.')) {
+                return;
+            }
+            $validatorIdentifier = ArrayUtility::getValueByPath(
+                $this->extractorDto->getPrototypeConfiguration(),
+                $validatorIdentifierPath,
+                '.'
+            );
+            $result['formElements'][$formElementType]['collections'][$propertyCollectionName][$validatorIdentifier]['creatable'] = true;
+        }
+
+        $this->extractorDto->setResult($result);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/MultiValuePropertiesExtractor.php b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/MultiValuePropertiesExtractor.php
new file mode 100644 (file)
index 0000000..baa32c5
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
+
+/**
+ * @internal
+ */
+class MultiValuePropertiesExtractor extends AbstractExtractor
+{
+
+    /**
+     * @param string $_
+     * @param mixed $value
+     * @param array $matches
+     */
+    public function __invoke(string $_, $value, array $matches)
+    {
+        [, $formElementType, $propertyCollectionName, $propertyCollectionIndex, $propertyCollectionEditorIndex] = $matches;
+
+        if (
+            $value !== 'Inspector-PropertyGridEditor'
+            && $value !== 'Inspector-MultiSelectEditor'
+            && $value !== 'Inspector-ValidationErrorMessageEditor'
+        ) {
+            return;
+        }
+
+        $propertyPath = implode(
+            '.',
+            [
+                'formElementsDefinition',
+                $formElementType,
+                'formEditor',
+                'propertyCollections',
+                $propertyCollectionName,
+                $propertyCollectionIndex,
+                'editors',
+                $propertyCollectionEditorIndex,
+                'propertyPath',
+            ]
+        );
+        $propertyValue = ArrayUtility::getValueByPath($this->extractorDto->getPrototypeConfiguration(), $propertyPath, '.');
+
+        $result = $this->extractorDto->getResult();
+
+        if (
+            $value === 'Inspector-PropertyGridEditor'
+            || $value === 'Inspector-MultiSelectEditor'
+        ) {
+            $identifierPath = implode(
+                '.',
+                [
+                    'formElementsDefinition',
+                    $formElementType,
+                    'formEditor',
+                    'propertyCollections',
+                    $propertyCollectionName,
+                    $propertyCollectionIndex,
+                    'identifier',
+                ]
+            );
+            $identifier = ArrayUtility::getValueByPath($this->extractorDto->getPrototypeConfiguration(), $identifierPath, '.');
+
+            $result['formElements'][$formElementType]['collections'][$propertyCollectionName][$identifier]['multiValueProperties'][] = $propertyValue;
+        } else {
+            $result['formElements'][$formElementType]['multiValueProperties'][] = $propertyValue;
+        }
+
+        $this->extractorDto->setResult($result);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/PredefinedDefaultsExtractor.php b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/PredefinedDefaultsExtractor.php
new file mode 100644 (file)
index 0000000..d92edae
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
+
+/**
+ * @internal
+ */
+class PredefinedDefaultsExtractor extends AbstractExtractor
+{
+
+    /**
+     * @param string $_
+     * @param mixed $value
+     * @param array $matches
+     */
+    public function __invoke(string $_, $value, array $matches)
+    {
+        [, $propertyCollectionName, $propertyCollectionElementIdentifier, $propertyPath] = $matches;
+        $propertyCollectionName = str_replace('Definition', '', $propertyCollectionName);
+
+        $result = $this->extractorDto->getResult();
+        $result['collections'][$propertyCollectionName][$propertyCollectionElementIdentifier]['predefinedDefaults'][$propertyPath] = $value;
+        $this->extractorDto->setResult($result);
+    }
+}
diff --git a/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/PropertyPathsExtractor.php b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/PropertyPathsExtractor.php
new file mode 100644 (file)
index 0000000..8f66bfe
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\PropertyCollectionElement;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+use TYPO3\CMS\Form\Domain\Configuration\FrameworkConfiguration\Extractors\AbstractExtractor;
+
+/**
+ * @internal
+ */
+class PropertyPathsExtractor extends AbstractExtractor
+{
+
+    /**
+     * @param string $_
+     * @param mixed $value
+     * @param array $matches
+     */
+    public function __invoke(string $_, $value, array $matches)
+    {
+        [, $formElementType, $propertyCollectionName, $propertyCollectionIndex] = $matches;
+
+        $identifierPath = implode(
+            '.',
+            [
+                'formElementsDefinition',
+                $formElementType,
+                'formEditor',
+                'propertyCollections',
+                $propertyCollectionName,
+                $propertyCollectionIndex,
+                'identifier',
+            ]
+        );
+        $identifier = ArrayUtility::getValueByPath($this->extractorDto->getPrototypeConfiguration(), $identifierPath, '.');
+
+        $result = $this->extractorDto->getResult();
+        $result['formElements'][$formElementType]['collections'][$propertyCollectionName][$identifier]['propertyPaths'][] = $value;
+        $this->extractorDto->setResult($result);
+    }
+}
index e05b32f..4ad9855 100644 (file)
@@ -15,10 +15,15 @@ namespace TYPO3\CMS\Form\Mvc\Property\TypeConverter;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
-use TYPO3\CMS\Extbase\Property\Exception as PropertyException;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
 use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter;
+use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
+use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionConversionService;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionValidationService;
 use TYPO3\CMS\Form\Type\FormDefinitionArray;
 
 /**
@@ -44,6 +49,11 @@ class FormDefinitionArrayConverter extends AbstractTypeConverter
     protected $priority = 10;
 
     /**
+     * @var ConfigurationService
+     */
+    protected $configurationService;
+
+    /**
      * Convert from $source to $targetType, a noop if the source is an array.
      * If it is an empty string it will be converted to an empty array.
      *
@@ -62,10 +72,40 @@ class FormDefinitionArrayConverter extends AbstractTypeConverter
             throw new PropertyException('Unable to decode JSON source: ' . json_last_error_msg(), 1512578002);
         }
 
-        $rawFormDefinitionArray = ArrayUtility::stripTagsFromValuesRecursive($rawFormDefinitionArray);
+        $formDefinitionValidationService = $this->getFormDefinitionValidationService();
+        $formDefinitionConversionService = $this->getFormDefinitionConversionService();
+
+        // Extend the hmac hashing key with the "per form editor session (load / save)" unique key.
+        // @see \TYPO3\CMS\Form\Domain\Configuration\FormDefinitionConversionService::addHmacData
+        $sessionToken = $this->retrieveSessionToken();
+
+        $prototypeName = $rawFormDefinitionArray['prototypeName'] ?? null;
+        $identifier = $rawFormDefinitionArray['identifier'] ?? null;
+
+        // A modification of the properties "prototypeName" and "identifier" from the root form element
+        // through the form editor is always forbidden.
+        try {
+            if (!$formDefinitionValidationService->isPropertyValueEqualToHistoricalValue([$identifier, 'identifier'], $identifier, $rawFormDefinitionArray['_orig_identifier'] ?? [], $sessionToken)) {
+                throw new PropertyException('Unauthorized modification of "identifier".', 1528538324);
+            }
+
+            if (!$formDefinitionValidationService->isPropertyValueEqualToHistoricalValue([$identifier, 'prototypeName'], $prototypeName, $rawFormDefinitionArray['_orig_prototypeName'] ?? [], $sessionToken)) {
+                throw new PropertyException('Unauthorized modification of "prototype name".', 1528538323);
+            }
+        } catch (PropertyException $e) {
+            throw new PropertyException('Unauthorized modification of "prototype name" or "identifier".', 1528538322);
+        }
+
+        $formDefinitionValidationService->validateFormDefinitionProperties($rawFormDefinitionArray, $prototypeName, $sessionToken);
+
+        // @todo move all the transformations to FormDefinitionConversionService
+        $rawFormDefinitionArray = $this->filterEmptyArrays($rawFormDefinitionArray);
         $rawFormDefinitionArray = $this->transformMultiValueElementsForFormFramework($rawFormDefinitionArray);
-        $formDefinitionArray = new FormDefinitionArray($rawFormDefinitionArray);
+        // @todo: replace with rte parsing
+        $rawFormDefinitionArray = ArrayUtility::stripTagsFromValuesRecursive($rawFormDefinitionArray);
+        $rawFormDefinitionArray = $formDefinitionConversionService->removeHmacData($rawFormDefinitionArray);
 
+        $formDefinitionArray = GeneralUtility::makeInstance(FormDefinitionArray::class, $rawFormDefinitionArray);
         return $formDefinitionArray;
     }
 
@@ -79,7 +119,7 @@ class FormDefinitionArrayConverter extends AbstractTypeConverter
      *   _value => 'value'
      * ]
      *
-     * This method transform this into:
+     * This method transforms this into:
      *
      * [
      *   'value' => 'label'
@@ -107,4 +147,62 @@ class FormDefinitionArrayConverter extends AbstractTypeConverter
 
         return $output;
     }
+
+    /**
+     * Remove keys from an array if the key value is an empty array
+     *
+     * @todo ArrayUtility?
+     * @param array $array
+     * @return array
+     */
+    protected function filterEmptyArrays(array $array): array
+    {
+        foreach ($array as $key => $value) {
+            if (!is_array($value)) {
+                continue;
+            }
+            if (empty($value)) {
+                unset($array[$key]);
+                continue;
+            }
+            $array[$key] = $this->filterEmptyArrays($value);
+            if (empty($array[$key])) {
+                unset($array[$key]);
+            }
+        }
+
+        return $array;
+    }
+
+    /**
+     * @return string
+     */
+    protected function retrieveSessionToken(): string
+    {
+        return $this->getBackendUser()->getSessionData('extFormProtectionSessionToken');
+    }
+
+    /**
+     * @return FormDefinitionValidationService
+     */
+    protected function getFormDefinitionValidationService(): FormDefinitionValidationService
+    {
+        return GeneralUtility::makeInstance(FormDefinitionValidationService::class);
+    }
+
+    /**
+     * @return FormDefinitionConversionService
+     */
+    protected function getFormDefinitionConversionService(): FormDefinitionConversionService
+    {
+        return GeneralUtility::makeInstance(FormDefinitionConversionService::class);
+    }
+
+    /**
+     * @return BackendUserAuthentication
+     */
+    protected function getBackendUser(): BackendUserAuthentication
+    {
+        return $GLOBALS['BE_USER'];
+    }
 }
index 1b3fcfd..359969f 100644 (file)
@@ -33,6 +33,15 @@ class FormEditorControllerTest extends UnitTestCase
     protected $resetSingletonInstances = true;
 
     /**
+     * Set up
+     */
+    public function setUp()
+    {
+        parent::setUp();
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] = 12345;
+    }
+
+    /**
      * @test
      */
     public function getInsertRenderablesPanelConfigurationReturnsGroupedAndSortedConfiguration(): void
index 83bcf02..c503b0c 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+declare(strict_types = 1);
 namespace TYPO3\CMS\Form\Tests\Unit\Domain\Configuration;
 
 /*
@@ -15,7 +16,10 @@ namespace TYPO3\CMS\Form\Tests\Unit\Domain\Configuration;
  */
 
 use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
+use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
 use TYPO3\CMS\Form\Domain\Configuration\Exception\PrototypeNotFoundException;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto;
+use TYPO3\CMS\Form\Service\TranslationService;
 
 /**
  * Test case
@@ -28,17 +32,26 @@ class ConfigurationServiceTest extends \TYPO3\TestingFramework\Core\Unit\UnitTes
      */
     public function getPrototypeConfigurationReturnsPrototypeConfiguration()
     {
-        $mockConfigurationService = $this->getAccessibleMock(ConfigurationService::class, [
-            'dummy'
-        ], [], '', false);
+        $mockConfigurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            [
+                'dummy',
+            ],
+            [],
+            '',
+            false
+        );
 
-        $mockConfigurationService->_set('formSettings', [
-            'prototypes' => [
-                'standard' => [
-                    'key' => 'value',
+        $mockConfigurationService->_set(
+            'formSettings',
+            [
+                'prototypes' => [
+                    'standard' => [
+                        'key' => 'value',
+                    ],
                 ],
-            ],
-        ]);
+            ]
+        );
 
         $expected = [
             'key' => 'value',
@@ -52,19 +65,1403 @@ class ConfigurationServiceTest extends \TYPO3\TestingFramework\Core\Unit\UnitTes
      */
     public function getPrototypeConfigurationThrowsExceptionIfNoPrototypeFound()
     {
-        $mockConfigurationService = $this->getAccessibleMock(ConfigurationService::class, [
-            'dummy'
-        ], [], '', false);
+        $mockConfigurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            [
+                'dummy',
+            ],
+            [],
+            '',
+            false
+        );
 
         $this->expectException(PrototypeNotFoundException::class);
         $this->expectExceptionCode(1475924277);
 
-        $mockConfigurationService->_set('formSettings', [
-            'prototypes' => [
-                'noStandard' => [],
+        $mockConfigurationService->_set(
+            'formSettings',
+            [
+                'prototypes' => [
+                    'noStandard' => [],
                 ],
-            ]);
+            ]
+        );
 
         $mockConfigurationService->getPrototypeConfiguration('standard');
     }
+
+    /**
+     * @test
+     */
+    public function getSelectablePrototypeNamesDefinedInFormEditorSetupReturnsPrototypes()
+    {
+        $configurationService = $this->getAccessibleMock(ConfigurationService::class, ['dummy'], [], '', false);
+
+        $configurationService->_set(
+            'formSettings',
+            [
+                'formManager' => [
+                    'selectablePrototypesConfiguration' => [
+                        0 => [
+                            'identifier' => 'standard',
+                        ],
+                        1 => [
+                            'identifier' => 'custom',
+                        ],
+                        'a' => [
+                            'identifier' => 'custom-2',
+                        ],
+                    ],
+                ],
+            ]
+        );
+
+        $expected = [
+            'standard',
+            'custom',
+        ];
+
+        $this->assertSame($expected, $configurationService->getSelectablePrototypeNamesDefinedInFormEditorSetup());
+    }
+
+    /**
+     * @test
+     * @dataProvider isFormElementPropertyDefinedInFormEditorSetupDataProvider
+     * @param array $configuration
+     * @param ValidationDto $validationDto
+     * @param bool $expectedReturn
+     */
+    public function isFormElementPropertyDefinedInFormEditorSetup(
+        array $configuration,
+        ValidationDto $validationDto,
+        bool $expectedReturn
+    ) {
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            ['buildFormDefinitionValidationConfigurationFromFormEditorSetup'],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method(
+            'buildFormDefinitionValidationConfigurationFromFormEditorSetup'
+        )->willReturn($configuration);
+
+        $this->assertSame(
+            $expectedReturn,
+            $configurationService->isFormElementPropertyDefinedInFormEditorSetup($validationDto)
+        );
+    }
+
+    /**
+     * @test
+     * @dataProvider isPropertyCollectionPropertyDefinedInFormEditorSetupDataProvider
+     * @param array $configuration
+     * @param ValidationDto $validationDto
+     * @param bool $expectedReturn
+     */
+    public function isPropertyCollectionPropertyDefinedInFormEditorSetup(
+        array $configuration,
+        ValidationDto $validationDto,
+        bool $expectedReturn
+    ) {
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            ['buildFormDefinitionValidationConfigurationFromFormEditorSetup'],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method(
+            'buildFormDefinitionValidationConfigurationFromFormEditorSetup'
+        )->willReturn($configuration);
+
+        $this->assertSame(
+            $expectedReturn,
+            $configurationService->isPropertyCollectionPropertyDefinedInFormEditorSetup($validationDto)
+        );
+    }
+
+    /**
+     * @test
+     * @dataProvider isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetupDataProvider
+     * @param array $configuration
+     * @param ValidationDto $validationDto
+     * @param bool $expectedReturn
+     */
+    public function isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup(
+        array $configuration,
+        ValidationDto $validationDto,
+        bool $expectedReturn
+    ) {
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            ['buildFormDefinitionValidationConfigurationFromFormEditorSetup'],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method(
+            'buildFormDefinitionValidationConfigurationFromFormEditorSetup'
+        )->willReturn($configuration);
+
+        $this->assertSame(
+            $expectedReturn,
+            $configurationService->isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup($validationDto)
+        );
+    }
+
+    /**
+     * @test
+     * @dataProvider isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetupDataProvider
+     * @param array $configuration
+     * @param ValidationDto $validationDto
+     * @param bool $expectedReturn
+     */
+    public function isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup(
+        array $configuration,
+        ValidationDto $validationDto,
+        bool $expectedReturn
+    ) {
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            ['buildFormDefinitionValidationConfigurationFromFormEditorSetup'],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method(
+            'buildFormDefinitionValidationConfigurationFromFormEditorSetup'
+        )->willReturn($configuration);
+
+        $this->assertSame(
+            $expectedReturn,
+            $configurationService->isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup(
+                $validationDto
+            )
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function getFormElementPredefinedDefaultValueFromFormEditorSetupThrowsExceptionIfNoPredefinedDefaultIsAvailable(
+    ) {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528578401);
+
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            ['isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup'],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method(
+            'isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup'
+        )->willReturn(false);
+        $validationDto = new ValidationDto(null, 'Text', null, 'properties.foo.1');
+
+        $configurationService->getFormElementPredefinedDefaultValueFromFormEditorSetup($validationDto);
+    }
+
+    /**
+     * @test
+     */
+    public function getFormElementPredefinedDefaultValueFromFormEditorSetupReturnsDefaultValue()
+    {
+        $expected = 'foo';
+        $configuration = ['formElements' => ['Text' => ['predefinedDefaults' => ['properties.foo.1' => $expected]]]];
+
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            [
+                'buildFormDefinitionValidationConfigurationFromFormEditorSetup',
+                'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup',
+            ],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method(
+            'buildFormDefinitionValidationConfigurationFromFormEditorSetup'
+        )->willReturn($configuration);
+        $configurationService->expects($this->any())->method(
+            'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup'
+        )->willReturn(true);
+
+        $validationDto = new ValidationDto('standard', 'Text', null, 'properties.foo.1');
+
+        $this->assertSame(
+            $expected,
+            $configurationService->getFormElementPredefinedDefaultValueFromFormEditorSetup($validationDto)
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function getPropertyCollectionPredefinedDefaultValueFromFormEditorSetupThrowsExceptionIfNoPredefinedDefaultIsAvailable(
+    ) {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528578402);
+
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            ['isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup'],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method(
+            'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup'
+        )->willReturn(false);
+        $validationDto = new ValidationDto(
+            null,
+            null,
+            null,
+            'properties.foo.1',
+            'validators',
+            'StringLength'
+        );
+
+        $configurationService->getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup($validationDto);
+    }
+
+    /**
+     * @test
+     */
+    public function getPropertyCollectionPredefinedDefaultValueFromFormEditorSetupReturnsDefaultValue()
+    {
+        $expected = 'foo';
+        $configuration = ['collections' => ['validators' => ['StringLength' => ['predefinedDefaults' => ['properties.foo.1' => $expected]]]]];
+
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            [
+                'buildFormDefinitionValidationConfigurationFromFormEditorSetup',
+                'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup',
+            ],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method(
+            'buildFormDefinitionValidationConfigurationFromFormEditorSetup'
+        )->willReturn($configuration);
+        $configurationService->expects($this->any())->method(
+            'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup'
+        )->willReturn(true);
+
+        $validationDto = new ValidationDto(
+            'standard',
+            null,
+            null,
+            'properties.foo.1',
+            'validators',
+            'StringLength'
+        );
+
+        $this->assertSame(
+            $expected,
+            $configurationService->getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup($validationDto)
+        );
+    }
+
+    /**
+     * @test
+     * @dataProvider isFormElementTypeCreatableByFormEditorDataProvider
+     * @param array $configuration
+     * @param ValidationDto $validationDto
+     * @param bool $expectedReturn
+     */
+    public function isFormElementTypeCreatableByFormEditor(
+        array $configuration,
+        ValidationDto $validationDto,
+        bool $expectedReturn
+    ) {
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            ['buildFormDefinitionValidationConfigurationFromFormEditorSetup'],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method(
+            'buildFormDefinitionValidationConfigurationFromFormEditorSetup'
+        )->willReturn($configuration);
+
+        $this->assertSame(
+            $expectedReturn,
+            $configurationService->isFormElementTypeCreatableByFormEditor($validationDto)
+        );
+    }
+
+    /**
+     * @test
+     * @dataProvider isPropertyCollectionElementIdentifierCreatableByFormEditorDataProvider
+     * @param array $configuration
+     * @param ValidationDto $validationDto
+     * @param bool $expectedReturn
+     */
+    public function isPropertyCollectionElementIdentifierCreatableByFormEditor(
+        array $configuration,
+        ValidationDto $validationDto,
+        bool $expectedReturn
+    ) {
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            ['buildFormDefinitionValidationConfigurationFromFormEditorSetup'],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method(
+            'buildFormDefinitionValidationConfigurationFromFormEditorSetup'
+        )->willReturn($configuration);
+
+        $this->assertSame(
+            $expectedReturn,
+            $configurationService->isPropertyCollectionElementIdentifierCreatableByFormEditor($validationDto)
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function isFormElementTypeDefinedInFormSetup()
+    {
+        $configuration = [
+            'formElementsDefinition' => [
+                'Text' => [],
+            ],
+        ];
+
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            ['getPrototypeConfiguration'],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method('getPrototypeConfiguration')->willReturn($configuration);
+
+        $validationDto = new ValidationDto('standard', 'Text');
+        $this->assertTrue($configurationService->isFormElementTypeDefinedInFormSetup($validationDto));
+
+        $validationDto = new ValidationDto('standard', 'Foo');
+        $this->assertFalse($configurationService->isFormElementTypeDefinedInFormSetup($validationDto));
+    }
+
+    /**
+     * @test
+     */
+    public function addAdditionalPropertyPathsFromHookThrowsExceptionIfHookResultIsNoFormDefinitionValidation()
+    {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528633966);
+
+        $configurationService = $this->getAccessibleMock(ConfigurationService::class, ['dummy'], [], '', false);
+        $input = ['dummy'];
+
+        $configurationService->_call('addAdditionalPropertyPathsFromHook', '', '', $input, []);
+    }
+
+    /**
+     * @test
+     */
+    public function addAdditionalPropertyPathsFromHookThrowsExceptionIfPrototypeDoesNotMatch()
+    {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528634966);
+
+        $configurationService = $this->getAccessibleMock(ConfigurationService::class, ['dummy'], [], '', false);
+        $validationDto = new ValidationDto('Bar', 'Foo');
+        $input = [$validationDto];
+
+        $configurationService->_call('addAdditionalPropertyPathsFromHook', '', 'standard', $input, []);
+    }
+
+    /**
+     * @test
+     */
+    public function addAdditionalPropertyPathsFromHookThrowsExceptionIfFormElementTypeDoesNotMatch()
+    {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528633967);
+
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            ['isFormElementTypeDefinedInFormSetup'],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method('isFormElementTypeDefinedInFormSetup')->willReturn(false);
+        $validationDto = new ValidationDto('standard', 'Text');
+        $input = [$validationDto];
+
+        $configurationService->_call('addAdditionalPropertyPathsFromHook', '', 'standard', $input, []);
+    }
+
+    /**
+     * @test
+     */
+    public function addAdditionalPropertyPathsFromHookThrowsExceptionIfPropertyCollectionNameIsInvalid()
+    {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528636941);
+
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            ['isFormElementTypeDefinedInFormSetup'],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method('isFormElementTypeDefinedInFormSetup')->willReturn(true);
+        $validationDto = new ValidationDto('standard', 'Text', null, null, 'Bar', 'Baz');
+        $input = [$validationDto];
+
+        $configurationService->_call('addAdditionalPropertyPathsFromHook', '', 'standard', $input, []);
+    }
+
+    /**
+     * @test
+     */
+    public function addAdditionalPropertyPathsFromHookAddPaths()
+    {
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            ['isFormElementTypeDefinedInFormSetup'],
+            [],
+            '',
+            false
+        );
+        $configurationService->expects($this->any())->method('isFormElementTypeDefinedInFormSetup')->willReturn(true);
+
+        $input = [
+            new ValidationDto('standard', 'Text', null, 'options.xxx', 'validators', 'Baz'),
+            new ValidationDto('standard', 'Text', null, 'options.yyy', 'validators', 'Baz'),
+            new ValidationDto('standard', 'Text', null, 'options.zzz', 'validators', 'Custom'),
+            new ValidationDto('standard', 'Text', null, 'properties.xxx'),
+            new ValidationDto('standard', 'Text', null, 'properties.yyy'),
+            new ValidationDto('standard', 'Custom', null, 'properties.xxx'),
+        ];
+        $expected = [
+            'formElements' => [
+                'Text' => [
+                    'collections' => [
+                        'validators' => [
+                            'Baz' => [
+                                'additionalPropertyPaths' => [
+                                    'options.xxx',
+                                    'options.yyy',
+                                ],
+                            ],
+                            'Custom' => [
+                                'additionalPropertyPaths' => [
+                                    'options.zzz',
+                                ],
+                            ],
+                        ],
+                    ],
+                    'additionalPropertyPaths' => [
+                        'properties.xxx',
+                        'properties.yyy',
+                    ],
+                ],
+                'Custom' => [
+                    'additionalPropertyPaths' => [
+                        'properties.xxx',
+                    ],
+                ],
+            ],
+        ];
+
+        $this->assertSame(
+            $expected,
+            $configurationService->_call('addAdditionalPropertyPathsFromHook', '', 'standard', $input, [])
+        );
+    }
+
+    /**
+     * @test
+     * @dataProvider buildFormDefinitionValidationConfigurationFromFormEditorSetupDataProvider
+     * @param array $configuration
+     * @param array $expected
+     */
+    public function buildFormDefinitionValidationConfigurationFromFormEditorSetup(array $configuration, array $expected)
+    {
+        $configurationService = $this->getAccessibleMock(
+            ConfigurationService::class,
+            [
+                'getCacheEntry',
+                'getPrototypeConfiguration',
+                'getTranslationService',
+                'executeBuildFormDefinitionValidationConfigurationHooks',
+                'setCacheEntry',
+            ],
+            [],
+            '',
+            false
+        );
+
+        $translationService = $this->getAccessibleMock(
+            TranslationService::class,
+            ['translateValuesRecursive'],
+            [],
+            '',
+            false
+        );
+        $translationService->expects($this->any())->method('translateValuesRecursive')->willReturnArgument(0);
+
+        $configurationService->expects($this->any())->method('getCacheEntry')->willReturn(null);
+        $configurationService->expects($this->any())->method('getPrototypeConfiguration')->willReturn($configuration);
+        $configurationService->expects($this->any())->method('getTranslationService')->willReturn($translationService);
+        $configurationService->expects($this->any())
+            ->method('executeBuildFormDefinitionValidationConfigurationHooks')
+            ->willReturnArgument(1);
+        $configurationService->expects($this->any())->method('setCacheEntry')->willReturn(null);
+
+        $this->assertSame(
+            $expected,
+            $configurationService->_call('buildFormDefinitionValidationConfigurationFromFormEditorSetup', 'standard')
+        );
+    }
+
+    /**
+     * @return array
+     */
+    public function isFormElementPropertyDefinedInFormEditorSetupDataProvider(): array
+    {
+        return [
+            [
+                ['formElements' => ['Text' => ['propertyPaths' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Text', null, 'properties.foo.1'),
+                true,
+            ],
+            [
+                ['formElements' => ['Text' => ['additionalElementPropertyPaths' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Text', null, 'properties.foo.1'),
+                true,
+            ],
+            [
+                ['formElements' => ['Text' => ['multiValueProperties' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Text', null, 'properties.foo.1'),
+                true,
+            ],
+            [
+                ['formElements' => ['Text' => ['multiValueProperties' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Text', null, 'properties.foo.1.bar'),
+                true,
+            ],
+            [
+                ['formElements' => ['Text' => ['additionalPropertyPaths' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Text', null, 'properties.foo.1'),
+                true,
+            ],
+            [
+                ['formElements' => ['Text' => ['propertyPaths' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Foo', null, 'properties.foo.1'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['propertyPaths' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Text', null, 'properties.bar.1'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['additionalElementPropertyPaths' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Foo', null, 'properties.foo.1'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['additionalElementPropertyPaths' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Text', null, 'properties.bar.1'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['multiValueProperties' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Foo', null, 'properties.foo.1'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['multiValueProperties' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Text', null, 'properties.bar.1'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['multiValueProperties' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Foo', null, 'properties.foo.1.bar'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['multiValueProperties' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Text', null, 'properties.bar.1.foo'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['additionalPropertyPaths' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Foo', null, 'properties.foo.1'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['additionalPropertyPaths' => ['properties.foo.1']]]],
+                new ValidationDto('standard', 'Text', null, 'properties.bar.1'),
+                false,
+            ],
+        ];
+    }
+
+    /**
+     * @return array
+     */
+    public function isPropertyCollectionPropertyDefinedInFormEditorSetupDataProvider(): array
+    {
+        return [
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['propertyPaths' => ['properties.foo.1']]]]]]],
+                new ValidationDto(
+                    'standard',
+                    'Text',
+                    null,
+                    'properties.foo.1',
+                    'validators',
+                    'StringLength'
+                ),
+                true,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['multiValueProperties' => ['properties.foo.1']]]]]]],
+                new ValidationDto(
+                    'standard',
+                    'Text',
+                    null,
+                    'properties.foo.1',
+                    'validators',
+                    'StringLength'
+                ),
+                true,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['multiValueProperties' => ['properties.foo.1']]]]]]],
+                new ValidationDto(
+                    'standard',
+                    'Text',
+                    null,
+                    'properties.foo.1.bar',
+                    'validators',
+                    'StringLength'
+                ),
+                true,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['additionalPropertyPaths' => ['properties.foo.1']]]]]]],
+                new ValidationDto(
+                    'standard',
+                    'Text',
+                    null,
+                    'properties.foo.1',
+                    'validators',
+                    'StringLength'
+                ),
+                true,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['propertyPaths' => ['properties.foo.1']]]]]]],
+                new ValidationDto(
+                    'standard',
+                    'Text',
+                    null,
+                    'properties.foo.2',
+                    'validators',
+                    'StringLength'
+                ),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['propertyPaths' => ['properties.foo.1']]]]]]],
+                new ValidationDto('standard', 'Foo', null, 'properties.foo.1', 'validators', 'StringLength'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['propertyPaths' => ['properties.foo.1']]]]]]],
+                new ValidationDto('standard', 'Text', null, 'properties.foo.1', 'foo', 'StringLength'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['propertyPaths' => ['properties.foo.1']]]]]]],
+                new ValidationDto('standard', 'Text', null, 'properties.foo.1', 'validators', 'Foo'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['multiValueProperties' => ['properties.foo.1']]]]]]],
+                new ValidationDto(
+                    'standard',
+                    'Text',
+                    null,
+                    'properties.foo.2',
+                    'validators',
+                    'StringLength'
+                ),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['multiValueProperties' => ['properties.foo.1']]]]]]],
+                new ValidationDto(
+                    'standard',
+                    'Foo',
+                    null,
+                    'properties.foo.1.bar',
+                    'validators',
+                    'StringLength'
+                ),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['multiValueProperties' => ['properties.foo.1']]]]]]],
+                new ValidationDto('standard', 'Text', null, 'properties.foo.1.bar', 'foo', 'StringLength'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['multiValueProperties' => ['properties.foo.1']]]]]]],
+                new ValidationDto('standard', 'Text', null, 'properties.foo.1.bar', 'validators', 'Foo'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['additionalPropertyPaths' => ['properties.foo.1']]]]]]],
+                new ValidationDto('standard', 'Foo', null, 'properties.foo.1', 'validators', 'StringLength'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['additionalPropertyPaths' => ['properties.foo.1']]]]]]],
+                new ValidationDto('standard', 'Text', null, 'properties.foo.1', 'foo', 'StringLength'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['additionalPropertyPaths' => ['properties.foo.1']]]]]]],
+                new ValidationDto('standard', 'Text', null, 'properties.foo.1', 'validators', 'Foo'),
+                false,
+            ],
+        ];
+    }
+
+    /**
+     * @return array
+     */
+    public function isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetupDataProvider(): array
+    {
+        return [
+            [
+                ['formElements' => ['Text' => ['predefinedDefaults' => ['properties.foo.1' => 'bar']]]],
+                new ValidationDto('standard', 'Text', null, 'properties.foo.1'),
+                true,
+            ],
+            [
+                ['formElements' => ['Text' => ['predefinedDefaults' => ['properties.foo.1' => 'bar']]]],
+                new ValidationDto('standard', 'Text', null, 'properties.foo.2'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['predefinedDefaults' => ['properties.foo.1' => 'bar']]]],
+                new ValidationDto('standard', 'Foo', null, 'properties.foo.1'),
+                false,
+            ],
+        ];
+    }
+
+    /**
+     * @return array
+     */
+    public function isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetupDataProvider(): array
+    {
+        return [
+            [
+                ['collections' => ['validators' => ['StringLength' => ['predefinedDefaults' => ['properties.foo.1' => 'bar']]]]],
+                new ValidationDto('standard', null, null, 'properties.foo.1', 'validators', 'StringLength'),
+                true,
+            ],
+            [
+                ['collections' => ['validators' => ['StringLength' => ['predefinedDefaults' => ['properties.foo.1' => 'bar']]]]],
+                new ValidationDto('standard', null, null, 'properties.foo.2', 'validators', 'StringLength'),
+                false,
+            ],
+            [
+                ['collections' => ['validators' => ['StringLength' => ['predefinedDefaults' => ['properties.foo.1' => 'bar']]]]],
+                new ValidationDto('standard', null, null, 'properties.foo.1', 'foo', 'StringLength'),
+                false,
+            ],
+            [
+                ['collections' => ['validators' => ['StringLength' => ['predefinedDefaults' => ['properties.foo.1' => 'bar']]]]],
+                new ValidationDto('standard', null, null, 'properties.foo.1', 'validators', 'Foo'),
+                false,
+            ],
+        ];
+    }
+
+    /**
+     * @return array
+     */
+    public function isFormElementTypeCreatableByFormEditorDataProvider(): array
+    {
+        return [
+            [
+                [],
+                new ValidationDto('standard', 'Form'),
+                true,
+            ],
+            [
+                ['formElements' => ['Text' => ['creatable' => true]]],
+                new ValidationDto('standard', 'Text'),
+                true,
+            ],
+            [
+                ['formElements' => ['Text' => ['creatable' => false]]],
+                new ValidationDto('standard', 'Text'),
+                false,
+            ],
+            [
+                ['formElements' => ['Foo' => ['creatable' => true]]],
+                new ValidationDto('standard', 'Text'),
+                false,
+            ],
+        ];
+    }
+
+    /**
+     * @return array
+     */
+    public function isPropertyCollectionElementIdentifierCreatableByFormEditorDataProvider(): array
+    {
+        return [
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['creatable' => true]]]]]],
+                new ValidationDto('standard', 'Text', null, null, 'validators', 'StringLength'),
+                true,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['StringLength' => ['creatable' => false]]]]]],
+                new ValidationDto('standard', 'Text', null, null, 'validators', 'StringLength'),
+                false,
+            ],
+            [
+                ['formElements' => ['Foo' => ['collections' => ['validators' => ['StringLength' => ['creatable' => true]]]]]],
+                new ValidationDto('standard', 'Text', null, null, 'validators', 'StringLength'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['foo' => ['StringLength' => ['creatable' => true]]]]]],
+                new ValidationDto('standard', 'Text', null, null, 'validators', 'StringLength'),
+                false,
+            ],
+            [
+                ['formElements' => ['Text' => ['collections' => ['validators' => ['Foo' => ['creatable' => true]]]]]],
+                new ValidationDto('standard', 'Text', null, null, 'validators', 'StringLength'),
+                false,
+            ],
+        ];
+    }
+
+    /**
+     * @return array
+     */
+    public function buildFormDefinitionValidationConfigurationFromFormEditorSetupDataProvider(): array
+    {
+        return [
+            [
+                [
+                    'formElementsDefinition' => [
+                        'Text' => [
+                            'formEditor' => [
+                                'editors' => [
+                                    [
+                                        'templateName' => 'Foo',
+                                        'propertyPath' => 'properties.foo',
+                                        'setup' => [
+                                            'propertyPath' => 'properties.bar',
+                                        ],
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'formElements' => [
+                        'Text' => [
+                            'propertyPaths' => [
+                                'properties.foo',
+                                'properties.bar',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+
+            [
+                [
+                    'formElementsDefinition' => [
+                        'Text' => [
+                            'formEditor' => [
+                                'editors' => [
+                                    [
+                                        'templateName' => 'Inspector-GridColumnViewPortConfigurationEditor',
+                                        'propertyPath' => 'properties.{@viewPortIdentifier}.foo',
+                                        'configurationOptions' => [
+                                            'viewPorts' => [
+                                                ['viewPortIdentifier' => 'viewFoo'],
+                                                ['viewPortIdentifier' => 'viewBar'],
+                                            ],
+                                        ],
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'formElements' => [
+                        'Text' => [
+                            'propertyPaths' => [
+                                'properties.viewFoo.foo',
+                                'properties.viewBar.foo',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+
+            [
+                [
+                    'formElementsDefinition' => [
+                        'Text' => [
+                            'formEditor' => [
+                                'editors' => [
+                                    [
+                                        'additionalElementPropertyPaths' => [
+                                            'properties.foo',
+                                            'properties.bar',
+                                        ],
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'formElements' => [
+                        'Text' => [
+                            'additionalElementPropertyPaths' => [
+                                'properties.foo',
+                                'properties.bar',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+
+            [
+                [
+                    'formElementsDefinition' => [
+                        'Text' => [
+                            'formEditor' => [
+                                'editors' => [
+                                    [
+                                        'templateName' => 'Inspector-PropertyGridEditor',
+                                        'propertyPath' => 'properties.foo.1',
+                                    ],
+                                    [
+                                        'templateName' => 'Inspector-MultiSelectEditor',
+                                        'propertyPath' => 'properties.foo.2',
+                                    ],
+                                    [
+                                        'templateName' => 'Inspector-ValidationErrorMessageEditor',
+                                        'propertyPath' => 'properties.foo.3',
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'formElements' => [
+                        'Text' => [
+                            'multiValueProperties' => [
+                                'properties.foo.1',
+                                'properties.foo.2',
+                                'properties.foo.3',
+                            ],
+                            'propertyPaths' => [
+                                'properties.foo.1',
+                                'properties.foo.2',
+                                'properties.foo.3',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+
+            [
+                [
+                    'formElementsDefinition' => [
+                        'Text' => [
+                            'formEditor' => [
+                                'predefinedDefaults' => [
+                                    'foo' => [
+                                        'bar' => 'xxx',
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'formElements' => [
+                        'Text' => [
+                            'predefinedDefaults' => [
+                                'foo.bar' => 'xxx',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+
+            [
+                [
+                    'formEditor' => [
+                        'formElementGroups' => [
+                            'Dummy' => [],
+                        ],
+                    ],
+                    'formElementsDefinition' => [
+                        'Text' => [
+                            'formEditor' => [
+                                'group' => 'Dummy',
+                                'groupSorting' => 10,
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'formElements' => [
+                        'Text' => [
+                            'creatable' => true,
+                        ],
+                    ],
+                ],
+            ],
+
+            [
+                [
+                    'formEditor' => [
+                        'formElementGroups' => [
+                            'Dummy' => [],
+                        ],
+                    ],
+                    'formElementsDefinition' => [
+                        'Text' => [
+                            'formEditor' => [
+                                'group' => 'Dummy',
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'formElements' => [
+                        'Text' => [
+                            'creatable' => false,
+                        ],
+                    ],
+                ],
+            ],
+
+            [
+                [
+                    'formEditor' => [
+                        'formElementGroups' => [
+                            'Foo' => [],
+                        ],
+                    ],
+                    'formElementsDefinition' => [
+                        'Text' => [
+                            'formEditor' => [
+                                'group' => 'Dummy',
+                                'groupSorting' => 10,
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'formElements' => [
+                        'Text' => [
+                            'creatable' => false,
+                        ],
+                    ],
+                ],
+            ],
+
+            [
+                [
+                    'formEditor' => [
+                        'formElementGroups' => [
+                            'Dummy' => [],
+                        ],
+                    ],
+                    'formElementsDefinition' => [
+                        'Text' => [
+                            'formEditor' => [
+                                'group' => 'Foo',
+                                'groupSorting' => 10,
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'formElements' => [
+                        'Text' => [
+                            'creatable' => false,
+                        ],
+                    ],
+                ],
+            ],
+
+            [
+                [
+                    'formElementsDefinition' => [
+                        'Text' => [
+                            'formEditor' => [
+                                'editors' => [
+                                    [
+                                        'templateName' => 'Inspector-FinishersEditor',
+                                        'selectOptions' => [
+                                            [
+                                                'value' => 'FooFinisher',
+                                            ],
+                                            [
+                                                'value' => 'BarFinisher',
+                                            ],
+                                        ],
+                                    ],
+                                    [
+                                        'templateName' => 'Inspector-ValidatorsEditor',
+                                        'selectOptions' => [
+                                            [
+                                                'value' => 'FooValidator',
+                                            ],
+                                            [
+                                                'value' => 'BarValidator',
+                                            ],
+                                        ],
+                                    ],
+                                    [
+                                        'templateName' => 'Inspector-RequiredValidatorEditor',
+                                        'validatorIdentifier' => 'NotEmpty',
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'formElements' => [
+                        'Text' => [
+                            'collections' => [
+                                'finishers' => [
+                                    'FooFinisher' => [
+                                        'creatable' => true,
+                                    ],
+                                    'BarFinisher' => [
+                                        'creatable' => true,
+                                    ],
+                                ],
+                                'validators' => [
+                                    'FooValidator' => [
+                                        'creatable' => true,
+                                    ],
+                                    'BarValidator' => [
+                                        'creatable' => true,
+                                    ],
+                                    'NotEmpty' => [
+                                        'creatable' => true,
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+
+            [
+                [
+                    'formElementsDefinition' => [
+                        'Text' => [
+                            'formEditor' => [
+                                'propertyCollections' => [
+                                    'validators' => [
+                                        [
+                                            'identifier' => 'fooValidator',
+                                            'editors' => [
+                                                [
+                                                    'propertyPath' => 'options.xxx',
+                                                ],
+                                                [
+                                                    'propertyPath' => 'options.yyy',
+                                                    'setup' => [
+                                                        'propertyPath' => 'options.zzz',
+                                                    ],
+                                                ],
+                                            ],
+                                        ],
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'formElements' => [
+                        'Text' => [
+                            'collections' => [
+                                'validators' => [
+                                    'fooValidator' => [
+                                        'propertyPaths' => [
+                                            'options.xxx',
+                                            'options.yyy',
+                                            'options.zzz',
+                                        ],
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+
+            [
+                [
+                    'formElementsDefinition' => [
+                        'Text' => [
+                            'formEditor' => [
+                                'propertyCollections' => [
+                                    'validators' => [
+                                        [
+                                            'identifier' => 'fooValidator',
+                                            'editors' => [
+                                                [
+                                                    'additionalElementPropertyPaths' => [
+                                                        'options.xxx',
+                                                    ],
+                                                ],
+                                                [
+                                                    'additionalElementPropertyPaths' => [
+                                                        'options.yyy',
+                                                        'options.zzz',
+                                                    ],
+                                                ],
+                                            ],
+                                        ],
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'formElements' => [
+                        'Text' => [
+                            'additionalElementPropertyPaths' => [
+                                'options.xxx',
+                                'options.yyy',
+                                'options.zzz',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+
+            [
+                [
+                    'formElementsDefinition' => [
+                        'Text' => [
+                            'formEditor' => [
+                                'propertyCollections' => [
+                                    'validators' => [
+                                        [
+                                            'identifier' => 'fooValidator',
+                                            'editors' => [
+                                                [
+                                                    'templateName' => 'Inspector-PropertyGridEditor',
+                                                    'propertyPath' => 'options.xxx',
+                                                ],
+                                                [
+                                                    'templateName' => 'Inspector-MultiSelectEditor',
+                                                    'propertyPath' => 'options.yyy',
+                                                ],
+                                                [
+                                                    'templateName' => 'Inspector-ValidationErrorMessageEditor',
+                                                    'propertyPath' => 'options.zzz',
+                                                ],
+                                            ],
+                                        ],
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'formElements' => [
+                        'Text' => [
+                            'collections' => [
+                                'validators' => [
+                                    'fooValidator' => [
+                                        'multiValueProperties' => [
+                                            'options.xxx',
+                                            'options.yyy',
+                                        ],
+                                        'propertyPaths' => [
+                                            'options.xxx',
+                                            'options.yyy',
+                                            'options.zzz',
+                                        ],
+                                    ],
+                                ],
+                            ],
+                            'multiValueProperties' => [
+                                'options.zzz',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+
+            [
+                [
+                    'validatorsDefinition' => [
+                        'someValidator' => [
+                            'formEditor' => [
+                                'predefinedDefaults' => [
+                                    'some' => [
+                                        'property' => 'value',
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+                [
+                    'collections' => [
+                        'validators' => [
+                            'someValidator' => [
+                                'predefinedDefaults' => [
+                                    'some.property' => 'value',
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+    }
 }
diff --git a/typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinition/Validators/CreatableFormElementPropertiesValidatorTest.php b/typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinition/Validators/CreatableFormElementPropertiesValidatorTest.php
new file mode 100644 (file)
index 0000000..32d119e
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Tests\Unit\Domain\Configuration\FormDefinition\Validators;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
+use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\CreatableFormElementPropertiesValidator;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class CreatableFormElementPropertiesValidatorTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function validateFormElementPredefinedDefaultValueThrowsExceptionIfValueDoesNotMatch()
+    {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528588035);
+
+        $validationDto = new ValidationDto(null, null, 'test-1', 'label');
+        $input = 'xxx';
+        $typeConverter = $this->getAccessibleMock(
+            CreatableFormElementPropertiesValidator::class,
+            ['getConfigurationService'],
+            [[], '', $validationDto]
+        );
+        $configurationService = $this->createMock(ConfigurationService::class);
+        $configurationService->expects($this->any())
+            ->method('getFormElementPredefinedDefaultValueFromFormEditorSetup')
+            ->willReturn('default');
+        $configurationService->expects($this->any())
+            ->method('isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup')
+            ->willReturn(true);
+        $typeConverter->expects($this->any())->method('getConfigurationService')->willReturn($configurationService);
+
+        $typeConverter($input, '');
+    }
+
+    /**
+     * @test
+     */
+    public function validateFormElementPredefinedDefaultValueThrowsNoExceptionIfValueMatches()
+    {
+        $validationDto = new ValidationDto(null, null, 'test-1', 'label');
+        $typeConverter = $this->getAccessibleMock(
+            CreatableFormElementPropertiesValidator::class,
+            ['getConfigurationService'],
+            [[], '', $validationDto]
+        );
+        $configurationService = $this->createMock(ConfigurationService::class);
+        $configurationService->expects($this->any())
+            ->method('getFormElementPredefinedDefaultValueFromFormEditorSetup')
+            ->willReturn('default');
+        $configurationService->expects($this->any())
+            ->method('isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup')
+            ->willReturn(true);
+        $typeConverter->expects($this->any())->method('getConfigurationService')->willReturn($configurationService);
+
+        $failed = false;
+        try {
+            $typeConverter('', 'default');
+        } catch (PropertyException $e) {
+            $failed = true;
+        }
+        $this->assertFalse($failed);
+    }
+}
diff --git a/typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinition/Validators/CreatablePropertyCollectionElementPropertiesValidatorTest.php b/typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinition/Validators/CreatablePropertyCollectionElementPropertiesValidatorTest.php
new file mode 100644 (file)
index 0000000..aebc308
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Tests\Unit\Domain\Configuration\FormDefinition\Validators;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
+use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\CreatablePropertyCollectionElementPropertiesValidator;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class CreatablePropertyCollectionElementPropertiesValidatorTest extends UnitTestCase
+{
+
+    /**
+     * @test
+     */
+    public function validatePropertyCollectionElementPredefinedDefaultValueThrowsExceptionIfValueDoesNotMatch()
+    {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528591502);
+
+        $validationDto = new ValidationDto(null, null, 'test-1', 'label', 'validators', 'StringLength');
+        $typeConverter = $this->getAccessibleMock(
+            CreatablePropertyCollectionElementPropertiesValidator::class,
+            ['getConfigurationService'],
+            [[], '', $validationDto]
+        );
+        $configurationService = $this->createMock(ConfigurationService::class);
+        $configurationService->expects($this->any())->method(
+            'getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup'
+        )->willReturn('default');
+        $typeConverter->expects($this->any())->method('getConfigurationService')->willReturn($configurationService);
+
+        $input = 'xxx';
+        $typeConverter->_call('validatePropertyCollectionElementPredefinedDefaultValue', $input, $validationDto);
+    }
+
+    /**
+     * @test
+     */
+    public function validatePropertyCollectionElementPredefinedDefaultValueThrowsNoExceptionIfValueMatchs()
+    {
+        $validationDto = new ValidationDto(null, null, 'test-1', 'label', 'validators', 'StringLength');
+        $typeConverter = $this->getAccessibleMock(
+            CreatablePropertyCollectionElementPropertiesValidator::class,
+            ['getConfigurationService'],
+            [[], '', $validationDto]
+        );
+        $configurationService = $this->createMock(ConfigurationService::class);
+        $configurationService->expects($this->any())->method(
+            'getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup'
+        )->willReturn('default');
+        $typeConverter->expects($this->any())->method('getConfigurationService')->willReturn($configurationService);
+
+        $input = 'default';
+
+        $failed = false;
+        try {
+            $typeConverter->_call('validatePropertyCollectionElementPredefinedDefaultValue', $input, $validationDto);
+        } catch (PropertyException $e) {
+            $failed = true;
+        }
+        $this->assertFalse($failed);
+    }
+}
diff --git a/typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinitionConversionServiceTest.php b/typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinitionConversionServiceTest.php
new file mode 100644 (file)
index 0000000..357ec71
--- /dev/null
@@ -0,0 +1,194 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Tests\Unit\Domain\Configuration;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionConversionService;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class FormDefinitionConversionServiceTest extends UnitTestCase
+{
+
+    /**
+     * @var bool Reset singletons created by subject
+     */
+    protected $resetSingletonInstances = true;
+
+    /**
+     * @test
+     */
+    public function addHmacDataAddsHmacHashes()
+    {
+        $formDefinitionConversionService = $this->getAccessibleMock(
+            FormDefinitionConversionService::class,
+            [
+                'generateSessionToken',
+                'persistSessionToken',
+            ],
+            [],
+            '',
+            false
+        );
+
+        $sessionToken = '123';
+        $formDefinitionConversionService->expects($this->any())->method(
+            'generateSessionToken'
+        )->willReturn($sessionToken);
+
+        $formDefinitionConversionService->expects($this->any())->method(
+            'persistSessionToken'
+        )->willReturn(null);
+
+        GeneralUtility::setSingletonInstance(FormDefinitionConversionService::class, $formDefinitionConversionService);
+
+        $input = [
+            'prototypeName' => 'standard',
+            'identifier' => 'test',
+            'type' => 'Form',
+            'heinz' => 1,
+            'klaus' => [],
+            'klaus1' => [
+                '_label' => 'x',
+                '_value' => 'y',
+            ],
+            'sabine' => [
+                'heinz' => '2',
+                'klaus' => [],
+                'horst' => [
+                    'heinz' => '',
+                    'paul' => [[]],
+                ],
+            ],
+        ];
+
+        $data = $formDefinitionConversionService->addHmacData($input);
+
+        $expected = [
+            'prototypeName' => 'standard',
+            'identifier' => 'test',
+            'type' => 'Form',
+            'heinz' => 1,
+            'klaus' => [],
+            'klaus1' => [
+                '_label' => 'x',
+                '_value' => 'y',
+            ],
+            'sabine' => [
+                'heinz' => '2',
+                'klaus' => [],
+                'horst' => [
+                    'heinz' => '',
+                    'paul' => [[]],
+                     '_orig_heinz' => [
+                         'value' => '',
+                         'hmac' => $data['sabine']['horst']['_orig_heinz']['hmac'],
+                     ],
+                ],
+                 '_orig_heinz' => [
+                     'value' => '2',
+                     'hmac' => $data['sabine']['_orig_heinz']['hmac'],
+                 ],
+            ],
+             '_orig_prototypeName' => [
+                 'value' => 'standard',
+                 'hmac' => $data['_orig_prototypeName']['hmac'],
+             ],
+             '_orig_identifier' => [
+                 'value' => 'test',
+                 'hmac' => $data['_orig_identifier']['hmac'],
+             ],
+             '_orig_type' => [
+                 'value' => 'Form',
+                 'hmac' => $data['_orig_type']['hmac'],
+             ],
+             '_orig_heinz' => [
+                 'value' => 1,
+                 'hmac' => $data['_orig_heinz']['hmac'],
+             ],
+        ];
+
+        $this->assertSame($expected, $data);
+    }
+
+    /**
+     * @test
+     */
+    public function removeHmacDataRemoveHmacs()
+    {
+        $formDefinitionConversionService = new FormDefinitionConversionService;
+        GeneralUtility::setSingletonInstance(FormDefinitionConversionService::class, $formDefinitionConversionService);
+
+        $input = [
+            'prototypeName' => 'standard',
+            'identifier' => 'test',
+            'heinz' => 1,
+            'klaus' => [],
+            'klaus1' => [
+                '_label' => 'x',
+                '_value' => 'y',
+            ],
+            'sabine' => [
+                'heinz' => '2',
+                'klaus' => [],
+                'horst' => [
+                    'heinz' => '',
+                    'paul' => [[]],
+                     '_orig_heinz' => [
+                         'value' => '',
+                         'hmac' => '12345',
+                     ],
+                ],
+                 '_orig_heinz' => [
+                     'value' => '2',
+                     'hmac' => '12345',
+                 ],
+            ],
+             '_orig_prototypeName' => [
+                 'value' => 'standard',
+                 'hmac' => '12345',
+             ],
+             '_orig_identifier' => [
+                 'value' => 'test',
+                 'hmac' => '12345',
+             ],
+             '_orig_heinz' => [
+                 'value' => 1,
+                 'hmac' => '12345',
+             ],
+        ];
+
+        $expected = [
+            'prototypeName' => 'standard',
+            'identifier' => 'test',
+            'heinz' => 1,
+            'klaus' => [],
+            'klaus1' => [
+                '_label' => 'x',
+                '_value' => 'y',
+            ],
+            'sabine' => [
+                'heinz' => '2',
+                'klaus' => [],
+                'horst' => [
+                    'heinz' => '',
+                    'paul' => [[]],
+                ],
+            ],
+        ];
+
+        $this->assertSame($expected, $formDefinitionConversionService->removeHmacData($input));
+    }
+}
diff --git a/typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinitionValidationServiceTest.php b/typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinitionValidationServiceTest.php
new file mode 100644 (file)
index 0000000..bdb098e
--- /dev/null
@@ -0,0 +1,592 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Tests\Unit\Domain\Configuration;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
+use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionValidationService;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class FormDefinitionValidationServiceTest extends UnitTestCase
+{
+
+    /**
+     * @test
+     */
+    public function validateAllFormElementPropertyValuesByHmacThrowsExceptionIfHmacIsInvalid()
+    {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528588036);
+
+        $typeConverter = $this->getAccessibleMock(FormDefinitionValidationService::class, ['dummy'], [], '', false);
+
+        $prototypeName = 'standard';
+        $identifier = 'some-text';
+
+        $sessionToken = '123';
+
+        $validationDto = new ValidationDto($prototypeName, 'Text', $identifier);
+
+        $input = [
+            'label' => 'xxx',
+            '_orig_label' => [
+                'value' => 'aaa',
+                'hmac' => GeneralUtility::hmac(serialize([$identifier, 'label', 'aaa']), $sessionToken),
+            ],
+        ];
+
+        $typeConverter->_call('validateAllFormElementPropertyValuesByHmac', $input, $sessionToken, $validationDto);
+    }
+
+    /**
+     * @test
+     */
+    public function validateAllFormElementPropertyValuesByHmacThrowsExceptionIfHmacDoesNotExists()
+    {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528588037);
+
+        $typeConverter = $this->getAccessibleMock(FormDefinitionValidationService::class, ['dummy'], [], '', false);
+
+        $prototypeName = 'standard';
+        $identifier = 'some-text';
+
+        $sessionToken = '123';
+
+        $validationDto = new ValidationDto($prototypeName, 'Text', $identifier);
+
+        $input = [
+            'label' => 'xxx',
+        ];
+
+        $typeConverter->_call('validateAllFormElementPropertyValuesByHmac', $input, $sessionToken, $validationDto);
+    }
+
+    /**
+     * @test
+     */
+    public function validateAllFormElementPropertyValuesByHmacThrowsNoExceptionIfHmacIsValid()
+    {
+        $typeConverter = $this->getAccessibleMock(FormDefinitionValidationService::class, ['dummy'], [], '', false);
+
+        $prototypeName = 'standard';
+        $identifier = 'some-text';
+
+        $sessionToken = '123';
+
+        $validationDto = new ValidationDto($prototypeName, 'Text', $identifier);
+
+        $input = [
+            'label' => 'aaa',
+            '_orig_label' => [
+                'value' => 'aaa',
+                'hmac' => GeneralUtility::hmac(serialize([$identifier, 'label', 'aaa']), $sessionToken),
+            ],
+        ];
+
+        $failed = false;
+        try {
+            $typeConverter->_call(
+                'validateAllFormElementPropertyValuesByHmac',
+                $input,
+                $sessionToken,
+                $validationDto
+            );
+        } catch (PropertyException $e) {
+            $failed = true;
+        }
+        $this->assertFalse($failed);
+    }
+
+    /**
+     * @test
+     */
+    public function validateAllPropertyCollectionElementValuesByHmacThrowsExceptionIfHmacIsInvalid()
+    {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528591586);
+
+        $typeConverter = $this->getAccessibleMock(FormDefinitionValidationService::class, ['dummy'], [], '', false);
+
+        $prototypeName = 'standard';
+        $identifier = 'some-text';
+
+        $sessionToken = '123';
+
+        $validationDto = new ValidationDto($prototypeName, 'Text', $identifier, null, 'validators');
+
+        $input = [
+            'identifier' => 'StringLength',
+            '_orig_identifier' => [
+                'value' => 'StringLength',
+                'hmac' => GeneralUtility::hmac(serialize([$identifier, 'validators', 'StringLength', 'identifier', 'StringLength']), $sessionToken),
+            ],
+            'options' => [
+                'test' => 'xxx',
+                '_orig_test' => [
+                    'value' => 'aaa',
+                    'hmac' => GeneralUtility::hmac(serialize([$identifier, 'validators', 'StringLength', 'options.test', 'aaa']), $sessionToken),
+                ],
+            ],
+        ];
+
+        $typeConverter->_call(
+            'validateAllPropertyCollectionElementValuesByHmac',
+            $input,
+            $sessionToken,
+            $validationDto
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function validateAllPropertyCollectionElementValuesByHmacThrowsExceptionIfHmacDoesNotExists()
+    {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528591585);
+
+        $typeConverter = $this->getAccessibleMock(FormDefinitionValidationService::class, ['dummy'], [], '', false);
+
+        $prototypeName = 'standard';
+        $identifier = 'some-text';
+
+        $sessionToken = '123';
+
+        $validationDto = new ValidationDto($prototypeName, 'Text', $identifier, null, 'validators');
+
+        $input = [
+            'identifier' => 'StringLength',
+            '_orig_identifier' => [
+                'value' => 'StringLength',
+                'hmac' => GeneralUtility::hmac(serialize([$identifier, 'validators', 'StringLength', 'identifier', 'StringLength']), $sessionToken),
+            ],
+            'options' => [
+                'test' => 'xxx',
+            ],
+        ];
+
+        $typeConverter->_call(
+            'validateAllPropertyCollectionElementValuesByHmac',
+            $input,
+            $sessionToken,
+            $validationDto
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function validateAllPropertyCollectionElementValuesByHmacThrowsNoExceptionIfHmacIsValid()
+    {
+        $typeConverter = $this->getAccessibleMock(FormDefinitionValidationService::class, ['dummy'], [], '', false);
+
+        $prototypeName = 'standard';
+        $identifier = 'some-text';
+
+        $sessionToken = '123';
+
+        $validationDto = new ValidationDto($prototypeName, 'Text', $identifier, null, 'validators');
+
+        $input = [
+            'identifier' => 'StringLength',
+            '_orig_identifier' => [
+                'value' => 'StringLength',
+                'hmac' => GeneralUtility::hmac(serialize([$identifier, 'validators', 'StringLength', 'identifier', 'StringLength']), $sessionToken),
+            ],
+            'options' => [
+                'test' => 'aaa',
+                '_orig_test' => [
+                    'value' => 'aaa',
+                    'hmac' => GeneralUtility::hmac(serialize([$identifier, 'validators', 'StringLength', 'options.test', 'aaa']), $sessionToken),
+                ],
+            ],
+        ];
+
+        $failed = false;
+        try {
+            $typeConverter->_call(
+                'validateAllPropertyCollectionElementValuesByHmac',
+                $input,
+                $sessionToken,
+                $validationDto
+            );
+        } catch (PropertyException $e) {
+            $failed = true;
+        }
+        $this->assertFalse($failed);
+    }
+
+    /**
+     * @test
+     * @dataProvider validateAllPropertyValuesFromCreatableFormElementDataProvider
+     * @param array $mockConfiguration
+     * @param array $formElement
+     * @param string $sessionToken
+     * @param int $exceptionCode
+     * @param ValidationDto $validationDto
+     */
+    public function validateAllPropertyValuesFromCreatableFormElement(
+        array $mockConfiguration,
+        array $formElement,
+        string $sessionToken,
+        int $exceptionCode,
+        ValidationDto $validationDto
+    ) {
+        $typeConverter = $this->getAccessibleMock(
+            FormDefinitionValidationService::class,
+            ['getConfigurationService'],
+            [],
+            '',
+            false
+        );
+
+        $configurationService = $this->createMock(ConfigurationService::class);
+        $configurationService->expects($this->any())
+            ->method('isFormElementPropertyDefinedInFormEditorSetup')
+            ->willReturn($mockConfiguration['isFormElementPropertyDefinedInFormEditorSetup']);
+        $configurationService->expects($this->any())->method(
+            'isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup'
+        )->willReturn($mockConfiguration['isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup']);
+        $configurationService->expects($this->any())
+            ->method('getFormElementPredefinedDefaultValueFromFormEditorSetup')
+            ->willReturn($mockConfiguration['getFormElementPredefinedDefaultValueFromFormEditorSetup']);
+        $typeConverter->expects($this->any())->method('getConfigurationService')->willReturn($configurationService);
+        $formDefinitionValidationService = $this->getAccessibleMock(FormDefinitionValidationService::class, ['getConfigurationService']);
+        $formDefinitionValidationService->expects($this->any())->method('getConfigurationService')->willReturn($configurationService);
+        GeneralUtility::setSingletonInstance(FormDefinitionValidationService::class, $formDefinitionValidationService);
+        GeneralUtility::setSingletonInstance(ConfigurationService::class, $configurationService);
+
+        $returnedExceptionCode = -1;
+        try {
+            $typeConverter->_call(
+                'validateAllPropertyValuesFromCreatableFormElement',
+                $formElement,
+                $sessionToken,
+                $validationDto
+            );
+        } catch (PropertyException $e) {
+            $returnedExceptionCode = $e->getCode();
+        }
+        $this->assertEquals($returnedExceptionCode, $exceptionCode);
+    }
+
+    /**
+     * @test
+     * @dataProvider validateAllPropertyValuesFromCreatablePropertyCollectionElementDataProvider
+     * @param array $mockConfiguration
+     * @param array $formElement
+     * @param string $sessionToken
+     * @param int $exceptionCode
+     * @param ValidationDto $validationDto
+     */
+    public function validateAllPropertyValuesFromCreatablePropertyCollectionElement(
+        array $mockConfiguration,
+        array $formElement,
+        string $sessionToken,
+        int $exceptionCode,
+        ValidationDto $validationDto
+    ) {
+        $typeConverter = $this->getAccessibleMock(
+            FormDefinitionValidationService::class,
+            ['getConfigurationService'],
+            [],
+            '',
+            false
+        );
+
+        $configurationService = $this->createMock(ConfigurationService::class);
+        $configurationService->expects($this->any())
+            ->method('isPropertyCollectionPropertyDefinedInFormEditorSetup')
+            ->willReturn($mockConfiguration['isPropertyCollectionPropertyDefinedInFormEditorSetup']);
+        $configurationService->expects($this->any())->method(
+            'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup'
+        )->willReturn($mockConfiguration['isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup']);
+        $configurationService->expects($this->any())->method(
+            'getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup'
+        )->willReturn($mockConfiguration['getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup']);
+        $typeConverter->expects($this->any())->method('getConfigurationService')->willReturn($configurationService);
+        $formDefinitionValidationService = $this->getAccessibleMock(FormDefinitionValidationService::class, ['getConfigurationService']);
+        $formDefinitionValidationService->expects($this->any())->method('getConfigurationService')->willReturn($configurationService);
+        GeneralUtility::setSingletonInstance(FormDefinitionValidationService::class, $formDefinitionValidationService);
+        GeneralUtility::setSingletonInstance(ConfigurationService::class, $configurationService);
+
+        $returnedExceptionCode = -1;
+        try {
+            $typeConverter->_call(
+                'validateAllPropertyValuesFromCreatablePropertyCollectionElement',
+                $formElement,
+                $sessionToken,
+                $validationDto
+            );
+        } catch (PropertyException $e) {
+            $returnedExceptionCode = $e->getCode();
+        }
+        $this->assertEquals($returnedExceptionCode, $exceptionCode);
+    }
+
+    /**
+     * @return array
+     */
+    public function validateAllPropertyValuesFromCreatableFormElementDataProvider(): array
+    {
+        $encryptionKeyBackup = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'];
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] = 12345;
+
+        $sessionToken = '54321';
+        $identifier = 'text-1';
+
+        $validationDto = new ValidationDto('standard', 'Text', $identifier);
+        $formElement = [
+            'test' => 'xxx',
+            '_orig_test' => [
+                'value' => 'xxx',
+                'hmac' => GeneralUtility::hmac(serialize([$identifier, 'test', 'xxx']), $sessionToken),
+            ],
+        ];
+
+        $invalidFormElement = [
+            'test' => 'xxx1',
+            '_orig_test' => [
+                'value' => 'xxx',
+                'hmac' => GeneralUtility::hmac(serialize([$identifier, 'test', 'xxx']), $sessionToken),
+            ],
+        ];
+
+        return [
+            [
+                [
+                    'isFormElementPropertyDefinedInFormEditorSetup' => true,
+                    'isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => false,
+                    'getFormElementPredefinedDefaultValueFromFormEditorSetup' => '',
+                ],
+                $formElement,
+                $sessionToken,
+                -1,
+                $validationDto
+            ],
+            [
+                [
+                    'isFormElementPropertyDefinedInFormEditorSetup' => false,
+                    'isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => false,
+                    'getFormElementPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                $formElement,
+                $sessionToken,
+                -1,
+                $validationDto
+            ],
+            [
+                [
+                    'isFormElementPropertyDefinedInFormEditorSetup' => false,
+                    'isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => false,
+                    'getFormElementPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                ['test' => 'xxx'],
+                $sessionToken,
+                1528588037,
+                $validationDto
+            ],
+            [
+                [
+                    'isFormElementPropertyDefinedInFormEditorSetup' => false,
+                    'isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => false,
+                    'getFormElementPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                ['test' => 'xxx', '_orig_test' => []],
+                $sessionToken,
+                1528538222,
+                $validationDto
+            ],
+            [
+                [
+                    'isFormElementPropertyDefinedInFormEditorSetup' => false,
+                    'isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => false,
+                    'getFormElementPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                ['test' => 'xxx', '_orig_test' => ['hmac' => '4242']],
+                $sessionToken,
+                1528538252,
+                $validationDto
+            ],
+            [
+                [
+                    'isFormElementPropertyDefinedInFormEditorSetup' => false,
+                    'isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => false,
+                    'getFormElementPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                $invalidFormElement,
+                $sessionToken,
+                1528588036,
+                $validationDto
+            ],
+
+            [
+                [
+                    'isFormElementPropertyDefinedInFormEditorSetup' => false,
+                    'isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => true,
+                    'getFormElementPredefinedDefaultValueFromFormEditorSetup' => 'xxx',
+                ],
+                $formElement,
+                $sessionToken,
+                -1,
+                $validationDto
+            ],
+            [
+                [
+                    'isFormElementPropertyDefinedInFormEditorSetup' => false,
+                    'isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => true,
+                    'getFormElementPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                $formElement,
+                $sessionToken,
+                1528588035,
+                $validationDto
+            ],
+        ];
+    }
+
+    /**
+     * @return array
+     */
+    public function validateAllPropertyValuesFromCreatablePropertyCollectionElementDataProvider(): array
+    {
+        $encryptionKeyBackup = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'];
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] = 12345;
+
+        $sessionToken = '54321';
+        $identifier = 'text-1';
+
+        $validationDto = new ValidationDto('standard', 'Text', $identifier, null, 'validators', 'StringLength');
+        $formElement = [
+            'test' => 'xxx',
+            '_orig_test' => [
+                'value' => 'xxx',
+                'hmac' => GeneralUtility::hmac(serialize([$identifier, 'validators', 'StringLength', 'test', 'xxx']), $sessionToken),
+            ],
+        ];
+
+        $invalidFormElement = [
+            'test' => 'xxx1',
+            '_orig_test' => [
+                'value' => 'xxx',
+                'hmac' => GeneralUtility::hmac(serialize([$identifier, 'validators', 'StringLength', 'test', 'xxx']), $sessionToken),
+            ],
+        ];
+
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] = $encryptionKeyBackup;
+
+        return [
+            [
+                [
+                    'isPropertyCollectionPropertyDefinedInFormEditorSetup' => true,
+                    'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => false,
+                    'getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                $formElement,
+                $sessionToken,
+                -1,
+                $validationDto
+            ],
+            [
+                [
+                    'isPropertyCollectionPropertyDefinedInFormEditorSetup' => false,
+                    'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => false,
+                    'getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                $formElement,
+                $sessionToken,
+                -1,
+                $validationDto
+            ],
+            [
+                [
+                    'isPropertyCollectionPropertyDefinedInFormEditorSetup' => false,
+                    'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => false,
+                    'getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                ['test' => 'xxx'],
+                $sessionToken,
+                1528591585,
+                $validationDto
+            ],
+            [
+                [
+                    'isPropertyCollectionPropertyDefinedInFormEditorSetup' => false,
+                    'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => false,
+                    'getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                ['test' => 'xxx', '_orig_test' => []],
+                $sessionToken,
+                1528538222,
+                $validationDto
+            ],
+            [
+                [
+                    'isPropertyCollectionPropertyDefinedInFormEditorSetup' => false,
+                    'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => false,
+                    'getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                ['test' => 'xxx', '_orig_test' => ['hmac' => '4242']],
+                $sessionToken,
+                1528538252,
+                $validationDto
+            ],
+            [
+                [
+                    'isPropertyCollectionPropertyDefinedInFormEditorSetup' => false,
+                    'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => false,
+                    'getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                $invalidFormElement,
+                $sessionToken,
+                1528591586,
+                $validationDto
+            ],
+
+            [
+                [
+                    'isPropertyCollectionPropertyDefinedInFormEditorSetup' => false,
+                    'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => true,
+                    'getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup' => 'xxx',
+                ],
+                $formElement,
+                $sessionToken,
+                -1,
+                $validationDto
+            ],
+            [
+                [
+                    'isPropertyCollectionPropertyDefinedInFormEditorSetup' => false,
+                    'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => true,
+                    'getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                $formElement,
+                $sessionToken,
+                1528591502,
+                $validationDto
+            ],
+        ];
+    }
+
+    public function tearDown()
+    {
+        GeneralUtility::resetSingletonInstances([]);
+        parent::tearDown();
+    }
+}
index 64000e3..3c4e398 100644 (file)
@@ -14,6 +14,9 @@ namespace TYPO3\CMS\Form\Tests\Unit\Mvc\Property\TypeConverter;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
+use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionValidationService;
 use TYPO3\CMS\Form\Mvc\Property\TypeConverter\FormDefinitionArrayConverter;
 use TYPO3\CMS\Form\Type\FormDefinitionArray;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
@@ -24,14 +27,68 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 class FormDefinitionArrayConverterTest extends UnitTestCase
 {
     /**
+     * @var bool Reset singletons created by subject
+     */
+    protected $resetSingletonInstances = true;
+
+    /**
+     * Set up
+     */
+    public function setUp()
+    {
+        parent::setUp();
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] = 12345;
+    }
+
+    /**
      * @test
      */
     public function convertsJsonStringToFormDefinitionArray()
     {
-        $typeConverter = new FormDefinitionArrayConverter();
-        $source = '{"francine":"stan","enabled":false,"properties":{"options":[{"_label":"label","_value":"value"}]}}';
+        $sessionToken = '123';
+
+        $data = [
+            'prototypeName' => 'standard',
+            'identifier' => 'test',
+            'type' => 'Text',
+            'enabled' => false,
+            'properties' => [
+                'options' => [
+                    [
+                        '_label' => 'label',
+                        '_value' => 'value',
+                    ],
+                ],
+            ],
+             '_orig_prototypeName' => [
+                 'value' => 'standard',
+                 'hmac' => GeneralUtility::hmac(serialize(['test', 'prototypeName', 'standard']), $sessionToken),
+             ],
+             '_orig_identifier' => [
+                 'value' => 'test',
+                 'hmac' => GeneralUtility::hmac(serialize(['test', 'identifier', 'test']), $sessionToken),
+             ],
+        ];
+
+        $typeConverter = $this->getAccessibleMock(FormDefinitionArrayConverter::class, ['getFormDefinitionValidationService', 'retrieveSessionToken'], [], '', false);
+        $formDefinitionValidationService = $this->getAccessibleMock(FormDefinitionValidationService::class, ['validateFormDefinitionProperties'], [], '', false);
+        $formDefinitionValidationService->expects($this->any())->method(
+            'validateFormDefinitionProperties'
+        )->willReturn(null);
+
+        $typeConverter->expects($this->any())->method(
+            'retrieveSessionToken'
+        )->willReturn($sessionToken);
+
+        $typeConverter->expects($this->any())->method(
+            'getFormDefinitionValidationService'
+        )->willReturn($formDefinitionValidationService);
+
+        $input = json_encode($data);
         $expected = [
-            'francine' => 'stan',
+            'prototypeName' => 'standard',
+            'identifier' => 'test',
+            'type' => 'Text',
             'enabled' => false,
             'properties' => [
                 'options' => [
@@ -39,9 +96,133 @@ class FormDefinitionArrayConverterTest extends UnitTestCase
                 ],
             ],
         ];
-        $result = $typeConverter->convertFrom($source, FormDefinitionArray::class);
+        $result = $typeConverter->convertFrom($input, FormDefinitionArray::class);
 
         $this->assertInstanceOf(FormDefinitionArray::class, $result);
         $this->assertSame($expected, $result->getArrayCopy());
     }
+
+    /**
+     * @test
+     */
+    public function convertFromThrowsExceptionIfJsonIsInvalid()
+    {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1512578002);
+
+        $typeConverter = new FormDefinitionArrayConverter();
+        $input = '{"francine":"stan",';
+
+        $typeConverter->convertFrom($input, FormDefinitionArray::class);
+    }
+
+    /**
+     * @test
+     */
+    public function transformMultiValueElementsForFormFrameworkTransformValues()
+    {
+        $typeConverter = $this->getAccessibleMock(FormDefinitionArrayConverter::class, ['dummy'], [], '', false);
+
+        $input = [
+            'foo1' => 'bar',
+            'foo2' => [
+                'foo3' => [
+                    [
+                        '_label' => 'xxx1',
+                        '_value' => 'yyy1',
+                    ],
+                    [
+                    '_label' => 'xxx2',
+                    '_value' => 'yyy2',
+                    ],
+                    [
+                    '_label' => 'xxx3',
+                    '_value' => 'yyy2',
+                    ],
+                ],
+                '_label' => 'xxx',
+                '_value' => 'yyy',
+            ],
+            '_label' => 'xxx',
+            '_value' => 'yyy',
+        ];
+
+        $expected = [
+            'foo1' => 'bar',
+            'foo2' => [
+                'foo3' => [
+                    'yyy1' => 'xxx1',
+                    'yyy2' => 'xxx3',
+                ],
+                '_label' => 'xxx',
+                '_value' => 'yyy',
+            ],
+            '_label' => 'xxx',
+            '_value' => 'yyy',
+        ];
+
+        $this->assertSame($expected, $typeConverter->_call('transformMultiValueElementsForFormFramework', $input));
+    }
+
+    /**
+     * @test
+     */
+    public function convertFromThrowsExceptionIfPrototypeNameWasChanged()
+    {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528538322);
+
+        $sessionToken = '123';
+        $typeConverter = $this->getAccessibleMock(FormDefinitionArrayConverter::class, ['retrieveSessionToken'], [], '', false);
+
+        $typeConverter->expects($this->any())->method(
+            'retrieveSessionToken'
+        )->willReturn($sessionToken);
+
+        $input = [
+            'prototypeName' => 'foo',
+            'identifier' => 'test',
+             '_orig_prototypeName' => [
+                 'value' => 'standard',
+                 'hmac' => GeneralUtility::hmac(serialize(['test', 'prototypeName', 'standard']), $sessionToken),
+             ],
+             '_orig_identifier' => [
+                 'value' => 'test',
+                 'hmac' => GeneralUtility::hmac(serialize(['test', 'identifier', 'test']), $sessionToken),
+             ],
+        ];
+
+        $typeConverter->convertFrom(json_encode($input), FormDefinitionArray::class);
+    }
+
+    /**
+     * @test
+     */
+    public function convertFromThrowsExceptionIfIdentifierWasChanged()
+    {
+        $this->expectException(PropertyException::class);
+        $this->expectExceptionCode(1528538322);
+
+        $sessionToken = '123';
+        $typeConverter = $this->getAccessibleMock(FormDefinitionArrayConverter::class, ['retrieveSessionToken'], [], '', false);
+
+        $typeConverter->expects($this->any())->method(
+            'retrieveSessionToken'
+        )->willReturn($sessionToken);
+
+        $input = [
+            'prototypeName' => 'standard',
+            'identifier' => 'xxx',
+             '_orig_prototypeName' => [
+                 'value' => 'standard',
+                 'hmac' => GeneralUtility::hmac(serialize(['test', 'prototypeName', 'standard']), $sessionToken),
+             ],
+             '_orig_identifier' => [
+                 'value' => 'test',
+                 'hmac' => GeneralUtility::hmac(serialize(['test', 'prototypeName', 'test']), $sessionToken),
+             ],
+        ];
+
+        $typeConverter->convertFrom(json_encode($input), FormDefinitionArray::class);
+    }
 }