Issues (2491)

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