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