From 08ae979e21c2004fe055c2ceaed97aea5e3fdb5e Mon Sep 17 00:00:00 2001
From: Alexander Bohndorf <bohndorf@web.de>
Date: Thu, 29 Nov 2018 13:40:51 +0100
Subject: [PATCH] [BUGFIX] Allow unique for fields with l10n_mode=exclude

Ignore uniqueness in translated records with same value

Resolves: #87038
Releases: master
Change-Id: I2b074e4a9c457a8a658bdc3f74c212a3976fe0fa
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/58979
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Daniel Goerz <daniel.goerz@posteo.de>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Tobi Kretschmann <tobi@tobishome.de>
Reviewed-by: Ulrich Mathes <mathes@sitegeist.de>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 .../core/Classes/DataHandling/DataHandler.php | 21 ++++-
 .../DataHandler/DataSet/LiveDefaultPages.csv  | 12 +--
 .../DataHandler/GetUniqueTranslationTest.php  | 77 +++++++++++++++++++
 3 files changed, 103 insertions(+), 7 deletions(-)

diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
index 0cce838aedb1..ca79781e3903 100644
--- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php
+++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
@@ -2425,6 +2425,26 @@ class DataHandler implements LoggerAwareInterface
                 $queryBuilder->expr()->eq($field, $queryBuilder->createPositionalParameter($value, \PDO::PARAM_STR)),
                 $queryBuilder->expr()->neq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT))
             );
+        // ignore translations of current record if field is configured with l10n_mode = "exclude"
+        if ($GLOBALS['TCA'][$table]['columns'][$field]['l10n_mode'] ?? '' === 'exclude'
+            && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? '' !== ''
+            && $GLOBALS['TCA'][$table]['columns'][$field]['languageField'] ?? '' !== '') {
+            $queryBuilder
+                ->andWhere(
+                    $queryBuilder->expr()->orX(
+                    // records without l10n_parent must be taken into account (in any language)
+                        $queryBuilder->expr()->eq(
+                            $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
+                            $queryBuilder->createPositionalParameter(0, \PDO::PARAM_INT)
+                        ),
+                        // translations of other records must be taken into account
+                        $queryBuilder->expr()->neq(
+                            $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
+                            $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT)
+                        )
+                    )
+                );
+        }
         if ($pid !== 0) {
             $queryBuilder->andWhere(
                 $queryBuilder->expr()->eq('pid', $queryBuilder->createPositionalParameter($pid, \PDO::PARAM_INT))
@@ -2435,7 +2455,6 @@ class DataHandler implements LoggerAwareInterface
                 $queryBuilder->expr()->gte('pid', $queryBuilder->createPositionalParameter(0, \PDO::PARAM_INT))
             );
         }
-
         return $queryBuilder->execute();
     }
 
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/LiveDefaultPages.csv b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/LiveDefaultPages.csv
index 2f118c797f9f..5eaacc9a803b 100644
--- a/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/LiveDefaultPages.csv
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/LiveDefaultPages.csv
@@ -1,6 +1,6 @@
-"pages",,,,,,,,,,,,
-,"uid","pid","sorting","deleted","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","keywords"
-,1,0,256,0,0,0,0,0,0,0,"FunctionalTest","functional"
-,88,1,256,0,0,0,0,0,0,0,"DataHandlerTest","datahandler"
-,89,88,256,0,0,0,0,0,0,0,"Relations","relations"
-,90,88,512,0,0,0,0,0,0,0,"Target","target"
+"pages",,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","keywords","nav_title"
+,1,0,256,0,0,0,0,0,0,0,"FunctionalTest","functional",""
+,88,1,256,0,0,0,0,0,0,0,"DataHandlerTest","datahandler","datahandler"
+,89,88,256,0,0,0,0,0,0,0,"Relations","relations",""
+,90,88,512,0,0,0,0,0,0,0,"Target","target",""
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/GetUniqueTranslationTest.php b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/GetUniqueTranslationTest.php
index 01b9b81b0fad..d08f52fd5f6f 100644
--- a/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/GetUniqueTranslationTest.php
+++ b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/GetUniqueTranslationTest.php
@@ -33,13 +33,90 @@ class GetUniqueTranslationTest extends AbstractDataHandlerActionTestCase
     {
         // Mis-using the "keywords" field in the scenario data-set to check for uniqueness
         $GLOBALS['TCA']['pages']['columns']['keywords']['l10n_mode'] = 'exclude';
+        $GLOBALS['TCA']['pages']['columns']['keywords']['transOrigPointerField'] = 'l10n_parent';
+        $GLOBALS['TCA']['pages']['columns']['keywords']['languageField'] = 'sys_language_uid';
         $GLOBALS['TCA']['pages']['columns']['keywords']['config']['eval'] = 'unique';
         $map = $this->actionService->localizeRecord('pages', self::PAGE_DATAHANDLER, 1);
         $newPageId = $map['pages'][self::PAGE_DATAHANDLER];
+
         $originalLanguageRecord = BackendUtility::getRecord('pages', self::PAGE_DATAHANDLER);
         $translatedRecord = BackendUtility::getRecord('pages', $newPageId);
 
         self::assertEquals('datahandler', $originalLanguageRecord['keywords']);
         self::assertEquals('datahandler', $translatedRecord['keywords']);
     }
+
+    /**
+     * @test
+     */
+    public function valueOfUniqueFieldExcludedInTranslationIsUntouchedInOriginalLanguage(): void
+    {
+        // Mis-using the "nav_title" field in the scenario data-set to check for uniqueness
+        $GLOBALS['TCA']['pages']['columns']['nav_title']['l10n_mode'] = 'exclude';
+        $GLOBALS['TCA']['pages']['columns']['nav_title']['transOrigPointerField'] = 'l10n_parent';
+        $GLOBALS['TCA']['pages']['columns']['nav_title']['languageField'] = 'sys_language_uid';
+        $GLOBALS['TCA']['pages']['columns']['nav_title']['config']['eval'] = 'unique';
+        $map = $this->actionService->localizeRecord('pages', self::PAGE_DATAHANDLER, 1);
+        $newPageId = $map['pages'][self::PAGE_DATAHANDLER];
+
+        $translatedRecord = BackendUtility::getRecord('pages', $newPageId);
+        $this->actionService->modifyRecord('pages', self::PAGE_DATAHANDLER, [
+            'title' => 'DataHandlerTest changed',
+            'nav_title' => 'datahandler'
+        ]);
+        $originalLanguageRecord = BackendUtility::getRecord('pages', self::PAGE_DATAHANDLER);
+
+        $this->assertEquals('DataHandlerTest changed', $originalLanguageRecord['title']);
+        $this->assertEquals('datahandler', $originalLanguageRecord['nav_title']);
+        $this->assertEquals('datahandler', $translatedRecord['nav_title']);
+    }
+
+    /**
+     * @test
+     */
+    public function valueOfUniqueFieldExcludedInTranslationIsIncrementedInNewOriginalRecord(): void
+    {
+        // Mis-using the "nav_title" field in the scenario data-set to check for uniqueness
+        $GLOBALS['TCA']['pages']['columns']['nav_title']['l10n_mode'] = 'exclude';
+        $GLOBALS['TCA']['pages']['columns']['nav_title']['transOrigPointerField'] = 'l10n_parent';
+        $GLOBALS['TCA']['pages']['columns']['nav_title']['languageField'] = 'sys_language_uid';
+        $GLOBALS['TCA']['pages']['columns']['nav_title']['config']['eval'] = 'unique';
+        $map = $this->actionService->createNewRecord('pages', -self::PAGE_DATAHANDLER, [
+            'title' => 'New Page',
+            'doktype' => 1
+        ]);
+        $newPageId = $map['pages'][0];
+
+        $this->actionService->modifyRecord('pages', $newPageId, [
+            'nav_title' => 'datahandler'
+        ]);
+        $originalLanguageRecord = BackendUtility::getRecord('pages', self::PAGE_DATAHANDLER);
+        $newRecord = BackendUtility::getRecord('pages', $newPageId);
+        $this->assertEquals('datahandler', $originalLanguageRecord['nav_title']);
+        $this->assertEquals('datahandler0', $newRecord['nav_title']);
+    }
+
+    /**
+     * @test
+     */
+    public function valueOfUniqueFieldExcludedInTranslationIsIncrementedInNewTranslatedRecord(): void
+    {
+        // Mis-using the "nav_title" field in the scenario data-set to check for uniqueness
+        $GLOBALS['TCA']['pages']['columns']['nav_title']['l10n_mode'] = 'exclude';
+        $GLOBALS['TCA']['pages']['columns']['nav_title']['transOrigPointerField'] = 'l10n_parent';
+        $GLOBALS['TCA']['pages']['columns']['nav_title']['languageField'] = 'sys_language_uid';
+        $GLOBALS['TCA']['pages']['columns']['nav_title']['config']['eval'] = 'unique';
+        $map = $this->actionService->createNewRecord('pages', -self::PAGE_DATAHANDLER, [
+            'title' => 'New Page',
+            'doktype' => 1,
+            'nav_title' => 'datahandler',
+            'sys_language_uid' => 1
+        ]);
+        $newPageId = $map['pages'][0];
+
+        $defaultLanguageRecord = BackendUtility::getRecord('pages', self::PAGE_DATAHANDLER);
+        $newRecord = BackendUtility::getRecord('pages', $newPageId);
+        $this->assertEquals('datahandler', $defaultLanguageRecord['nav_title']);
+        $this->assertEquals('datahandler0', $newRecord['nav_title']);
+    }
 }
-- 
GitLab