[BUGFIX] Respect IRRE parent config in Ajax calls 83/51783/3
authorHelmut Hummel <typo3@helhum.io>
Sat, 25 Feb 2017 19:32:39 +0000 (20:32 +0100)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Sun, 26 Feb 2017 18:56:22 +0000 (19:56 +0100)
The code to transfer the inline parent context to form engine
in Ajax requests exists but is currently non functional in some
situations.

The config is stored as array, which is hashed by serializing
the array, and building the hash on that string. However
that string is not transferred over the wire,
but the json encoded array.

If a float value was present at some place in this array,
json_encode and json_decode will add a slight offset
to these numbers than if the value is serialized.

To avoid such errors, the hmac is now calculated and
checked against the json encoded value.

We also clean up the code in this area to avoid duplication
and improve the hash calculation and comparison.

By doing so, we can clean up and simplify the flex form handling
for IRRE fields as well.

Resolves: #79999
Releases: master
Change-Id: I049d699f9f30edad0a9c8b06bbc3970e2cdac417
Reviewed-on: https://review.typo3.org/51783
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
typo3/sysext/backend/Classes/Controller/FormInlineAjaxController.php
typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php
typo3/sysext/backend/Classes/Form/InlineStackProcessor.php

index df048ed..3eb58a9 100644 (file)
@@ -45,6 +45,7 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController
     public function createAction(ServerRequestInterface $request, ResponseInterface $response)
     {
         $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
+        $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
 
         $domObjectId = $ajaxArguments[0];
         $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
@@ -57,7 +58,7 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController
         /** @var InlineStackProcessor $inlineStackProcessor */
         $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
         $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
-        $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
+        $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
         $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
 
         // Parent, this table embeds the child table
@@ -77,14 +78,11 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController
             $vanillaUid = (int)$inlineFirstPid;
         }
 
-        $flexDataStructureIdentifier = $this->getFlexFormDataStructureIdentifierFromAjaxContext($ajaxArguments);
-        $processedTca = [];
-        if ($flexDataStructureIdentifier) {
-            $processedTca = $GLOBALS['TCA'][$parent['table']];
+        $processedTca = $GLOBALS['TCA'][$parent['table']];
+        $processedTca['columns'][$parentFieldName]['config'] = $parentConfig;
+        if (!empty($parentConfig['dataStructureIdentifier'])) {
             $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
-            $dataStructure = $flexFormTools->parseDataStructureByIdentifier($flexDataStructureIdentifier);
-            $processedTca['columns'][$parentFieldName]['config']['dataStructureIdentifier'] = $flexDataStructureIdentifier;
-            $processedTca['columns'][$parentFieldName]['config']['ds'] = $dataStructure;
+            $processedTca['columns'][$parentFieldName]['config']['ds'] = $flexFormTools->parseDataStructureByIdentifier($parentConfig['dataStructureIdentifier']);
         }
 
         $formDataCompilerInputForParent = [
@@ -242,12 +240,13 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController
 
         $domObjectId = $ajaxArguments[0];
         $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
+        $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
 
         // Parse the DOM identifier, add the levels to the structure stack
         /** @var InlineStackProcessor $inlineStackProcessor */
         $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
         $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
-        $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
+        $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
 
         // Parent, this table embeds the child table
         $parent = $inlineStackProcessor->getStructureLevel(-1);
@@ -258,14 +257,11 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController
             'uid' => (int)$parent['uid'],
         ];
 
-        $flexDataStructureIdentifier = $this->getFlexFormDataStructureIdentifierFromAjaxContext($ajaxArguments);
-        $processedTca = [];
-        if ($flexDataStructureIdentifier) {
-            $processedTca = $GLOBALS['TCA'][$parent['table']];
+        $processedTca = $GLOBALS['TCA'][$parent['table']];
+        $processedTca['columns'][$parentFieldName]['config'] = $parentConfig;
+        if (!empty($parentConfig['dataStructureIdentifier'])) {
             $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
-            $dataStructure = $flexFormTools->parseDataStructureByIdentifier($flexDataStructureIdentifier);
-            $processedTca['columns'][$parentFieldName]['config']['dataStructureIdentifier'] = $flexDataStructureIdentifier;
-            $processedTca['columns'][$parentFieldName]['config']['ds'] = $dataStructure;
+            $processedTca['columns'][$parentFieldName]['config']['ds'] = $flexFormTools->parseDataStructureByIdentifier($parentConfig['dataStructureIdentifier']);
         }
 
         $formDataCompilerInputForParent = [
@@ -350,12 +346,13 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController
         $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax'];
         $domObjectId = $ajaxArguments[0];
         $type = $ajaxArguments[1];
+        $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
 
         /** @var InlineStackProcessor $inlineStackProcessor */
         $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
         // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
         $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
-        $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']);
+        $inlineStackProcessor->injectAjaxConfiguration($parentConfig);
         $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
 
         $jsonArray = false;
@@ -364,6 +361,9 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController
             $parent = $inlineStackProcessor->getStructureLevel(-1);
             $parentFieldName = $parent['field'];
 
+            $processedTca = $GLOBALS['TCA'][$parent['table']];
+            $processedTca['columns'][$parentFieldName]['config'] = $parentConfig;
+
             // Child, a record from this table should be rendered
             $child = $inlineStackProcessor->getUnstableStructure();
 
@@ -375,6 +375,7 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController
                     // TcaInlineExpandCollapseState needs this
                     'uid' => (int)$parent['uid'],
                 ],
+                'processedTca' => $processedTca,
                 'inlineFirstPid' => $inlineFirstPid,
                 'columnsToProcess' => [
                     $parentFieldName
@@ -919,25 +920,26 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController
     }
 
     /**
-     * Inline fields within a flex form need the data structure identifier that
-     * specifies the specific flex form this inline element is in. Retrieve it from
-     * the context array.
+     * Validates the config that is transferred over the wire to provide the
+     * correct TCA config for the parent table
      *
-     * @param array $ajaxArguments The AJAX request arguments
-     * @return string Data structure identifier as json string
+     * @param string $contextString
+     * @return array
+     * @todo: Review this construct - Why can't the ajax call fetch these data on its own and transfers it to client instead?
      */
-    protected function getFlexFormDataStructureIdentifierFromAjaxContext(array $ajaxArguments)
+    protected function extractSignedParentConfigFromRequest(string $contextString): array
     {
-        if (!isset($ajaxArguments['context'])) {
-            return '';
+        if ($contextString === '') {
+            return [];
         }
-
-        $context = json_decode($ajaxArguments['context'], true);
-        if (GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) {
-            return '';
+        $context = json_decode($contextString, true);
+        if (empty($context['config'])) {
+            return [];
         }
-
-        return $context['config']['flexDataStructureIdentifier'] ?? '';
+        if (!\hash_equals(GeneralUtility::hmac(json_encode($context['config']), 'InlineContext'), $context['hmac'])) {
+            return [];
+        }
+        return $context['config'];
     }
 
     /**
index 53a958a..5149db7 100644 (file)
@@ -134,7 +134,7 @@ class InlineControlContainer extends AbstractContainer
         if (!empty($newStructureItem['flexform'])
             && isset($this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'])
         ) {
-            $config['flexDataStructureIdentifier'] = $this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'];
+            $config['dataStructureIdentifier'] = $this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'];
         }
 
         // e.g. data[<table>][<uid>][<field>]
@@ -169,7 +169,7 @@ class InlineControlContainer extends AbstractContainer
             ],
             'context' => [
                 'config' => $config,
-                'hmac' => GeneralUtility::hmac(serialize($config)),
+                'hmac' => GeneralUtility::hmac(json_encode($config), 'InlineContext'),
             ],
         ];
         $this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack'];
index be7cb0f..95a68d6 100644 (file)
@@ -108,26 +108,19 @@ class InlineStackProcessor
     /**
      * Injects configuration via AJAX calls.
      * This is used by inline ajax calls that transfer configuration options back to the stack for initialization
-     * The configuration is validated using HMAC to avoid hijacking.
      *
-     * @param string $contextString Given context string from ajax call
+     * @param array $config Given config extracted from ajax call
      * @return void
      * @todo: Review this construct - Why can't the ajax call fetch these data on its own and transfers it to client instead?
      */
-    public function injectAjaxConfiguration($contextString = '')
+    public function injectAjaxConfiguration(array $config)
     {
         $level = $this->calculateStructureLevel(-1);
-        if (empty($contextString) || $level === false) {
+        if (empty($config) || $level === false) {
             return;
         }
         $current = &$this->inlineStructure['stable'][$level];
-        $context = json_decode($contextString, true);
-        if (GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) {
-            return;
-        }
-        // Remove the data structure pointers, only relevant for the FormInlineAjaxController
-        unset($context['flexDataStructurePointers']);
-        $current['config'] = $context['config'];
+        $current['config'] = $config;
         $current['localizationMode'] = BackendUtility::getInlineLocalizationMode(
             $current['table'],
             $current['config']