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