[BUGFIX] Proper form definition validation if backend language changes 38/60638/5
authorRalf Zimmermann <rz@tritum.de>
Wed, 1 May 2019 17:44:40 +0000 (19:44 +0200)
committerSusanne Moog <look@susi.dev>
Sat, 26 Oct 2019 09:09:21 +0000 (11:09 +0200)
If a form element property is not defined through a form editor
inspector, the "predefinedDefaults" value from the form editor setup
will be used for some data integrity checks (such properties are
immutable).

Now, such checks against the "predefinedDefaults" values are *only* used
in scenarios where form elements are newely created. All the following
integrety checks on this value will be based on hmac validation.

In addition, this patchset fixes a faulty validation in the following
(edgy) scenario:

* backend language is EN
* open the form edtior and add a ContentElement form element
* switch to another browser tab and change the backend language to DE
* clear the cache
* go back to the form editor and click the save button

This is done by getting all translations (for all backend languages)
for the untranslated! "predefinedDefaults" value and compare the
(already translated) value (from the form definition) against the
possible translations from "predefinedDefaults".

There is an extended scenario which is out of scope for fixing:

* the same scenario as above + delete the previous chosen backend
  language within the maintenance tool

Resolves: #87520
Releases: master, 9.5
Change-Id: I6f486956c24121c0065b67b4f2179301e2a344c4
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/60638
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Tobi Kretschmann <tobi@tobishome.de>
Tested-by: Sascha Rademacher <sascha.rademacher+typo3@gmail.com>
Tested-by: Julian Geils <j_geils@web.de>
Tested-by: Susanne Moog <look@susi.dev>
Reviewed-by: Tobi Kretschmann <tobi@tobishome.de>
Reviewed-by: Sascha Rademacher <sascha.rademacher+typo3@gmail.com>
Reviewed-by: Julian Geils <j_geils@web.de>
Reviewed-by: Susanne Moog <look@susi.dev>
typo3/sysext/form/Classes/Domain/Configuration/ConfigurationService.php
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CreatableFormElementPropertiesValidator.php
typo3/sysext/form/Classes/Domain/Configuration/FormDefinition/Validators/CreatablePropertyCollectionElementPropertiesValidator.php
typo3/sysext/form/Classes/Service/TranslationService.php
typo3/sysext/form/Tests/Unit/Domain/Configuration/ConfigurationServiceTest.php
typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinition/Validators/CreatableFormElementPropertiesValidatorTest.php
typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinition/Validators/CreatablePropertyCollectionElementPropertiesValidatorTest.php
typo3/sysext/form/Tests/Unit/Domain/Configuration/FormDefinitionValidationServiceTest.php

index 4a9e13d..b5ee05f 100644 (file)
@@ -214,11 +214,12 @@ class ConfigurationService implements SingletonInterface
      * * formElementsDefinition.<formElementType>.formEditor.predefinedDefaults.<propertyPath> = "default value"
      *
      * @param ValidationDto $dto
+     * @param bool $translated
      * @return mixed
      * @throws PropertyException
      * @internal
      */
-    public function getFormElementPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto)
+    public function getFormElementPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto, bool $translated = true)
     {
         if (!$this->isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
             throw new PropertyException(
@@ -234,7 +235,9 @@ class ConfigurationService implements SingletonInterface
         $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
             $dto->getPrototypeName()
         );
-        return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()]['predefinedDefaults'][$dto->getPropertyPath()];
+
+        $property = $translated ? 'predefinedDefaults' : 'untranslatedPredefinedDefaults';
+        return $formDefinitionValidationConfiguration['formElements'][$dto->getFormElementType()][$property][$dto->getPropertyPath()];
     }
 
     /**
@@ -265,11 +268,12 @@ class ConfigurationService implements SingletonInterface
      * * <validatorsDefinition|finishersDefinition>.<index>.formEditor.predefinedDefaults.<propertyPath> = "default value"
      *
      * @param ValidationDto $dto
+     * @param bool $translated
      * @return mixed
      * @throws PropertyException
      * @internal
      */
-    public function getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto)
+    public function getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup(ValidationDto $dto, bool $translated = true)
     {
         if (!$this->isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
             throw new PropertyException(
@@ -286,7 +290,9 @@ class ConfigurationService implements SingletonInterface
         $formDefinitionValidationConfiguration = $this->buildFormDefinitionValidationConfigurationFromFormEditorSetup(
             $dto->getPrototypeName()
         );
-        return $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()]['predefinedDefaults'][$dto->getPropertyPath()];
+
+        $property = $translated ? 'predefinedDefaults' : 'untranslatedPredefinedDefaults';
+        return $formDefinitionValidationConfiguration['collections'][$dto->getPropertyCollectionName()][$dto->getPropertyCollectionElementIdentifier()][$property][$dto->getPropertyPath()];
     }
 
     /**
@@ -351,6 +357,22 @@ class ConfigurationService implements SingletonInterface
     }
 
     /**
+     * @param string $key
+     * @param string $prototypeName
+     * @return array
+     */
+    public function getAllBackendTranslationsForTranslationKey(string $key, string $prototypeName): array
+    {
+        $prototypeConfiguration = $this->getPrototypeConfiguration($prototypeName);
+
+        return $this->getTranslationService()->translateToAllBackendLanguages(
+            $key,
+            [],
+            $prototypeConfiguration['formEditor']['translationFiles'] ?? []
+        );
+    }
+
+    /**
      * Collect all the form editor configurations which are needed to check if a
      * form definition property can be written or not.
      *
@@ -626,6 +648,7 @@ class ConfigurationService implements SingletonInterface
             if (!isset($formElement['predefinedDefaults'])) {
                 continue;
             }
+            $formElement['untranslatedPredefinedDefaults'] = $formElement['predefinedDefaults'];
             $formElement['predefinedDefaults'] = $this->getTranslationService()->translateValuesRecursive(
                 $formElement['predefinedDefaults'],
                 $prototypeConfiguration['formEditor']['translationFiles'] ?? []
index 2166871..3eda63e 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
 
 /**
@@ -37,20 +38,53 @@ class CreatableFormElementPropertiesValidator extends ElementBasedValidator
         $dto = $this->validationDto->withPropertyPath($key);
 
         if (!$this->getConfigurationService()->isFormElementPropertyDefinedInFormEditorSetup($dto)) {
-            if ($this->getConfigurationService()->isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
+            if (
+                $this->getConfigurationService()->isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)
+                && !ArrayUtility::isValidPath($this->currentElement, $this->buildHmacDataPath($dto->getPropertyPath()), '.')
+            ) {
+                // If the form element is newely created, we have to compare the $value (form definition) with $predefinedDefaultValue (form setup)
+                // to check the integrity (at this time we don't have a hmac for the $value to check the integrity)
                 $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
-                    );
+                    $throwException = true;
+
+                    if (is_string($predefinedDefaultValue)) {
+                        // Last chance:
+                        // Get all translations (from all backend languages) for the untranslated! $predefinedDefaultValue and
+                        // compare the (already translated) $value (from the form definition) against the possible
+                        // translations from $predefinedDefaultValue.
+                        // Usecase:
+                        //   * backend language is EN
+                        //   * open the form edtior and add a ContentElement form element
+                        //   * switch to another browser tab and change the backend language to DE
+                        //   * clear the cache
+                        //   * go back to the form editor and click the save button
+                        // Out of scope:
+                        //   * the same scenario as above + delete the previous chosen backend language within the maintenance tool
+                        $untranslatedPredefinedDefaultValue = $this->getConfigurationService()->getFormElementPredefinedDefaultValueFromFormEditorSetup($dto, false);
+                        $translations = $this->getConfigurationService()->getAllBackendTranslationsForTranslationKey(
+                            $untranslatedPredefinedDefaultValue,
+                            $dto->getPrototypeName()
+                        );
+
+                        if (in_array($value, $translations, true)) {
+                            $throwException = false;
+                        }
+                    }
+
+                    if ($throwException) {
+                        $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(
index 5fee270..ef7fefc 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
 
 /**
@@ -38,7 +39,10 @@ class CreatablePropertyCollectionElementPropertiesValidator extends CollectionBa
         $dto = $this->validationDto->withPropertyPath($key);
 
         if (!$this->getConfigurationService()->isPropertyCollectionPropertyDefinedInFormEditorSetup($dto)) {
-            if ($this->getConfigurationService()->isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)) {
+            if (
+                $this->getConfigurationService()->isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup($dto)
+                && !ArrayUtility::isValidPath($this->currentElement, $this->buildHmacDataPath($dto->getPropertyPath()), '.')
+            ) {
                 $this->validatePropertyCollectionElementPredefinedDefaultValue($value, $dto);
             } else {
                 $this->validatePropertyCollectionElementPropertyValueByHmacData(
@@ -63,21 +67,43 @@ class CreatablePropertyCollectionElementPropertiesValidator extends CollectionBa
         $value,
         ValidationDto $dto
     ): void {
+        // If the property collection element is newely created, we have to compare the $value (form definition) with $predefinedDefaultValue (form setup)
+        // to check the integrity (at this time we don't have a hmac on the value to check the integrity)
         $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
-            );
+            $throwException = true;
+
+            if (is_string($predefinedDefaultValue)) {
+                // Last chance:
+                // Get all translations (from all backend languages) for the untranslated! $predefinedDefaultValue and
+                // compare the (already translated) $value (from the form definition) against the possible
+                // translations from $predefinedDefaultValue.
+                $untranslatedPredefinedDefaultValue = $this->getConfigurationService()->getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup($dto, false);
+                $translations = $this->getConfigurationService()->getAllBackendTranslationsForTranslationKey(
+                    $untranslatedPredefinedDefaultValue,
+                    $dto->getPrototypeName()
+                );
+
+                if (in_array($value, $translations, true)) {
+                    $throwException = false;
+                }
+            }
+
+            if ($throwException) {
+                $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
+                );
+            }
         }
     }
 }
index 80e7a06..9d4e070 100644 (file)
@@ -205,6 +205,35 @@ class TranslationService implements SingletonInterface
     }
 
     /**
+     * @param string $key
+     * @param array $arguments
+     * @param array $translationFiles
+     * @return array the modified array
+     * @internal
+     */
+    public function translateToAllBackendLanguages(
+        string $key,
+        array $arguments = null,
+        array $translationFiles = []
+    ): array {
+        $result = [];
+        $translationFiles = $this->sortArrayWithIntegerKeysDescending($translationFiles);
+
+        foreach ($this->getAllTypo3BackendLanguages() as $language) {
+            $result[$language] = $key;
+            foreach ($translationFiles as $translationFile) {
+                $translatedValue = $this->translate($key, $arguments, $translationFile, $language, $key);
+                if ($translatedValue !== $key) {
+                    $result[$language] = $translatedValue;
+                    break;
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
      * @param FormRuntime $formRuntime
      * @param string $finisherIdentifier
      * @param string $optionKey
@@ -684,6 +713,19 @@ class TranslationService implements SingletonInterface
     }
 
     /**
+     * @return array
+     */
+    protected function getAllTypo3BackendLanguages(): array
+    {
+        $languages = array_merge(
+            ['default'],
+            array_values($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lang']['availableLanguages'] ?? [])
+        );
+
+        return $languages;
+    }
+
+    /**
      * @return LanguageService
      */
     protected function getLanguageService(): LanguageService
index c4df732..725edf5 100644 (file)
@@ -1136,6 +1136,9 @@ class ConfigurationServiceTest extends UnitTestCase
                             'predefinedDefaults' => [
                                 'foo.bar' => 'xxx',
                             ],
+                            'untranslatedPredefinedDefaults' => [
+                                'foo.bar' => 'xxx',
+                            ],
                         ],
                     ],
                 ],
@@ -1472,6 +1475,9 @@ class ConfigurationServiceTest extends UnitTestCase
                                 'predefinedDefaults' => [
                                     'some.property' => 'value',
                                 ],
+                                'untranslatedPredefinedDefaults' => [
+                                    'some.property' => 'value',
+                                ],
                             ],
                         ],
                     ],
index 3bb1895..c43b33c 100644 (file)
@@ -31,7 +31,7 @@ class CreatableFormElementPropertiesValidatorTest extends UnitTestCase
         $this->expectException(PropertyException::class);
         $this->expectExceptionCode(1528588035);
 
-        $validationDto = new ValidationDto(null, null, 'test-1', 'label');
+        $validationDto = new ValidationDto('standard', null, 'test-1', 'label');
         $input = 'xxx';
         $typeConverter = $this->getAccessibleMock(
             CreatableFormElementPropertiesValidator::class,
index c3829d6..679d0f3 100644 (file)
@@ -32,7 +32,7 @@ class CreatablePropertyCollectionElementPropertiesValidatorTest extends UnitTest
         $this->expectException(PropertyException::class);
         $this->expectExceptionCode(1528591502);
 
-        $validationDto = new ValidationDto(null, null, 'test-1', 'label', 'validators', 'StringLength');
+        $validationDto = new ValidationDto('standard', null, 'test-1', 'label', 'validators', 'StringLength');
         $typeConverter = $this->getAccessibleMock(
             CreatablePropertyCollectionElementPropertiesValidator::class,
             ['getConfigurationService'],
index 113b73f..ab9cd3f 100644 (file)
@@ -377,6 +377,10 @@ class FormDefinitionValidationServiceTest extends UnitTestCase
             ],
         ];
 
+        $formElementWithoutHmac = [
+            'test' => 'xxx',
+        ];
+
         $invalidFormElement = [
             'test' => 'xxx1',
             '_orig_test' => [
@@ -455,7 +459,6 @@ class FormDefinitionValidationServiceTest extends UnitTestCase
                 1528588036,
                 $validationDto
             ],
-
             [
                 [
                     'isFormElementPropertyDefinedInFormEditorSetup' => false,
@@ -475,6 +478,17 @@ class FormDefinitionValidationServiceTest extends UnitTestCase
                 ],
                 $formElement,
                 $sessionToken,
+                -1,
+                $validationDto
+            ],
+            [
+                [
+                    'isFormElementPropertyDefinedInFormEditorSetup' => false,
+                    'isFormElementPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => true,
+                    'getFormElementPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                $formElementWithoutHmac,
+                $sessionToken,
                 1528588035,
                 $validationDto
             ],
@@ -501,6 +515,10 @@ class FormDefinitionValidationServiceTest extends UnitTestCase
             ],
         ];
 
+        $formElementWithoutHmac = [
+            'test' => 'xxx',
+        ];
+
         $invalidFormElement = [
             'test' => 'xxx1',
             '_orig_test' => [
@@ -599,6 +617,17 @@ class FormDefinitionValidationServiceTest extends UnitTestCase
                 ],
                 $formElement,
                 $sessionToken,
+                -1,
+                $validationDto
+            ],
+            [
+                [
+                    'isPropertyCollectionPropertyDefinedInFormEditorSetup' => false,
+                    'isPropertyCollectionPropertyDefinedInPredefinedDefaultsInFormEditorSetup' => true,
+                    'getPropertyCollectionPredefinedDefaultValueFromFormEditorSetup' => 'default',
+                ],
+                $formElementWithoutHmac,
+                $sessionToken,
                 1528591502,
                 $validationDto
             ],