xref: /webtrees/app/Services/ServerCheckService.php (revision cc5c497c7d71322d728a99be63ed07985f3d9f59)
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 */
16declare(strict_types=1);
17
18namespace Fisharebest\Webtrees\Services;
19
20use Fisharebest\Webtrees\I18N;
21use Illuminate\Support\Collection;
22use Illuminate\Support\Str;
23use SQLite3;
24use function array_map;
25use function class_exists;
26use function date;
27use function e;
28use function explode;
29use function extension_loaded;
30use function function_exists;
31use function in_array;
32use function preg_replace;
33use function strpos;
34use function strtolower;
35use function sys_get_temp_dir;
36use function trim;
37use function version_compare;
38use const PATH_SEPARATOR;
39use const PHP_MAJOR_VERSION;
40use const PHP_MINOR_VERSION;
41use const PHP_VERSION;
42
43/**
44 * Check if the server meets the minimum requirements for webtrees.
45 */
46class 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