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