[BUGFIX] Prevent crash in Install Tool and EM with invalid ext_tables.sql
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Controller / Action / Tool / ImportantActions.php
1 <?php
2 namespace TYPO3\CMS\Install\Controller\Action\Tool;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Core\Bootstrap;
18 use TYPO3\CMS\Core\Core\ClassLoadingInformation;
19 use TYPO3\CMS\Core\Crypto\Random;
20 use TYPO3\CMS\Core\Database\ConnectionPool;
21 use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
22 use TYPO3\CMS\Core\Database\Schema\SqlReader;
23 use TYPO3\CMS\Core\Service\OpcodeCacheService;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25 use TYPO3\CMS\Install\Controller\Action;
26
27 /**
28 * Handle important actions
29 */
30 class ImportantActions extends Action\AbstractAction
31 {
32 /**
33 * Executes the tool
34 *
35 * @return string Rendered content
36 */
37 protected function executeAction()
38 {
39 if (isset($this->postValues['set']['changeEncryptionKey'])) {
40 $this->setNewEncryptionKeyAndLogOut();
41 }
42
43 $actionMessages = [];
44 if (isset($this->postValues['set']['changeInstallToolPassword'])) {
45 $actionMessages[] = $this->changeInstallToolPassword();
46 }
47 if (isset($this->postValues['set']['changeSiteName'])) {
48 $actionMessages[] = $this->changeSiteName();
49 }
50 if (isset($this->postValues['set']['createAdministrator'])) {
51 $actionMessages[] = $this->createAdministrator();
52 }
53 if (isset($this->postValues['set']['clearAllCache'])) {
54 $actionMessages[] = $this->clearAllCache();
55 $actionMessages[] = $this->clearOpcodeCache();
56 }
57 if (isset($this->postValues['set']['dumpAutoload'])) {
58 $actionMessages[] = $this->dumpAutoload();
59 }
60 if (isset($this->postValues['set']['tcaMigrations'])) {
61 $tcaMessages = $this->checkTcaMigrations();
62
63 if (count($tcaMessages) === 0) {
64 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
65 $message->setTitle('No TCA migrations need to be applied');
66 $message->setMessage('Your TCA looks good.');
67 $actionMessages[] = $message;
68 } else {
69 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\WarningStatus::class);
70 $message->setTitle('TCA migrations need to be applied');
71 $messageContent = ['Check the following list and apply needed changes. <br><br><ol>'];
72 foreach ($tcaMessages as $tcaMessage) {
73 $messageContent[] = '<li>' . $tcaMessage . '</li>';
74 }
75 $messageContent[] = '</ol>';
76 $message->setMessage(implode('', $messageContent));
77 $actionMessages[] = $message;
78 }
79 }
80
81 // Database analyzer handling
82 if (isset($this->postValues['set']['databaseAnalyzerExecute'])
83 || isset($this->postValues['set']['databaseAnalyzerAnalyze'])
84 ) {
85 $this->loadExtLocalconfDatabaseAndExtTables();
86 }
87 if (isset($this->postValues['set']['databaseAnalyzerExecute'])) {
88 $actionMessages = array_merge($actionMessages, $this->databaseAnalyzerExecute());
89 }
90 if (isset($this->postValues['set']['databaseAnalyzerAnalyze'])) {
91 try {
92 $actionMessages[] = $this->databaseAnalyzerAnalyze();
93 } catch (\TYPO3\CMS\Core\Database\Schema\Exception\StatementException $e) {
94 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
95 $message->setTitle('Database analysis failed');
96 $message->setMessage($e->getMessage());
97 $actionMessages[] = $message;
98 }
99 }
100
101 $this->view->assign('actionMessages', $actionMessages);
102
103 $operatingSystem = TYPO3_OS === 'WIN' ? 'Windows' : 'Unix';
104
105 $opcodeCacheService = GeneralUtility::makeInstance(OpcodeCacheService::class);
106
107 /** @var \TYPO3\CMS\Install\Service\CoreUpdateService $coreUpdateService */
108 $coreUpdateService = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Service\CoreUpdateService::class);
109 $this->view
110 ->assign('enableCoreUpdate', $coreUpdateService->isCoreUpdateEnabled())
111 ->assign('composerMode', Bootstrap::usesComposerClassLoading())
112 ->assign('operatingSystem', $operatingSystem)
113 ->assign('cgiDetected', GeneralUtility::isRunningOnCgiServerApi())
114 ->assign('extensionCompatibilityTesterProtocolFile', GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . 'typo3temp/assets/ExtensionCompatibilityTester.txt')
115 ->assign('extensionCompatibilityTesterErrorProtocolFile', GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . 'typo3temp/assets/ExtensionCompatibilityTesterErrors.json')
116 ->assign('extensionCompatibilityTesterMessages', $this->getExtensionCompatibilityTesterMessages())
117 ->assign('listOfOpcodeCaches', $opcodeCacheService->getAllActive());
118
119 $connectionInfos = [];
120 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
121 foreach ($connectionPool->getConnectionNames() as $connectionName) {
122 $connection = $connectionPool->getConnectionByName($connectionName);
123 $connectionParameters = $connection->getParams();
124 $connectionInfo = [
125 'connectionName' => $connectionName,
126 'version' => $connection->getServerVersion(),
127 'databaseName' => $connection->getDatabase(),
128 'username' => $connection->getUsername(),
129 'host' => $connection->getHost(),
130 'port' => $connection->getPort(),
131 'socket' => $connectionParameters['unix_socket'] ?? '',
132 'numberOfTables' => count($connection->getSchemaManager()->listTables()),
133 'numberOfMappedTables' => 0,
134 ];
135 if (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
136 && is_array($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
137 ) {
138 // Count number of array keys having $connectionName as value
139 $connectionInfo['numberOfMappedTables'] = count(array_intersect(
140 $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'],
141 [$connectionName]
142 ));
143 }
144 $connectionInfos[] = $connectionInfo;
145 }
146
147 $this->view->assign('connections', $connectionInfos);
148
149 return $this->view->render();
150 }
151
152 /**
153 * Set new password if requested
154 *
155 * @return \TYPO3\CMS\Install\Status\StatusInterface
156 */
157 protected function changeInstallToolPassword()
158 {
159 $values = $this->postValues['values'];
160 if ($values['newInstallToolPassword'] !== $values['newInstallToolPasswordCheck']) {
161 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
162 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
163 $message->setTitle('Install tool password not changed');
164 $message->setMessage('Given passwords do not match.');
165 } elseif (strlen($values['newInstallToolPassword']) < 8) {
166 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
167 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
168 $message->setTitle('Install tool password not changed');
169 $message->setMessage('Given password must be at least eight characters long.');
170 } else {
171 /** @var \TYPO3\CMS\Core\Configuration\ConfigurationManager $configurationManager */
172 $configurationManager = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\ConfigurationManager::class);
173 $configurationManager->setLocalConfigurationValueByPath(
174 'BE/installToolPassword',
175 $this->getHashedPassword($values['newInstallToolPassword'])
176 );
177 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
178 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
179 $message->setTitle('Install tool password changed');
180 }
181 return $message;
182 }
183
184 /**
185 * Set new site name
186 *
187 * @return \TYPO3\CMS\Install\Status\StatusInterface
188 */
189 protected function changeSiteName()
190 {
191 $values = $this->postValues['values'];
192 if (isset($values['newSiteName']) && $values['newSiteName'] !== '') {
193 /** @var \TYPO3\CMS\Core\Configuration\ConfigurationManager $configurationManager */
194 $configurationManager = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\ConfigurationManager::class);
195 $configurationManager->setLocalConfigurationValueByPath('SYS/sitename', $values['newSiteName']);
196 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
197 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
198 $message->setTitle('Site name changed');
199 $this->view->assign('siteName', $values['newSiteName']);
200 } else {
201 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
202 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
203 $message->setTitle('Site name not changed');
204 $message->setMessage('Site name must be at least one character long.');
205 }
206 return $message;
207 }
208
209 /**
210 * Clear all caches
211 *
212 * @return \TYPO3\CMS\Install\Status\StatusInterface
213 */
214 protected function clearAllCache()
215 {
216 /** @var \TYPO3\CMS\Install\Service\ClearCacheService $clearCacheService */
217 $clearCacheService = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Service\ClearCacheService::class);
218 $clearCacheService->clearAll();
219 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
220 $message->setTitle('Successfully cleared all caches');
221 return $message;
222 }
223
224 /**
225 * Clear PHP opcode cache
226 *
227 * @return \TYPO3\CMS\Install\Status\StatusInterface
228 */
229 protected function clearOpcodeCache()
230 {
231 GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
232 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
233 $message->setTitle('Successfully cleared all available opcode caches');
234 return $message;
235 }
236
237 /**
238 * Dumps Extension Autoload Information
239 *
240 * @return \TYPO3\CMS\Install\Status\StatusInterface
241 */
242 protected function dumpAutoload(): \TYPO3\CMS\Install\Status\StatusInterface
243 {
244 if (Bootstrap::usesComposerClassLoading()) {
245 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\NoticeStatus::class);
246 $message->setTitle('Skipped generating additional class loading information in composer mode.');
247 } else {
248 ClassLoadingInformation::dumpClassLoadingInformation();
249 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
250 $message->setTitle('Successfully dumped class loading information for extensions.');
251 }
252 return $message;
253 }
254
255 /**
256 * Set new encryption key
257 *
258 * @return void
259 */
260 protected function setNewEncryptionKeyAndLogOut()
261 {
262 $newKey = GeneralUtility::makeInstance(Random::class)->generateRandomHexString(96);
263 /** @var \TYPO3\CMS\Core\Configuration\ConfigurationManager $configurationManager */
264 $configurationManager = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Configuration\ConfigurationManager::class);
265 $configurationManager->setLocalConfigurationValueByPath('SYS/encryptionKey', $newKey);
266 /** @var $formProtection \TYPO3\CMS\Core\FormProtection\InstallToolFormProtection */
267 $formProtection = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get(
268 \TYPO3\CMS\Core\FormProtection\InstallToolFormProtection::class
269 );
270 $formProtection->clean();
271 /** @var \TYPO3\CMS\Install\Service\SessionService $session */
272 $session = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Service\SessionService::class);
273 $session->destroySession();
274 \TYPO3\CMS\Core\Utility\HttpUtility::redirect('Install.php?install[context]=' . $this->getContext());
275 }
276
277 /**
278 * Create administrator user
279 *
280 * @return \TYPO3\CMS\Install\Status\StatusInterface
281 */
282 protected function createAdministrator()
283 {
284 $values = $this->postValues['values'];
285 $username = preg_replace('/\\s/i', '', $values['newUserUsername']);
286 $password = $values['newUserPassword'];
287 $passwordCheck = $values['newUserPasswordCheck'];
288
289 if (strlen($username) < 1) {
290 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
291 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
292 $message->setTitle('Administrator user not created');
293 $message->setMessage('No valid username given.');
294 } elseif ($password !== $passwordCheck) {
295 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
296 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
297 $message->setTitle('Administrator user not created');
298 $message->setMessage('Passwords do not match.');
299 } elseif (strlen($password) < 8) {
300 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
301 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
302 $message->setTitle('Administrator user not created');
303 $message->setMessage('Password must be at least eight characters long.');
304 } else {
305 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
306 $userExists = $connectionPool->getConnectionForTable('be_users')
307 ->count(
308 'uid',
309 'be_users',
310 ['username' => $username]
311 );
312
313 if ($userExists) {
314 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
315 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
316 $message->setTitle('Administrator user not created');
317 $message->setMessage('A user with username "' . $username . '" exists already.');
318 } else {
319 $hashedPassword = $this->getHashedPassword($password);
320 $adminUserFields = [
321 'username' => $username,
322 'password' => $hashedPassword,
323 'admin' => 1,
324 'tstamp' => $GLOBALS['EXEC_TIME'],
325 'crdate' => $GLOBALS['EXEC_TIME']
326 ];
327 $connectionPool->getConnectionForTable('be_users')
328 ->insert('be_users', $adminUserFields);
329 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
330 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
331 $message->setTitle('Administrator created with username "' . $username . '".');
332 }
333 }
334
335 return $message;
336 }
337
338 /**
339 * Execute database migration
340 *
341 * @return array<\TYPO3\CMS\Install\Status\StatusInterface>
342 */
343 protected function databaseAnalyzerExecute()
344 {
345 $messages = [];
346
347 // Early return in case no update was selected
348 if (empty($this->postValues['values'])) {
349 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
350 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\WarningStatus::class);
351 $message->setTitle('No database changes selected');
352 $messages[] = $message;
353 return $messages;
354 }
355
356 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
357 $sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
358 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
359
360 $statementHashesToPerform = $this->postValues['values'];
361
362 $results = $schemaMigrationService->migrate($sqlStatements, $statementHashesToPerform);
363
364 // Create error flash messages if any
365 foreach ($results as $errorMessage) {
366 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
367 $message->setTitle('Database update failed');
368 $message->setMessage('Error: ' . $errorMessage);
369 $messages[] = $message;
370 }
371
372 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
373 $message->setTitle('Executed database updates');
374 $messages[] = $message;
375
376 return $messages;
377 }
378
379 /**
380 * "Compare" action of analyzer
381 *
382 * @return \TYPO3\CMS\Install\Status\StatusInterface
383 * @throws \Doctrine\DBAL\DBALException
384 * @throws \Doctrine\DBAL\Schema\SchemaException
385 * @throws \InvalidArgumentException
386 * @throws \TYPO3\CMS\Core\Database\Schema\Exception\StatementException
387 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
388 * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
389 * @throws \TYPO3\CMS\Core\Database\Schema\Exception\UnexpectedSignalReturnValueTypeException
390 * @throws \RuntimeException
391 */
392 protected function databaseAnalyzerAnalyze()
393 {
394 $databaseAnalyzerSuggestion = [];
395
396 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
397 $sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
398 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
399
400 $addCreateChange = $schemaMigrationService->getUpdateSuggestions($sqlStatements);
401 // Aggregate the per-connection statements into one flat array
402 $addCreateChange = array_merge_recursive(...array_values($addCreateChange));
403
404 if (isset($addCreateChange['create_table'])) {
405 $databaseAnalyzerSuggestion['addTable'] = [];
406 foreach ($addCreateChange['create_table'] as $hash => $statement) {
407 $databaseAnalyzerSuggestion['addTable'][$hash] = [
408 'hash' => $hash,
409 'statement' => $statement,
410 ];
411 }
412 }
413 if (isset($addCreateChange['add'])) {
414 $databaseAnalyzerSuggestion['addField'] = [];
415 foreach ($addCreateChange['add'] as $hash => $statement) {
416 $databaseAnalyzerSuggestion['addField'][$hash] = [
417 'hash' => $hash,
418 'statement' => $statement,
419 ];
420 }
421 }
422 if (isset($addCreateChange['change'])) {
423 $databaseAnalyzerSuggestion['change'] = [];
424 foreach ($addCreateChange['change'] as $hash => $statement) {
425 $databaseAnalyzerSuggestion['change'][$hash] = [
426 'hash' => $hash,
427 'statement' => $statement,
428 ];
429 if (isset($addCreateChange['change_currentValue'][$hash])) {
430 $databaseAnalyzerSuggestion['change'][$hash]['current'] = $addCreateChange['change_currentValue'][$hash];
431 }
432 }
433 }
434
435 // Difference from current to expected
436 $dropRename = $schemaMigrationService->getUpdateSuggestions($sqlStatements, true);
437 // Aggregate the per-connection statements into one flat array
438 $dropRename = array_merge_recursive(...array_values($dropRename));
439 if (isset($dropRename['change_table'])) {
440 $databaseAnalyzerSuggestion['renameTableToUnused'] = [];
441 foreach ($dropRename['change_table'] as $hash => $statement) {
442 $databaseAnalyzerSuggestion['renameTableToUnused'][$hash] = [
443 'hash' => $hash,
444 'statement' => $statement,
445 ];
446 if (!empty($dropRename['tables_count'][$hash])) {
447 $databaseAnalyzerSuggestion['renameTableToUnused'][$hash]['count'] = $dropRename['tables_count'][$hash];
448 }
449 }
450 }
451 if (isset($dropRename['change'])) {
452 $databaseAnalyzerSuggestion['renameTableFieldToUnused'] = [];
453 foreach ($dropRename['change'] as $hash => $statement) {
454 $databaseAnalyzerSuggestion['renameTableFieldToUnused'][$hash] = [
455 'hash' => $hash,
456 'statement' => $statement,
457 ];
458 }
459 }
460 if (isset($dropRename['drop'])) {
461 $databaseAnalyzerSuggestion['deleteField'] = [];
462 foreach ($dropRename['drop'] as $hash => $statement) {
463 $databaseAnalyzerSuggestion['deleteField'][$hash] = [
464 'hash' => $hash,
465 'statement' => $statement,
466 ];
467 }
468 }
469 if (isset($dropRename['drop_table'])) {
470 $databaseAnalyzerSuggestion['deleteTable'] = [];
471 foreach ($dropRename['drop_table'] as $hash => $statement) {
472 $databaseAnalyzerSuggestion['deleteTable'][$hash] = [
473 'hash' => $hash,
474 'statement' => $statement,
475 ];
476 if (!empty($dropRename['tables_count'][$hash])) {
477 $databaseAnalyzerSuggestion['deleteTable'][$hash]['count'] = $dropRename['tables_count'][$hash];
478 }
479 }
480 }
481
482 $this->view->assign('databaseAnalyzerSuggestion', $databaseAnalyzerSuggestion);
483
484 /** @var $message \TYPO3\CMS\Install\Status\StatusInterface */
485 $message = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\OkStatus::class);
486 $message->setTitle('Analyzed current database');
487 return $message;
488 }
489
490 /**
491 * "TCA migration" action
492 *
493 * @return array The TCA migration messages
494 */
495 protected function checkTcaMigrations()
496 {
497 GeneralUtility::makeInstance(\TYPO3\CMS\Install\Service\LoadTcaService::class)
498 ->loadExtensionTablesWithoutMigration();
499 $tcaMigration = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Migrations\TcaMigration::class);
500 $GLOBALS['TCA'] = $tcaMigration->migrate($GLOBALS['TCA']);
501 return $tcaMigration->getMessages();
502 }
503 }