2 declare(strict_types
= 1);
4 namespace TYPO3\CMS\Install\Service
;
7 * This file is part of the TYPO3 CMS project.
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.
13 * For the full copyright and license information, please read the
14 * LICENSE.txt file that was distributed with this source code.
16 * The TYPO3 project - inspiring people to share!
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
;
40 * Service class helping managing upgrade wizards
42 class UpgradeWizardsService
46 public function __construct()
48 $this->output
= new StreamOutput(fopen('php://temp', 'wb'), Output
::VERBOSITY_NORMAL
, false
);
52 * Force creation / update of caching framework tables that are needed by some update wizards
54 * @return array List of executed statements
56 public function silentCacheFrameworkTableSchemaMigration(): array
58 $sqlReader = GeneralUtility
::makeInstance(SqlReader
::class);
59 $cachingFrameworkDatabaseSchemaService = GeneralUtility
::makeInstance(DatabaseSchemaService
::class);
60 $createTableStatements = $sqlReader->getStatementArray(
61 $cachingFrameworkDatabaseSchemaService->getCachingFrameworkRequiredDatabaseSchema()
64 if (!empty($createTableStatements)) {
65 $schemaMigrationService = GeneralUtility
::makeInstance(SchemaMigrator
::class);
66 $statements = $schemaMigrationService->install($createTableStatements);
72 * @return array List of wizards marked as done in registry
74 public function listOfWizardsDone(): array
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(),
88 return $wizardsDoneInRegistry;
92 * @return array List of row updaters marked as done in registry
93 * @throws \RuntimeException
95 public function listOfRowUpdatersDone(): array
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)) {
105 /** @var RowUpdaterInterface $rowUpdater */
106 $rowUpdater = GeneralUtility
::makeInstance($rowUpdaterClassName);
107 if (!$rowUpdater instanceof RowUpdaterInterface
) {
108 throw new \
RuntimeException(
109 'Row updater must implement RowUpdaterInterface',
113 $rowUpdatersDone[] = [
114 'class' => $rowUpdaterClassName,
115 'identifier' => $rowUpdaterClassName,
116 'title' => $rowUpdater->getTitle(),
119 return $rowUpdatersDone;
123 * Mark one wizard as undone. This can be a "casual" wizard
124 * or a single "row updater".
126 * @param string $identifier Wizard or RowUpdater identifier
127 * @return bool True if wizard has been marked as undone
129 public function markWizardUndone(string $identifier): bool
131 $registry = GeneralUtility
::makeInstance(Registry
::class);
132 $aWizardHasBeenMarkedUndone = false
;
133 $wizardsDoneList = $this->listOfWizardsDone();
134 foreach ($wizardsDoneList as $wizard) {
135 if ($wizard['identifier'] === $identifier) {
136 $aWizardHasBeenMarkedUndone = true
;
137 $registry->set('installUpdate', $wizard['class'], 0);
140 if (!$aWizardHasBeenMarkedUndone) {
141 $rowUpdatersDoneList = $this->listOfRowUpdatersDone();
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]);
152 $registry->set('installUpdateRows', 'rowUpdatersDone', $registryArray);
156 return $aWizardHasBeenMarkedUndone;
160 * Get a list of tables, single columns and indexes to add.
162 * @return array Array with possible keys "tables", "columns", "indexes"
164 public function getBlockingDatabaseAdds(): array
166 $sqlReader = GeneralUtility
::makeInstance(SqlReader
::class);
167 $databaseDefinitions = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
169 $schemaMigrator = GeneralUtility
::makeInstance(SchemaMigrator
::class);
170 $databaseDifferences = $schemaMigrator->getSchemaDiffs($databaseDefinitions);
173 foreach ($databaseDifferences as $schemaDiff) {
174 foreach ($schemaDiff->newTables
as $newTable) {
175 /** @var Table $newTable */
176 if (!is_array($adds['tables'])) {
177 $adds['tables'] = [];
179 $adds['tables'][] = [
180 'table' => $newTable->getName(),
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'] = [];
189 $adds['columns'][] = [
190 'table' => $changedTable->name
,
191 'field' => $addedColumn->getName(),
194 foreach ($changedTable->addedIndexes
as $addedIndex) {
195 /** $var Index $addedIndex */
196 if (!is_array($adds['indexes'])) {
197 $adds['indexes'] = [];
199 $adds['indexes'][] = [
200 'table' => $changedTable->name
,
201 'index' => $addedIndex->getName(),
211 * Add missing tables, indexes and fields to DB.
213 public function addMissingTablesAndFields(): array
215 $sqlReader = GeneralUtility
::makeInstance(SqlReader
::class);
216 $databaseDefinitions = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
217 $schemaMigrator = GeneralUtility
::makeInstance(SchemaMigrator
::class);
218 return $schemaMigrator->install($databaseDefinitions, true
);
222 * True if DB main charset on mysql is utf8
224 * @return bool True if charset is ok
226 public function isDatabaseCharsetUtf8(): bool
228 /** @var \TYPO3\CMS\Core\Database\Connection $connection */
229 $connection = GeneralUtility
::makeInstance(ConnectionPool
::class)
230 ->getConnectionByName(ConnectionPool
::DEFAULT_CONNECTION_NAME
);
232 $isDefaultConnectionMysql = ($connection->getDatabasePlatform() instanceof MySqlPlatform
);
234 if (!$isDefaultConnectionMysql) {
235 // Not tested on non mysql
238 $queryBuilder = $connection->createQueryBuilder();
239 $charset = (string)$queryBuilder->select('DEFAULT_CHARACTER_SET_NAME')
240 ->from('information_schema.SCHEMATA')
242 $queryBuilder->expr()->eq(
244 $queryBuilder->createNamedParameter($connection->getDatabase(), \PDO
::PARAM_STR
)
250 // check if database charset is utf-8, also allows utf8mb4
251 $charsetOk = strpos($charset, 'utf8') === 0;
257 * Set default connection MySQL database charset to utf8.
258 * Should be called only *if* default database connection is actually MySQL
260 public function setDatabaseCharsetUtf8()
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);
269 * Get list of registered upgrade wizards.
271 * @return array List of upgrade wizards in correct order with detail information
273 public function getUpgradeWizardsList(): array
276 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $class) {
277 /** @var AbstractUpdate $wizardInstance */
278 $wizardInstance = GeneralUtility
::makeInstance($class);
280 // $explanation is changed by reference in Update objects!
281 // @todo deprecate once all wizards are migrated
283 $shouldRenderWizard = false
;
284 if (!($wizardInstance instanceof UpgradeWizardInterface
) && $wizardInstance instanceof AbstractUpdate
) {
285 $wizardInstance->checkForUpdate($explanation);
286 $shouldRenderWizard = $wizardInstance->shouldRenderWizard();
288 if ($wizardInstance instanceof UpgradeWizardInterface
) {
289 if ($wizardInstance instanceof ChattyInterface
) {
290 $wizardInstance->setOutput($this->output
);
292 $shouldRenderWizard = $wizardInstance->updateNecessary();
293 $explanation = $wizardInstance->getDescription();
298 'identifier' => $identifier,
299 'title' => $wizardInstance->getTitle(),
300 'shouldRenderWizard' => $shouldRenderWizard,
301 'markedDoneInRegistry' => GeneralUtility
::makeInstance(Registry
::class)->get(
306 'explanation' => $explanation,
313 * Execute the "get user input" step of a wizard
315 * @param string $identifier
317 * @throws \RuntimeException
319 public function getWizardUserInput(string $identifier): array
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'])
325 throw new \
RuntimeException(
326 'No valid wizard identifier given',
330 $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
331 $updateObject = GeneralUtility
::makeInstance($class);
333 if (method_exists($updateObject, 'getUserInput')) {
334 $wizardHtml = $updateObject->getUserInput('install[values][' . htmlspecialchars($identifier) . ']');
335 } elseif ($updateObject instanceof UpgradeWizardInterface
&& $updateObject instanceof ConfirmableInterface
) {
337 <div class="panel panel-danger">
338 <div class="panel-heading">' .
339 htmlspecialchars($updateObject->getConfirmation()->getTitle()) .
341 <div class="panel-body">
343 nl2br(htmlspecialchars($updateObject->getConfirmation()->getMessage())) .
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
351 <label class="btn btn-default">
352 <input type="radio" name="install[values][' .
353 htmlspecialchars($updateObject->getIdentifier()) .
354 '][install]" value="1" /> yes
363 'identifier' => $identifier,
364 'title' => $updateObject->getTitle(),
365 'wizardHtml' => $wizardHtml,
372 * Execute a single update wizard
374 * @param string $identifier
375 * @param int $showDatabaseQueries
376 * @return FlashMessageQueue
377 * @throws \RuntimeException
379 public function executeWizard(string $identifier, int $showDatabaseQueries = null
): FlashMessageQueue
381 if (empty($identifier)
382 ||
!array_key_exists($identifier, $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'])
384 throw new \
RuntimeException(
385 'No valid wizard identifier given',
389 $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
390 $updateObject = GeneralUtility
::makeInstance($class);
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
400 $wizardInputErrorMessage ?
: 'Something went wrong!',
401 'Input parameter broken',
406 if (!($updateObject instanceof UpgradeWizardInterface
) && !method_exists($updateObject, 'performUpdate')) {
407 throw new \
RuntimeException(
408 'No performUpdate method in update wizard with identifier ' . $identifier,
413 // Both variables are used by reference in performUpdate()
415 $databaseQueries = [];
416 if ($updateObject instanceof UpgradeWizardInterface
) {
417 $requestParams = GeneralUtility
::_GP('install');
418 if ($updateObject instanceof ConfirmableInterface
420 isset($requestParams['values'][$updateObject->getIdentifier()]['install'])
421 && empty($requestParams['values'][$updateObject->getIdentifier()]['install'])
424 // confirmation was set to "no"
425 $performResult = true
;
427 // confirmation yes or non-confirmable
428 if ($updateObject instanceof ChattyInterface
) {
429 $updateObject->setOutput($this->output
);
431 $performResult = $updateObject->executeUpdate();
435 $performResult = $updateObject->performUpdate($databaseQueries, $message);
438 $stream = $this->output
->getStream();
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());
447 stream_get_contents($stream),
454 stream_get_contents($stream),
460 if ($showDatabaseQueries) {
462 foreach ($databaseQueries as $query) {
477 * Marks some wizard as being "seen" so that it not shown again.
478 * Writes the info in LocalConfiguration.php
480 * @param string $identifier
482 public function markWizardAsDone(string $identifier): void
484 GeneralUtility
::makeInstance(Registry
::class)->set('installUpdate', $identifier, 1);
488 * Checks if this wizard has been "done" before
490 * @param string $identifier
491 * @return bool TRUE if wizard has been done before, FALSE otherwise
493 public function isWizardDone(string $identifier): bool
495 return (bool
)GeneralUtility
::makeInstance(Registry
::class)->get('installUpdate', $identifier, false
);