[BUGFIX] FormEngine: Select correct FlexForm DS for inline records 14/44914/3
authorMorton Jonuschat <m.jonuschat@mojocode.de>
Mon, 23 Nov 2015 11:12:08 +0000 (12:12 +0100)
committerWouter Wolters <typo3@wouterwolters.nl>
Tue, 1 Dec 2015 12:59:32 +0000 (13:59 +0100)
If an inline field is defined within a FlexForm that has multiple data
structures defined the FormAjaxInlineController always selects the
default data structure due to missing context information.

This patch transmits the required context within the AJAX request to
enable selecting the appropriate data structure for the flexform.

It also changes the method to determine the path through the flexform
data structure to work in the case of an inline element configured
within a flexform within another inline element.

Resolves: #71436
Related: #70918
Related: #71564
Related: #71655
Releases: master
Change-Id: I7ecd174b78997ad3b1d1513a15a78cdc7bac23ff
Reviewed-on: https://review.typo3.org/44914
Tested-by: Armin Ruediger Vieweg <armin@v.ieweg.de>
Tested-by: Sebastian Michaelsen <michaelsen@t3seo.de>
Tested-by: Felix Rauch <rauch@skaiamail.de>
Reviewed-by: Claus Due <claus@phpmind.net>
Tested-by: Christopher Orth <corthmail@gmail.com>
Tested-by: Torsten <info@by-torsten.com>
Tested-by: Philipp Wrann <philippwrann@gmail.com>
Reviewed-by: Andreas Fernandez <typo3@scripting-base.de>
Tested-by: Andreas Fernandez <typo3@scripting-base.de>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
typo3/sysext/backend/Classes/Controller/FormInlineAjaxController.php
typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaFlexProcess.php
typo3/sysext/backend/Classes/Form/InlineStackProcessor.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaFlexProcessTest.php

index a17020d..1337a68 100644 (file)
@@ -71,15 +71,18 @@ class FormInlineAjaxController
             $databaseRow = [];
             $vanillaUid = (int)$inlineFirstPid;
         }
+        $databaseRow = $this->addFlexFormDataStructurePointersFromAjaxContext($ajaxArguments, $databaseRow);
+
         $formDataCompilerInputForParent = [
             'vanillaUid' => $vanillaUid,
             'command' => $command,
             'tableName' => $parent['table'],
             'databaseRow' => $databaseRow,
             'inlineFirstPid' => $inlineFirstPid,
-            'columnsToProcess' => [
-                $parentFieldName
-            ],
+            'columnsToProcess' => array_merge(
+                [$parentFieldName],
+                array_keys($databaseRow)
+            ),
             // Do not resolve existing children, we don't need them now
             'inlineResolveExistingChildren' => false,
         ];
@@ -782,11 +785,15 @@ class FormInlineAjaxController
         $pattern = '/^data' . '-' . '(?<firstPidValue>.+?)' . '-' . '(?<anything>.+)$/';
 
         $flexFormPath = [];
+        // Will be checked against the FlexForm configuration as an additional safeguard
+        $foreignTableName = '';
 
         if (preg_match($pattern, $domObjectId, $match)) {
-            $parts = explode('-', $match['anything']);
+            // The flexform path should be the second to last array element,
+            // the foreign table name the last.
+            $parts = array_slice(explode('-', $match['anything']), -2, 2);
 
-            if (!isset($parts[2]) || strpos($parts[2], ':') === false) {
+            if (count($parts) !== 2 || !isset($parts[0]) || strpos($parts[0], ':') === false) {
                 throw new \UnexpectedValueException(
                     'DOM Object ID' . $domObjectId . 'does not contain required information '
                     . 'to extract inline field configuration.',
@@ -794,7 +801,7 @@ class FormInlineAjaxController
                 );
             }
 
-            $fieldParts = GeneralUtility::trimExplode(':', $parts[2]);
+            $fieldParts = GeneralUtility::trimExplode(':', $parts[0]);
 
             // FlexForm parts start with data:
             if (empty($fieldParts) || !isset($fieldParts[1]) || $fieldParts[1] !== 'data') {
@@ -805,13 +812,14 @@ class FormInlineAjaxController
             }
 
             $flexFormPath = array_slice($fieldParts, 2);
+            $foreignTableName = $parts[1];
         }
 
         $childConfig = $parentConfig['ds']['sheets'];
 
         foreach ($flexFormPath as $flexFormNode) {
             // We are dealing with configuration information from a flexform,
-            // not value storage, identifiers that the reference language or
+            // not value storage, identifiers that reference language or
             // value nodes must be skipped.
             if (!isset($childConfig[$flexFormNode]) && preg_match('/^[lv][[:alpha:]]+$/', $flexFormNode)) {
                 continue;
@@ -827,6 +835,7 @@ class FormInlineAjaxController
         if (!isset($childConfig['config'])
             || !is_array($childConfig['config'])
             || $childConfig['config']['type'] !== 'inline'
+            || $childConfig['config']['foreign_table'] !== $foreignTableName
         ) {
             throw new \UnexpectedValueException(
                 'Configuration retrieved from FlexForm is incomplete or not of type "inline".',
@@ -835,4 +844,34 @@ class FormInlineAjaxController
         }
         return $childConfig['config'];
     }
+
+    /**
+     * Flexforms require additional database columns to be processed to determine the correct
+     * data structure to be used from a flexform. The required columns and their values are
+     * transmitted in the AJAX context of the request and need to be added to the fake database
+     * row for the inline parent.
+     *
+     * @param array $ajaxArguments The AJAX request arguments
+     * @param array $databaseRow The fake database row
+     * @return array The database row with the flexform data structure pointer columns added
+     */
+    protected function addFlexFormDataStructurePointersFromAjaxContext(array $ajaxArguments, array $databaseRow)
+    {
+        if (!isset($ajaxArguments['context'])) {
+            return $databaseRow;
+        }
+
+        $context = json_decode($ajaxArguments['context'], true);
+        if (GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) {
+            return $databaseRow;
+        }
+
+        if (isset($context['config']['flexDataStructurePointers'])
+            && is_array($context['config']['flexDataStructurePointers'])
+        ) {
+            $databaseRow = array_merge($context['config']['flexDataStructurePointers'], $databaseRow);
+        }
+
+        return $databaseRow;
+    }
 }
index e0f22cd..7270ecc 100644 (file)
@@ -121,6 +121,13 @@ class InlineControlContainer extends AbstractContainer
         }
         $inlineStackProcessor->pushStableStructureItem($newStructureItem);
 
+        // Transport the flexform DS identifier fields to the FormAjaxInlineController
+        if (!empty($newStructureItem['flexform'])
+            && isset($this->data['processedTca']['columns'][$field]['config']['ds']['meta']['dataStructurePointers'])
+        ) {
+            $config['flexDataStructurePointers'] = $this->data['processedTca']['columns'][$field]['config']['ds']['meta']['dataStructurePointers'];
+        }
+
         // e.g. data[<table>][<uid>][<field>]
         $nameForm = $inlineStackProcessor->getCurrentStructureFormPrefix();
         // e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2>
index eff12da..b199ae3 100644 (file)
@@ -50,6 +50,7 @@ class TcaFlexProcess implements FormDataProviderInterface
             $result = $this->removeExcludeFieldsFromDataStructure($result, $fieldName, $flexIdentifier);
             $result = $this->removeDisabledFieldsFromDataStructure($result, $fieldName, $pageTsConfigOfFlex);
             $result = $this->modifyDataStructureAndDataValuesByFlexFormSegmentGroup($result, $fieldName, $pageTsConfigOfFlex);
+            $result = $this->addDataStructurePointersToMetaData($result, $fieldName);
         }
 
         return $result;
@@ -533,4 +534,36 @@ class TcaFlexProcess implements FormDataProviderInterface
     {
         return $GLOBALS['BE_USER'];
     }
+
+    /**
+     * Add fields and values used by ds_pointerField to the meta data array so they can be used in AJAX context during rendering.
+     *
+     * @todo: This method is a stopgap measure to get required information into the AJAX controller
+     *
+     * @param array $result Result array
+     * @param string $fieldName Current handle field name
+     * @return array
+     * @internal
+     */
+    protected function addDataStructurePointersToMetaData(array $result, $fieldName)
+    {
+        if (empty($result['processedTca']['columns'][$fieldName]['config']['ds_pointerField'])) {
+            return $result;
+        }
+
+        $pointerFields = GeneralUtility::trimExplode(
+            ',',
+            $result['processedTca']['columns'][$fieldName]['config']['ds_pointerField']
+        );
+        $dsPointers = [
+            $pointerFields[0] => !empty($result['databaseRow'][$pointerFields[0]]) ? $result['databaseRow'][$pointerFields[0]] : ''
+        ];
+
+        if (!empty($pointerFields[1])) {
+            $dsPointers[$pointerFields[1]] =
+                !empty($result['databaseRow'][$pointerFields[1]]) ? $result['databaseRow'][$pointerFields[1]] : '';
+        }
+        $result['processedTca']['columns'][$fieldName]['config']['ds']['meta']['dataStructurePointers'] = $dsPointers;
+        return $result;
+    }
 }
index ed653cf..f60d16b 100644 (file)
@@ -125,6 +125,8 @@ class InlineStackProcessor
         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['localizationMode'] = BackendUtility::getInlineLocalizationMode(
             $current['table'],
index e15666f..08c60eb 100644 (file)
@@ -111,6 +111,11 @@ class TcaFlexProcessTest extends UnitTestCase
         $expected = $input;
         $expected['processedTca']['columns']['aField']['config']['ds'] = [
             'sheets' => [],
+            'meta' => [
+                'dataStructurePointers' => [
+                    'pointerField' => 'aFlex',
+                ],
+            ],
         ];
 
         $this->assertEquals($expected, $this->subject->addData($input));
@@ -189,6 +194,11 @@ class TcaFlexProcessTest extends UnitTestCase
                     ],
                 ],
             ],
+            'meta' => [
+                'dataStructurePointers' => [
+                    'pointerField' => 'aFlex',
+                ],
+            ],
         ];
 
         $this->assertEquals($expected, $this->subject->addData($input));
@@ -267,6 +277,11 @@ class TcaFlexProcessTest extends UnitTestCase
                     ],
                 ],
             ],
+            'meta' => [
+                'dataStructurePointers' => [
+                    'pointerField' => 'aFlex',
+                ],
+            ],
         ];
 
         $this->assertEquals($expected, $this->subject->addData($input));
@@ -345,6 +360,11 @@ class TcaFlexProcessTest extends UnitTestCase
                     ],
                 ],
             ],
+            'meta' => [
+                'dataStructurePointers' => [
+                    'pointerField' => 'aFlex',
+                ],
+            ],
         ];
 
         $this->assertEquals($expected, $this->subject->addData($input));
@@ -423,6 +443,11 @@ class TcaFlexProcessTest extends UnitTestCase
                     ],
                 ],
             ],
+            'meta' => [
+                'dataStructurePointers' => [
+                    'pointerField' => 'aFlex',
+                ],
+            ],
         ];
 
         $this->assertEquals($expected, $this->subject->addData($input));
@@ -485,6 +510,11 @@ class TcaFlexProcessTest extends UnitTestCase
                     ],
                 ],
             ],
+            'meta' => [
+                'dataStructurePointers' => [
+                    'pointerField' => 'aFlex',
+                ],
+            ],
         ];
 
         $this->assertEquals($expected, $this->subject->addData($input));
@@ -555,6 +585,11 @@ class TcaFlexProcessTest extends UnitTestCase
                     ],
                 ],
             ],
+            'meta' => [
+                'dataStructurePointers' => [
+                    'pointerField' => 'aFlex',
+                ],
+            ],
         ];
 
         $this->assertEquals($expected, $this->subject->addData($input));
@@ -625,6 +660,11 @@ class TcaFlexProcessTest extends UnitTestCase
                     ],
                 ],
             ],
+            'meta' => [
+                'dataStructurePointers' => [
+                    'pointerField' => 'aFlex',
+                ],
+            ],
         ];
 
         $this->assertEquals($expected, $this->subject->addData($input));
@@ -697,6 +737,11 @@ class TcaFlexProcessTest extends UnitTestCase
                     ],
                 ],
             ],
+            'meta' => [
+                'dataStructurePointers' => [
+                    'pointerField' => 'aFlex',
+                ],
+            ],
         ];
 
         $this->assertEquals($expected, $this->subject->addData($input));
@@ -802,6 +847,11 @@ class TcaFlexProcessTest extends UnitTestCase
                     ],
                 ],
             ],
+            'meta' => [
+                'dataStructurePointers' => [
+                    'pointerField' => 'aFlex',
+                ],
+            ],
         ];
 
         $this->assertEquals($expected, $this->subject->addData($input));
@@ -864,6 +914,11 @@ class TcaFlexProcessTest extends UnitTestCase
         $this->backendUserProphecy->checkLanguageAccess(Argument::cetera())->willReturn(true);
 
         $expected = $input;
+        $expected['processedTca']['columns']['aField']['config']['ds']['meta'] = [
+            'dataStructurePointers' => [
+                'pointerField' => 'aFlex'
+            ],
+        ];
         $expected['databaseRow']['aField']['data']['sDEF']['lDEF']['aFlexField']['vDEF'] = 'defaultValue';
 
         $this->assertEquals($expected, $this->subject->addData($input));