[TASK] CGL: Whitespace fixes
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Controller / MaintenanceController.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Install\Controller;
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 Psr\Http\Message\ResponseInterface;
19 use Psr\Http\Message\ServerRequestInterface;
20 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
21 use TYPO3\CMS\Core\Core\Bootstrap;
22 use TYPO3\CMS\Core\Core\ClassLoadingInformation;
23 use TYPO3\CMS\Core\Database\ConnectionPool;
24 use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
25 use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
26 use TYPO3\CMS\Core\Database\Schema\SqlReader;
27 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
28 use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection;
29 use TYPO3\CMS\Core\Http\JsonResponse;
30 use TYPO3\CMS\Core\Messaging\FlashMessage;
31 use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
32 use TYPO3\CMS\Core\Service\OpcodeCacheService;
33 use TYPO3\CMS\Core\Utility\GeneralUtility;
34 use TYPO3\CMS\Install\Service\ClearCacheService;
35 use TYPO3\CMS\Install\Service\ClearTableService;
36 use TYPO3\CMS\Install\Service\Typo3tempFileService;
37 use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
38
39 /**
40 * Maintenance controller
41 */
42 class MaintenanceController extends AbstractController
43 {
44 /**
45 * Main "show the cards" view
46 *
47 * @param ServerRequestInterface $request
48 * @return ResponseInterface
49 */
50 public function cardsAction(ServerRequestInterface $request): ResponseInterface
51 {
52 $view = $this->initializeStandaloneView($request, 'Maintenance/Cards.html');
53 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
54 $view->assignMultiple([
55 'clearAllCacheOpcodeCaches' => (new OpcodeCacheService())->getAllActive(),
56 'clearTablesClearToken' => $formProtection->generateToken('installTool', 'clearTablesClear'),
57 'clearTypo3tempFilesToken' => $formProtection->generateToken('installTool', 'clearTypo3tempFiles'),
58 'createAdminToken' => $formProtection->generateToken('installTool', 'createAdmin'),
59 'databaseAnalyzerExecuteToken' => $formProtection->generateToken('installTool', 'databaseAnalyzerExecute'),
60 ]);
61 return new JsonResponse([
62 'success' => true,
63 'html' => $view->render(),
64 ]);
65 }
66
67 /**
68 * Clear cache framework and opcode caches
69 *
70 * @return ResponseInterface
71 */
72 public function cacheClearAllAction(): ResponseInterface
73 {
74 GeneralUtility::makeInstance(ClearCacheService::class)->clearAll();
75 GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
76 $messageQueue = (new FlashMessageQueue('install'))->enqueue(
77 new FlashMessage('Successfully cleared all caches and all available opcode caches.')
78 );
79 return new JsonResponse([
80 'success' => true,
81 'status' => $messageQueue,
82 ]);
83 }
84
85 /**
86 * Clear typo3temp files statistics action
87 *
88 * @return ResponseInterface
89 */
90 public function clearTypo3tempFilesStatsAction(): ResponseInterface
91 {
92 return new JsonResponse(
93 [
94 'success' => true,
95 'stats' => (new Typo3tempFileService())->getDirectoryStatistics(),
96 ]
97 );
98 }
99
100 /**
101 * Clear Processed Files
102 *
103 * @param ServerRequestInterface $request
104 * @return ResponseInterface
105 */
106 public function clearTypo3tempFilesAction(ServerRequestInterface $request): ResponseInterface
107 {
108 $messageQueue = new FlashMessageQueue('install');
109 $typo3tempFileService = new Typo3tempFileService();
110 $folder = $request->getParsedBody()['install']['folder'];
111 if ($folder === '_processed_') {
112 $failedDeletions = $typo3tempFileService->clearProcessedFiles();
113 if ($failedDeletions) {
114 $messageQueue->enqueue(new FlashMessage(
115 'Failed to delete ' . $failedDeletions . ' processed files. See TYPO3 log (by default typo3temp/var/logs/typo3_*.log)',
116 '',
117 FlashMessage::ERROR
118 ));
119 } else {
120 $messageQueue->enqueue(new FlashMessage('Cleared processed files'));
121 }
122 } else {
123 $typo3tempFileService->clearAssetsFolder($folder);
124 $messageQueue->enqueue(new FlashMessage('Cleared files in "' . $folder . '" folder'));
125 }
126 return new JsonResponse([
127 'success' => true,
128 'status' => $messageQueue,
129 ]);
130 }
131
132 /**
133 * Dump autoload information
134 *
135 * @return ResponseInterface
136 */
137 public function dumpAutoloadAction(): ResponseInterface
138 {
139 $messageQueue = new FlashMessageQueue('install');
140 if (Bootstrap::usesComposerClassLoading()) {
141 $messageQueue->enqueue(new FlashMessage(
142 '',
143 'Skipped generating additional class loading information in composer mode.',
144 FlashMessage::NOTICE
145 ));
146 } else {
147 ClassLoadingInformation::dumpClassLoadingInformation();
148 $messageQueue->enqueue(new FlashMessage(
149 '',
150 'Successfully dumped class loading information for extensions.'
151 ));
152 }
153 return new JsonResponse([
154 'success' => true,
155 'status' => $messageQueue,
156 ]);
157 }
158
159 /**
160 * Analyze current database situation
161 *
162 * @return ResponseInterface
163 */
164 public function databaseAnalyzerAnalyzeAction(): ResponseInterface
165 {
166 $this->loadExtLocalconfDatabaseAndExtTables();
167 $messageQueue = new FlashMessageQueue('install');
168
169 $suggestions = [];
170 try {
171 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
172 $sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
173 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
174 $addCreateChange = $schemaMigrationService->getUpdateSuggestions($sqlStatements);
175
176 // Aggregate the per-connection statements into one flat array
177 $addCreateChange = array_merge_recursive(...array_values($addCreateChange));
178 if (!empty($addCreateChange['create_table'])) {
179 $suggestion = [
180 'key' => 'addTable',
181 'label' => 'Add tables',
182 'enabled' => true,
183 'children' => [],
184 ];
185 foreach ($addCreateChange['create_table'] as $hash => $statement) {
186 $suggestion['children'][] = [
187 'hash' => $hash,
188 'statement' => $statement,
189 ];
190 }
191 $suggestions[] = $suggestion;
192 }
193 if (!empty($addCreateChange['add'])) {
194 $suggestion = [
195 'key' => 'addField',
196 'label' => 'Add fields to tables',
197 'enabled' => true,
198 'children' => [],
199 ];
200 foreach ($addCreateChange['add'] as $hash => $statement) {
201 $suggestion['children'][] = [
202 'hash' => $hash,
203 'statement' => $statement,
204 ];
205 }
206 $suggestions[] = $suggestion;
207 }
208 if (!empty($addCreateChange['change'])) {
209 $suggestion = [
210 'key' => 'change',
211 'label' => 'Change fields',
212 'enabled' => false,
213 'children' => [],
214 ];
215 foreach ($addCreateChange['change'] as $hash => $statement) {
216 $child = [
217 'hash' => $hash,
218 'statement' => $statement,
219 ];
220 if (isset($addCreateChange['change_currentValue'][$hash])) {
221 $child['current'] = $addCreateChange['change_currentValue'][$hash];
222 }
223 $suggestion['children'][] = $child;
224 }
225 $suggestions[] = $suggestion;
226 }
227
228 // Difference from current to expected
229 $dropRename = $schemaMigrationService->getUpdateSuggestions($sqlStatements, true);
230
231 // Aggregate the per-connection statements into one flat array
232 $dropRename = array_merge_recursive(...array_values($dropRename));
233 if (!empty($dropRename['change_table'])) {
234 $suggestion = [
235 'key' => 'renameTableToUnused',
236 'label' => 'Remove tables (rename with prefix)',
237 'enabled' => false,
238 'children' => [],
239 ];
240 foreach ($dropRename['change_table'] as $hash => $statement) {
241 $child = [
242 'hash' => $hash,
243 'statement' => $statement,
244 ];
245 if (!empty($dropRename['tables_count'][$hash])) {
246 $child['rowCount'] = $dropRename['tables_count'][$hash];
247 }
248 $suggestion['children'][] = $child;
249 }
250 $suggestions[] = $suggestion;
251 }
252 if (!empty($dropRename['change'])) {
253 $suggestion = [
254 'key' => 'renameTableFieldToUnused',
255 'label' => 'Remove unused fields (rename with prefix)',
256 'enabled' => false,
257 'children' => [],
258 ];
259 foreach ($dropRename['change'] as $hash => $statement) {
260 $suggestion['children'][] = [
261 'hash' => $hash,
262 'statement' => $statement,
263 ];
264 }
265 $suggestions[] = $suggestion;
266 }
267 if (!empty($dropRename['drop'])) {
268 $suggestion = [
269 'key' => 'deleteField',
270 'label' => 'Drop fields (really!)',
271 'enabled' => false,
272 'children' => [],
273 ];
274 foreach ($dropRename['drop'] as $hash => $statement) {
275 $suggestion['children'][] = [
276 'hash' => $hash,
277 'statement' => $statement,
278 ];
279 }
280 $suggestions[] = $suggestion;
281 }
282 if (!empty($dropRename['drop_table'])) {
283 $suggestion = [
284 'key' => 'deleteTable',
285 'label' => 'Drop tables (really!)',
286 'enabled' => false,
287 'children' => [],
288 ];
289 foreach ($dropRename['drop_table'] as $hash => $statement) {
290 $child = [
291 'hash' => $hash,
292 'statement' => $statement,
293 ];
294 if (!empty($dropRename['tables_count'][$hash])) {
295 $child['rowCount'] = $dropRename['tables_count'][$hash];
296 }
297 $suggestion['children'][] = $child;
298 }
299 $suggestions[] = $suggestion;
300 }
301
302 $messageQueue->enqueue(new FlashMessage(
303 '',
304 'Analyzed current database'
305 ));
306 } catch (StatementException $e) {
307 $messageQueue->enqueue(new FlashMessage(
308 '',
309 'Database analysis failed',
310 FlashMessage::ERROR
311 ));
312 }
313 return new JsonResponse([
314 'success' => true,
315 'status' => $messageQueue,
316 'suggestions' => $suggestions,
317 ]);
318 }
319
320 /**
321 * Apply selected database changes
322 *
323 * @param ServerRequestInterface $request
324 * @return ResponseInterface
325 */
326 public function databaseAnalyzerExecuteAction(ServerRequestInterface $request): ResponseInterface
327 {
328 $this->loadExtLocalconfDatabaseAndExtTables();
329 $messageQueue = new FlashMessageQueue('install');
330 $selectedHashes = $request->getParsedBody()['install']['hashes'] ?? [];
331 if (empty($selectedHashes)) {
332 $messageQueue->enqueue(new FlashMessage(
333 '',
334 'No database changes selected',
335 FlashMessage::WARNING
336 ));
337 } else {
338 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
339 $sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
340 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
341 $statementHashesToPerform = array_flip($selectedHashes);
342 $results = $schemaMigrationService->migrate($sqlStatements, $statementHashesToPerform);
343 // Create error flash messages if any
344 foreach ($results as $errorMessage) {
345 $messageQueue->enqueue(new FlashMessage(
346 'Error: ' . $errorMessage,
347 'Database update failed',
348 FlashMessage::ERROR
349 ));
350 }
351 $messageQueue->enqueue(new FlashMessage(
352 '',
353 'Executed database updates'
354 ));
355 }
356 return new JsonResponse([
357 'success' => true,
358 'status' => $messageQueue,
359 ]);
360 }
361
362 /**
363 * Clear table overview statistics action
364 *
365 * @return ResponseInterface
366 */
367 public function clearTablesStatsAction(): ResponseInterface
368 {
369 return new JsonResponse([
370 'success' => true,
371 'stats' => (new ClearTableService())->getTableStatistics(),
372 ]);
373 }
374
375 /**
376 * Truncate a specific table
377 *
378 * @param ServerRequestInterface $request
379 * @return ResponseInterface
380 */
381 public function clearTablesClearAction(ServerRequestInterface $request): ResponseInterface
382 {
383 $table = $request->getParsedBody()['install']['table'];
384 if (empty($table)) {
385 throw new \RuntimeException(
386 'No table name given',
387 1501944076
388 );
389 }
390 (new ClearTableService())->clearSelectedTable($table);
391 $messageQueue = (new FlashMessageQueue('install'))->enqueue(
392 new FlashMessage('Cleared table')
393 );
394 return new JsonResponse([
395 'success' => true,
396 'status' => $messageQueue
397 ]);
398 }
399
400 /**
401 * Create a backend administrator from given username and password
402 *
403 * @param ServerRequestInterface $request
404 * @return ResponseInterface
405 */
406 public function createAdminAction(ServerRequestInterface $request): ResponseInterface
407 {
408 $username = preg_replace('/\\s/i', '', $request->getParsedBody()['install']['userName']);
409 $password = $request->getParsedBody()['install']['userPassword'];
410 $passwordCheck = $request->getParsedBody()['install']['userPasswordCheck'];
411 $isSystemMaintainer = ((bool)$request->getParsedBody()['install']['userSystemMaintainer'] == '1') ? true : false;
412
413 $messages = new FlashMessageQueue('install');
414
415 if (strlen($username) < 1) {
416 $messages->enqueue(new FlashMessage(
417 'No valid username given.',
418 'Administrator user not created',
419 FlashMessage::ERROR
420 ));
421 } elseif ($password !== $passwordCheck) {
422 $messages->enqueue(new FlashMessage(
423 'Passwords do not match.',
424 'Administrator user not created',
425 FlashMessage::ERROR
426 ));
427 } elseif (strlen($password) < 8) {
428 $messages->enqueue(new FlashMessage(
429 'Password must be at least eight characters long.',
430 'Administrator user not created',
431 FlashMessage::ERROR
432 ));
433 } else {
434 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
435 $userExists = $connectionPool->getConnectionForTable('be_users')
436 ->count(
437 'uid',
438 'be_users',
439 ['username' => $username]
440 );
441 if ($userExists) {
442 $messages->enqueue(new FlashMessage(
443 'A user with username "' . $username . '" exists already.',
444 'Administrator user not created',
445 FlashMessage::ERROR
446 ));
447 } else {
448 $saltFactory = SaltFactory::getSaltingInstance(null, 'BE');
449 $hashedPassword = $saltFactory->getHashedPassword($password);
450 $adminUserFields = [
451 'username' => $username,
452 'password' => $hashedPassword,
453 'admin' => 1,
454 'tstamp' => $GLOBALS['EXEC_TIME'],
455 'crdate' => $GLOBALS['EXEC_TIME']
456 ];
457 $connectionPool->getConnectionForTable('be_users')->insert('be_users', $adminUserFields);
458
459 if ($isSystemMaintainer) {
460
461 // Get the new admin user uid juste created
462 $newAdminUserUid = (int)$connectionPool->getConnectionForTable('be_users')->lastInsertId('be_users');
463
464 // Get the list of the existing systemMaintainer
465 $existingSystemMaintainersList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
466
467 // Add the new admin user to the existing systemMaintainer list
468 $newSystemMaintainersList = $existingSystemMaintainersList;
469 $newSystemMaintainersList[] = $newAdminUserUid;
470
471 // Update the LocalConfiguration.php file with the new list
472 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
473 $configurationManager->setLocalConfigurationValuesByPathValuePairs(
474 ['SYS/systemMaintainers' => $newSystemMaintainersList]
475 );
476 }
477
478 $messages->enqueue(new FlashMessage(
479 '',
480 'Administrator created with username "' . $username . '".'
481 ));
482 }
483 }
484 return new JsonResponse([
485 'success' => true,
486 'status' => $messages,
487 ]);
488 }
489
490 /**
491 * Set 'uc' field of all backend users to empty string
492 *
493 * @return ResponseInterface
494 */
495 public function resetBackendUserUcAction(): ResponseInterface
496 {
497 GeneralUtility::makeInstance(ConnectionPool::class)
498 ->getQueryBuilderForTable('be_users')
499 ->update('be_users')
500 ->set('uc', '')
501 ->execute();
502 $messageQueue = new FlashMessageQueue('install');
503 $messageQueue->enqueue(new FlashMessage(
504 '',
505 'Reset all backend users preferences'
506 ));
507 return new JsonResponse([
508 'success' => true,
509 'status' => $messageQueue
510 ]);
511 }
512 }