Test Failed
Branch master (7b1793)
by Tymoteusz
15:35
created

DatabaseRowsUpdateWizard::checkForUpdate()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 15
nc 8
nop 1
dl 0
loc 24
rs 8.5125
c 0
b 0
f 0
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 $key => $table) {
138
            if ($table === $startPosition['table']) {
139
                break;
140
            }
141
            unset($listOfAllTables[$key]);
142
        }
143
144
        // Ask each row updater if it potentially has field updates for rows of a table
145
        $tableToUpdaterList = [];
146
        foreach ($listOfAllTables as $table) {
147
            foreach ($rowUpdaterInstances as $updater) {
148
                if ($updater->hasPotentialUpdateForTable($table)) {
149
                    if (!is_array($tableToUpdaterList[$table])) {
150
                        $tableToUpdaterList[$table] = [];
151
                    }
152
                    $tableToUpdaterList[$table][] = $updater;
153
                }
154
            }
155
        }
156
157
        // Iterate through all rows of all tables that have potential row updaters attached,
158
        // feed each single row to each updater and finally update each row in database if
159
        // a row updater changed a fields
160
        foreach ($tableToUpdaterList as $table => $updaters) {
161
            /** @var RowUpdaterInterface[] $updaters */
162
            $connectionForTable = $connectionPool->getConnectionForTable($table);
163
            $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
164
            $queryBuilder->getRestrictions()->removeAll();
165
            $queryBuilder->select('*')
166
                ->from($table)
167
                ->orderBy('uid');
168
            if ($table === $startPosition['table']) {
169
                $queryBuilder->where(
170
                    $queryBuilder->expr()->gt('uid', $queryBuilder->createNamedParameter($startPosition['uid']))
171
                );
172
            }
173
            $statement = $queryBuilder->execute();
174
            $rowCountWithoutUpdate = 0;
175
            while ($row = $rowBefore = $statement->fetch()) {
176
                foreach ($updaters as $updater) {
177
                    $row = $updater->updateTableRow($table, $row);
178
                }
179
                $updatedFields = array_diff_assoc($row, $rowBefore);
180
                if (empty($updatedFields)) {
181
                    // Updaters changed no field of that row
182
                    $rowCountWithoutUpdate ++;
183
                    if ($rowCountWithoutUpdate >= 200) {
184
                        // Update startPosition if there were many rows without data change
185
                        $startPosition = [
186
                            'table' => $table,
187
                            'uid' => $row['uid'],
188
                        ];
189
                        $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
190
                        $rowCountWithoutUpdate = 0;
191
                    }
192
                } else {
193
                    $rowCountWithoutUpdate = 0;
194
                    $startPosition = [
195
                        'table' => $table,
196
                        'uid' => $rowBefore['uid'],
197
                    ];
198
                    if ($connectionForSysRegistry === $connectionForTable) {
199
                        // Target table and sys_registry table are on the same connection, use a transaction
200
                        $connectionForTable->beginTransaction();
201
                        try {
202
                            $connectionForTable->update(
203
                                $table,
204
                                $updatedFields,
205
                                [
206
                                    'uid' => $rowBefore['uid'],
207
                                ]
208
                            );
209
                            $connectionForTable->update(
210
                                'sys_registry',
211
                                [
212
                                    'entry_value' => serialize($startPosition),
213
                                ],
214
                                [
215
                                    'entry_namespace' => 'installUpdateRows',
216
                                    'entry_key' => 'rowUpdatePosition',
217
                                ]
218
                            );
219
                            $connectionForTable->commit();
220
                        } catch (\Exception $up) {
221
                            $connectionForTable->rollBack();
222
                            throw $up;
223
                        }
224
                    } else {
225
                        // Different connections for table and sys_registry -> execute two
226
                        // distinct queries and hope for the best.
227
                        $connectionForTable->update(
228
                            $table,
229
                            $updatedFields,
230
                            [
231
                                'uid' => $rowBefore['uid'],
232
                            ]
233
                        );
234
                        $connectionForSysRegistry->update(
235
                            'sys_registry',
236
                            [
237
                                'entry_value' => serialize($startPosition),
238
                            ],
239
                            [
240
                                'entry_namespace' => 'installUpdateRows',
241
                                'entry_key' => 'rowUpdatePosition',
242
                            ]
243
                        );
244
                    }
245
                }
246
            }
247
        }
248
249
        // Ready with updates, remove position information from sys_registry
250
        $registry->remove('installUpdateRows', 'rowUpdatePosition');
251
        // Mark row updaters that were executed as done
252
        foreach ($rowUpdaterInstances as $updater) {
253
            $this->setRowUpdaterExecuted($updater);
254
        }
255
256
        return true;
257
    }
258
259
    /**
260
     * Return an array of class names that are not yet marked as done.
261
     *
262
     * @return array Class names
263
     */
264
    protected function getRowUpdatersToExecute(): array
265
    {
266
        $doneRowUpdater = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
267
        return array_diff($this->rowUpdater, $doneRowUpdater);
268
    }
269
270
    /**
271
     * Mark a single updater as done
272
     *
273
     * @param RowUpdaterInterface $updater
274
     */
275
    protected function setRowUpdaterExecuted(RowUpdaterInterface $updater)
276
    {
277
        $registry = GeneralUtility::makeInstance(Registry::class);
278
        $doneRowUpdater = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
279
        $doneRowUpdater[] = get_class($updater);
280
        $registry->set('installUpdateRows', 'rowUpdatersDone', $doneRowUpdater);
281
    }
282
283
    /**
284
     * Return an array with table / uid combination that specifies the start position the
285
     * update row process should start with.
286
     *
287
     * @param string $firstTable Table name of the first TCA in case the start position needs to be initialized
288
     * @return array New start position
289
     */
290
    protected function getStartPosition(string $firstTable): array
291
    {
292
        $registry = GeneralUtility::makeInstance(Registry::class);
293
        $startPosition = $registry->get('installUpdateRows', 'rowUpdatePosition', []);
294
        if (empty($startPosition)) {
295
            $startPosition = [
296
                'table' => $firstTable,
297
                'uid' => 0,
298
            ];
299
            $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
300
        }
301
        return $startPosition;
302
    }
303
}
304