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