0cc2df813c2f4a1e8aef1a02230bbd6653608cdd
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Updates / DatabaseRowsUpdateWizard.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Install\Updates;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17 use TYPO3\CMS\Core\Database\ConnectionPool;
18 use TYPO3\CMS\Core\Registry;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20 use TYPO3\CMS\Install\Updates\RowUpdater\ImageCropUpdater;
21 use TYPO3\CMS\Install\Updates\RowUpdater\L10nModeUpdater;
22 use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface;
23
24 /**
25 * This is a generic updater to migrate content of TCA rows.
26 *
27 * Multiple classes implementing interface "RowUpdateInterface" can be
28 * registered here, each for a specific update purpose.
29 *
30 * The updater fetches each row of all TCA registered tables and
31 * visits the client classes who may modify the row content.
32 *
33 * The updater remembers for each class if it run through, so the updater
34 * will be shown again if a new updater class is registered that has not
35 * been run yet.
36 *
37 * A start position pointer is stored in the registry that is updated during
38 * the run process, so if for instance the PHP process runs into a timeout,
39 * the job can restart at the position it stopped.
40 */
41 class DatabaseRowsUpdateWizard extends AbstractUpdate
42 {
43 /**
44 * @var string Title of this updater
45 */
46 protected $title = 'Execute database migrations on single rows';
47
48 /**
49 * @var array Single classes that may update rows
50 */
51 protected $rowUpdater = [
52 L10nModeUpdater::class,
53 ImageCropUpdater::class,
54 ];
55
56 /**
57 * Checks if an update is needed by looking up in registry if all
58 * registered update row classes are marked as done or not.
59 *
60 * @param string &$description The description for the update
61 * @return bool Whether an update is needed (TRUE) or not (FALSE)
62 */
63 public function checkForUpdate(&$description)
64 {
65 $updateNeeded = false;
66 $rowUpdaterNotExecuted = $this->getRowUpdatersToExecute();
67 if (!empty($rowUpdaterNotExecuted)) {
68 $updateNeeded = true;
69 }
70 if (!$updateNeeded) {
71 return false;
72 }
73
74 $description = 'Some row updaters have not been executed:';
75 foreach ($rowUpdaterNotExecuted as $rowUpdateClassName) {
76 $rowUpdater = GeneralUtility::makeInstance($rowUpdateClassName);
77 if (!$rowUpdater instanceof RowUpdaterInterface) {
78 throw new \RuntimeException(
79 'Row updater must implement RowUpdaterInterface',
80 1484066647
81 );
82 }
83 $description .= '<br />' . htmlspecialchars($rowUpdater->getTitle());
84 }
85
86 return $updateNeeded;
87 }
88
89 /**
90 * Performs the configuration update.
91 *
92 * @param array &$databaseQueries Queries done in this update - not filled for this updater
93 * @param string &$customMessage Custom message
94 * @return bool
95 * @throws \Doctrine\DBAL\ConnectionException
96 * @throws \Exception
97 */
98 public function performUpdate(array &$databaseQueries, &$customMessage)
99 {
100 $registry = GeneralUtility::makeInstance(Registry::class);
101
102 // If rows from the target table that is updated and the sys_registry table are on the
103 // same connection, the row update statement and sys_registry position update will be
104 // handled in a transaction to have an atomic operation in case of errors during execution.
105 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
106 $connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
107
108 /** @var RowUpdaterInterface[] $rowUpdaterInstances */
109 $rowUpdaterInstances = [];
110 // Single row updater instances are created only once for this method giving
111 // them a chance to set up local properties during hasPotentialUpdateForTable()
112 // and using that in updateTableRow()
113 foreach ($this->getRowUpdatersToExecute() as $rowUpdater) {
114 $rowUpdaterInstance = GeneralUtility::makeInstance($rowUpdater);
115 if (!$rowUpdaterInstance instanceof RowUpdaterInterface) {
116 throw new \RuntimeException(
117 'Row updater must implement RowUpdaterInterface',
118 1484071612
119 );
120 }
121 $rowUpdaterInstances[] = $rowUpdaterInstance;
122 }
123
124 // Scope of the row updater is to update all rows that have TCA,
125 // our list of tables is just the list of loaded TCA tables.
126 $listOfAllTables = array_keys($GLOBALS['TCA']);
127
128 // In case the PHP ended for whatever reason, fetch the last position from registry
129 // and throw away all tables before that start point.
130 sort($listOfAllTables);
131 reset($listOfAllTables);
132 $firstTable = current($listOfAllTables);
133 $startPosition = $this->getStartPosition($firstTable);
134 foreach ($listOfAllTables as $table) {
135 if ($table === $startPosition['table']) {
136 break;
137 } else {
138 unset($listOfAllTables[$table]);
139 }
140 }
141
142 // Ask each row updater if it potentially has field updates for rows of a table
143 $tableToUpdaterList = [];
144 foreach ($listOfAllTables as $table) {
145 foreach ($rowUpdaterInstances as $updater) {
146 if ($updater->hasPotentialUpdateForTable($table)) {
147 if (!is_array($tableToUpdaterList[$table])) {
148 $tableToUpdaterList[$table] = [];
149 }
150 $tableToUpdaterList[$table][] = $updater;
151 }
152 }
153 }
154
155 // Iterate through all rows of all tables that have potential row updaters attached,
156 // feed each single row to each updater and finally update each row in database if
157 // a row updater changed a fields
158 foreach ($tableToUpdaterList as $table => $updaters) {
159 /** @var RowUpdaterInterface[] $updaters */
160 $connectionForTable = $connectionPool->getConnectionForTable($table);
161 $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
162 $queryBuilder->getRestrictions()->removeAll();
163 $queryBuilder->select('*')
164 ->from($table)
165 ->orderBy('uid');
166 if ($table === $startPosition['table']) {
167 $queryBuilder->where(
168 $queryBuilder->expr()->gt('uid', $queryBuilder->createNamedParameter($startPosition['uid']))
169 );
170 }
171 $statement = $queryBuilder->execute();
172 $rowCountWithoutUpdate = 0;
173 while ($row = $rowBefore = $statement->fetch()) {
174 foreach ($updaters as $updater) {
175 $row = $updater->updateTableRow($table, $row);
176 }
177 $updatedFields = array_diff_assoc($row, $rowBefore);
178 if (empty($updatedFields)) {
179 // Updaters changed no field of that row
180 $rowCountWithoutUpdate ++;
181 if ($rowCountWithoutUpdate >= 200) {
182 // Update startPosition if there were many rows without data change
183 $startPosition = [
184 'table' => $table,
185 'uid' => $row['uid'],
186 ];
187 $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
188 $rowCountWithoutUpdate = 0;
189 }
190 } else {
191 $rowCountWithoutUpdate = 0;
192 $startPosition = [
193 'table' => $table,
194 'uid' => $rowBefore['uid'],
195 ];
196 if ($connectionForSysRegistry === $connectionForTable) {
197 // Target table and sys_registry table are on the same connection, use a transaction
198 $connectionForTable->beginTransaction();
199 try {
200 $connectionForTable->update(
201 $table,
202 $updatedFields,
203 [
204 'uid' => $rowBefore['uid'],
205 ]
206 );
207 $connectionForTable->update(
208 'sys_registry',
209 [
210 'entry_value' => serialize($startPosition),
211 ],
212 [
213 'entry_namespace' => 'installUpdateRows',
214 'entry_key' => 'rowUpdatePosition',
215 ]
216 );
217 $connectionForTable->commit();
218 } catch (\Exception $up) {
219 $connectionForTable->rollBack();
220 throw $up;
221 }
222 } else {
223 // Different connections for table and sys_registry -> execute two
224 // distinct queries and hope for the best.
225 $connectionForTable->update(
226 $table,
227 $updatedFields,
228 [
229 'uid' => $rowBefore['uid'],
230 ]
231 );
232 $connectionForSysRegistry->update(
233 'sys_registry',
234 [
235 'entry_value' => serialize($startPosition),
236 ],
237 [
238 'entry_namespace' => 'installUpdateRows',
239 'entry_key' => 'rowUpdatePosition',
240 ]
241 );
242 }
243 }
244 }
245 }
246
247 // Ready with updates, remove position information from sys_registry
248 $registry->remove('installUpdateRows', 'rowUpdatePosition');
249 // Mark row updaters that were executed as done
250 foreach ($rowUpdaterInstances as $updater) {
251 $this->setRowUpdaterExecuted($updater);
252 }
253
254 return true;
255 }
256
257 /**
258 * Return an array of class names that are not yet marked as done.
259 *
260 * @return array Class names
261 */
262 protected function getRowUpdatersToExecute(): array
263 {
264 $doneRowUpdater = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
265 return array_diff($this->rowUpdater, $doneRowUpdater);
266 }
267
268 /**
269 * Mark a single updater as done
270 *
271 * @param RowUpdaterInterface $updater
272 */
273 protected function setRowUpdaterExecuted(RowUpdaterInterface $updater)
274 {
275 $registry = GeneralUtility::makeInstance(Registry::class);
276 $doneRowUpdater = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
277 $doneRowUpdater[] = get_class($updater);
278 $registry->set('installUpdateRows', 'rowUpdatersDone', $doneRowUpdater);
279 }
280
281 /**
282 * Return an array with table / uid combination that specifies the start position the
283 * update row process should start with.
284 *
285 * @param string $firstTable Table name of the first TCA in case the start position needs to be initialized
286 * @return array New start position
287 */
288 protected function getStartPosition(string $firstTable): array
289 {
290 $registry = GeneralUtility::makeInstance(Registry::class);
291 $startPosition = $registry->get('installUpdateRows', 'rowUpdaterPosition', []);
292 if (empty($startPosition)) {
293 $startPosition = [
294 'table' => $firstTable,
295 'uid' => 0,
296 ];
297 $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
298 }
299 return $startPosition;
300 }
301 }