1<?php
2
3declare(strict_types=1);
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
18namespace TYPO3\CMS\Install\Controller;
19
20use Psr\Http\Message\ResponseInterface;
21use Psr\Http\Message\ServerRequestInterface;
22use TYPO3\CMS\Core\Cache\CacheManager;
23use TYPO3\CMS\Core\Configuration\ConfigurationManager;
24use TYPO3\CMS\Core\Core\ClassLoadingInformation;
25use TYPO3\CMS\Core\Core\Environment;
26use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
27use TYPO3\CMS\Core\Database\ConnectionPool;
28use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
29use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
30use TYPO3\CMS\Core\Database\Schema\SqlReader;
31use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
32use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection;
33use TYPO3\CMS\Core\Http\JsonResponse;
34use TYPO3\CMS\Core\Localization\Locales;
35use TYPO3\CMS\Core\Messaging\FlashMessage;
36use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
37use TYPO3\CMS\Core\Service\OpcodeCacheService;
38use TYPO3\CMS\Core\Utility\GeneralUtility;
39use TYPO3\CMS\Install\Service\ClearCacheService;
40use TYPO3\CMS\Install\Service\ClearTableService;
41use TYPO3\CMS\Install\Service\LanguagePackService;
42use TYPO3\CMS\Install\Service\LateBootService;
43use TYPO3\CMS\Install\Service\Typo3tempFileService;
44
45/**
46 * Maintenance controller
47 * @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
48 */
49class MaintenanceController extends AbstractController
50{
51    /**
52     * @var LateBootService
53     */
54    private $lateBootService;
55
56    /**
57     * @var ClearCacheService
58     */
59    private $clearCacheService;
60
61    /**
62     * @var ConfigurationManager
63     */
64    private $configurationManager;
65
66    /**
67     * @var PasswordHashFactory
68     */
69    private $passwordHashFactory;
70
71    /**
72     * @var Locales
73     */
74    private $locales;
75
76    public function __construct(
77        LateBootService $lateBootService,
78        ClearCacheService $clearCacheService,
79        ConfigurationManager $configurationManager,
80        PasswordHashFactory $passwordHashFactory,
81        Locales $locales
82    ) {
83        $this->lateBootService = $lateBootService;
84        $this->clearCacheService = $clearCacheService;
85        $this->configurationManager = $configurationManager;
86        $this->passwordHashFactory = $passwordHashFactory;
87        $this->locales = $locales;
88    }
89    /**
90     * Main "show the cards" view
91     *
92     * @param ServerRequestInterface $request
93     * @return ResponseInterface
94     */
95    public function cardsAction(ServerRequestInterface $request): ResponseInterface
96    {
97        $view = $this->initializeStandaloneView($request, 'Maintenance/Cards.html');
98        return new JsonResponse([
99            'success' => true,
100            'html' => $view->render(),
101        ]);
102    }
103
104    /**
105     * Clear cache framework and opcode caches
106     *
107     * @return ResponseInterface
108     */
109    public function cacheClearAllAction(): ResponseInterface
110    {
111        $this->clearCacheService->clearAll();
112        GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
113        $messageQueue = (new FlashMessageQueue('install'))->enqueue(
114            new FlashMessage('Successfully cleared all caches and all available opcode caches.', 'Caches cleared')
115        );
116        return new JsonResponse([
117            'success' => true,
118            'status' => $messageQueue,
119        ]);
120    }
121
122    /**
123     * Clear typo3temp files statistics action
124     *
125     * @param ServerRequestInterface $request
126     * @return ResponseInterface
127     */
128    public function clearTypo3tempFilesStatsAction(ServerRequestInterface $request): ResponseInterface
129    {
130        $container = $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(false);
131        $typo3tempFileService = $container->get(Typo3tempFileService::class);
132
133        $view = $this->initializeStandaloneView($request, 'Maintenance/ClearTypo3tempFiles.html');
134        $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
135        $view->assignMultiple([
136            'clearTypo3tempFilesToken' => $formProtection->generateToken('installTool', 'clearTypo3tempFiles'),
137        ]);
138        return new JsonResponse(
139            [
140                'success' => true,
141                'stats' => $typo3tempFileService->getDirectoryStatistics(),
142                'html' => $view->render(),
143                'buttons' => [
144                    [
145                        'btnClass' => 'btn-default t3js-clearTypo3temp-stats',
146                        'text' => 'Scan again',
147                    ],
148                ],
149            ]
150        );
151    }
152
153    /**
154     * Clear typo3temp/assets or FAL processed Files
155     *
156     * @param ServerRequestInterface $request
157     * @return ResponseInterface
158     */
159    public function clearTypo3tempFilesAction(ServerRequestInterface $request): ResponseInterface
160    {
161        $container = $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(false);
162        $typo3tempFileService = $container->get(Typo3tempFileService::class);
163        $messageQueue = new FlashMessageQueue('install');
164        $folder = $request->getParsedBody()['install']['folder'];
165        // storageUid is an optional post param if FAL storages should be cleaned
166        $storageUid = $request->getParsedBody()['install']['storageUid'] ?? null;
167        if ($storageUid === null) {
168            $typo3tempFileService->clearAssetsFolder($folder);
169            $messageQueue->enqueue(new FlashMessage('The directory "' . $folder . '" has been cleared successfully', 'Directory cleared'));
170        } else {
171            $storageUid = (int)$storageUid;
172            // We have to get the stats before deleting files, otherwise we're not able to retrieve the amount of files anymore
173            $stats = $typo3tempFileService->getStatsFromStorageByUid($storageUid);
174            $failedDeletions = $typo3tempFileService->clearProcessedFiles($storageUid);
175            if ($failedDeletions) {
176                $messageQueue->enqueue(new FlashMessage(
177                    'Failed to delete ' . $failedDeletions . ' processed files. See TYPO3 log (by default typo3temp/var/log/typo3_*.log)',
178                    'Failed to delete files',
179                    FlashMessage::ERROR
180                ));
181            } else {
182                $messageQueue->enqueue(new FlashMessage(
183                    sprintf('Removed %d files from directory "%s"', $stats['numberOfFiles'], $stats['directory']),
184                    'Deleted processed files'
185                ));
186            }
187        }
188        return new JsonResponse([
189            'success' => true,
190            'status' => $messageQueue,
191        ]);
192    }
193
194    /**
195     * Dump autoload information
196     *
197     * @return ResponseInterface
198     */
199    public function dumpAutoloadAction(): ResponseInterface
200    {
201        $messageQueue = new FlashMessageQueue('install');
202        if (Environment::isComposerMode()) {
203            $messageQueue->enqueue(new FlashMessage(
204                'Skipped generating additional class loading information in Composer mode.',
205                'Autoloader not dumped',
206                FlashMessage::NOTICE
207            ));
208        } else {
209            ClassLoadingInformation::dumpClassLoadingInformation();
210            $messageQueue->enqueue(new FlashMessage(
211                'Successfully dumped class loading information for extensions.',
212                'Dumped autoloader'
213            ));
214        }
215        return new JsonResponse([
216            'success' => true,
217            'status' => $messageQueue,
218        ]);
219    }
220
221    /**
222     * Get main database analyzer modal HTML
223     *
224     * @param ServerRequestInterface $request
225     * @return ResponseInterface
226     */
227    public function databaseAnalyzerAction(ServerRequestInterface $request): ResponseInterface
228    {
229        $view = $this->initializeStandaloneView($request, 'Maintenance/DatabaseAnalyzer.html');
230        $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
231        $view->assignMultiple([
232            'databaseAnalyzerExecuteToken' => $formProtection->generateToken('installTool', 'databaseAnalyzerExecute'),
233        ]);
234        return new JsonResponse([
235            'success' => true,
236            'html' => $view->render(),
237            'buttons' => [
238                [
239                    'btnClass' => 'btn-default t3js-databaseAnalyzer-analyze',
240                    'text' => 'Run database compare again',
241                ], [
242                    'btnClass' => 'btn-warning t3js-databaseAnalyzer-execute',
243                    'text' => 'Apply selected changes',
244                ],
245            ],
246        ]);
247    }
248
249    /**
250     * Analyze current database situation
251     *
252     * @param ServerRequestInterface $request
253     * @return ResponseInterface
254     */
255    public function databaseAnalyzerAnalyzeAction(ServerRequestInterface $request): ResponseInterface
256    {
257        $container = $this->lateBootService->loadExtLocalconfDatabaseAndExtTables();
258        $messageQueue = new FlashMessageQueue('install');
259        $suggestions = [];
260        try {
261            $sqlReader = $container->get(SqlReader::class);
262            $sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
263            $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
264            $addCreateChange = $schemaMigrationService->getUpdateSuggestions($sqlStatements);
265
266            // Aggregate the per-connection statements into one flat array
267            $addCreateChange = array_merge_recursive(...array_values($addCreateChange));
268            if (!empty($addCreateChange['create_table'])) {
269                $suggestion = [
270                    'key' => 'addTable',
271                    'label' => 'Add tables',
272                    'enabled' => true,
273                    'children' => [],
274                ];
275                foreach ($addCreateChange['create_table'] as $hash => $statement) {
276                    $suggestion['children'][] = [
277                        'hash' => $hash,
278                        'statement' => $statement,
279                    ];
280                }
281                $suggestions[] = $suggestion;
282            }
283            if (!empty($addCreateChange['add'])) {
284                $suggestion = [
285                    'key' => 'addField',
286                    'label' => 'Add fields to tables',
287                    'enabled' => true,
288                    'children' => [],
289                ];
290                foreach ($addCreateChange['add'] as $hash => $statement) {
291                    $suggestion['children'][] = [
292                        'hash' => $hash,
293                        'statement' => $statement,
294                    ];
295                }
296                $suggestions[] = $suggestion;
297            }
298            if (!empty($addCreateChange['change'])) {
299                $suggestion = [
300                    'key' => 'change',
301                    'label' => 'Change fields',
302                    'enabled' => false,
303                    'children' => [],
304                ];
305                foreach ($addCreateChange['change'] as $hash => $statement) {
306                    $child = [
307                        'hash' => $hash,
308                        'statement' => $statement,
309                    ];
310                    if (isset($addCreateChange['change_currentValue'][$hash])) {
311                        $child['current'] = $addCreateChange['change_currentValue'][$hash];
312                    }
313                    $suggestion['children'][] = $child;
314                }
315                $suggestions[] = $suggestion;
316            }
317
318            // Difference from current to expected
319            $dropRename = $schemaMigrationService->getUpdateSuggestions($sqlStatements, true);
320
321            // Aggregate the per-connection statements into one flat array
322            $dropRename = array_merge_recursive(...array_values($dropRename));
323            if (!empty($dropRename['change_table'])) {
324                $suggestion = [
325                    'key' => 'renameTableToUnused',
326                    'label' => 'Remove tables (rename with prefix)',
327                    'enabled' => false,
328                    'children' => [],
329                ];
330                foreach ($dropRename['change_table'] as $hash => $statement) {
331                    $child = [
332                        'hash' => $hash,
333                        'statement' => $statement,
334                    ];
335                    if (!empty($dropRename['tables_count'][$hash])) {
336                        $child['rowCount'] = $dropRename['tables_count'][$hash];
337                    }
338                    $suggestion['children'][] = $child;
339                }
340                $suggestions[] = $suggestion;
341            }
342            if (!empty($dropRename['change'])) {
343                $suggestion = [
344                    'key' => 'renameTableFieldToUnused',
345                    'label' => 'Remove unused fields (rename with prefix)',
346                    'enabled' => false,
347                    'children' => [],
348                ];
349                foreach ($dropRename['change'] as $hash => $statement) {
350                    $suggestion['children'][] = [
351                        'hash' => $hash,
352                        'statement' => $statement,
353                    ];
354                }
355                $suggestions[] = $suggestion;
356            }
357            if (!empty($dropRename['drop'])) {
358                $suggestion = [
359                    'key' => 'deleteField',
360                    'label' => 'Drop fields (really!)',
361                    'enabled' => false,
362                    'children' => [],
363                ];
364                foreach ($dropRename['drop'] as $hash => $statement) {
365                    $suggestion['children'][] = [
366                        'hash' => $hash,
367                        'statement' => $statement,
368                    ];
369                }
370                $suggestions[] = $suggestion;
371            }
372            if (!empty($dropRename['drop_table'])) {
373                $suggestion = [
374                    'key' => 'deleteTable',
375                    'label' => 'Drop tables (really!)',
376                    'enabled' => false,
377                    'children' => [],
378                ];
379                foreach ($dropRename['drop_table'] as $hash => $statement) {
380                    $child = [
381                        'hash' => $hash,
382                        'statement' => $statement,
383                    ];
384                    if (!empty($dropRename['tables_count'][$hash])) {
385                        $child['rowCount'] = $dropRename['tables_count'][$hash];
386                    }
387                    $suggestion['children'][] = $child;
388                }
389                $suggestions[] = $suggestion;
390            }
391        } catch (StatementException $e) {
392            $messageQueue->enqueue(new FlashMessage(
393                $e->getMessage(),
394                'Database analysis failed',
395                FlashMessage::ERROR
396            ));
397        }
398        return new JsonResponse([
399            'success' => true,
400            'status' => $messageQueue,
401            'suggestions' => $suggestions,
402        ]);
403    }
404
405    /**
406     * Apply selected database changes
407     *
408     * @param ServerRequestInterface $request
409     * @return ResponseInterface
410     */
411    public function databaseAnalyzerExecuteAction(ServerRequestInterface $request): ResponseInterface
412    {
413        $container = $this->lateBootService->loadExtLocalconfDatabaseAndExtTables();
414        $messageQueue = new FlashMessageQueue('install');
415        $selectedHashes = $request->getParsedBody()['install']['hashes'] ?? [];
416        if (empty($selectedHashes)) {
417            $messageQueue->enqueue(new FlashMessage(
418                'Please select any change by activating their respective checkboxes.',
419                'No database changes selected',
420                FlashMessage::WARNING
421            ));
422        } else {
423            $sqlReader = $container->get(SqlReader::class);
424            $sqlStatements = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
425            $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
426            $statementHashesToPerform = array_flip($selectedHashes);
427            $results = $schemaMigrationService->migrate($sqlStatements, $statementHashesToPerform);
428            // Create error flash messages if any
429            foreach ($results as $errorMessage) {
430                $messageQueue->enqueue(new FlashMessage(
431                    'Error: ' . $errorMessage,
432                    'Database update failed',
433                    FlashMessage::ERROR
434                ));
435            }
436            $messageQueue->enqueue(new FlashMessage(
437                'Executed database updates',
438                'Executed database updates'
439            ));
440        }
441        return new JsonResponse([
442            'success' => true,
443            'status' => $messageQueue,
444        ]);
445    }
446
447    /**
448     * Clear table overview statistics action
449     *
450     * @param ServerRequestInterface $request
451     * @return ResponseInterface
452     */
453    public function clearTablesStatsAction(ServerRequestInterface $request): ResponseInterface
454    {
455        $view = $this->initializeStandaloneView($request, 'Maintenance/ClearTables.html');
456        $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
457        $view->assignMultiple([
458            'clearTablesClearToken' => $formProtection->generateToken('installTool', 'clearTablesClear'),
459        ]);
460        return new JsonResponse([
461            'success' => true,
462            'stats' => (new ClearTableService())->getTableStatistics(),
463            'html' => $view->render(),
464            'buttons' => [
465                [
466                    'btnClass' => 'btn-default t3js-clearTables-stats',
467                    'text' => 'Scan again',
468                ],
469            ],
470        ]);
471    }
472
473    /**
474     * Truncate a specific table
475     *
476     * @param ServerRequestInterface $request
477     * @return ResponseInterface
478     * @throws \RuntimeException
479     */
480    public function clearTablesClearAction(ServerRequestInterface $request): ResponseInterface
481    {
482        $table = $request->getParsedBody()['install']['table'];
483        if (empty($table)) {
484            throw new \RuntimeException(
485                'No table name given',
486                1501944076
487            );
488        }
489        (new ClearTableService())->clearSelectedTable($table);
490        $messageQueue = (new FlashMessageQueue('install'))->enqueue(
491            new FlashMessage('The table ' . $table . ' has been cleared.', 'Table cleared')
492        );
493        return new JsonResponse([
494            'success' => true,
495            'status' => $messageQueue,
496        ]);
497    }
498    /**
499     * Create Admin Get Data action
500     *
501     * @param ServerRequestInterface $request
502     * @return ResponseInterface
503     */
504    public function createAdminGetDataAction(ServerRequestInterface $request): ResponseInterface
505    {
506        $view = $this->initializeStandaloneView($request, 'Maintenance/CreateAdmin.html');
507        $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
508        $view->assignMultiple([
509            'createAdminToken' => $formProtection->generateToken('installTool', 'createAdmin'),
510        ]);
511        return new JsonResponse([
512            'success' => true,
513            'html' => $view->render(),
514            'buttons' => [
515                [
516                    'btnClass' => 'btn-default t3js-createAdmin-create',
517                    'text' => 'Create administrator user',
518                ],
519            ],
520        ]);
521    }
522
523    /**
524     * Create a backend administrator from given username and password
525     *
526     * @param ServerRequestInterface $request
527     * @return ResponseInterface
528     */
529    public function createAdminAction(ServerRequestInterface $request): ResponseInterface
530    {
531        $username = preg_replace('/\\s/i', '', $request->getParsedBody()['install']['userName']);
532        $password = $request->getParsedBody()['install']['userPassword'];
533        $passwordCheck = $request->getParsedBody()['install']['userPasswordCheck'];
534        $email = $request->getParsedBody()['install']['userEmail'] ?? '';
535        $isSystemMaintainer = ((bool)$request->getParsedBody()['install']['userSystemMaintainer'] == '1') ? true : false;
536
537        $messages = new FlashMessageQueue('install');
538
539        if ($username === '') {
540            $messages->enqueue(new FlashMessage(
541                'No username given.',
542                'Administrator user not created',
543                FlashMessage::ERROR
544            ));
545        } elseif ($password !== $passwordCheck) {
546            $messages->enqueue(new FlashMessage(
547                'Passwords do not match.',
548                'Administrator user not created',
549                FlashMessage::ERROR
550            ));
551        } elseif (strlen($password) < 8) {
552            $messages->enqueue(new FlashMessage(
553                'Password must be at least eight characters long.',
554                'Administrator user not created',
555                FlashMessage::ERROR
556            ));
557        } else {
558            $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
559            $userExists = $connectionPool->getConnectionForTable('be_users')
560                ->count(
561                    'uid',
562                    'be_users',
563                    ['username' => $username]
564                );
565            if ($userExists) {
566                $messages->enqueue(new FlashMessage(
567                    'A user with username "' . $username . '" exists already.',
568                    'Administrator user not created',
569                    FlashMessage::ERROR
570                ));
571            } else {
572                $hashInstance = $this->passwordHashFactory->getDefaultHashInstance('BE');
573                $hashedPassword = $hashInstance->getHashedPassword($password);
574                $adminUserFields = [
575                    'username' => $username,
576                    'password' => $hashedPassword,
577                    'admin' => 1,
578                    'tstamp' => $GLOBALS['EXEC_TIME'],
579                    'crdate' => $GLOBALS['EXEC_TIME'],
580                ];
581                if (GeneralUtility::validEmail($email)) {
582                    $adminUserFields['email'] = $email;
583                }
584                $connectionPool->getConnectionForTable('be_users')->insert('be_users', $adminUserFields);
585
586                if ($isSystemMaintainer) {
587
588                    // Get the new admin user uid just created
589                    $newAdminUserUid = (int)$connectionPool->getConnectionForTable('be_users')->lastInsertId('be_users');
590
591                    // Get the list of the existing systemMaintainer
592                    $existingSystemMaintainersList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
593
594                    // Add the new admin user to the existing systemMaintainer list
595                    $newSystemMaintainersList = $existingSystemMaintainersList;
596                    $newSystemMaintainersList[] = $newAdminUserUid;
597
598                    // Update the LocalConfiguration.php file with the new list
599                    $this->configurationManager->setLocalConfigurationValuesByPathValuePairs(
600                        ['SYS/systemMaintainers' => $newSystemMaintainersList]
601                    );
602                }
603
604                $messages->enqueue(new FlashMessage(
605                    'Administrator created',
606                    'An administrator with username "' . $username . '" has been created successfully.'
607                ));
608            }
609        }
610        return new JsonResponse([
611            'success' => true,
612            'status' => $messages,
613        ]);
614    }
615
616    /**
617     * Entry action of language packs module gets
618     * * list of available languages with details like active or not and last update
619     * * list of loaded extensions
620     *
621     * @param ServerRequestInterface $request
622     * @return ResponseInterface
623     */
624    public function languagePacksGetDataAction(ServerRequestInterface $request): ResponseInterface
625    {
626        $view = $this->initializeStandaloneView($request, 'Maintenance/LanguagePacks.html');
627        $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
628        $view->assignMultiple([
629            'languagePacksActivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksActivateLanguage'),
630            'languagePacksDeactivateLanguageToken' => $formProtection->generateToken('installTool', 'languagePacksDeactivateLanguage'),
631            'languagePacksUpdatePackToken' => $formProtection->generateToken('installTool', 'languagePacksUpdatePack'),
632            'languagePacksUpdateIsoTimesToken' => $formProtection->generateToken('installTool', 'languagePacksUpdateIsoTimes'),
633        ]);
634        // This action needs TYPO3_CONF_VARS for full GeneralUtility::getUrl() config
635        $container = $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(false, true);
636        $languagePackService = $container->get(LanguagePackService::class);
637        $extensions = $languagePackService->getExtensionLanguagePackDetails();
638        return new JsonResponse([
639            'success' => true,
640            'languages' => $languagePackService->getLanguageDetails(),
641            'extensions' => $extensions,
642            'activeLanguages' => $languagePackService->getActiveLanguages(),
643            'activeExtensions' => array_column($extensions, 'key'),
644            'html' => $view->render(),
645        ]);
646    }
647
648    /**
649     * Activate a language and any possible dependency it may have
650     *
651     * @param ServerRequestInterface $request
652     * @return ResponseInterface
653     */
654    public function languagePacksActivateLanguageAction(ServerRequestInterface $request): ResponseInterface
655    {
656        $messageQueue = new FlashMessageQueue('install');
657        $languagePackService = GeneralUtility::makeInstance(LanguagePackService::class);
658        $availableLanguages = $languagePackService->getAvailableLanguages();
659        $activeLanguages = $languagePackService->getActiveLanguages();
660        $iso = $request->getParsedBody()['install']['iso'];
661        $activateArray = [];
662        foreach ($availableLanguages as $availableIso => $name) {
663            if ($availableIso === $iso && !in_array($availableIso, $activeLanguages, true)) {
664                $activateArray[] = $iso;
665                $dependencies = $this->locales->getLocaleDependencies($availableIso);
666                if (!empty($dependencies)) {
667                    foreach ($dependencies as $dependency) {
668                        if (!in_array($dependency, $activeLanguages, true)) {
669                            $activateArray[] = $dependency;
670                        }
671                    }
672                }
673            }
674        }
675        if (!empty($activateArray)) {
676            $activeLanguages = array_merge($activeLanguages, $activateArray);
677            sort($activeLanguages);
678            $this->configurationManager->setLocalConfigurationValueByPath(
679                'EXTCONF/lang',
680                ['availableLanguages' => $activeLanguages]
681            );
682            $activationArray = [];
683            foreach ($activateArray as $activateIso) {
684                $activationArray[] = $availableLanguages[$activateIso] . ' (' . $activateIso . ')';
685            }
686            $messageQueue->enqueue(
687                new FlashMessage(
688                    'These languages have been activated: ' . implode(', ', $activationArray)
689                )
690            );
691        } else {
692            $messageQueue->enqueue(
693                new FlashMessage('Language with ISO code "' . $iso . '" not found or already active.', '', FlashMessage::ERROR)
694            );
695        }
696        return new JsonResponse([
697            'success' => true,
698            'status' => $messageQueue,
699        ]);
700    }
701
702    /**
703     * Deactivate a language if no other active language depends on it
704     *
705     * @param ServerRequestInterface $request
706     * @return ResponseInterface
707     * @throws \RuntimeException
708     */
709    public function languagePacksDeactivateLanguageAction(ServerRequestInterface $request): ResponseInterface
710    {
711        $messageQueue = new FlashMessageQueue('install');
712        $languagePackService = GeneralUtility::makeInstance(LanguagePackService::class);
713        $availableLanguages = $languagePackService->getAvailableLanguages();
714        $activeLanguages = $languagePackService->getActiveLanguages();
715        $iso = $request->getParsedBody()['install']['iso'];
716        if (empty($iso)) {
717            throw new \RuntimeException('No iso code given', 1520109807);
718        }
719        $otherActiveLanguageDependencies = [];
720        foreach ($activeLanguages as $activeLanguage) {
721            if ($activeLanguage === $iso) {
722                continue;
723            }
724            $dependencies = $this->locales->getLocaleDependencies($activeLanguage);
725            if (in_array($iso, $dependencies, true)) {
726                $otherActiveLanguageDependencies[] = $activeLanguage;
727            }
728        }
729        if (!empty($otherActiveLanguageDependencies)) {
730            // Error: Must disable dependencies first
731            $dependentArray = [];
732            foreach ($otherActiveLanguageDependencies as $dependency) {
733                $dependentArray[] = $availableLanguages[$dependency] . ' (' . $dependency . ')';
734            }
735            $messageQueue->enqueue(
736                new FlashMessage(
737                    'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" can not be deactivated. These'
738                    . ' other languages depend on it and need to be deactivated before:'
739                    . implode(', ', $dependentArray),
740                    '',
741                    FlashMessage::ERROR
742                )
743            );
744        } else {
745            if (in_array($iso, $activeLanguages, true)) {
746                // Deactivate this language
747                $newActiveLanguages = [];
748                foreach ($activeLanguages as $activeLanguage) {
749                    if ($activeLanguage === $iso) {
750                        continue;
751                    }
752                    $newActiveLanguages[] = $activeLanguage;
753                }
754                $this->configurationManager->setLocalConfigurationValueByPath(
755                    'EXTCONF/lang',
756                    ['availableLanguages' => $newActiveLanguages]
757                );
758                $messageQueue->enqueue(
759                    new FlashMessage(
760                        'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has been deactivated'
761                    )
762                );
763            } else {
764                $messageQueue->enqueue(
765                    new FlashMessage(
766                        'Language "' . $availableLanguages[$iso] . ' (' . $iso . ')" has not been deactivated',
767                        '',
768                        FlashMessage::ERROR
769                    )
770                );
771            }
772        }
773        return new JsonResponse([
774            'success' => true,
775            'status' => $messageQueue,
776        ]);
777    }
778
779    /**
780     * Update a pack of one extension and one language
781     *
782     * @param ServerRequestInterface $request
783     * @return ResponseInterface
784     * @throws \RuntimeException
785     */
786    public function languagePacksUpdatePackAction(ServerRequestInterface $request): ResponseInterface
787    {
788        $container = $this->lateBootService->loadExtLocalconfDatabaseAndExtTables(false, true);
789        $iso = $request->getParsedBody()['install']['iso'];
790        $key = $request->getParsedBody()['install']['extension'];
791
792        $languagePackService = $container->get(LanguagePackService::class);
793
794        return new JsonResponse([
795            'success' => true,
796            'packResult' => $languagePackService->languagePackDownload($key, $iso),
797        ]);
798    }
799
800    /**
801     * Set "last updated" time in registry for fully updated language packs.
802     *
803     * @param ServerRequestInterface $request
804     * @return ResponseInterface
805     */
806    public function languagePacksUpdateIsoTimesAction(ServerRequestInterface $request): ResponseInterface
807    {
808        $isos = $request->getParsedBody()['install']['isos'];
809        $languagePackService = GeneralUtility::makeInstance(LanguagePackService::class);
810        $languagePackService->setLastUpdatedIsoCode($isos);
811
812        // The cache manager is already instantiated in the install tool
813        // with some hacked settings to disable caching of extbase and fluid.
814        // We want a "fresh" object here to operate on a different cache setup.
815        // cacheManager implements SingletonInterface, so the only way to get a "fresh"
816        // instance is by circumventing makeInstance and using new directly!
817        $cacheManager = new CacheManager();
818        $cacheManager->setCacheConfigurations($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']);
819        $cacheManager->getCache('l10n')->flush();
820
821        return new JsonResponse(['success' => true]);
822    }
823
824    /**
825     * Set 'uc' field of all backend users to empty string
826     *
827     * @return ResponseInterface
828     */
829    public function resetBackendUserUcAction(): ResponseInterface
830    {
831        GeneralUtility::makeInstance(ConnectionPool::class)
832            ->getQueryBuilderForTable('be_users')
833            ->update('be_users')
834            ->set('uc', '')
835            ->executeStatement();
836        $messageQueue = new FlashMessageQueue('install');
837        $messageQueue->enqueue(new FlashMessage(
838            'Preferences of all backend users have been reset',
839            'Reset preferences of all backend users'
840        ));
841        return new JsonResponse([
842            'success' => true,
843            'status' => $messageQueue,
844        ]);
845    }
846}
847