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