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