[TASK] Finalize L10nModeUpdater for enhanced l10n_modes 46/51646/9
authorOliver Hader <oliver@typo3.org>
Sun, 12 Feb 2017 20:03:13 +0000 (21:03 +0100)
committerChristian Kuhn <lolli@schwarzbu.ch>
Tue, 14 Feb 2017 13:49:28 +0000 (14:49 +0100)
L10nModeUpdater has to be adjusted to reflect all possibilities that
became available with introducing allowLanguageSynchronization and
the implicit synchronization for the exclude mode.

Instead of cloning the logic again inside the upgrade wizard, the
DataHandler and DataMapResolver is used to determine required changes
as well as performing especially relation resolving and handling.

Resolves: #79768
Releases: master
Change-Id: Id61c419fe9f6aa6fdbbe6f3d6335b0e9c1bf0693
Reviewed-on: https://review.typo3.org/51646
Reviewed-by: Andreas Fernandez <typo3@scripting-base.de>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
typo3/sysext/core/Classes/DataHandling/Localization/State.php
typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php
typo3/sysext/install/Classes/Updates/RowUpdater/ImageCropUpdater.php
typo3/sysext/install/Classes/Updates/RowUpdater/L10nModeUpdater.php

index e3b4976..8c8f437 100644 (file)
@@ -192,6 +192,14 @@ class State
     }
 
     /**
+     * @return array
+     */
+    public function toArray(): array
+    {
+        return $this->states ?? [];
+    }
+
+    /**
      * @return string[]
      */
     public function getModifiedFieldNames()
index 1f3e889..0cc2df8 100644 (file)
@@ -49,7 +49,7 @@ class DatabaseRowsUpdateWizard extends AbstractUpdate
      * @var array Single classes that may update rows
      */
     protected $rowUpdater = [
-//        L10nModeUpdater::class,
+        L10nModeUpdater::class,
         ImageCropUpdater::class,
     ];
 
index 31ef750..c1d69d1 100644 (file)
@@ -30,6 +30,16 @@ use TYPO3\CMS\Install\Service\LoadTcaService;
 class ImageCropUpdater implements RowUpdaterInterface
 {
     /**
+     * @var array Full, migrated TCA as prepared by upgrade wizard controller
+     */
+    protected $migratedTca;
+
+    /**
+     * @var array Full, but NOT migrated TCA
+     */
+    protected $notMigratedTca;
+
+    /**
      * List of tables with information about to migrate fields.
      * Created during hasPotentialUpdateForTable(), used in updateTableRow()
      *
@@ -38,6 +48,18 @@ class ImageCropUpdater implements RowUpdaterInterface
     protected $payload = [];
 
     /**
+     * Prepare non-migrated TCA to be used in 'hasPotentialUpdateForTable' step
+     */
+    public function __construct()
+    {
+        $this->migratedTca = $GLOBALS['TCA'];
+        $loadTcaService = GeneralUtility::makeInstance(LoadTcaService::class);
+        $loadTcaService->loadExtensionTablesWithoutMigration();
+        $this->notMigratedTca = $GLOBALS['TCA'];
+        $GLOBALS['TCA'] = $this->migratedTca;
+    }
+
+    /**
      * Get title
      *
      * @return string
@@ -55,12 +77,14 @@ class ImageCropUpdater implements RowUpdaterInterface
      */
     public function hasPotentialUpdateForTable(string $tableName): bool
     {
+        $GLOBALS['TCA'] = $this->notMigratedTca;
         $result = false;
         $payload = $this->getPayloadForTable($tableName);
         if (count($payload) !== 0) {
             $this->payload[$tableName] = $payload;
             $result = true;
         }
+        $GLOBALS['TCA'] = $this->migratedTca;
         return $result;
     }
 
@@ -78,13 +102,16 @@ class ImageCropUpdater implements RowUpdaterInterface
         foreach ($tablePayload['fields'] as $field) {
             if (strpos($inputRow[$field], '{"x":') === 0) {
                 $file = $this->getFile($inputRow, $tablePayload['fileReferenceField'] ?: 'uid_local');
-                $cropArea = Area::createFromConfiguration(json_decode($inputRow[$field], true));
-                $cropVariantCollectionConfig = [
-                    'default' => [
-                        'cropArea' => $cropArea->makeRelativeBasedOnFile($file)->asArray(),
-                    ]
-                ];
-                $inputRow[$field] = json_encode($cropVariantCollectionConfig);
+                $cropArray = json_decode($inputRow[$field], true);
+                if (is_array($cropArray)) {
+                    $cropArea = Area::createFromConfiguration(json_decode($inputRow[$field], true));
+                    $cropVariantCollectionConfig = [
+                        'default' => [
+                            'cropArea' => $cropArea->makeRelativeBasedOnFile($file)->asArray(),
+                        ]
+                    ];
+                    $inputRow[$field] = json_encode($cropVariantCollectionConfig);
+                }
             }
         }
 
@@ -106,8 +133,6 @@ class ImageCropUpdater implements RowUpdaterInterface
      */
     protected function getPayloadForTable(string $tableName): array
     {
-        $loadTcaService = GeneralUtility::makeInstance(LoadTcaService::class);
-        $loadTcaService->loadExtensionTablesWithoutMigration();
         if (!is_array($GLOBALS['TCA'][$tableName])) {
             throw new \RuntimeException(
                 'Globals TCA of given table name must exist',
index ee99575..16a6736 100644 (file)
@@ -18,18 +18,30 @@ 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\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Versioning\VersionState;
 use TYPO3\CMS\Install\Service\LoadTcaService;
+use TYPO3\CMS\Lang\LanguageService;
 
 /**
  * Migrate values for database records having columns
- * using "l10n_mode" set to "mergeIfNotBlank".
- *
- * @todo: This needs a review and finish
+ * using "l10n_mode" set to "mergeIfNotBlank" or "exclude".
  */
 class L10nModeUpdater implements RowUpdaterInterface
 {
     /**
+     * @var array Full, migrated TCA as prepared by upgrade wizard controller
+     */
+    protected $migratedTca;
+
+    /**
+     * @var array Full, but NOT migrated TCA
+     */
+    protected $notMigratedTca;
+
+    /**
      * List of tables with information about to migrate fields.
      * Created during hasPotentialUpdateForTable(), used in updateTableRow()
      *
@@ -38,6 +50,18 @@ class L10nModeUpdater implements RowUpdaterInterface
     protected $payload = [];
 
     /**
+     * Prepare non-migrated TCA to be used in 'hasPotentialUpdateForTable' step
+     */
+    public function __construct()
+    {
+        $this->migratedTca = $GLOBALS['TCA'];
+        $loadTcaService = GeneralUtility::makeInstance(LoadTcaService::class);
+        $loadTcaService->loadExtensionTablesWithoutMigration();
+        $this->notMigratedTca = $GLOBALS['TCA'];
+        $GLOBALS['TCA'] = $this->migratedTca;
+    }
+
+    /**
      * Get title
      *
      * @return string
@@ -56,13 +80,10 @@ class L10nModeUpdater implements RowUpdaterInterface
      */
     public function hasPotentialUpdateForTable(string $tableName): bool
     {
-        $result = false;
-        $payload = $this->getL10nModePayloadForTable($tableName);
-        if (count($payload) !== 0) {
-            $this->payload[$tableName] = $payload;
-            $result = true;
-        }
-        return $result;
+        $GLOBALS['TCA'] = $this->notMigratedTca;
+        $this->payload[$tableName] = $this->getL10nModePayloadForTable($tableName);
+        $GLOBALS['TCA'] = $this->migratedTca;
+        return !empty($this->payload[$tableName]['localizations']);
     }
 
     /**
@@ -74,109 +95,110 @@ class L10nModeUpdater implements RowUpdaterInterface
      */
     public function updateTableRow(string $tableName, array $inputRow): array
     {
-        $tablePayload = $this->payload[$tableName];
+        $currentId = $inputRow['uid'];
 
-        $uid = $inputRow['uid'];
-        if (empty($tablePayload['localizations'][$uid])) {
+        if (empty($this->payload[$tableName]['localizations'][$currentId])) {
             return $inputRow;
         }
 
-        $source = $tablePayload['localizations'][$uid];
-
-        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
-        $fakeAdminUser = GeneralUtility::makeInstance(BackendUserAuthentication::class);
-        $fakeAdminUser->user = ['admin' => 1];
-
         // disable DataHandler hooks for processing this update
         if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php'])) {
             $dataHandlerHooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php'];
             unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']);
         }
 
-        $fields = $tablePayload['fields'];
-        $fieldNames = array_keys($fields);
-        $fieldTypes = $tablePayload['fieldTypes'];
-        $sourceFieldName = $tablePayload['sourceFieldName'];
+        if (empty($GLOBALS['LANG'])) {
+            $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageService::class);
+        }
+        if (!empty($GLOBALS['BE_USER'])) {
+            $adminUser = $GLOBALS['BE_USER'];
+        }
+        // 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);
+        $GLOBALS['BE_USER'] = $fakeAdminUser;
 
-        $sourceTableName = $tableName;
-        if ($tableName === 'pages_language_overlay') {
-            $sourceTableName = 'pages';
+        $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'];
         }
-        $sourceRow = $this->getRow($sourceTableName, $source);
 
-        $updateValues = [];
-        $l10nState = [];
+        $dataMap = [];
 
-        $row = $this->getRow($tableName, $uid);
-        foreach ($row as $fieldName => $fieldValue) {
-            if (!in_array($fieldName, $fieldNames)) {
-                continue;
+        // 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]);
+            }
+        }
 
-            $l10nState[$fieldName] = 'custom';
-
-            if (
-                // default
-                empty($fieldTypes[$fieldName])
-                && trim((string)$fieldValue) === ''
-                // group types (basically as comma seprated values)
-                || $fieldTypes[$fieldName] === 'group'
-                && (
-                    $fieldValue === ''
-                    || $fieldValue === null
-                    || (string)$fieldValue === '0'
-                )
-            ) {
-                $updateValues[$fieldName] = $sourceRow[$fieldName];
-                $l10nState[$fieldName] = 'parent';
+        // define localization states and thus trigger updates later
+        if (State::isApplicable($tableName)) {
+            $stateUpdates = [];
+            foreach ($tablePayload['fieldModes'] as $fieldName => $fieldMode) {
+                if ($fieldMode !== 'mergeIfNotBlank') {
+                    continue;
+                }
+                if (!empty($inputRow[$fieldName])) {
+                    $stateUpdates[$fieldName] = State::STATE_CUSTOM;
+                } else {
+                    $stateUpdates[$fieldName] = State::STATE_PARENT;
+                }
             }
-            // inline types, but only file references
-            if (
-                !empty($fieldTypes[$fieldName])
-                && $fieldTypes[$fieldName] === 'inline/FAL'
-            ) {
-                $parentId = (!empty($row['t3ver_oid']) ? $row['t3ver_oid'] : $source);
-                $commandMap = [
-                    $sourceTableName => [
-                        $parentId => [
-                            'inlineLocalizeSynchronize' => [
-                                'action' => 'localize',
-                                'language' => $row[$sourceFieldName],
-                                'field' => $fieldName,
-                            ]
+
+            $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()
                         ]
                     ]
                 ];
-                $fakeAdminUser->workspace = $row['t3ver_wsid'];
-                $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
-                $dataHandler->start([], $commandMap, $fakeAdminUser);
-                $dataHandler->process_cmdmap();
-                $l10nState[$fieldName] = 'parent';
             }
         }
 
-        $updateValues['l10n_state'] = json_encode($l10nState);
-
-        $queryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
-        foreach ($updateValues as $updateFieldName => $updateValue) {
-            $queryBuilder->set($updateFieldName, $updateValue);
+        if (empty($dataMap)) {
+            return $inputRow;
         }
 
-        $queryBuilder
-            ->update($tableName)
-            ->where(
-                $queryBuilder->expr()->eq(
-                    'uid',
-                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
-                )
-            )
-            ->execute();
-        $databaseQueries[] = $queryBuilder->getSQL();
+        // 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;
         }
+        if (!empty($adminUser)) {
+            $GLOBALS['BE_USER'] = $adminUser;
+        }
 
+        // the unchanged(!) state as submitted
         return $inputRow;
     }
 
@@ -195,30 +217,27 @@ class L10nModeUpdater implements RowUpdaterInterface
      */
     protected function getL10nModePayloadForTable(string $tableName): array
     {
-        $loadTcaService = GeneralUtility::makeInstance(LoadTcaService::class);
-        $loadTcaService->loadExtensionTablesWithoutMigration();
         if (!is_array($GLOBALS['TCA'][$tableName])) {
             throw new \RuntimeException(
                 'Globals TCA of given table name must exist',
                 1484176136
             );
         }
-        $tableDefinition = $GLOBALS['TCA'][$tableName];
 
-        $payload = [];
-        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $tableDefinition = $GLOBALS['TCA'][$tableName];
+        $languageFieldName = ($tableDefinition['ctrl']['languageField'] ?? null);
+        $parentFieldName = ($tableDefinition['ctrl']['transOrigPointerField'] ?? null);
 
         if (
             empty($tableDefinition['columns'])
             || !is_array($tableDefinition['columns'])
-            || empty($tableDefinition['ctrl']['languageField'])
-            || empty($tableDefinition['ctrl']['transOrigPointerField'])
+            || empty($languageFieldName)
+            || empty($parentFieldName)
         ) {
-            return $payload;
+            return [];
         }
 
-        $fields = [];
-        $fieldTypes = [];
+        $fieldModes = [];
         foreach ($tableDefinition['columns'] as $fieldName => $fieldConfiguration) {
             if (
                 empty($fieldConfiguration['l10n_mode'])
@@ -230,71 +249,77 @@ class L10nModeUpdater implements RowUpdaterInterface
                 $fieldConfiguration['l10n_mode'] === 'exclude'
                 || $fieldConfiguration['l10n_mode'] === 'mergeIfNotBlank'
             ) {
-                $fields[$fieldName] = $fieldConfiguration;
+                $fieldModes[$fieldName] = $fieldConfiguration['l10n_mode'];
             }
         }
 
-        if (empty($fields)) {
-            return $payload;
+        if (empty($fieldModes)) {
+            return [];
         }
 
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
         $queryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
         $queryBuilder->getRestrictions()->removeAll();
         $queryBuilder->from($tableName);
 
-        foreach ($fields as $fieldName => $fieldConfiguration) {
-            if (empty($fieldConfiguration['config']['type'])) {
-                continue;
-            }
-
-            if ($fieldConfiguration['config']['type'] === 'group') {
-                $fieldTypes[$fieldName] = 'group';
-            }
-            if (
-                $fieldConfiguration['config']['type'] === 'inline'
-                && !empty($fieldConfiguration['config']['foreign_field'])
-                && $fieldConfiguration['config']['foreign_field'] === 'uid_foreign'
-                && !empty($fieldConfiguration['config']['foreign_table'])
-                && $fieldConfiguration['config']['foreign_table'] === 'sys_file_reference'
-            ) {
-                $fieldTypes[$fieldName] = 'inline/FAL';
-            }
-        }
-
-        $sourceFieldName = $tableDefinition['ctrl']['transOrigPointerField'];
-        $selectFieldNames = ['uid', $sourceFieldName];
+        $parentFieldName = $tableDefinition['ctrl']['transOrigPointerField'];
+        $selectFieldNames = ['uid', $parentFieldName];
+
+        $predicates = [
+            $queryBuilder->expr()->gt(
+                $tableDefinition['ctrl']['languageField'],
+                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
+            ),
+            $queryBuilder->expr()->gt(
+                $parentFieldName,
+                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
+            )
+        ];
 
         if (!empty($tableDefinition['ctrl']['versioningWS'])) {
             $selectFieldNames = array_merge(
                 $selectFieldNames,
-                ['t3ver_wsid', 't3ver_oid']
+                ['t3ver_wsid', 't3ver_oid', 't3ver_state']
+            );
+            $predicates[] = $queryBuilder->expr()->orX(
+                $queryBuilder->expr()->eq(
+                    't3ver_state',
+                    $queryBuilder->createNamedParameter(
+                        VersionState::NEW_PLACEHOLDER_VERSION,
+                        \PDO::PARAM_INT
+                    )
+                ),
+                $queryBuilder->expr()->eq(
+                    't3ver_state',
+                    $queryBuilder->createNamedParameter(
+                        VersionState::DEFAULT_STATE,
+                        \PDO::PARAM_INT
+                    )
+                ),
+                $queryBuilder->expr()->eq(
+                    't3ver_state',
+                    $queryBuilder->createNamedParameter(
+                        VersionState::MOVE_POINTER,
+                        \PDO::PARAM_INT
+                    )
+                )
             );
         }
 
         $statement = $queryBuilder
             ->select(...$selectFieldNames)
-            ->andWhere(
-                $queryBuilder->expr()->gt(
-                    $tableDefinition['ctrl']['languageField'],
-                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
-                ),
-                $queryBuilder->expr()->gt(
-                    $sourceFieldName,
-                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
-                )
-            )
+            ->andWhere(...$predicates)
             ->execute();
 
+        $payload = [];
+
         foreach ($statement as $row) {
-            $source = $row[$sourceFieldName];
-            $payload['sources'][$source][] = $row['uid'];
-            $payload['localizations'][$row['uid']] = $source;
+            $translationId = $row['uid'];
+            $parentId = $row[$parentFieldName];
+            $payload['localizations'][$translationId] = $parentId;
         }
-
-        if (!empty($payload['sources'])) {
-            $payload['fields'] = $fields;
-            $payload['fieldTypes'] = $fieldTypes;
-            $payload['sourceFieldName'] = $sourceFieldName;
+        if (!empty($payload['localizations'])) {
+            $payload['fieldModes'] = $fieldModes;
         }
 
         return $payload;