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