[BUGFIX] Set l10n_state before synchronizing fields 57/54657/8
authorHelmut Hummel <typo3@helhum.io>
Wed, 15 Nov 2017 17:17:49 +0000 (18:17 +0100)
committerOliver Hader <oliver.hader@typo3.org>
Thu, 16 Nov 2017 20:46:17 +0000 (21:46 +0100)
L10nModeUpdater checks whether fields in translated records are
modified, thus not synchronized with the parent record and the
new l10n_state for this field is set to "custom" in this case
or "parent" if the field is empty.

However this is done after the data is synchronized, which
leads to wrong data being written by the DataMapProcessor.

To fix this, we move setting l10n_state upwards in the code,
so that relations and fields from translated records can be
updated correctly.

Resolves: #83006
Releases: master, 8.7
Change-Id: Ibb06eecb6cf743bd6bba1990ca6f9391c983821f
Reviewed-on: https://review.typo3.org/54657
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
typo3/sysext/install/Classes/Updates/RowUpdater/L10nModeUpdater.php
typo3/sysext/install/Tests/Functional/Updates/RowUpdater/DataSet/LiveDefaultElements.csv [new file with mode: 0644]
typo3/sysext/install/Tests/Functional/Updates/RowUpdater/DataSet/LiveDefaultPages.csv [new file with mode: 0644]
typo3/sysext/install/Tests/Functional/Updates/RowUpdater/DataSet/recordsCanBeUpdated.csv [new file with mode: 0644]
typo3/sysext/install/Tests/Functional/Updates/RowUpdater/L10nModeUpdaterTest.php [new file with mode: 0644]

index 5d21dc5..dadd54e 100644 (file)
@@ -18,7 +18,6 @@ namespace TYPO3\CMS\Install\Updates\RowUpdater;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\DataHandling\DataHandler;
-use TYPO3\CMS\Core\DataHandling\Localization\DataMapProcessor;
 use TYPO3\CMS\Core\DataHandling\Localization\State;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -70,7 +69,7 @@ class L10nModeUpdater implements RowUpdaterInterface
      */
     public function updateTableRow(string $tableName, array $inputRow): array
     {
-        $currentId = $inputRow['uid'];
+        $currentId = (int)$inputRow['uid'];
 
         if (empty($this->payload[$tableName]['localizations'][$currentId])) {
             return $inputRow;
@@ -91,39 +90,21 @@ class L10nModeUpdater implements RowUpdaterInterface
         // the admin user is required to defined workspace state when working with DataHandler
         $fakeAdminUser = GeneralUtility::makeInstance(BackendUserAuthentication::class);
         $fakeAdminUser->user = ['uid' => 0, 'username' => '_migration_', 'admin' => 1];
-        $fakeAdminUser->workspace = ($inputRow['t3ver_wsid'] ?? 0);
+        $fakeAdminUser->workspace = (int)($inputRow['t3ver_wsid'] ?? 0);
         $GLOBALS['BE_USER'] = $fakeAdminUser;
 
         $tablePayload = $this->payload[$tableName];
-        $parentId = $tablePayload['localizations'][$currentId];
-        $parentTableName = ($tableName === 'pages_language_overlay' ? 'pages' : $tableName);
 
         $liveId = $currentId;
         if (!empty($inputRow['t3ver_wsid'])
             && !empty($inputRow['t3ver_oid'])
             && !VersionState::cast($inputRow['t3ver_state'])
                 ->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
-            $liveId = $inputRow['t3ver_oid'];
+            $liveId = (int)$inputRow['t3ver_oid'];
         }
 
         $dataMap = [];
 
-        // simulate modifying a parent record to trigger dependent updates
-        if (in_array('exclude', $tablePayload['fieldModes'])) {
-            $parentRecord = $this->getRow($parentTableName, $parentId);
-            foreach ($tablePayload['fieldModes'] as $fieldName => $fieldMode) {
-                if ($fieldMode !== 'exclude') {
-                    continue;
-                }
-                $dataMap[$parentTableName][$parentId][$fieldName] = $parentRecord[$fieldName];
-            }
-            $dataMap = DataMapProcessor::instance($dataMap, $fakeAdminUser)->process();
-            unset($dataMap[$parentTableName][$parentId]);
-            if (empty($dataMap[$parentTableName])) {
-                unset($dataMap[$parentTableName]);
-            }
-        }
-
         // define localization states and thus trigger updates later
         if (State::isApplicable($tableName)) {
             $stateUpdates = [];
@@ -138,33 +119,38 @@ class L10nModeUpdater implements RowUpdaterInterface
                 }
             }
 
+            // fetch the language state upfront, so that calling DataMapProcessor below
+            // will handle mergeIfNotBlank fields properly
             $languageState = State::create($tableName);
             $languageState->update($stateUpdates);
-            // only consider field names that still used mergeIfNotBlank
-            $modifiedFieldNames = array_intersect(
-                array_keys($tablePayload['fieldModes']),
-                $languageState->getModifiedFieldNames()
-            );
-            if (!empty($modifiedFieldNames)) {
-                $dataMap = [
-                    $tableName => [
-                        $liveId => [
-                            'l10n_state' => $languageState->toArray()
-                        ]
+            $dataMap = [
+                $tableName => [
+                    $liveId => [
+                        'l10n_state' => $languageState->toArray()
                     ]
-                ];
-            }
+                ]
+            ];
         }
 
-        if (empty($dataMap)) {
-            return $inputRow;
+        // simulate modifying a parent record to trigger dependent updates
+        if (in_array('exclude', $tablePayload['fieldModes'], true)) {
+            $record = $this->getRow($tableName, $liveId);
+            foreach ($tablePayload['fieldModes'] as $fieldName => $fieldMode) {
+                if ($fieldMode !== 'exclude') {
+                    continue;
+                }
+                $dataMap[$tableName][$liveId][$fieldName] = $record[$fieldName];
+            }
         }
 
-        // let DataHandler process all updates, $inputRow won't change
-        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
-        $dataHandler->enableLogging = false;
-        $dataHandler->start($dataMap, [], $fakeAdminUser);
-        $dataHandler->process_datamap();
+        // in case $dataMap is empty, nothing has to be updated
+        if (!empty($dataMap)) {
+            // let DataHandler process all updates, $inputRow won't change
+            $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
+            $dataHandler->enableLogging = false;
+            $dataHandler->start($dataMap, [], $fakeAdminUser);
+            $dataHandler->process_datamap();
+        }
 
         if (!empty($dataHandlerHooks)) {
             $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php'] = $dataHandlerHooks;
diff --git a/typo3/sysext/install/Tests/Functional/Updates/RowUpdater/DataSet/LiveDefaultElements.csv b/typo3/sysext/install/Tests/Functional/Updates/RowUpdater/DataSet/LiveDefaultElements.csv
new file mode 100644 (file)
index 0000000..5e532ac
--- /dev/null
@@ -0,0 +1,29 @@
+"sys_language",,,,,,,,,,,,,,,,,,
+,"uid","pid","hidden","title","flag",,,,,,,,,,,,,
+,1,0,0,"Dansk","dk",,,,,,,,,,,,,
+,2,0,0,"Deutsch","de",,,,,,,,,,,,,
+"tt_content",,,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","header","image","tx_irretutorial_1nff_hotels",,
+,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1",0,2,,
+,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2",0,0,,
+,299,89,1024,0,1,297,0,0,0,0,0,0,"",0,0,,
+,300,89,2048,0,1,298,0,0,0,0,0,0,"Regular Element #2++",0,1,,
+"tx_irretutorial_1nff_hotel",,,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","l18n_diffsource","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","parentid","parenttable","parentidentifier","offers"
+,2,89,1,0,0,0,,0,0,0,0,0,0,"Hotel #0",89,"pages",,0
+,3,89,1,0,0,0,,0,0,0,0,0,0,"Hotel #1",297,"tt_content",,2
+,4,89,2,0,0,0,,0,0,0,0,0,0,"Hotel #2",297,"tt_content",,1
+,5,89,1,0,1,0,,0,0,0,0,0,0,"Hotel #3",300,"tt_content",,0
+"tx_irretutorial_1nff_offer",,,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","l18n_diffsource","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","parentid","parenttable","parentidentifier","prices"
+,5,89,1,0,0,0,,0,0,0,0,0,0,"Offer #1.1",3,"tx_irretutorial_1nff_hotel",,3
+,6,89,2,0,0,0,,0,0,0,0,0,0,"Offer #1.2",3,"tx_irretutorial_1nff_hotel",,2
+,7,89,1,0,0,0,,0,0,0,0,0,0,"Offer #2.1",4,"tx_irretutorial_1nff_hotel",,1
+"tx_irretutorial_1nff_price",,,,,,,,,,,,,,,,,,
+,"uid","pid","sorting","deleted","sys_language_uid","l18n_parent","l18n_diffsource","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","parentid","parenttable","parentidentifier",
+,7,89,1,0,0,0,,0,0,0,0,0,0,"Price #1.1.1",5,"tx_irretutorial_1nff_offer",,
+,8,89,2,0,0,0,,0,0,0,0,0,0,"Price #1.1.2",5,"tx_irretutorial_1nff_offer",,
+,9,89,3,0,0,0,,0,0,0,0,0,0,"Price #1.1.3",5,"tx_irretutorial_1nff_offer",,
+,10,89,1,0,0,0,,0,0,0,0,0,0,"Price #1.2.1",6,"tx_irretutorial_1nff_offer",,
+,11,89,2,0,0,0,,0,0,0,0,0,0,"Price #1.2.2",6,"tx_irretutorial_1nff_offer",,
+,12,89,1,0,0,0,,0,0,0,0,0,0,"Price #2.1.1",7,"tx_irretutorial_1nff_offer",,
diff --git a/typo3/sysext/install/Tests/Functional/Updates/RowUpdater/DataSet/LiveDefaultPages.csv b/typo3/sysext/install/Tests/Functional/Updates/RowUpdater/DataSet/LiveDefaultPages.csv
new file mode 100644 (file)
index 0000000..b40e9e3
--- /dev/null
@@ -0,0 +1,6 @@
+"pages"
+,"uid","pid","sorting","deleted","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title","tx_irretutorial_hotels"
+,1,0,256,0,0,0,0,0,0,0,"FunctionalTest",0
+,88,1,256,0,0,0,0,0,0,0,"DataHandlerTest",0
+,89,88,256,0,0,0,0,0,0,0,"Relations",1
+,90,88,512,0,0,0,0,0,0,0,"Target",0
diff --git a/typo3/sysext/install/Tests/Functional/Updates/RowUpdater/DataSet/recordsCanBeUpdated.csv b/typo3/sysext/install/Tests/Functional/Updates/RowUpdater/DataSet/recordsCanBeUpdated.csv
new file mode 100644 (file)
index 0000000..169b198
--- /dev/null
@@ -0,0 +1,40 @@
+sys_language
+,uid,pid,hidden,title,flag
+,1,0,0,Dansk,dk
+,2,0,0,Deutsch,de
+tt_content
+,uid,pid,sorting,deleted,sys_language_uid,l18n_parent,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,header,image,tx_irretutorial_1nff_hotels,l10n_state
+,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1",0,2,"\NULL"
+,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2",0,0,"\NULL"
+,299,89,1024,0,1,297,0,0,0,0,0,0,"Regular Element #1",0,2,"{""header"":""parent"",""tx_irretutorial_1nff_hotels"":""parent""}"
+,300,89,2048,0,1,298,0,0,0,0,0,0,"Regular Element #2++",0,1,"{""header"":""custom"",""tx_irretutorial_1nff_hotels"":""custom""}"
+tx_irretutorial_1nff_hotel
+,uid,pid,sorting,deleted,sys_language_uid,l18n_parent,l18n_diffsource,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,title,parentid,parenttable,parentidentifier,offers,l10n_state
+,2,89,512,0,0,0,,0,0,0,0,0,0,"Hotel #0",89,pages,,0,"\NULL"
+,3,89,768,0,0,0,,0,0,0,0,0,0,"Hotel #1",297,tt_content,,2,"\NULL"
+,4,89,1536,0,0,0,,0,0,0,0,0,0,"Hotel #2",297,tt_content,,1,"\NULL"
+,5,89,1280,0,1,0,,0,0,0,0,0,0,"Hotel #3",300,tt_content,,0,"\NULL"
+,6,89,1,0,1,3,,3,0,0,0,0,0,"[Translate to Dansk:] Hotel #1",299,tt_content,,2,"\NULL"
+,7,89,2,0,1,4,,4,0,0,0,0,0,"[Translate to Dansk:] Hotel #2",299,tt_content,,1,"\NULL"
+tx_irretutorial_1nff_offer
+,uid,pid,sorting,deleted,sys_language_uid,l18n_parent,l18n_diffsource,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,title,parentid,parenttable,parentidentifier,prices,l10n_state
+,5,89,1280,0,0,0,,0,0,0,0,0,0,"Offer #1.1",3,tx_irretutorial_1nff_hotel,,3,"\NULL"
+,6,89,1792,0,0,0,,0,0,0,0,0,0,"Offer #1.2",3,tx_irretutorial_1nff_hotel,,2,"\NULL"
+,7,89,1536,0,0,0,,0,0,0,0,0,0,"Offer #2.1",4,tx_irretutorial_1nff_hotel,,1,"\NULL"
+,8,89,512,0,1,5,,5,0,0,0,0,0,"[Translate to Dansk:] Offer #1.1",6,tx_irretutorial_1nff_hotel,,3,"\NULL"
+,9,89,1024,0,1,6,,6,0,0,0,0,0,"[Translate to Dansk:] Offer #1.2",6,tx_irretutorial_1nff_hotel,,2,"\NULL"
+,10,89,1,0,1,7,,7,0,0,0,0,0,"[Translate to Dansk:] Offer #2.1",7,tx_irretutorial_1nff_hotel,,1,"\NULL"
+tx_irretutorial_1nff_price
+,uid,pid,sorting,deleted,sys_language_uid,l18n_parent,l18n_diffsource,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,title,parentid,parenttable,parentidentifier,l10n_state
+,7,89,2048,0,0,0,,0,0,0,0,0,0,"Price #1.1.1",5,tx_irretutorial_1nff_offer,,"\NULL"
+,8,89,2816,0,0,0,,0,0,0,0,0,0,"Price #1.1.2",5,tx_irretutorial_1nff_offer,,"\NULL"
+,9,89,3328,0,0,0,,0,0,0,0,0,0,"Price #1.1.3",5,tx_irretutorial_1nff_offer,,"\NULL"
+,10,89,2304,0,0,0,,0,0,0,0,0,0,"Price #1.2.1",6,tx_irretutorial_1nff_offer,,"\NULL"
+,11,89,3072,0,0,0,,0,0,0,0,0,0,"Price #1.2.2",6,tx_irretutorial_1nff_offer,,"\NULL"
+,12,89,2560,0,0,0,,0,0,0,0,0,0,"Price #2.1.1",7,tx_irretutorial_1nff_offer,,"\NULL"
+,13,89,1280,0,1,7,,7,0,0,0,0,0,"[Translate to Dansk:] Price #1.1.1",8,tx_irretutorial_1nff_offer,,"\NULL"
+,14,89,1536,0,1,8,,8,0,0,0,0,0,"[Translate to Dansk:] Price #1.1.2",8,tx_irretutorial_1nff_offer,,"\NULL"
+,15,89,1792,0,1,9,,9,0,0,0,0,0,"[Translate to Dansk:] Price #1.1.3",8,tx_irretutorial_1nff_offer,,"\NULL"
+,16,89,512,0,1,10,,10,0,0,0,0,0,"[Translate to Dansk:] Price #1.2.1",9,tx_irretutorial_1nff_offer,,"\NULL"
+,17,89,1024,0,1,11,,11,0,0,0,0,0,"[Translate to Dansk:] Price #1.2.2",9,tx_irretutorial_1nff_offer,,"\NULL"
+,18,89,1,0,1,12,,12,0,0,0,0,0,"[Translate to Dansk:] Price #2.1.1",10,tx_irretutorial_1nff_offer,,"\NULL"
diff --git a/typo3/sysext/install/Tests/Functional/Updates/RowUpdater/L10nModeUpdaterTest.php b/typo3/sysext/install/Tests/Functional/Updates/RowUpdater/L10nModeUpdaterTest.php
new file mode 100644 (file)
index 0000000..e8a2d7d
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+namespace TYPO3\CMS\Install\Tests\Functional\Updates\RowUpdater;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Updates\RowUpdater\L10nModeUpdater;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+/**
+ * Test Class for L10nModeUpdater
+ */
+class L10nModeUpdaterTest extends FunctionalTestCase
+{
+    /**
+     * @var string
+     */
+    protected $scenarioDataSetDirectory = 'typo3/sysext/install/Tests/Functional/Updates/RowUpdater/DataSet/';
+
+    /**
+     * @var string
+     */
+    protected $assertionDataSetDirectory = 'typo3/sysext/install/Tests/Functional/Updates/RowUpdater/DataSet/';
+
+    /**
+     * @var string[]
+     */
+    protected $coreExtensionsToLoad = [
+        'workspaces',
+    ];
+
+    /**
+     * @var string[]
+     */
+    protected $testExtensionsToLoad = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/irre_tutorial',
+    ];
+
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->importScenarioDataSet('LiveDefaultPages');
+        $this->importScenarioDataSet('LiveDefaultElements');
+
+        $GLOBALS['TCA']['tt_content']['columns']['image']['l10n_mode'] = 'exclude';
+        $GLOBALS['TCA']['tt_content']['columns']['header']['config']['behaviour']['allowLanguageSynchronization'] = true;
+        $GLOBALS['TCA']['tt_content']['columns']['tx_irretutorial_1nff_hotels']['config']['behaviour']['allowLanguageSynchronization'] = true;
+    }
+
+    /**
+     * @param string $dataSetName
+     */
+    protected function importScenarioDataSet($dataSetName)
+    {
+        $fileName = rtrim($this->scenarioDataSetDirectory, '/') . '/' . $dataSetName . '.csv';
+        $fileName = GeneralUtility::getFileAbsFileName($fileName);
+        $this->importCSVDataSet($fileName);
+    }
+
+    protected function assertAssertionDataSet($dataSetName)
+    {
+        $fileName = rtrim($this->assertionDataSetDirectory, '/') . '/' . $dataSetName . '.csv';
+        $fileName = GeneralUtility::getFileAbsFileName($fileName);
+        $this->assertCSVDataSet($fileName);
+    }
+
+    /**
+     * @return array
+     */
+    protected function getTableNames(): array
+    {
+        return array_keys($GLOBALS['TCA']);
+    }
+
+    /**
+     * @test
+     */
+    public function recordsCanBeUpdated()
+    {
+        $updater = new L10nModeUpdater();
+        foreach ($this->getTableNames() as $tableName) {
+            $updater->hasPotentialUpdateForTable($tableName);
+            foreach ($this->getAllRecords($tableName) as $record) {
+                $updater->updateTableRow($tableName, $record);
+            }
+        }
+
+        $this->assertAssertionDataSet('recordsCanBeUpdated');
+    }
+}