xref: /webtrees/app/Services/ServerCheckService.php (revision c9dab104b9f744488eba8df3010acc39b8f4fc28)
1b7059dccSGreg Roach<?php
23976b470SGreg Roach
3b7059dccSGreg Roach/**
4b7059dccSGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
6b7059dccSGreg Roach * This program is free software: you can redistribute it and/or modify
7b7059dccSGreg Roach * it under the terms of the GNU General Public License as published by
8b7059dccSGreg Roach * the Free Software Foundation, either version 3 of the License, or
9b7059dccSGreg Roach * (at your option) any later version.
10b7059dccSGreg Roach * This program is distributed in the hope that it will be useful,
11b7059dccSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12b7059dccSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13b7059dccSGreg Roach * GNU General Public License for more details.
14b7059dccSGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16b7059dccSGreg Roach */
17fcfa147eSGreg Roach
18b7059dccSGreg Roachdeclare(strict_types=1);
19b7059dccSGreg Roach
20b7059dccSGreg Roachnamespace Fisharebest\Webtrees\Services;
21b7059dccSGreg Roach
2252550490SGreg Roachuse Fisharebest\Webtrees\DB;
23b7059dccSGreg Roachuse Fisharebest\Webtrees\I18N;
24b7059dccSGreg Roachuse Illuminate\Support\Collection;
25b7059dccSGreg Roachuse SQLite3;
263976b470SGreg Roach
27b7059dccSGreg Roachuse function array_map;
28b33d97e2SGreg Roachuse function class_exists;
29b33d97e2SGreg Roachuse function date;
30b33d97e2SGreg Roachuse function e;
31b7059dccSGreg Roachuse function explode;
32b7059dccSGreg Roachuse function extension_loaded;
33b33d97e2SGreg Roachuse function function_exists;
34b7059dccSGreg Roachuse function in_array;
35dec352c1SGreg Roachuse function str_ends_with;
36dec352c1SGreg Roachuse function str_starts_with;
377bb10f9aSGreg Roachuse function strtolower;
38b7059dccSGreg Roachuse function sys_get_temp_dir;
39b7059dccSGreg Roachuse function trim;
40b7059dccSGreg Roachuse function version_compare;
413976b470SGreg Roach
42b7059dccSGreg Roachuse const PATH_SEPARATOR;
43b7059dccSGreg Roachuse const PHP_MAJOR_VERSION;
44b7059dccSGreg Roachuse const PHP_MINOR_VERSION;
45b33d97e2SGreg Roachuse const PHP_VERSION;
46b7059dccSGreg Roach
47b7059dccSGreg Roach/**
48b7059dccSGreg Roach * Check if the server meets the minimum requirements for webtrees.
49b7059dccSGreg Roach */
50b7059dccSGreg Roachclass ServerCheckService
51b7059dccSGreg Roach{
522ddcca20SGreg Roach    private const PHP_SUPPORT_URL   = 'https://www.php.net/supported-versions.php';
53bb5a472eSGreg Roach    private const PHP_MINOR_VERSION = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
54bb5a472eSGreg Roach    private const PHP_SUPPORT_DATES = [
55*c9dab104SGreg Roach        '8.1' => '2025-12-31',
56*c9dab104SGreg Roach        '8.2' => '2026-12-31',
57*c9dab104SGreg Roach        '8.3' => '2027-12-31',
58*c9dab104SGreg Roach        '8.4' => '2028-12-31',
59b7059dccSGreg Roach    ];
60b7059dccSGreg Roach
616f6acc7aSGreg Roach    // As required by illuminate/database 8.x
626f6acc7aSGreg Roach    private const MINIMUM_SQLITE_VERSION = '3.8.8';
63b7059dccSGreg Roach
64b7059dccSGreg Roach    /**
65b7059dccSGreg Roach     * Things that may cause webtrees to break.
66b7059dccSGreg Roach     *
67b7059dccSGreg Roach     * @param string $driver
68b7059dccSGreg Roach     *
6936779af1SGreg Roach     * @return Collection<int,string>
70b7059dccSGreg Roach     */
7173d58381SGreg Roach    public function serverErrors(string $driver = ''): Collection
72b7059dccSGreg Roach    {
73b7059dccSGreg Roach        $errors = Collection::make([
74b7059dccSGreg Roach            $this->databaseDriverErrors($driver),
75b7059dccSGreg Roach            $this->checkPhpExtension('mbstring'),
76b7059dccSGreg Roach            $this->checkPhpExtension('iconv'),
77b7059dccSGreg Roach            $this->checkPhpExtension('pcre'),
78b7059dccSGreg Roach            $this->checkPhpExtension('session'),
79b7059dccSGreg Roach            $this->checkPhpExtension('xml'),
80b7059dccSGreg Roach            $this->checkPhpFunction('parse_ini_file'),
81b7059dccSGreg Roach        ]);
82b7059dccSGreg Roach
83b7059dccSGreg Roach        return $errors
84b7059dccSGreg Roach            ->flatten()
85b7059dccSGreg Roach            ->filter();
86b7059dccSGreg Roach    }
87b7059dccSGreg Roach
88b7059dccSGreg Roach    /**
89b7059dccSGreg Roach     * Things that should be fixed, but which won't stop completely webtrees from running.
90b7059dccSGreg Roach     *
91b7059dccSGreg Roach     * @param string $driver
92b7059dccSGreg Roach     *
9336779af1SGreg Roach     * @return Collection<int,string>
94b7059dccSGreg Roach     */
9573d58381SGreg Roach    public function serverWarnings(string $driver = ''): Collection
96b7059dccSGreg Roach    {
97b7059dccSGreg Roach        $warnings = Collection::make([
98b7059dccSGreg Roach            $this->databaseDriverWarnings($driver),
99b7059dccSGreg Roach            $this->checkPhpExtension('curl'),
100c95eb19bSGreg Roach            $this->checkPhpExtension('fileinfo'),
101b7059dccSGreg Roach            $this->checkPhpExtension('gd'),
102f7440925SGreg Roach            $this->checkPhpExtension('intl'),
10323c3b21dSGreg Roach            $this->checkPhpExtension('zip'),
104b7059dccSGreg Roach            $this->checkPhpIni('file_uploads', true),
105b7059dccSGreg Roach            $this->checkSystemTemporaryFolder(),
106b7059dccSGreg Roach            $this->checkPhpVersion(),
107b7059dccSGreg Roach        ]);
108b7059dccSGreg Roach
109b7059dccSGreg Roach        return $warnings
110b7059dccSGreg Roach            ->flatten()
111b7059dccSGreg Roach            ->filter();
112b7059dccSGreg Roach    }
113b7059dccSGreg Roach
114b7059dccSGreg Roach    /**
115b7059dccSGreg Roach     * Check if a PHP extension is loaded.
116b7059dccSGreg Roach     *
117b7059dccSGreg Roach     * @param string $extension
118b7059dccSGreg Roach     *
119b7059dccSGreg Roach     * @return string
120b7059dccSGreg Roach     */
121b7059dccSGreg Roach    private function checkPhpExtension(string $extension): string
122b7059dccSGreg Roach    {
123b7059dccSGreg Roach        if (!extension_loaded($extension)) {
124b7059dccSGreg Roach            return I18N::translate('The PHP extension “%s” is not installed.', $extension);
125b7059dccSGreg Roach        }
126b7059dccSGreg Roach
127b7059dccSGreg Roach        return '';
128b7059dccSGreg Roach    }
129b7059dccSGreg Roach
130b7059dccSGreg Roach    /**
131b7059dccSGreg Roach     * Check if a PHP setting is correct.
132b7059dccSGreg Roach     *
133b7059dccSGreg Roach     * @param string $varname
134b7059dccSGreg Roach     * @param bool   $expected
135b7059dccSGreg Roach     *
136b7059dccSGreg Roach     * @return string
137b7059dccSGreg Roach     */
138b7059dccSGreg Roach    private function checkPhpIni(string $varname, bool $expected): string
139b7059dccSGreg Roach    {
14010e06497SGreg Roach        $actual = (bool) ini_get($varname);
141b7059dccSGreg Roach
14210e06497SGreg Roach        if ($expected && !$actual) {
143b7059dccSGreg Roach            return I18N::translate('The PHP.INI setting “%1$s” is disabled.', $varname);
144b7059dccSGreg Roach        }
145b7059dccSGreg Roach
14610e06497SGreg Roach        if (!$expected && $actual) {
147b7059dccSGreg Roach            return I18N::translate('The PHP.INI setting “%1$s” is enabled.', $varname);
148b7059dccSGreg Roach        }
149b7059dccSGreg Roach
150b7059dccSGreg Roach        return '';
151b7059dccSGreg Roach    }
152b7059dccSGreg Roach
153b7059dccSGreg Roach    /**
1547bb10f9aSGreg Roach     * Check if a PHP function is in the list of disabled functions.
1557bb10f9aSGreg Roach     *
1567bb10f9aSGreg Roach     * @param string $function
1577bb10f9aSGreg Roach     *
1587d99559cSGreg Roach     * @return bool
1597bb10f9aSGreg Roach     */
1607bb10f9aSGreg Roach    public function isFunctionDisabled(string $function): bool
1617bb10f9aSGreg Roach    {
1627bb10f9aSGreg Roach        $function = strtolower($function);
1637bb10f9aSGreg Roach
164d8809d62SGreg Roach        $disable_functions = explode(',', (string) ini_get('disable_functions'));
165d8809d62SGreg Roach        $disable_functions = array_map(static fn (string $func): string => strtolower(trim($func)), $disable_functions);
166d8809d62SGreg Roach
1677bb10f9aSGreg Roach        return in_array($function, $disable_functions, true) || !function_exists($function);
1687bb10f9aSGreg Roach    }
1697bb10f9aSGreg Roach
1707bb10f9aSGreg Roach    /**
1717bb10f9aSGreg Roach     * Create a warning message for a disabled function.
172b7059dccSGreg Roach     *
173b7059dccSGreg Roach     * @param string $function
174b7059dccSGreg Roach     *
175b7059dccSGreg Roach     * @return string
176b7059dccSGreg Roach     */
177b7059dccSGreg Roach    private function checkPhpFunction(string $function): string
178b7059dccSGreg Roach    {
1797bb10f9aSGreg Roach        if ($this->isFunctionDisabled($function)) {
180acf70b2aSGreg Roach            return I18N::translate('The PHP function “%1$s” is disabled.', $function . '()');
181b7059dccSGreg Roach        }
182b7059dccSGreg Roach
183b7059dccSGreg Roach        return '';
184b7059dccSGreg Roach    }
185b7059dccSGreg Roach
186b7059dccSGreg Roach    /**
187fceda430SGreg Roach     * Some servers configure their temporary folder in an inaccessible place.
188b7059dccSGreg Roach     */
189b7059dccSGreg Roach    private function checkPhpVersion(): string
190b7059dccSGreg Roach    {
191b7059dccSGreg Roach        $today = date('Y-m-d');
192b7059dccSGreg Roach
193b7059dccSGreg Roach        foreach (self::PHP_SUPPORT_DATES as $version => $end_date) {
194497c5612SGreg Roach            if ($today > $end_date && version_compare(self::PHP_MINOR_VERSION, $version) <= 0) {
195b7059dccSGreg Roach                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>';
196b7059dccSGreg Roach            }
197b7059dccSGreg Roach        }
198b7059dccSGreg Roach
199b7059dccSGreg Roach        return '';
200b7059dccSGreg Roach    }
201b7059dccSGreg Roach
202b7059dccSGreg Roach    /**
203b7059dccSGreg Roach     * Check the
204b7059dccSGreg Roach     *
205b7059dccSGreg Roach     * @return string
206b7059dccSGreg Roach     */
207b7059dccSGreg Roach    private function checkSqliteVersion(): string
208b7059dccSGreg Roach    {
209b7059dccSGreg Roach        if (class_exists(SQLite3::class)) {
210b7059dccSGreg Roach            $sqlite_version = SQLite3::version()['versionString'];
211b7059dccSGreg Roach
212b7059dccSGreg Roach            if (version_compare($sqlite_version, self::MINIMUM_SQLITE_VERSION) < 0) {
213b7059dccSGreg Roach                return I18N::translate('SQLite version %s is installed. SQLite version %s or later is required.', $sqlite_version, self::MINIMUM_SQLITE_VERSION);
214b7059dccSGreg Roach            }
215b7059dccSGreg Roach        }
216b7059dccSGreg Roach
217b7059dccSGreg Roach        return '';
218b7059dccSGreg Roach    }
219b7059dccSGreg Roach
220b7059dccSGreg Roach    /**
221fceda430SGreg Roach     * Some servers configure their temporary folder in an inaccessible place.
222b7059dccSGreg Roach     */
223b7059dccSGreg Roach    private function checkSystemTemporaryFolder(): string
224b7059dccSGreg Roach    {
225b7059dccSGreg Roach        $open_basedir = ini_get('open_basedir');
226b7059dccSGreg Roach
227b33d97e2SGreg Roach        if ($open_basedir === '') {
228b33d97e2SGreg Roach            // open_basedir not used.
229b7059dccSGreg Roach            return '';
230b7059dccSGreg Roach        }
231b7059dccSGreg Roach
232b33d97e2SGreg Roach        $open_basedirs = explode(PATH_SEPARATOR, $open_basedir);
233b33d97e2SGreg Roach
234b33d97e2SGreg Roach        $sys_temp_dir = sys_get_temp_dir();
235b33d97e2SGreg Roach        $sys_temp_dir = $this->normalizeFolder($sys_temp_dir);
236b33d97e2SGreg Roach
237b33d97e2SGreg Roach        foreach ($open_basedirs as $dir) {
238b33d97e2SGreg Roach            $dir = $this->normalizeFolder($dir);
239b33d97e2SGreg Roach
240dec352c1SGreg Roach            if (str_starts_with($sys_temp_dir, $dir)) {
241b33d97e2SGreg Roach                return '';
242b33d97e2SGreg Roach            }
243b33d97e2SGreg Roach        }
244b33d97e2SGreg Roach
245b7059dccSGreg Roach        $message = I18N::translate('The server’s temporary folder cannot be accessed.');
246b7059dccSGreg Roach        $message .= '<br>sys_get_temp_dir() = "' . e($sys_temp_dir) . '"';
247b7059dccSGreg Roach        $message .= '<br>ini_get("open_basedir") = "' . e($open_basedir) . '"';
248b7059dccSGreg Roach
249b7059dccSGreg Roach        return $message;
250b7059dccSGreg Roach    }
251b7059dccSGreg Roach
252b7059dccSGreg Roach    /**
253b33d97e2SGreg Roach     * Convert a folder name to a canonical form:
254b33d97e2SGreg Roach     * - forward slashes.
255b33d97e2SGreg Roach     * - trailing slash.
256b33d97e2SGreg Roach     * We can't use realpath() as this can trigger open_basedir restrictions,
257b33d97e2SGreg Roach     * and we are using this code to find out whether open_basedir will affect us.
258b33d97e2SGreg Roach     *
259b33d97e2SGreg Roach     * @param string $path
260b33d97e2SGreg Roach     *
261b33d97e2SGreg Roach     * @return string
262b33d97e2SGreg Roach     */
263b33d97e2SGreg Roach    private function normalizeFolder(string $path): string
264b33d97e2SGreg Roach    {
265dec352c1SGreg Roach        $path = strtr($path, ['\\' => '/']);
266b33d97e2SGreg Roach
267dec352c1SGreg Roach        if (str_ends_with($path, '/')) {
268b33d97e2SGreg Roach            return $path;
269b33d97e2SGreg Roach        }
270b33d97e2SGreg Roach
271dec352c1SGreg Roach        return $path . '/';
272dec352c1SGreg Roach    }
273dec352c1SGreg Roach
274b33d97e2SGreg Roach    /**
275b7059dccSGreg Roach     * @param string $driver
276b7059dccSGreg Roach     *
27736779af1SGreg Roach     * @return Collection<int,string>
278b7059dccSGreg Roach     */
279b7059dccSGreg Roach    private function databaseDriverErrors(string $driver): Collection
280b7059dccSGreg Roach    {
281b7059dccSGreg Roach        switch ($driver) {
28252550490SGreg Roach            case DB::MYSQL:
283b7059dccSGreg Roach                return Collection::make([
284b7059dccSGreg Roach                    $this->checkPhpExtension('pdo'),
285b7059dccSGreg Roach                    $this->checkPhpExtension('pdo_mysql'),
286b7059dccSGreg Roach                ]);
287b7059dccSGreg Roach
28852550490SGreg Roach            case DB::SQLITE:
289b7059dccSGreg Roach                return Collection::make([
290b7059dccSGreg Roach                    $this->checkPhpExtension('pdo'),
291b7059dccSGreg Roach                    $this->checkPhpExtension('sqlite3'),
292b7059dccSGreg Roach                    $this->checkPhpExtension('pdo_sqlite'),
293b7059dccSGreg Roach                    $this->checkSqliteVersion(),
294b7059dccSGreg Roach                ]);
295b7059dccSGreg Roach
29652550490SGreg Roach            case DB::POSTGRES:
297b7059dccSGreg Roach                return Collection::make([
298b7059dccSGreg Roach                    $this->checkPhpExtension('pdo'),
299b7059dccSGreg Roach                    $this->checkPhpExtension('pdo_pgsql'),
300b7059dccSGreg Roach                ]);
301b7059dccSGreg Roach
30252550490SGreg Roach            case DB::SQL_SERVER:
303b7059dccSGreg Roach                return Collection::make([
304b7059dccSGreg Roach                    $this->checkPhpExtension('pdo'),
305b7059dccSGreg Roach                    $this->checkPhpExtension('pdo_odbc'),
306b7059dccSGreg Roach                ]);
307b7059dccSGreg Roach
308b7059dccSGreg Roach            default:
309b7059dccSGreg Roach                return new Collection();
310b7059dccSGreg Roach        }
311b7059dccSGreg Roach    }
312b7059dccSGreg Roach
313b7059dccSGreg Roach    /**
314b7059dccSGreg Roach     * @param string $driver
315b7059dccSGreg Roach     *
31636779af1SGreg Roach     * @return Collection<int,string>
317b7059dccSGreg Roach     */
318b7059dccSGreg Roach    private function databaseDriverWarnings(string $driver): Collection
319b7059dccSGreg Roach    {
320b7059dccSGreg Roach        switch ($driver) {
32152550490SGreg Roach            case DB::SQLITE:
322b7059dccSGreg Roach                return new Collection([
323b7059dccSGreg Roach                    I18N::translate('SQLite is only suitable for small sites, testing and evaluation.'),
324b7059dccSGreg Roach                ]);
325b7059dccSGreg Roach
32652550490SGreg Roach            case DB::POSTGRES:
327b7059dccSGreg Roach                return new Collection([
328b7059dccSGreg Roach                    I18N::translate('Support for PostgreSQL is experimental.'),
329b7059dccSGreg Roach                ]);
330b7059dccSGreg Roach
33152550490SGreg Roach            case DB::SQL_SERVER:
332b7059dccSGreg Roach                return new Collection([
333b7059dccSGreg Roach                    I18N::translate('Support for SQL Server is experimental.'),
334b7059dccSGreg Roach                ]);
335b7059dccSGreg Roach
336b7059dccSGreg Roach            default:
337b7059dccSGreg Roach                return new Collection();
338b7059dccSGreg Roach        }
339b7059dccSGreg Roach    }
340b7059dccSGreg Roach}
341