Passed
Push — master ( 316db8...97e0d0 )
by Greg
05:28
created

SetupController::createConfigFile()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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