Completed
Push — master ( d1e7e8...3d9931 )
by
unknown
15:20 queued 01:03
created

checkRequiredDatabasePermissions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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