[FEATURE] Install tool table row updaters 53/51253/5
authorChristian Kuhn <lolli@schwarzbu.ch>
Tue, 10 Jan 2017 21:38:43 +0000 (22:38 +0100)
committerBenni Mack <benni@typo3.org>
Wed, 11 Jan 2017 17:53:55 +0000 (18:53 +0100)
Introduce an upgrade wizard in install tool that walks through all
TCA table rows and calls registered row updaters to manipulate single
row data.

Change-Id: I25425e79d966d229da0fa6a181f0eabf97208a70
Resolves: #79279
Releases: master
Reviewed-on: https://review.typo3.org/51253
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: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
typo3/sysext/core/Documentation/Changelog/8.5/Feature-77757-EnableRecheckingWhetherAnUpdateWizardShouldRun.rst
typo3/sysext/install/Classes/Controller/Action/Tool/UpgradeWizard.php
typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php [new file with mode: 0644]
typo3/sysext/install/Classes/Updates/RowUpdater/RowUpdaterInterface.php [new file with mode: 0644]
typo3/sysext/install/Resources/Private/Partials/Action/Tool/UpgradeWizard/ListUpdates.html
typo3/sysext/install/ext_localconf.php

index c3613c3..0db3dd6 100644 (file)
@@ -10,7 +10,7 @@ Description
 ===========
 
 It is now possible to reset the upgrade wizards marked as done. In Install Tool you
-will find a list of wizards that has been marked as done, additionally with a
+will find a list of wizards that have been marked as done, additionally with a
 checkbox for each to reset their status. Then the wizard will test whether
 it needs to be executed again.
 
index 06af430..c1d2b18 100644 (file)
@@ -23,7 +23,11 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\VersionNumberUtility;
 use TYPO3\CMS\Install\Controller\Action;
 use TYPO3\CMS\Install\Status\ErrorStatus;
+use TYPO3\CMS\Install\Status\NoticeStatus;
+use TYPO3\CMS\Install\Status\OkStatus;
+use TYPO3\CMS\Install\Status\StatusInterface;
 use TYPO3\CMS\Install\Updates\AbstractUpdate;
+use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface;
 
 /**
  * Handle update wizards
@@ -77,7 +81,7 @@ class UpgradeWizard extends Action\AbstractAction
                 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['finalUpdateDatabaseSchema'] = \TYPO3\CMS\Install\Updates\FinalDatabaseSchemaUpdate::class;
             }
         } catch (StatementException $exception) {
-            /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
+            /** @var $message StatusInterface */
             $message = GeneralUtility::makeInstance(ErrorStatus::class);
             $message->setTitle('SQL error');
             $message->setMessage($exception->getMessage());
@@ -94,10 +98,22 @@ class UpgradeWizard extends Action\AbstractAction
             $actionMessages[] = $this->performUpdate();
             $this->view->assign('updateAction', 'performUpdate');
         } elseif (isset($this->postValues['set']['recheckWizards'])) {
-            $actionMessages[] = $this->recheckWizards();
+            $actionMessages[] = $this->recheckWizardsAndRowUpdaters();
+            if (empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'])) {
+                /** @var $message StatusInterface */
+                $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\WarningStatus::class);
+                $message->setTitle('No update wizards registered');
+                $actionMessages[] = $message;
+            }
             $this->listUpdates();
             $this->view->assign('updateAction', 'listUpdates');
         } else {
+            if (empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'])) {
+                /** @var $message StatusInterface */
+                $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\WarningStatus::class);
+                $message->setTitle('No update wizards registered');
+                $actionMessages[] = $message;
+            }
             $this->listUpdates();
             $this->view->assign('updateAction', 'listUpdates');
         }
@@ -114,69 +130,83 @@ class UpgradeWizard extends Action\AbstractAction
      */
     protected function listUpdates()
     {
-        if (empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'])) {
-            /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
-            $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\WarningStatus::class);
-            $message->setTitle('No update wizards registered');
-            return $message;
-        }
-
         $availableUpdates = [];
+        $markedWizardsDoneInRegistry = [];
+        $markedWizardsDoneByCallingShouldRenderWizard = [];
+        $registry = GeneralUtility::makeInstance(Registry::class);
         foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $className) {
             $updateObject = $this->getUpdateObjectInstance($className, $identifier);
-            if ($updateObject->shouldRenderWizard()) {
-                // $explanation is changed by reference in Update objects!
-                $explanation = '';
-                $updateObject->checkForUpdate($explanation);
-                $availableUpdates[$identifier] = [
+            $markedDoneInRegistry = $registry->get('installUpdate', $className, false);
+            if ($markedDoneInRegistry) {
+                $markedWizardsDoneInRegistry[] = [
                     'identifier' => $identifier,
                     'title' => $updateObject->getTitle(),
-                    'explanation' => $explanation,
-                    'renderNext' => false,
                 ];
-                if ($identifier === 'initialUpdateDatabaseSchema') {
-                    $availableUpdates['initialUpdateDatabaseSchema']['renderNext'] = $this->needsInitialUpdateDatabaseSchema;
-                    // initialUpdateDatabaseSchema is always the first update
-                    // we stop immediately here as the remaining updates may
-                    // require the new fields to be present in order to avoid SQL errors
-                    break;
-                } elseif ($identifier === 'finalUpdateDatabaseSchema') {
-                    // Okay to check here because finalUpdateDatabaseSchema is last element in array
-                    $availableUpdates['finalUpdateDatabaseSchema']['renderNext'] = count($availableUpdates) === 1;
-                } elseif (!$this->needsInitialUpdateDatabaseSchema && $updateObject->shouldRenderNextButton()) {
-                    // There are Updates that only show text and don't want to be executed
-                    $availableUpdates[$identifier]['renderNext'] = true;
+            } else {
+                if ($updateObject->shouldRenderWizard()) {
+                    // $explanation is changed by reference in Update objects!
+                    $explanation = '';
+                    $updateObject->checkForUpdate($explanation);
+                    $availableUpdates[$identifier] = [
+                        'identifier' => $identifier,
+                        'title' => $updateObject->getTitle(),
+                        'explanation' => $explanation,
+                        'renderNext' => false,
+                    ];
+                    if ($identifier === 'initialUpdateDatabaseSchema') {
+                        $availableUpdates['initialUpdateDatabaseSchema']['renderNext'] = $this->needsInitialUpdateDatabaseSchema;
+                        // initialUpdateDatabaseSchema is always the first update
+                        // we stop immediately here as the remaining updates may
+                        // require the new fields to be present in order to avoid SQL errors
+                        break;
+                    } elseif ($identifier === 'finalUpdateDatabaseSchema') {
+                        // Okay to check here because finalUpdateDatabaseSchema is last element in array
+                        $availableUpdates['finalUpdateDatabaseSchema']['renderNext'] = count($availableUpdates) === 1;
+                    } elseif (!$this->needsInitialUpdateDatabaseSchema && $updateObject->shouldRenderNextButton()) {
+                        // There are Updates that only show text and don't want to be executed
+                        $availableUpdates[$identifier]['renderNext'] = true;
+                    }
+                } else {
+                    $markedWizardsDoneByCallingShouldRenderWizard[] = [
+                        'identifier' => $identifier,
+                        'title' => $updateObject->getTitle(),
+                    ];
                 }
             }
         }
 
-        $this->view->assign('availableUpdates', $availableUpdates);
-
-        // compute done wizards for statistics
-        $wizardsDone = [];
-        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $className) {
-            /** @var AbstractUpdate $updateObject */
-            $updateObject = $this->getUpdateObjectInstance($className, $identifier);
-            if ($updateObject->shouldRenderWizard() !== true) {
-                $doneWizard = [
-                    'identifier' => $identifier,
-                    'title'      => $updateObject->getTitle()
-                ];
-                $wizardsDone[] = $doneWizard;
+        // List of row updaters marked as done from "DatabaseRowsUpdateWizard"
+        $rowUpdatersDoneClassNames = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
+        $rowUpdatersDone = [];
+        foreach ($rowUpdatersDoneClassNames as $rowUpdaterClassName) {
+            /** @var RowUpdaterInterface $rowUpdater */
+            $rowUpdater = GeneralUtility::makeInstance($rowUpdaterClassName);
+            if (!$rowUpdater instanceof RowUpdaterInterface) {
+                throw new \RuntimeException(
+                    'Row updater must implement RowUpdaterInterface',
+                    1484152906
+                );
             }
+            $rowUpdatersDone[] = [
+                'identifier' => $rowUpdaterClassName,
+                'title' => $rowUpdater->getTitle(),
+            ];
         }
-        $this->view->assign('wizardsDone', $wizardsDone);
 
-        $wizardsTotal = (count($wizardsDone) + count($availableUpdates));
-        $this->view->assign('wizardsTotal', $wizardsTotal);
+        $wizardsTotal = (count($markedWizardsDoneInRegistry) + count($markedWizardsDoneByCallingShouldRenderWizard) + count($availableUpdates));
+        $percentageDone = floor(($wizardsTotal - count($availableUpdates)) * 100 / $wizardsTotal);
 
-        $this->view->assign('wizardsPercentageDone', floor(($wizardsTotal - count($availableUpdates)) * 100 / $wizardsTotal));
+        $this->view->assign('wizardsDone', $markedWizardsDoneInRegistry);
+        $this->view->assign('rowUpdatersDone', $rowUpdatersDone);
+        $this->view->assign('availableUpdates', $availableUpdates);
+        $this->view->assign('wizardsTotal', $wizardsTotal);
+        $this->view->assign('wizardsPercentageDone', $percentageDone);
     }
 
     /**
      * Get user input of update wizard
      *
-     * @return \TYPO3\CMS\Install\Status\StatusInterface
+     * @return StatusInterface
      */
     protected function getUserInputForUpdate()
     {
@@ -197,30 +227,47 @@ class UpgradeWizard extends Action\AbstractAction
 
         $this->view->assign('updateData', $updateData);
 
-        /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
-        $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
+        /** @var $message StatusInterface */
+        $message = GeneralUtility::makeInstance(OkStatus::class);
         $message->setTitle('Show wizard options');
         return $message;
     }
 
     /**
-     * Rechecks whether the chosen wizards should be executed
+     * Rechecks the chosen wizards and row updaters to mark them as "was not executed" again.
      *
-     * @return \TYPO3\CMS\Install\Status\StatusInterface
+     * @return StatusInterface
      */
-    protected function recheckWizards()
+    protected function recheckWizardsAndRowUpdaters()
     {
-        if (empty($this->postValues['values']['recheck'])) {
-            $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\NoticeStatus::class);
+        if (empty($this->postValues['values']['recheck']) && empty($this->postValues['values']['recheckRowUpdater'])) {
+            $message = GeneralUtility::makeInstance(NoticeStatus::class);
             $message->setTitle('No wizards selected to recheck');
             return $message;
         }
-        foreach ($this->postValues['values']['recheck'] as $wizardIdentifier => $value) {
-            $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$wizardIdentifier];
-            $updateObject = $this->getUpdateObjectInstance($className, $wizardIdentifier);
-            GeneralUtility::makeInstance(Registry::class)->set('installUpdate', get_class($updateObject), 0);
+        $registry = GeneralUtility::makeInstance(Registry::class);
+        if (!empty($this->postValues['values']['recheck'])) {
+            foreach ($this->postValues['values']['recheck'] as $wizardIdentifier => $value) {
+                $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$wizardIdentifier];
+                $updateObject = $this->getUpdateObjectInstance($className, $wizardIdentifier);
+                $registry->set('installUpdate', get_class($updateObject), 0);
+            }
         }
-        $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
+        if (!empty($this->postValues['values']['recheckRowUpdater'])) {
+            $rowUpdatersToRecheck = $this->postValues['values']['recheckRowUpdater'];
+            $rowUpdatersMarkedAsDone = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
+            foreach ($rowUpdatersToRecheck as $rowUpdaterToReCheckClassName => $value) {
+                foreach ($rowUpdatersMarkedAsDone as $rowUpdaterMarkedAsDonePosition => $rowUpdaterMarkedAsDone) {
+                    if ($rowUpdaterMarkedAsDone === $rowUpdaterToReCheckClassName) {
+                        unset($rowUpdatersMarkedAsDone[$rowUpdaterMarkedAsDonePosition]);
+                        break;
+                    }
+                }
+            }
+            $registry->set('installUpdateRows', 'rowUpdatersDone', $rowUpdatersMarkedAsDone);
+        }
+
+        $message = GeneralUtility::makeInstance(OkStatus::class);
         $message->setTitle('Successfully rechecked');
         return $message;
     }
@@ -229,7 +276,7 @@ class UpgradeWizard extends Action\AbstractAction
      * Perform update of a specific wizard
      *
      * @throws \TYPO3\CMS\Install\Exception
-     * @return \TYPO3\CMS\Install\Status\StatusInterface
+     * @return StatusInterface
      */
     protected function performUpdate()
     {
@@ -247,8 +294,8 @@ class UpgradeWizard extends Action\AbstractAction
         // $wizardInputErrorMessage is given as reference to wizard object!
         $wizardInputErrorMessage = '';
         if (method_exists($updateObject, 'checkUserInput') && !$updateObject->checkUserInput($wizardInputErrorMessage)) {
-            /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
-            $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
+            /** @var $message StatusInterface */
+            $message = GeneralUtility::makeInstance(ErrorStatus::class);
             $message->setTitle('Input parameter broken');
             $message->setMessage($wizardInputErrorMessage ?: 'Something went wrong!');
             $wizardData['wizardInputBroken'] = true;
@@ -266,12 +313,12 @@ class UpgradeWizard extends Action\AbstractAction
             $performResult = $updateObject->performUpdate($databaseQueries, $customOutput);
 
             if ($performResult) {
-                /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
-                $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
+                /** @var $message StatusInterface */
+                $message = GeneralUtility::makeInstance(OkStatus::class);
                 $message->setTitle('Update successful');
             } else {
-                /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
-                $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
+                /** @var $message StatusInterface */
+                $message = GeneralUtility::makeInstance(ErrorStatus::class);
                 $message->setTitle('Update failed!');
                 if ($customOutput) {
                     $message->setMessage($customOutput);
diff --git a/typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php b/typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php
new file mode 100644 (file)
index 0000000..edeed7b
--- /dev/null
@@ -0,0 +1,299 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Install\Updates;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Registry;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface;
+
+/**
+ * This is a generic updater to migrate content of TCA rows.
+ *
+ * Multiple classes implementing interface "RowUpdateInterface" can be
+ * registered here, each for a specific update purpose,
+ *
+ * The updater fetches each row of all TCA registered tables and
+ * visits the client classes who may modify the row content.
+ *
+ * The updater remembers for each class if it run through, so the updater
+ * will be shown again if a new updater class is registered that has not
+ * been run yet.
+ *
+ * A start position pointer is stored in the registry that is updated during
+ * the run process, so if for instance the PHP process runs into a timeout,
+ * the job can restart at the position it stopped again.
+ */
+class DatabaseRowsUpdateWizard extends AbstractUpdate
+{
+    /**
+     * @var string Title of this updater
+     */
+    protected $title = 'Execute database migrations on single rows';
+
+    /**
+     * @var array Single classes that may update rows
+     */
+    protected $rowUpdater = [
+    ];
+
+    /**
+     * Checks if an update is needed by looking up in registry if all
+     * registered update row classes are marked as done or not.
+     *
+     * @param string &$description The description for the update
+     * @return bool Whether an update is needed (TRUE) or not (FALSE)
+     */
+    public function checkForUpdate(&$description)
+    {
+        $updateNeeded = false;
+        $rowUpdaterNotExecuted = $this->getRowUpdatersToExecute();
+        if (!empty($rowUpdaterNotExecuted)) {
+            $updateNeeded = true;
+        }
+        if (!$updateNeeded) {
+            return false;
+        }
+
+        $description = 'Some row updates have not been run yet:';
+        foreach ($rowUpdaterNotExecuted as $rowUpdateClassName) {
+            $rowUpdater = GeneralUtility::makeInstance($rowUpdateClassName);
+            if (!$rowUpdater instanceof RowUpdaterInterface) {
+                throw new \RuntimeException(
+                    'Row updater must implement RowUpdaterInterface',
+                    1484066647
+                );
+            }
+            $description .= '<br />' . htmlspecialchars($rowUpdater->getTitle());
+        }
+
+        return $updateNeeded;
+    }
+
+    /**
+     * Performs the configuration update.
+     *
+     * @param array &$databaseQueries Queries done in this update
+     * @param mixed &$customMessages Custom messages
+     * @return bool
+     * @throws \Doctrine\DBAL\ConnectionException
+     * @throws \Exception
+     */
+    public function performUpdate(array &$databaseQueries, &$customMessages)
+    {
+        $registry = GeneralUtility::makeInstance(Registry::class);
+
+        // If rows from the target table that is updated and the sys_registry table are on the
+        // same connection, the row update statement and sys_registry position update will be
+        // handled in a transaction to have an atomic operation in case of errors during execution.
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
+
+        /** @var RowUpdaterInterface[] $rowUpdaterInstances */
+        $rowUpdaterInstances = [];
+        // Single row updater instances are created only once for this method giving
+        // them a chance to set up local properties during hasPotentialUpdateForTable()
+        // and using that in updateTableRow()
+        foreach ($this->getRowUpdatersToExecute() as $rowUpdater) {
+            $rowUpdaterInstance = GeneralUtility::makeInstance($rowUpdater);
+            if (!$rowUpdaterInstance instanceof RowUpdaterInterface) {
+                throw new \RuntimeException(
+                    'Row updater must implement RowUpdaterInterface',
+                    1484071612
+                );
+            }
+            $rowUpdaterInstances[] = $rowUpdaterInstance;
+        }
+
+        // Scope of the row updater is to update all rows that have TCA,
+        // so our list of tables is just the list of loaded TCA tables.
+        $listOfAllTables = array_keys($GLOBALS['TCA']);
+
+        // @todo: hack
+        $listOfAllTables = [ 'tx_styleguide_staticdata' ];
+
+        // In case the PHP ended for whatever reason, fetch the last position from registry
+        // and throw away all tables before the first table from registry table name.
+        sort($listOfAllTables);
+        reset($listOfAllTables);
+        $firstTable = current($listOfAllTables);
+        $startPosition = $this->getStartPosition($firstTable);
+        foreach ($listOfAllTables as $table) {
+            if ($table === $startPosition['table']) {
+                break;
+            } else {
+                unset($listOfAllTables[$table]);
+            }
+        }
+
+        // Ask each row updater if it potentially has field updates for rows of each table
+        $tableToUpdaterList = [];
+        foreach ($listOfAllTables as $table) {
+            foreach ($rowUpdaterInstances as $updater) {
+                if ($updater->hasPotentialUpdateForTable($table)) {
+                    if (!is_array($tableToUpdaterList[$table])) {
+                        $tableToUpdaterList[$table] = [];
+                    }
+                    $tableToUpdaterList[$table][] = $updater;
+                }
+            }
+        }
+
+        // Iterate through all rows of all table that have potential row updaters attached,
+        // feed them each single row to each updater and finally update each row
+        foreach ($tableToUpdaterList as $table => $updaters) {
+            /** @var RowUpdaterInterface[] $updaters */
+            $connectionForTable = $connectionPool->getConnectionForTable($table);
+            $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
+            $queryBuilder->getRestrictions()->removeAll();
+            $queryBuilder->select('*')
+                ->from($table)
+                ->orderBy('uid');
+            if ($table === $startPosition['table']) {
+                $queryBuilder->where(
+                    $queryBuilder->expr()->gt('uid', $queryBuilder->createNamedParameter($startPosition['uid']))
+                );
+            }
+            $statement = $queryBuilder->execute();
+            $rowCountWithoutUpdate = 0;
+            while ($row = $rowBefore = $statement->fetch()) {
+                foreach ($updaters as $updater) {
+                    $row = $updater->updateTableRow($table, $row);
+                }
+                $updatedFields = array_diff_assoc($row, $rowBefore);
+                if (empty($updatedFields)) {
+                    // Updaters changed no field of that row
+                    $rowCountWithoutUpdate ++;
+                    if ($rowCountWithoutUpdate >= 200) {
+                        // Update startPosition every number rows without update
+                        $startPosition = [
+                            'table' => $table,
+                            'uid' => $row['uid'],
+                        ];
+                        $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
+                        $rowCountWithoutUpdate = 0;
+                    }
+                } else {
+                    $rowCountWithoutUpdate = 0;
+                    $startPosition = [
+                        'table' => $table,
+                        'uid' => $rowBefore['uid'],
+                    ];
+                    if ($connectionForSysRegistry === $connectionForTable) {
+                        // Target table and sys_registry table are on the same connection, use a transaction
+                        $connectionForTable->beginTransaction();
+                        try {
+                            $connectionForTable->update(
+                                $table,
+                                $updatedFields,
+                                [
+                                    'uid' => $rowBefore['uid'],
+                                ]
+                            );
+                            $connectionForTable->update(
+                                'sys_registry',
+                                [
+                                    'entry_value' => serialize($startPosition),
+                                ],
+                                [
+                                    'entry_namespace' => 'installUpdateRows',
+                                    'entry_key' => 'rowUpdatePosition',
+                                ]
+                            );
+                            $connectionForTable->commit();
+                        } catch (\Exception $up) {
+                            $connectionForTable->rollBack();
+                            throw $up;
+                        }
+                    } else {
+                        // Different connections for table and sys_registry -> execute two
+                        // distinct queries  and hope for the best.
+                        $connectionForTable->update(
+                            $table,
+                            $updatedFields,
+                            [
+                                'uid' => $rowBefore['uid'],
+                            ]
+                        );
+                        $connectionForSysRegistry->update(
+                            'sys_registry',
+                            [
+                                'entry_value' => serialize($startPosition),
+                            ],
+                            [
+                                'entry_namespace' => 'installUpdateRows',
+                                'entry_key' => 'rowUpdatePosition',
+                            ]
+                        );
+                    }
+                }
+            }
+        }
+
+        // Ready with updates, remove position information from sys_registry
+        $registry->remove('installUpdateRows', 'rowUpdatePosition');
+        // Mark those row updaters as done
+        foreach ($rowUpdaterInstances as $updater) {
+            $this->setRowUpdaterExecuted($updater);
+        }
+
+        return true;
+    }
+
+    /**
+     * Return an array of class names that are not yet marked as done.
+     *
+     * @return array Class names
+     */
+    protected function getRowUpdatersToExecute(): array
+    {
+        $doneRowUpdater = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
+        return array_diff($this->rowUpdater, $doneRowUpdater);
+    }
+
+    /**
+     * Mark a single updater as done
+     *
+     * @param RowUpdaterInterface $updater
+     */
+    protected function setRowUpdaterExecuted(RowUpdaterInterface $updater)
+    {
+        $registry = GeneralUtility::makeInstance(Registry::class);
+        $doneRowUpdater = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
+        $doneRowUpdater[] = get_class($updater);
+        $registry->set('installUpdateRows', 'rowUpdatersDone', $doneRowUpdater);
+    }
+
+    /**
+     * Return an array with table / uid combination that specifies the start position the
+     * update row process should start with.
+     *
+     * @param string $firstTable Table name of the first TCA in case the start position needs to be initialized
+     * @return array New start position
+     */
+    protected function getStartPosition(string $firstTable): array
+    {
+        $registry = GeneralUtility::makeInstance(Registry::class);
+        $startPosition = $registry->get('installUpdateRows', 'rowUpdaterPosition', []);
+        if (empty($startPosition)) {
+            $startPosition = [
+                'table' => $firstTable,
+                'uid' => 0,
+            ];
+            $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
+        }
+        return $startPosition;
+    }
+}
diff --git a/typo3/sysext/install/Classes/Updates/RowUpdater/RowUpdaterInterface.php b/typo3/sysext/install/Classes/Updates/RowUpdater/RowUpdaterInterface.php
new file mode 100644 (file)
index 0000000..deedbff
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Install\Updates\RowUpdater;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Interface each single row updater must implement.
+ */
+interface RowUpdaterInterface
+{
+    /**
+     * Get a description of this single row updater
+     *
+     * @return string
+     */
+    public function getTitle(): string;
+
+    /**
+     * Return true if this row updater may have updates for given table rows.
+     *
+     * @param string $tableName Given table
+     * @return bool
+     */
+    public function hasPotentialUpdateForTable(string $tableName): bool;
+
+    /**
+     * Update a single row from a table.
+     *
+     * @param string $tableName Given table
+     * @param array $row Given row
+     * @return array Potentially modified row
+     */
+    public function updateTableRow(string $tableName, array $row): array;
+}
index 27ead4d..56668df 100644 (file)
 <f:if condition="{wizardsDone}">
        <form method="post">
                <f:render partial="Action/Common/HiddenFormFields" arguments="{_all}" />
-               <h2>Wizards done</h2>
+               <h2>Wizards marked as done</h2>
+               <p>
+                       Some wizards fully automatically check whether they should be executed, while others just set a flag in
+                       the system registry if they have been executed once. This "I have been executed" flag can be reset by
+                       selecting specific wizards from the list below, so the according wizards show up as possible upgrade
+                       wizards again.
+               </p>
                <table class="table table-striped">
                        <tbody>
                        <f:for each="{wizardsDone}" as="wizardDone">
                                        </td>
                                </tr>
                        </f:for>
+                       <f:for each="{rowUpdatersDone}" as="rowUpdaterDone">
+                               <tr>
+                                       <td>
+                                               <input id="t3-recheck-{rowUpdaterDone.identifier}" type="checkbox" name="install[values][recheckRowUpdater][{rowUpdaterDone.identifier}]" value="1" />
+                                       </td>
+                                       <td>
+                                               Row updater: {rowUpdaterDone.title}
+                                       </td>
+                               </tr>
+                       </f:for>
                        </tbody>
                </table>
                <f:render
index 379841f..3e47e15 100644 (file)
@@ -45,3 +45,5 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][\TYPO3\CMS\In
     = \TYPO3\CMS\Install\Updates\FrontendUserImageUpdateWizard::class;
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][\TYPO3\CMS\Install\Updates\DbalAndAdodbExtractionUpdate::class]
     = \TYPO3\CMS\Install\Updates\DbalAndAdodbExtractionUpdate::class;
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][\TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard::class]
+    = \TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard::class;