xref: /webtrees/app/Services/ServerCheckService.php (revision 5d4b7ec24d2b390be41475c9b93e00d957e93fef)
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
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Services;
21
22use Fisharebest\Webtrees\I18N;
23use Illuminate\Support\Collection;
24use Illuminate\Support\Str;
25use SQLite3;
26
27use function array_map;
28use function class_exists;
29use function date;
30use function e;
31use function explode;
32use function extension_loaded;
33use function function_exists;
34use function in_array;
35use function preg_replace;
36use function strpos;
37use function strtolower;
38use function sys_get_temp_dir;
39use function trim;
40use function version_compare;
41
42use const PATH_SEPARATOR;
43use const PHP_MAJOR_VERSION;
44use const PHP_MINOR_VERSION;
45use const PHP_VERSION;
46
47/**
48 * Check if the server meets the minimum requirements for webtrees.
49 */
50class 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('exif'),
101            $this->checkPhpExtension('fileinfo'),
102            $this->checkPhpExtension('gd'),
103            $this->checkPhpExtension('intl'),
104            $this->checkPhpExtension('zip'),
105            $this->checkPhpExtension('simplexml'),
106            $this->checkPhpIni('file_uploads', true),
107            $this->checkSystemTemporaryFolder(),
108            $this->checkPhpVersion(),
109        ]);
110
111        return $warnings
112            ->flatten()
113            ->filter();
114    }
115
116    /**
117     * Check if a PHP extension is loaded.
118     *
119     * @param string $extension
120     *
121     * @return string
122     */
123    private function checkPhpExtension(string $extension): string
124    {
125        if (!extension_loaded($extension)) {
126            return I18N::translate('The PHP extension “%s” is not installed.', $extension);
127        }
128
129        return '';
130    }
131
132    /**
133     * Check if a PHP setting is correct.
134     *
135     * @param string $varname
136     * @param bool   $expected
137     *
138     * @return string
139     */
140    private function checkPhpIni(string $varname, bool $expected): string
141    {
142        $ini_get = (bool) ini_get($varname);
143
144        if ($expected && $ini_get !== $expected) {
145            return I18N::translate('The PHP.INI setting “%1$s” is disabled.', $varname);
146        }
147
148        if (!$expected && $ini_get !== $expected) {
149            return I18N::translate('The PHP.INI setting “%1$s” is enabled.', $varname);
150        }
151
152        return '';
153    }
154
155    /**
156     * Check if a PHP function is in the list of disabled functions.
157     *
158     * @param string $function
159     *
160     * @return bool
161     */
162    public function isFunctionDisabled(string $function): bool
163    {
164        $disable_functions = explode(',', ini_get('disable_functions'));
165        $disable_functions = array_map(static function (string $func): string {
166            return strtolower(trim($func));
167        }, $disable_functions);
168
169        $function = strtolower($function);
170
171        return in_array($function, $disable_functions, true) || !function_exists($function);
172    }
173
174    /**
175     * Create a warning message for a disabled function.
176     *
177     * @param string $function
178     *
179     * @return string
180     */
181    private function checkPhpFunction(string $function): string
182    {
183        if ($this->isFunctionDisabled($function)) {
184            return I18N::translate('The PHP function “%1$s” is disabled.', $function . '()');
185        }
186
187        return '';
188    }
189
190    /**
191     * Some servers configure their temporary folder in an unaccessible place.
192     */
193    private function checkPhpVersion(): string
194    {
195        $today = date('Y-m-d');
196
197        foreach (self::PHP_SUPPORT_DATES as $version => $end_date) {
198            if ($today > $end_date && version_compare(self::PHP_MINOR_VERSION, $version) <= 0) {
199                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>';
200            }
201        }
202
203        return '';
204    }
205
206    /**
207     * Check the
208     *
209     * @return string
210     */
211    private function checkSqliteVersion(): string
212    {
213        if (class_exists(SQLite3::class)) {
214            $sqlite_version = SQLite3::version()['versionString'];
215
216            if (version_compare($sqlite_version, self::MINIMUM_SQLITE_VERSION) < 0) {
217                return I18N::translate('SQLite version %s is installed. SQLite version %s or later is required.', $sqlite_version, self::MINIMUM_SQLITE_VERSION);
218            }
219        }
220
221        return '';
222    }
223
224    /**
225     * Some servers configure their temporary folder in an unaccessible place.
226     */
227    private function checkSystemTemporaryFolder(): string
228    {
229        $open_basedir = ini_get('open_basedir');
230
231        if ($open_basedir === '') {
232            // open_basedir not used.
233            return '';
234        }
235
236        $open_basedirs = explode(PATH_SEPARATOR, $open_basedir);
237
238        $sys_temp_dir = sys_get_temp_dir();
239        $sys_temp_dir = $this->normalizeFolder($sys_temp_dir);
240
241        foreach ($open_basedirs as $dir) {
242            $dir = $this->normalizeFolder($dir);
243
244            if (strpos($sys_temp_dir, $dir) === 0) {
245                return '';
246            }
247        }
248
249        $message = I18N::translate('The server’s temporary folder cannot be accessed.');
250        $message .= '<br>sys_get_temp_dir() = "' . e($sys_temp_dir) . '"';
251        $message .= '<br>ini_get("open_basedir") = "' . e($open_basedir) . '"';
252
253        return $message;
254    }
255
256    /**
257     * Convert a folder name to a canonical form:
258     * - forward slashes.
259     * - trailing slash.
260     * We can't use realpath() as this can trigger open_basedir restrictions,
261     * and we are using this code to find out whether open_basedir will affect us.
262     *
263     * @param string $path
264     *
265     * @return string
266     */
267    private function normalizeFolder(string $path): string
268    {
269        $path = preg_replace('/[\\/]+/', '/', $path);
270        $path = Str::finish($path, '/');
271
272        return $path;
273    }
274
275    /**
276     * @param string $driver
277     *
278     * @return Collection<string>
279     */
280    private function databaseDriverErrors(string $driver): Collection
281    {
282        switch ($driver) {
283            case 'mysql':
284                return Collection::make([
285                    $this->checkPhpExtension('pdo'),
286                    $this->checkPhpExtension('pdo_mysql'),
287                ]);
288
289            case 'sqlite':
290                return Collection::make([
291                    $this->checkPhpExtension('pdo'),
292                    $this->checkPhpExtension('sqlite3'),
293                    $this->checkPhpExtension('pdo_sqlite'),
294                    $this->checkSqliteVersion(),
295                ]);
296
297            case 'pgsql':
298                return Collection::make([
299                    $this->checkPhpExtension('pdo'),
300                    $this->checkPhpExtension('pdo_pgsql'),
301                ]);
302
303            case 'sqlsvr':
304                return Collection::make([
305                    $this->checkPhpExtension('pdo'),
306                    $this->checkPhpExtension('pdo_odbc'),
307                ]);
308
309            default:
310                return new Collection();
311        }
312    }
313
314    /**
315     * @param string $driver
316     *
317     * @return Collection<string>
318     */
319    private function databaseDriverWarnings(string $driver): Collection
320    {
321        switch ($driver) {
322            case 'sqlite':
323                return new Collection([
324                    I18N::translate('SQLite is only suitable for small sites, testing and evaluation.'),
325                ]);
326
327            case 'pgsql':
328                return new Collection([
329                    I18N::translate('Support for PostgreSQL is experimental.'),
330                ]);
331
332            case 'sqlsvr':
333                return new Collection([
334                    I18N::translate('Support for SQL Server is experimental.'),
335                ]);
336
337            default:
338                return new Collection();
339        }
340    }
341}
342