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