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