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