[TASK] Install tool: Use ext:core messaging
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Service / UpgradeWizardsService.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Install\Service;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Doctrine\DBAL\Schema\Column;
19 use Doctrine\DBAL\Schema\Table;
20 use TYPO3\CMS\Core\Cache\DatabaseSchemaService;
21 use TYPO3\CMS\Core\Database\ConnectionPool;
22 use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
23 use TYPO3\CMS\Core\Database\Schema\SqlReader;
24 use TYPO3\CMS\Core\Messaging\FlashMessage;
25 use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
26 use TYPO3\CMS\Core\Registry;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28 use TYPO3\CMS\Install\Updates\AbstractUpdate;
29 use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface;
30
31 /**
32 * Service class helping managing upgrade wizards
33 */
34 class UpgradeWizardsService
35 {
36 /**
37 * Force creation / update of caching framework tables that are needed by some update wizards
38 *
39 * @return array List of executed statements
40 */
41 public function silentCacheFrameworkTableSchemaMigration(): array
42 {
43 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
44 $cachingFrameworkDatabaseSchemaService = GeneralUtility::makeInstance(DatabaseSchemaService::class);
45 $createTableStatements = $sqlReader->getStatementArray(
46 $cachingFrameworkDatabaseSchemaService->getCachingFrameworkRequiredDatabaseSchema()
47 );
48 $statements = [];
49 if (!empty($createTableStatements)) {
50 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
51 $statements = $schemaMigrationService->install($createTableStatements);
52 }
53 return $statements;
54 }
55
56 /**
57 * @return array List of wizards marked as done in registry
58 */
59 public function listOfWizardsDoneInRegistry(): array
60 {
61 $wizardsDoneInRegistry = [];
62 $registry = GeneralUtility::makeInstance(Registry::class);
63 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $className) {
64 if ($registry->get('installUpdate', $className, false)) {
65 $wizardInstance = GeneralUtility::makeInstance($className);
66 $wizardsDoneInRegistry[] = [
67 'class' => $className,
68 'identifier' => $identifier,
69 'title' => $wizardInstance->getTitle(),
70 ];
71 }
72 }
73 return $wizardsDoneInRegistry;
74 }
75
76 /**
77 * @return array List of row updaters marked as done in registry
78 * @throws \RuntimeException
79 */
80 public function listOfRowUpdatersDoneInRegistry(): array
81 {
82 $registry = GeneralUtility::makeInstance(Registry::class);
83 $rowUpdatersDoneClassNames = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
84 $rowUpdatersDone = [];
85 foreach ($rowUpdatersDoneClassNames as $rowUpdaterClassName) {
86 // Silently skip non existing DatabaseRowsUpdateWizards
87 if (!class_exists($rowUpdaterClassName)) {
88 continue;
89 }
90 /** @var RowUpdaterInterface $rowUpdater */
91 $rowUpdater = GeneralUtility::makeInstance($rowUpdaterClassName);
92 if (!$rowUpdater instanceof RowUpdaterInterface) {
93 throw new \RuntimeException(
94 'Row updater must implement RowUpdaterInterface',
95 1484152906
96 );
97 }
98 $rowUpdatersDone[] = [
99 'class' => $rowUpdaterClassName,
100 'identifier' => $rowUpdaterClassName,
101 'title' => $rowUpdater->getTitle(),
102 ];
103 }
104 return $rowUpdatersDone;
105 }
106
107 /**
108 * Mark one wizard as undone. This can be a "casual" wizard
109 * or a single "row updater".
110 *
111 * @param string $identifier Wizard or RowUpdater identifier
112 * @return bool True if wizard has been marked as undone
113 */
114 public function markWizardUndoneInRegistry(string $identifier): bool
115 {
116 $registry = GeneralUtility::makeInstance(Registry::class);
117 $aWizardHasBeenMarkedUndone = false;
118 $wizardsDoneList = $this->listOfWizardsDoneInRegistry();
119 foreach ($wizardsDoneList as $wizard) {
120 if ($wizard['identifier'] === $identifier) {
121 $aWizardHasBeenMarkedUndone = true;
122 $registry->set('installUpdate', $wizard['class'], 0);
123 }
124 }
125 if (!$aWizardHasBeenMarkedUndone) {
126 $rowUpdatersDoneList = $this->listOfRowUpdatersDoneInRegistry();
127 $registryArray = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
128 foreach ($rowUpdatersDoneList as $rowUpdater) {
129 if ($rowUpdater['identifier'] === $identifier) {
130 $aWizardHasBeenMarkedUndone = true;
131 foreach ($registryArray as $rowUpdaterMarkedAsDonePosition => $rowUpdaterMarkedAsDone) {
132 if ($rowUpdaterMarkedAsDone === $rowUpdater['class']) {
133 unset($registryArray[$rowUpdaterMarkedAsDonePosition]);
134 break;
135 }
136 }
137 $registry->set('installUpdateRows', 'rowUpdatersDone', $registryArray);
138 }
139 }
140 }
141 return $aWizardHasBeenMarkedUndone;
142 }
143
144 /**
145 * Get a list of tables, single columns and indexes to add.
146 *
147 * @return array Array with possible keys "tables", "columns", "indexes"
148 */
149 public function getBlockingDatabaseAdds(): array
150 {
151 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
152 $databaseDefinitions = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
153
154 $schemaMigrator = GeneralUtility::makeInstance(SchemaMigrator::class);
155 $databaseDifferences = $schemaMigrator->getSchemaDiffs($databaseDefinitions);
156
157 $adds = [];
158 foreach ($databaseDifferences as $schemaDiff) {
159 foreach ($schemaDiff->newTables as $newTable) {
160 /** @var Table $newTable*/
161 if (!is_array($adds['tables'])) {
162 $adds['tables'] = [];
163 }
164 $adds['tables'][] = [
165 'table' => $newTable->getName(),
166 ];
167 }
168 foreach ($schemaDiff->changedTables as $changedTable) {
169 foreach ($changedTable->addedColumns as $addedColumn) {
170 /** @var Column $addedColumn */
171 if (!is_array($adds['columns'])) {
172 $adds['columns'] = [];
173 }
174 $adds['columns'][] = [
175 'table' => $changedTable->name,
176 'field' => $addedColumn->getName(),
177 ];
178 }
179 foreach ($changedTable->addedIndexes as $addedIndex) {
180 /** $var Index $addedIndex */
181 if (!is_array($adds['indexes'])) {
182 $adds['indexes'] = [];
183 }
184 $adds['indexes'][] = [
185 'table' => $changedTable->name,
186 'index' => $addedIndex->getName(),
187 ];
188 }
189 }
190 }
191
192 return $adds;
193 }
194
195 /**
196 * Add missing tables, indexes and fields to DB.
197 */
198 public function addMissingTablesAndFields()
199 {
200 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
201 $databaseDefinitions = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
202 $schemaMigrator = GeneralUtility::makeInstance(SchemaMigrator::class);
203 $schemaMigrator->install($databaseDefinitions, true);
204 }
205
206 /**
207 * True if DB main charset on mysql is utf8
208 *
209 * @return bool True if charset is ok
210 */
211 public function isDatabaseCharsetUtf8(): bool
212 {
213 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
214 ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
215 $isDefaultConnectionMysql = strpos($connection->getServerVersion(), 'MySQL') === 0;
216
217 if (!$isDefaultConnectionMysql) {
218 // Not tested on non mysql
219 $charsetOk = true;
220 } else {
221 $queryBuilder = $connection->createQueryBuilder();
222 $charset = (string)$queryBuilder->select('DEFAULT_CHARACTER_SET_NAME')
223 ->from('information_schema.SCHEMATA')
224 ->where(
225 $queryBuilder->expr()->eq(
226 'SCHEMA_NAME',
227 $queryBuilder->createNamedParameter($connection->getDatabase(), \PDO::PARAM_STR)
228 )
229 )
230 ->setMaxResults(1)
231 ->execute()
232 ->fetchColumn();
233 // check if database charset is utf-8, also allows utf8mb4
234 $charsetOk = strpos($charset, 'utf8') !== 0;
235 }
236 return $charsetOk;
237 }
238
239 /**
240 * Set default connection MySQL database charset to utf8.
241 * Should be called only *if* default database connection is actually MySQL
242 */
243 public function setDatabaseCharsetUtf8()
244 {
245 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
246 ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
247 $sql = 'ALTER DATABASE ' . $connection->quoteIdentifier($connection->getDatabase()) . ' CHARACTER SET utf8';
248 $connection->exec($sql);
249 }
250
251 /**
252 * Get list of registered upgrade wizards.
253 *
254 * @return array List of upgrade wizards in correct order with detail information
255 */
256 public function getUpgradeWizardsList(): array
257 {
258 $wizards = [];
259 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $class) {
260 /** @var AbstractUpdate $wizardInstance */
261 $wizardInstance = GeneralUtility::makeInstance($class);
262
263 // $explanation is changed by reference in Update objects!
264 $explanation = '';
265 $wizardInstance->checkForUpdate($explanation);
266
267 $wizards[] = [
268 'class' => $class,
269 'identifier' => $identifier,
270 'title' => $wizardInstance->getTitle(),
271 'shouldRenderWizard' => $wizardInstance->shouldRenderWizard(),
272 'markedDoneInRegistry' => GeneralUtility::makeInstance(Registry::class)->get('installUpdate', $class, false),
273 'explanation' => $explanation,
274 ];
275 }
276 return $wizards;
277 }
278
279 /**
280 * Execute the "get user input" step of a wizard
281 *
282 * @param string $identifier
283 * @return array
284 * @throws \RuntimeException
285 */
286 public function getWizardUserInput(string $identifier): array
287 {
288 // Validate identifier exists in upgrade wizard list
289 if (empty($identifier)
290 || !array_key_exists($identifier, $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'])
291 ) {
292 throw new \RuntimeException(
293 'No valid wizard identifier given',
294 1502721731
295 );
296 }
297 $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
298 $updateObject = GeneralUtility::makeInstance($class);
299 $wizardHtml = '';
300 if (method_exists($updateObject, 'getUserInput')) {
301 $wizardHtml = $updateObject->getUserInput('install[values][' . $identifier . ']');
302 }
303
304 $result = [
305 'identifier' => $identifier,
306 'title' => $updateObject->getTitle(),
307 'wizardHtml' => $wizardHtml,
308 ];
309
310 return $result;
311 }
312
313 /**
314 * Execute a single update wizard
315 *
316 * @param string $identifier
317 * @param array $postValues
318 * @return FlashMessageQueue
319 * @throws \RuntimeException
320 */
321 public function executeWizard(string $identifier, array $postValues = []): FlashMessageQueue
322 {
323 if (empty($identifier)
324 || !array_key_exists($identifier, $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'])
325 ) {
326 throw new \RuntimeException(
327 'No valid wizard identifier given',
328 1502721732
329 );
330 }
331 $class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
332 $updateObject = GeneralUtility::makeInstance($class);
333
334 $wizardData = [
335 'identifier' => $identifier,
336 'title' => $updateObject->getTitle(),
337 ];
338
339 $messages = new FlashMessageQueue('install');
340 // $wizardInputErrorMessage is given as reference to wizard object!
341 $wizardInputErrorMessage = '';
342 if (method_exists($updateObject, 'checkUserInput') && !$updateObject->checkUserInput($wizardInputErrorMessage)) {
343 $messages->enqueue(new FlashMessage(
344 $wizardInputErrorMessage ?: 'Something went wrong!',
345 'Input parameter broken',
346 FlashMessage::ERROR
347 ));
348 } else {
349 if (!method_exists($updateObject, 'performUpdate')) {
350 throw new \RuntimeException(
351 'No performUpdate method in update wizard with identifier ' . $identifier,
352 1371035200
353 );
354 }
355
356 // Both variables are used by reference in performUpdate()
357 $customOutput = '';
358 $databaseQueries = [];
359 $performResult = $updateObject->performUpdate($databaseQueries, $customOutput);
360
361 if ($performResult) {
362 $messages->enqueue(new FlashMessage(
363 '',
364 'Update successful'
365 ));
366 } else {
367 $messages->enqueue(new FlashMessage(
368 $customOutput,
369 'Update failed!',
370 FlashMessage::ERROR
371 ));
372 }
373
374 if ($postValues['values']['showDatabaseQueries'] == 1) {
375 $wizardData['queries'] = $databaseQueries;
376 }
377 }
378
379 return $messages;
380 }
381 }