[BUGFIX] Sanitize undefined TCA columns required for data integrity 11/60511/4
authorOliver Hader <oliver@typo3.org>
Thu, 18 Apr 2019 11:17:39 +0000 (13:17 +0200)
committerGeorg Ringer <georg.ringer@gmail.com>
Wed, 24 Apr 2019 10:55:12 +0000 (12:55 +0200)
TCA's 'ctrl' section allows to define several database columns that
shall be used to store according integrity information, such as the
current language or pointers to ancestors used during localization.

In case those names are not defined properly in TCA's 'columns'
section, several commands (like copy of localize) are executed,
but without actually maintaining these values in the database.

In order to ensure integrity, missing columns that are defined in
the 'ctrl' section but missing in the 'columns' section are applied
with the TCA type 'passthrough'. This applies to 'ctrl' properties

* origUid
* languageField
* translationSource
* transOrigPointerField

Resolves: #88057
Releases: master, 9.5
Change-Id: I39a28dc2e1eddafe6363b7dd633fd84968fc620f
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/60511
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Daniel Goerz <daniel.goerz@posteo.de>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
typo3/sysext/core/Classes/Migrations/TcaMigration.php
typo3/sysext/core/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php
typo3/sysext/core/Tests/Functional/DataHandling/Regular/Modify/ActionTest.php
typo3/sysext/core/Tests/Unit/Migrations/TcaMigrationTest.php

index ad86d5a..c3ebba0 100644 (file)
@@ -51,6 +51,7 @@ class TcaMigration
         $tca = $this->migratePagesLanguageOverlayRemoval($tca);
         $tca = $this->removeSelIconFieldPath($tca);
         $tca = $this->removeSetToDefaultOnCopy($tca);
+        $tca = $this->sanitizeControlSectionIntegrity($tca);
 
         return $tca;
     }
@@ -207,4 +208,41 @@ class TcaMigration
         }
         return $tca;
     }
+
+    /**
+     * Ensures that system internal columns that are required for data integrity
+     * (e.g. localize or copy a record) are available in case they have been defined
+     * in $GLOBALS['TCA'][<table-name>]['ctrl'].
+     *
+     * The list of references to usages below is not necessarily complete.
+     *
+     * @param array $tca
+     * @return array
+     *
+     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::fillInFieldArray()
+     */
+    protected function sanitizeControlSectionIntegrity(array $tca): array
+    {
+        $controlSectionNames = [
+            'origUid',
+            'languageField',
+            'transOrigPointerField',
+            'translationSource'
+        ];
+        foreach ($tca as $tableName => &$configuration) {
+            foreach ($controlSectionNames as $controlSectionName) {
+                $columnName = $configuration['ctrl'][$controlSectionName] ?? null;
+                if (empty($columnName) || !empty($configuration['columns'][$columnName])) {
+                    continue;
+                }
+                $configuration['columns'][$columnName] = [
+                    'config' => [
+                        'type' => 'passthrough',
+                        'default' => 0,
+                    ],
+                ];
+            }
+        }
+        return $tca;
+    }
 }
index 82fafe4..9648e33 100644 (file)
@@ -170,6 +170,28 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D
         $this->recordIds['localizedContentId'] = $localizedTableIds[self::TABLE_Content][self::VALUE_ContentIdSecond];
     }
 
+    /**
+     * @see DataSet/localizeContentRecord.csv
+     * @see \TYPO3\CMS\Core\Migrations\TcaMigration::sanitizeControlSectionIntegrity()
+     */
+    public function localizeContentWithEmptyTcaIntegrityColumns()
+    {
+        $integrityFieldNames = [
+            'origin' => $GLOBALS['TCA'][self::TABLE_Content]['ctrl']['origUid'] ?? null,
+            'language' => $GLOBALS['TCA'][self::TABLE_Content]['ctrl']['languageField'] ?? null,
+            'languageParent' => $GLOBALS['TCA'][self::TABLE_Content]['ctrl']['transOrigPointerField'] ?? null,
+            'languageSource' => $GLOBALS['TCA'][self::TABLE_Content]['ctrl']['translationSource'] ?? null,
+        ];
+        // explicitly unset integrity columns in TCA
+        foreach ($integrityFieldNames as $integrityFieldName) {
+            unset($GLOBALS['TCA'][self::TABLE_Content]['columns'][$integrityFieldName]);
+        }
+        // explicitly call TcaMigration (which was executed already earlier in functional testing bootstrap)
+        $GLOBALS['TCA'] = (new \TYPO3\CMS\Core\Migrations\TcaMigration())->migrate($GLOBALS['TCA']);
+        // perform actions to be tested
+        self::localizeContent();
+    }
+
     public function localizeContentWithLanguageSynchronization()
     {
         $GLOBALS['TCA']['tt_content']['columns']['header']['config']['behaviour']['allowLanguageSynchronization'] = true;
index 43110bb..08d49f4 100644 (file)
@@ -217,6 +217,21 @@ class ActionTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\Regular\A
 
     /**
      * @test
+     * @see DataSet/localizeContentRecord.csv
+     * @see \TYPO3\CMS\Core\Migrations\TcaMigration::sanitizeControlSectionIntegrity()
+     */
+    public function localizeContentWithEmptyTcaIntegrityColumns()
+    {
+        parent::localizeContentWithEmptyTcaIntegrityColumns();
+        $this->assertAssertionDataSet('localizeContent');
+
+        $responseSections = $this->getFrontendResponse(self::VALUE_PageId, self::VALUE_LanguageId)->getResponseSections();
+        $this->assertThat($responseSections, $this->getRequestSectionHasRecordConstraint()
+            ->setTable(self::TABLE_Content)->setField('header')->setValues('[Translate to Dansk:] Regular Element #1', '[Translate to Dansk:] Regular Element #2'));
+    }
+
+    /**
+     * @test
      * @see DataSet/localizeContentWSynchronization.csv
      */
     public function localizeContentWithLanguageSynchronization()
index d220256..0cf214a 100644 (file)
@@ -213,4 +213,217 @@ class TcaMigrationTest extends UnitTestCase
         $subject = new TcaMigration();
         $this->assertEquals($expected, $subject->migrate($input));
     }
+
+    /**
+     * @return array
+     */
+    public function ctrlIntegrityColumnsAreAvailableDataProvider(): array
+    {
+        return [
+            'filled columns' => [
+                // tca
+                [
+                    'aTable' => [
+                        'ctrl' => [
+                            'origUid' => 'aField',
+                            'languageField' => 'bField',
+                            'transOrigPointerField' => 'cField',
+                            'translationSource' => 'dField',
+                        ],
+                        'columns' => [
+                            'aField' => [
+                                'label' => 'aField',
+                                'config' => [
+                                    'type' => 'none',
+                                ],
+                            ],
+                            'bField' => [
+                                'label' => 'bField',
+                                'config' => [
+                                    'type' => 'none',
+                                ],
+                            ],
+                            'cField' => [
+                                'label' => 'cField',
+                                'config' => [
+                                    'type' => 'none',
+                                ],
+                            ],
+                            'dField' => [
+                                'label' => 'dField',
+                                'config' => [
+                                    'type' => 'none',
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+                // expectation
+                [
+                    'aTable' => [
+                        'ctrl' => [
+                            'origUid' => 'aField',
+                            'languageField' => 'bField',
+                            'transOrigPointerField' => 'cField',
+                            'translationSource' => 'dField',
+                        ],
+                        'columns' => [
+                            'aField' => [
+                                'label' => 'aField',
+                                'config' => [
+                                    'type' => 'none',
+                                ],
+                            ],
+                            'bField' => [
+                                'label' => 'bField',
+                                'config' => [
+                                    'type' => 'none',
+                                ],
+                            ],
+                            'cField' => [
+                                'label' => 'cField',
+                                'config' => [
+                                    'type' => 'none',
+                                ],
+                            ],
+                            'dField' => [
+                                'label' => 'dField',
+                                'config' => [
+                                    'type' => 'none',
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+            'mixed columns' => [
+                // tca
+                [
+                    'aTable' => [
+                        'ctrl' => [
+                            'origUid' => 'aField',
+                            'languageField' => 'bField',
+                            'transOrigPointerField' => 'cField',
+                            'translationSource' => 'dField',
+                        ],
+                        'columns' => [
+                            'aField' => [
+                                'label' => 'aField',
+                                'config' => [
+                                    'type' => 'none',
+                                ],
+                            ],
+                            'bField' => [
+                                'label' => 'bField',
+                                'config' => [
+                                    'type' => 'none',
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+                // expectation
+                [
+                    'aTable' => [
+                        'ctrl' => [
+                            'origUid' => 'aField',
+                            'languageField' => 'bField',
+                            'transOrigPointerField' => 'cField',
+                            'translationSource' => 'dField',
+                        ],
+                        'columns' => [
+                            'aField' => [
+                                'label' => 'aField',
+                                'config' => [
+                                    'type' => 'none',
+                                ],
+                            ],
+                            'bField' => [
+                                'label' => 'bField',
+                                'config' => [
+                                    'type' => 'none',
+                                ],
+                            ],
+                            'cField' => [
+                                'config' => [
+                                    'type' => 'passthrough',
+                                    'default' => 0,
+                                ],
+                            ],
+                            'dField' => [
+                                'config' => [
+                                    'type' => 'passthrough',
+                                    'default' => 0,
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+            'empty columns' => [
+                // tca
+                [
+                    'aTable' => [
+                        'ctrl' => [
+                            'origUid' => 'aField',
+                            'languageField' => 'bField',
+                            'transOrigPointerField' => 'cField',
+                            'translationSource' => 'dField',
+                        ],
+                        'columns' => [],
+                    ],
+                ],
+                // expectation
+                [
+                    'aTable' => [
+                        'ctrl' => [
+                            'origUid' => 'aField',
+                            'languageField' => 'bField',
+                            'transOrigPointerField' => 'cField',
+                            'translationSource' => 'dField',
+                        ],
+                        'columns' => [
+                            'aField' => [
+                                'config' => [
+                                    'type' => 'passthrough',
+                                    'default' => 0,
+                                ],
+                            ],
+                            'bField' => [
+                                'config' => [
+                                    'type' => 'passthrough',
+                                    'default' => 0,
+                                ],
+                            ],
+                            'cField' => [
+                                'config' => [
+                                    'type' => 'passthrough',
+                                    'default' => 0,
+                                ],
+                            ],
+                            'dField' => [
+                                'config' => [
+                                    'type' => 'passthrough',
+                                    'default' => 0,
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+    }
+
+    /**
+     * @param array $tca
+     * @param array $expectation
+     *
+     * @test
+     * @dataProvider ctrlIntegrityColumnsAreAvailableDataProvider
+     */
+    public function ctrlIntegrityColumnsAreAvailable(array $tca, array $expectation)
+    {
+        $subject = new TcaMigration();
+        self::assertSame($expectation, $subject->migrate($tca));
+    }
 }