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