1<?php 2/** 3 * Core functions used all over the scripts. 4 * This script is distinct from libraries/common.inc.php because this 5 * script is called from /test. 6 */ 7 8declare(strict_types=1); 9 10namespace PhpMyAdmin; 11 12use PhpMyAdmin\Plugins\AuthenticationPlugin; 13use Symfony\Component\Config\FileLocator; 14use Symfony\Component\DependencyInjection\ContainerBuilder; 15use Symfony\Component\DependencyInjection\ContainerInterface; 16use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; 17 18use const DATE_RFC1123; 19use const E_USER_ERROR; 20use const E_USER_WARNING; 21use const FILTER_VALIDATE_IP; 22use function array_keys; 23use function array_pop; 24use function array_walk_recursive; 25use function chr; 26use function count; 27use function date_default_timezone_get; 28use function date_default_timezone_set; 29use function defined; 30use function explode; 31use function extension_loaded; 32use function filter_var; 33use function function_exists; 34use function getenv; 35use function gettype; 36use function gmdate; 37use function hash_equals; 38use function hash_hmac; 39use function header; 40use function htmlspecialchars; 41use function http_build_query; 42use function implode; 43use function in_array; 44use function ini_get; 45use function ini_set; 46use function intval; 47use function is_array; 48use function is_numeric; 49use function is_scalar; 50use function is_string; 51use function json_encode; 52use function mb_internal_encoding; 53use function mb_strlen; 54use function mb_strpos; 55use function mb_strrpos; 56use function mb_substr; 57use function parse_str; 58use function parse_url; 59use function preg_match; 60use function preg_replace; 61use function session_id; 62use function session_write_close; 63use function sprintf; 64use function str_replace; 65use function strlen; 66use function strpos; 67use function strtolower; 68use function strtr; 69use function substr; 70use function trigger_error; 71use function unserialize; 72use function urldecode; 73use function vsprintf; 74use function json_decode; 75 76/** 77 * Core class 78 */ 79class Core 80{ 81 /** 82 * checks given $var and returns it if valid, or $default of not valid 83 * given $var is also checked for type being 'similar' as $default 84 * or against any other type if $type is provided 85 * 86 * <code> 87 * // $_REQUEST['db'] not set 88 * echo Core::ifSetOr($_REQUEST['db'], ''); // '' 89 * // $_POST['sql_query'] not set 90 * echo Core::ifSetOr($_POST['sql_query']); // null 91 * // $cfg['EnableFoo'] not set 92 * echo Core::ifSetOr($cfg['EnableFoo'], false, 'boolean'); // false 93 * echo Core::ifSetOr($cfg['EnableFoo']); // null 94 * // $cfg['EnableFoo'] set to 1 95 * echo Core::ifSetOr($cfg['EnableFoo'], false, 'boolean'); // false 96 * echo Core::ifSetOr($cfg['EnableFoo'], false, 'similar'); // 1 97 * echo Core::ifSetOr($cfg['EnableFoo'], false); // 1 98 * // $cfg['EnableFoo'] set to true 99 * echo Core::ifSetOr($cfg['EnableFoo'], false, 'boolean'); // true 100 * </code> 101 * 102 * @see self::isValid() 103 * 104 * @param mixed $var param to check 105 * @param mixed $default default value 106 * @param mixed $type var type or array of values to check against $var 107 * 108 * @return mixed $var or $default 109 */ 110 public static function ifSetOr(&$var, $default = null, $type = 'similar') 111 { 112 if (! self::isValid($var, $type, $default)) { 113 return $default; 114 } 115 116 return $var; 117 } 118 119 /** 120 * checks given $var against $type or $compare 121 * 122 * $type can be: 123 * - false : no type checking 124 * - 'scalar' : whether type of $var is integer, float, string or boolean 125 * - 'numeric' : whether type of $var is any number representation 126 * - 'length' : whether type of $var is scalar with a string length > 0 127 * - 'similar' : whether type of $var is similar to type of $compare 128 * - 'equal' : whether type of $var is identical to type of $compare 129 * - 'identical' : whether $var is identical to $compare, not only the type! 130 * - or any other valid PHP variable type 131 * 132 * <code> 133 * // $_REQUEST['doit'] = true; 134 * Core::isValid($_REQUEST['doit'], 'identical', 'true'); // false 135 * // $_REQUEST['doit'] = 'true'; 136 * Core::isValid($_REQUEST['doit'], 'identical', 'true'); // true 137 * </code> 138 * 139 * NOTE: call-by-reference is used to not get NOTICE on undefined vars, 140 * but the var is not altered inside this function, also after checking a var 141 * this var exists nut is not set, example: 142 * <code> 143 * // $var is not set 144 * isset($var); // false 145 * functionCallByReference($var); // false 146 * isset($var); // true 147 * functionCallByReference($var); // true 148 * </code> 149 * 150 * to avoid this we set this var to null if not isset 151 * 152 * @see https://www.php.net/gettype 153 * 154 * @param mixed $var variable to check 155 * @param mixed $type var type or array of valid values to check against $var 156 * @param mixed $compare var to compare with $var 157 * 158 * @return bool whether valid or not 159 * 160 * @todo add some more var types like hex, bin, ...? 161 */ 162 public static function isValid(&$var, $type = 'length', $compare = null): bool 163 { 164 if (! isset($var)) { 165 // var is not even set 166 return false; 167 } 168 169 if ($type === false) { 170 // no vartype requested 171 return true; 172 } 173 174 if (is_array($type)) { 175 return in_array($var, $type); 176 } 177 178 // allow some aliases of var types 179 $type = strtolower($type); 180 switch ($type) { 181 case 'identic': 182 $type = 'identical'; 183 break; 184 case 'len': 185 $type = 'length'; 186 break; 187 case 'bool': 188 $type = 'boolean'; 189 break; 190 case 'float': 191 $type = 'double'; 192 break; 193 case 'int': 194 $type = 'integer'; 195 break; 196 case 'null': 197 $type = 'NULL'; 198 break; 199 } 200 201 if ($type === 'identical') { 202 return $var === $compare; 203 } 204 205 // whether we should check against given $compare 206 if ($type === 'similar') { 207 switch (gettype($compare)) { 208 case 'string': 209 case 'boolean': 210 $type = 'scalar'; 211 break; 212 case 'integer': 213 case 'double': 214 $type = 'numeric'; 215 break; 216 default: 217 $type = gettype($compare); 218 } 219 } elseif ($type === 'equal') { 220 $type = gettype($compare); 221 } 222 223 // do the check 224 if ($type === 'length' || $type === 'scalar') { 225 $is_scalar = is_scalar($var); 226 if ($is_scalar && $type === 'length') { 227 return strlen((string) $var) > 0; 228 } 229 230 return $is_scalar; 231 } 232 233 if ($type === 'numeric') { 234 return is_numeric($var); 235 } 236 237 return gettype($var) === $type; 238 } 239 240 /** 241 * Removes insecure parts in a path; used before include() or 242 * require() when a part of the path comes from an insecure source 243 * like a cookie or form. 244 * 245 * @param string $path The path to check 246 */ 247 public static function securePath(string $path): string 248 { 249 // change .. to . 250 return (string) preg_replace('@\.\.*@', '.', $path); 251 } 252 253 /** 254 * displays the given error message on phpMyAdmin error page in foreign language, 255 * ends script execution and closes session 256 * 257 * loads language file if not loaded already 258 * 259 * @param string $error_message the error message or named error message 260 * @param string|array $message_args arguments applied to $error_message 261 */ 262 public static function fatalError( 263 string $error_message, 264 $message_args = null 265 ): void { 266 global $dbi; 267 268 /* Use format string if applicable */ 269 if (is_string($message_args)) { 270 $error_message = sprintf($error_message, $message_args); 271 } elseif (is_array($message_args)) { 272 $error_message = vsprintf($error_message, $message_args); 273 } 274 275 /* 276 * Avoid using Response class as config does not have to be loaded yet 277 * (this can happen on early fatal error) 278 */ 279 if (isset($dbi, $GLOBALS['PMA_Config']) && $dbi !== null 280 && $GLOBALS['PMA_Config']->get('is_setup') === false 281 && Response::getInstance()->isAjax() 282 ) { 283 $response = Response::getInstance(); 284 $response->setRequestStatus(false); 285 $response->addJSON('message', Message::error($error_message)); 286 } elseif (! empty($_REQUEST['ajax_request'])) { 287 // Generate JSON manually 288 self::headerJSON(); 289 echo json_encode( 290 [ 291 'success' => false, 292 'message' => Message::error($error_message)->getDisplay(), 293 ] 294 ); 295 } else { 296 $error_message = strtr($error_message, ['<br>' => '[br]']); 297 $template = new Template(); 298 299 echo $template->render('error/generic', [ 300 'lang' => $GLOBALS['lang'] ?? 'en', 301 'dir' => $GLOBALS['text_dir'] ?? 'ltr', 302 'error_message' => Sanitize::sanitizeMessage($error_message), 303 ]); 304 } 305 if (! defined('TESTSUITE')) { 306 exit; 307 } 308 } 309 310 /** 311 * Returns a link to the PHP documentation 312 * 313 * @param string $target anchor in documentation 314 * 315 * @return string the URL 316 * 317 * @access public 318 */ 319 public static function getPHPDocLink(string $target): string 320 { 321 /* List of PHP documentation translations */ 322 $php_doc_languages = [ 323 'pt_BR', 324 'zh', 325 'fr', 326 'de', 327 'it', 328 'ja', 329 'ro', 330 'ru', 331 'es', 332 'tr', 333 ]; 334 335 $lang = 'en'; 336 if (isset($GLOBALS['lang']) && in_array($GLOBALS['lang'], $php_doc_languages)) { 337 $lang = $GLOBALS['lang']; 338 } 339 340 return self::linkURL('https://www.php.net/manual/' . $lang . '/' . $target); 341 } 342 343 /** 344 * Warn or fail on missing extension. 345 * 346 * @param string $extension Extension name 347 * @param bool $fatal Whether the error is fatal. 348 * @param string $extra Extra string to append to message. 349 */ 350 public static function warnMissingExtension( 351 string $extension, 352 bool $fatal = false, 353 string $extra = '' 354 ): void { 355 /** @var ErrorHandler $error_handler */ 356 global $error_handler; 357 358 /* Gettext does not have to be loaded yet here */ 359 if (function_exists('__')) { 360 $message = __( 361 'The %s extension is missing. Please check your PHP configuration.' 362 ); 363 } else { 364 $message 365 = 'The %s extension is missing. Please check your PHP configuration.'; 366 } 367 $doclink = self::getPHPDocLink('book.' . $extension . '.php'); 368 $message = sprintf( 369 $message, 370 '[a@' . $doclink . '@Documentation][em]' . $extension . '[/em][/a]' 371 ); 372 if ($extra != '') { 373 $message .= ' ' . $extra; 374 } 375 if ($fatal) { 376 self::fatalError($message); 377 378 return; 379 } 380 381 $error_handler->addError( 382 $message, 383 E_USER_WARNING, 384 '', 385 0, 386 false 387 ); 388 } 389 390 /** 391 * returns count of tables in given db 392 * 393 * @param string $db database to count tables for 394 * 395 * @return int count of tables in $db 396 */ 397 public static function getTableCount(string $db): int 398 { 399 global $dbi; 400 401 $tables = $dbi->tryQuery( 402 'SHOW TABLES FROM ' . Util::backquote($db) . ';', 403 DatabaseInterface::CONNECT_USER, 404 DatabaseInterface::QUERY_STORE 405 ); 406 if ($tables) { 407 $num_tables = $dbi->numRows($tables); 408 $dbi->freeResult($tables); 409 } else { 410 $num_tables = 0; 411 } 412 413 return $num_tables; 414 } 415 416 /** 417 * Converts numbers like 10M into bytes 418 * Used with permission from Moodle (https://moodle.org) by Martin Dougiamas 419 * (renamed with PMA prefix to avoid double definition when embedded 420 * in Moodle) 421 * 422 * @param string|int $size size (Default = 0) 423 */ 424 public static function getRealSize($size = 0): int 425 { 426 if (! $size) { 427 return 0; 428 } 429 430 $binaryprefixes = [ 431 'T' => 1099511627776, 432 't' => 1099511627776, 433 'G' => 1073741824, 434 'g' => 1073741824, 435 'M' => 1048576, 436 'm' => 1048576, 437 'K' => 1024, 438 'k' => 1024, 439 ]; 440 441 if (preg_match('/^([0-9]+)([KMGT])/i', (string) $size, $matches)) { 442 return (int) ($matches[1] * $binaryprefixes[$matches[2]]); 443 } 444 445 return (int) $size; 446 } 447 448 /** 449 * Checks given $page against given $allowList and returns true if valid 450 * it optionally ignores query parameters in $page (script.php?ignored) 451 * 452 * @param string $page page to check 453 * @param array $allowList allow list to check page against 454 * @param bool $include whether the page is going to be included 455 * 456 * @return bool whether $page is valid or not (in $allowList or not) 457 */ 458 public static function checkPageValidity(&$page, array $allowList = [], $include = false): bool 459 { 460 if (empty($allowList)) { 461 $allowList = ['index.php']; 462 } 463 if (empty($page)) { 464 return false; 465 } 466 467 if (in_array($page, $allowList)) { 468 return true; 469 } 470 if ($include) { 471 return false; 472 } 473 474 $_page = mb_substr( 475 $page, 476 0, 477 (int) mb_strpos($page . '?', '?') 478 ); 479 if (in_array($_page, $allowList)) { 480 return true; 481 } 482 483 $_page = urldecode($page); 484 $_page = mb_substr( 485 $_page, 486 0, 487 (int) mb_strpos($_page . '?', '?') 488 ); 489 490 return in_array($_page, $allowList); 491 } 492 493 /** 494 * tries to find the value for the given environment variable name 495 * 496 * searches in $_SERVER, $_ENV then tries getenv() and apache_getenv() 497 * in this order 498 * 499 * @param string $var_name variable name 500 * 501 * @return string value of $var or empty string 502 */ 503 public static function getenv(string $var_name): string 504 { 505 if (isset($_SERVER[$var_name])) { 506 return (string) $_SERVER[$var_name]; 507 } 508 509 if (isset($_ENV[$var_name])) { 510 return (string) $_ENV[$var_name]; 511 } 512 513 if (getenv($var_name)) { 514 return (string) getenv($var_name); 515 } 516 517 if (function_exists('apache_getenv') 518 && apache_getenv($var_name, true) 519 ) { 520 return (string) apache_getenv($var_name, true); 521 } 522 523 return ''; 524 } 525 526 /** 527 * Send HTTP header, taking IIS limits into account (600 seems ok) 528 * 529 * @param string $uri the header to send 530 * @param bool $use_refresh whether to use Refresh: header when running on IIS 531 */ 532 public static function sendHeaderLocation(string $uri, bool $use_refresh = false): void 533 { 534 if ($GLOBALS['PMA_Config']->get('PMA_IS_IIS') && mb_strlen($uri) > 600) { 535 Response::getInstance()->disable(); 536 537 $template = new Template(); 538 echo $template->render('header_location', ['uri' => $uri]); 539 540 return; 541 } 542 543 /* 544 * Avoid relative path redirect problems in case user entered URL 545 * like /phpmyadmin/index.php/ which some web servers happily accept. 546 */ 547 if ($uri[0] === '.') { 548 $uri = $GLOBALS['PMA_Config']->getRootPath() . substr($uri, 2); 549 } 550 551 $response = Response::getInstance(); 552 553 session_write_close(); 554 if ($response->headersSent()) { 555 trigger_error( 556 'Core::sendHeaderLocation called when headers are already sent!', 557 E_USER_ERROR 558 ); 559 } 560 // bug #1523784: IE6 does not like 'Refresh: 0', it 561 // results in a blank page 562 // but we need it when coming from the cookie login panel) 563 if ($GLOBALS['PMA_Config']->get('PMA_IS_IIS') && $use_refresh) { 564 $response->header('Refresh: 0; ' . $uri); 565 } else { 566 $response->header('Location: ' . $uri); 567 } 568 } 569 570 /** 571 * Outputs application/json headers. This includes no caching. 572 */ 573 public static function headerJSON(): void 574 { 575 if (defined('TESTSUITE')) { 576 return; 577 } 578 // No caching 579 self::noCacheHeader(); 580 // MIME type 581 header('Content-Type: application/json; charset=UTF-8'); 582 // Disable content sniffing in browser 583 // This is needed in case we include HTML in JSON, browser might assume it's 584 // html to display 585 header('X-Content-Type-Options: nosniff'); 586 } 587 588 /** 589 * Outputs headers to prevent caching in browser (and on the way). 590 */ 591 public static function noCacheHeader(): void 592 { 593 if (defined('TESTSUITE')) { 594 return; 595 } 596 // rfc2616 - Section 14.21 597 header('Expires: ' . gmdate(DATE_RFC1123)); 598 // HTTP/1.1 599 header( 600 'Cache-Control: no-store, no-cache, must-revalidate,' 601 . ' pre-check=0, post-check=0, max-age=0' 602 ); 603 604 header('Pragma: no-cache'); // HTTP/1.0 605 // test case: exporting a database into a .gz file with Safari 606 // would produce files not having the current time 607 // (added this header for Safari but should not harm other browsers) 608 header('Last-Modified: ' . gmdate(DATE_RFC1123)); 609 } 610 611 /** 612 * Sends header indicating file download. 613 * 614 * @param string $filename Filename to include in headers if empty, 615 * none Content-Disposition header will be sent. 616 * @param string $mimetype MIME type to include in headers. 617 * @param int $length Length of content (optional) 618 * @param bool $no_cache Whether to include no-caching headers. 619 */ 620 public static function downloadHeader( 621 string $filename, 622 string $mimetype, 623 int $length = 0, 624 bool $no_cache = true 625 ): void { 626 if ($no_cache) { 627 self::noCacheHeader(); 628 } 629 /* Replace all possibly dangerous chars in filename */ 630 $filename = Sanitize::sanitizeFilename($filename); 631 if (! empty($filename)) { 632 header('Content-Description: File Transfer'); 633 header('Content-Disposition: attachment; filename="' . $filename . '"'); 634 } 635 header('Content-Type: ' . $mimetype); 636 // inform the server that compression has been done, 637 // to avoid a double compression (for example with Apache + mod_deflate) 638 $notChromeOrLessThan43 = PMA_USR_BROWSER_AGENT != 'CHROME' // see bug #4942 639 || (PMA_USR_BROWSER_AGENT == 'CHROME' && PMA_USR_BROWSER_VER < 43); 640 if (strpos($mimetype, 'gzip') !== false && $notChromeOrLessThan43) { 641 header('Content-Encoding: gzip'); 642 } 643 header('Content-Transfer-Encoding: binary'); 644 if ($length <= 0) { 645 return; 646 } 647 648 header('Content-Length: ' . $length); 649 } 650 651 /** 652 * Returns value of an element in $array given by $path. 653 * $path is a string describing position of an element in an associative array, 654 * eg. Servers/1/host refers to $array[Servers][1][host] 655 * 656 * @param string $path path in the array 657 * @param array $array the array 658 * @param mixed $default default value 659 * 660 * @return array|mixed|null array element or $default 661 */ 662 public static function arrayRead(string $path, array $array, $default = null) 663 { 664 $keys = explode('/', $path); 665 $value =& $array; 666 foreach ($keys as $key) { 667 if (! isset($value[$key])) { 668 return $default; 669 } 670 $value =& $value[$key]; 671 } 672 673 return $value; 674 } 675 676 /** 677 * Stores value in an array 678 * 679 * @param string $path path in the array 680 * @param array $array the array 681 * @param mixed $value value to store 682 */ 683 public static function arrayWrite(string $path, array &$array, $value): void 684 { 685 $keys = explode('/', $path); 686 $last_key = array_pop($keys); 687 $a =& $array; 688 foreach ($keys as $key) { 689 if (! isset($a[$key])) { 690 $a[$key] = []; 691 } 692 $a =& $a[$key]; 693 } 694 $a[$last_key] = $value; 695 } 696 697 /** 698 * Removes value from an array 699 * 700 * @param string $path path in the array 701 * @param array $array the array 702 */ 703 public static function arrayRemove(string $path, array &$array): void 704 { 705 $keys = explode('/', $path); 706 $keys_last = array_pop($keys); 707 $path = []; 708 $depth = 0; 709 710 $path[0] =& $array; 711 $found = true; 712 // go as deep as required or possible 713 foreach ($keys as $key) { 714 if (! isset($path[$depth][$key])) { 715 $found = false; 716 break; 717 } 718 $depth++; 719 $path[$depth] =& $path[$depth - 1][$key]; 720 } 721 // if element found, remove it 722 if ($found) { 723 unset($path[$depth][$keys_last]); 724 $depth--; 725 } 726 727 // remove empty nested arrays 728 for (; $depth >= 0; $depth--) { 729 if (isset($path[$depth + 1]) && count($path[$depth + 1]) !== 0) { 730 break; 731 } 732 733 unset($path[$depth][$keys[$depth]]); 734 } 735 } 736 737 /** 738 * Returns link to (possibly) external site using defined redirector. 739 * 740 * @param string $url URL where to go. 741 * 742 * @return string URL for a link. 743 */ 744 public static function linkURL(string $url): string 745 { 746 if (! preg_match('#^https?://#', $url)) { 747 return $url; 748 } 749 750 $params = []; 751 $params['url'] = $url; 752 753 $url = Url::getCommon($params); 754 //strip off token and such sensitive information. Just keep url. 755 $arr = parse_url($url); 756 757 if (! is_array($arr)) { 758 $arr = []; 759 } 760 761 parse_str($arr['query'] ?? '', $vars); 762 $query = http_build_query(['url' => $vars['url']]); 763 764 if ($GLOBALS['PMA_Config'] !== null && $GLOBALS['PMA_Config']->get('is_setup')) { 765 $url = '../url.php?' . $query; 766 } else { 767 $url = './url.php?' . $query; 768 } 769 770 return $url; 771 } 772 773 /** 774 * Checks whether domain of URL is an allowed domain or not. 775 * Use only for URLs of external sites. 776 * 777 * @param string $url URL of external site. 778 * 779 * @return bool True: if domain of $url is allowed domain, 780 * False: otherwise. 781 */ 782 public static function isAllowedDomain(string $url): bool 783 { 784 $arr = parse_url($url); 785 786 if (! is_array($arr)) { 787 $arr = []; 788 } 789 790 // We need host to be set 791 if (! isset($arr['host']) || strlen($arr['host']) == 0) { 792 return false; 793 } 794 // We do not want these to be present 795 $blocked = [ 796 'user', 797 'pass', 798 'port', 799 ]; 800 foreach ($blocked as $part) { 801 if (isset($arr[$part]) && strlen((string) $arr[$part]) != 0) { 802 return false; 803 } 804 } 805 $domain = $arr['host']; 806 $domainAllowList = [ 807 /* Include current domain */ 808 $_SERVER['SERVER_NAME'], 809 /* phpMyAdmin domains */ 810 'wiki.phpmyadmin.net', 811 'www.phpmyadmin.net', 812 'phpmyadmin.net', 813 'demo.phpmyadmin.net', 814 'docs.phpmyadmin.net', 815 /* mysql.com domains */ 816 'dev.mysql.com', 817 'bugs.mysql.com', 818 /* mariadb domains */ 819 'mariadb.org', 820 'mariadb.com', 821 /* php.net domains */ 822 'php.net', 823 'www.php.net', 824 /* Github domains*/ 825 'github.com', 826 'www.github.com', 827 /* Percona domains */ 828 'www.percona.com', 829 /* Following are doubtful ones. */ 830 'mysqldatabaseadministration.blogspot.com', 831 ]; 832 833 return in_array($domain, $domainAllowList); 834 } 835 836 /** 837 * Replace some html-unfriendly stuff 838 * 839 * @param string $buffer String to process 840 * 841 * @return string Escaped and cleaned up text suitable for html 842 */ 843 public static function mimeDefaultFunction(string $buffer): string 844 { 845 $buffer = htmlspecialchars($buffer); 846 $buffer = str_replace(' ', ' ', $buffer); 847 848 return (string) preg_replace("@((\015\012)|(\015)|(\012))@", '<br>' . "\n", $buffer); 849 } 850 851 /** 852 * Displays SQL query before executing. 853 * 854 * @param array|string $query_data Array containing queries or query itself 855 */ 856 public static function previewSQL($query_data): void 857 { 858 $retval = '<div class="preview_sql">'; 859 if (empty($query_data)) { 860 $retval .= __('No change'); 861 } elseif (is_array($query_data)) { 862 foreach ($query_data as $query) { 863 $retval .= Html\Generator::formatSql($query); 864 } 865 } else { 866 $retval .= Html\Generator::formatSql($query_data); 867 } 868 $retval .= '</div>'; 869 $response = Response::getInstance(); 870 $response->addJSON('sql_data', $retval); 871 } 872 873 /** 874 * recursively check if variable is empty 875 * 876 * @param mixed $value the variable 877 * 878 * @return bool true if empty 879 */ 880 public static function emptyRecursive($value): bool 881 { 882 $empty = true; 883 if (is_array($value)) { 884 array_walk_recursive( 885 $value, 886 /** 887 * @param mixed $item 888 */ 889 static function ($item) use (&$empty) { 890 $empty = $empty && empty($item); 891 } 892 ); 893 } else { 894 $empty = empty($value); 895 } 896 897 return $empty; 898 } 899 900 /** 901 * Creates some globals from $_POST variables matching a pattern 902 * 903 * @param array $post_patterns The patterns to search for 904 */ 905 public static function setPostAsGlobal(array $post_patterns): void 906 { 907 global $containerBuilder; 908 909 foreach (array_keys($_POST) as $post_key) { 910 foreach ($post_patterns as $one_post_pattern) { 911 if (! preg_match($one_post_pattern, $post_key)) { 912 continue; 913 } 914 915 $GLOBALS[$post_key] = $_POST[$post_key]; 916 $containerBuilder->setParameter($post_key, $GLOBALS[$post_key]); 917 } 918 } 919 } 920 921 public static function setDatabaseAndTableFromRequest(ContainerInterface $containerBuilder): void 922 { 923 global $db, $table, $url_params; 924 925 $databaseFromRequest = $_POST['db'] ?? $_GET['db'] ?? $_REQUEST['db'] ?? null; 926 $tableFromRequest = $_POST['table'] ?? $_GET['table'] ?? $_REQUEST['table'] ?? null; 927 928 $db = self::isValid($databaseFromRequest) ? $databaseFromRequest : ''; 929 $table = self::isValid($tableFromRequest) ? $tableFromRequest : ''; 930 931 $url_params['db'] = $db; 932 $url_params['table'] = $table; 933 $containerBuilder->setParameter('db', $db); 934 $containerBuilder->setParameter('table', $table); 935 $containerBuilder->setParameter('url_params', $url_params); 936 } 937 938 /** 939 * PATH_INFO could be compromised if set, so remove it from PHP_SELF 940 * and provide a clean PHP_SELF here 941 */ 942 public static function cleanupPathInfo(): void 943 { 944 global $PMA_PHP_SELF; 945 946 $PMA_PHP_SELF = self::getenv('PHP_SELF'); 947 if (empty($PMA_PHP_SELF)) { 948 $PMA_PHP_SELF = urldecode(self::getenv('REQUEST_URI')); 949 } 950 $_PATH_INFO = self::getenv('PATH_INFO'); 951 if (! empty($_PATH_INFO) && ! empty($PMA_PHP_SELF)) { 952 $question_pos = mb_strpos($PMA_PHP_SELF, '?'); 953 if ($question_pos != false) { 954 $PMA_PHP_SELF = mb_substr($PMA_PHP_SELF, 0, $question_pos); 955 } 956 $path_info_pos = mb_strrpos($PMA_PHP_SELF, $_PATH_INFO); 957 if ($path_info_pos !== false) { 958 $path_info_part = mb_substr($PMA_PHP_SELF, $path_info_pos, mb_strlen($_PATH_INFO)); 959 if ($path_info_part == $_PATH_INFO) { 960 $PMA_PHP_SELF = mb_substr($PMA_PHP_SELF, 0, $path_info_pos); 961 } 962 } 963 } 964 965 $path = []; 966 foreach (explode('/', $PMA_PHP_SELF) as $part) { 967 // ignore parts that have no value 968 if (empty($part) || $part === '.') { 969 continue; 970 } 971 972 if ($part !== '..') { 973 // cool, we found a new part 974 $path[] = $part; 975 } elseif (count($path) > 0) { 976 // going back up? sure 977 array_pop($path); 978 } 979 // Here we intentionall ignore case where we go too up 980 // as there is nothing sane to do 981 } 982 983 $PMA_PHP_SELF = htmlspecialchars('/' . implode('/', $path)); 984 } 985 986 /** 987 * Checks that required PHP extensions are there. 988 */ 989 public static function checkExtensions(): void 990 { 991 /** 992 * Warning about mbstring. 993 */ 994 if (! function_exists('mb_detect_encoding')) { 995 self::warnMissingExtension('mbstring'); 996 } 997 998 /** 999 * We really need this one! 1000 */ 1001 if (! function_exists('preg_replace')) { 1002 self::warnMissingExtension('pcre', true); 1003 } 1004 1005 /** 1006 * JSON is required in several places. 1007 */ 1008 if (! function_exists('json_encode')) { 1009 self::warnMissingExtension('json', true); 1010 } 1011 1012 /** 1013 * ctype is required for Twig. 1014 */ 1015 if (! function_exists('ctype_alpha')) { 1016 self::warnMissingExtension('ctype', true); 1017 } 1018 1019 /** 1020 * hash is required for cookie authentication. 1021 */ 1022 if (function_exists('hash_hmac')) { 1023 return; 1024 } 1025 1026 self::warnMissingExtension('hash', true); 1027 } 1028 1029 /** 1030 * Gets the "true" IP address of the current user 1031 * 1032 * @return string|bool the ip of the user 1033 * 1034 * @access private 1035 */ 1036 public static function getIp() 1037 { 1038 /* Get the address of user */ 1039 if (empty($_SERVER['REMOTE_ADDR'])) { 1040 /* We do not know remote IP */ 1041 return false; 1042 } 1043 1044 $direct_ip = $_SERVER['REMOTE_ADDR']; 1045 1046 /* Do we trust this IP as a proxy? If yes we will use it's header. */ 1047 if (! isset($GLOBALS['cfg']['TrustedProxies'][$direct_ip])) { 1048 /* Return true IP */ 1049 return $direct_ip; 1050 } 1051 1052 /** 1053 * Parse header in form: 1054 * X-Forwarded-For: client, proxy1, proxy2 1055 */ 1056 // Get header content 1057 $value = self::getenv($GLOBALS['cfg']['TrustedProxies'][$direct_ip]); 1058 // Grab first element what is client adddress 1059 $value = explode(',', $value)[0]; 1060 // checks that the header contains only one IP address, 1061 $is_ip = filter_var($value, FILTER_VALIDATE_IP); 1062 1063 if ($is_ip !== false) { 1064 // True IP behind a proxy 1065 return $value; 1066 } 1067 1068 // We could not parse header 1069 return false; 1070 } 1071 1072 /** 1073 * Sanitizes MySQL hostname 1074 * 1075 * * strips p: prefix(es) 1076 * 1077 * @param string $name User given hostname 1078 */ 1079 public static function sanitizeMySQLHost(string $name): string 1080 { 1081 while (strtolower(substr($name, 0, 2)) === 'p:') { 1082 $name = substr($name, 2); 1083 } 1084 1085 return $name; 1086 } 1087 1088 /** 1089 * Sanitizes MySQL username 1090 * 1091 * * strips part behind null byte 1092 * 1093 * @param string $name User given username 1094 */ 1095 public static function sanitizeMySQLUser(string $name): string 1096 { 1097 $position = strpos($name, chr(0)); 1098 if ($position !== false) { 1099 return substr($name, 0, $position); 1100 } 1101 1102 return $name; 1103 } 1104 1105 /** 1106 * Safe unserializer wrapper 1107 * 1108 * It does not unserialize data containing objects 1109 * 1110 * @param string $data Data to unserialize 1111 * 1112 * @return mixed|null 1113 */ 1114 public static function safeUnserialize(string $data) 1115 { 1116 if (! is_string($data)) { 1117 return null; 1118 } 1119 1120 /* validate serialized data */ 1121 $length = strlen($data); 1122 $depth = 0; 1123 for ($i = 0; $i < $length; $i++) { 1124 $value = $data[$i]; 1125 1126 switch ($value) { 1127 case '}': 1128 /* end of array */ 1129 if ($depth <= 0) { 1130 return null; 1131 } 1132 $depth--; 1133 break; 1134 case 's': 1135 /* string */ 1136 // parse sting length 1137 $strlen = intval(substr($data, $i + 2)); 1138 // string start 1139 $i = strpos($data, ':', $i + 2); 1140 if ($i === false) { 1141 return null; 1142 } 1143 // skip string, quotes and ; 1144 $i += 2 + $strlen + 1; 1145 if ($data[$i] !== ';') { 1146 return null; 1147 } 1148 break; 1149 1150 case 'b': 1151 case 'i': 1152 case 'd': 1153 /* bool, integer or double */ 1154 // skip value to separator 1155 $i = strpos($data, ';', $i); 1156 if ($i === false) { 1157 return null; 1158 } 1159 break; 1160 case 'a': 1161 /* array */ 1162 // find array start 1163 $i = strpos($data, '{', $i); 1164 if ($i === false) { 1165 return null; 1166 } 1167 // remember nesting 1168 $depth++; 1169 break; 1170 case 'N': 1171 /* null */ 1172 // skip to end 1173 $i = strpos($data, ';', $i); 1174 if ($i === false) { 1175 return null; 1176 } 1177 break; 1178 default: 1179 /* any other elements are not wanted */ 1180 return null; 1181 } 1182 } 1183 1184 // check unterminated arrays 1185 if ($depth > 0) { 1186 return null; 1187 } 1188 1189 return unserialize($data); 1190 } 1191 1192 /** 1193 * Applies changes to PHP configuration. 1194 */ 1195 public static function configure(): void 1196 { 1197 /** 1198 * Set utf-8 encoding for PHP 1199 */ 1200 ini_set('default_charset', 'utf-8'); 1201 mb_internal_encoding('utf-8'); 1202 1203 /** 1204 * Set precision to sane value, with higher values 1205 * things behave slightly unexpectedly, for example 1206 * round(1.2, 2) returns 1.199999999999999956. 1207 */ 1208 ini_set('precision', '14'); 1209 1210 /** 1211 * check timezone setting 1212 * this could produce an E_WARNING - but only once, 1213 * if not done here it will produce E_WARNING on every date/time function 1214 */ 1215 date_default_timezone_set(@date_default_timezone_get()); 1216 } 1217 1218 /** 1219 * Check whether PHP configuration matches our needs. 1220 */ 1221 public static function checkConfiguration(): void 1222 { 1223 /** 1224 * As we try to handle charsets by ourself, mbstring overloads just 1225 * break it, see bug 1063821. 1226 * 1227 * We specifically use empty here as we are looking for anything else than 1228 * empty value or 0. 1229 */ 1230 if (extension_loaded('mbstring') && ! empty(ini_get('mbstring.func_overload'))) { 1231 self::fatalError( 1232 __( 1233 'You have enabled mbstring.func_overload in your PHP ' 1234 . 'configuration. This option is incompatible with phpMyAdmin ' 1235 . 'and might cause some data to be corrupted!' 1236 ) 1237 ); 1238 } 1239 1240 /** 1241 * The ini_set and ini_get functions can be disabled using 1242 * disable_functions but we're relying quite a lot of them. 1243 */ 1244 if (function_exists('ini_get') && function_exists('ini_set')) { 1245 return; 1246 } 1247 1248 self::fatalError( 1249 __( 1250 'The ini_get and/or ini_set functions are disabled in php.ini. ' 1251 . 'phpMyAdmin requires these functions!' 1252 ) 1253 ); 1254 } 1255 1256 /** 1257 * Checks request and fails with fatal error if something problematic is found 1258 */ 1259 public static function checkRequest(): void 1260 { 1261 if (isset($_REQUEST['GLOBALS']) || isset($_FILES['GLOBALS'])) { 1262 self::fatalError(__('GLOBALS overwrite attempt')); 1263 } 1264 1265 /** 1266 * protect against possible exploits - there is no need to have so much variables 1267 */ 1268 if (count($_REQUEST) <= 1000) { 1269 return; 1270 } 1271 1272 self::fatalError(__('possible exploit')); 1273 } 1274 1275 /** 1276 * Sign the sql query using hmac using the session token 1277 * 1278 * @param string $sqlQuery The sql query 1279 * 1280 * @return string 1281 */ 1282 public static function signSqlQuery($sqlQuery) 1283 { 1284 global $cfg; 1285 1286 $secret = $_SESSION[' HMAC_secret '] ?? ''; 1287 1288 return hash_hmac('sha256', $sqlQuery, $secret . $cfg['blowfish_secret']); 1289 } 1290 1291 /** 1292 * Check that the sql query has a valid hmac signature 1293 * 1294 * @param string $sqlQuery The sql query 1295 * @param string $signature The Signature to check 1296 * 1297 * @return bool 1298 */ 1299 public static function checkSqlQuerySignature($sqlQuery, $signature) 1300 { 1301 global $cfg; 1302 1303 $secret = $_SESSION[' HMAC_secret '] ?? ''; 1304 $hmac = hash_hmac('sha256', $sqlQuery, $secret . $cfg['blowfish_secret']); 1305 1306 return hash_equals($hmac, $signature); 1307 } 1308 1309 /** 1310 * Check whether user supplied token is valid, if not remove any possibly 1311 * dangerous stuff from request. 1312 * 1313 * Check for token mismatch only if the Request method is POST. 1314 * GET Requests would never have token and therefore checking 1315 * mis-match does not make sense. 1316 */ 1317 public static function checkTokenRequestParam(): void 1318 { 1319 global $token_mismatch, $token_provided; 1320 1321 $token_mismatch = true; 1322 $token_provided = false; 1323 1324 if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { 1325 return; 1326 } 1327 1328 if (self::isValid($_POST['token'])) { 1329 $token_provided = true; 1330 $token_mismatch = ! @hash_equals($_SESSION[' PMA_token '], $_POST['token']); 1331 } 1332 1333 if (! $token_mismatch) { 1334 return; 1335 } 1336 1337 // Warn in case the mismatch is result of failed setting of session cookie 1338 if (isset($_POST['set_session']) && $_POST['set_session'] !== session_id()) { 1339 trigger_error( 1340 __( 1341 'Failed to set session cookie. Maybe you are using ' 1342 . 'HTTP instead of HTTPS to access phpMyAdmin.' 1343 ), 1344 E_USER_ERROR 1345 ); 1346 } 1347 1348 /** 1349 * We don't allow any POST operation parameters if the token is mismatched 1350 * or is not provided. 1351 */ 1352 $allowList = ['ajax_request']; 1353 Sanitize::removeRequestVars($allowList); 1354 } 1355 1356 public static function setGotoAndBackGlobals(ContainerInterface $container, Config $config): void 1357 { 1358 global $goto, $back, $url_params; 1359 1360 // Holds page that should be displayed. 1361 $goto = ''; 1362 $container->setParameter('goto', $goto); 1363 1364 if (isset($_REQUEST['goto']) && self::checkPageValidity($_REQUEST['goto'])) { 1365 $goto = $_REQUEST['goto']; 1366 $url_params['goto'] = $goto; 1367 $container->setParameter('goto', $goto); 1368 $container->setParameter('url_params', $url_params); 1369 } else { 1370 if ($config->issetCookie('goto')) { 1371 $config->removeCookie('goto'); 1372 } 1373 unset($_REQUEST['goto'], $_GET['goto'], $_POST['goto']); 1374 } 1375 1376 if (isset($_REQUEST['back']) && self::checkPageValidity($_REQUEST['back'])) { 1377 // Returning page. 1378 $back = $_REQUEST['back']; 1379 $container->setParameter('back', $back); 1380 } else { 1381 if ($config->issetCookie('back')) { 1382 $config->removeCookie('back'); 1383 } 1384 unset($_REQUEST['back'], $_GET['back'], $_POST['back']); 1385 } 1386 } 1387 1388 public static function connectToDatabaseServer(DatabaseInterface $dbi, AuthenticationPlugin $auth): void 1389 { 1390 global $cfg; 1391 1392 /** 1393 * Try to connect MySQL with the control user profile (will be used to get the privileges list for the current 1394 * user but the true user link must be open after this one so it would be default one for all the scripts). 1395 */ 1396 $controlLink = false; 1397 if ($cfg['Server']['controluser'] !== '') { 1398 $controlLink = $dbi->connect(DatabaseInterface::CONNECT_CONTROL); 1399 } 1400 1401 // Connects to the server (validates user's login) 1402 $userLink = $dbi->connect(DatabaseInterface::CONNECT_USER); 1403 1404 if ($userLink === false) { 1405 $auth->showFailure('mysql-denied'); 1406 } 1407 1408 if ($controlLink) { 1409 return; 1410 } 1411 1412 /** 1413 * Open separate connection for control queries, this is needed to avoid problems with table locking used in 1414 * main connection and phpMyAdmin issuing queries to configuration storage, which is not locked by that time. 1415 */ 1416 $dbi->connect(DatabaseInterface::CONNECT_USER, null, DatabaseInterface::CONNECT_CONTROL); 1417 } 1418 1419 /** 1420 * Get the container builder 1421 */ 1422 public static function getContainerBuilder(): ContainerBuilder 1423 { 1424 $containerBuilder = new ContainerBuilder(); 1425 $loader = new PhpFileLoader($containerBuilder, new FileLocator(ROOT_PATH . 'libraries')); 1426 $loader->load('services_loader.php'); 1427 1428 return $containerBuilder; 1429 } 1430 1431 /** 1432 * @return void 1433 */ 1434 public static function populateRequestWithEncryptedQueryParams() 1435 { 1436 if ( 1437 (! isset($_GET['eq']) || ! is_string($_GET['eq'])) 1438 && (! isset($_POST['eq']) || ! is_string($_POST['eq'])) 1439 ) { 1440 unset($_GET['eq'], $_POST['eq'], $_REQUEST['eq']); 1441 1442 return; 1443 } 1444 1445 $isFromPost = isset($_POST['eq']); 1446 $decryptedQuery = Url::decryptQuery($isFromPost ? $_POST['eq'] : $_GET['eq']); 1447 unset($_GET['eq'], $_POST['eq'], $_REQUEST['eq']); 1448 if ($decryptedQuery === null) { 1449 return; 1450 } 1451 1452 $urlQueryParams = (array) json_decode($decryptedQuery); 1453 foreach ($urlQueryParams as $urlQueryParamKey => $urlQueryParamValue) { 1454 if ($isFromPost) { 1455 $_POST[$urlQueryParamKey] = $urlQueryParamValue; 1456 } else { 1457 $_GET[$urlQueryParamKey] = $urlQueryParamValue; 1458 } 1459 1460 $_REQUEST[$urlQueryParamKey] = $urlQueryParamValue; 1461 } 1462 } 1463} 1464