Passed
Push — master ( 419ce1...415cca )
by Greg
06:23
created

ServerCheckService::databaseEngineWarnings()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 27
nc 4
nop 0
dl 0
loc 44
rs 9.488
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\Services;
21
22
use Fisharebest\Webtrees\I18N;
23
use Illuminate\Support\Collection;
24
use Illuminate\Support\Str;
25
use SQLite3;
26
27
use function array_map;
28
use function class_exists;
29
use function date;
30
use function e;
31
use function explode;
32
use function extension_loaded;
33
use function function_exists;
34
use function in_array;
35
use function preg_replace;
36
use function strpos;
37
use function strtolower;
38
use function sys_get_temp_dir;
39
use function trim;
40
use function version_compare;
41
42
use const PATH_SEPARATOR;
43
use const PHP_MAJOR_VERSION;
44
use const PHP_MINOR_VERSION;
45
use const PHP_VERSION;
46
47
/**
48
 * Check if the server meets the minimum requirements for webtrees.
49
 */
50
class ServerCheckService
51
{
52
    private const PHP_SUPPORT_URL   = 'https://www.php.net/supported-versions.php';
53
    private const PHP_MINOR_VERSION = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
54
    private const PHP_SUPPORT_DATES = [
55
        '7.1' => '2019-12-01',
56
        '7.2' => '2020-11-30',
57
        '7.3' => '2021-12-06',
58
        '7.4' => '2022-11-28',
59
    ];
60
61
    // As required by illuminate/database 5.8
62
    private const MINIMUM_SQLITE_VERSION = '3.7.11';
63
64
    /**
65
     * Things that may cause webtrees to break.
66
     *
67
     * @param string $driver
68
     *
69
     * @return Collection<string>
70
     */
71
    public function serverErrors($driver = ''): Collection
72
    {
73
        $errors = Collection::make([
74
            $this->databaseDriverErrors($driver),
75
            $this->checkPhpExtension('mbstring'),
76
            $this->checkPhpExtension('iconv'),
77
            $this->checkPhpExtension('pcre'),
78
            $this->checkPhpExtension('session'),
79
            $this->checkPhpExtension('xml'),
80
            $this->checkPhpFunction('parse_ini_file'),
81
        ]);
82
83
        return $errors
84
            ->flatten()
85
            ->filter();
86
    }
87
88
    /**
89
     * Things that should be fixed, but which won't stop completely webtrees from running.
90
     *
91
     * @param string $driver
92
     *
93
     * @return Collection<string>
94
     */
95
    public function serverWarnings($driver = ''): Collection
96
    {
97
        $warnings = Collection::make([
98
            $this->databaseDriverWarnings($driver),
99
            $this->checkPhpExtension('curl'),
100
            $this->checkPhpExtension('fileinfo'),
101
            $this->checkPhpExtension('gd'),
102
            $this->checkPhpExtension('zip'),
103
            $this->checkPhpExtension('simplexml'),
104
            $this->checkPhpIni('file_uploads', true),
105
            $this->checkSystemTemporaryFolder(),
106
            $this->checkPhpVersion(),
107
        ]);
108
109
        return $warnings
110
            ->flatten()
111
            ->filter();
112
    }
113
114
    /**
115
     * Check if a PHP extension is loaded.
116
     *
117
     * @param string $extension
118
     *
119
     * @return string
120
     */
121
    private function checkPhpExtension(string $extension): string
122
    {
123
        if (!extension_loaded($extension)) {
124
            return I18N::translate('The PHP extension “%s” is not installed.', $extension);
125
        }
126
127
        return '';
128
    }
129
130
    /**
131
     * Check if a PHP setting is correct.
132
     *
133
     * @param string $varname
134
     * @param bool   $expected
135
     *
136
     * @return string
137
     */
138
    private function checkPhpIni(string $varname, bool $expected): string
139
    {
140
        $ini_get = (bool) ini_get($varname);
141
142
        if ($expected && $ini_get !== $expected) {
143
            return I18N::translate('The PHP.INI setting “%1$s” is disabled.', $varname);
144
        }
145
146
        if (!$expected && $ini_get !== $expected) {
147
            return I18N::translate('The PHP.INI setting “%1$s” is enabled.', $varname);
148
        }
149
150
        return '';
151
    }
152
153
    /**
154
     * Check if a PHP function is in the list of disabled functions.
155
     *
156
     * @param string $function
157
     *
158
     * @return bool
159
     */
160
    public function isFunctionDisabled(string $function): bool
161
    {
162
        $disable_functions = explode(',', ini_get('disable_functions'));
163
        $disable_functions = array_map(static function (string $func): string {
164
            return strtolower(trim($func));
165
        }, $disable_functions);
166
167
        $function = strtolower($function);
168
169
        return in_array($function, $disable_functions, true) || !function_exists($function);
170
    }
171
172
    /**
173
     * Create a warning message for a disabled function.
174
     *
175
     * @param string $function
176
     *
177
     * @return string
178
     */
179
    private function checkPhpFunction(string $function): string
180
    {
181
        if ($this->isFunctionDisabled($function)) {
182
            return I18N::translate('The PHP function “%1$s” is disabled.', $function . '()');
183
        }
184
185
        return '';
186
    }
187
188
    /**
189
     * Some servers configure their temporary folder in an unaccessible place.
190
     */
191
    private function checkPhpVersion(): string
192
    {
193
        $today = date('Y-m-d');
194
195
        foreach (self::PHP_SUPPORT_DATES as $version => $end_date) {
196
            if ($today > $end_date && version_compare(self::PHP_MINOR_VERSION, $version) <= 0) {
197
                return I18N::translate('Your web server is using PHP version %s, which is no longer receiving security updates. You should upgrade to a later version as soon as possible.', PHP_VERSION) . ' <a href="' . e(self::PHP_SUPPORT_URL) . '">' . e(self::PHP_SUPPORT_URL) . '</a>';
198
            }
199
        }
200
201
        return '';
202
    }
203
204
    /**
205
     * Check the
206
     *
207
     * @return string
208
     */
209
    private function checkSqliteVersion(): string
210
    {
211
        if (class_exists(SQLite3::class)) {
212
            $sqlite_version = SQLite3::version()['versionString'];
213
214
            if (version_compare($sqlite_version, self::MINIMUM_SQLITE_VERSION) < 0) {
215
                return I18N::translate('SQLite version %s is installed. SQLite version %s or later is required.', $sqlite_version, self::MINIMUM_SQLITE_VERSION);
216
            }
217
        }
218
219
        return '';
220
    }
221
222
    /**
223
     * Some servers configure their temporary folder in an unaccessible place.
224
     */
225
    private function checkSystemTemporaryFolder(): string
226
    {
227
        $open_basedir = ini_get('open_basedir');
228
229
        if ($open_basedir === '') {
230
            // open_basedir not used.
231
            return '';
232
        }
233
234
        $open_basedirs = explode(PATH_SEPARATOR, $open_basedir);
235
236
        $sys_temp_dir = sys_get_temp_dir();
237
        $sys_temp_dir = $this->normalizeFolder($sys_temp_dir);
238
239
        foreach ($open_basedirs as $dir) {
240
            $dir = $this->normalizeFolder($dir);
241
242
            if (strpos($sys_temp_dir, $dir) === 0) {
243
                return '';
244
            }
245
        }
246
247
        $message = I18N::translate('The server’s temporary folder cannot be accessed.');
248
        $message .= '<br>sys_get_temp_dir() = "' . e($sys_temp_dir) . '"';
249
        $message .= '<br>ini_get("open_basedir") = "' . e($open_basedir) . '"';
250
251
        return $message;
252
    }
253
254
    /**
255
     * Convert a folder name to a canonical form:
256
     * - forward slashes.
257
     * - trailing slash.
258
     * We can't use realpath() as this can trigger open_basedir restrictions,
259
     * and we are using this code to find out whether open_basedir will affect us.
260
     *
261
     * @param string $path
262
     *
263
     * @return string
264
     */
265
    private function normalizeFolder(string $path): string
266
    {
267
        $path = preg_replace('/[\\/]+/', '/', $path);
268
        $path = Str::finish($path, '/');
269
270
        return $path;
271
    }
272
273
    /**
274
     * @param string $driver
275
     *
276
     * @return Collection<string>
277
     */
278
    private function databaseDriverErrors(string $driver): Collection
279
    {
280
        switch ($driver) {
281
            case 'mysql':
282
                return Collection::make([
283
                    $this->checkPhpExtension('pdo'),
284
                    $this->checkPhpExtension('pdo_mysql'),
285
                ]);
286
287
            case 'sqlite':
288
                return Collection::make([
289
                    $this->checkPhpExtension('pdo'),
290
                    $this->checkPhpExtension('sqlite3'),
291
                    $this->checkPhpExtension('pdo_sqlite'),
292
                    $this->checkSqliteVersion(),
293
                ]);
294
295
            case 'pgsql':
296
                return Collection::make([
297
                    $this->checkPhpExtension('pdo'),
298
                    $this->checkPhpExtension('pdo_pgsql'),
299
                ]);
300
301
            case 'sqlsvr':
302
                return Collection::make([
303
                    $this->checkPhpExtension('pdo'),
304
                    $this->checkPhpExtension('pdo_odbc'),
305
                ]);
306
307
            default:
308
                return new Collection();
309
        }
310
    }
311
312
    /**
313
     * @param string $driver
314
     *
315
     * @return Collection<string>
316
     */
317
    private function databaseDriverWarnings(string $driver): Collection
318
    {
319
        switch ($driver) {
320
            case 'sqlite':
321
                return new Collection([
322
                    I18N::translate('SQLite is only suitable for small sites, testing and evaluation.'),
323
                ]);
324
325
            case 'pgsql':
326
                return new Collection([
327
                    I18N::translate('Support for PostgreSQL is experimental.'),
328
                ]);
329
330
            case 'sqlsvr':
331
                return new Collection([
332
                    I18N::translate('Support for SQL Server is experimental.'),
333
                ]);
334
335
            default:
336
                return new Collection();
337
        }
338
    }
339
}
340