Issues (2564)

app/Services/ServerCheckService.php (1 issue)

Labels
Severity
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Services;
21
22
use Fisharebest\Webtrees\DB;
23
use Fisharebest\Webtrees\I18N;
24
use Illuminate\Support\Collection;
25
use SQLite3;
26
27
use function array_map;
28
use function class_exists;
29
use function date;
30
use function e;
31
use function explode;
32
use function in_array;
33
use function str_ends_with;
34
use function str_starts_with;
35
use function strtolower;
36
use function version_compare;
37
38
use const PATH_SEPARATOR;
39
use const PHP_MAJOR_VERSION;
40
use const PHP_MINOR_VERSION;
41
use const PHP_VERSION;
42
43
/**
44
 * Check if the server meets the minimum requirements for webtrees.
45
 */
46
class ServerCheckService
47
{
48
    private const string PHP_SUPPORT_URL   = 'https://www.php.net/supported-versions.php';
0 ignored issues
show
A parse error occurred: Syntax error, unexpected T_STRING, expecting '=' on line 48 at column 25
Loading history...
49
    private const string PHP_MINOR_VERSION = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
50
    private const array PHP_SUPPORT_DATES = [
51
        '8.1' => '2025-12-31',
52
        '8.2' => '2026-12-31',
53
        '8.3' => '2027-12-31',
54
        '8.4' => '2028-12-31',
55
    ];
56
57
    // As required by illuminate/database 8.x
58
    private const string MINIMUM_SQLITE_VERSION = '3.8.8';
59
60
    public function __construct(private PhpService $php_service)
61
    {
62
    }
63
64
    /**
65
     * Things that may cause webtrees to break.
66
     *
67
     * @param string $driver
68
     *
69
     * @return Collection<int,string>
70
     */
71
    public function serverErrors(string $driver = ''): Collection
72
    {
73
        $errors = Collection::make([
74
            $this->databaseDriverErrors($driver),
75
            $this->checkPhpExtension('mbstring'),
76
            $this->checkPhpExtension('iconv'),
77
            $this->checkPhpExtension('pcre'),
78
            $this->checkPhpExtension('session'),
79
            $this->checkPhpExtension('xml'),
80
            $this->checkPhpFunction('parse_ini_file'),
81
        ]);
82
83
        return $errors
84
            ->flatten()
85
            ->filter();
86
    }
87
88
    /**
89
     * Things that should be fixed, but which won't stop completely webtrees from running.
90
     *
91
     * @param string $driver
92
     *
93
     * @return Collection<int,string>
94
     */
95
    public function serverWarnings(string $driver = ''): Collection
96
    {
97
        $warnings = Collection::make([
98
            $this->databaseDriverWarnings($driver),
99
            $this->checkPhpExtension('curl'),
100
            $this->checkPhpExtension('fileinfo'),
101
            $this->checkPhpExtension('gd'),
102
            $this->checkPhpExtension('intl'),
103
            $this->checkPhpExtension('zip'),
104
            $this->checkPhpIni('file_uploads', true),
105
            $this->checkSystemTemporaryFolder(),
106
            $this->checkPhpVersion(),
107
        ]);
108
109
        return $warnings
110
            ->flatten()
111
            ->filter();
112
    }
113
114
    private function checkPhpExtension(string $extension): string
115
    {
116
        if (!$this->php_service->extensionLoaded(extension: $extension)) {
117
            return I18N::translate('The PHP extension “%s” is not installed.', $extension);
118
        }
119
120
        return '';
121
    }
122
123
    private function checkPhpIni(string $varname, bool $expected): string
124
    {
125
        $actual = (bool) $this->php_service->iniGet(option: $varname);
126
127
        if ($expected && !$actual) {
128
            return I18N::translate('The PHP.INI setting “%1$s” is disabled.', $varname);
129
        }
130
131
        if (!$expected && $actual) {
132
            return I18N::translate('The PHP.INI setting “%1$s” is enabled.', $varname);
133
        }
134
135
        return '';
136
    }
137
138
    public function isFunctionDisabled(string $function): bool
139
    {
140
        $function = strtolower($function);
141
142
        $disable_functions = explode(',', $this->php_service->iniGet('disable_functions'));
143
        $disable_functions = array_map(trim(...), $disable_functions);
144
        $disable_functions = array_map(strtolower(...), $disable_functions);
145
146
        return
147
            in_array($function, $disable_functions, true) ||
148
            !$this->php_service->functionExists(function: $function);
149
    }
150
151
    /**
152
     * Create a warning message for a disabled function.
153
     */
154
    private function checkPhpFunction(string $function): string
155
    {
156
        if ($this->isFunctionDisabled($function)) {
157
            return I18N::translate('The PHP function “%1$s” is disabled.', $function . '()');
158
        }
159
160
        return '';
161
    }
162
163
    private function checkPhpVersion(): string
164
    {
165
        $today = date('Y-m-d');
166
167
        foreach (self::PHP_SUPPORT_DATES as $version => $end_date) {
168
            if ($today > $end_date && version_compare(self::PHP_MINOR_VERSION, $version) <= 0) {
169
                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>';
170
            }
171
        }
172
173
        return '';
174
    }
175
176
    private function checkSqliteVersion(): string
177
    {
178
        if (class_exists(SQLite3::class)) {
179
            $sqlite_version = SQLite3::version()['versionString'];
180
181
            if (version_compare($sqlite_version, self::MINIMUM_SQLITE_VERSION) < 0) {
182
                return I18N::translate('SQLite version %s is installed. SQLite version %s or later is required.', $sqlite_version, self::MINIMUM_SQLITE_VERSION);
183
            }
184
        }
185
186
        return '';
187
    }
188
189
    /**
190
     * Some servers configure their temporary folder in an inaccessible place.
191
     */
192
    private function checkSystemTemporaryFolder(): string
193
    {
194
        $open_basedir = $this->php_service->iniGet(option: 'open_basedir');
195
196
        if ($open_basedir === '') {
197
            // open_basedir not used.
198
            return '';
199
        }
200
201
        $open_basedirs = explode(PATH_SEPARATOR, $open_basedir);
202
203
        $sys_temp_dir = $this->php_service->sysGetTempDir();
204
        $sys_temp_dir = $this->normalizeFolder($sys_temp_dir);
205
206
        foreach ($open_basedirs as $dir) {
207
            $dir = $this->normalizeFolder($dir);
208
209
            if (str_starts_with($sys_temp_dir, $dir)) {
210
                return '';
211
            }
212
        }
213
214
        $message = I18N::translate('The server’s temporary folder cannot be accessed.');
215
        $message .= '<br>sys_get_temp_dir() = "' . e($sys_temp_dir) . '"';
216
        $message .= '<br>ini_get("open_basedir") = "' . e($open_basedir) . '"';
217
218
        return $message;
219
    }
220
221
    /**
222
     * Convert a folder name to a canonical form:
223
     * - forward slashes.
224
     * - trailing slash.
225
     * We can't use realpath() as this can trigger open_basedir restrictions,
226
     * and we are using this code to find out whether open_basedir will affect us.
227
     */
228
    private function normalizeFolder(string $path): string
229
    {
230
        $path = strtr($path, ['\\' => '/']);
231
232
        if (str_ends_with($path, '/')) {
233
            return $path;
234
        }
235
236
        return $path . '/';
237
    }
238
239
    /**
240
     * @return Collection<int,string>
241
     */
242
    private function databaseDriverErrors(string $driver): Collection
243
    {
244
        switch ($driver) {
245
            case DB::MYSQL:
246
                return Collection::make([
247
                    $this->checkPhpExtension('pdo'),
248
                    $this->checkPhpExtension('pdo_mysql'),
249
                ]);
250
251
            case DB::SQLITE:
252
                return Collection::make([
253
                    $this->checkPhpExtension('pdo'),
254
                    $this->checkPhpExtension('sqlite3'),
255
                    $this->checkPhpExtension('pdo_sqlite'),
256
                    $this->checkSqliteVersion(),
257
                ]);
258
259
            case DB::POSTGRES:
260
                return Collection::make([
261
                    $this->checkPhpExtension('pdo'),
262
                    $this->checkPhpExtension('pdo_pgsql'),
263
                ]);
264
265
            case DB::SQL_SERVER:
266
                return Collection::make([
267
                    $this->checkPhpExtension('pdo'),
268
                    $this->checkPhpExtension('pdo_odbc'),
269
                ]);
270
271
            default:
272
                return new Collection();
273
        }
274
    }
275
276
    /**
277
     * @return Collection<int,string>
278
     */
279
    private function databaseDriverWarnings(string $driver): Collection
280
    {
281
        switch ($driver) {
282
            case DB::SQLITE:
283
                return new Collection([
284
                    I18N::translate('SQLite is only suitable for small sites, testing and evaluation.'),
285
                ]);
286
287
            case DB::POSTGRES:
288
                return new Collection([
289
                    I18N::translate('Support for PostgreSQL is experimental.'),
290
                ]);
291
292
            case DB::SQL_SERVER:
293
                return new Collection([
294
                    I18N::translate('Support for SQL Server is experimental.'),
295
                ]);
296
297
            default:
298
                return new Collection();
299
        }
300
    }
301
}
302