Completed
Push — master ( 4af5ab...d26efc )
by
unknown
16:50
created

UpgradeWizardsService::isWizardDone()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
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
        $this->output = new StreamOutput(fopen('php://temp', 'wb'), Output::VERBOSITY_NORMAL, false);
0 ignored issues
show
Bug introduced by
It seems like fopen('php://temp', 'wb') can also be of type false; however, parameter $stream of Symfony\Component\Consol...amOutput::__construct() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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