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

InstallerController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 18
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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