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