[TASK] Do not repopulate $GLOBALS['TYPO3_CONF_VARS'] in installer
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Controller / InstallerController.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 Doctrine\DBAL\DBALException;
19 use Doctrine\DBAL\DriverManager;
20 use Psr\Http\Message\ResponseInterface;
21 use Psr\Http\Message\ServerRequestInterface;
22 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
23 use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
24 use TYPO3\CMS\Core\Core\Bootstrap;
25 use TYPO3\CMS\Core\Database\Connection;
26 use TYPO3\CMS\Core\Database\ConnectionPool;
27 use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
28 use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
29 use TYPO3\CMS\Core\Database\Schema\SqlReader;
30 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
31 use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection;
32 use TYPO3\CMS\Core\Http\HtmlResponse;
33 use TYPO3\CMS\Core\Http\JsonResponse;
34 use TYPO3\CMS\Core\Messaging\FlashMessage;
35 use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
36 use TYPO3\CMS\Core\Package\PackageInterface;
37 use TYPO3\CMS\Core\Package\PackageManager;
38 use TYPO3\CMS\Core\Registry;
39 use TYPO3\CMS\Core\Utility\GeneralUtility;
40 use TYPO3\CMS\Fluid\View\StandaloneView;
41 use TYPO3\CMS\Install\Configuration\FeatureManager;
42 use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
43 use TYPO3\CMS\Install\Service\EnableFileService;
44 use TYPO3\CMS\Install\Service\Exception\ConfigurationChangedException;
45 use TYPO3\CMS\Install\Service\SilentConfigurationUpgradeService;
46 use TYPO3\CMS\Install\SystemEnvironment\Check;
47 use TYPO3\CMS\Install\SystemEnvironment\SetupCheck;
48 use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
49
50 /**
51 * Install step controller, dispatcher class of step actions.
52 */
53 class InstallerController
54 {
55 /**
56 * Init action loads <head> with JS initiating further stuff
57 *
58 * @return ResponseInterface
59 */
60 public function initAction(): ResponseInterface
61 {
62 $view = $this->initializeStandaloneView('Installer/Init.html');
63 return new HtmlResponse(
64 $view->render(),
65 200,
66 [
67 'Cache-Control' => 'no-cache, must-revalidate',
68 'Pragma' => 'no-cache'
69 ]
70 );
71 }
72
73 /**
74 * Main layout with progress bar, header
75 *
76 * @return ResponseInterface
77 */
78 public function mainLayoutAction(): ResponseInterface
79 {
80 $view = $this->initializeStandaloneView('Installer/MainLayout.html');
81 return new JsonResponse([
82 'success' => true,
83 'html' => $view->render(),
84 ]);
85 }
86
87 /**
88 * Render "FIRST_INSTALL file need to exist" view
89 *
90 * @return ResponseInterface
91 */
92 public function showInstallerNotAvailableAction(): ResponseInterface
93 {
94 $view = $this->initializeStandaloneView('Installer/ShowInstallerNotAvailable.html');
95 return new JsonResponse([
96 'success' => true,
97 'html' => $view->render(),
98 ]);
99 }
100
101 /**
102 * Check if "environment and folders" should be shown
103 *
104 * @return ResponseInterface
105 */
106 public function checkEnvironmentAndFoldersAction(): ResponseInterface
107 {
108 return new JsonResponse([
109 'success' => @is_file(PATH_typo3conf . 'LocalConfiguration.php'),
110 ]);
111 }
112
113 /**
114 * Render "environment and folders"
115 *
116 * @return ResponseInterface
117 */
118 public function showEnvironmentAndFoldersAction(): ResponseInterface
119 {
120 $view = $this->initializeStandaloneView('Installer/ShowEnvironmentAndFolders.html');
121 $systemCheckMessageQueue = new FlashMessageQueue('install');
122 $checkMessages = (new Check())->getStatus();
123 foreach ($checkMessages as $message) {
124 $systemCheckMessageQueue->enqueue($message);
125 }
126 $setupCheckMessages = (new SetupCheck())->getStatus();
127 foreach ($setupCheckMessages as $message) {
128 $systemCheckMessageQueue->enqueue($message);
129 }
130 $folderStructureFactory = GeneralUtility::makeInstance(DefaultFactory::class);
131 $structureFacade = $folderStructureFactory->getStructure();
132 $structureMessageQueue = $structureFacade->getStatus();
133 return new JsonResponse([
134 'success' => true,
135 'html' => $view->render(),
136 'environmentStatusErrors' => $systemCheckMessageQueue->getAllMessages(FlashMessage::ERROR),
137 'environmentStatusWarnings' => $systemCheckMessageQueue->getAllMessages(FlashMessage::WARNING),
138 'structureErrors' => $structureMessageQueue->getAllMessages(FlashMessage::ERROR),
139 ]);
140 }
141
142 /**
143 * Create main folder layout, LocalConfiguration, PackageStates
144 *
145 * @return ResponseInterface
146 */
147 public function executeEnvironmentAndFoldersAction(): ResponseInterface
148 {
149 $folderStructureFactory = GeneralUtility::makeInstance(DefaultFactory::class);
150 $structureFacade = $folderStructureFactory->getStructure();
151 $structureFixMessageQueue = $structureFacade->fix();
152 $errorsFromStructure = $structureFixMessageQueue->getAllMessages(FlashMessage::ERROR);
153
154 if (@is_dir(PATH_typo3conf)) {
155 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
156 $configurationManager->createLocalConfigurationFromFactoryConfiguration();
157
158 // Create a PackageStates.php with all packages activated marked as "part of factory default"
159 if (!file_exists(PATH_typo3conf . 'PackageStates.php')) {
160 $packageManager = Bootstrap::getInstance()->getEarlyInstance(PackageManager::class);
161 $packages = $packageManager->getAvailablePackages();
162 foreach ($packages as $package) {
163 if ($package instanceof PackageInterface
164 && $package->isPartOfFactoryDefault()
165 ) {
166 $packageManager->activatePackage($package->getPackageKey());
167 }
168 }
169 $packageManager->forceSortAndSavePackageStates();
170 }
171 $extensionConfiguration = new ExtensionConfiguration();
172 $extensionConfiguration->synchronizeExtConfTemplateWithLocalConfigurationOfAllExtensions();
173
174 return new JsonResponse([
175 'success' => true,
176 ]);
177 }
178 return new JsonResponse([
179 'success' => false,
180 'status' => $errorsFromStructure,
181 ]);
182 }
183
184 /**
185 * Check if trusted hosts pattern needs to be adjusted
186 *
187 * @return ResponseInterface
188 */
189 public function checkTrustedHostsPatternAction(): ResponseInterface
190 {
191 return new JsonResponse([
192 'success' => GeneralUtility::hostHeaderValueMatchesTrustedHostsPattern($_SERVER['HTTP_HOST']),
193 ]);
194 }
195
196 /**
197 * Adjust trusted hosts pattern to '.*' if it does not match yet
198 *
199 * @return ResponseInterface
200 */
201 public function executeAdjustTrustedHostsPatternAction(): ResponseInterface
202 {
203 if (!GeneralUtility::hostHeaderValueMatchesTrustedHostsPattern($_SERVER['HTTP_HOST'])) {
204 $configurationManager = new ConfigurationManager();
205 $configurationManager->setLocalConfigurationValueByPath('SYS/trustedHostsPattern', '.*');
206 }
207 return new JsonResponse([
208 'success' => true,
209 ]);
210 }
211
212 /**
213 * Execute silent configuration update. May be called multiple times until success = true is returned.
214 *
215 * @return ResponseInterface success = true if no change has been done
216 */
217 public function executeSilentConfigurationUpdateAction(): ResponseInterface
218 {
219 $silentUpdate = new SilentConfigurationUpgradeService();
220 $success = true;
221 try {
222 $silentUpdate->execute();
223 } catch (ConfigurationChangedException $e) {
224 $success = false;
225 }
226 return new JsonResponse([
227 'success' => $success,
228 ]);
229 }
230
231 /**
232 * Check if database connect step needs to be shown
233 *
234 * @return ResponseInterface
235 */
236 public function checkDatabaseConnectAction(): ResponseInterface
237 {
238 return new JsonResponse([
239 'success' => $this->isDatabaseConnectSuccessful() && $this->isDatabaseConfigurationComplete(),
240 ]);
241 }
242
243 /**
244 * Show database connect step
245 *
246 * @return ResponseInterface
247 */
248 public function showDatabaseConnectAction(): ResponseInterface
249 {
250 $view = $this->initializeStandaloneView('Installer/ShowDatabaseConnect.html');
251 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
252 $hasAtLeastOneOption = false;
253 $activeAvailableOption = '';
254 if (extension_loaded('mysqli')) {
255 $hasAtLeastOneOption = true;
256 $view->assign('hasMysqliManualConfiguration', true);
257 $mysqliManualConfigurationOptions = [
258 'username' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'] ?? '',
259 'password' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'] ?? '',
260 'port' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port'] ?? 3306,
261 ];
262 $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'] ?? '127.0.0.1';
263 if ($host === 'localhost') {
264 $host = '127.0.0.1';
265 }
266 $mysqliManualConfigurationOptions['host'] = $host;
267 $view->assign('mysqliManualConfigurationOptions', $mysqliManualConfigurationOptions);
268 $activeAvailableOption = 'mysqliManualConfiguration';
269
270 $view->assign('hasMysqliSocketManualConfiguration', true);
271 $view->assign(
272 'mysqliSocketManualConfigurationOptions',
273 [
274 'username' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'] ?? '',
275 'password' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'] ?? '',
276 'socket' => $this->getDatabaseConfiguredMysqliSocket(),
277 ]
278 );
279 if ($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driver'] === 'mysqli'
280 && $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'] === 'localhost') {
281 $activeAvailableOption = 'mysqliSocketManualConfiguration';
282 }
283 }
284 if (extension_loaded('pdo_pgsql')) {
285 $hasAtLeastOneOption = true;
286 $view->assign('hasPostgresManualConfiguration', true);
287 $view->assign(
288 'postgresManualConfigurationOptions',
289 [
290 'username' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'] ?? '',
291 'password' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'] ?? '',
292 'host' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'] ?? '127.0.0.1',
293 'port' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port'] ?? 5432,
294 'database' => $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] ?? '',
295 ]
296 );
297 if ($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driver'] === 'pdo_pgsql') {
298 $activeAvailableOption = 'postgresManualConfiguration';
299 }
300 }
301
302 if (!empty($this->getDatabaseConfigurationFromEnvironment())) {
303 $hasAtLeastOneOption = true;
304 $activeAvailableOption = 'configurationFromEnvironment';
305 $view->assign('hasConfigurationFromEnvironment', true);
306 }
307
308 $view->assignMultiple([
309 'hasAtLeastOneOption' => $hasAtLeastOneOption,
310 'activeAvailableOption' => $activeAvailableOption,
311 'executeDatabaseConnectToken' => $formProtection->generateToken('installTool', 'executeDatabaseConnect'),
312 ]);
313
314 return new JsonResponse([
315 'success' => true,
316 'html' => $view->render(),
317 ]);
318 }
319
320 /**
321 * Test database connect data
322 *
323 * @param ServerRequestInterface $request
324 * @return ResponseInterface
325 */
326 public function executeDatabaseConnectAction(ServerRequestInterface $request): ResponseInterface
327 {
328 $messages = [];
329 $postValues = $request->getParsedBody()['install']['values'];
330 $defaultConnectionSettings = [];
331
332 if ($postValues['availableSet'] === 'configurationFromEnvironment') {
333 $defaultConnectionSettings = $this->getDatabaseConfigurationFromEnvironment();
334 } else {
335 if (isset($postValues['driver'])) {
336 $validDrivers = [
337 'mysqli',
338 'pdo_mysql',
339 'pdo_pgsql',
340 'mssql',
341 ];
342 if (in_array($postValues['driver'], $validDrivers, true)) {
343 $defaultConnectionSettings['driver'] = $postValues['driver'];
344 } else {
345 $messages[] = new FlashMessage(
346 'Given driver must be one of ' . implode(', ', $validDrivers),
347 'Database driver unknown',
348 FlashMessage::ERROR
349 );
350 }
351 }
352 if (isset($postValues['username'])) {
353 $value = $postValues['username'];
354 if (strlen($value) <= 50) {
355 $defaultConnectionSettings['user'] = $value;
356 } else {
357 $messages[] = new FlashMessage(
358 'Given username must be shorter than fifty characters.',
359 'Database username not valid',
360 FlashMessage::ERROR
361 );
362 }
363 }
364 if (isset($postValues['password'])) {
365 $defaultConnectionSettings['password'] = $postValues['password'];
366 }
367 if (isset($postValues['host'])) {
368 $value = $postValues['host'];
369 if (preg_match('/^[a-zA-Z0-9_\\.-]+(:.+)?$/', $value) && strlen($value) <= 255) {
370 $defaultConnectionSettings['host'] = $value;
371 } else {
372 $messages[] = new FlashMessage(
373 'Given host is not alphanumeric (a-z, A-Z, 0-9 or _-.:) or longer than 255 characters.',
374 'Database host not valid',
375 FlashMessage::ERROR
376 );
377 }
378 }
379 if (isset($postValues['port']) && $postValues['host'] !== 'localhost') {
380 $value = $postValues['port'];
381 if (preg_match('/^[0-9]+(:.+)?$/', $value) && $value > 0 && $value <= 65535) {
382 $defaultConnectionSettings['port'] = (int)$value;
383 } else {
384 $messages[] = new FlashMessage(
385 'Given port is not numeric or within range 1 to 65535.',
386 'Database port not valid',
387 FlashMessage::ERROR
388 );
389 }
390 }
391 if (isset($postValues['socket']) && $postValues['socket'] !== '') {
392 if (@file_exists($postValues['socket'])) {
393 $defaultConnectionSettings['unix_socket'] = $postValues['socket'];
394 } else {
395 $messages[] = new FlashMessage(
396 'Given socket location does not exist on server.',
397 'Socket does not exist',
398 FlashMessage::ERROR
399 );
400 }
401 }
402 if (isset($postValues['database'])) {
403 $value = $postValues['database'];
404 if (strlen($value) <= 50) {
405 $defaultConnectionSettings['dbname'] = $value;
406 } else {
407 $messages[] = new FlashMessage(
408 'Given database name must be shorter than fifty characters.',
409 'Database name not valid',
410 FlashMessage::ERROR
411 );
412 }
413 }
414 }
415
416 $success = false;
417 if (!empty($defaultConnectionSettings)) {
418 // Test connection settings and write to config if connect is successful
419 try {
420 $connectionParams = $defaultConnectionSettings;
421 $connectionParams['wrapperClass'] = Connection::class;
422 $connectionParams['charset'] = 'utf-8';
423 DriverManager::getConnection($connectionParams)->ping();
424 $success = true;
425 } catch (DBALException $e) {
426 $messages[] = new FlashMessage(
427 'Connecting to the database with given settings failed: ' . $e->getMessage(),
428 'Database connect not successful',
429 FlashMessage::ERROR
430 );
431 }
432 $localConfigurationPathValuePairs = [];
433 foreach ($defaultConnectionSettings as $settingsName => $value) {
434 $localConfigurationPathValuePairs['DB/Connections/Default/' . $settingsName] = $value;
435 }
436 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
437 // Remove full default connection array
438 $configurationManager->removeLocalConfigurationKeysByPath(['DB/Connections/Default']);
439 // Write new values
440 $configurationManager->setLocalConfigurationValuesByPathValuePairs($localConfigurationPathValuePairs);
441 }
442
443 return new JsonResponse([
444 'success' => $success,
445 'status' => $messages,
446 ]);
447 }
448
449 /**
450 * Check if a database needs to be selected
451 *
452 * @return ResponseInterface
453 */
454 public function checkDatabaseSelectAction(): ResponseInterface
455 {
456 $success = false;
457 if ((string)$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] !== '') {
458 try {
459 $success = GeneralUtility::makeInstance(ConnectionPool::class)
460 ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME)
461 ->ping();
462 } catch (DBALException $e) {
463 }
464 }
465 return new JsonResponse([
466 'success' => $success,
467 ]);
468 }
469
470 /**
471 * Render "select a database"
472 *
473 * @return ResponseInterface
474 */
475 public function showDatabaseSelectAction(): ResponseInterface
476 {
477 $view = $this->initializeStandaloneView('Installer/ShowDatabaseSelect.html');
478 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
479 $errors = [];
480 try {
481 $view->assign('databaseList', $this->getDatabaseList());
482 } catch (\Exception $exception) {
483 $errors[] = $exception->getMessage();
484 }
485 $view->assignMultiple([
486 'errors' => $errors,
487 'executeDatabaseSelectToken' => $formProtection->generateToken('installTool', 'executeDatabaseSelect'),
488 ]);
489 return new JsonResponse([
490 'success' => true,
491 'html' => $view->render(),
492 ]);
493 }
494
495 /**
496 * Select / create and test a database
497 *
498 * @param ServerRequestInterface $request
499 * @return ResponseInterface
500 */
501 public function executeDatabaseSelectAction(ServerRequestInterface $request): ResponseInterface
502 {
503 $postValues = $request->getParsedBody()['install']['values'];
504 if ($postValues['type'] === 'new') {
505 $status = $this->createNewDatabase($postValues['new']);
506 if ($status->getSeverity() === FlashMessage::ERROR) {
507 return new JsonResponse([
508 'success' => false,
509 'status' => [$status],
510 ]);
511 }
512 } elseif ($postValues['type'] === 'existing' && !empty($postValues['existing'])) {
513 $status = $this->checkExistingDatabase($postValues['existing']);
514 if ($status->getSeverity() === FlashMessage::ERROR) {
515 return new JsonResponse([
516 'success' => false,
517 'status' => [$status],
518 ]);
519 }
520 } else {
521 return new JsonResponse([
522 'sucess' => true,
523 'status' => [
524 new FlashMessage(
525 'You must select a database.',
526 'No Database selected',
527 FlashMessage::ERROR
528 ),
529 ],
530 ]);
531 }
532 return new JsonResponse([
533 'success' => true,
534 ]);
535 }
536
537 /**
538 * Check if initial data needs to be imported
539 *
540 * @return ResponseInterface
541 */
542 public function checkDatabaseDataAction(): ResponseInterface
543 {
544 $existingTables = GeneralUtility::makeInstance(ConnectionPool::class)
545 ->getConnectionByName('Default')
546 ->getSchemaManager()
547 ->listTableNames();
548 return new JsonResponse([
549 'success' => !empty($existingTables),
550 ]);
551 }
552
553 /**
554 * Render "import initial data"
555 *
556 * @return ResponseInterface
557 */
558 public function showDatabaseDataAction(): ResponseInterface
559 {
560 $view = $this->initializeStandaloneView('Installer/ShowDatabaseData.html');
561 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
562 $view->assignMultiple([
563 'siteName' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
564 'executeDatabaseDataToken' => $formProtection->generateToken('installTool', 'executeDatabaseData'),
565 ]);
566 return new JsonResponse([
567 'success' => true,
568 'html' => $view->render(),
569 ]);
570 }
571
572 /**
573 * Create main db layout
574 *
575 * @param ServerRequestInterface $request
576 * @return ResponseInterface
577 */
578 public function executeDatabaseDataAction(ServerRequestInterface $request): ResponseInterface
579 {
580 $messages = [];
581 $configurationManager = new ConfigurationManager();
582 $postValues = $request->getParsedBody()['install']['values'];
583 $username = (string)$postValues['username'] !== '' ? $postValues['username'] : 'admin';
584 // Check password and return early if not good enough
585 $password = $postValues['password'];
586 if (empty($password) || strlen($password) < 8) {
587 $messages[] = new FlashMessage(
588 'You are setting an important password here! It gives an attacker full control over your instance if cracked.'
589 . ' It should be strong (include lower and upper case characters, special characters and numbers) and must be at least eight characters long.',
590 'Administrator password not secure enough!',
591 FlashMessage::ERROR
592 );
593 return new JsonResponse([
594 'success' => false,
595 'status' => $messages,
596 ]);
597 }
598 // Set site name
599 if (!empty($postValues['sitename'])) {
600 $configurationManager->setLocalConfigurationValueByPath('SYS/sitename', $postValues['sitename']);
601 }
602 try {
603 $messages = $this->importDatabaseData();
604 if (!empty($messages)) {
605 return new JsonResponse([
606 'success' => false,
607 'status' => $messages,
608 ]);
609 }
610 } catch (StatementException $exception) {
611 $messages[] = new FlashMessage(
612 'Error detected in SQL statement:' . LF . $exception->getMessage(),
613 'Import of database data could not be performed',
614 FlashMessage::ERROR
615 );
616 return new JsonResponse([
617 'success' => false,
618 'status' => $messages,
619 ]);
620 }
621 // Insert admin user
622 $adminUserFields = [
623 'username' => $username,
624 'password' => $this->getHashedPassword($password),
625 'admin' => 1,
626 'tstamp' => $GLOBALS['EXEC_TIME'],
627 'crdate' => $GLOBALS['EXEC_TIME']
628 ];
629 $databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users');
630 try {
631 $databaseConnection->insert('be_users', $adminUserFields);
632 $adminUserUid = (int)$databaseConnection->lastInsertId('be_users');
633 } catch (DBALException $exception) {
634 $messages[] = new FlashMessage(
635 'The administrator account could not be created. The following error occurred:' . LF
636 . $exception->getPrevious()->getMessage(),
637 'Administrator account not created!',
638 FlashMessage::ERROR
639 );
640 return new JsonResponse([
641 'success' => false,
642 'status' => $messages,
643 ]);
644 }
645 // Set password as install tool password, add admin user to system maintainers
646 $configurationManager->setLocalConfigurationValuesByPathValuePairs([
647 'BE/installToolPassword' => $this->getHashedPassword($password),
648 'SYS/systemMaintainers' => [$adminUserUid]
649 ]);
650 return new JsonResponse([
651 'success' => true,
652 'status' => $messages,
653 ]);
654 }
655
656 /**
657 * Show last "create empty site / install distribution"
658 *
659 * @return ResponseInterface
660 */
661 public function showDefaultConfigurationAction(): ResponseInterface
662 {
663 $view = $this->initializeStandaloneView('Installer/ShowDefaultConfiguration.html');
664 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
665 $view->assignMultiple([
666 'composerMode' => Bootstrap::usesComposerClassLoading(),
667 'executeDefaultConfigurationToken' => $formProtection->generateToken('installTool', 'executeDefaultConfiguration'),
668 ]);
669 return new JsonResponse([
670 'success' => true,
671 'html' => $view->render(),
672 ]);
673 }
674
675 /**
676 * Last step execution: clean up, remove FIRST_INSTALL file, ...
677 *
678 * @param ServerRequestInterface $request
679 * @return ResponseInterface
680 */
681 public function executeDefaultConfigurationAction(ServerRequestInterface $request): ResponseInterface
682 {
683 $featureManager = new FeatureManager();
684 // Get best matching configuration presets
685 $configurationValues = $featureManager->getBestMatchingConfigurationForAllFeatures();
686 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
687
688 // Let the admin user redirect to the distributions page on first login
689 switch ($request->getParsedBody()['install']['values']['sitesetup']) {
690 // Update the admin backend user to show the distribution management on login
691 case 'loaddistribution':
692 $adminUserFirstLogin = [
693 'startModuleOnFirstLogin' => 'tools_ExtensionmanagerExtensionmanager->tx_extensionmanager_tools_extensionmanagerextensionmanager%5Baction%5D=distributions&tx_extensionmanager_tools_extensionmanagerextensionmanager%5Bcontroller%5D=List',
694 'ucSetByInstallTool' => '1',
695 ];
696 $connectionPool->getConnectionForTable('be_users')->update(
697 'be_users',
698 ['uc' => serialize($adminUserFirstLogin)],
699 ['admin' => 1]
700 );
701 break;
702
703 // Create a page with UID 1 and PID1 and fluid_styled_content for page TS config, respect ownership
704 case 'createsite':
705 $databaseConnectionForPages = $connectionPool->getConnectionForTable('pages');
706 $databaseConnectionForPages->insert(
707 'pages',
708 [
709 'pid' => 0,
710 'crdate' => time(),
711 'cruser_id' => 1,
712 'tstamp' => time(),
713 'title' => 'Home',
714 'doktype' => 1,
715 'is_siteroot' => 1,
716 'perms_userid' => 1,
717 'perms_groupid' => 1,
718 'perms_user' => 31,
719 'perms_group' => 31,
720 'perms_everybody' => 1
721 ]
722 );
723 $pageUid = $databaseConnectionForPages->lastInsertId('pages');
724
725 // add a root sys_template with fluid_styled_content and a default PAGE typoscript snippet
726 $connectionPool->getConnectionForTable('sys_template')->insert(
727 'sys_template',
728 [
729 'pid' => $pageUid,
730 'crdate' => time(),
731 'cruser_id' => 1,
732 'tstamp' => time(),
733 'title' => 'Main TypoScript Rendering',
734 'sitetitle' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
735 'root' => 1,
736 'clear' => 1,
737 'include_static_file' => 'EXT:fluid_styled_content/Configuration/TypoScript/,EXT:fluid_styled_content/Configuration/TypoScript/Styling/',
738 'constants' => '',
739 'config' => 'page = PAGE
740 page.10 = TEXT
741 page.10.value (
742 <div style="width: 800px; margin: 15% auto;">
743 <div style="width: 300px;">
744 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 42"><path d="M60.2 14.4v27h-3.8v-27h-6.7v-3.3h17.1v3.3h-6.6zm20.2 12.9v14h-3.9v-14l-7.7-16.2h4.1l5.7 12.2 5.7-12.2h3.9l-7.8 16.2zm19.5 2.6h-3.6v11.4h-3.8V11.1s3.7-.3 7.3-.3c6.6 0 8.5 4.1 8.5 9.4 0 6.5-2.3 9.7-8.4 9.7m.4-16c-2.4 0-4.1.3-4.1.3v12.6h4.1c2.4 0 4.1-1.6 4.1-6.3 0-4.4-1-6.6-4.1-6.6m21.5 27.7c-7.1 0-9-5.2-9-15.8 0-10.2 1.9-15.1 9-15.1s9 4.9 9 15.1c.1 10.6-1.8 15.8-9 15.8m0-27.7c-3.9 0-5.2 2.6-5.2 12.1 0 9.3 1.3 12.4 5.2 12.4 3.9 0 5.2-3.1 5.2-12.4 0-9.4-1.3-12.1-5.2-12.1m19.9 27.7c-2.1 0-5.3-.6-5.7-.7v-3.1c1 .2 3.7.7 5.6.7 2.2 0 3.6-1.9 3.6-5.2 0-3.9-.6-6-3.7-6H138V24h3.1c3.5 0 3.7-3.6 3.7-5.3 0-3.4-1.1-4.8-3.2-4.8-1.9 0-4.1.5-5.3.7v-3.2c.5-.1 3-.7 5.2-.7 4.4 0 7 1.9 7 8.3 0 2.9-1 5.5-3.3 6.3 2.6.2 3.8 3.1 3.8 7.3 0 6.6-2.5 9-7.3 9"/><path fill="#FF8700" d="M31.7 28.8c-.6.2-1.1.2-1.7.2-5.2 0-12.9-18.2-12.9-24.3 0-2.2.5-3 1.3-3.6C12 1.9 4.3 4.2 1.9 7.2 1.3 8 1 9.1 1 10.6c0 9.5 10.1 31 17.3 31 3.3 0 8.8-5.4 13.4-12.8M28.4.5c6.6 0 13.2 1.1 13.2 4.8 0 7.6-4.8 16.7-7.2 16.7-4.4 0-9.9-12.1-9.9-18.2C24.5 1 25.6.5 28.4.5"/></svg>
745 </div>
746 <h4 style="font-family: sans-serif;">Welcome to a default website made with <a href="https://typo3.org">TYPO3</a></h4>
747 </div>
748 )
749 page.100 =< styles.content.get',
750 'description' => 'This is an Empty Site Package TypoScript template.
751
752 For each website you need a TypoScript template on the main page of your website (on the top level). For better maintenance all TypoScript should be extracted into external files via <INCLUDE_TYPOSCRIPT: source="FILE:EXT:site_myproject/Configuration/TypoScript/setup.typoscript">.'
753 ]
754 );
755 break;
756 }
757
758 // Mark upgrade wizards as done
759 $this->loadExtLocalconfDatabaseAndExtTables();
760 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'])) {
761 $registry = GeneralUtility::makeInstance(Registry::class);
762 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $updateClassName) {
763 $registry->set('installUpdate', $updateClassName, 1);
764 }
765 }
766
767 $configurationManager = new ConfigurationManager();
768 $configurationManager->setLocalConfigurationValuesByPathValuePairs($configurationValues);
769
770 $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
771 $formProtection->clean();
772
773 EnableFileService::removeFirstInstallFile();
774
775 return new JsonResponse([
776 'success' => true,
777 'redirect' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir . 'index.php',
778 ]);
779 }
780
781 /**
782 * Helper method to initialize a standalone view instance.
783 *
784 * @param string $templatePath
785 * @return StandaloneView
786 * @internal param string $template
787 */
788 protected function initializeStandaloneView(string $templatePath): StandaloneView
789 {
790 $viewRootPath = GeneralUtility::getFileAbsFileName('EXT:install/Resources/Private/');
791 $view = GeneralUtility::makeInstance(StandaloneView::class);
792 $view->getRequest()->setControllerExtensionName('Install');
793 $view->setTemplatePathAndFilename($viewRootPath . 'Templates/' . $templatePath);
794 $view->setLayoutRootPaths([$viewRootPath . 'Layouts/']);
795 $view->setPartialRootPaths([$viewRootPath . 'Partials/']);
796 return $view;
797 }
798
799 /**
800 * Test connection with given credentials and return exception message if exception trown
801 *
802 * @return bool
803 */
804 protected function isDatabaseConnectSuccessful(): bool
805 {
806 try {
807 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionByName('Default')->ping();
808 } catch (DBALException $e) {
809 return false;
810 }
811 return true;
812 }
813
814 /**
815 * Check LocalConfiguration.php for required database settings:
816 * - 'username' and 'password' are mandatory, but may be empty
817 *
818 * @return bool TRUE if required settings are present
819 */
820 protected function isDatabaseConfigurationComplete()
821 {
822 $configurationComplete = true;
823 if (!isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'])) {
824 $configurationComplete = false;
825 }
826 if (!isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'])) {
827 $configurationComplete = false;
828 }
829 return $configurationComplete;
830 }
831
832 /**
833 * Returns configured socket, if set.
834 *
835 * @return string
836 */
837 protected function getDatabaseConfiguredMysqliSocket()
838 {
839 $socket = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket'] ?? '';
840 if ($socket === '') {
841 // If no configured socket, use default php socket
842 $defaultSocket = (string)ini_get('mysqli.default_socket');
843 if ($defaultSocket !== '') {
844 $socket = $defaultSocket;
845 }
846 }
847 return $socket;
848 }
849
850 /**
851 * Try to fetch db credentials from a .env file and see if connect works
852 *
853 * @return array Empty array if no file is found or connect is not successful, else working credentials
854 */
855 protected function getDatabaseConfigurationFromEnvironment(): array
856 {
857 $envCredentials = [];
858 foreach (['driver', 'host', 'user', 'password', 'port', 'dbname', 'unix_socket'] as $value) {
859 $envVar = 'TYPO3_INSTALL_DB_' . strtoupper($value);
860 if (getenv($envVar) !== false) {
861 $envCredentials[$value] = getenv($envVar);
862 }
863 }
864 if (!empty($envCredentials)) {
865 $connectionParams = $envCredentials;
866 $connectionParams['wrapperClass'] = Connection::class;
867 $connectionParams['charset'] = 'utf-8';
868 try {
869 DriverManager::getConnection($connectionParams)->ping();
870 return $envCredentials;
871 } catch (DBALException $e) {
872 return [];
873 }
874 }
875 return [];
876 }
877
878 /**
879 * Returns list of available databases (with access-check based on username/password)
880 *
881 * @return array List of available databases
882 */
883 protected function getDatabaseList()
884 {
885 $connectionParams = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME];
886 unset($connectionParams['dbname']);
887
888 // Establishing the connection using the Doctrine DriverManager directly
889 // as we need a connection without selecting a database right away. Otherwise
890 // an invalid database name would lead to exceptions which would prevent
891 // changing the currently configured database.
892 $connection = DriverManager::getConnection($connectionParams);
893 $databaseArray = $connection->getSchemaManager()->listDatabases();
894 $connection->close();
895
896 // Remove organizational tables from database list
897 $reservedDatabaseNames = ['mysql', 'information_schema', 'performance_schema'];
898 $allPossibleDatabases = array_diff($databaseArray, $reservedDatabaseNames);
899
900 // In first installation we show all databases but disable not empty ones (with tables)
901 $databases = [];
902 foreach ($allPossibleDatabases as $databaseName) {
903 // Reestablising the connection for each database since there is no
904 // portable way to switch databases on the same Doctrine connection.
905 // Directly using the Doctrine DriverManager here to avoid messing with
906 // the $GLOBALS database configuration array.
907 $connectionParams['dbname'] = $databaseName;
908 $connection = DriverManager::getConnection($connectionParams);
909
910 $databases[] = [
911 'name' => $databaseName,
912 'tables' => count($connection->getSchemaManager()->listTableNames()),
913 ];
914 $connection->close();
915 }
916
917 return $databases;
918 }
919
920 /**
921 * Creates a new database on the default connection
922 *
923 * @param string $dbName name of database
924 * @return FlashMessage
925 */
926 protected function createNewDatabase($dbName)
927 {
928 if (!$this->isValidDatabaseName($dbName)) {
929 return new FlashMessage(
930 'Given database name must be shorter than fifty characters'
931 . ' and consist solely of basic latin letters (a-z), digits (0-9), dollar signs ($)'
932 . ' and underscores (_).',
933 'Database name not valid',
934 FlashMessage::ERROR
935 );
936 }
937 try {
938 GeneralUtility::makeInstance(ConnectionPool::class)
939 ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME)
940 ->getSchemaManager()
941 ->createDatabase($dbName);
942 GeneralUtility::makeInstance(ConfigurationManager::class)
943 ->setLocalConfigurationValueByPath('DB/Connections/Default/dbname', $dbName);
944 } catch (DBALException $e) {
945 return new FlashMessage(
946 'Database with name "' . $dbName . '" could not be created.'
947 . ' Either your database name contains a reserved keyword or your database'
948 . ' user does not have sufficient permissions to create it or the database already exists.'
949 . ' Please choose an existing (empty) database, choose another name or contact administration.',
950 'Unable to create database',
951 FlashMessage::ERROR
952 );
953 }
954 return new FlashMessage(
955 '',
956 'Database created'
957 );
958 }
959
960 /**
961 * Validate the database name against the lowest common denominator of valid identifiers across different DBMS
962 *
963 * @param string $databaseName
964 * @return bool
965 */
966 protected function isValidDatabaseName($databaseName)
967 {
968 return strlen($databaseName) <= 50 && preg_match('/^[a-zA-Z0-9\$_]*$/', $databaseName);
969 }
970
971 /**
972 * Checks whether an existing database on the default connection
973 * can be used for a TYPO3 installation. The database name is only
974 * persisted to the local configuration if the database is empty.
975 *
976 * @param string $dbName name of the database
977 * @return FlashMessage
978 */
979 protected function checkExistingDatabase($dbName)
980 {
981 $result = new FlashMessage('');
982 $localConfigurationPathValuePairs = [];
983 $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
984
985 $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['dbname'] = $dbName;
986 try {
987 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
988 ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
989
990 if (!empty($connection->getSchemaManager()->listTableNames())) {
991 $result = new FlashMessage(
992 sprintf('Cannot use database "%s"', $dbName)
993 . ', because it already contains tables. Please select a different database or choose to create one!',
994 'Selected database is not empty!',
995 FlashMessage::ERROR
996 );
997 }
998 } catch (\Exception $e) {
999 $result = new FlashMessage(
1000 sprintf('Could not connect to database "%s"', $dbName)
1001 . '! Make sure it really exists and your database user has the permissions to select it!',
1002 'Could not connect to selected database!',
1003 FlashMessage::ERROR
1004 );
1005 }
1006
1007 if ($result->getSeverity() === FlashMessage::OK) {
1008 $localConfigurationPathValuePairs['DB/Connections/Default/dbname'] = $dbName;
1009 }
1010
1011 // check if database charset is utf-8 - also allow utf8mb4
1012 $defaultDatabaseCharset = $this->getDefaultDatabaseCharset($dbName);
1013 if (substr($defaultDatabaseCharset, 0, 4) !== 'utf8') {
1014 $result = new FlashMessage(
1015 'Your database uses character set "' . $defaultDatabaseCharset . '", '
1016 . 'but only "utf8" is supported with TYPO3. You probably want to change this before proceeding.',
1017 'Invalid Charset',
1018 FlashMessage::ERROR
1019 );
1020 }
1021
1022 if ($result->getSeverity() === FlashMessage::OK && !empty($localConfigurationPathValuePairs)) {
1023 $configurationManager->setLocalConfigurationValuesByPathValuePairs($localConfigurationPathValuePairs);
1024 }
1025
1026 return $result;
1027 }
1028
1029 /**
1030 * Retrieves the default character set of the database.
1031 *
1032 * @todo this function is MySQL specific. If the core has migrated to Doctrine it should be reexamined
1033 * whether this function and the check in $this->checkExistingDatabase could be deleted and utf8 otherwise
1034 * enforced (guaranteeing compatibility with other database servers).
1035 *
1036 * @param string $dbName
1037 * @return string
1038 */
1039 protected function getDefaultDatabaseCharset(string $dbName): string
1040 {
1041 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
1042 ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
1043 $queryBuilder = $connection->createQueryBuilder();
1044 $defaultDatabaseCharset = $queryBuilder->select('DEFAULT_CHARACTER_SET_NAME')
1045 ->from('information_schema.SCHEMATA')
1046 ->where(
1047 $queryBuilder->expr()->eq(
1048 'SCHEMA_NAME',
1049 $queryBuilder->createNamedParameter($dbName, \PDO::PARAM_STR)
1050 )
1051 )
1052 ->setMaxResults(1)
1053 ->execute()
1054 ->fetchColumn();
1055
1056 return (string)$defaultDatabaseCharset;
1057 }
1058
1059 /**
1060 * This function returns a salted hashed key.
1061 *
1062 * @param string $password
1063 * @return string
1064 */
1065 protected function getHashedPassword($password)
1066 {
1067 $saltFactory = SaltFactory::getSaltingInstance(null, 'BE');
1068 return $saltFactory->getHashedPassword($password);
1069 }
1070
1071 /**
1072 * Create tables and import static rows
1073 *
1074 * @return FlashMessage[]
1075 */
1076 protected function importDatabaseData()
1077 {
1078 // Will load ext_localconf and ext_tables. This is pretty safe here since we are
1079 // in first install (database empty), so it is very likely that no extension is loaded
1080 // that could trigger a fatal at this point.
1081 $this->loadExtLocalconfDatabaseAndExtTables();
1082
1083 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
1084 $sqlCode = $sqlReader->getTablesDefinitionString(true);
1085 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
1086 $createTableStatements = $sqlReader->getCreateTableStatementArray($sqlCode);
1087 $results = $schemaMigrationService->install($createTableStatements);
1088
1089 // Only keep statements with error messages
1090 $results = array_filter($results);
1091 if (count($results) === 0) {
1092 $insertStatements = $sqlReader->getInsertStatementArray($sqlCode);
1093 $results = $schemaMigrationService->importStaticData($insertStatements);
1094 }
1095 foreach ($results as $statement => &$message) {
1096 if ($message === '') {
1097 unset($results[$statement]);
1098 continue;
1099 }
1100 $message = new FlashMessage(
1101 'Query:' . LF . ' ' . $statement . LF . 'Error:' . LF . ' ' . $message,
1102 'Database query failed!',
1103 FlashMessage::ERROR
1104 );
1105 }
1106 return array_values($results);
1107 }
1108
1109 /**
1110 * Some actions like the database analyzer and the upgrade wizards need additional
1111 * bootstrap actions performed.
1112 *
1113 * Those actions can potentially fatal if some old extension is loaded that triggers
1114 * a fatal in ext_localconf or ext_tables code! Use only if really needed.
1115 */
1116 protected function loadExtLocalconfDatabaseAndExtTables()
1117 {
1118 \TYPO3\CMS\Core\Core\Bootstrap::getInstance()
1119 ->loadTypo3LoadedExtAndExtLocalconf(false)
1120 ->unsetReservedGlobalVariables()
1121 ->loadBaseTca(false)
1122 ->loadExtTables(false);
1123 }
1124 }