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