From bca913e9d8da4b349c72203b9448c04fcb074f60 Mon Sep 17 00:00:00 2001 From: Ralf Zimmermann Date: Thu, 12 Jul 2018 11:32:45 +0200 Subject: [PATCH] [SECURITY] Filter disallowed properties in form editor 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: bcf5957567fe680866303c0758c37b26afb2c58f Security-Bulletin: TYPO3-CORE-SA-2018-003 Change-Id: Id3d260681419b992553c98a9a408280094191c27 Reviewed-on: https://review.typo3.org/57548 Reviewed-by: Oliver Hader Tested-by: Oliver Hader --- ...FilterDisallowedPropertiesInFormEditor.rst | 109 ++ .../Controller/FormEditorController.php | 52 +- .../ArrayProcessing/ArrayProcessing.php | 76 + .../ArrayProcessing/ArrayProcessor.php | 96 ++ .../Configuration/ConfigurationService.php | 634 +++++++- .../Exception/ArrayProcessorException.php | 25 + .../Exception/PropertyException.php | 27 + .../Converters/AbstractConverter.php | 43 + .../Converters/AddHmacDataConverter.php | 94 ++ ...HmacDataToFormElementPropertyConverter.php | 49 + ...taToPropertyCollectionElementConverter.php | 63 + .../Converters/ConverterDto.php | 169 ++ .../Converters/ConverterInterface.php | 35 + .../Converters/RemoveHmacDataConverter.php | 43 + .../Validators/AbstractValidator.php | 102 ++ .../Validators/CollectionBasedValidator.php | 84 + ...reatableFormElementPropertiesValidator.php | 65 + ...tyCollectionElementPropertiesValidator.php | 83 + .../Validators/ElementBasedValidator.php | 70 + .../FormElementHmacDataValidator.php | 40 + ...ertyCollectionElementHmacDataValidator.php | 42 + .../Validators/ValidationDto.php | 229 +++ .../Validators/ValidatorInterface.php | 36 + .../FormDefinitionConversionService.php | 118 ++ .../FormDefinitionValidationService.php | 386 +++++ .../Extractors/AbstractExtractor.php | 36 + ...dditionalElementPropertyPathsExtractor.php | 37 + .../Extractors/ExtractorDto.php | 67 + .../Extractors/ExtractorInterface.php | 35 + .../IsCreatableFormElementExtractor.php | 62 + .../MultiValuePropertiesExtractor.php | 64 + .../PredefinedDefaultsExtractor.php | 39 + .../FormElement/PropertyPathsExtractor.php | 95 ++ ...ablePropertyCollectionElementExtractor.php | 104 ++ .../MultiValuePropertiesExtractor.php | 87 + .../PredefinedDefaultsExtractor.php | 40 + .../PropertyPathsExtractor.php | 54 + .../FormDefinitionArrayConverter.php | 106 +- .../Controller/FormEditorControllerTest.php | 2 + .../ConfigurationServiceTest.php | 1429 ++++++++++++++++- ...ableFormElementPropertiesValidatorTest.php | 81 + ...llectionElementPropertiesValidatorTest.php | 78 + .../FormDefinitionConversionServiceTest.php | 194 +++ .../FormDefinitionValidationServiceTest.php | 624 +++++++ .../FormDefinitionArrayConverterTest.php | 189 ++- 45 files changed, 6155 insertions(+), 38 deletions(-) create mode 100644 typo3/sysext/core/Documentation/Changelog/8.7.x/Important-85044-FilterDisallowedPropertiesInFormEditor.rst create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/ArrayProcessing/ArrayProcessing.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/ArrayProcessing/ArrayProcessor.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/Exception/ArrayProcessorException.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/Exception/PropertyException.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AbstractConverter.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataConverter.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataToFormElementPropertyConverter.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataToPropertyCollectionElementConverter.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/ConverterDto.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/ConverterInterface.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/RemoveHmacDataConverter.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/AbstractValidator.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CollectionBasedValidator.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CreatableFormElementPropertiesValidator.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CreatablePropertyCollectionElementPropertiesValidator.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ElementBasedValidator.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/FormElementHmacDataValidator.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/PropertyCollectionElementHmacDataValidator.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ValidationDto.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ValidatorInterface.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinitionConversionService.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FormDefinitionValidationService.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/AbstractExtractor.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/AdditionalElementPropertyPathsExtractor.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/ExtractorDto.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/ExtractorInterface.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/IsCreatableFormElementExtractor.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/MultiValuePropertiesExtractor.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/PredefinedDefaultsExtractor.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/PropertyPathsExtractor.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/IsCreatablePropertyCollectionElementExtractor.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/MultiValuePropertiesExtractor.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/PredefinedDefaultsExtractor.php create mode 100644 typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/PropertyPathsExtractor.php create mode 100644 typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinition/Validators/CreatableFormElementPropertiesValidatorTest.php create mode 100644 typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinition/Validators/CreatablePropertyCollectionElementPropertiesValidatorTest.php create mode 100644 typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinitionConversionServiceTest.php create mode 100644 typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinitionValidationServiceTest.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 index 000000000000..8f57e0ea0956 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/8.7.x/Important-85044-FilterDisallowedPropertiesInFormEditor.rst @@ -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..formEditor.editors..propertyPath` +* :yaml:`formElementsDefinition..formEditor.editors..*.propertyPath` +* :yaml:`formElementsDefinition..formEditor.editors..additionalElementPropertyPaths` +* :yaml:`formElementsDefinition..formEditor.propertyCollections...editors..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..formEditor.editors..templateName` = "Inspector-PropertyGridEditor" and :yaml:`formElementsDefinition..formEditor.editors..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..formEditor.propertyCollections...editors..propertyPath` +* :yaml:`formElementsDefinition..formEditor.propertyCollections...editors..*.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..formEditor.propertyCollections...editors..templateName` = "Inspector-PropertyGridEditor" +and :yaml:`formElementsDefinition..formEditor.propertyCollections...editors..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.`). + $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.`). + $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.`). + $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..editors..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..editors..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 diff --git a/typo3/sysext/form/Classes/Controller/FormEditorController.php b/typo3/sysext/form/Classes/Controller/FormEditorController.php index f3c7197bb0fb..82e9f13424e0 100644 --- a/typo3/sysext/form/Classes/Controller/FormEditorController.php +++ b/typo3/sysext/form/Classes/Controller/FormEditorController.php @@ -25,8 +25,10 @@ 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; @@ -73,15 +75,21 @@ public function indexAction(string $formPersistenceIdentifier, string $prototype 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); - if (empty($prototypeName)) { - $prototypeName = isset($formDefinition['prototypeName']) ? $formDefinition['prototypeName'] : 'standard'; + + 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); + } } - $formDefinition['prototypeName'] = $prototypeName; - $formDefinition = $this->filterEmptyArrays($formDefinition); - $configurationService = $this->objectManager->get(ConfigurationService::class); + $formDefinition['prototypeName'] = $prototypeName; $this->prototypeConfiguration = $configurationService->getPrototypeConfiguration($prototypeName); $formDefinition = $this->transformFormDefinitionForFormEditor($formDefinition); @@ -154,7 +162,7 @@ public function initializeSaveFormAction() public function saveFormAction(string $formPersistenceIdentifier, FormDefinitionArray $formDefinition) { $formDefinition = $formDefinition->getArrayCopy(); - $formDefinition = $this->filterEmptyArrays($formDefinition); + if ( isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave']) && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave']) @@ -176,6 +184,10 @@ public function saveFormAction(string $formPersistenceIdentifier, FormDefinition 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', @@ -184,8 +196,6 @@ public function saveFormAction(string $formPersistenceIdentifier, FormDefinition ]; } - $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. @@ -209,12 +219,14 @@ public function renderFormPageAction(FormDefinitionArray $formDefinition, int $p if (empty($prototypeName)) { $prototypeName = isset($formDefinition['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->overrideCurrentPage($pageIndex); + return $form->render(); } @@ -431,6 +443,7 @@ protected function renderFormEditorTemplates(array $formEditorDefinitions): stri } /** + * @todo move this to FormDefinitionConversionService * @param array $formDefinition * @return array */ @@ -448,7 +461,16 @@ protected function transformFormDefinitionForFormEditor(array $formDefinition): } } - 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; } /** @@ -546,6 +568,14 @@ protected function filterEmptyArrays(array $array): array return $array; } + /** + * @return FormDefinitionConversionService + */ + protected function getFormDefinitionConversionService(): FormDefinitionConversionService + { + return GeneralUtility::makeInstance(FormDefinitionConversionService::class); + } + /** * Returns the current BE user. * 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 index 000000000000..b9daf349822a --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/ArrayProcessing/ArrayProcessing.php @@ -0,0 +1,76 @@ +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 index 000000000000..f7c220e9aac2 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/ArrayProcessing/ArrayProcessor.php @@ -0,0 +1,96 @@ +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( + '/(?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; + } +} diff --git a/typo3/sysext/form/Classes/Domain/Configuration/ConfigurationService.php b/typo3/sysext/form/Classes/Domain/Configuration/ConfigurationService.php index c9576dda17bf..37ff8856624b 100644 --- a/typo3/sysext/form/Classes/Domain/Configuration/ConfigurationService.php +++ b/typo3/sysext/form/Classes/Domain/Configuration/ConfigurationService.php @@ -15,17 +15,35 @@ * 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 { /** @@ -33,6 +51,16 @@ class ConfigurationService */ protected $formSettings; + /** + * @var array + */ + protected $firstLevelCache = []; + + /** + * @var TranslationService + */ + protected $translationService; + /** * @internal */ @@ -54,8 +82,608 @@ public function initializeObject() 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..formEditor.editors..propertyPath + * * formElementsDefinition..formEditor.editors..*.propertyPath + * * formElementsDefinition..formEditor.editors..additionalElementPropertyPaths + * * formElementsDefinition..formEditor.propertyCollections...editors..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..formEditor.editors..templateName = Inspector-PropertyGridEditor" + * and + * "formElementsDefinition..formEditor.editors..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..formEditor.propertyCollections...editors..propertyPath + * * formElementsDefinition..formEditor.propertyCollections...editors..*.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..formEditor.propertyCollections...editors..templateName = Inspector-PropertyGridEditor" + * and + * "formElementsDefinition..formEditor.propertyCollections...editors..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..formEditor.predefinedDefaults. = "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..formEditor.predefinedDefaults. = "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: + * * ..formEditor.predefinedDefaults. = "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: + * * ..formEditor.predefinedDefaults. = "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..formEditor.group + * * formElementsDefinition..formEditor.groupSorting + * And the value from "formElementsDefinition..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..formEditor.editors..templateName = Inspector-FinishersEditor" + * or + * "formElementsDefinition..formEditor.editors..templateName = Inspector-ValidatorsEditor" + * and + * "formElementsDefinition..formEditor.editors..selectOptions..value = " + * + * @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) + { + $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 index 000000000000..0f448db7f670 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/Exception/ArrayProcessorException.php @@ -0,0 +1,25 @@ +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 index 000000000000..6cbe2c07155c --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataConverter.php @@ -0,0 +1,94 @@ +" as a sibling of the property key. + * "_orig_" is an array which contains the property value + * and a hmac hash for the property value. + * "_orig_" 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) + { + $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 index 000000000000..c550b26465da --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataToFormElementPropertyConverter.php @@ -0,0 +1,49 @@ +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 index 000000000000..1de7e1463630 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/AddHmacDataToPropertyCollectionElementConverter.php @@ -0,0 +1,63 @@ +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 index 000000000000..d5bb7d6081c5 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/ConverterDto.php @@ -0,0 +1,169 @@ +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 index 000000000000..c07d1db61825 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Converters/ConverterInterface.php @@ -0,0 +1,35 @@ +") for the corresponding property. + * + * @param string $key + * @param mixed $value + */ + public function __invoke(string $key, $value) + { + $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 index 000000000000..24e8fd689f0f --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/AbstractValidator.php @@ -0,0 +1,102 @@ +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 index 000000000000..d732f6de27bb --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CollectionBasedValidator.php @@ -0,0 +1,84 @@ +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 index 000000000000..f23c0bceff41 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CreatableFormElementPropertiesValidator.php @@ -0,0 +1,65 @@ +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 index 000000000000..a9ab1a5c1a9c --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CreatablePropertyCollectionElementPropertiesValidator.php @@ -0,0 +1,83 @@ +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 + ) { + $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 index 000000000000..ccff4748292e --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ElementBasedValidator.php @@ -0,0 +1,70 @@ +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 index 000000000000..114d471c8306 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/FormElementHmacDataValidator.php @@ -0,0 +1,40 @@ +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 index 000000000000..d065e703cf5c --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/PropertyCollectionElementHmacDataValidator.php @@ -0,0 +1,42 @@ +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 index 000000000000..19178d75fdba --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ValidationDto.php @@ -0,0 +1,229 @@ +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 index 000000000000..1d790c73a17f --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/ValidatorInterface.php @@ -0,0 +1,36 @@ +" for each scalar property value + * within the form definition as a sibling of the property key. + * "_orig_" is an array which contains the property value + * and a hmac hash for the property value. + * "_orig_" 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_" 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 index 000000000000..0412b45a85c3 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FormDefinitionValidationService.php @@ -0,0 +1,386 @@ +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 + ) { + 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 + ) { + 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 + ) { + 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 + ) { + 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 index 000000000000..c4ac1e1b2a9d --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/AbstractExtractor.php @@ -0,0 +1,36 @@ +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 index 000000000000..f11b22d0b65d --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/AdditionalElementPropertyPathsExtractor.php @@ -0,0 +1,37 @@ +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 index 000000000000..9049de21f647 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/ExtractorDto.php @@ -0,0 +1,67 @@ +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 index 000000000000..cb7e12db43c8 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/ExtractorInterface.php @@ -0,0 +1,35 @@ +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 index 000000000000..0154ffeaaec4 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/MultiValuePropertiesExtractor.php @@ -0,0 +1,64 @@ +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 index 000000000000..2e2da44fec3f --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/PredefinedDefaultsExtractor.php @@ -0,0 +1,39 @@ +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 index 000000000000..7f88e8aa9d82 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/FormElement/PropertyPathsExtractor.php @@ -0,0 +1,95 @@ +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 + { + list(, $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 index 000000000000..72346c3cc45f --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/IsCreatablePropertyCollectionElementExtractor.php @@ -0,0 +1,104 @@ +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 index 000000000000..bc925bc73251 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/MultiValuePropertiesExtractor.php @@ -0,0 +1,87 @@ +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 index 000000000000..f1b831e3eaa9 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/PredefinedDefaultsExtractor.php @@ -0,0 +1,40 @@ +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 index 000000000000..86f932cf301f --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Configuration/FrameworkConfiguration/Extractors/PropertyCollectionElement/PropertyPathsExtractor.php @@ -0,0 +1,54 @@ +extractorDto->getPrototypeConfiguration(), $identifierPath, '.'); + + $result = $this->extractorDto->getResult(); + $result['formElements'][$formElementType]['collections'][$propertyCollectionName][$identifier]['propertyPaths'][] = $value; + $this->extractorDto->setResult($result); + } +} diff --git a/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/FormDefinitionArrayConverter.php b/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/FormDefinitionArrayConverter.php index e05b32fed6f4..4ad985582d58 100644 --- a/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/FormDefinitionArrayConverter.php +++ b/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/FormDefinitionArrayConverter.php @@ -15,10 +15,15 @@ * 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; /** @@ -43,6 +48,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 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie 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 @@ public function convertFrom($source, $targetType, array $convertedChildPropertie * _value => 'value' * ] * - * This method transform this into: + * This method transforms this into: * * [ * 'value' => 'label' @@ -107,4 +147,62 @@ protected function transformMultiValueElementsForFormFramework(array $input): ar 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']; + } } diff --git a/typo3/sysext/form/Tests/Unit/Controller/FormEditorControllerTest.php b/typo3/sysext/form/Tests/Unit/Controller/FormEditorControllerTest.php index f52df4861e34..653d6c522448 100644 --- a/typo3/sysext/form/Tests/Unit/Controller/FormEditorControllerTest.php +++ b/typo3/sysext/form/Tests/Unit/Controller/FormEditorControllerTest.php @@ -36,7 +36,9 @@ class FormEditorControllerTest extends \TYPO3\TestingFramework\Core\Unit\UnitTes */ public function setUp() { + parent::setUp(); $this->singletonInstances = GeneralUtility::getSingletonInstances(); + $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] = 12345; } /** diff --git a/typo3/sysext/form/Tests/Unit/Domain/Configuration/ConfigurationServiceTest.php b/typo3/sysext/form/Tests/Unit/Domain/Configuration/ConfigurationServiceTest.php index 83bcf02669a6..c503b0ca0051 100644 --- a/typo3/sysext/form/Tests/Unit/Domain/Configuration/ConfigurationServiceTest.php +++ b/typo3/sysext/form/Tests/Unit/Domain/Configuration/ConfigurationServiceTest.php @@ -1,4 +1,5 @@ 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 @@ public function getPrototypeConfigurationReturnsPrototypeConfiguration() */ 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 index 000000000000..32d119e3437f --- /dev/null +++ b/typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinition/Validators/CreatableFormElementPropertiesValidatorTest.php @@ -0,0 +1,81 @@ +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 index 000000000000..aebc30873d3d --- /dev/null +++ b/typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinition/Validators/CreatablePropertyCollectionElementPropertiesValidatorTest.php @@ -0,0 +1,78 @@ +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 index 000000000000..357ec71cab1e --- /dev/null +++ b/typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinitionConversionServiceTest.php @@ -0,0 +1,194 @@ +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 index 000000000000..e9f9382a4fe7 --- /dev/null +++ b/typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinitionValidationServiceTest.php @@ -0,0 +1,624 @@ +singletonInstances = GeneralUtility::getSingletonInstances(); + } + + /** + * Tear down + */ + public function tearDown() + { + GeneralUtility::resetSingletonInstances($this->singletonInstances); + parent::tearDown(); + } + + /** + * @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); + + $objectMangerProphecy = $this->prophesize(ObjectManager::class); + GeneralUtility::setSingletonInstance(ObjectManager::class, $objectMangerProphecy->reveal()); + + $objectMangerProphecy + ->get(ConfigurationService::class) + ->willReturn($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); + + $objectMangerProphecy = $this->prophesize(ObjectManager::class); + GeneralUtility::setSingletonInstance(ObjectManager::class, $objectMangerProphecy->reveal()); + + $objectMangerProphecy + ->get(ConfigurationService::class) + ->willReturn($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 + ], + ]; + } +} diff --git a/typo3/sysext/form/Tests/Unit/Mvc/Property/TypeConverter/FormDefinitionArrayConverterTest.php b/typo3/sysext/form/Tests/Unit/Mvc/Property/TypeConverter/FormDefinitionArrayConverterTest.php index 64000e31f326..3c4e398c40e6 100644 --- a/typo3/sysext/form/Tests/Unit/Mvc/Property/TypeConverter/FormDefinitionArrayConverterTest.php +++ b/typo3/sysext/form/Tests/Unit/Mvc/Property/TypeConverter/FormDefinitionArrayConverterTest.php @@ -14,6 +14,9 @@ * 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; @@ -23,15 +26,69 @@ */ 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 @@ public function convertsJsonStringToFormDefinitionArray() ], ], ]; - $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); + } }