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