Passed
Push — master ( 8e4875...07bfbc )
by
unknown
12:19
created

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