Completed
Push — master ( 238fb0...a9a2cf )
by
unknown
15:40
created

UpgradeWizardsService   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 424
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 203
dl 0
loc 424
rs 4.5599
c 0
b 0
f 0
wmc 58

15 Methods

Rating   Name   Duplication   Size   Complexity  
A isWizardDone() 0 6 1
A markWizardAsDone() 0 6 1
A listOfRowUpdatersDone() 0 25 4
A listOfWizardsDone() 0 15 3
A assertIdentifierIsValid() 0 4 4
A getUpgradeWizardsList() 0 12 3
A isDatabaseCharsetUtf8() 0 28 2
A getWizardInformationByIdentifier() 0 27 4
B markWizardUndone() 0 29 8
A getWizardUserInput() 0 39 4
B getBlockingDatabaseAdds() 0 44 9
A addMissingTablesAndFields() 0 6 1
B executeWizard() 0 63 11
A setDatabaseCharsetUtf8() 0 6 1
A __construct() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like UpgradeWizardsService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UpgradeWizardsService, and based on these observations, apply Extract Interface, too.

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\Service;
19
20
use Doctrine\DBAL\Platforms\MySqlPlatform;
21
use Doctrine\DBAL\Schema\Column;
22
use Doctrine\DBAL\Schema\Table;
23
use Symfony\Component\Console\Output\Output;
24
use Symfony\Component\Console\Output\StreamOutput;
25
use TYPO3\CMS\Core\Database\ConnectionPool;
26
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
27
use TYPO3\CMS\Core\Database\Schema\SqlReader;
28
use TYPO3\CMS\Core\Messaging\FlashMessage;
29
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
30
use TYPO3\CMS\Core\Registry;
31
use TYPO3\CMS\Core\Utility\GeneralUtility;
32
use TYPO3\CMS\Install\Updates\ChattyInterface;
33
use TYPO3\CMS\Install\Updates\ConfirmableInterface;
34
use TYPO3\CMS\Install\Updates\RepeatableInterface;
35
use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface;
36
use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
37
38
/**
39
 * Service class helping managing upgrade wizards
40
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
41
 */
42
class UpgradeWizardsService
43
{
44
    /**
45
     * @var StreamOutput
46
     */
47
    private $output;
48
49
    public function __construct()
50
    {
51
        $fileName = 'php://temp';
52
        if (($stream = fopen($fileName, 'wb')) === false) {
53
            throw new \RuntimeException('Unable to open stream "' . $fileName . '"', 1598341765);
54
        }
55
        $this->output = new StreamOutput($stream, Output::VERBOSITY_NORMAL, false);
56
    }
57
58
    /**
59
     * @return array List of wizards marked as done in registry
60
     */
61
    public function listOfWizardsDone(): array
62
    {
63
        $wizardsDoneInRegistry = [];
64
        $registry = GeneralUtility::makeInstance(Registry::class);
65
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $className) {
66
            if ($registry->get('installUpdate', $className, false)) {
67
                $wizardInstance = GeneralUtility::makeInstance($className);
68
                $wizardsDoneInRegistry[] = [
69
                    'class' => $className,
70
                    'identifier' => $identifier,
71
                    'title' => $wizardInstance->getTitle(),
72
                ];
73
            }
74
        }
75
        return $wizardsDoneInRegistry;
76
    }
77
78
    /**
79
     * @return array List of row updaters marked as done in registry
80
     * @throws \RuntimeException
81
     */
82
    public function listOfRowUpdatersDone(): array
83
    {
84
        $registry = GeneralUtility::makeInstance(Registry::class);
85
        $rowUpdatersDoneClassNames = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
86
        $rowUpdatersDone = [];
87
        foreach ($rowUpdatersDoneClassNames as $rowUpdaterClassName) {
88
            // Silently skip non existing DatabaseRowsUpdateWizards
89
            if (!class_exists($rowUpdaterClassName)) {
90
                continue;
91
            }
92
            /** @var RowUpdaterInterface $rowUpdater */
93
            $rowUpdater = GeneralUtility::makeInstance($rowUpdaterClassName);
94
            if (!$rowUpdater instanceof RowUpdaterInterface) {
95
                throw new \RuntimeException(
96
                    'Row updater must implement RowUpdaterInterface',
97
                    1484152906
98
                );
99
            }
100
            $rowUpdatersDone[] = [
101
                'class' => $rowUpdaterClassName,
102
                'identifier' => $rowUpdaterClassName,
103
                'title' => $rowUpdater->getTitle(),
104
            ];
105
        }
106
        return $rowUpdatersDone;
107
    }
108
109
    /**
110
     * Mark one wizard as undone. This can be a "casual" wizard
111
     * or a single "row updater".
112
     *
113
     * @param string $identifier Wizard or RowUpdater identifier
114
     * @return bool True if wizard has been marked as undone
115
     * @throws \RuntimeException
116
     */
117
    public function markWizardUndone(string $identifier): bool
118
    {
119
        $this->assertIdentifierIsValid($identifier);
120
121
        $registry = GeneralUtility::makeInstance(Registry::class);
122
        $aWizardHasBeenMarkedUndone = false;
123
        foreach ($this->listOfWizardsDone() as $wizard) {
124
            if ($wizard['identifier'] === $identifier) {
125
                $aWizardHasBeenMarkedUndone = true;
126
                $registry->set('installUpdate', $wizard['class'], 0);
127
            }
128
        }
129
        if (!$aWizardHasBeenMarkedUndone) {
130
            $rowUpdatersDoneList = $this->listOfRowUpdatersDone();
131
            $registryArray = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
132
            foreach ($rowUpdatersDoneList as $rowUpdater) {
133
                if ($rowUpdater['identifier'] === $identifier) {
134
                    $aWizardHasBeenMarkedUndone = true;
135
                    foreach ($registryArray as $rowUpdaterMarkedAsDonePosition => $rowUpdaterMarkedAsDone) {
136
                        if ($rowUpdaterMarkedAsDone === $rowUpdater['class']) {
137
                            unset($registryArray[$rowUpdaterMarkedAsDonePosition]);
138
                            break;
139
                        }
140
                    }
141
                    $registry->set('installUpdateRows', 'rowUpdatersDone', $registryArray);
142
                }
143
            }
144
        }
145
        return $aWizardHasBeenMarkedUndone;
146
    }
147
148
    /**
149
     * Get a list of tables, single columns and indexes to add.
150
     *
151
     * @return array Array with possible keys "tables", "columns", "indexes"
152
     */
153
    public function getBlockingDatabaseAdds(): array
154
    {
155
        $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
156
        $databaseDefinitions = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
157
158
        $schemaMigrator = GeneralUtility::makeInstance(SchemaMigrator::class);
159
        $databaseDifferences = $schemaMigrator->getSchemaDiffs($databaseDefinitions);
160
161
        $adds = [];
162
        foreach ($databaseDifferences as $schemaDiff) {
163
            foreach ($schemaDiff->newTables as $newTable) {
164
                /** @var Table $newTable */
165
                if (!is_array($adds['tables'])) {
166
                    $adds['tables'] = [];
167
                }
168
                $adds['tables'][] = [
169
                    'table' => $newTable->getName(),
170
                ];
171
            }
172
            foreach ($schemaDiff->changedTables as $changedTable) {
173
                foreach ($changedTable->addedColumns as $addedColumn) {
174
                    /** @var Column $addedColumn */
175
                    if (!is_array($adds['columns'])) {
176
                        $adds['columns'] = [];
177
                    }
178
                    $adds['columns'][] = [
179
                        'table' => $changedTable->name,
180
                        'field' => $addedColumn->getName(),
181
                    ];
182
                }
183
                foreach ($changedTable->addedIndexes as $addedIndex) {
184
                    /** $var Index $addedIndex */
185
                    if (!is_array($adds['indexes'])) {
186
                        $adds['indexes'] = [];
187
                    }
188
                    $adds['indexes'][] = [
189
                        'table' => $changedTable->name,
190
                        'index' => $addedIndex->getName(),
191
                    ];
192
                }
193
            }
194
        }
195
196
        return $adds;
197
    }
198
199
    /**
200
     * Add missing tables, indexes and fields to DB.
201
     */
202
    public function addMissingTablesAndFields(): array
203
    {
204
        $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
205
        $databaseDefinitions = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
206
        $schemaMigrator = GeneralUtility::makeInstance(SchemaMigrator::class);
207
        return $schemaMigrator->install($databaseDefinitions, true);
208
    }
209
210
    /**
211
     * True if DB main charset on mysql is utf8
212
     *
213
     * @return bool True if charset is ok
214
     */
215
    public function isDatabaseCharsetUtf8(): bool
216
    {
217
        /** @var \TYPO3\CMS\Core\Database\Connection $connection */
218
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)
219
            ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
220
221
        $isDefaultConnectionMysql = ($connection->getDatabasePlatform() instanceof MySqlPlatform);
222
223
        if (!$isDefaultConnectionMysql) {
224
            // Not tested on non mysql
225
            $charsetOk = true;
226
        } else {
227
            $queryBuilder = $connection->createQueryBuilder();
228
            $charset = (string)$queryBuilder->select('DEFAULT_CHARACTER_SET_NAME')
229
                ->from('information_schema.SCHEMATA')
230
                ->where(
231
                    $queryBuilder->expr()->eq(
232
                        'SCHEMA_NAME',
233
                        $queryBuilder->createNamedParameter($connection->getDatabase(), \PDO::PARAM_STR)
234
                    )
235
                )
236
                ->setMaxResults(1)
237
                ->execute()
238
                ->fetchColumn();
239
            // check if database charset is utf-8, also allows utf8mb4
240
            $charsetOk = strpos($charset, 'utf8') === 0;
241
        }
242
        return $charsetOk;
243
    }
244
245
    /**
246
     * Set default connection MySQL database charset to utf8.
247
     * Should be called only *if* default database connection is actually MySQL
248
     */
249
    public function setDatabaseCharsetUtf8()
250
    {
251
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)
252
            ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
253
        $sql = 'ALTER DATABASE ' . $connection->quoteIdentifier($connection->getDatabase()) . ' CHARACTER SET utf8';
254
        $connection->exec($sql);
255
    }
256
257
    /**
258
     * Get list of registered upgrade wizards not marked done.
259
     *
260
     * @return array List of upgrade wizards in correct order with detail information
261
     */
262
    public function getUpgradeWizardsList(): array
263
    {
264
        $wizards = [];
265
        foreach (array_keys($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']) as $identifier) {
266
            $identifier = (string)$identifier;
267
            if ($this->isWizardDone($identifier)) {
268
                continue;
269
            }
270
271
            $wizards[] = $this->getWizardInformationByIdentifier($identifier);
272
        }
273
        return $wizards;
274
    }
275
276
    public function getWizardInformationByIdentifier(string $identifier): array
277
    {
278
        if (class_exists($identifier)) {
279
            $class = $identifier;
280
        } else {
281
            $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
282
        }
283
        /** @var UpgradeWizardInterface $wizardInstance */
284
        $wizardInstance = GeneralUtility::makeInstance($class);
285
        $explanation = '';
286
287
        // $explanation is changed by reference in Update objects!
288
        $shouldRenderWizard = false;
289
        if ($wizardInstance instanceof UpgradeWizardInterface) {
0 ignored issues
show
introduced by
$wizardInstance is always a sub-type of TYPO3\CMS\Install\Updates\UpgradeWizardInterface.
Loading history...
290
            if ($wizardInstance instanceof ChattyInterface) {
291
                $wizardInstance->setOutput($this->output);
292
            }
293
            $shouldRenderWizard = $wizardInstance->updateNecessary();
294
            $explanation = $wizardInstance->getDescription();
295
        }
296
297
        return [
298
            'class' => $class,
299
            'identifier' => $identifier,
300
            'title' => $wizardInstance->getTitle(),
301
            'shouldRenderWizard' => $shouldRenderWizard,
302
            'explanation' => $explanation,
303
        ];
304
    }
305
306
    /**
307
     * Execute the "get user input" step of a wizard
308
     *
309
     * @param string $identifier
310
     * @return array
311
     * @throws \RuntimeException
312
     */
313
    public function getWizardUserInput(string $identifier): array
314
    {
315
        $this->assertIdentifierIsValid($identifier);
316
317
        $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
318
        $updateObject = GeneralUtility::makeInstance($class);
319
        $wizardHtml = '';
320
        if ($updateObject instanceof UpgradeWizardInterface && $updateObject instanceof ConfirmableInterface) {
321
            $markup = [];
322
            $radioAttributes = [
323
                'type' => 'radio',
324
                'name' => 'install[values][' . $updateObject->getIdentifier() . '][install]',
325
                'value' => '0'
326
            ];
327
            $markup[] = '<div class="panel panel-danger">';
328
            $markup[] = '   <div class="panel-heading">';
329
            $markup[] = htmlspecialchars($updateObject->getConfirmation()->getTitle());
330
            $markup[] = '    </div>';
331
            $markup[] = '    <div class="panel-body">';
332
            $markup[] = '        <p>' . nl2br(htmlspecialchars($updateObject->getConfirmation()->getMessage())) . '</p>';
333
            $markup[] = '        <div class="btn-group" data-toggle="buttons">';
334
            if (!$updateObject->getConfirmation()->isRequired()) {
335
                $markup[] = '        <label class="btn btn-default active"><input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . ' checked="checked" />' . $updateObject->getConfirmation()->getDeny() . '</label>';
336
            }
337
            $radioAttributes['value'] = '1';
338
            $markup[] = '            <label class="btn btn-default"><input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . ' />' . $updateObject->getConfirmation()->getConfirm() . '</label>';
339
            $markup[] = '        </div>';
340
            $markup[] = '    </div>';
341
            $markup[] = '</div>';
342
            $wizardHtml = implode('', $markup);
343
        }
344
345
        $result = [
346
            'identifier' => $identifier,
347
            'title' => $updateObject->getTitle(),
348
            'wizardHtml' => $wizardHtml,
349
        ];
350
351
        return $result;
352
    }
353
354
    /**
355
     * Execute a single update wizard
356
     *
357
     * @param string $identifier
358
     * @return FlashMessageQueue
359
     * @throws \RuntimeException
360
     */
361
    public function executeWizard(string $identifier): FlashMessageQueue
362
    {
363
        $performResult = false;
364
        $this->assertIdentifierIsValid($identifier);
365
366
        $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
367
        $updateObject = GeneralUtility::makeInstance($class);
368
369
        if ($updateObject instanceof ChattyInterface) {
370
            $updateObject->setOutput($this->output);
371
        }
372
        $messages = new FlashMessageQueue('install');
373
374
        if ($updateObject instanceof UpgradeWizardInterface) {
375
            $requestParams = GeneralUtility::_GP('install');
376
            if ($updateObject instanceof ConfirmableInterface) {
377
                // value is set in request but is empty
378
                $isSetButEmpty = isset($requestParams['values'][$updateObject->getIdentifier()]['install'])
379
                    && empty($requestParams['values'][$updateObject->getIdentifier()]['install']);
380
381
                $checkValue = (int)$requestParams['values'][$updateObject->getIdentifier()]['install'];
382
383
                if ($checkValue === 1) {
384
                    // confirmation = yes, we do the update
385
                    $performResult = $updateObject->executeUpdate();
386
                } elseif ($updateObject->getConfirmation()->isRequired()) {
387
                    // confirmation = no, but is required, we do *not* the update and fail
388
                    $performResult = false;
389
                } elseif ($isSetButEmpty) {
390
                    // confirmation = no, but it is *not* required, we do *not* the update, but mark the wizard as done
391
                    $this->output->writeln('No changes applied, marking wizard as done.');
392
                    // confirmation was set to "no"
393
                    $performResult = true;
394
                }
395
            } else {
396
                // confirmation yes or non-confirmable
397
                $performResult = $updateObject->executeUpdate();
398
            }
399
        }
400
401
        $stream = $this->output->getStream();
402
        rewind($stream);
403
        if ($performResult) {
404
            if ($updateObject instanceof UpgradeWizardInterface && !($updateObject instanceof RepeatableInterface)) {
405
                // mark wizard as done if it's not repeatable and was successful
406
                $this->markWizardAsDone($updateObject->getIdentifier());
407
            }
408
            $messages->enqueue(
409
                new FlashMessage(
410
                    (string)stream_get_contents($stream),
411
                    'Update successful'
412
                )
413
            );
414
        } else {
415
            $messages->enqueue(
416
                new FlashMessage(
417
                    (string)stream_get_contents($stream),
418
                    'Update failed!',
419
                    FlashMessage::ERROR
420
                )
421
            );
422
        }
423
        return $messages;
424
    }
425
426
    /**
427
     * Marks some wizard as being "seen" so that it not shown again.
428
     * Writes the info in LocalConfiguration.php
429
     *
430
     * @param string $identifier
431
     * @throws \RuntimeException
432
     */
433
    public function markWizardAsDone(string $identifier): void
434
    {
435
        $this->assertIdentifierIsValid($identifier);
436
437
        $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
438
        GeneralUtility::makeInstance(Registry::class)->set('installUpdate', $class, 1);
439
    }
440
441
    /**
442
     * Checks if this wizard has been "done" before
443
     *
444
     * @param string $identifier
445
     * @return bool TRUE if wizard has been done before, FALSE otherwise
446
     * @throws \RuntimeException
447
     */
448
    public function isWizardDone(string $identifier): bool
449
    {
450
        $this->assertIdentifierIsValid($identifier);
451
452
        $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
453
        return (bool)GeneralUtility::makeInstance(Registry::class)->get('installUpdate', $class, false);
454
    }
455
456
    /**
457
     * Validate identifier exists in upgrade wizard list
458
     *
459
     * @param string $identifier
460
     * @throws \RuntimeException
461
     */
462
    protected function assertIdentifierIsValid(string $identifier): void
463
    {
464
        if ($identifier === '' || (!isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier]) && !is_subclass_of($identifier, RowUpdaterInterface::class))) {
465
            throw new \RuntimeException('No valid wizard identifier given', 1502721731);
466
        }
467
    }
468
}
469