[TASK] Re-style record list search box
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Updates / DatabaseRowsUpdateWizard.php
1 <?php
2
3 declare(strict_types=1);
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 namespace TYPO3\CMS\Install\Updates;
19
20 use Doctrine\DBAL\Platforms\SQLServer2012Platform as SQLServerPlatform;
21 use TYPO3\CMS\Core\Database\Connection;
22 use TYPO3\CMS\Core\Database\ConnectionPool;
23 use TYPO3\CMS\Core\Registry;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25 use TYPO3\CMS\Install\Updates\RowUpdater\L18nDiffsourceToJsonMigration;
26 use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface;
27 use TYPO3\CMS\Install\Updates\RowUpdater\WorkspaceMovePlaceholderRemovalMigration;
28 use TYPO3\CMS\Install\Updates\RowUpdater\WorkspaceNewPlaceholderRemovalMigration;
29 use TYPO3\CMS\Install\Updates\RowUpdater\WorkspaceVersionRecordsMigration;
30
31 /**
32 * This is a generic updater to migrate content of TCA rows.
33 *
34 * Multiple classes implementing interface "RowUpdateInterface" can be
35 * registered here, each for a specific update purpose.
36 *
37 * The updater fetches each row of all TCA registered tables and
38 * visits the client classes who may modify the row content.
39 *
40 * The updater remembers for each class if it run through, so the updater
41 * will be shown again if a new updater class is registered that has not
42 * been run yet.
43 *
44 * A start position pointer is stored in the registry that is updated during
45 * the run process, so if for instance the PHP process runs into a timeout,
46 * the job can restart at the position it stopped.
47 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
48 */
49 class DatabaseRowsUpdateWizard implements UpgradeWizardInterface, RepeatableInterface
50 {
51 /**
52 * @var array Single classes that may update rows
53 */
54 protected $rowUpdater = [
55 WorkspaceVersionRecordsMigration::class,
56 L18nDiffsourceToJsonMigration::class,
57 WorkspaceMovePlaceholderRemovalMigration::class,
58 WorkspaceNewPlaceholderRemovalMigration::class,
59 ];
60
61 /**
62 * @internal
63 * @return string[]
64 */
65 public function getAvailableRowUpdater(): array
66 {
67 return $this->rowUpdater;
68 }
69
70 /**
71 * @return string Unique identifier of this updater
72 */
73 public function getIdentifier(): string
74 {
75 return 'databaseRowsUpdateWizard';
76 }
77
78 /**
79 * @return string Title of this updater
80 */
81 public function getTitle(): string
82 {
83 return 'Execute database migrations on single rows';
84 }
85
86 /**
87 * @return string Longer description of this updater
88 * @throws \RuntimeException
89 */
90 public function getDescription(): string
91 {
92 $rowUpdaterNotExecuted = $this->getRowUpdatersToExecute();
93 $description = 'Row updaters that have not been executed:';
94 foreach ($rowUpdaterNotExecuted as $rowUpdateClassName) {
95 $rowUpdater = GeneralUtility::makeInstance($rowUpdateClassName);
96 if (!$rowUpdater instanceof RowUpdaterInterface) {
97 throw new \RuntimeException(
98 'Row updater must implement RowUpdaterInterface',
99 1484066647
100 );
101 }
102 $description .= LF . $rowUpdater->getTitle();
103 }
104 return $description;
105 }
106
107 /**
108 * @return bool True if at least one row updater is not marked done
109 */
110 public function updateNecessary(): bool
111 {
112 return !empty($this->getRowUpdatersToExecute());
113 }
114
115 /**
116 * @return string[] All new fields and tables must exist
117 */
118 public function getPrerequisites(): array
119 {
120 return [
121 DatabaseUpdatedPrerequisite::class
122 ];
123 }
124
125 /**
126 * Performs the configuration update.
127 *
128 * @return bool
129 * @throws \Doctrine\DBAL\ConnectionException
130 * @throws \Exception
131 */
132 public function executeUpdate(): bool
133 {
134 $registry = GeneralUtility::makeInstance(Registry::class);
135
136 // If rows from the target table that is updated and the sys_registry table are on the
137 // same connection, the row update statement and sys_registry position update will be
138 // handled in a transaction to have an atomic operation in case of errors during execution.
139 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
140 $connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
141
142 /** @var RowUpdaterInterface[] $rowUpdaterInstances */
143 $rowUpdaterInstances = [];
144 // Single row updater instances are created only once for this method giving
145 // them a chance to set up local properties during hasPotentialUpdateForTable()
146 // and using that in updateTableRow()
147 foreach ($this->getRowUpdatersToExecute() as $rowUpdater) {
148 $rowUpdaterInstance = GeneralUtility::makeInstance($rowUpdater);
149 if (!$rowUpdaterInstance instanceof RowUpdaterInterface) {
150 throw new \RuntimeException(
151 'Row updater must implement RowUpdaterInterface',
152 1484071612
153 );
154 }
155 $rowUpdaterInstances[] = $rowUpdaterInstance;
156 }
157
158 // Scope of the row updater is to update all rows that have TCA,
159 // our list of tables is just the list of loaded TCA tables.
160 /** @var string[] $listOfAllTables */
161 $listOfAllTables = array_keys($GLOBALS['TCA']);
162
163 // In case the PHP ended for whatever reason, fetch the last position from registry
164 // and throw away all tables before that start point.
165 sort($listOfAllTables);
166 reset($listOfAllTables);
167 $firstTable = current($listOfAllTables) ?: '';
168 $startPosition = $this->getStartPosition($firstTable);
169 foreach ($listOfAllTables as $key => $table) {
170 if ($table === $startPosition['table']) {
171 break;
172 }
173 unset($listOfAllTables[$key]);
174 }
175
176 // Ask each row updater if it potentially has field updates for rows of a table
177 $tableToUpdaterList = [];
178 foreach ($listOfAllTables as $table) {
179 foreach ($rowUpdaterInstances as $updater) {
180 if ($updater->hasPotentialUpdateForTable($table)) {
181 if (!isset($tableToUpdaterList[$table]) || !is_array($tableToUpdaterList[$table])) {
182 $tableToUpdaterList[$table] = [];
183 }
184 $tableToUpdaterList[$table][] = $updater;
185 }
186 }
187 }
188
189 // Iterate through all rows of all tables that have potential row updaters attached,
190 // feed each single row to each updater and finally update each row in database if
191 // a row updater changed a fields
192 foreach ($tableToUpdaterList as $table => $updaters) {
193 /** @var RowUpdaterInterface[] $updaters */
194 $connectionForTable = $connectionPool->getConnectionForTable($table);
195 $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
196 $queryBuilder->getRestrictions()->removeAll();
197 $queryBuilder->select('*')
198 ->from($table)
199 ->orderBy('uid');
200 if ($table === $startPosition['table']) {
201 $queryBuilder->where(
202 $queryBuilder->expr()->gt('uid', $queryBuilder->createNamedParameter($startPosition['uid']))
203 );
204 }
205 $statement = $queryBuilder->execute();
206 $rowCountWithoutUpdate = 0;
207 while ($row = $rowBefore = $statement->fetch()) {
208 foreach ($updaters as $updater) {
209 $row = $updater->updateTableRow($table, $row);
210 }
211 $updatedFields = array_diff_assoc($row, $rowBefore);
212 if (empty($updatedFields)) {
213 // Updaters changed no field of that row
214 $rowCountWithoutUpdate++;
215 if ($rowCountWithoutUpdate >= 200) {
216 // Update startPosition if there were many rows without data change
217 $startPosition = [
218 'table' => $table,
219 'uid' => $row['uid'],
220 ];
221 $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
222 $rowCountWithoutUpdate = 0;
223 }
224 } else {
225 $rowCountWithoutUpdate = 0;
226 $startPosition = [
227 'table' => $table,
228 'uid' => $rowBefore['uid'],
229 ];
230 if ($connectionForSysRegistry === $connectionForTable
231 && !($connectionForSysRegistry->getDatabasePlatform() instanceof SQLServerPlatform)
232 ) {
233 // Target table and sys_registry table are on the same connection and not mssql, use a transaction
234 $connectionForTable->beginTransaction();
235 try {
236 $this->updateOrDeleteRow(
237 $connectionForTable,
238 $connectionForTable,
239 $table,
240 (int)$rowBefore['uid'],
241 $updatedFields,
242 $startPosition
243 );
244 $connectionForTable->commit();
245 } catch (\Exception $up) {
246 $connectionForTable->rollBack();
247 throw $up;
248 }
249 } else {
250 // Either different connections for table and sys_registry, or mssql.
251 // SqlServer can not run a transaction for a table if the same table is queried
252 // currently - our above ->fetch() main loop.
253 // So, execute two distinct queries and hope for the best.
254 $this->updateOrDeleteRow(
255 $connectionForTable,
256 $connectionForSysRegistry,
257 $table,
258 (int)$rowBefore['uid'],
259 $updatedFields,
260 $startPosition
261 );
262 }
263 }
264 }
265 }
266
267 // Ready with updates, remove position information from sys_registry
268 $registry->remove('installUpdateRows', 'rowUpdatePosition');
269 // Mark row updaters that were executed as done
270 foreach ($rowUpdaterInstances as $updater) {
271 $this->setRowUpdaterExecuted($updater);
272 }
273
274 return true;
275 }
276
277 /**
278 * Return an array of class names that are not yet marked as done.
279 *
280 * @return array Class names
281 */
282 protected function getRowUpdatersToExecute(): array
283 {
284 $doneRowUpdater = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
285 return array_diff($this->rowUpdater, $doneRowUpdater);
286 }
287
288 /**
289 * Mark a single updater as done
290 *
291 * @param RowUpdaterInterface $updater
292 */
293 protected function setRowUpdaterExecuted(RowUpdaterInterface $updater)
294 {
295 $registry = GeneralUtility::makeInstance(Registry::class);
296 $doneRowUpdater = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
297 $doneRowUpdater[] = get_class($updater);
298 $registry->set('installUpdateRows', 'rowUpdatersDone', $doneRowUpdater);
299 }
300
301 /**
302 * Return an array with table / uid combination that specifies the start position the
303 * update row process should start with.
304 *
305 * @param string $firstTable Table name of the first TCA in case the start position needs to be initialized
306 * @return array New start position
307 */
308 protected function getStartPosition(string $firstTable): array
309 {
310 $registry = GeneralUtility::makeInstance(Registry::class);
311 $startPosition = $registry->get('installUpdateRows', 'rowUpdatePosition', []);
312 if (empty($startPosition)) {
313 $startPosition = [
314 'table' => $firstTable,
315 'uid' => 0,
316 ];
317 $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
318 }
319 return $startPosition;
320 }
321
322 /**
323 * @param Connection $connectionForTable
324 * @param string $table
325 * @param array $updatedFields
326 * @param int $uid
327 * @param Connection $connectionForSysRegistry
328 * @param array $startPosition
329 */
330 protected function updateOrDeleteRow(Connection $connectionForTable, Connection $connectionForSysRegistry, string $table, int $uid, array $updatedFields, array $startPosition): void
331 {
332 $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? null;
333 if ($deleteField === null && $updatedFields['deleted'] === 1) {
334 $connectionForTable->delete(
335 $table,
336 [
337 'uid' => $uid,
338 ]
339 );
340 } else {
341 $connectionForTable->update(
342 $table,
343 $updatedFields,
344 [
345 'uid' => $uid,
346 ]
347 );
348 }
349 $connectionForSysRegistry->update(
350 'sys_registry',
351 [
352 'entry_value' => serialize($startPosition),
353 ],
354 [
355 'entry_namespace' => 'installUpdateRows',
356 'entry_key' => 'rowUpdatePosition',
357 ],
358 [
359 // Needs to be declared LOB, so MSSQL can handle the conversion from string (nvarchar) to blob (varbinary)
360 'entry_value' => \PDO::PARAM_LOB,
361 'entry_namespace' => \PDO::PARAM_STR,
362 'entry_key' => \PDO::PARAM_STR,
363 ]
364 );
365 }
366 }