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