[BUGFIX] Add allowLanguageSynchronization chain resolving 52/51952/15
authorOliver Hader <oliver@typo3.org>
Sat, 4 Mar 2017 12:39:15 +0000 (13:39 +0100)
committerOliver Hader <oliver.hader@typo3.org>
Sat, 11 Mar 2017 18:12:39 +0000 (19:12 +0100)
Currently the localization behavior does not consider localization
chains concerning field values to be synchronized over multiple
localization hops that use the relative l10n_state "source".

Imagine the following scenario of content elements:
* { uid:10, sys_language_uid:0, l18n_parent:0, l10n_source:0,
    header:Value, l10n_state:null }
* { uid:11, sys_language_uid:1, l18n_parent:10, l10n_source:10,
    header:Value, l10n_state:{header:parent} }
* { uid:12, sys_language_uid:2, l18n_parent:10, l10n_source:11,
    header:Value, l10n_state:{header:source} }

Now if the record of the default language (uid:10) will be updated and
the header value set to "Modified", only direct dependents would be
synchronized. The automated update of the direct-child localization
record (uid:11) does not trigger another update for the grand-child
localization (uid:12).
To achieve this, the data-map processor has been extended to collect
new modifications to the data-map caused by synchronization processes
- as long as modifications could be determined, another synchronization
round is triggered for the modified items.
This way the localization chain is completely synchronized if required,
depending on the according l10n_state settings.

Change-Id: Ic08460f3ed0071f3dca6c6d1666031895bc3d832
Resolves: #80141
Releases: master
Reviewed-on: https://review.typo3.org/51952
Tested-by: TYPO3com <no-reply@typo3.com>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
13 files changed:
typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php
typo3/sysext/core/Tests/Functional/DataHandling/Group/AbstractActionTestCase.php
typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/ActionTest.php
typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/DataSet/localizeContentChainOfRelationWSynchronizationSource.csv [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/DataSet/localizeContentOfRelationWSynchronization.csv
typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php
typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/ActionTest.php
typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentChainWAllChildrenSelectNLanguageSynchronizationSource.csv [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentWAllChildrenSelectNLanguageSynchronization.csv
typo3/sysext/core/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php
typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/ActionTest.php
typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/localizeContentFromNonDefaultLanguageWSynchronizationDefault.csv
typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/localizeContentFromNonDefaultLanguageWSynchronizationSource.csv [new file with mode: 0644]

index b55057f..c21e2ba 100644 (file)
@@ -49,7 +49,12 @@ class DataMapProcessor
     /**
      * @var array
      */
-    protected $dataMap = [];
+    protected $allDataMap = [];
+
+    /**
+     * @var array
+     */
+    protected $modifiedDataMap = [];
 
     /**
      * @var array
@@ -64,7 +69,12 @@ class DataMapProcessor
     /**
      * @var DataMapItem[]
      */
-    protected $items = [];
+    protected $allItems = [];
+
+    /**
+     * @var DataMapItem[]
+     */
+    protected $nextItems = [];
 
     /**
      * Class generator
@@ -88,7 +98,8 @@ class DataMapProcessor
      */
     public function __construct(array $dataMap, BackendUserAuthentication $backendUser)
     {
-        $this->dataMap = $dataMap;
+        $this->allDataMap = $dataMap;
+        $this->modifiedDataMap = $dataMap;
         $this->backendUser = $backendUser;
     }
 
@@ -100,14 +111,26 @@ class DataMapProcessor
      */
     public function process()
     {
-        foreach ($this->dataMap as $tableName => $idValues) {
-            $this->collectItems($tableName, $idValues);
-        }
-        if (!empty($this->items)) {
-            $this->sanitize();
-            $this->enrich();
+        $iterations = 0;
+
+        while (!empty($this->modifiedDataMap)) {
+            $this->nextItems = [];
+            foreach ($this->modifiedDataMap as $tableName => $idValues) {
+                $this->collectItems($tableName, $idValues);
+            }
+
+            $this->modifiedDataMap = [];
+            if (empty($this->nextItems)) {
+                break;
+            }
+
+            if ($iterations++ === 0) {
+                $this->sanitize($this->allItems);
+            }
+            $this->enrich($this->nextItems);
         }
-        return $this->dataMap;
+
+        return $this->allDataMap;
     }
 
     /**
@@ -143,51 +166,64 @@ class DataMapProcessor
             $translationValues = $this->fetchTranslationValues(
                 $tableName,
                 $fieldNames,
-                $this->filterNumericIds(array_keys($idValues))
+                $this->filterNewItemIds(
+                    $tableName,
+                    $this->filterNumericIds(array_keys($idValues))
+                )
             );
         }
 
         $dependencies = $this->fetchDependencies(
             $forTableName,
-            $this->filterNumericIds(array_keys($idValues))
+            $this->filterNewItemIds(
+                $forTableName,
+                $this->filterNumericIds(array_keys($idValues))
+            )
         );
 
         foreach ($idValues as $id => $values) {
-            $recordValues = $translationValues[$id] ?? [];
-            $item = DataMapItem::build(
-                $tableName,
-                $id,
-                $values,
-                $recordValues,
-                $fieldNames
-            );
+            $item = $this->findItem($tableName, $id);
+            // build item if it has not been created in a previous iteration
+            if ($item === null) {
+                $recordValues = $translationValues[$id] ?? [];
+                $item = DataMapItem::build(
+                    $tableName,
+                    $id,
+                    $values,
+                    $recordValues,
+                    $fieldNames
+                );
 
-            // elements using "all language" cannot be localized
-            if ($item->getLanguage() === -1) {
-                unset($item);
-                continue;
-            }
-            // must be any kind of localization and in connected mode
-            if ($item->getLanguage() > 0 && empty($item->getParent())) {
-                unset($item);
-                continue;
-            }
-            // add dependencies
-            if (!empty($dependencies[$id])) {
-                $item->setDependencies($dependencies[$id]);
+                // elements using "all language" cannot be localized
+                if ($item->getLanguage() === -1) {
+                    unset($item);
+                    continue;
+                }
+                // must be any kind of localization and in connected mode
+                if ($item->getLanguage() > 0 && empty($item->getParent())) {
+                    unset($item);
+                    continue;
+                }
+                // add dependencies
+                if (!empty($dependencies[$id])) {
+                    $item->setDependencies($dependencies[$id]);
+                }
             }
-            $this->items[$tableName . ':' . $id] = $item;
+            // add item to $this->allItems and $this->nextItems
+            $this->addNextItem($item);
         }
     }
 
     /**
      * Sanitizes the submitted data-map and removes fields which are not
      * defined as custom and thus rely on either parent or source values.
+     *
+     * @param DataMapItem[] $items
      */
-    protected function sanitize()
+    protected function sanitize(array $items)
     {
-        foreach (['grandChild', 'directChild'] as $type) {
-            foreach ($this->filterItemsByType($type) as $item) {
+        foreach (['directChild', 'grandChild'] as $type) {
+            foreach ($this->filterItemsByType($type, $items) as $item) {
                 $this->sanitizeTranslationItem($item);
             }
         }
@@ -195,11 +231,13 @@ class DataMapProcessor
 
     /**
      * Handle synchronization of an item list
+     *
+     * @param DataMapItem[] $items
      */
-    protected function enrich()
+    protected function enrich(array $items)
     {
-        foreach (['grandChild', 'directChild'] as $type) {
-            foreach ($this->filterItemsByType($type) as $item) {
+        foreach (['directChild', 'grandChild'] as $type) {
+            foreach ($this->filterItemsByType($type, $items) as $item) {
                 foreach ($item->getApplicableScopes() as $scope) {
                     $fromId = $item->getIdForScope($scope);
                     $fieldNames = $this->getFieldNamesForItemScope($item, $scope, !$item->isNew());
@@ -209,7 +247,7 @@ class DataMapProcessor
                 $this->finishTranslationItem($item);
             }
         }
-        foreach ($this->filterItemsByType('parent') as $item) {
+        foreach ($this->filterItemsByType('parent', $items) as $item) {
             $this->populateTranslationItem($item);
         }
     }
@@ -223,20 +261,23 @@ class DataMapProcessor
      */
     protected function sanitizeTranslationItem(DataMapItem $item)
     {
-        $fieldNames = array_merge(
-            $this->getFieldNamesForItemScope($item, DataMapItem::SCOPE_PARENT, !$item->isNew()),
-            $this->getFieldNamesForItemScope($item, DataMapItem::SCOPE_SOURCE, !$item->isNew())
-        );
+        $fieldNames = [];
+        foreach ($item->getApplicableScopes() as $scope) {
+            $fieldNames = array_merge(
+                $fieldNames,
+                $this->getFieldNamesForItemScope($item, $scope, !$item->isNew())
+            );
+        }
 
         $fieldNameMap = array_combine($fieldNames, $fieldNames);
         // separate fields, that are submitted in data-map, but not defined as custom
         $this->sanitizationMap[$item->getTableName()][$item->getId()] = array_intersect_key(
-            $this->dataMap[$item->getTableName()][$item->getId()],
+            $this->allDataMap[$item->getTableName()][$item->getId()],
             $fieldNameMap
         );
         // remove fields, that are submitted in data-map, but not defined as custom
-        $this->dataMap[$item->getTableName()][$item->getId()] = array_diff_key(
-            $this->dataMap[$item->getTableName()][$item->getId()],
+        $this->allDataMap[$item->getTableName()][$item->getId()] = array_diff_key(
+            $this->allDataMap[$item->getTableName()][$item->getId()],
             $fieldNameMap
         );
     }
@@ -327,7 +368,7 @@ class DataMapProcessor
             return;
         }
 
-        $this->dataMap[$item->getTableName()][$item->getId()]['l10n_state'] = $item->getState()->export();
+        $this->allDataMap[$item->getTableName()][$item->getId()]['l10n_state'] = $item->getState()->export();
     }
 
     /**
@@ -341,16 +382,20 @@ class DataMapProcessor
     protected function synchronizeFieldValues(DataMapItem $item, string $fieldName, array $fromRecord, array $forRecord)
     {
         // skip if this field has been processed already, assumed that proper sanitation happened
-        if (!empty($this->dataMap[$item->getTableName()][$item->getId()][$fieldName])) {
+        if (isset($this->allDataMap[$item->getTableName()][$item->getId()][$fieldName])) {
             return;
         }
 
         $fromId = $fromRecord['uid'];
-        $fromValue = $this->dataMap[$item->getFromTableName()][$fromId][$fieldName] ?? $fromRecord[$fieldName];
+        $fromValue = $this->allDataMap[$item->getFromTableName()][$fromId][$fieldName] ?? $fromRecord[$fieldName];
 
         // plain values
         if (!$this->isRelationField($item->getFromTableName(), $fieldName)) {
-            $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = $fromValue;
+            $this->modifyDataMap(
+                $item->getTableName(),
+                $item->getId(),
+                [$fieldName => $fromValue]
+            );
         // direct relational values
         } elseif (!$this->isInlineRelationField($item->getFromTableName(), $fieldName)) {
             $this->synchronizeDirectRelations($item, $fieldName, $fromRecord);
@@ -370,17 +415,21 @@ class DataMapProcessor
     protected function synchronizeDirectRelations(DataMapItem $item, string $fieldName, array $fromRecord)
     {
         $fromId = $fromRecord['uid'];
-        $fromValue = $this->dataMap[$item->getFromTableName()][$fromId][$fieldName] ?? $fromRecord[$fieldName];
+        $fromValue = $this->allDataMap[$item->getFromTableName()][$fromId][$fieldName] ?? $fromRecord[$fieldName];
         $configuration = $GLOBALS['TCA'][$item->getFromTableName()]['columns'][$fieldName];
 
         // non-MM relations are stored as comma separated values, just use them
         // if values are available in data-map already, just use them as well
         if (
             empty($configuration['config']['MM'])
-            || isset($this->dataMap[$item->getFromTableName()][$fromId][$fieldName])
+            || isset($this->allDataMap[$item->getFromTableName()][$fromId][$fieldName])
             || ($configuration['config']['special'] ?? null) === 'languages'
         ) {
-            $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = $fromValue;
+            $this->modifyDataMap(
+                $item->getTableName(),
+                $item->getId(),
+                [$fieldName => $fromValue]
+            );
             return;
         }
 
@@ -407,14 +456,15 @@ class DataMapProcessor
 
         // provide list of relations, optionally prepended with table name
         // e.g. "13,19,23" or "tt_content_27,tx_extension_items_28"
-        $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = implode(
-            ',',
-            $relationHandler->getValueArray()
+        $this->modifyDataMap(
+            $item->getTableName(),
+            $item->getId(),
+            [$fieldName => implode(',', $relationHandler->getValueArray())]
         );
     }
 
     /**
-     * Handle synchonization of inline relations
+     * Handle synchronization of inline relations
      *
      * @param DataMapItem $item
      * @param string $fieldName
@@ -439,10 +489,10 @@ class DataMapProcessor
 
         // determine suggested elements of either translation parent or source record
         // from data-map, in case the accordant language parent/source record was modified
-        if (isset($this->dataMap[$item->getFromTableName()][$fromId][$fieldName])) {
+        if (isset($this->allDataMap[$item->getFromTableName()][$fromId][$fieldName])) {
             $suggestedAncestorIds = GeneralUtility::trimExplode(
                 ',',
-                $this->dataMap[$item->getFromTableName()][$fromId][$fieldName],
+                $this->allDataMap[$item->getFromTableName()][$fromId][$fieldName],
                 true
             );
         // determine suggested elements of either translation parent or source record from storage
@@ -471,7 +521,7 @@ class DataMapProcessor
         $persistedIds = $this->mapRelationItemId($relationHandler->itemArray);
         // The dependent ID map points from language parent/source record to
         // localization, thus keys: parents/sources & values: localizations
-        $dependentIdMap = $this->fetchDependentIdMap($foreignTableName, $suggestedAncestorIds);
+        $dependentIdMap = $this->fetchDependentIdMap($foreignTableName, $suggestedAncestorIds, $item->getLanguage());
         // filter incomplete structures - this is a drawback of DataHandler's remap stack, since
         // just created IRRE translations still belong to the language parent - filter them out
         $suggestedAncestorIds = array_diff($suggestedAncestorIds, array_values($dependentIdMap));
@@ -494,11 +544,12 @@ class DataMapProcessor
                 $desiredIdMap[$ancestorId] = $translationId;
             }
         }
-        // nothing to synchronize, but element order could have been changed
+        // no children to be synchronized, but element order could have been changed
         if (empty($removeAncestorIds) && empty($missingAncestorIds)) {
-            $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = implode(
-                ',',
-                array_values($desiredIdMap)
+            $this->modifyDataMap(
+                $item->getTableName(),
+                $item->getId(),
+                [$fieldName => implode(',', array_values($desiredIdMap))]
             );
             return;
         }
@@ -515,7 +566,11 @@ class DataMapProcessor
             && $sanitizedValue !== null && !$isTranslatable
             && count(GeneralUtility::trimExplode(',', $sanitizedValue)) === count($missingAncestorIds)
         ) {
-            $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = $sanitizedValue;
+            $this->modifyDataMap(
+                $item->getTableName(),
+                $item->getId(),
+                [$fieldName => $sanitizedValue]
+            );
             return;
         }
 
@@ -551,7 +606,8 @@ class DataMapProcessor
                     }
                     throw new \RuntimeException(
                         'Child record was not processed' . $additionalInformation,
-                        1486233164);
+                        1486233164
+                    );
                 }
                 $newLocalizationId = $localDataHandler->copyMappingArray_merged[$foreignTableName][$createAncestorId];
                 $newLocalizationId = $localDataHandler->getAutoVersionId($foreignTableName, $newLocalizationId) ?? $newLocalizationId;
@@ -563,22 +619,80 @@ class DataMapProcessor
             foreach ($populateAncestorIds as $populateAncestorId) {
                 $newLocalizationId = StringUtility::getUniqueId('NEW');
                 $desiredIdMap[$populateAncestorId] = $newLocalizationId;
-                $this->dataMap[$foreignTableName][$newLocalizationId] = $this->duplicateFromDataMap(
+                $this->modifyDataMap(
                     $foreignTableName,
-                    $populateAncestorId,
-                    $item->getLanguage(),
-                    $fieldNames
+                    $newLocalizationId,
+                    $this->duplicateFromDataMap(
+                        $foreignTableName,
+                        $populateAncestorId,
+                        $item->getLanguage(),
+                        $fieldNames
+                    )
                 );
             }
         }
         // update inline parent field references - required to update pointer fields
-        $this->dataMap[$item->getTableName()][$item->getId()][$fieldName] = implode(
-            ',',
-            array_values($desiredIdMap)
+        $this->modifyDataMap(
+            $item->getTableName(),
+            $item->getId(),
+            [$fieldName => implode(',', array_values($desiredIdMap))]
         );
     }
 
     /**
+     * Applies modifications to the data-map, calling this method is essential
+     * to determine new data-map items to be process for synchronizing chained
+     * record localizations.
+     *
+     * @param string $tableName
+     * @param string|int $id
+     * @param array $values
+     * @throws \RuntimeException
+     */
+    protected function modifyDataMap(string $tableName, $id, array $values)
+    {
+        // avoid superfluous iterations by data-map changes with values
+        // that actually have not been changed and were available already
+        $sameValues = array_intersect_assoc(
+            $this->allDataMap[$tableName][$id] ?? [],
+            $values
+        );
+        if (!empty($sameValues)) {
+            $fieldNames = implode(', ', array_keys($sameValues));
+            throw new \RuntimeException(
+                sprintf(
+                    'Issued data-map change for table %s with same values '
+                    . 'for these fields names %s',
+                    $tableName,
+                    $fieldNames
+                ),
+                1488634845
+            );
+        }
+
+        $this->modifiedDataMap[$tableName][$id] = array_merge(
+            $this->modifiedDataMap[$tableName][$id] ?? [],
+            $values
+        );
+        $this->allDataMap[$tableName][$id] = array_merge(
+            $this->allDataMap[$tableName][$id] ?? [],
+            $values
+        );
+    }
+
+    /**
+     * @param DataMapItem $item
+     */
+    protected function addNextItem(DataMapItem $item)
+    {
+        $identifier = $item->getTableName() . ':' . $item->getId();
+        if (!isset($this->allItems[$identifier])) {
+            $this->allItems[$identifier] = $item;
+        }
+        $this->nextItems[$identifier] = $item;
+    }
+
+    /**
      * Fetches translation related field values for the items submitted in
      * the data-map. That's why further adjustment for the tables pages vs.
      * pages_language_overlay is not required.
@@ -617,6 +731,16 @@ class DataMapProcessor
     /**
      * Fetches translation dependencies for a given parent/source record ids.
      *
+     * Existing records in database:
+     * + [uid:5, l10n_parent=0, l10n_source=0, sys_language_uid=0]
+     * + [uid:6, l10n_parent=5, l10n_source=5, sys_language_uid=1]
+     * + [uid:7, l10n_parent=5, l10n_source=6, sys_language_uid=2]
+     *
+     * Input $ids and their results:
+     * + [5]   -> [DataMapItem(6), DataMapItem(7)] # since 5 is parent/source
+     * + [6]   -> [DataMapItem(7)]                 # since 6 is source
+     * + [7]   -> []                               # since there's nothing
+     *
      * @param string $tableName
      * @param array $ids
      * @return DataMapItem[][]
@@ -669,16 +793,32 @@ class DataMapProcessor
      * their parent or source field for translatable tables or their origin
      * field for non-translatable tables and creates an id mapping.
      *
+     * This method expands the search criteria by expanding to ancestors.
+     *
+     * Existing records in database:
+     * + [uid:5, l10n_parent=0, l10n_source=0, sys_language_uid=0]
+     * + [uid:6, l10n_parent=5, l10n_source=5, sys_language_uid=1]
+     * + [uid:7, l10n_parent=5, l10n_source=6, sys_language_uid=2]
+     *
+     * Input $ids and $desiredLanguage and their results:
+     * + $ids=[5], $lang=1 -> [5 => 6] # since 5 is source of 6
+     * + $ids=[5], $lang=2 -> []       # since 5 is parent of 7, but different language
+     * + $ids=[6], $lang=1 -> []       # since there's nothing
+     * + $ids=[6], $lang=2 -> [6 => 7] # since 6 has source 5, which is ancestor of 7
+     * + $ids=[7], $lang=* -> []       # since there's nothing
+     *
      * @param string $tableName
      * @param array $ids
+     * @param int $desiredLanguage
      * @return array
      */
-    protected function fetchDependentIdMap(string $tableName, array $ids)
+    protected function fetchDependentIdMap(string $tableName, array $ids, int $desiredLanguage)
     {
         if ($tableName === 'pages') {
             $tableName = 'pages_language_overlay';
         }
 
+        $ids = $this->filterNumericIds($ids, true);
         $isTranslatable = BackendUtility::isTableLocalizable($tableName);
         $originFieldName = ($GLOBALS['TCA'][$tableName]['ctrl']['origUid'] ?? null);
 
@@ -703,23 +843,42 @@ class DataMapProcessor
             ];
         }
 
-        $dependentElements = $this->fetchDependentElements($tableName, $ids, $fieldNames);
+        $fetchIds = $ids;
+        if ($isTranslatable) {
+            // expand search criteria via parent and source elements
+            $translationValues = $this->fetchTranslationValues($tableName, $fieldNames, $ids);
+            $ancestorIdMap = $this->buildElementAncestorIdMap($fieldNames, $translationValues);
+            $fetchIds = array_unique(array_merge($ids, array_keys($ancestorIdMap)));
+        }
+
+        $dependentElements = $this->fetchDependentElements($tableName, $fetchIds, $fieldNames);
 
         $dependentIdMap = [];
         foreach ($dependentElements as $dependentElement) {
             $dependentId = $dependentElement['uid'];
             // implicit: use origin pointer if table cannot be translated
             if (!$isTranslatable) {
-                $dependentIdMap[$dependentElement[$fieldNames['origin']]] = $dependentId;
-            // implicit: having source value different to parent value, use source pointer
-            } elseif (
-                !empty($fieldNames['source'])
-                && $dependentElement[$fieldNames['source']] !== $dependentElement[$fieldNames['parent']]
-            ) {
-                $dependentIdMap[$dependentElement[$fieldNames['source']]] = $dependentId;
-            // implicit: otherwise, use parent pointer
+                $ancestorId = (int)$dependentElement[$fieldNames['origin']];
+            // only consider element if it reflects the desired language
+            } elseif ((int)$dependentElement[$fieldNames['language']] === $desiredLanguage) {
+                $ancestorId = $this->resolveAncestorId($fieldNames, $dependentElement);
+            // otherwise skip the element completely
             } else {
-                $dependentIdMap[$dependentElement[$fieldNames['parent']]] = $dependentId;
+                continue;
+            }
+            // only keep ancestors that were initially requested before expanding
+            if (in_array($ancestorId, $ids)) {
+                $dependentIdMap[$ancestorId] = $dependentId;
+            // resolve from previously expanded search criteria
+            } elseif (!empty($ancestorIdMap[$ancestorId])) {
+                $possibleChainedIds = array_intersect(
+                    $ids,
+                    $ancestorIdMap[$ancestorId]
+                );
+                if (!empty($possibleChainedIds)) {
+                    $ancestorId = $possibleChainedIds[0];
+                    $dependentIdMap[$ancestorId] = $dependentId;
+                }
             }
         }
         return $dependentIdMap;
@@ -812,12 +971,13 @@ class DataMapProcessor
      * Return array of data map items that are of given type
      *
      * @param string $type
+     * @param DataMapItem[] $items
      * @return DataMapItem[]
      */
-    protected function filterItemsByType(string $type)
+    protected function filterItemsByType(string $type, array $items)
     {
         return array_filter(
-            $this->items,
+            $items,
             function (DataMapItem $item) use ($type) {
                 return $item->getType() === $type;
             }
@@ -825,9 +985,9 @@ class DataMapProcessor
     }
 
     /**
-     * Return only id's that are integer - so no NEW...
+     * Return only ids that are integer - so no "NEW..." values
      *
-     * @param array $ids
+     * @param string[]|int[] $ids
      * @param bool $numeric
      * @return array
      */
@@ -842,6 +1002,23 @@ class DataMapProcessor
     }
 
     /**
+     * Return only ids that don't have an item equivalent in $this->allItems.
+     *
+     * @param string $tableName
+     * @param int[] $ids
+     * @return array
+     */
+    protected function filterNewItemIds(string $tableName, array $ids)
+    {
+        return array_filter(
+            $ids,
+            function ($id) use ($tableName) {
+                return $this->findItem($tableName, $id) === null;
+            }
+        );
+    }
+
+    /**
      * Flatten array
      *
      * @param array $relationItems
@@ -851,13 +1028,55 @@ class DataMapProcessor
     {
         return array_map(
             function (array $relationItem) {
-                return (string)$relationItem['id'];
+                return (int)$relationItem['id'];
             },
             $relationItems
         );
     }
 
     /**
+     * @param array $fieldNames
+     * @param array $element
+     * @return int|null
+     */
+    protected function resolveAncestorId(array $fieldNames, array $element)
+    {
+        // implicit: having source value different to parent value, use source pointer
+        if (
+            !empty($fieldNames['source'])
+            && $element[$fieldNames['source']] !== $element[$fieldNames['parent']]
+        ) {
+            return (int)$fieldNames['source'];
+        // implicit: use parent pointer if defined
+        } elseif (!empty($fieldNames['parent'])) {
+            return (int)$element[$fieldNames['parent']];
+        }
+        return null;
+    }
+
+    /**
+     * Builds a map from ancestor ids to accordant localization dependents.
+     *
+     * The result of e.g. [5 => [6, 7]] refers to ids 6 and 7 being dependents
+     * (either used in parent or source field) of the ancestor with id 5.
+     *
+     * @param array $fieldNames
+     * @param array $elements
+     * @return array
+     */
+    protected function buildElementAncestorIdMap(array $fieldNames, array $elements)
+    {
+        $ancestorIdMap = [];
+        foreach ($elements as $element) {
+            $ancestorId = $this->resolveAncestorId($fieldNames, $element);
+            if ($ancestorId !== null) {
+                $ancestorIdMap[$ancestorId][] = (int)$element['uid'];
+            }
+        }
+        return $ancestorIdMap;
+    }
+
+    /**
      * See if an items is in item list and return it
      *
      * @param string $tableName
@@ -866,7 +1085,7 @@ class DataMapProcessor
      */
     protected function findItem(string $tableName, $id)
     {
-        return $this->items[$tableName . ':' . $id] ?? null;
+        return $this->allItems[$tableName . ':' . $id] ?? null;
     }
 
     /**
@@ -874,14 +1093,14 @@ class DataMapProcessor
      * if applicable for the accordant field name.
      *
      * @param string $tableName
-     * @param string|int $id
+     * @param string|int $fromId
      * @param int $language
      * @param array $fieldNames
      * @return array
      */
-    protected function duplicateFromDataMap(string $tableName, $id, int $language, array $fieldNames)
+    protected function duplicateFromDataMap(string $tableName, $fromId, int $language, array $fieldNames)
     {
-        $data = $this->dataMap[$tableName][$id];
+        $data = $this->allDataMap[$tableName][$fromId];
         $isTranslatable = (!empty($fieldNames['language']) && !empty($fieldNames['parent']));
 
         if (empty($language) || !$isTranslatable) {
@@ -889,11 +1108,13 @@ class DataMapProcessor
         }
 
         $data[$fieldNames['language']] = $language;
-        // @todo Only $id used in TCA type 'select' is resolved in DataHandler's remapStack
-        $data[$fieldNames['parent']] = $id;
+        if (empty($data[$fieldNames['parent']])) {
+            // @todo Only $id used in TCA type 'select' is resolved in DataHandler's remapStack
+            $data[$fieldNames['parent']] = $fromId;
+        }
         if (!empty($fieldNames['source'])) {
             // @todo Not sure, whether $id is resolved in DataHandler's remapStack
-            $data[$fieldNames['source']] = $id;
+            $data[$fieldNames['source']] = $fromId;
         }
 
         $prefixFieldNames = array_intersect(
@@ -906,7 +1127,7 @@ class DataMapProcessor
 
         $languageService = $this->getLanguageService();
         $languageRecord = BackendUtility::getRecord('sys_language', $language, 'title');
-        list($pageId) = BackendUtility::getTSCpid($tableName, $id, $data['pid'] ?? null);
+        list($pageId) = BackendUtility::getTSCpid($tableName, $fromId, $data['pid'] ?? null);
 
         $TSconfig =  $this->backendUser->getTSConfig(
             'TCEMAIN',
index b091ed9..1dc2b77 100644 (file)
@@ -24,6 +24,7 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
     const VALUE_ContentIdFirst = 297;
     const VALUE_ContentIdLast = 298;
     const VALUE_LanguageId = 1;
+    const VALUE_LanguageIdSecond = 2;
     const VALUE_ElementIdFirst = 1;
     const VALUE_ElementIdSecond = 2;
     const VALUE_ElementIdThird = 3;
@@ -219,7 +220,7 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
 
     public function localizeContentOfRelationWithLanguageSynchronization()
     {
-        $GLOBALS['TCA']['tt_content']['columns']['tx_testdatahandler_group']['config']['behaviour']['allowLanguageSynchronization'] = true;
+        $GLOBALS['TCA']['tt_content']['columns'][self::FIELD_ContentElement]['config']['behaviour']['allowLanguageSynchronization'] = true;
         $newTableIds = $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdLast, self::VALUE_LanguageId);
         $this->recordIds['localizedContentId'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast];
         $this->actionService->modifyReferences(
@@ -227,6 +228,23 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
         );
     }
 
+    public function localizeContentChainOfRelationWithLanguageSynchronizationSource()
+    {
+        $GLOBALS['TCA']['tt_content']['columns'][self::FIELD_ContentElement]['config']['behaviour']['allowLanguageSynchronization'] = true;
+        $newTableIds = $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdLast, self::VALUE_LanguageId);
+        $this->recordIds['localizedContentIdFirst'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast];
+        $newTableIds = $this->actionService->localizeRecord(self::TABLE_Content, $this->recordIds['localizedContentIdFirst'], self::VALUE_LanguageIdSecond);
+        $this->recordIds['localizedContentIdSecond'] = $newTableIds[self::TABLE_Content][$this->recordIds['localizedContentIdFirst']];
+        $this->actionService->modifyRecord(
+            self::TABLE_Content,
+            $this->recordIds['localizedContentIdSecond'],
+            ['l10n_state' => [self::FIELD_ContentElement => 'source']]
+        );
+        $this->actionService->modifyReferences(
+            self::TABLE_Content, self::VALUE_ContentIdLast, self::FIELD_ContentElement, [self::VALUE_ElementIdFirst, self::VALUE_ElementIdSecond]
+        );
+    }
+
     /**
      * @test
      * @see DataSet/localizeElementOfRelation.csv
index 98d197a..12bfc7f 100644 (file)
@@ -303,6 +303,21 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\Group\Abs
 
     /**
      * @test
+     * @see DataSet/localizeContentChainOfRelationWSynchronizationSource.csv
+     */
+    public function localizeContentChainOfRelationWithLanguageSynchronizationSource()
+    {
+        parent::localizeContentChainOfRelationWithLanguageSynchronizationSource();
+        $this->assertAssertionDataSet('localizeContentChainOfRelationWSynchronizationSource');
+
+        $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->getResponseSections();
+        $this->assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
+            ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField(self::FIELD_ContentElement)
+            ->setTable(self::TABLE_Element)->setField('title')->setValues('Element #1', 'Element #2'));
+    }
+
+    /**
+     * @test
      * @see DataSet/localizeElementOfRelation.csv
      */
     public function localizeElementOfRelation()
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/DataSet/localizeContentChainOfRelationWSynchronizationSource.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Group/Modify/DataSet/localizeContentChainOfRelationWSynchronizationSource.csv
new file mode 100644 (file)
index 0000000..cdb795d
--- /dev/null
@@ -0,0 +1,17 @@
+pages
+,uid,pid,sorting,deleted,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,title
+,1,0,256,0,0,0,0,0,0,0,FunctionalTest
+,88,1,256,0,0,0,0,0,0,0,DataHandlerTest
+,89,88,256,0,0,0,0,0,0,0,Relations
+,90,88,512,0,0,0,0,0,0,0,Target
+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,tx_testdatahandler_group,l10n_state
+,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1","1,2",
+,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2","1,2",
+,299,89,768,0,1,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2","1,2","{""tx_testdatahandler_group"":""parent""}"
+,300,89,1024,0,2,298,299,0,0,0,0,0,"[Translate to Deutsch:] [Translate to Dansk:] Regular Element #2","1,2","{""tx_testdatahandler_group"":""source""}"
+tx_testdatahandler_element
+,uid,pid,sorting,deleted,sys_language_uid,l10n_parent,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,title
+,1,89,256,0,0,0,0,0,0,0,0,0,"Element #1"
+,2,89,512,0,0,0,0,0,0,0,0,0,"Element #2"
+,3,89,768,0,0,0,0,0,0,0,0,0,"Element #3"
index f52954f..deb11f5 100644 (file)
@@ -1,16 +1,16 @@
-"pages",,,,,,,,,,,,,,
-,"uid","pid","sorting","deleted","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title",,,
-,1,0,256,0,0,0,0,0,0,0,"FunctionalTest",,,
-,88,1,256,0,0,0,0,0,0,0,"DataHandlerTest",,,
-,89,88,256,0,0,0,0,0,0,0,"Relations",,,
-,90,88,512,0,0,0,0,0,0,0,"Target",,,
-"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","tx_testdatahandler_group"
-,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1","1,2"
-,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2","1,2"
-,299,89,768,0,1,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2","1,2"
-"tx_testdatahandler_element",,,,,,,,,,,,,,
-,"uid","pid","sorting","deleted","sys_language_uid","l10n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","t3ver_move_id","title",
-,1,89,256,0,0,0,0,0,0,0,0,0,"Element #1",
-,2,89,512,0,0,0,0,0,0,0,0,0,"Element #2",
-,3,89,768,0,0,0,0,0,0,0,0,0,"Element #3",
+pages
+,uid,pid,sorting,deleted,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,title
+,1,0,256,0,0,0,0,0,0,0,FunctionalTest
+,88,1,256,0,0,0,0,0,0,0,DataHandlerTest
+,89,88,256,0,0,0,0,0,0,0,Relations
+,90,88,512,0,0,0,0,0,0,0,Target
+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,tx_testdatahandler_group,l10n_state
+,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1","1,2",\NULL
+,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2","1,2",\NULL
+,299,89,768,0,1,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2","1,2","{""tx_testdatahandler_group"":""parent""}"
+tx_testdatahandler_element
+,uid,pid,sorting,deleted,sys_language_uid,l10n_parent,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,title
+,1,89,256,0,0,0,0,0,0,0,0,0,"Element #1"
+,2,89,512,0,0,0,0,0,0,0,0,0,"Element #2"
+,3,89,768,0,0,0,0,0,0,0,0,0,"Element #3"
index 69c279e..6cc6dbe 100644 (file)
@@ -28,6 +28,7 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
     const VALUE_HotelIdSecond = 4;
     const VALUE_HotelIdThird = 5;
     const VALUE_LanguageId = 1;
+    const VALUE_LanguageIdSecond = 2;
 
     const TABLE_Page = 'pages';
     const TABLE_PageOverlay = 'pages_language_overlay';
@@ -224,6 +225,33 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
     }
 
     /**
+     * @see DataSet/localizeParentContentWAllChildrenSelect.csv
+     */
+    public function localizeParentContentChainWithAllChildrenInSelectModeAndLanguageSynchronizationSource()
+    {
+        $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['localizationMode'] = 'select';
+        $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['allowLanguageSynchronization'] = true;
+        $GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['localizeChildrenAtParentLocalization'] = true;
+        $GLOBALS['TCA'][self::TABLE_Hotel]['columns'][self::FIELD_HotelOffer]['config']['behaviour']['localizeChildrenAtParentLocalization'] = true;
+        $newTableIds = $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdLast, self::VALUE_LanguageId);
+        $this->recordIds['localizedContentIdFirst'] = $newTableIds[self::TABLE_Content][self::VALUE_ContentIdLast];
+        $newTableIds = $this->actionService->localizeRecord(self::TABLE_Content, $this->recordIds['localizedContentIdFirst'], self::VALUE_LanguageIdSecond);
+        $this->recordIds['localizedContentIdSecond'] = $newTableIds[self::TABLE_Content][$this->recordIds['localizedContentIdFirst']];
+        $this->actionService->modifyRecord(
+            self::TABLE_Content,
+            $this->recordIds['localizedContentIdSecond'],
+            ['l10n_state' => [self::FIELD_ContentHotel => 'source']]
+        );
+        $this->actionService->modifyRecords(
+            self::VALUE_PageId,
+            [
+                self::TABLE_Content => ['uid' => self::VALUE_ContentIdLast, self::FIELD_ContentHotel => '5,__nextUid'],
+                self::TABLE_Hotel => ['uid' => '__NEW', 'title' => 'Hotel #2'],
+            ]
+        );
+    }
+
+    /**
      * @see DataSet/changeParentContentRecordSorting.csv
      */
     public function changeParentContentSorting()
index 71f47a7..df12b51 100644 (file)
@@ -245,6 +245,21 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\IRRE\Fore
 
     /**
      * @test
+     * @see DataSet/localizeParentContentChainWAllChildrenSelectNLanguageSynchronizationSource.csv
+     */
+    public function localizeParentContentChainWithAllChildrenInSelectModeAndLanguageSynchronizationSource()
+    {
+        parent::localizeParentContentChainWithAllChildrenInSelectModeAndLanguageSynchronizationSource();
+        $this->assertAssertionDataSet('localizeParentContentChainWAllChildrenSelectNLanguageSynchronizationSource');
+
+        $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageIdSecond)->getResponseSections('Default', 'Extbase:list()');
+        $this->assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
+            ->setRecordIdentifier(self::TABLE_Content . ':' . self::VALUE_ContentIdLast)->setRecordField(self::FIELD_ContentHotel)
+            ->setTable(self::TABLE_Hotel)->setField('title')->setValues('[Translate to Deutsch:] [Translate to Dansk:] Hotel #1', '[Translate to Deutsch:] [Translate to Dansk:] Hotel #2'));
+    }
+
+    /**
+     * @test
      * @see DataSet/changeParentContentRecordSorting.csv
      */
     public function changeParentContentSorting()
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentChainWAllChildrenSelectNLanguageSynchronizationSource.csv b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeParentContentChainWAllChildrenSelectNLanguageSynchronizationSource.csv
new file mode 100644 (file)
index 0000000..e6a1d51
--- /dev/null
@@ -0,0 +1,36 @@
+tt_content
+,uid,pid,sorting,deleted,sys_language_uid,l18n_parent,l10n_source,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,header,tx_irretutorial_1nff_hotels,l10n_state
+,297,89,256,0,0,0,0,0,0,0,0,0,0,"Regular Element #1",2,
+,298,89,512,0,0,0,0,0,0,0,0,0,0,"Regular Element #2",2,
+,299,89,768,0,1,298,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2",2,"{""tx_irretutorial_1nff_hotels"":""parent""}"
+,300,89,1024,0,2,298,299,299,0,0,0,0,0,"[Translate to Deutsch:] [Translate to Dansk:] Regular Element #2",2,"{""tx_irretutorial_1nff_hotels"":""source""}"
+tx_irretutorial_1nff_hotel
+,uid,pid,sorting,deleted,sys_language_uid,l18n_parent,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,title,parentid,parenttable,parentidentifier,offers
+,2,89,1280,0,0,0,0,0,0,0,0,0,"Hotel #0",89,pages,,0
+,3,89,1536,0,0,0,0,0,0,0,0,0,"Hotel #1",297,tt_content,,2
+,4,89,2048,0,0,0,0,0,0,0,0,0,"Hotel #2",297,tt_content,,1
+,5,89,1,0,0,0,0,0,0,0,0,0,"Hotel #1",298,tt_content,,1
+,6,89,1,0,1,5,5,0,0,0,0,0,"[Translate to Dansk:] Hotel #1",299,tt_content,,1
+,7,89,1,0,2,5,6,0,0,0,0,0,"[Translate to Deutsch:] [Translate to Dansk:] Hotel #1",300,tt_content,,1
+,8,89,2,0,0,0,0,0,0,0,0,0,"Hotel #2",298,tt_content,,0
+,9,89,2,0,1,8,0,0,0,0,0,0,"[Translate to Dansk:] Hotel #2",299,tt_content,,0
+,10,89,2,0,2,8,0,0,0,0,0,0,"[Translate to Deutsch:] [Translate to Dansk:] Hotel #2",300,tt_content,,0
+tx_irretutorial_1nff_offer
+,uid,pid,sorting,deleted,sys_language_uid,l18n_parent,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,title,parentid,parenttable,parentidentifier,prices
+,5,89,512,0,0,0,0,0,0,0,0,0,"Offer #1.1",3,tx_irretutorial_1nff_hotel,,3
+,6,89,1536,0,0,0,0,0,0,0,0,0,"Offer #1.2",3,tx_irretutorial_1nff_hotel,,2
+,7,89,768,0,0,0,0,0,0,0,0,0,"Offer #2.1",4,tx_irretutorial_1nff_hotel,,1
+,8,89,1024,0,0,0,0,0,0,0,0,0,"Offer #1.1",5,tx_irretutorial_1nff_hotel,,1
+,9,89,1,0,1,8,8,0,0,0,0,0,"[Translate to Dansk:] Offer #1.1",6,tx_irretutorial_1nff_hotel,,1
+,10,89,1,0,2,8,9,0,0,0,0,0,"[Translate to Deutsch:] [Translate to Dansk:] Offer #1.1",7,tx_irretutorial_1nff_hotel,,1
+tx_irretutorial_1nff_price
+,uid,pid,sorting,deleted,sys_language_uid,l18n_parent,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,title,parentid,parenttable,parentidentifier
+,7,89,512,0,0,0,0,0,0,0,0,0,"Price #1.1.1",5,tx_irretutorial_1nff_offer,
+,8,89,1792,0,0,0,0,0,0,0,0,0,"Price #1.1.2",5,tx_irretutorial_1nff_offer,
+,9,89,2304,0,0,0,0,0,0,0,0,0,"Price #1.1.3",5,tx_irretutorial_1nff_offer,
+,10,89,768,0,0,0,0,0,0,0,0,0,"Price #1.2.1",6,tx_irretutorial_1nff_offer,
+,11,89,2048,0,0,0,0,0,0,0,0,0,"Price #1.2.2",6,tx_irretutorial_1nff_offer,
+,12,89,1024,0,0,0,0,0,0,0,0,0,"Price #2.1.1",7,tx_irretutorial_1nff_offer,
+,13,89,1280,0,0,0,0,0,0,0,0,0,"Price #1.1.1",8,tx_irretutorial_1nff_offer,
+,14,89,1,0,1,13,13,0,0,0,0,0,"[Translate to Dansk:] Price #1.1.1",9,tx_irretutorial_1nff_offer,
+,15,89,1,0,2,13,14,0,0,0,0,0,"[Translate to Deutsch:] [Translate to Dansk:] Price #1.1.1",10,tx_irretutorial_1nff_offer,
index 37fe4ef..e872a5f 100644 (file)
@@ -1,8 +1,8 @@
 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,tx_irretutorial_1nff_hotels
-,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1",2
-,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2",2
-,299,89,768,0,1,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2",2
+,uid,pid,sorting,deleted,sys_language_uid,l18n_parent,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,header,tx_irretutorial_1nff_hotels,l10n_state
+,297,89,256,0,0,0,0,0,0,0,0,0,"Regular Element #1",2,\NULL
+,298,89,512,0,0,0,0,0,0,0,0,0,"Regular Element #2",2,\NULL
+,299,89,768,0,1,298,298,0,0,0,0,0,"[Translate to Dansk:] Regular Element #2",2,"{""tx_irretutorial_1nff_hotels"":""parent""}"
 tx_irretutorial_1nff_hotel
 ,uid,pid,sorting,deleted,sys_language_uid,l18n_parent,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,title,parentid,parenttable,parentidentifier,offers
 ,2,89,1024,0,0,0,0,0,0,0,0,0,"Hotel #0",89,pages,,0
index 2bbfb2a..f0d3258 100644 (file)
@@ -172,6 +172,15 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
         $this->actionService->modifyRecord(self::TABLE_Content, self::VALUE_ContentIdThird, ['header' => 'Testing #1']);
     }
 
+    public function localizeContentFromNonDefaultLanguageWithLanguageSynchronizationSource()
+    {
+        $GLOBALS['TCA']['tt_content']['columns']['header']['config']['behaviour']['allowLanguageSynchronization'] = true;
+        $localizedTableIds = $this->actionService->localizeRecord(self::TABLE_Content, self::VALUE_ContentIdThirdLocalized, self::VALUE_LanguageIdSecond);
+        $this->recordIds['localizedContentId'] = $localizedTableIds[self::TABLE_Content][self::VALUE_ContentIdThirdLocalized];
+        $this->actionService->modifyRecord(self::TABLE_Content, $this->recordIds['localizedContentId'], ['l10n_state' => ['header' => 'source']]);
+        $this->actionService->modifyRecord(self::TABLE_Content, self::VALUE_ContentIdThird, ['header' => 'Testing #1']);
+    }
+
     /**
      * @see DataSet/changeContentRecordSorting.csv
      */
index 44d14bd..3e041b4 100644 (file)
@@ -225,6 +225,21 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\Regular\A
 
     /**
      * @test
+     * @see DataSet/localizeContentFromNonDefaultLanguageWSynchronizationSource.csv
+     */
+    public function localizeContentFromNonDefaultLanguageWithLanguageSynchronizationSource()
+    {
+        parent::localizeContentFromNonDefaultLanguageWithLanguageSynchronizationSource();
+
+        $this->assertAssertionDataSet('localizeContentFromNonDefaultLanguageWSynchronizationSource');
+
+        $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageIdSecond)->getResponseSections();
+        $this->assertThat($responseSections, $this->getRequestSectionHasRecordConstraint()
+            ->setTable(self::TABLE_Content)->setField('header')->setValues('[Translate to Deutsch:] [Translate to Dansk:] Regular Element #1', 'Testing #1'));
+    }
+
+    /**
+     * @test
      * @see DataSet/changeContentRecordSorting.csv
      */
     public function changeContentSorting()
index d78c544..41100ba 100644 (file)
@@ -3,7 +3,7 @@ tt_content,,,,,,,,,,,,,,
 ,297,89,256,0,0,0,0,0,0,0,0,0,0,Regular Element #1,
 ,298,89,512,0,0,0,0,0,0,0,0,0,0,Regular Element #2,
 ,299,89,768,0,0,0,0,0,0,0,0,0,0,Testing #1,
-,300,89,1024,0,1,299,299,299,0,0,0,0,0,Testing #1,\NULL
+,300,89,1024,0,1,299,299,299,0,0,0,0,0,Testing #1,"{""header"":""parent""}"
 ,301,89,384,0,1,297,297,297,0,0,0,0,0,[Translate to Dansk:] Regular Element #1,
 ,302,89,448,0,2,297,301,301,0,0,0,0,0,[Translate to Deutsch:] [Translate to Dansk:] Regular Element #1,
 ,303,89,1280,0,2,299,300,300,0,0,0,0,0,Testing #1,"{""header"":""parent""}"
diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/localizeContentFromNonDefaultLanguageWSynchronizationSource.csv b/typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/DataSet/localizeContentFromNonDefaultLanguageWSynchronizationSource.csv
new file mode 100644 (file)
index 0000000..2a7a6e0
--- /dev/null
@@ -0,0 +1,9 @@
+tt_content,,,,,,,,,,,,,,
+,uid,pid,sorting,deleted,sys_language_uid,l18n_parent,l10n_source,t3_origuid,t3ver_wsid,t3ver_state,t3ver_stage,t3ver_oid,t3ver_move_id,header,l10n_state
+,297,89,256,0,0,0,0,0,0,0,0,0,0,Regular Element #1,
+,298,89,512,0,0,0,0,0,0,0,0,0,0,Regular Element #2,
+,299,89,768,0,0,0,0,0,0,0,0,0,0,Testing #1,
+,300,89,1024,0,1,299,299,299,0,0,0,0,0,Testing #1,"{""header"":""parent""}"
+,301,89,384,0,1,297,297,297,0,0,0,0,0,[Translate to Dansk:] Regular Element #1,
+,302,89,448,0,2,297,301,301,0,0,0,0,0,[Translate to Deutsch:] [Translate to Dansk:] Regular Element #1,
+,303,89,1280,0,2,299,300,300,0,0,0,0,0,Testing #1,"{""header"":""source""}"