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