‪TYPO3CMS  10.4
UpgradeWizardsService.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
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 
19 
20 use Doctrine\DBAL\Platforms\MySqlPlatform;
21 use Doctrine\DBAL\Schema\Column;
22 use Doctrine\DBAL\Schema\Table;
23 use Symfony\Component\Console\Output\Output;
24 use Symfony\Component\Console\Output\StreamOutput;
37 
43 {
47  private ‪$output;
48 
49  public function ‪__construct()
50  {
51  $fileName = 'php://temp';
52  if (($stream = fopen($fileName, 'wb')) === false) {
53  throw new \RuntimeException('Unable to open stream "' . $fileName . '"', 1598341765);
54  }
55  $this->output = new StreamOutput($stream, Output::VERBOSITY_NORMAL, false);
56  }
57 
61  public function ‪listOfWizardsDone(): array
62  {
63  $wizardsDoneInRegistry = [];
64  $registry = GeneralUtility::makeInstance(Registry::class);
65  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $className) {
66  if ($registry->get('installUpdate', $className, false)) {
67  $wizardInstance = GeneralUtility::makeInstance($className);
68  $wizardsDoneInRegistry[] = [
69  'class' => $className,
70  'identifier' => $identifier,
71  'title' => $wizardInstance->getTitle(),
72  ];
73  }
74  }
75  return $wizardsDoneInRegistry;
76  }
77 
82  public function ‪listOfRowUpdatersDone(): array
83  {
84  $registry = GeneralUtility::makeInstance(Registry::class);
85  $rowUpdatersDoneClassNames = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
86  $rowUpdatersDone = [];
87  foreach ($rowUpdatersDoneClassNames as $rowUpdaterClassName) {
88  // Silently skip non existing DatabaseRowsUpdateWizards
89  if (!class_exists($rowUpdaterClassName)) {
90  continue;
91  }
93  $rowUpdater = GeneralUtility::makeInstance($rowUpdaterClassName);
94  if (!$rowUpdater instanceof RowUpdaterInterface) {
95  throw new \RuntimeException(
96  'Row updater must implement RowUpdaterInterface',
97  1484152906
98  );
99  }
100  $rowUpdatersDone[] = [
101  'class' => $rowUpdaterClassName,
102  'identifier' => $rowUpdaterClassName,
103  'title' => $rowUpdater->getTitle(),
104  ];
105  }
106  return $rowUpdatersDone;
107  }
108 
117  public function ‪markWizardUndone(string $identifier): bool
118  {
119  $this->‪assertIdentifierIsValid($identifier);
120 
121  $registry = GeneralUtility::makeInstance(Registry::class);
122  $aWizardHasBeenMarkedUndone = false;
123  foreach ($this->‪listOfWizardsDone() as $wizard) {
124  if ($wizard['identifier'] === $identifier) {
125  $aWizardHasBeenMarkedUndone = true;
126  $registry->set('installUpdate', $wizard['class'], 0);
127  }
128  }
129  if (!$aWizardHasBeenMarkedUndone) {
130  $rowUpdatersDoneList = $this->‪listOfRowUpdatersDone();
131  $registryArray = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
132  foreach ($rowUpdatersDoneList as $rowUpdater) {
133  if ($rowUpdater['identifier'] === $identifier) {
134  $aWizardHasBeenMarkedUndone = true;
135  foreach ($registryArray as $rowUpdaterMarkedAsDonePosition => $rowUpdaterMarkedAsDone) {
136  if ($rowUpdaterMarkedAsDone === $rowUpdater['class']) {
137  unset($registryArray[$rowUpdaterMarkedAsDonePosition]);
138  break;
139  }
140  }
141  $registry->set('installUpdateRows', 'rowUpdatersDone', $registryArray);
142  }
143  }
144  }
145  return $aWizardHasBeenMarkedUndone;
146  }
147 
153  public function ‪getBlockingDatabaseAdds(): array
154  {
155  $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
156  $databaseDefinitions = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
157 
158  $schemaMigrator = GeneralUtility::makeInstance(SchemaMigrator::class);
159  $databaseDifferences = $schemaMigrator->getSchemaDiffs($databaseDefinitions);
160 
161  $adds = [];
162  foreach ($databaseDifferences as $schemaDiff) {
163  foreach ($schemaDiff->newTables as $newTable) {
165  if (!is_array($adds['tables'])) {
166  $adds['tables'] = [];
167  }
168  $adds['tables'][] = [
169  'table' => $newTable->getName(),
170  ];
171  }
172  foreach ($schemaDiff->changedTables as $changedTable) {
173  foreach ($changedTable->addedColumns as $addedColumn) {
175  if (!is_array($adds['columns'])) {
176  $adds['columns'] = [];
177  }
178  $adds['columns'][] = [
179  'table' => $changedTable->name,
180  'field' => $addedColumn->getName(),
181  ];
182  }
183  foreach ($changedTable->addedIndexes as $addedIndex) {
185  if (!is_array($adds['indexes'])) {
186  $adds['indexes'] = [];
187  }
188  $adds['indexes'][] = [
189  'table' => $changedTable->name,
190  'index' => $addedIndex->getName(),
191  ];
192  }
193  }
194  }
195 
196  return $adds;
197  }
198 
202  public function ‪addMissingTablesAndFields(): array
203  {
204  $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
205  $databaseDefinitions = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
206  $schemaMigrator = GeneralUtility::makeInstance(SchemaMigrator::class);
207  return $schemaMigrator->install($databaseDefinitions, true);
208  }
209 
215  public function ‪isDatabaseCharsetUtf8(): bool
216  {
218  $connection = GeneralUtility::makeInstance(ConnectionPool::class)
219  ->getConnectionByName(‪ConnectionPool::DEFAULT_CONNECTION_NAME);
220 
221  $isDefaultConnectionMysql = ($connection->getDatabasePlatform() instanceof MySqlPlatform);
222 
223  if (!$isDefaultConnectionMysql) {
224  // Not tested on non mysql
225  $charsetOk = true;
226  } else {
227  $queryBuilder = $connection->createQueryBuilder();
228  $charset = (string)$queryBuilder->select('DEFAULT_CHARACTER_SET_NAME')
229  ->from('information_schema.SCHEMATA')
230  ->where(
231  $queryBuilder->expr()->eq(
232  'SCHEMA_NAME',
233  $queryBuilder->createNamedParameter($connection->getDatabase(), \PDO::PARAM_STR)
234  )
235  )
236  ->setMaxResults(1)
237  ->execute()
238  ->fetchColumn();
239  // check if database charset is utf-8, also allows utf8mb4
240  $charsetOk = strpos($charset, 'utf8') === 0;
241  }
242  return $charsetOk;
243  }
244 
249  public function ‪setDatabaseCharsetUtf8()
250  {
251  $connection = GeneralUtility::makeInstance(ConnectionPool::class)
252  ->getConnectionByName(‪ConnectionPool::DEFAULT_CONNECTION_NAME);
253  $sql = 'ALTER DATABASE ' . $connection->quoteIdentifier($connection->getDatabase()) . ' CHARACTER SET utf8';
254  $connection->exec($sql);
255  }
256 
262  public function ‪getUpgradeWizardsList(): array
263  {
264  $wizards = [];
265  foreach (array_keys(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']) as $identifier) {
266  $identifier = (string)$identifier;
267  if ($this->‪isWizardDone($identifier)) {
268  continue;
269  }
270 
271  $wizards[] = $this->‪getWizardInformationByIdentifier($identifier);
272  }
273  return $wizards;
274  }
275 
276  public function ‪getWizardInformationByIdentifier(string $identifier): array
277  {
278  if (class_exists($identifier)) {
279  $class = $identifier;
280  } else {
281  $class = ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
282  }
284  $wizardInstance = GeneralUtility::makeInstance($class);
285  $explanation = '';
286 
287  // $explanation is changed by reference in Update objects!
288  $shouldRenderWizard = false;
289  if ($wizardInstance instanceof ‪UpgradeWizardInterface) {
290  if ($wizardInstance instanceof ‪ChattyInterface) {
291  $wizardInstance->setOutput($this->output);
292  }
293  $shouldRenderWizard = $wizardInstance->updateNecessary();
294  $explanation = $wizardInstance->getDescription();
295  }
296 
297  return [
298  'class' => $class,
299  'identifier' => $identifier,
300  'title' => $wizardInstance->getTitle(),
301  'shouldRenderWizard' => $shouldRenderWizard,
302  'explanation' => $explanation,
303  ];
304  }
305 
313  public function ‪getWizardUserInput(string $identifier): array
314  {
315  $this->‪assertIdentifierIsValid($identifier);
316 
317  $class = ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
318  $updateObject = GeneralUtility::makeInstance($class);
319  $wizardHtml = '';
320  if ($updateObject instanceof ‪UpgradeWizardInterface && $updateObject instanceof ‪ConfirmableInterface) {
321  $markup = [];
322  $radioAttributes = [
323  'type' => 'radio',
324  'name' => 'install[values][' . $updateObject->getIdentifier() . '][install]',
325  'value' => '0'
326  ];
327  $markup[] = '<div class="panel panel-danger">';
328  $markup[] = ' <div class="panel-heading">';
329  $markup[] = htmlspecialchars($updateObject->getConfirmation()->getTitle());
330  $markup[] = ' </div>';
331  $markup[] = ' <div class="panel-body">';
332  $markup[] = ' <p>' . nl2br(htmlspecialchars($updateObject->getConfirmation()->getMessage())) . '</p>';
333  $markup[] = ' <div class="btn-group" data-toggle="buttons">';
334  if (!$updateObject->getConfirmation()->isRequired()) {
335  $markup[] = ' <label class="btn btn-default active"><input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . ' checked="checked" />' . $updateObject->getConfirmation()->getDeny() . '</label>';
336  }
337  $radioAttributes['value'] = '1';
338  $markup[] = ' <label class="btn btn-default"><input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . ' />' . $updateObject->getConfirmation()->getConfirm() . '</label>';
339  $markup[] = ' </div>';
340  $markup[] = ' </div>';
341  $markup[] = '</div>';
342  $wizardHtml = implode('', $markup);
343  }
344 
345  $result = [
346  'identifier' => $identifier,
347  'title' => $updateObject->getTitle(),
348  'description' => $updateObject->getDescription(),
349  'wizardHtml' => $wizardHtml,
350  ];
351 
352  return $result;
353  }
354 
362  public function ‪executeWizard(string $identifier): FlashMessageQueue
363  {
364  $performResult = false;
365  $this->‪assertIdentifierIsValid($identifier);
366 
367  $class = ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
368  $updateObject = GeneralUtility::makeInstance($class);
369 
370  if ($updateObject instanceof ChattyInterface) {
371  $updateObject->setOutput($this->output);
372  }
373  $messages = new FlashMessageQueue('install');
374 
375  if ($updateObject instanceof UpgradeWizardInterface) {
376  $requestParams = GeneralUtility::_GP('install');
377  if ($updateObject instanceof ConfirmableInterface) {
378  // value is set in request but is empty
379  $isSetButEmpty = isset($requestParams['values'][$updateObject->getIdentifier()]['install'])
380  && empty($requestParams['values'][$updateObject->getIdentifier()]['install']);
381 
382  $checkValue = (int)$requestParams['values'][$updateObject->getIdentifier()]['install'];
383 
384  if ($checkValue === 1) {
385  // confirmation = yes, we do the update
386  $performResult = $updateObject->executeUpdate();
387  } elseif ($updateObject->getConfirmation()->isRequired()) {
388  // confirmation = no, but is required, we do *not* the update and fail
389  $performResult = false;
390  } elseif ($isSetButEmpty) {
391  // confirmation = no, but it is *not* required, we do *not* the update, but mark the wizard as done
392  $this->output->writeln('No changes applied, marking wizard as done.');
393  // confirmation was set to "no"
394  $performResult = true;
395  }
396  } else {
397  // confirmation yes or non-confirmable
398  $performResult = $updateObject->executeUpdate();
399  }
400  }
401 
402  $stream = $this->output->getStream();
403  rewind($stream);
404  if ($performResult) {
405  if ($updateObject instanceof UpgradeWizardInterface && !($updateObject instanceof RepeatableInterface)) {
406  // mark wizard as done if it's not repeatable and was successful
407  $this->‪markWizardAsDone($updateObject->getIdentifier());
408  }
409  $messages->enqueue(
410  new FlashMessage(
411  (string)stream_get_contents($stream),
412  'Update successful'
413  )
414  );
415  } else {
416  $messages->enqueue(
417  new FlashMessage(
418  (string)stream_get_contents($stream),
419  'Update failed!',
421  )
422  );
423  }
424  return $messages;
425  }
426 
434  public function ‪markWizardAsDone(string $identifier): void
435  {
436  $this->‪assertIdentifierIsValid($identifier);
437 
438  $class = ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
439  GeneralUtility::makeInstance(Registry::class)->set('installUpdate', $class, 1);
440  }
441 
449  public function ‪isWizardDone(string $identifier): bool
450  {
451  $this->‪assertIdentifierIsValid($identifier);
452 
453  $class = ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
454  return (bool)GeneralUtility::makeInstance(Registry::class)->get('installUpdate', $class, false);
455  }
456 
463  protected function ‪assertIdentifierIsValid(string $identifier): void
464  {
465  if ($identifier === '' || (!isset(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier]) && !is_subclass_of($identifier, RowUpdaterInterface::class))) {
466  throw new \RuntimeException('No valid wizard identifier given', 1502721731);
467  }
468  }
469 }
‪TYPO3\CMS\Install\Updates\RepeatableInterface
Definition: RepeatableInterface.php:26
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\getUpgradeWizardsList
‪array getUpgradeWizardsList()
Definition: UpgradeWizardsService.php:261
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\getBlockingDatabaseAdds
‪array getBlockingDatabaseAdds()
Definition: UpgradeWizardsService.php:152
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\__construct
‪__construct()
Definition: UpgradeWizardsService.php:48
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\setDatabaseCharsetUtf8
‪setDatabaseCharsetUtf8()
Definition: UpgradeWizardsService.php:248
‪TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface
Definition: RowUpdaterInterface.php:24
‪TYPO3\CMS\Install\Updates\ChattyInterface
Definition: ChattyInterface.php:26
‪TYPO3\CMS\Core\Registry
Definition: Registry.php:33
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\listOfWizardsDone
‪array listOfWizardsDone()
Definition: UpgradeWizardsService.php:60
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\getWizardUserInput
‪array getWizardUserInput(string $identifier)
Definition: UpgradeWizardsService.php:312
‪TYPO3\CMS\Core\Database\Schema\SqlReader
Definition: SqlReader.php:31
‪TYPO3\CMS\Core\Database\ConnectionPool\DEFAULT_CONNECTION_NAME
‪const DEFAULT_CONNECTION_NAME
Definition: ConnectionPool.php:50
‪TYPO3\CMS\Core\Database\Schema\SchemaMigrator
Definition: SchemaMigrator.php:36
‪TYPO3\CMS\Install\Service\UpgradeWizardsService
Definition: UpgradeWizardsService.php:43
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\assertIdentifierIsValid
‪assertIdentifierIsValid(string $identifier)
Definition: UpgradeWizardsService.php:462
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\markWizardUndone
‪bool markWizardUndone(string $identifier)
Definition: UpgradeWizardsService.php:116
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\listOfRowUpdatersDone
‪array listOfRowUpdatersDone()
Definition: UpgradeWizardsService.php:81
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\addMissingTablesAndFields
‪addMissingTablesAndFields()
Definition: UpgradeWizardsService.php:201
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\isWizardDone
‪bool isWizardDone(string $identifier)
Definition: UpgradeWizardsService.php:448
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\markWizardAsDone
‪markWizardAsDone(string $identifier)
Definition: UpgradeWizardsService.php:433
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\$output
‪StreamOutput $output
Definition: UpgradeWizardsService.php:46
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\isDatabaseCharsetUtf8
‪bool isDatabaseCharsetUtf8()
Definition: UpgradeWizardsService.php:214
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\getWizardInformationByIdentifier
‪getWizardInformationByIdentifier(string $identifier)
Definition: UpgradeWizardsService.php:275
‪TYPO3\CMS\Install\Updates\UpgradeWizardInterface
Definition: UpgradeWizardInterface.php:24
‪TYPO3\CMS\Core\Messaging\FlashMessage
Definition: FlashMessage.php:24
‪TYPO3\CMS\Install\Service\UpgradeWizardsService\executeWizard
‪FlashMessageQueue executeWizard(string $identifier)
Definition: UpgradeWizardsService.php:361
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪TYPO3\CMS\Core\Messaging\FlashMessageQueue
Definition: FlashMessageQueue.php:29
‪TYPO3\CMS\Install\Service
Definition: ClearCacheService.php:16
‪TYPO3\CMS\Core\Messaging\AbstractMessage\ERROR
‪const ERROR
Definition: AbstractMessage.php:31
‪TYPO3\CMS\Install\Updates\ConfirmableInterface
Definition: ConfirmableInterface.php:24