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