Completed
Push — master ( e28c49...c020e9 )
by
unknown
19:48
created

getDatabaseConfiguredPdoMysqlSocket()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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