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