xref: /webtrees/app/Services/ServerCheckService.php (revision 36de22acf6348b1059dac63e3cd19589574906ac)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2021 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
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Services;
21
22use Fisharebest\Webtrees\I18N;
23use Illuminate\Support\Collection;
24use SQLite3;
25
26use function array_map;
27use function class_exists;
28use function date;
29use function e;
30use function explode;
31use function extension_loaded;
32use function function_exists;
33use function in_array;
34use function str_ends_with;
35use function str_starts_with;
36use function strtolower;
37use function sys_get_temp_dir;
38use function trim;
39use function version_compare;
40
41use const PATH_SEPARATOR;
42use const PHP_MAJOR_VERSION;
43use const PHP_MINOR_VERSION;
44use const PHP_VERSION;
45
46/**
47 * Check if the server meets the minimum requirements for webtrees.
48 */
49class ServerCheckService
50{
51    private const PHP_SUPPORT_URL   = 'https://www.php.net/supported-versions.php';
52    private const PHP_MINOR_VERSION = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
53    private const PHP_SUPPORT_DATES = [
54        '7.3' => '2021-12-06',
55        '7.4' => '2022-11-28',
56        '8.0' => '2023-11-26',
57    ];
58
59    // As required by illuminate/database 5.8
60    private const MINIMUM_SQLITE_VERSION = '3.7.11';
61
62    /**
63     * Things that may cause webtrees to break.
64     *
65     * @param string $driver
66     *
67     * @return Collection<string>
68     */
69    public function serverErrors($driver = ''): Collection
70    {
71        $errors = Collection::make([
72            $this->databaseDriverErrors($driver),
73            $this->checkPhpExtension('mbstring'),
74            $this->checkPhpExtension('iconv'),
75            $this->checkPhpExtension('pcre'),
76            $this->checkPhpExtension('session'),
77            $this->checkPhpExtension('xml'),
78            $this->checkPhpFunction('parse_ini_file'),
79        ]);
80
81        return $errors
82            ->flatten()
83            ->filter();
84    }
85
86    /**
87     * Things that should be fixed, but which won't stop completely webtrees from running.
88     *
89     * @param string $driver
90     *
91     * @return Collection<string>
92     */
93    public function serverWarnings($driver = ''): Collection
94    {
95        $warnings = Collection::make([
96            $this->databaseDriverWarnings($driver),
97            $this->checkPhpExtension('curl'),
98            $this->checkPhpExtension('exif'),
99            $this->checkPhpExtension('fileinfo'),
100            $this->checkPhpExtension('gd'),
101            $this->checkPhpExtension('intl'),
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 inaccessible 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 inaccessible 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 (str_starts_with($sys_temp_dir, $dir)) {
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 = strtr($path, ['\\' => '/']);
268
269        if (str_ends_with($path, '/')) {
270            return $path;
271        }
272
273        return $path . '/';
274    }
275
276    /**
277     * @param string $driver
278     *
279     * @return Collection<string>
280     */
281    private function databaseDriverErrors(string $driver): Collection
282    {
283        switch ($driver) {
284            case 'mysql':
285                return Collection::make([
286                    $this->checkPhpExtension('pdo'),
287                    $this->checkPhpExtension('pdo_mysql'),
288                ]);
289
290            case 'sqlite':
291                return Collection::make([
292                    $this->checkPhpExtension('pdo'),
293                    $this->checkPhpExtension('sqlite3'),
294                    $this->checkPhpExtension('pdo_sqlite'),
295                    $this->checkSqliteVersion(),
296                ]);
297
298            case 'pgsql':
299                return Collection::make([
300                    $this->checkPhpExtension('pdo'),
301                    $this->checkPhpExtension('pdo_pgsql'),
302                ]);
303
304            case 'sqlsvr':
305                return Collection::make([
306                    $this->checkPhpExtension('pdo'),
307                    $this->checkPhpExtension('pdo_odbc'),
308                ]);
309
310            default:
311                return new Collection();
312        }
313    }
314
315    /**
316     * @param string $driver
317     *
318     * @return Collection<string>
319     */
320    private function databaseDriverWarnings(string $driver): Collection
321    {
322        switch ($driver) {
323            case 'sqlite':
324                return new Collection([
325                    I18N::translate('SQLite is only suitable for small sites, testing and evaluation.'),
326                ]);
327
328            case 'pgsql':
329                return new Collection([
330                    I18N::translate('Support for PostgreSQL is experimental.'),
331                ]);
332
333            case 'sqlsvr':
334                return new Collection([
335                    I18N::translate('Support for SQL Server is experimental.'),
336                ]);
337
338            default:
339                return new Collection();
340        }
341    }
342}
343