From f35b145bbaaad49a83265c92e7c9368c65356fe5 Mon Sep 17 00:00:00 2001
From: Oliver Bartsch <bo@cedev.de>
Date: Fri, 14 Jun 2024 20:08:27 +0200
Subject: [PATCH] [TASK] Render generator fields as hidden fields in
 columnsOnly mode

When using the "columnsOnly" mode to render just
a subset of available fields of a record, while
the subset includes a field of TCA type "slug",
FormEngine needs to also render configured generator
fields. Otherwise the slug fields won't work as
expected, e.g. when recalculating.

However, since it might be confusing for an editor
to get fields rendered, which were not selected,
are those fields now rendered as hidden fields.

This is done by adding those fields to a hidden
palette while omitting duplicates and performing
sanitization.

Resolves: #104115
Releases: main, 12.4
Change-Id: Ia31571bae9913028e81df910462d76dc4314644c
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/84721
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Jochen Roth <rothjochen@gmail.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Jochen Roth <rothjochen@gmail.com>
Tested-by: core-ci <typo3@b13.com>
---
 .../Controller/EditDocumentController.php     | 15 +++++-
 .../Form/Container/ListOfFieldsContainer.php  | 46 ++++++++++++-----
 .../Controller/EditDocumentControllerTest.php | 19 ++++---
 .../Container/ListOfFieldsContainerTest.php   | 50 +++++++++++++++++++
 4 files changed, 107 insertions(+), 23 deletions(-)

diff --git a/typo3/sysext/backend/Classes/Controller/EditDocumentController.php b/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
index 8ae7bdd3fbc5..08def5278569 100644
--- a/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
+++ b/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
@@ -496,11 +496,19 @@ class EditDocumentController
                             $fieldGroups = [$fieldGroups];
                         }
                         foreach ($fieldGroups as $fields) {
-                            $this->columnsOnly[$table] = array_merge($this->columnsOnly[$table], (is_array($fields) ? $fields : GeneralUtility::trimExplode(',', $fields, true)));
+                            $this->columnsOnly['__hiddenGeneratorFields'][$table] = array_merge(
+                                $this->columnsOnly['__hiddenGeneratorFields'][$table] ?? [],
+                                (is_array($fields) ? $fields : GeneralUtility::trimExplode(',', $fields, true))
+                            );
                         }
                     }
                 }
-                $this->columnsOnly[$table] = array_unique($this->columnsOnly[$table]);
+                if (!empty($this->columnsOnly['__hiddenGeneratorFields'][$table])) {
+                    $this->columnsOnly['__hiddenGeneratorFields'][$table] = array_diff(
+                        array_unique($this->columnsOnly['__hiddenGeneratorFields'][$table]),
+                        $this->columnsOnly[$table]
+                    );
+                }
             }
         }
     }
@@ -1185,6 +1193,9 @@ class EditDocumentController
                         // ListOfFieldsContainer instead of FullRecordContainer in OuterWrapContainer
                         if (!empty($this->columnsOnly[$table])) {
                             $formData['fieldListToRender'] = implode(',', $this->columnsOnly[$table]);
+                            if (!empty($this->columnsOnly['__hiddenGeneratorFields'][$table])) {
+                                $formData['hiddenFieldListToRender'] = implode(',', $this->columnsOnly['__hiddenGeneratorFields'][$table]);
+                            }
                         }
 
                         $formData['renderType'] = 'outerWrapContainer';
diff --git a/typo3/sysext/backend/Classes/Form/Container/ListOfFieldsContainer.php b/typo3/sysext/backend/Classes/Form/Container/ListOfFieldsContainer.php
index 4c5fe37bf412..afa2165cc422 100644
--- a/typo3/sysext/backend/Classes/Form/Container/ListOfFieldsContainer.php
+++ b/typo3/sysext/backend/Classes/Form/Container/ListOfFieldsContainer.php
@@ -24,6 +24,9 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  * This is an entry container called from FormEngine to handle a
  * list of specific fields. Access rights are checked here and globalOption array
  * is prepared for further processing of single fields by PaletteAndSingleContainer.
+ *
+ * Using "hiddenFieldListToRender" it's also possible to render additional fields as
+ * hidden fields, which is e.g. used for the "generatorFields" of TCA type "slug".
  */
 class ListOfFieldsContainer extends AbstractContainer
 {
@@ -34,20 +37,41 @@ class ListOfFieldsContainer extends AbstractContainer
      */
     public function render(): array
     {
-        $fieldListToRender = $this->data['fieldListToRender'];
-        $recordTypeValue = $this->data['recordTypeValue'];
+        $options = $this->data;
+        $options['fieldsArray'] = $this->sanitizeFieldList($this->data['fieldListToRender']);
 
-        $fieldListToRender = array_unique(GeneralUtility::trimExplode(',', $fieldListToRender, true));
+        if ($this->data['hiddenFieldListToRender'] ?? false) {
+            $hiddenFieldList = array_diff(
+                $this->sanitizeFieldList($this->data['hiddenFieldListToRender']),
+                $options['fieldsArray']
+            );
+            if ($hiddenFieldList !== []) {
+                $hiddenFieldList = implode(',', $hiddenFieldList);
+                $hiddenPaletteName = 'hiddenFieldsPalette' . md5($hiddenFieldList);
+                $options['processedTca']['palettes'][$hiddenPaletteName] = [
+                    'isHiddenPalette' => true,
+                    'showitem' => $hiddenFieldList,
+                ];
+                $options['fieldsArray'][] = '--palette--;;' . $hiddenPaletteName;
+            }
+        }
 
-        $fieldsByShowitem = $this->data['processedTca']['types'][$recordTypeValue]['showitem'];
+        $options['renderType'] = 'paletteAndSingleContainer';
+        return $this->nodeFactory->create($options)->render();
+    }
+
+    protected function sanitizeFieldList(string $fieldList): array
+    {
+        $fields = array_unique(GeneralUtility::trimExplode(',', $fieldList, true));
+        $fieldsByShowitem = $this->data['processedTca']['types'][$this->data['recordTypeValue']]['showitem'];
         $fieldsByShowitem = GeneralUtility::trimExplode(',', $fieldsByShowitem, true);
 
-        $finalFieldsList = [];
-        foreach ($fieldListToRender as $fieldName) {
+        $allowedFields = [];
+        foreach ($fields as $fieldName) {
             foreach ($fieldsByShowitem as $fieldByShowitem) {
                 $fieldByShowitemArray = $this->explodeSingleFieldShowItemConfiguration($fieldByShowitem);
                 if ($fieldByShowitemArray['fieldName'] === $fieldName) {
-                    $finalFieldsList[] = implode(';', $fieldByShowitemArray);
+                    $allowedFields[] = implode(';', $fieldByShowitemArray);
                     break;
                 }
                 if ($fieldByShowitemArray['fieldName'] === '--palette--'
@@ -59,18 +83,14 @@ class ListOfFieldsContainer extends AbstractContainer
                     foreach ($paletteFields as $paletteField) {
                         $paletteFieldArray = $this->explodeSingleFieldShowItemConfiguration($paletteField);
                         if ($paletteFieldArray['fieldName'] === $fieldName) {
-                            $finalFieldsList[] = implode(';', $paletteFieldArray);
+                            $allowedFields[] = implode(';', $paletteFieldArray);
                             break;
                         }
                     }
                 }
             }
         }
-
-        $options = $this->data;
-        $options['fieldsArray'] = $finalFieldsList;
-        $options['renderType'] = 'paletteAndSingleContainer';
-        return $this->nodeFactory->create($options)->render();
+        return $allowedFields;
     }
 
     protected function getLanguageService(): LanguageService
diff --git a/typo3/sysext/backend/Tests/Unit/Controller/EditDocumentControllerTest.php b/typo3/sysext/backend/Tests/Unit/Controller/EditDocumentControllerTest.php
index e63b8051d2ca..639485944473 100644
--- a/typo3/sysext/backend/Tests/Unit/Controller/EditDocumentControllerTest.php
+++ b/typo3/sysext/backend/Tests/Unit/Controller/EditDocumentControllerTest.php
@@ -43,14 +43,15 @@ final class EditDocumentControllerTest extends UnitTestCase
         ];
         $editDocumentControllerMock->_call('addSlugFieldsToColumnsOnly', $queryParams);
 
-        self::assertEquals($result, array_values($editDocumentControllerMock->_get('columnsOnly')[$tableName]));
+        self::assertEquals($selectedFields, array_values($editDocumentControllerMock->_get('columnsOnly')[$tableName] ?? []));
+        self::assertEquals($result, array_values($editDocumentControllerMock->_get('columnsOnly')['__hiddenGeneratorFields'][$tableName] ?? []));
     }
 
     public static function slugDependentFieldsAreAddedToColumnsOnlyDataProvider(): array
     {
         return [
             'fields in string' => [
-                ['fo', 'bar', 'slug', 'title'],
+                ['title'],
                 ['fo', 'bar', 'slug'],
                 'fake',
                 [
@@ -65,7 +66,7 @@ final class EditDocumentControllerTest extends UnitTestCase
                 ],
             ],
             'fields in string and array' => [
-                ['slug', 'fo', 'title', 'nav_title', 'other_field'],
+                ['nav_title', 'other_field'],
                 ['slug', 'fo', 'title'],
                 'fake',
                 [
@@ -80,7 +81,7 @@ final class EditDocumentControllerTest extends UnitTestCase
                 ],
             ],
             'unique fields' => [
-                ['slug', 'fo', 'title', 'some_field'],
+                ['some_field'],
                 ['slug', 'fo', 'title'],
                 'fake',
                 [
@@ -95,7 +96,7 @@ final class EditDocumentControllerTest extends UnitTestCase
                 ],
             ],
             'fields as comma-separated list' => [
-                ['slug', 'fo', 'title', 'nav_title', 'some_field'],
+                ['nav_title', 'some_field'],
                 ['slug', 'fo', 'title'],
                 'fake',
                 [
@@ -110,7 +111,7 @@ final class EditDocumentControllerTest extends UnitTestCase
                 ],
             ],
             'no slug field given' => [
-                ['slug', 'fo'],
+                [],
                 ['slug', 'fo'],
                 'fake',
                 [
@@ -168,8 +169,10 @@ final class EditDocumentControllerTest extends UnitTestCase
         ];
         $editDocumentControllerMock->_call('addSlugFieldsToColumnsOnly', $queryParams);
 
-        self::assertEquals(['aField', 'aTitle'], array_values($editDocumentControllerMock->_get('columnsOnly')['aTable']));
-        self::assertEquals(['bField', 'bTitle'], array_values($editDocumentControllerMock->_get('columnsOnly')['bTable']));
+        self::assertEquals(['aField'], array_values($editDocumentControllerMock->_get('columnsOnly')['aTable']));
+        self::assertEquals(['aTitle'], array_values($editDocumentControllerMock->_get('columnsOnly')['__hiddenGeneratorFields']['aTable']));
+        self::assertEquals(['bField'], array_values($editDocumentControllerMock->_get('columnsOnly')['bTable']));
+        self::assertEquals(['bTitle'], array_values($editDocumentControllerMock->_get('columnsOnly')['__hiddenGeneratorFields']['bTable']));
     }
 
     public static function resolvePreviewRecordIdDataProvider(): array
diff --git a/typo3/sysext/backend/Tests/Unit/Form/Container/ListOfFieldsContainerTest.php b/typo3/sysext/backend/Tests/Unit/Form/Container/ListOfFieldsContainerTest.php
index c133c965293d..c89d337e0f09 100644
--- a/typo3/sysext/backend/Tests/Unit/Form/Container/ListOfFieldsContainerTest.php
+++ b/typo3/sysext/backend/Tests/Unit/Form/Container/ListOfFieldsContainerTest.php
@@ -180,4 +180,54 @@ final class ListOfFieldsContainerTest extends UnitTestCase
         $subject->setData($input);
         $subject->render();
     }
+
+    #[Test]
+    public function renderAddsHiddenFields(): void
+    {
+        $nodeFactoryMock = $this->createMock(NodeFactory::class);
+        $paletteAndSingleContainerMock = $this->createMock(PaletteAndSingleContainer::class);
+        $paletteAndSingleContainerMock->expects(self::atLeastOnce())->method('render')->withAnyParameters()->willReturn([]);
+
+        $input = [
+            'tableName' => 'aTable',
+            'recordTypeValue' => 'aType',
+            'processedTca' => [
+                'types' => [
+                    'aType' => [
+                        'showitem' => '--palette--;;aPalette,uniqueField,hiddenField,--palette--;;bPalette,',
+                    ],
+                ],
+                'palettes' => [
+                    'aPalette' => [
+                        'showitem' => 'aField',
+                    ],
+                    'bPalette' => [
+                        'showitem' => 'hiddenInPalette',
+                    ],
+                ],
+            ],
+            'fieldListToRender' => 'aField,uniqueField',
+            'hiddenFieldListToRender' => 'hiddenField,uniqueField,iDontExist,hiddenInPalette',
+        ];
+
+        $expected = $input;
+        $expected['renderType'] = 'paletteAndSingleContainer';
+        $expected['processedTca']['palettes']['hiddenFieldsPalette' . md5('hiddenField;;,hiddenInPalette;;')] = [
+            'isHiddenPalette' => true,
+            'showitem' => 'hiddenField;;,hiddenInPalette;;',
+        ];
+        // "uniqueField" is onl rendered once and "iDontExist" is not rendered at all
+        $expected['fieldsArray'] = [
+            'aField;;',
+            'uniqueField;;',
+            '--palette--;;' . 'hiddenFieldsPalette' . md5('hiddenField;;,hiddenInPalette;;'),
+        ];
+
+        $nodeFactoryMock->method('create')->with($expected)->willReturn($paletteAndSingleContainerMock);
+
+        $subject = new ListOfFieldsContainer();
+        $subject->injectNodeFactory($nodeFactoryMock);
+        $subject->setData($input);
+        $subject->render();
+    }
 }
-- 
GitLab