From 640b789108d7972bc4a0b5a91943b62d47d94c97 Mon Sep 17 00:00:00 2001
From: Oliver Bartsch <bo@cedev.de>
Date: Fri, 14 Jun 2024 11:41:04 +0200
Subject: [PATCH] [BUGFIX] Make columnsOnly table-aware

Defining columnsOnly, which is used by the
EditDocumentController to instruct the FormEngine
to only render a subset of available fields
for records is now table aware.

This allows to render records from different
tables in the same request, while respecting
the fields to be rendered on a per-table basis.

The "columnsOnly" parameter therefore now
requires the fields as array under the
corresponding table name:

&columnsOnly[pages][0]=title

A backwards-compatibility layer is in place,
migrating the previous syntax to the new one
by adding the configured fields for the
tables from the edit configuration.

Resolves: #104108
Releases: main, 12.4
Change-Id: I46500100eb369ad117a2cf5c61a9811c6fd5c7a3
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/84711
Tested-by: Jochen Roth <rothjochen@gmail.com>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Jochen Roth <rothjochen@gmail.com>
---
 Build/phpstan/phpstan-baseline.neon           |   5 -
 .../Controller/EditDocumentController.php     |  66 ++++++----
 .../ViewHelpers/Link/EditRecordViewHelper.php |   8 +-
 .../ViewHelpers/Uri/EditRecordViewHelper.php  |   6 +-
 .../Link/EditRecordViewHelperTest.php         |   5 +-
 .../Uri/EditRecordViewHelperTest.php          |   5 +-
 .../Controller/EditDocumentControllerTest.php |  93 ++++++++++++--
 ...-TableDependantDefinitionOfColumnsOnly.rst | 115 ++++++++++++++++++
 .../Controller/PageInformationController.php  |   4 +-
 .../TranslationStatusController.php           |   8 +-
 .../Controller/LinkValidatorController.php    |   4 +-
 11 files changed, 268 insertions(+), 51 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/13.2/Deprecation-104108-TableDependantDefinitionOfColumnsOnly.rst

diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon
index ab0bed3152ec..54679d180d21 100644
--- a/Build/phpstan/phpstan-baseline.neon
+++ b/Build/phpstan/phpstan-baseline.neon
@@ -5,11 +5,6 @@ parameters:
 			count: 1
 			path: ../../typo3/sysext/backend/Classes/Controller/EditDocumentController.php
 
-		-
-			message: "#^If condition is always false\\.$#"
-			count: 1
-			path: ../../typo3/sysext/backend/Classes/Controller/EditDocumentController.php
-
 		-
 			message: "#^Offset 2 does not exist on array\\{\\}\\.$#"
 			count: 1
diff --git a/typo3/sysext/backend/Classes/Controller/EditDocumentController.php b/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
index a435cf518054..8ae7bdd3fbc5 100644
--- a/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
+++ b/typo3/sysext/backend/Classes/Controller/EditDocumentController.php
@@ -93,12 +93,10 @@ class EditDocumentController
     protected $editconf = [];
 
     /**
-     * Comma list of field names to edit. If specified, only those fields will be rendered.
-     * Otherwise all (available) fields in the record are shown according to the TCA type.
-     *
-     * @var string|null
+     * Array of tables with a lists of field names to edit for those tables. If specified, only those fields
+     * will be rendered. Otherwise all (available) fields in the record are shown according to the TCA type.
      */
-    protected $columnsOnly;
+    protected ?array $columnsOnly = null;
 
     /**
      * Default values for fields
@@ -417,12 +415,30 @@ class EditDocumentController
         $this->editconf = $parsedBody['edit'] ?? $queryParams['edit'] ?? [];
         $this->defVals = $parsedBody['defVals'] ?? $queryParams['defVals'] ?? null;
         $this->overrideVals = $parsedBody['overrideVals'] ?? $queryParams['overrideVals'] ?? null;
-        $this->columnsOnly = $parsedBody['columnsOnly'] ?? $queryParams['columnsOnly'] ?? null;
         $this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '');
         $this->closeDoc = (int)($parsedBody['closeDoc'] ?? $queryParams['closeDoc'] ?? self::DOCUMENT_CLOSE_MODE_DEFAULT);
         $this->doSave = ($parsedBody['doSave'] ?? false) && $request->getMethod() === 'POST';
         $this->returnEditConf = (bool)($parsedBody['returnEditConf'] ?? $queryParams['returnEditConf'] ?? false);
 
+        $columnsOnly = $parsedBody['columnsOnly'] ?? $queryParams['columnsOnly'] ?? null;
+        if (is_string($columnsOnly) && $columnsOnly !== '') {
+            // @deprecated remove fallback in v14
+            // Store given columns for the first table - only for b/w compatibility
+            trigger_error(
+                'Providing columnsOnly with no table context is deprecated and will be removed in v14. Define columnsOnly[table][]=field instead.',
+                E_USER_DEPRECATED
+            );
+            $tables = array_keys($this->editconf);
+            foreach ($tables as $table) {
+                $this->columnsOnly[$table] = GeneralUtility::trimExplode(',', $columnsOnly, true);
+            }
+        }
+        if (is_array($columnsOnly) && $columnsOnly !== []) {
+            foreach ($columnsOnly as $table => $fields) {
+                $this->columnsOnly[$table] = is_array($fields) ? $fields : GeneralUtility::trimExplode(',', $fields, true);
+            }
+        }
+
         // Set overrideVals as default values if defVals does not exist.
         // @todo: Why?
         if (!is_array($this->defVals) && is_array($this->overrideVals)) {
@@ -465,20 +481,26 @@ class EditDocumentController
     protected function addSlugFieldsToColumnsOnly(array $queryParams): void
     {
         $data = $queryParams['edit'] ?? [];
-        $data = array_keys($data);
-        $table = reset($data);
-        if ($this->columnsOnly && $table !== false && isset($GLOBALS['TCA'][$table])) {
-            $fields = GeneralUtility::trimExplode(',', $this->columnsOnly, true);
-            foreach ($fields as $field) {
-                $postModifiers = $GLOBALS['TCA'][$table]['columns'][$field]['config']['generatorOptions']['postModifiers'] ?? [];
-                if (isset($GLOBALS['TCA'][$table]['columns'][$field])
-                    && $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'slug'
-                    && (!is_array($postModifiers) || $postModifiers === [])
-                ) {
-                    foreach ($GLOBALS['TCA'][$table]['columns'][$field]['config']['generatorOptions']['fields'] ?? [] as $fields) {
-                        $this->columnsOnly .= ',' . (is_array($fields) ? implode(',', $fields) : $fields);
+        $tables = array_keys($data);
+        foreach ($tables as $table) {
+            if (!empty($this->columnsOnly[$table]) && isset($GLOBALS['TCA'][$table])) {
+                foreach ($this->columnsOnly[$table] as $field) {
+                    $postModifiers = $GLOBALS['TCA'][$table]['columns'][$field]['config']['generatorOptions']['postModifiers'] ?? [];
+                    if (isset($GLOBALS['TCA'][$table]['columns'][$field])
+                        && $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'slug'
+                        && (!is_array($postModifiers) || $postModifiers === [])
+                    ) {
+
+                        $fieldGroups = $GLOBALS['TCA'][$table]['columns'][$field]['config']['generatorOptions']['fields'] ?? [];
+                        if (is_string($fieldGroups)) {
+                            $fieldGroups = [$fieldGroups];
+                        }
+                        foreach ($fieldGroups as $fields) {
+                            $this->columnsOnly[$table] = array_merge($this->columnsOnly[$table], (is_array($fields) ? $fields : GeneralUtility::trimExplode(',', $fields, true)));
+                        }
                     }
                 }
+                $this->columnsOnly[$table] = array_unique($this->columnsOnly[$table]);
             }
         }
     }
@@ -1161,12 +1183,8 @@ class EditDocumentController
 
                         // Set list if only specific fields should be rendered. This will trigger
                         // ListOfFieldsContainer instead of FullRecordContainer in OuterWrapContainer
-                        if ($this->columnsOnly) {
-                            if (is_array($this->columnsOnly)) {
-                                $formData['fieldListToRender'] = $this->columnsOnly[$table];
-                            } else {
-                                $formData['fieldListToRender'] = $this->columnsOnly;
-                            }
+                        if (!empty($this->columnsOnly[$table])) {
+                            $formData['fieldListToRender'] = implode(',', $this->columnsOnly[$table]);
                         }
 
                         $formData['renderType'] = 'outerWrapContainer';
diff --git a/typo3/sysext/backend/Classes/ViewHelpers/Link/EditRecordViewHelper.php b/typo3/sysext/backend/Classes/ViewHelpers/Link/EditRecordViewHelper.php
index 5b47b8b86259..816205f21de2 100644
--- a/typo3/sysext/backend/Classes/ViewHelpers/Link/EditRecordViewHelper.php
+++ b/typo3/sysext/backend/Classes/ViewHelpers/Link/EditRecordViewHelper.php
@@ -56,7 +56,7 @@ use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
  *
  * Output::
  *
- *    <a href="/typo3/record/edit?edit[pages][42]=edit&returnUrl=foo/bar&columnsOnly=title,subtitle">
+ *    <a href="/typo3/record/edit?edit[pages][42]=edit&returnUrl=foo/bar&columnsOnly[pages]=title,subtitle">
  *        Edit record
  *    </a>
  */
@@ -73,7 +73,7 @@ final class EditRecordViewHelper extends AbstractTagBasedViewHelper
         $this->registerUniversalTagAttributes();
         $this->registerArgument('uid', 'int', 'uid of record to be edited', true);
         $this->registerArgument('table', 'string', 'target database table', true);
-        $this->registerArgument('fields', 'string', 'Edit only these fields (comma separated list)', false);
+        $this->registerArgument('fields', 'string', 'Edit only these fields (comma separated list)');
         $this->registerArgument('returnUrl', 'string', 'return to this URL after closing the edit dialog', false, '');
     }
 
@@ -101,7 +101,9 @@ final class EditRecordViewHelper extends AbstractTagBasedViewHelper
             'returnUrl' => $this->arguments['returnUrl'],
         ];
         if ($this->arguments['fields'] ?? false) {
-            $params['columnsOnly'] = $this->arguments['fields'];
+            $params['columnsOnly'] = [
+                $this->arguments['table'] => GeneralUtility::trimExplode(',', $this->arguments['fields'], true),
+            ];
         }
         $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
         $uri = (string)$uriBuilder->buildUriFromRoute('record_edit', $params);
diff --git a/typo3/sysext/backend/Classes/ViewHelpers/Uri/EditRecordViewHelper.php b/typo3/sysext/backend/Classes/ViewHelpers/Uri/EditRecordViewHelper.php
index 5d64632a31e7..a012cbdfef38 100644
--- a/typo3/sysext/backend/Classes/ViewHelpers/Uri/EditRecordViewHelper.php
+++ b/typo3/sysext/backend/Classes/ViewHelpers/Uri/EditRecordViewHelper.php
@@ -46,7 +46,7 @@ use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
  *
  *    <be:uri.editRecord uid="42" table="pages" fields="title,subtitle" returnUrl="foo/bar" />
  *
- * ``<a href="/typo3/record/edit&edit[pages][42]=edit&returnUrl=foo/bar&columnsOnly=title,subtitle">``
+ * ``<a href="/typo3/record/edit&edit[pages][42]=edit&returnUrl=foo/bar&columnsOnly[pages]=title,subtitle">``
  */
 final class EditRecordViewHelper extends AbstractViewHelper
 {
@@ -82,7 +82,9 @@ final class EditRecordViewHelper extends AbstractViewHelper
             'returnUrl' => $arguments['returnUrl'],
         ];
         if ($arguments['fields'] ?? false) {
-            $params['columnsOnly'] = $arguments['fields'];
+            $params['columnsOnly'] = [
+                $arguments['table'] => GeneralUtility::trimExplode(',', $arguments['fields'], true),
+            ];
         }
         $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
         return (string)$uriBuilder->buildUriFromRoute('record_edit', $params);
diff --git a/typo3/sysext/backend/Tests/Functional/ViewHelpers/Link/EditRecordViewHelperTest.php b/typo3/sysext/backend/Tests/Functional/ViewHelpers/Link/EditRecordViewHelperTest.php
index 604b888220dc..1b7b67c641e4 100644
--- a/typo3/sysext/backend/Tests/Functional/ViewHelpers/Link/EditRecordViewHelperTest.php
+++ b/typo3/sysext/backend/Tests/Functional/ViewHelpers/Link/EditRecordViewHelperTest.php
@@ -89,7 +89,7 @@ final class EditRecordViewHelperTest extends FunctionalTestCase
 
         self::assertStringContainsString('/typo3/record/edit', $result);
         self::assertStringContainsString('edit[c_table][43]=edit', $result);
-        self::assertStringContainsString('columnsOnly=canonical_url', $result);
+        self::assertStringContainsString('columnsOnly[c_table][0]=canonical_url', $result);
     }
 
     #[Test]
@@ -103,7 +103,8 @@ final class EditRecordViewHelperTest extends FunctionalTestCase
 
         self::assertStringContainsString('/typo3/record/edit', $result);
         self::assertStringContainsString('edit[c_table][43]=edit', $result);
-        self::assertStringContainsString('columnsOnly=canonical_url,title', $result);
+        self::assertStringContainsString('columnsOnly[c_table][0]=canonical_url', $result);
+        self::assertStringContainsString('columnsOnly[c_table][1]=title', $result);
     }
 
     #[Test]
diff --git a/typo3/sysext/backend/Tests/Functional/ViewHelpers/Uri/EditRecordViewHelperTest.php b/typo3/sysext/backend/Tests/Functional/ViewHelpers/Uri/EditRecordViewHelperTest.php
index 4a8341942dbb..9fe9b2afa3ea 100644
--- a/typo3/sysext/backend/Tests/Functional/ViewHelpers/Uri/EditRecordViewHelperTest.php
+++ b/typo3/sysext/backend/Tests/Functional/ViewHelpers/Uri/EditRecordViewHelperTest.php
@@ -89,7 +89,7 @@ final class EditRecordViewHelperTest extends FunctionalTestCase
 
         self::assertStringContainsString('/typo3/record/edit', $result);
         self::assertStringContainsString('edit[c_table][43]=edit', $result);
-        self::assertStringContainsString('columnsOnly=canonical_url', $result);
+        self::assertStringContainsString('columnsOnly[c_table][0]=canonical_url', $result);
     }
 
     #[Test]
@@ -103,7 +103,8 @@ final class EditRecordViewHelperTest extends FunctionalTestCase
 
         self::assertStringContainsString('/typo3/record/edit', $result);
         self::assertStringContainsString('edit[c_table][43]=edit', $result);
-        self::assertStringContainsString('columnsOnly=canonical_url,title', $result);
+        self::assertStringContainsString('columnsOnly[c_table][0]=canonical_url', $result);
+        self::assertStringContainsString('columnsOnly[c_table][1]=title', $result);
     }
 
     #[Test]
diff --git a/typo3/sysext/backend/Tests/Unit/Controller/EditDocumentControllerTest.php b/typo3/sysext/backend/Tests/Unit/Controller/EditDocumentControllerTest.php
index ac2ff665ceaa..e63b8051d2ca 100644
--- a/typo3/sysext/backend/Tests/Unit/Controller/EditDocumentControllerTest.php
+++ b/typo3/sysext/backend/Tests/Unit/Controller/EditDocumentControllerTest.php
@@ -28,12 +28,12 @@ final class EditDocumentControllerTest extends UnitTestCase
 
     #[DataProvider('slugDependentFieldsAreAddedToColumnsOnlyDataProvider')]
     #[Test]
-    public function slugDependentFieldsAreAddedToColumnsOnly(string $result, string $selectedFields, string $tableName, array $configuration): void
+    public function slugDependentFieldsAreAddedToColumnsOnly(array $result, array $selectedFields, string $tableName, array $configuration): void
     {
         $GLOBALS['TCA'][$tableName]['columns'] = $configuration;
 
         $editDocumentControllerMock = $this->getAccessibleMock(EditDocumentController::class, null, [], '', false);
-        $editDocumentControllerMock->_set('columnsOnly', $selectedFields);
+        $editDocumentControllerMock->_set('columnsOnly', [$tableName => $selectedFields]);
         $queryParams = [
             'edit' => [
                 $tableName => [
@@ -43,15 +43,15 @@ final class EditDocumentControllerTest extends UnitTestCase
         ];
         $editDocumentControllerMock->_call('addSlugFieldsToColumnsOnly', $queryParams);
 
-        self::assertEquals($result, $editDocumentControllerMock->_get('columnsOnly'));
+        self::assertEquals($result, array_values($editDocumentControllerMock->_get('columnsOnly')[$tableName]));
     }
 
     public static function slugDependentFieldsAreAddedToColumnsOnlyDataProvider(): array
     {
         return [
             'fields in string' => [
-                'fo,bar,slug,title',
-                'fo,bar,slug',
+                ['fo', 'bar', 'slug', 'title'],
+                ['fo', 'bar', 'slug'],
                 'fake',
                 [
                     'slug' => [
@@ -65,8 +65,8 @@ final class EditDocumentControllerTest extends UnitTestCase
                 ],
             ],
             'fields in string and array' => [
-                'slug,fo,title,nav_title,title,other_field',
-                'slug,fo,title',
+                ['slug', 'fo', 'title', 'nav_title', 'other_field'],
+                ['slug', 'fo', 'title'],
                 'fake',
                 [
                     'slug' => [
@@ -79,9 +79,39 @@ final class EditDocumentControllerTest extends UnitTestCase
                     ],
                 ],
             ],
+            'unique fields' => [
+                ['slug', 'fo', 'title', 'some_field'],
+                ['slug', 'fo', 'title'],
+                'fake',
+                [
+                    'slug' => [
+                        'config' => [
+                            'type' => 'slug',
+                            'generatorOptions' => [
+                                'fields' => ['title', 'some_field'],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+            'fields as comma-separated list' => [
+                ['slug', 'fo', 'title', 'nav_title', 'some_field'],
+                ['slug', 'fo', 'title'],
+                'fake',
+                [
+                    'slug' => [
+                        'config' => [
+                            'type' => 'slug',
+                            'generatorOptions' => [
+                                'fields' => 'nav_title,some_field',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
             'no slug field given' => [
-                'slug,fo',
-                'slug,fo',
+                ['slug', 'fo'],
+                ['slug', 'fo'],
                 'fake',
                 [
                     'slug' => [
@@ -97,6 +127,51 @@ final class EditDocumentControllerTest extends UnitTestCase
         ];
     }
 
+    #[Test]
+    public function addSlugFieldsToColumnsOnlySupportsMultipleTables(): void
+    {
+        $GLOBALS['TCA']['aTable']['columns'] = [
+            'aField' => [
+                'config' => [
+                    'type' => 'slug',
+                    'generatorOptions' => [
+                        'fields' => ['aTitle'],
+                    ],
+                ],
+            ],
+        ];
+        $GLOBALS['TCA']['bTable']['columns'] = [
+            'bField' => [
+                'config' => [
+                    'type' => 'slug',
+                    'generatorOptions' => [
+                        'fields' => ['bTitle'],
+                    ],
+                ],
+            ],
+        ];
+
+        $editDocumentControllerMock = $this->getAccessibleMock(EditDocumentController::class, null, [], '', false);
+        $editDocumentControllerMock->_set('columnsOnly', [
+            'aTable' => ['aField'],
+            'bTable' => ['bField'],
+        ]);
+        $queryParams = [
+            'edit' => [
+                'aTable' => [
+                    '123' => 'edit',
+                ],
+                'bTable' => [
+                    '456' => 'edit',
+                ],
+            ],
+        ];
+        $editDocumentControllerMock->_call('addSlugFieldsToColumnsOnly', $queryParams);
+
+        self::assertEquals(['aField', 'aTitle'], array_values($editDocumentControllerMock->_get('columnsOnly')['aTable']));
+        self::assertEquals(['bField', 'bTitle'], array_values($editDocumentControllerMock->_get('columnsOnly')['bTable']));
+    }
+
     public static function resolvePreviewRecordIdDataProvider(): array
     {
         return [
diff --git a/typo3/sysext/core/Documentation/Changelog/13.2/Deprecation-104108-TableDependantDefinitionOfColumnsOnly.rst b/typo3/sysext/core/Documentation/Changelog/13.2/Deprecation-104108-TableDependantDefinitionOfColumnsOnly.rst
new file mode 100644
index 000000000000..3eedc43248b6
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/13.2/Deprecation-104108-TableDependantDefinitionOfColumnsOnly.rst
@@ -0,0 +1,115 @@
+.. include:: /Includes.rst.txt
+
+.. _deprecation-104108-1718354448:
+
+================================================================
+Deprecation: #104108 - Table dependant definition of columnsOnly
+================================================================
+
+See :issue:`104108`
+
+Description
+===========
+
+When linking to the edit form it's possible to instruct the
+:php:`EditDocumentController` to only render a subset of available
+fields for corresponding records using the `columnsOnly` functionality,
+by adding the fields to be rendered as a comma-separated list.
+
+However, besides rendering only records form a single table, the edit form
+is also capable of rendering records of different tables in the same request.
+
+Therefore, the limit fields funcionality has been extended to allow
+set the fields to be rendered on a per-table basis. This means that
+passing just a comma-separated list of fields as value for `columnsOnly`
+has been deprecated.
+
+Impact
+======
+
+Passing a comma-separated list of fields as value for `columnsOnly` will
+trigger a PHP deprecation warning. A compatibility layer will automatically
+set the field list for the given tables.
+
+
+Affected installations
+======================
+
+All installations passing a comma-separated list of fields as value for
+`columnsOnly`.
+
+
+Migration
+=========
+
+The field to be rendered have to be passed as :php:`array` under the
+corresponding table name.
+
+An example, buidling such link using the `UriBuilder`:
+
+.. code-block:: php
+
+    $urlParameters = [
+        'edit' => [
+            'pages' => [
+                1 => 'edit',
+            ],
+        ],
+        'columnsOnly' => 'title,slug'
+        'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
+    ];
+
+    GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('record_edit', $urlParameters);
+
+Above example has to be migrated to:
+
+.. code-block:: php
+
+    $urlParameters = [
+        'edit' => [
+            'pages' => [
+                1 => 'edit',
+            ],
+        ],
+        'columnsOnly' => [
+            'pages' => [
+                'title',
+                'slug'
+            ]
+        ],
+        'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
+    ];
+
+    GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('record_edit', $urlParameters);
+
+Additionally, when rendering records form different tables, a configuration
+could look like the following:
+
+.. code-block:: php
+
+    $urlParameters = [
+        'edit' => [
+            'pages' => [
+                1 => 'edit',
+            ],
+            'tt_content' => [
+                2 => 'edit',
+            ],
+        ],
+        'columnsOnly' => [
+            'pages' => [
+                'title',
+                'slug'
+            ],
+            'tt_content' => [
+                'header',
+                'subheader'
+            ]
+        ],
+        'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
+    ];
+
+    // https:://example.com/typo3/record/edit?edit[pages][1]=edit&edit[tt_content][2]=edit&columnsOnly[pages][0]=title&columnsOnly[pages][1]=slug&columnsOnly[tt_content][0]=header&columnsOnly[tt_content][1]=subheader
+    $link = GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('record_edit', $urlParameters);
+
+.. index:: Backend, NotScanned, ext:backend
diff --git a/typo3/sysext/info/Classes/Controller/PageInformationController.php b/typo3/sysext/info/Classes/Controller/PageInformationController.php
index 5d98e2cc1612..6094d608909c 100644
--- a/typo3/sysext/info/Classes/Controller/PageInformationController.php
+++ b/typo3/sysext/info/Classes/Controller/PageInformationController.php
@@ -206,7 +206,9 @@ class PageInformationController extends InfoModuleController
                                 $editIdList => 'edit',
                             ],
                         ],
-                        'columnsOnly' => $field,
+                        'columnsOnly' => [
+                            'pages' => [$field],
+                        ],
                         'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
                     ];
                     $url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
diff --git a/typo3/sysext/info/Classes/Controller/TranslationStatusController.php b/typo3/sysext/info/Classes/Controller/TranslationStatusController.php
index 7d3706e93213..014fb77be982 100644
--- a/typo3/sysext/info/Classes/Controller/TranslationStatusController.php
+++ b/typo3/sysext/info/Classes/Controller/TranslationStatusController.php
@@ -293,7 +293,9 @@ class TranslationStatusController extends InfoModuleController
                         implode(',', $langRecUids[0]) => 'edit',
                     ],
                 ],
-                'columnsOnly' => 'title,nav_title,l18n_cfg,hidden',
+                'columnsOnly' => [
+                    'pages' => ['title', 'nav_title', 'l18n_cfg', 'hidden'],
+                ],
                 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
             ]);
             $editIco = '<a href="' . htmlspecialchars($editUrl)
@@ -325,7 +327,9 @@ class TranslationStatusController extends InfoModuleController
                                 implode(',', $langRecUids[$languageId]) => 'edit',
                             ],
                         ],
-                        'columnsOnly' => 'title,nav_title,hidden',
+                        'columnsOnly' => [
+                            'pages' =>  ['title', 'nav_title', 'hidden'],
+                        ],
                         'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
                     ]);
                     $editButton = '<a href="' . htmlspecialchars($editUrl)
diff --git a/typo3/sysext/linkvalidator/Classes/Controller/LinkValidatorController.php b/typo3/sysext/linkvalidator/Classes/Controller/LinkValidatorController.php
index 44156339a131..9a8d19a232bd 100644
--- a/typo3/sysext/linkvalidator/Classes/Controller/LinkValidatorController.php
+++ b/typo3/sysext/linkvalidator/Classes/Controller/LinkValidatorController.php
@@ -406,7 +406,9 @@ class LinkValidatorController
                         $row['record_uid'] => 'edit',
                     ],
                 ],
-                'columnsOnly' => $row['field'],
+                'columnsOnly' => [
+                    $table => [$row['field']],
+                ],
                 'returnUrl' => $this->getModuleUri(
                     'report',
                     [
-- 
GitLab