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