[BUGFIX] DataMapProcessor::synchronizeInlineRelations removes entities 44/53544/4
authorOliver Hader <oliver@typo3.org>
Wed, 19 Jul 2017 10:52:41 +0000 (12:52 +0200)
committerOliver Hader <oliver.hader@typo3.org>
Mon, 24 Jul 2017 20:21:10 +0000 (22:21 +0200)
In case RelationHandler::readForeignField() is called with invalid UID
values for a parent record (e.g. zero, or non-numeric values), all records
might be selected if the database default value for a parent pointer field
is set to zero instead of NULL.

Besides that DataMapProcessor::synchronizeInlineRelations() uses the
mentioned RelationHandler method to resolve suggested and actually
persisted relations. The processing is adjusted to avoid invoking
RelationHandler using non-numeric parent pointer values.

Resolves: #81915
Releases: master, 8.7
Change-Id: I108501c69c9cdb732bb88526830f0f73f2d680af
Reviewed-on: https://review.typo3.org/53544
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: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
typo3/sysext/core/Classes/DataHandling/Localization/DataMapProcessor.php
typo3/sysext/core/Classes/Database/RelationHandler.php
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/localizeChildrenHStandaloneChildrenWAllChildrenSelectNLanguageSynchronization.csv [new file with mode: 0644]

index 5f27a1f..9a18b5e 100644 (file)
@@ -494,11 +494,9 @@ class DataMapProcessor
      */
     protected function synchronizeInlineRelations(DataMapItem $item, string $fieldName, array $fromRecord, array $forRecord)
     {
-        $fromId = $fromRecord['uid'];
         $configuration = $GLOBALS['TCA'][$item->getFromTableName()]['columns'][$fieldName];
         $isLocalizationModeExclude = ($configuration['l10n_mode'] ?? null) === 'exclude';
         $foreignTableName = $configuration['config']['foreign_table'];
-        $manyToManyTable = ($configuration['config']['MM'] ?? '');
 
         $fieldNames = [
             'language' => ($GLOBALS['TCA'][$foreignTableName]['ctrl']['languageField'] ?? null),
@@ -507,38 +505,17 @@ class DataMapProcessor
         ];
         $isTranslatable = (!empty($fieldNames['language']) && !empty($fieldNames['parent']));
 
-        // determine suggested elements of either translation parent or source record
-        // from data-map, in case the accordant language parent/source record was modified
-        if ($this->isSetInDataMap($item->getFromTableName(), $fromId, $fieldName)) {
-            $suggestedAncestorIds = GeneralUtility::trimExplode(
-                ',',
-                $this->allDataMap[$item->getFromTableName()][$fromId][$fieldName],
-                true
-            );
-        // determine suggested elements of either translation parent or source record from storage
-        } else {
-            $relationHandler = $this->createRelationHandler();
-            $relationHandler->start(
-                $fromRecord[$fieldName],
-                $foreignTableName,
-                $manyToManyTable,
-                $fromId,
-                $item->getFromTableName(),
-                $configuration['config']
-            );
-            $suggestedAncestorIds = $this->mapRelationItemId($relationHandler->itemArray);
-        }
-        // determine persisted elements for the current data-map item
-        $relationHandler = $this->createRelationHandler();
-        $relationHandler->start(
-            $forRecord[$fieldName] ?? '',
-            $foreignTableName,
-            $manyToManyTable,
-            $item->getId(),
-            $item->getTableName(),
-            $configuration['config']
+        $suggestedAncestorIds = $this->resolveSuggestedInlineRelations(
+            $item,
+            $fieldName,
+            $fromRecord
         );
-        $persistedIds = $this->mapRelationItemId($relationHandler->itemArray);
+        $persistedIds = $this->resolvePersistedInlineRelations(
+            $item,
+            $fieldName,
+            $forRecord
+        );
+
         // The dependent ID map points from language parent/source record to
         // localization, thus keys: parents/sources & values: localizations
         $dependentIdMap = $this->fetchDependentIdMap($foreignTableName, $suggestedAncestorIds, $item->getLanguage());
@@ -662,6 +639,81 @@ class DataMapProcessor
     }
 
     /**
+     * Determines suggest inline relations of either translation parent or
+     * source record from data-map or storage in case records have been
+     * persisted already.
+     *
+     * @param DataMapItem $item
+     * @param string $fieldName
+     * @param array $fromRecord
+     * @return int[]|string[]
+     */
+    protected function resolveSuggestedInlineRelations(DataMapItem $item, string $fieldName, array $fromRecord): array
+    {
+        $suggestedAncestorIds = [];
+        $fromId = $fromRecord['uid'];
+        $configuration = $GLOBALS['TCA'][$item->getFromTableName()]['columns'][$fieldName];
+        $foreignTableName = $configuration['config']['foreign_table'];
+        $manyToManyTable = ($configuration['config']['MM'] ?? '');
+
+        // determine suggested elements of either translation parent or source record
+        // from data-map, in case the accordant language parent/source record was modified
+        if ($this->isSetInDataMap($item->getFromTableName(), $fromId, $fieldName)) {
+            $suggestedAncestorIds = GeneralUtility::trimExplode(
+                ',',
+                $this->allDataMap[$item->getFromTableName()][$fromId][$fieldName],
+                true
+            );
+        // determine suggested elements of either translation parent or source record from storage
+        } elseif (MathUtility::canBeInterpretedAsInteger($fromId)) {
+            $relationHandler = $this->createRelationHandler();
+            $relationHandler->start(
+                $fromRecord[$fieldName],
+                $foreignTableName,
+                $manyToManyTable,
+                $fromId,
+                $item->getFromTableName(),
+                $configuration['config']
+            );
+            $suggestedAncestorIds = $this->mapRelationItemId($relationHandler->itemArray);
+        }
+
+        return $suggestedAncestorIds;
+    }
+
+    /**
+     * Determine persisted inline relations for current data-map-item.
+     *
+     * @param DataMapItem $item
+     * @param string $fieldName
+     * @param array $forRecord
+     * @return int[]
+     */
+    private function resolvePersistedInlineRelations(DataMapItem $item, string $fieldName, array $forRecord): array
+    {
+        $persistedIds = [];
+        $configuration = $GLOBALS['TCA'][$item->getFromTableName()]['columns'][$fieldName];
+        $foreignTableName = $configuration['config']['foreign_table'];
+        $manyToManyTable = ($configuration['config']['MM'] ?? '');
+
+        // determine persisted elements for the current data-map item
+        if (!$item->isNew()) {
+            $relationHandler = $this->createRelationHandler();
+            $relationHandler->start(
+                $forRecord[$fieldName] ?? '',
+                $foreignTableName,
+                $manyToManyTable,
+                $item->getId(),
+                $item->getTableName(),
+                $configuration['config']
+            );
+            $persistedIds = $this->mapRelationItemId($relationHandler->itemArray);
+        }
+
+        return $persistedIds;
+    }
+
+    /**
      * Determines whether a combination of table name, id and field name is
      * set in data-map. This method considers null values as well, that would
      * not be considered by a plain isset() invocation.
index a8a2f87..d7a31b6 100644 (file)
@@ -906,6 +906,12 @@ class RelationHandler
 
         $key = 0;
         $uid = (int)$uid;
+        // skip further processing if $uid does not
+        // point to a valid parent record
+        if ($uid === 0) {
+            return;
+        }
+
         $foreign_table = $conf['foreign_table'];
         $foreign_table_field = $conf['foreign_table_field'];
         $useDeleteClause = !$this->undeleteRecord;
index c196738..f019a31 100644 (file)
@@ -306,6 +306,29 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
         $this->recordIds['newPriceId'] = $this->actionService->getDataHandler()->substNEWwithIDs[$newPriceId];
     }
 
+    public function localizeChildrenHavingStandaloneChildrenInSelectModeAndLanguageSynchronization()
+    {
+        $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;
+        unset($GLOBALS['TCA'][self::TABLE_Content]['columns'][self::FIELD_ContentHotel]['config']['behaviour']['localizeChildrenAtParentLocalization']);
+        unset($GLOBALS['TCA'][self::TABLE_Hotel]['columns'][self::FIELD_HotelOffer]['config']['behaviour']['localizeChildrenAtParentLocalization']);
+
+        $this->actionService->createNewRecords(self::VALUE_PageId, [
+            self::TABLE_Hotel =>  ['title' => 'Hotel Standalone', 'parenttable' => 'tt_content'],
+            self::TABLE_Offer =>  ['title' => 'Offer Standalone', 'parenttable' => 'tt_content'],
+        ]);
+
+        $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->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
      */
index 4ea1806..4adb65f 100644 (file)
@@ -290,6 +290,22 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\IRRE\Fore
 
     /**
      * @test
+     * @see DataSet/localizeChildrenHStandaloneChildrenWAllChildrenSelectNLanguageSynchronization.csv
+     * @see https://forge.typo3.org/issues/81915
+     */
+    public function localizeChildrenHavingStandaloneChildrenInSelectModeAndLanguageSynchronization()
+    {
+        parent::localizeChildrenHavingStandaloneChildrenInSelectModeAndLanguageSynchronization();
+        $this->assertAssertionDataSet('localizeChildrenHStandaloneChildrenWAllChildrenSelectNLanguageSynchronization');
+
+        $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->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 Dansk:] Hotel #1', '[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/localizeChildrenHStandaloneChildrenWAllChildrenSelectNLanguageSynchronization.csv b/typo3/sysext/core/Tests/Functional/DataHandling/IRRE/ForeignField/Modify/DataSet/localizeChildrenHStandaloneChildrenWAllChildrenSelectNLanguageSynchronization.csv
new file mode 100644 (file)
index 0000000..a6e8337
--- /dev/null
@@ -0,0 +1,31 @@
+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,l10n_state
+,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,"{""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,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,1024,0,0,0,0,0,0,0,0,0,"Hotel Standalone",0,tt_content,,0
+,7,89,1,0,1,5,5,0,0,0,0,0,"[Translate to Dansk:] Hotel #1",299,tt_content,,0
+,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
+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,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
+,8,89,1,0,0,0,0,0,0,0,0,0,"Offer #1.1",5,tx_irretutorial_1nff_hotel,,1
+,9,89,0,0,0,0,0,0,0,0,0,0,"Offer Standalone",0,tt_content,,0
+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,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,
+,13,89,1,0,0,0,0,0,0,0,0,0,"Price #1.1.1",8,tx_irretutorial_1nff_offer,