Passed
Push — master ( d5ab84...b33d97 )
by Greg
05:49
created

ServerCheckService::serverErrors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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