From f9d64b006d6897d0154c349e485ba4bee3db256d Mon Sep 17 00:00:00 2001
From: Jochen Roth <jochen.roth@b13.com>
Date: Tue, 10 Sep 2024 12:04:39 +0200
Subject: [PATCH] [TASK] Disable custom colPos and CType for translations
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Previously, it was possible to allow editors to modify
colPos and CType in FormEngine via connected CEs.

This has been changed when editing translated CEs, that
CType and colPos cannot be changed anymore.
An upgrade wizard is added to ensure that all translated
CEs really contain the same colPos and CType values as
their default language pendants.

Both fields are now set to l10n_mode=exclude to avoid
future inconsistencies.

Resolves: #60357
Releases: main
Change-Id: Id04636d8c131ce8fbc7c9a87d5fc13a6b406eec2
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/85978
Tested-by: André Buchmann <andy.schliesser@gmail.com>
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: André Buchmann <andy.schliesser@gmail.com>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Oliver Bartsch <bo@cedev.de>
---
 .../DataSet/modifyTranslatedContent.csv       |   2 +-
 .../frontend/Configuration/TCA/tt_content.php |   4 +
 ...onizeColPosAndCTypeWithDefaultLanguage.php | 132 ++++++++++++++++++
 .../SynchronizeColPosAndCTypeBase.csv         |  18 +++
 .../SynchronizeColPosAndCTypeResult.csv       |  18 +++
 ...eColPosAndCTypeWithDefaultLanguageTest.php |  40 ++++++
 6 files changed, 213 insertions(+), 1 deletion(-)
 create mode 100644 typo3/sysext/install/Classes/Updates/SynchronizeColPosAndCTypeWithDefaultLanguage.php
 create mode 100644 typo3/sysext/install/Tests/Functional/Updates/Fixtures/SynchronizeColPosAndCTypeBase.csv
 create mode 100644 typo3/sysext/install/Tests/Functional/Updates/Fixtures/SynchronizeColPosAndCTypeResult.csv
 create mode 100644 typo3/sysext/install/Tests/Functional/Updates/SynchronizeColPosAndCTypeWithDefaultLanguageTest.php

diff --git a/typo3/sysext/core/Tests/Functional/DataScenarios/Regular/Modify/DataSet/modifyTranslatedContent.csv b/typo3/sysext/core/Tests/Functional/DataScenarios/Regular/Modify/DataSet/modifyTranslatedContent.csv
index 373cacbcb728..468d684c7b1c 100644
--- a/typo3/sysext/core/Tests/Functional/DataScenarios/Regular/Modify/DataSet/modifyTranslatedContent.csv
+++ b/typo3/sysext/core/Tests/Functional/DataScenarios/Regular/Modify/DataSet/modifyTranslatedContent.csv
@@ -15,7 +15,7 @@
 ,297,89,384,0,0,0,0,0,0,0,0,"Regular Element #1",
 ,298,89,512,0,0,0,0,0,0,0,0,"Regular Element #2",
 ,299,89,768,0,0,0,0,0,0,0,0,"Regular Element #3",
-,300,89,1024,0,1,299,299,0,0,0,0,"Testing Translation #3","{""header"":""Regular Element #3"",""starttime"":""0"",""endtime"":""0""}"
+,300,89,1024,0,1,299,299,0,0,0,0,"Testing Translation #3","{""header"":""Regular Element #3"",""CType"":""text"",""colPos"":""0"",""starttime"":""0"",""endtime"":""0""}"
 ,301,89,384,0,1,297,297,0,0,0,0,"[Translate to Dansk:] Regular Element #1",
 ,302,89,448,0,2,297,301,0,0,0,0,"[Translate to Deutsch:] [Translate to Dansk:] Regular Element #1",
 "sys_refindex"
diff --git a/typo3/sysext/frontend/Configuration/TCA/tt_content.php b/typo3/sysext/frontend/Configuration/TCA/tt_content.php
index d54256e53337..84df3d84a6bb 100644
--- a/typo3/sysext/frontend/Configuration/TCA/tt_content.php
+++ b/typo3/sysext/frontend/Configuration/TCA/tt_content.php
@@ -44,6 +44,8 @@ return [
     'columns' => [
         'CType' => [
             'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.type',
+            'l10n_mode' => 'exclude',
+            'l10n_display' => 'defaultAsReadonly',
             'config' => [
                 'type' => 'select',
                 'renderType' => 'selectSingle',
@@ -171,6 +173,8 @@ return [
         ],
         'colPos' => [
             'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos',
+            'l10n_mode' => 'exclude',
+            'l10n_display' => 'defaultAsReadonly',
             'config' => [
                 'type' => 'select',
                 'renderType' => 'selectSingle',
diff --git a/typo3/sysext/install/Classes/Updates/SynchronizeColPosAndCTypeWithDefaultLanguage.php b/typo3/sysext/install/Classes/Updates/SynchronizeColPosAndCTypeWithDefaultLanguage.php
new file mode 100644
index 000000000000..22cd728f5146
--- /dev/null
+++ b/typo3/sysext/install/Classes/Updates/SynchronizeColPosAndCTypeWithDefaultLanguage.php
@@ -0,0 +1,132 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Install\Updates;
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Attribute\UpgradeWizard;
+
+/**
+ * @since 13.3
+ * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
+ */
+#[UpgradeWizard('synchronizeColPosAndCTypeWithDefaultLanguage')]
+class SynchronizeColPosAndCTypeWithDefaultLanguage implements UpgradeWizardInterface
+{
+    protected const TABLE_NAME = 'tt_content';
+
+    public function getTitle(): string
+    {
+        return 'Migrate "colPos" and "CType" of "tt_content" translations to match their parent.';
+    }
+
+    public function getDescription(): string
+    {
+        return 'Inherit "colPos" and "CType" for "tt_content" translations from their parent elements for consistent translation behavior.';
+    }
+
+    public function getPrerequisites(): array
+    {
+        return [
+            DatabaseUpdatedPrerequisite::class,
+        ];
+    }
+
+    public function updateNecessary(): bool
+    {
+        return $this->getRecordsToUpdate() !== [];
+    }
+
+    public function executeUpdate(): bool
+    {
+        $connection = $this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME);
+        foreach ($this->getRecordsToUpdate() as $record) {
+            $parent = $this->getParentRecord((int)$record['l18n_parent']);
+            if ($parent === [] || $parent === false) {
+                continue;
+            }
+            $connection
+                ->update(
+                    self::TABLE_NAME,
+                    [
+                        'colPos' => (int)$parent['colPos'],
+                        'CType' => (string)$parent['CType'],
+                    ],
+                    [
+                        'uid' => (int)$record['uid'],
+                    ]
+                );
+        }
+
+        return true;
+    }
+
+    protected function getRecordsToUpdate(): array
+    {
+        $queryBuilder = $this->getConnectionPool()->getQueryBuilderForTable(self::TABLE_NAME);
+        $queryBuilder->getRestrictions()->removeAll();
+        $queryBuilder
+            ->select(
+                'translation.uid',
+                'translation.l18n_parent',
+                'translation.deleted',
+                'translation.colPos',
+                'translation.CType',
+                'parent.deleted',
+                'parent.colPos',
+                'parent.CType'
+            )
+            ->from(self::TABLE_NAME, 'translation')
+            ->leftJoin(
+                'translation',
+                self::TABLE_NAME,
+                'parent',
+                $queryBuilder->expr()->eq('translation.l18n_parent', 'parent.uid')
+            )
+            ->where(
+                $queryBuilder->expr()->neq(
+                    'translation.l18n_parent',
+                    $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
+                ),
+                $queryBuilder->expr()->or(
+                    $queryBuilder->expr()->neq('translation.colPos', $queryBuilder->quoteIdentifier('parent.colPos')),
+                    $queryBuilder->expr()->neq('translation.CType', $queryBuilder->quoteIdentifier('parent.CType')),
+                ),
+                $queryBuilder->expr()->eq('translation.deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
+                $queryBuilder->expr()->eq('parent.deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
+            );
+
+        return $queryBuilder->executeQuery()->fetchAllAssociative() ?: [];
+    }
+
+    protected function getParentRecord(int $uid): array|false
+    {
+        return $this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME)
+            ->select(
+                ['colPos', 'CType'],
+                self::TABLE_NAME,
+                ['uid' => $uid]
+            )->fetchAssociative();
+    }
+
+    protected function getConnectionPool(): ConnectionPool
+    {
+        return GeneralUtility::makeInstance(ConnectionPool::class);
+    }
+}
diff --git a/typo3/sysext/install/Tests/Functional/Updates/Fixtures/SynchronizeColPosAndCTypeBase.csv b/typo3/sysext/install/Tests/Functional/Updates/Fixtures/SynchronizeColPosAndCTypeBase.csv
new file mode 100644
index 000000000000..659c0c40396a
--- /dev/null
+++ b/typo3/sysext/install/Tests/Functional/Updates/Fixtures/SynchronizeColPosAndCTypeBase.csv
@@ -0,0 +1,18 @@
+"tt_content",,,,,,,,
+,"uid","pid","colPos","CType","sys_language_uid","l18n_parent","l10n_source","deleted"
+,39,31,0,"header",0,0,0,0
+,41,31,0,"header",0,0,0,0
+,40,31,1,"text",22,39,39,0
+,42,31,1,"text",22,41,41,0
+,43,31,2,"text",23,0,39,0
+,44,31,2,"text",23,0,41,0
+,45,31,2,"text",24,40,43,0
+,46,31,2,"text",24,42,44,0
+,47,31,2,"text",25,0,45,0
+,48,31,2,"text",25,0,46,0
+,49,31,2,"text",22,1234,39,0
+,50,31,2,"text",22,1234,39,0
+,51,31,1,"text",22,39,39,1
+,52,31,1,"text",26,0,39,1
+,53,31,0,"text",27,39,39,0
+,54,31,1,"header",27,39,39,0
diff --git a/typo3/sysext/install/Tests/Functional/Updates/Fixtures/SynchronizeColPosAndCTypeResult.csv b/typo3/sysext/install/Tests/Functional/Updates/Fixtures/SynchronizeColPosAndCTypeResult.csv
new file mode 100644
index 000000000000..e850c0e38f3d
--- /dev/null
+++ b/typo3/sysext/install/Tests/Functional/Updates/Fixtures/SynchronizeColPosAndCTypeResult.csv
@@ -0,0 +1,18 @@
+"tt_content",,,,,,,,
+,"uid","pid","colPos","CType","sys_language_uid","l18n_parent","l10n_source","deleted"
+,39,31,0,"header",0,0,0,0
+,41,31,0,"header",0,0,0,0
+,40,31,0,"header",22,39,39,0
+,42,31,0,"header",22,41,41,0
+,43,31,2,"text",23,0,39,0
+,44,31,2,"text",23,0,41,0
+,45,31,0,"header",24,40,43,0
+,46,31,0,"header",24,42,44,0
+,47,31,2,"text",25,0,45,0
+,48,31,2,"text",25,0,46,0
+,49,31,2,"text",22,1234,39,0
+,50,31,2,"text",22,1234,39,0
+,51,31,1,"text",22,39,39,1
+,52,31,1,"text",26,0,39,1
+,53,31,0,"header",27,39,39,0
+,54,31,0,"header",27,39,39,0
diff --git a/typo3/sysext/install/Tests/Functional/Updates/SynchronizeColPosAndCTypeWithDefaultLanguageTest.php b/typo3/sysext/install/Tests/Functional/Updates/SynchronizeColPosAndCTypeWithDefaultLanguageTest.php
new file mode 100644
index 000000000000..e1df5d87832d
--- /dev/null
+++ b/typo3/sysext/install/Tests/Functional/Updates/SynchronizeColPosAndCTypeWithDefaultLanguageTest.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Install\Tests\Functional\Updates;
+
+use PHPUnit\Framework\Attributes\Test;
+use TYPO3\CMS\Install\Updates\SynchronizeColPosAndCTypeWithDefaultLanguage;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+final class SynchronizeColPosAndCTypeWithDefaultLanguageTest extends FunctionalTestCase
+{
+    protected string $baseDataSet = __DIR__ . '/Fixtures/SynchronizeColPosAndCTypeBase.csv';
+    protected string $resultDataSet = __DIR__ . '/Fixtures/SynchronizeColPosAndCTypeResult.csv';
+
+    #[Test]
+    public function synchronizeColPosWithDefaultLanguage(): void
+    {
+        $subject = new SynchronizeColPosAndCTypeWithDefaultLanguage();
+
+        $this->importCSVDataSet($this->baseDataSet);
+        self::assertTrue($subject->updateNecessary());
+        $subject->executeUpdate();
+        $this->assertCSVDataSet($this->resultDataSet);
+        self::assertFalse($subject->updateNecessary());
+    }
+}
-- 
GitLab