[TASK] Migrate DatabaseRowsUpdateWizard to UpgradeWizardInterface
[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 not marked done.
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 if ($this->isWizardDone($identifier)) {
281 continue;
282 }
283 /** @var AbstractUpdate $wizardInstance */
284 $wizardInstance = GeneralUtility::makeInstance($class);
285
286 // $explanation is changed by reference in Update objects!
287 // @todo deprecate once all wizards are migrated
288 $explanation = '';
289 $shouldRenderWizard = false;
290 if (!($wizardInstance instanceof UpgradeWizardInterface) && $wizardInstance instanceof AbstractUpdate) {
291 $wizardInstance->checkForUpdate($explanation);
292 $shouldRenderWizard = $wizardInstance->shouldRenderWizard();
293 }
294 if ($wizardInstance instanceof UpgradeWizardInterface) {
295 if ($wizardInstance instanceof ChattyInterface) {
296 $wizardInstance->setOutput($this->output);
297 }
298 $shouldRenderWizard = $wizardInstance->updateNecessary();
299 $explanation = $wizardInstance->getDescription();
300 }
301
302 $wizards[] = [
303 'class' => $class,
304 'identifier' => $identifier,
305 'title' => $wizardInstance->getTitle(),
306 'shouldRenderWizard' => $shouldRenderWizard,
307 'explanation' => $explanation,
308 ];
309 }
310 return $wizards;
311 }
312
313 /**
314 * Execute the "get user input" step of a wizard
315 *
316 * @param string $identifier
317 * @return array
318 * @throws \RuntimeException
319 */
320 public function getWizardUserInput(string $identifier): array
321 {
322 $this->assertIdentifierIsValid($identifier);
323
324 $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
325 $updateObject = GeneralUtility::makeInstance($class);
326 $wizardHtml = '';
327 if (method_exists($updateObject, 'getUserInput')) {
328 $wizardHtml = $updateObject->getUserInput('install[values][' . htmlspecialchars($identifier) . ']');
329 } elseif ($updateObject instanceof UpgradeWizardInterface && $updateObject instanceof ConfirmableInterface) {
330 $wizardHtml = '
331 <div class="panel panel-danger">
332 <div class="panel-heading">' .
333 htmlspecialchars($updateObject->getConfirmation()->getTitle()) .
334 '</div>
335 <div class="panel-body">
336 ' .
337 nl2br(htmlspecialchars($updateObject->getConfirmation()->getMessage())) .
338 '
339 <div class="btn-group clearfix" data-toggle="buttons">
340 <label class="btn btn-default active">
341 <input type="radio" name="install[values][' .
342 htmlspecialchars($updateObject->getIdentifier()) .
343 '][install]" value="0" checked="checked" /> no
344 </label>
345 <label class="btn btn-default">
346 <input type="radio" name="install[values][' .
347 htmlspecialchars($updateObject->getIdentifier()) .
348 '][install]" value="1" /> yes
349 </label>
350 </div>
351 </div>
352 </div>
353 ';
354 }
355
356 $result = [
357 'identifier' => $identifier,
358 'title' => $updateObject->getTitle(),
359 'wizardHtml' => $wizardHtml,
360 ];
361
362 return $result;
363 }
364
365 /**
366 * Execute a single update wizard
367 *
368 * @param string $identifier
369 * @param int $showDatabaseQueries
370 * @return FlashMessageQueue
371 * @throws \RuntimeException
372 */
373 public function executeWizard(string $identifier, int $showDatabaseQueries = null): FlashMessageQueue
374 {
375 $this->assertIdentifierIsValid($identifier);
376
377 $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
378 $updateObject = GeneralUtility::makeInstance($class);
379
380 $messages = new FlashMessageQueue('install');
381 // $wizardInputErrorMessage is given as reference to wizard object!
382 $wizardInputErrorMessage = '';
383 if (method_exists($updateObject, 'checkUserInput') &&
384 !$updateObject->checkUserInput($wizardInputErrorMessage)) {
385 // @todo deprecate, unused
386 $messages->enqueue(
387 new FlashMessage(
388 $wizardInputErrorMessage ?: 'Something went wrong!',
389 'Input parameter broken',
390 FlashMessage::ERROR
391 )
392 );
393 } else {
394 if (!($updateObject instanceof UpgradeWizardInterface) && !method_exists($updateObject, 'performUpdate')) {
395 throw new \RuntimeException(
396 'No performUpdate method in update wizard with identifier ' . $identifier,
397 1371035200
398 );
399 }
400
401 // Both variables are used by reference in performUpdate()
402 $message = '';
403 $databaseQueries = [];
404 if ($updateObject instanceof UpgradeWizardInterface) {
405 $requestParams = GeneralUtility::_GP('install');
406 if ($updateObject instanceof ConfirmableInterface
407 && (
408 isset($requestParams['values'][$updateObject->getIdentifier()]['install'])
409 && empty($requestParams['values'][$updateObject->getIdentifier()]['install'])
410 )
411 ) {
412 // confirmation was set to "no"
413 $performResult = true;
414 } else {
415 // confirmation yes or non-confirmable
416 if ($updateObject instanceof ChattyInterface) {
417 $updateObject->setOutput($this->output);
418 }
419 $performResult = $updateObject->executeUpdate();
420 }
421 } else {
422 // @todo deprecate
423 $performResult = $updateObject->performUpdate($databaseQueries, $message);
424 }
425
426 $stream = $this->output->getStream();
427 rewind($stream);
428 if ($performResult) {
429 if ($updateObject instanceof UpgradeWizardInterface && !($updateObject instanceof RepeatableInterface)) {
430 // mark wizard as done if it's not repeatable and was successful
431 $this->markWizardAsDone($updateObject->getIdentifier());
432 }
433 $messages->enqueue(
434 new FlashMessage(
435 stream_get_contents($stream),
436 'Update successful'
437 )
438 );
439 } else {
440 $messages->enqueue(
441 new FlashMessage(
442 stream_get_contents($stream),
443 'Update failed!',
444 FlashMessage::ERROR
445 )
446 );
447 }
448 if ($showDatabaseQueries) {
449 // @todo deprecate
450 foreach ($databaseQueries as $query) {
451 $messages->enqueue(
452 new FlashMessage(
453 $query,
454 '',
455 FlashMessage::INFO
456 )
457 );
458 }
459 }
460 }
461 return $messages;
462 }
463
464 /**
465 * Marks some wizard as being "seen" so that it not shown again.
466 * Writes the info in LocalConfiguration.php
467 *
468 * @param string $identifier
469 * @throws \RuntimeException
470 */
471 public function markWizardAsDone(string $identifier): void
472 {
473 $this->assertIdentifierIsValid($identifier);
474
475 $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
476 GeneralUtility::makeInstance(Registry::class)->set('installUpdate', $class, 1);
477 }
478
479 /**
480 * Checks if this wizard has been "done" before
481 *
482 * @param string $identifier
483 * @return bool TRUE if wizard has been done before, FALSE otherwise
484 * @throws \RuntimeException
485 */
486 public function isWizardDone(string $identifier): bool
487 {
488 $this->assertIdentifierIsValid($identifier);
489
490 $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
491 return (bool)GeneralUtility::makeInstance(Registry::class)->get('installUpdate', $class, false);
492 }
493
494 /**
495 * Validate identifier exists in upgrade wizard list
496 *
497 * @param string $identifier
498 * @throws \RuntimeException
499 */
500 protected function assertIdentifierIsValid(string $identifier): void
501 {
502 if ($identifier === '' || !isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier])) {
503 throw new \RuntimeException('No valid wizard identifier given', 1502721731);
504 }
505 }
506 }