SetupWizard   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 410
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 182
dl 0
loc 410
rs 8.64
c 2
b 0
f 0
wmc 47

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A step5Administrator() 0 15 3
A step1Language() 0 3 1
A maxExecutionTime() 0 3 1
B handle() 0 66 10
A createConfigFile() 0 39 3
A step6Install() 0 18 3
B memoryLimit() 0 18 7
A step2CheckServer() 0 3 1
A step3DatabaseType() 0 7 2
A userData() 0 9 2
A checkFolderIsWritable() 0 13 2
A step4DatabaseConnection() 0 7 2
A connectToDatabase() 0 38 3
A checkAdminUser() 0 11 6

How to fix   Complexity   

Complex Class

Complex classes like SetupWizard often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SetupWizard, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2023 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Http\RequestHandlers;
21
22
use Exception;
23
use Fisharebest\Localization\Locale;
24
use Fisharebest\Localization\Locale\LocaleEnUs;
25
use Fisharebest\Localization\Locale\LocaleInterface;
26
use Fisharebest\Webtrees\Auth;
27
use Fisharebest\Webtrees\Contracts\UserInterface;
28
use Fisharebest\Webtrees\DB;
29
use Fisharebest\Webtrees\Factories\CacheFactory;
30
use Fisharebest\Webtrees\Http\ViewResponseTrait;
31
use Fisharebest\Webtrees\I18N;
32
use Fisharebest\Webtrees\Module\ModuleLanguageInterface;
33
use Fisharebest\Webtrees\Registry;
34
use Fisharebest\Webtrees\Services\MigrationService;
35
use Fisharebest\Webtrees\Services\ModuleService;
36
use Fisharebest\Webtrees\Services\ServerCheckService;
37
use Fisharebest\Webtrees\Services\UserService;
38
use Fisharebest\Webtrees\Session;
39
use Fisharebest\Webtrees\Validator;
40
use Fisharebest\Webtrees\Webtrees;
41
use Illuminate\Support\Collection;
42
use Psr\Http\Message\ResponseInterface;
43
use Psr\Http\Message\ServerRequestInterface;
44
use Psr\Http\Server\RequestHandlerInterface;
45
use Throwable;
46
47
use function e;
48
use function file_get_contents;
49
use function file_put_contents;
50
use function ini_get;
51
use function random_bytes;
52
use function realpath;
53
use function redirect;
54
use function substr;
55
use function touch;
56
use function unlink;
57
use function view;
58
59
/**
60
 * Controller for the installation wizard
61
 */
62
class SetupWizard implements RequestHandlerInterface
63
{
64
    use ViewResponseTrait;
65
66
    private const DEFAULT_DBTYPE = DB::MYSQL;
67
    private const DEFAULT_PREFIX = 'wt_';
68
    private const DEFAULT_DATA   = [
69
        'baseurl' => '',
70
        'lang'    => '',
71
        'dbtype'  => self::DEFAULT_DBTYPE,
72
        'dbhost'  => '',
73
        'dbport'  => '',
74
        'dbuser'  => '',
75
        'dbpass'  => '',
76
        'dbname'  => '',
77
        'tblpfx'  => self::DEFAULT_PREFIX,
78
        'wtname'  => '',
79
        'wtuser'  => '',
80
        'wtpass'  => '',
81
        'wtemail' => '',
82
    ];
83
84
    private const DEFAULT_PORTS = [
85
        DB::MYSQL      => '3306',
86
        DB::POSTGRES   => '5432',
87
        DB::SQLITE     => '',
88
        DB::SQL_SERVER => '', // Do not use default, as it is valid to have no port number.
89
    ];
90
91
    private MigrationService $migration_service;
92
93
    private ModuleService $module_service;
94
95
    private ServerCheckService $server_check_service;
96
97
    private UserService $user_service;
98
99
    /**
100
     * @param MigrationService   $migration_service
101
     * @param ModuleService      $module_service
102
     * @param ServerCheckService $server_check_service
103
     * @param UserService        $user_service
104
     */
105
    public function __construct(
106
        MigrationService $migration_service,
107
        ModuleService $module_service,
108
        ServerCheckService $server_check_service,
109
        UserService $user_service
110
    ) {
111
        $this->user_service         = $user_service;
112
        $this->migration_service    = $migration_service;
113
        $this->module_service       = $module_service;
114
        $this->server_check_service = $server_check_service;
115
    }
116
117
    /**
118
     * Installation wizard - check user input and proceed to the next step.
119
     *
120
     * @param ServerRequestInterface $request
121
     *
122
     * @return ResponseInterface
123
     */
124
    public function handle(ServerRequestInterface $request): ResponseInterface
125
    {
126
        $this->layout = 'layouts/setup';
127
128
        // Some functions need a cache, but we don't have one yet.
129
        Registry::cache(new CacheFactory());
130
131
        // We will need an IP address for the logs.
132
        $ip_address = Validator::serverParams($request)->string('REMOTE_ADDR', '127.0.0.1');
133
        $request    = $request->withAttribute('client-ip', $ip_address);
134
135
        Registry::container()->set(ServerRequestInterface::class, $request);
136
137
        $data = $this->userData($request);
138
139
        $step = Validator::parsedBody($request)->integer('step', 1);
140
141
        $locales = $this->module_service
142
            ->setupLanguages()
143
            ->map(static fn (ModuleLanguageInterface $module): LocaleInterface => $module->locale());
144
145
        if ($data['lang'] === '') {
146
            $default = new LocaleEnUs();
147
148
            $locale  = Locale::httpAcceptLanguage($request->getServerParams(), $locales->all(), $default);
149
150
            $data['lang'] = $locale->languageTag();
151
        }
152
153
        I18N::init($data['lang'], true);
154
155
        $data['cpu_limit']    = $this->maxExecutionTime();
156
        $data['locales']      = $locales;
157
        $data['memory_limit'] = $this->memoryLimit();
158
159
        // Only show database errors after the user has chosen a driver.
160
        if ($step >= 4) {
161
            $data['errors']   = $this->server_check_service->serverErrors($data['dbtype']);
162
            $data['warnings'] = $this->server_check_service->serverWarnings($data['dbtype']);
163
        } else {
164
            $data['errors']   = $this->server_check_service->serverErrors();
165
            $data['warnings'] = $this->server_check_service->serverWarnings();
166
        }
167
168
        if (!$this->checkFolderIsWritable(Webtrees::DATA_DIR)) {
169
            $data['errors']->push(
170
                '<code>' . e(realpath(Webtrees::DATA_DIR)) . '</code><br>' .
171
                I18N::translate('Oops! webtrees was unable to create files in this folder.') . ' ' .
172
                I18N::translate('This usually means that you need to change the folder permissions to 777.')
173
            );
174
        }
175
176
        switch ($step) {
177
            default:
178
            case 1:
179
                return $this->step1Language($data);
180
            case 2:
181
                return $this->step2CheckServer($data);
182
            case 3:
183
                return $this->step3DatabaseType($data);
184
            case 4:
185
                return $this->step4DatabaseConnection($data);
186
            case 5:
187
                return $this->step5Administrator($data);
188
            case 6:
189
                return $this->step6Install($data);
190
        }
191
    }
192
193
    /**
194
     * @param ServerRequestInterface $request
195
     *
196
     * @return array<string,mixed>
197
     */
198
    private function userData(ServerRequestInterface $request): array
199
    {
200
        $data = [];
201
202
        foreach (self::DEFAULT_DATA as $key => $default) {
203
            $data[$key] = Validator::parsedBody($request)->string($key, $default);
204
        }
205
206
        return $data;
207
    }
208
209
    /**
210
     * The server's memory limit
211
     *
212
     * @return int
213
     */
214
    private function maxExecutionTime(): int
215
    {
216
        return (int) ini_get('max_execution_time');
217
    }
218
219
    /**
220
     * The server's memory limit (in MB).
221
     *
222
     * @return int
223
     */
224
    private function memoryLimit(): int
225
    {
226
        $memory_limit = ini_get('memory_limit');
227
228
        $number = (int) $memory_limit;
229
230
        switch (substr($memory_limit, -1)) {
231
            case 'g':
232
            case 'G':
233
                return $number * 1024;
234
            case 'm':
235
            case 'M':
236
                return $number;
237
            case 'k':
238
            case 'K':
239
                return (int) ($number / 1024);
240
            default:
241
                return (int) ($number / 1048576);
242
        }
243
    }
244
245
    /**
246
     * Check we can write to the data folder.
247
     *
248
     * @param string $data_dir
249
     *
250
     * @return bool
251
     */
252
    private function checkFolderIsWritable(string $data_dir): bool
253
    {
254
        $text1 = random_bytes(32);
255
256
        try {
257
            file_put_contents($data_dir . 'test.txt', $text1);
258
            $text2 = file_get_contents(Webtrees::DATA_DIR . 'test.txt');
259
            unlink(Webtrees::DATA_DIR . 'test.txt');
260
        } catch (Exception) {
261
            return false;
262
        }
263
264
        return $text1 === $text2;
265
    }
266
267
    /**
268
     * @param array<string,mixed> $data
269
     *
270
     * @return ResponseInterface
271
     */
272
    private function step1Language(array $data): ResponseInterface
273
    {
274
        return $this->viewResponse('setup/step-1-language', $data);
275
    }
276
277
    /**
278
     * @param array<string,mixed> $data
279
     *
280
     * @return ResponseInterface
281
     */
282
    private function step2CheckServer(array $data): ResponseInterface
283
    {
284
        return $this->viewResponse('setup/step-2-server-checks', $data);
285
    }
286
287
    /**
288
     * @param array<string,mixed> $data
289
     *
290
     * @return ResponseInterface
291
     */
292
    private function step3DatabaseType(array $data): ResponseInterface
293
    {
294
        if ($data['errors']->isNotEmpty()) {
295
            return $this->viewResponse('setup/step-2-server-checks', $data);
296
        }
297
298
        return $this->viewResponse('setup/step-3-database-type', $data);
299
    }
300
301
    /**
302
     * @param array<string,mixed> $data
303
     *
304
     * @return ResponseInterface
305
     */
306
    private function step4DatabaseConnection(array $data): ResponseInterface
307
    {
308
        if ($data['errors']->isNotEmpty()) {
309
            return $this->step3DatabaseType($data);
310
        }
311
312
        return $this->viewResponse('setup/step-4-database-' . $data['dbtype'], $data);
313
    }
314
315
    /**
316
     * @param array<string,mixed> $data
317
     *
318
     * @return ResponseInterface
319
     */
320
    private function step5Administrator(array $data): ResponseInterface
321
    {
322
        // Use default port, if none specified.
323
        $data['dbport'] = $data['dbport'] ?: self::DEFAULT_PORTS[$data['dbtype']];
324
325
        try {
326
            $this->connectToDatabase($data);
327
        } catch (Throwable $ex) {
328
            $data['errors']->push($ex->getMessage());
329
330
            // Don't jump to step 4, as the error will make it jump to step 3.
331
            return $this->viewResponse('setup/step-4-database-' . $data['dbtype'], $data);
332
        }
333
334
        return $this->viewResponse('setup/step-5-administrator', $data);
335
    }
336
337
    /**
338
     * @param array<string,mixed> $data
339
     *
340
     * @return ResponseInterface
341
     */
342
    private function step6Install(array $data): ResponseInterface
343
    {
344
        $error = $this->checkAdminUser($data['wtname'], $data['wtuser'], $data['wtpass'], $data['wtemail']);
345
346
        if ($error !== '') {
347
            $data['errors']->push($error);
348
349
            return $this->step5Administrator($data);
350
        }
351
352
        try {
353
            $this->createConfigFile($data);
354
        } catch (Throwable $exception) {
355
            return $this->viewResponse('setup/step-6-failed', ['exception' => $exception]);
356
        }
357
358
        // Done - start using webtrees!
359
        return redirect($data['baseurl']);
360
    }
361
362
    /**
363
     * @param string $wtname
364
     * @param string $wtuser
365
     * @param string $wtpass
366
     * @param string $wtemail
367
     *
368
     * @return string
369
     */
370
    private function checkAdminUser(string $wtname, string $wtuser, string $wtpass, string $wtemail): string
371
    {
372
        if ($wtname === '' || $wtuser === '' || $wtpass === '' || $wtemail === '') {
373
            return I18N::translate('You must enter all the administrator account fields.');
374
        }
375
376
        if (mb_strlen($wtpass) < 6) {
377
            return I18N::translate('The password needs to be at least six characters long.');
378
        }
379
380
        return '';
381
    }
382
383
    /**
384
     * @param array<string,mixed> $data
385
     *
386
     * @return void
387
     */
388
    private function createConfigFile(array $data): void
389
    {
390
        // Create/update the database tables.
391
        $this->connectToDatabase($data);
392
        $this->migration_service->updateSchema('\Fisharebest\Webtrees\Schema', 'WT_SCHEMA_VERSION', Webtrees::SCHEMA_VERSION);
393
394
        // Add some default/necessary configuration data.
395
        $this->migration_service->seedDatabase();
396
397
        // If we are re-installing, then this user may already exist.
398
        $admin = $this->user_service->findByIdentifier($data['wtemail']);
399
        if ($admin === null) {
400
            $admin = $this->user_service->findByIdentifier($data['wtuser']);
401
        }
402
        // Create the user
403
        if ($admin === null) {
404
            $admin = $this->user_service->create($data['wtuser'], $data['wtname'], $data['wtemail'], $data['wtpass']);
405
            $admin->setPreference(UserInterface::PREF_LANGUAGE, $data['lang']);
406
            $admin->setPreference(UserInterface::PREF_IS_VISIBLE_ONLINE, '1');
407
        } else {
408
            $admin->setPassword($_POST['wtpass']);
409
        }
410
        // Make the user an administrator
411
        $admin->setPreference(UserInterface::PREF_IS_ADMINISTRATOR, '1');
412
        $admin->setPreference(UserInterface::PREF_IS_EMAIL_VERIFIED, '1');
413
        $admin->setPreference(UserInterface::PREF_IS_ACCOUNT_APPROVED, '1');
414
415
        // Write the config file. We already checked that this would work.
416
        $config_ini_php = view('setup/config.ini', $data);
417
418
        file_put_contents(Webtrees::CONFIG_FILE, $config_ini_php);
419
420
        // Login as the new user
421
        $request = Registry::container()->get(ServerRequestInterface::class)
422
            ->withAttribute('base_url', $data['baseurl']);
423
424
        Session::start($request);
425
        Auth::login($admin);
426
        Session::put('language', $data['lang']);
427
    }
428
429
    /**
430
     * @param array<string,mixed> $data
431
     *
432
     * @return void
433
     */
434
    private function connectToDatabase(array $data): void
435
    {
436
        // Try to create the database, if it does not already exist.
437
        switch ($data['dbtype']) {
438
            case DB::SQLITE:
439
                touch(Webtrees::ROOT_DIR . 'data/' . $data['dbname'] . '.sqlite');
440
                break;
441
442
            case DB::MYSQL:
443
                DB::connect(
444
                    driver: $data['dbtype'],
445
                    host: $data['dbhost'],
446
                    port: $data['dbport'],
447
                    database: '',
448
                    username: $data['dbuser'],
449
                    password: $data['dbpass'],
450
                    prefix: $data['tblpfx'],
451
                    key: $data['dbkey'],
452
                    certificate: $data['dbcert'],
453
                    ca: $data['dbca'],
454
                    verify_certificate: (bool) $data['dbverify'],
455
                );
456
                DB::exec('CREATE DATABASE IF NOT EXISTS `' . $data['dbname'] . '` COLLATE utf8mb4_unicode_ci');
457
                break;
458
        }
459
460
        DB::connect(
461
            driver: $data['dbtype'],
462
            host: $data['dbhost'],
463
            port: $data['dbport'],
464
            database: $data['dbname'],
465
            username: $data['dbuser'],
466
            password: $data['dbpass'],
467
            prefix: $data['tblpfx'],
468
            key: $data['dbkey'],
469
            certificate: $data['dbcert'],
470
            ca: $data['dbca'],
471
            verify_certificate: (bool) $data['dbverify'],
472
        );
473
    }
474
}
475