1<?php 2/** 3 * Form validation for configuration editor 4 */ 5 6declare(strict_types=1); 7 8namespace PhpMyAdmin\Config; 9 10use PhpMyAdmin\Core; 11use PhpMyAdmin\Util; 12use function mysqli_report; 13use const FILTER_FLAG_IPV4; 14use const FILTER_FLAG_IPV6; 15use const FILTER_VALIDATE_IP; 16use const MYSQLI_REPORT_OFF; 17use const PHP_INT_MAX; 18use function array_map; 19use function array_merge; 20use function array_shift; 21use function call_user_func_array; 22use function count; 23use function error_clear_last; 24use function error_get_last; 25use function explode; 26use function filter_var; 27use function htmlspecialchars; 28use function intval; 29use function is_array; 30use function is_object; 31use function mb_strpos; 32use function mb_substr; 33use function mysqli_close; 34use function mysqli_connect; 35use function preg_match; 36use function preg_replace; 37use function sprintf; 38use function str_replace; 39use function trim; 40 41/** 42 * Validation class for various validation functions 43 * 44 * Validation function takes two argument: id for which it is called 45 * and array of fields' values (usually values for entire formset). 46 * The function must always return an array with an error (or error array) 47 * assigned to a form element (formset name or field path). Even if there are 48 * no errors, key must be set with an empty value. 49 * 50 * Validation functions are assigned in $cfg_db['_validators'] (config.values.php). 51 */ 52class Validator 53{ 54 /** 55 * Returns validator list 56 * 57 * @param ConfigFile $cf Config file instance 58 * 59 * @return array 60 */ 61 public static function getValidators(ConfigFile $cf) 62 { 63 static $validators = null; 64 65 if ($validators !== null) { 66 return $validators; 67 } 68 69 $validators = $cf->getDbEntry('_validators', []); 70 if ($GLOBALS['PMA_Config']->get('is_setup')) { 71 return $validators; 72 } 73 74 // not in setup script: load additional validators for user 75 // preferences we need original config values not overwritten 76 // by user preferences, creating a new PhpMyAdmin\Config instance is a 77 // better idea than hacking into its code 78 $uvs = $cf->getDbEntry('_userValidators', []); 79 foreach ($uvs as $field => $uvList) { 80 $uvList = (array) $uvList; 81 foreach ($uvList as &$uv) { 82 if (! is_array($uv)) { 83 continue; 84 } 85 for ($i = 1, $nb = count($uv); $i < $nb; $i++) { 86 if (mb_substr($uv[$i], 0, 6) !== 'value:') { 87 continue; 88 } 89 90 $uv[$i] = Core::arrayRead( 91 mb_substr($uv[$i], 6), 92 $GLOBALS['PMA_Config']->baseSettings 93 ); 94 } 95 } 96 $validators[$field] = isset($validators[$field]) 97 ? array_merge((array) $validators[$field], $uvList) 98 : $uvList; 99 } 100 101 return $validators; 102 } 103 104 /** 105 * Runs validation $validator_id on values $values and returns error list. 106 * 107 * Return values: 108 * o array, keys - field path or formset id, values - array of errors 109 * when $isPostSource is true values is an empty array to allow for error list 110 * cleanup in HTML document 111 * o false - when no validators match name(s) given by $validator_id 112 * 113 * @param ConfigFile $cf Config file instance 114 * @param string|array $validatorId ID of validator(s) to run 115 * @param array $values Values to validate 116 * @param bool $isPostSource tells whether $values are directly from 117 * POST request 118 * 119 * @return bool|array 120 */ 121 public static function validate( 122 ConfigFile $cf, 123 $validatorId, 124 array &$values, 125 $isPostSource 126 ) { 127 // find validators 128 $validatorId = (array) $validatorId; 129 $validators = static::getValidators($cf); 130 $vids = []; 131 foreach ($validatorId as &$vid) { 132 $vid = $cf->getCanonicalPath($vid); 133 if (! isset($validators[$vid])) { 134 continue; 135 } 136 137 $vids[] = $vid; 138 } 139 if (empty($vids)) { 140 return false; 141 } 142 143 // create argument list with canonical paths and remember path mapping 144 $arguments = []; 145 $keyMap = []; 146 foreach ($values as $k => $v) { 147 $k2 = $isPostSource ? str_replace('-', '/', $k) : $k; 148 $k2 = mb_strpos($k2, '/') 149 ? $cf->getCanonicalPath($k2) 150 : $k2; 151 $keyMap[$k2] = $k; 152 $arguments[$k2] = $v; 153 } 154 155 // validate 156 $result = []; 157 foreach ($vids as $vid) { 158 // call appropriate validation functions 159 foreach ((array) $validators[$vid] as $validator) { 160 $vdef = (array) $validator; 161 $vname = array_shift($vdef); 162 $vname = 'PhpMyAdmin\Config\Validator::' . $vname; 163 $args = array_merge([$vid, &$arguments], $vdef); 164 $r = call_user_func_array($vname, $args); 165 166 // merge results 167 if (! is_array($r)) { 168 continue; 169 } 170 171 foreach ($r as $key => $errorList) { 172 // skip empty values if $isPostSource is false 173 if (! $isPostSource && empty($errorList)) { 174 continue; 175 } 176 if (! isset($result[$key])) { 177 $result[$key] = []; 178 } 179 180 $errorList = array_map('PhpMyAdmin\Sanitize::sanitizeMessage', (array) $errorList); 181 $result[$key] = array_merge($result[$key], $errorList); 182 } 183 } 184 } 185 186 // restore original paths 187 $newResult = []; 188 foreach ($result as $k => $v) { 189 $k2 = $keyMap[$k] ?? $k; 190 $newResult[$k2] = $v; 191 } 192 193 return empty($newResult) ? true : $newResult; 194 } 195 196 /** 197 * Test database connection 198 * 199 * @param string $host host name 200 * @param string $port tcp port to use 201 * @param string $socket socket to use 202 * @param string $user username to use 203 * @param string $pass password to use 204 * @param string $errorKey key to use in return array 205 * 206 * @return bool|array 207 */ 208 public static function testDBConnection( 209 $host, 210 $port, 211 $socket, 212 $user, 213 $pass = null, 214 $errorKey = 'Server' 215 ) { 216 if ($GLOBALS['cfg']['DBG']['demo']) { 217 // Connection test disabled on the demo server! 218 return true; 219 } 220 221 $error = null; 222 $host = Core::sanitizeMySQLHost($host); 223 224 error_clear_last(); 225 226 $socket = empty($socket) ? null : $socket; 227 $port = empty($port) ? null : $port; 228 229 mysqli_report(MYSQLI_REPORT_OFF); 230 231 $conn = @mysqli_connect($host, $user, (string) $pass, '', $port, (string) $socket); 232 if (! $conn) { 233 $error = __('Could not connect to the database server!'); 234 } else { 235 mysqli_close($conn); 236 } 237 if ($error !== null) { 238 $lastError = error_get_last(); 239 if ($lastError !== null) { 240 $error .= ' - ' . $lastError['message']; 241 } 242 } 243 244 return $error === null ? true : [$errorKey => $error]; 245 } 246 247 /** 248 * Validate server config 249 * 250 * @param string $path path to config, not used 251 * keep this parameter since the method is invoked using 252 * reflection along with other similar methods 253 * @param array $values config values 254 * 255 * @return array 256 */ 257 public static function validateServer($path, array $values) 258 { 259 $result = [ 260 'Server' => '', 261 'Servers/1/user' => '', 262 'Servers/1/SignonSession' => '', 263 'Servers/1/SignonURL' => '', 264 ]; 265 $error = false; 266 if (empty($values['Servers/1/auth_type'])) { 267 $values['Servers/1/auth_type'] = ''; 268 $result['Servers/1/auth_type'] = __('Invalid authentication type!'); 269 $error = true; 270 } 271 if ($values['Servers/1/auth_type'] === 'config' 272 && empty($values['Servers/1/user']) 273 ) { 274 $result['Servers/1/user'] = __( 275 'Empty username while using [kbd]config[/kbd] authentication method!' 276 ); 277 $error = true; 278 } 279 if ($values['Servers/1/auth_type'] === 'signon' 280 && empty($values['Servers/1/SignonSession']) 281 ) { 282 $result['Servers/1/SignonSession'] = __( 283 'Empty signon session name ' 284 . 'while using [kbd]signon[/kbd] authentication method!' 285 ); 286 $error = true; 287 } 288 if ($values['Servers/1/auth_type'] === 'signon' 289 && empty($values['Servers/1/SignonURL']) 290 ) { 291 $result['Servers/1/SignonURL'] = __( 292 'Empty signon URL while using [kbd]signon[/kbd] authentication ' 293 . 'method!' 294 ); 295 $error = true; 296 } 297 298 if (! $error && $values['Servers/1/auth_type'] === 'config') { 299 $password = ''; 300 if (! empty($values['Servers/1/password'])) { 301 $password = $values['Servers/1/password']; 302 } 303 $test = static::testDBConnection( 304 empty($values['Servers/1/host']) ? '' : $values['Servers/1/host'], 305 empty($values['Servers/1/port']) ? '' : $values['Servers/1/port'], 306 empty($values['Servers/1/socket']) ? '' : $values['Servers/1/socket'], 307 empty($values['Servers/1/user']) ? '' : $values['Servers/1/user'], 308 $password, 309 'Server' 310 ); 311 312 if ($test !== true) { 313 $result = array_merge($result, $test); 314 } 315 } 316 317 return $result; 318 } 319 320 /** 321 * Validate pmadb config 322 * 323 * @param string $path path to config, not used 324 * keep this parameter since the method is invoked using 325 * reflection along with other similar methods 326 * @param array $values config values 327 * 328 * @return array 329 */ 330 public static function validatePMAStorage($path, array $values) 331 { 332 $result = [ 333 'Server_pmadb' => '', 334 'Servers/1/controluser' => '', 335 'Servers/1/controlpass' => '', 336 ]; 337 $error = false; 338 339 if (empty($values['Servers/1/pmadb'])) { 340 return $result; 341 } 342 343 $result = []; 344 if (empty($values['Servers/1/controluser'])) { 345 $result['Servers/1/controluser'] = __( 346 'Empty phpMyAdmin control user while using phpMyAdmin configuration ' 347 . 'storage!' 348 ); 349 $error = true; 350 } 351 if (empty($values['Servers/1/controlpass'])) { 352 $result['Servers/1/controlpass'] = __( 353 'Empty phpMyAdmin control user password while using phpMyAdmin ' 354 . 'configuration storage!' 355 ); 356 $error = true; 357 } 358 if (! $error) { 359 $test = static::testDBConnection( 360 empty($values['Servers/1/host']) ? '' : $values['Servers/1/host'], 361 empty($values['Servers/1/port']) ? '' : $values['Servers/1/port'], 362 empty($values['Servers/1/socket']) ? '' : $values['Servers/1/socket'], 363 empty($values['Servers/1/controluser']) ? '' : $values['Servers/1/controluser'], 364 empty($values['Servers/1/controlpass']) ? '' : $values['Servers/1/controlpass'], 365 'Server_pmadb' 366 ); 367 if ($test !== true) { 368 $result = array_merge($result, $test); 369 } 370 } 371 372 return $result; 373 } 374 375 /** 376 * Validates regular expression 377 * 378 * @param string $path path to config 379 * @param array $values config values 380 * 381 * @return array 382 */ 383 public static function validateRegex($path, array $values) 384 { 385 $result = [$path => '']; 386 387 if (empty($values[$path])) { 388 return $result; 389 } 390 391 error_clear_last(); 392 393 $matches = []; 394 // in libraries/ListDatabase.php _checkHideDatabase(), 395 // a '/' is used as the delimiter for hide_db 396 @preg_match('/' . Util::requestString($values[$path]) . '/', '', $matches); 397 398 $currentError = error_get_last(); 399 400 if ($currentError !== null) { 401 $error = preg_replace('/^preg_match\(\): /', '', $currentError['message']); 402 403 return [$path => $error]; 404 } 405 406 return $result; 407 } 408 409 /** 410 * Validates TrustedProxies field 411 * 412 * @param string $path path to config 413 * @param array $values config values 414 * 415 * @return array 416 */ 417 public static function validateTrustedProxies($path, array $values) 418 { 419 $result = [$path => []]; 420 421 if (empty($values[$path])) { 422 return $result; 423 } 424 425 if (is_array($values[$path]) || is_object($values[$path])) { 426 // value already processed by FormDisplay::save 427 $lines = []; 428 foreach ($values[$path] as $ip => $v) { 429 $v = Util::requestString($v); 430 $lines[] = preg_match('/^-\d+$/', $ip) 431 ? $v 432 : $ip . ': ' . $v; 433 } 434 } else { 435 // AJAX validation 436 $lines = explode("\n", $values[$path]); 437 } 438 foreach ($lines as $line) { 439 $line = trim($line); 440 $matches = []; 441 // we catch anything that may (or may not) be an IP 442 if (! preg_match('/^(.+):(?:[ ]?)\\w+$/', $line, $matches)) { 443 $result[$path][] = __('Incorrect value:') . ' ' 444 . htmlspecialchars($line); 445 continue; 446 } 447 // now let's check whether we really have an IP address 448 if (filter_var($matches[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false 449 && filter_var($matches[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false 450 ) { 451 $ip = htmlspecialchars(trim($matches[1])); 452 $result[$path][] = sprintf(__('Incorrect IP address: %s'), $ip); 453 continue; 454 } 455 } 456 457 return $result; 458 } 459 460 /** 461 * Tests integer value 462 * 463 * @param string $path path to config 464 * @param array $values config values 465 * @param bool $allowNegative allow negative values 466 * @param bool $allowZero allow zero 467 * @param int $maxValue max allowed value 468 * @param string $errorString error message string 469 * 470 * @return string empty string if test is successful 471 */ 472 public static function validateNumber( 473 $path, 474 array $values, 475 $allowNegative, 476 $allowZero, 477 $maxValue, 478 $errorString 479 ) { 480 if (empty($values[$path])) { 481 return ''; 482 } 483 484 $value = Util::requestString($values[$path]); 485 486 if (intval($value) != $value 487 || (! $allowNegative && $value < 0) 488 || (! $allowZero && $value == 0) 489 || $value > $maxValue 490 ) { 491 return $errorString; 492 } 493 494 return ''; 495 } 496 497 /** 498 * Validates port number 499 * 500 * @param string $path path to config 501 * @param array $values config values 502 * 503 * @return array 504 */ 505 public static function validatePortNumber($path, array $values) 506 { 507 return [ 508 $path => static::validateNumber( 509 $path, 510 $values, 511 false, 512 false, 513 65535, 514 __('Not a valid port number!') 515 ), 516 ]; 517 } 518 519 /** 520 * Validates positive number 521 * 522 * @param string $path path to config 523 * @param array $values config values 524 * 525 * @return array 526 */ 527 public static function validatePositiveNumber($path, array $values) 528 { 529 return [ 530 $path => static::validateNumber( 531 $path, 532 $values, 533 false, 534 false, 535 PHP_INT_MAX, 536 __('Not a positive number!') 537 ), 538 ]; 539 } 540 541 /** 542 * Validates non-negative number 543 * 544 * @param string $path path to config 545 * @param array $values config values 546 * 547 * @return array 548 */ 549 public static function validateNonNegativeNumber($path, array $values) 550 { 551 return [ 552 $path => static::validateNumber( 553 $path, 554 $values, 555 false, 556 true, 557 PHP_INT_MAX, 558 __('Not a non-negative number!') 559 ), 560 ]; 561 } 562 563 /** 564 * Validates value according to given regular expression 565 * Pattern and modifiers must be a valid for PCRE <b>and</b> JavaScript RegExp 566 * 567 * @param string $path path to config 568 * @param array $values config values 569 * @param string $regex regular expression to match 570 * 571 * @return array|string 572 */ 573 public static function validateByRegex($path, array $values, $regex) 574 { 575 if (! isset($values[$path])) { 576 return ''; 577 } 578 $result = preg_match($regex, Util::requestString($values[$path])); 579 580 return [$path => $result ? '' : __('Incorrect value!')]; 581 } 582 583 /** 584 * Validates upper bound for numeric inputs 585 * 586 * @param string $path path to config 587 * @param array $values config values 588 * @param int $maxValue maximal allowed value 589 * 590 * @return array 591 */ 592 public static function validateUpperBound($path, array $values, $maxValue) 593 { 594 $result = $values[$path] <= $maxValue; 595 596 return [ 597 $path => $result ? '' : sprintf( 598 __('Value must be less than or equal to %s!'), 599 $maxValue 600 ), 601 ]; 602 } 603} 604