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