1<?php 2/** 3 * Configuration handling. 4 */ 5 6declare(strict_types=1); 7 8namespace PhpMyAdmin; 9 10use const DIRECTORY_SEPARATOR; 11use const E_USER_ERROR; 12use const PHP_OS; 13use const PHP_URL_PATH; 14use const PHP_URL_SCHEME; 15use const PHP_VERSION_ID; 16use function array_filter; 17use function array_flip; 18use function array_intersect_key; 19use function array_keys; 20use function array_merge; 21use function array_replace_recursive; 22use function array_slice; 23use function count; 24use function define; 25use function defined; 26use function error_get_last; 27use function error_reporting; 28use function explode; 29use function fclose; 30use function file_exists; 31use function filemtime; 32use function fileperms; 33use function fopen; 34use function fread; 35use function function_exists; 36use function gd_info; 37use function implode; 38use function ini_get; 39use function intval; 40use function is_dir; 41use function is_int; 42use function is_numeric; 43use function is_readable; 44use function is_string; 45use function is_writable; 46use function max; 47use function mb_strstr; 48use function mb_strtolower; 49use function md5; 50use function min; 51use function mkdir; 52use function ob_end_clean; 53use function ob_get_clean; 54use function ob_start; 55use function parse_url; 56use function preg_match; 57use function realpath; 58use function rtrim; 59use function setcookie; 60use function sprintf; 61use function str_replace; 62use function stripos; 63use function strlen; 64use function strpos; 65use function strtolower; 66use function substr; 67use function sys_get_temp_dir; 68use function time; 69use function trigger_error; 70use function trim; 71use function crc32; 72 73/** 74 * Configuration class 75 */ 76class Config 77{ 78 /** @var string default config source */ 79 public $defaultSource = ROOT_PATH . 'libraries/config.default.php'; 80 81 /** @var array default configuration settings */ 82 public $default = []; 83 84 /** @var array configuration settings, without user preferences applied */ 85 public $baseSettings = []; 86 87 /** @var array configuration settings */ 88 public $settings = []; 89 90 /** @var string config source */ 91 public $source = ''; 92 93 /** @var int source modification time */ 94 public $sourceMtime = 0; 95 96 /** @var int */ 97 public $defaultSourceMtime = 0; 98 99 /** @var int */ 100 public $setMtime = 0; 101 102 /** @var bool */ 103 public $errorConfigFile = false; 104 105 /** @var bool */ 106 public $errorConfigDefaultFile = false; 107 108 /** @var array */ 109 public $defaultServer = []; 110 111 /** 112 * @var bool whether init is done or not 113 * set this to false to force some initial checks 114 * like checking for required functions 115 */ 116 public $done = false; 117 118 /** 119 * @param string $source source to read config from 120 */ 121 public function __construct(?string $source = null) 122 { 123 $this->settings = ['is_setup' => false]; 124 125 // functions need to refresh in case of config file changed goes in 126 // PhpMyAdmin\Config::load() 127 $this->load($source); 128 129 // other settings, independent from config file, comes in 130 $this->checkSystem(); 131 132 $this->baseSettings = $this->settings; 133 } 134 135 /** 136 * sets system and application settings 137 */ 138 public function checkSystem(): void 139 { 140 // All the version handling is now done in the Version class 141 $this->set('PMA_VERSION', Version::VERSION); 142 $this->set('PMA_MAJOR_VERSION', Version::SERIES); 143 144 $this->checkWebServerOs(); 145 $this->checkWebServer(); 146 $this->checkGd2(); 147 $this->checkClient(); 148 $this->checkUpload(); 149 $this->checkUploadSize(); 150 $this->checkOutputCompression(); 151 } 152 153 /** 154 * whether to use gzip output compression or not 155 */ 156 public function checkOutputCompression(): void 157 { 158 // If zlib output compression is set in the php configuration file, no 159 // output buffering should be run 160 if (ini_get('zlib.output_compression')) { 161 $this->set('OBGzip', false); 162 } 163 164 // enable output-buffering (if set to 'auto') 165 if (strtolower((string) $this->get('OBGzip')) !== 'auto') { 166 return; 167 } 168 169 $this->set('OBGzip', true); 170 } 171 172 /** 173 * Sets the client platform based on user agent 174 * 175 * @param string $user_agent the user agent 176 */ 177 private function setClientPlatform(string $user_agent): void 178 { 179 if (mb_strstr($user_agent, 'Win')) { 180 $this->set('PMA_USR_OS', 'Win'); 181 } elseif (mb_strstr($user_agent, 'Mac')) { 182 $this->set('PMA_USR_OS', 'Mac'); 183 } elseif (mb_strstr($user_agent, 'Linux')) { 184 $this->set('PMA_USR_OS', 'Linux'); 185 } elseif (mb_strstr($user_agent, 'Unix')) { 186 $this->set('PMA_USR_OS', 'Unix'); 187 } elseif (mb_strstr($user_agent, 'OS/2')) { 188 $this->set('PMA_USR_OS', 'OS/2'); 189 } else { 190 $this->set('PMA_USR_OS', 'Other'); 191 } 192 } 193 194 /** 195 * Determines platform (OS), browser and version of the user 196 * Based on a phpBuilder article: 197 * 198 * @see http://www.phpbuilder.net/columns/tim20000821.php 199 */ 200 public function checkClient(): void 201 { 202 if (Core::getenv('HTTP_USER_AGENT')) { 203 $HTTP_USER_AGENT = Core::getenv('HTTP_USER_AGENT'); 204 } else { 205 $HTTP_USER_AGENT = ''; 206 } 207 208 // 1. Platform 209 $this->setClientPlatform($HTTP_USER_AGENT); 210 211 // 2. browser and version 212 // (must check everything else before Mozilla) 213 214 $is_mozilla = preg_match( 215 '@Mozilla/([0-9]\.[0-9]{1,2})@', 216 $HTTP_USER_AGENT, 217 $mozilla_version 218 ); 219 220 if (preg_match( 221 '@Opera(/| )([0-9]\.[0-9]{1,2})@', 222 $HTTP_USER_AGENT, 223 $log_version 224 )) { 225 $this->set('PMA_USR_BROWSER_VER', $log_version[2]); 226 $this->set('PMA_USR_BROWSER_AGENT', 'OPERA'); 227 } elseif (preg_match( 228 '@(MS)?IE ([0-9]{1,2}\.[0-9]{1,2})@', 229 $HTTP_USER_AGENT, 230 $log_version 231 )) { 232 $this->set('PMA_USR_BROWSER_VER', $log_version[2]); 233 $this->set('PMA_USR_BROWSER_AGENT', 'IE'); 234 } elseif (preg_match( 235 '@Trident/(7)\.0@', 236 $HTTP_USER_AGENT, 237 $log_version 238 )) { 239 $this->set('PMA_USR_BROWSER_VER', intval($log_version[1]) + 4); 240 $this->set('PMA_USR_BROWSER_AGENT', 'IE'); 241 } elseif (preg_match( 242 '@OmniWeb/([0-9]{1,3})@', 243 $HTTP_USER_AGENT, 244 $log_version 245 )) { 246 $this->set('PMA_USR_BROWSER_VER', $log_version[1]); 247 $this->set('PMA_USR_BROWSER_AGENT', 'OMNIWEB'); 248 // Konqueror 2.2.2 says Konqueror/2.2.2 249 // Konqueror 3.0.3 says Konqueror/3 250 } elseif (preg_match( 251 '@(Konqueror/)(.*)(;)@', 252 $HTTP_USER_AGENT, 253 $log_version 254 )) { 255 $this->set('PMA_USR_BROWSER_VER', $log_version[2]); 256 $this->set('PMA_USR_BROWSER_AGENT', 'KONQUEROR'); 257 // must check Chrome before Safari 258 } elseif ($is_mozilla 259 && preg_match('@Chrome/([0-9.]*)@', $HTTP_USER_AGENT, $log_version) 260 ) { 261 $this->set('PMA_USR_BROWSER_VER', $log_version[1]); 262 $this->set('PMA_USR_BROWSER_AGENT', 'CHROME'); 263 // newer Safari 264 } elseif ($is_mozilla 265 && preg_match('@Version/(.*) Safari@', $HTTP_USER_AGENT, $log_version) 266 ) { 267 $this->set( 268 'PMA_USR_BROWSER_VER', 269 $log_version[1] 270 ); 271 $this->set('PMA_USR_BROWSER_AGENT', 'SAFARI'); 272 // older Safari 273 } elseif ($is_mozilla 274 && preg_match('@Safari/([0-9]*)@', $HTTP_USER_AGENT, $log_version) 275 ) { 276 $this->set( 277 'PMA_USR_BROWSER_VER', 278 $mozilla_version[1] . '.' . $log_version[1] 279 ); 280 $this->set('PMA_USR_BROWSER_AGENT', 'SAFARI'); 281 // Firefox 282 } elseif (! mb_strstr($HTTP_USER_AGENT, 'compatible') 283 && preg_match('@Firefox/([\w.]+)@', $HTTP_USER_AGENT, $log_version) 284 ) { 285 $this->set( 286 'PMA_USR_BROWSER_VER', 287 $log_version[1] 288 ); 289 $this->set('PMA_USR_BROWSER_AGENT', 'FIREFOX'); 290 } elseif (preg_match('@rv:1\.9(.*)Gecko@', $HTTP_USER_AGENT)) { 291 $this->set('PMA_USR_BROWSER_VER', '1.9'); 292 $this->set('PMA_USR_BROWSER_AGENT', 'GECKO'); 293 } elseif ($is_mozilla) { 294 $this->set('PMA_USR_BROWSER_VER', $mozilla_version[1]); 295 $this->set('PMA_USR_BROWSER_AGENT', 'MOZILLA'); 296 } else { 297 $this->set('PMA_USR_BROWSER_VER', 0); 298 $this->set('PMA_USR_BROWSER_AGENT', 'OTHER'); 299 } 300 } 301 302 /** 303 * Whether GD2 is present 304 */ 305 public function checkGd2(): void 306 { 307 if ($this->get('GD2Available') === 'yes') { 308 $this->set('PMA_IS_GD2', 1); 309 310 return; 311 } 312 313 if ($this->get('GD2Available') === 'no') { 314 $this->set('PMA_IS_GD2', 0); 315 316 return; 317 } 318 319 if (! function_exists('imagecreatetruecolor')) { 320 $this->set('PMA_IS_GD2', 0); 321 322 return; 323 } 324 325 if (function_exists('gd_info')) { 326 $gd_nfo = gd_info(); 327 if (mb_strstr($gd_nfo['GD Version'], '2.')) { 328 $this->set('PMA_IS_GD2', 1); 329 } else { 330 $this->set('PMA_IS_GD2', 0); 331 } 332 } else { 333 $this->set('PMA_IS_GD2', 0); 334 } 335 } 336 337 /** 338 * Whether the Web server php is running on is IIS 339 */ 340 public function checkWebServer(): void 341 { 342 // some versions return Microsoft-IIS, some Microsoft/IIS 343 // we could use a preg_match() but it's slower 344 if (Core::getenv('SERVER_SOFTWARE') 345 && stripos(Core::getenv('SERVER_SOFTWARE'), 'Microsoft') !== false 346 && stripos(Core::getenv('SERVER_SOFTWARE'), 'IIS') !== false 347 ) { 348 $this->set('PMA_IS_IIS', 1); 349 } else { 350 $this->set('PMA_IS_IIS', 0); 351 } 352 } 353 354 /** 355 * Whether the os php is running on is windows or not 356 */ 357 public function checkWebServerOs(): void 358 { 359 // Default to Unix or Equiv 360 $this->set('PMA_IS_WINDOWS', false); 361 // If PHP_OS is defined then continue 362 if (! defined('PHP_OS')) { 363 return; 364 } 365 366 if (stripos(PHP_OS, 'win') !== false && stripos(PHP_OS, 'darwin') === false) { 367 // Is it some version of Windows 368 $this->set('PMA_IS_WINDOWS', true); 369 } elseif (stripos(PHP_OS, 'OS/2') !== false) { 370 // Is it OS/2 (No file permissions like Windows) 371 $this->set('PMA_IS_WINDOWS', true); 372 } 373 } 374 375 /** 376 * loads default values from default source 377 * 378 * @return bool success 379 */ 380 public function loadDefaults(): bool 381 { 382 global $isConfigLoading; 383 384 /** @var array<string,mixed> $cfg */ 385 $cfg = []; 386 if (! @file_exists($this->defaultSource)) { 387 $this->errorConfigDefaultFile = true; 388 389 return false; 390 } 391 $canUseErrorReporting = Util::isErrorReportingAvailable(); 392 $oldErrorReporting = null; 393 if ($canUseErrorReporting) { 394 $oldErrorReporting = error_reporting(0); 395 } 396 397 ob_start(); 398 $isConfigLoading = true; 399 $eval_result = include $this->defaultSource; 400 $isConfigLoading = false; 401 ob_end_clean(); 402 403 if ($canUseErrorReporting) { 404 error_reporting($oldErrorReporting); 405 } 406 407 if ($eval_result === false) { 408 $this->errorConfigDefaultFile = true; 409 410 return false; 411 } 412 413 $this->defaultSourceMtime = filemtime($this->defaultSource); 414 415 $this->defaultServer = $cfg['Servers'][1]; 416 unset($cfg['Servers']); 417 418 $this->default = $cfg; 419 $this->settings = array_replace_recursive($this->settings, $cfg); 420 421 $this->errorConfigDefaultFile = false; 422 423 return true; 424 } 425 426 /** 427 * loads configuration from $source, usually the config file 428 * should be called on object creation 429 * 430 * @param string $source config file 431 */ 432 public function load(?string $source = null): bool 433 { 434 global $isConfigLoading; 435 436 $this->loadDefaults(); 437 438 if ($source !== null) { 439 $this->setSource($source); 440 } 441 442 if (! $this->checkConfigSource()) { 443 return false; 444 } 445 446 $cfg = []; 447 448 /** 449 * Parses the configuration file, we throw away any errors or 450 * output. 451 */ 452 $canUseErrorReporting = Util::isErrorReportingAvailable(); 453 $oldErrorReporting = null; 454 if ($canUseErrorReporting) { 455 $oldErrorReporting = error_reporting(0); 456 } 457 458 ob_start(); 459 $isConfigLoading = true; 460 $eval_result = include $this->getSource(); 461 $isConfigLoading = false; 462 ob_end_clean(); 463 464 if ($canUseErrorReporting) { 465 error_reporting($oldErrorReporting); 466 } 467 468 if ($eval_result === false) { 469 $this->errorConfigFile = true; 470 } else { 471 $this->errorConfigFile = false; 472 $this->sourceMtime = filemtime($this->getSource()); 473 } 474 475 /** 476 * Ignore keys with / as we do not use these 477 * 478 * These can be confusing for user configuration layer as it 479 * flatten array using / and thus don't see difference between 480 * $cfg['Export/method'] and $cfg['Export']['method'], while rest 481 * of the code uses the setting only in latter form. 482 * 483 * This could be removed once we consistently handle both values 484 * in the functional code as well. 485 * 486 * It could use array_filter(...ARRAY_FILTER_USE_KEY), but it's not 487 * supported on PHP 5.5 and HHVM. 488 */ 489 $matched_keys = array_filter( 490 array_keys($cfg), 491 static function ($key) { 492 return strpos($key, '/') === false; 493 } 494 ); 495 496 $cfg = array_intersect_key($cfg, array_flip($matched_keys)); 497 498 $this->settings = array_replace_recursive($this->settings, $cfg); 499 500 return true; 501 } 502 503 /** 504 * Sets the connection collation 505 */ 506 private function setConnectionCollation(): void 507 { 508 global $dbi; 509 510 $collation_connection = $this->get('DefaultConnectionCollation'); 511 if (empty($collation_connection) 512 || $collation_connection == $GLOBALS['collation_connection'] 513 ) { 514 return; 515 } 516 517 $dbi->setCollation($collation_connection); 518 } 519 520 /** 521 * Loads user preferences and merges them with current config 522 * must be called after control connection has been established 523 */ 524 public function loadUserPreferences(): void 525 { 526 $userPreferences = new UserPreferences(); 527 // index.php should load these settings, so that phpmyadmin.css.php 528 // will have everything available in session cache 529 $server = $GLOBALS['server'] ?? (! empty($GLOBALS['cfg']['ServerDefault']) 530 ? $GLOBALS['cfg']['ServerDefault'] 531 : 0); 532 $cache_key = 'server_' . $server; 533 if ($server > 0 && ! defined('PMA_MINIMUM_COMMON')) { 534 $config_mtime = max($this->defaultSourceMtime, $this->sourceMtime); 535 // cache user preferences, use database only when needed 536 if (! isset($_SESSION['cache'][$cache_key]['userprefs']) 537 || $_SESSION['cache'][$cache_key]['config_mtime'] < $config_mtime 538 ) { 539 $prefs = $userPreferences->load(); 540 $_SESSION['cache'][$cache_key]['userprefs'] 541 = $userPreferences->apply($prefs['config_data']); 542 $_SESSION['cache'][$cache_key]['userprefs_mtime'] = $prefs['mtime']; 543 $_SESSION['cache'][$cache_key]['userprefs_type'] = $prefs['type']; 544 $_SESSION['cache'][$cache_key]['config_mtime'] = $config_mtime; 545 } 546 } elseif ($server == 0 547 || ! isset($_SESSION['cache'][$cache_key]['userprefs']) 548 ) { 549 $this->set('user_preferences', false); 550 551 return; 552 } 553 $config_data = $_SESSION['cache'][$cache_key]['userprefs']; 554 // type is 'db' or 'session' 555 $this->set( 556 'user_preferences', 557 $_SESSION['cache'][$cache_key]['userprefs_type'] 558 ); 559 $this->set( 560 'user_preferences_mtime', 561 $_SESSION['cache'][$cache_key]['userprefs_mtime'] 562 ); 563 564 // load config array 565 $this->settings = array_replace_recursive($this->settings, $config_data); 566 $GLOBALS['cfg'] = array_replace_recursive($GLOBALS['cfg'], $config_data); 567 if (defined('PMA_MINIMUM_COMMON')) { 568 return; 569 } 570 571 // settings below start really working on next page load, but 572 // changes are made only in index.php so everything is set when 573 // in frames 574 575 // save theme 576 /** @var ThemeManager $tmanager */ 577 $tmanager = ThemeManager::getInstance(); 578 if ($tmanager->getThemeCookie() || isset($_REQUEST['set_theme'])) { 579 if ((! isset($config_data['ThemeDefault']) 580 && $tmanager->theme->getId() !== 'original') 581 || isset($config_data['ThemeDefault']) 582 && $config_data['ThemeDefault'] != $tmanager->theme->getId() 583 ) { 584 // new theme was set in common.inc.php 585 $this->setUserValue( 586 null, 587 'ThemeDefault', 588 $tmanager->theme->getId(), 589 'original' 590 ); 591 } 592 } else { 593 // no cookie - read default from settings 594 if ($tmanager->theme !== null 595 && $this->settings['ThemeDefault'] != $tmanager->theme->getId() 596 && $tmanager->checkTheme($this->settings['ThemeDefault']) 597 ) { 598 $tmanager->setActiveTheme($this->settings['ThemeDefault']); 599 $tmanager->setThemeCookie(); 600 } 601 } 602 603 // save language 604 if ($this->issetCookie('pma_lang') || isset($_POST['lang'])) { 605 if ((! isset($config_data['lang']) 606 && $GLOBALS['lang'] !== 'en') 607 || isset($config_data['lang']) 608 && $GLOBALS['lang'] != $config_data['lang'] 609 ) { 610 $this->setUserValue(null, 'lang', $GLOBALS['lang'], 'en'); 611 } 612 } else { 613 // read language from settings 614 if (isset($config_data['lang'])) { 615 $language = LanguageManager::getInstance()->getLanguage( 616 $config_data['lang'] 617 ); 618 if ($language !== false) { 619 $language->activate(); 620 $this->setCookie('pma_lang', $language->getCode()); 621 } 622 } 623 } 624 625 // set connection collation 626 $this->setConnectionCollation(); 627 } 628 629 /** 630 * Sets config value which is stored in user preferences (if available) 631 * or in a cookie. 632 * 633 * If user preferences are not yet initialized, option is applied to 634 * global config and added to a update queue, which is processed 635 * by {@link loadUserPreferences()} 636 * 637 * @param string|null $cookie_name can be null 638 * @param string $cfg_path configuration path 639 * @param string $new_cfg_value new value 640 * @param string|null $default_value default value 641 * 642 * @return true|Message 643 */ 644 public function setUserValue( 645 ?string $cookie_name, 646 string $cfg_path, 647 $new_cfg_value, 648 $default_value = null 649 ) { 650 $userPreferences = new UserPreferences(); 651 $result = true; 652 // use permanent user preferences if possible 653 $prefs_type = $this->get('user_preferences'); 654 if ($prefs_type) { 655 if ($default_value === null) { 656 $default_value = Core::arrayRead($cfg_path, $this->default); 657 } 658 $result = $userPreferences->persistOption($cfg_path, $new_cfg_value, $default_value); 659 } 660 if ($prefs_type !== 'db' && $cookie_name) { 661 // fall back to cookies 662 if ($default_value === null) { 663 $default_value = Core::arrayRead($cfg_path, $this->settings); 664 } 665 $this->setCookie($cookie_name, $new_cfg_value, $default_value); 666 } 667 Core::arrayWrite($cfg_path, $GLOBALS['cfg'], $new_cfg_value); 668 Core::arrayWrite($cfg_path, $this->settings, $new_cfg_value); 669 670 return $result; 671 } 672 673 /** 674 * Reads value stored by {@link setUserValue()} 675 * 676 * @param string $cookie_name cookie name 677 * @param mixed $cfg_value config value 678 * 679 * @return mixed 680 */ 681 public function getUserValue(string $cookie_name, $cfg_value) 682 { 683 $cookie_exists = ! empty($this->getCookie($cookie_name)); 684 $prefs_type = $this->get('user_preferences'); 685 if ($prefs_type === 'db') { 686 // permanent user preferences value exists, remove cookie 687 if ($cookie_exists) { 688 $this->removeCookie($cookie_name); 689 } 690 } elseif ($cookie_exists) { 691 return $this->getCookie($cookie_name); 692 } 693 694 // return value from $cfg array 695 return $cfg_value; 696 } 697 698 /** 699 * set source 700 * 701 * @param string $source source 702 */ 703 public function setSource(string $source): void 704 { 705 $this->source = trim($source); 706 } 707 708 /** 709 * check config source 710 * 711 * @return bool whether source is valid or not 712 */ 713 public function checkConfigSource(): bool 714 { 715 if (! $this->getSource()) { 716 // no configuration file set at all 717 return false; 718 } 719 720 if (! @file_exists($this->getSource())) { 721 $this->sourceMtime = 0; 722 723 return false; 724 } 725 726 if (! @is_readable($this->getSource())) { 727 // manually check if file is readable 728 // might be bug #3059806 Supporting running from CIFS/Samba shares 729 730 $contents = false; 731 $handle = @fopen($this->getSource(), 'r'); 732 if ($handle !== false) { 733 $contents = @fread($handle, 1); // reading 1 byte is enough to test 734 fclose($handle); 735 } 736 if ($contents === false) { 737 $this->sourceMtime = 0; 738 Core::fatalError( 739 sprintf( 740 function_exists('__') 741 ? __('Existing configuration file (%s) is not readable.') 742 : 'Existing configuration file (%s) is not readable.', 743 $this->getSource() 744 ) 745 ); 746 747 return false; 748 } 749 } 750 751 return true; 752 } 753 754 /** 755 * verifies the permissions on config file (if asked by configuration) 756 * (must be called after config.inc.php has been merged) 757 */ 758 public function checkPermissions(): void 759 { 760 // Check for permissions (on platforms that support it): 761 if (! $this->get('CheckConfigurationPermissions') || ! @file_exists($this->getSource())) { 762 return; 763 } 764 765 $perms = @fileperms($this->getSource()); 766 if ($perms === false || (! ($perms & 2))) { 767 return; 768 } 769 770 // This check is normally done after loading configuration 771 $this->checkWebServerOs(); 772 if ($this->get('PMA_IS_WINDOWS') === true) { 773 return; 774 } 775 776 $this->sourceMtime = 0; 777 Core::fatalError( 778 __( 779 'Wrong permissions on configuration file, ' 780 . 'should not be world writable!' 781 ) 782 ); 783 } 784 785 /** 786 * Checks for errors 787 * (must be called after config.inc.php has been merged) 788 */ 789 public function checkErrors(): void 790 { 791 if ($this->errorConfigDefaultFile) { 792 Core::fatalError( 793 sprintf( 794 __('Could not load default configuration from: %1$s'), 795 $this->defaultSource 796 ) 797 ); 798 } 799 800 if (! $this->errorConfigFile) { 801 return; 802 } 803 804 $error = '[strong]' . __('Failed to read configuration file!') . '[/strong]' 805 . '[br][br]' 806 . __( 807 'This usually means there is a syntax error in it, ' 808 . 'please check any errors shown below.' 809 ) 810 . '[br][br]' 811 . '[conferr]'; 812 trigger_error($error, E_USER_ERROR); 813 } 814 815 /** 816 * returns specific config setting 817 * 818 * @param string $setting config setting 819 * 820 * @return mixed|null value 821 */ 822 public function get(string $setting) 823 { 824 if (isset($this->settings[$setting])) { 825 return $this->settings[$setting]; 826 } 827 828 return null; 829 } 830 831 /** 832 * sets configuration variable 833 * 834 * @param string $setting configuration option 835 * @param mixed $value new value for configuration option 836 */ 837 public function set(string $setting, $value): void 838 { 839 if (isset($this->settings[$setting]) 840 && $this->settings[$setting] === $value 841 ) { 842 return; 843 } 844 845 $this->settings[$setting] = $value; 846 $this->setMtime = time(); 847 } 848 849 /** 850 * returns source for current config 851 * 852 * @return string config source 853 */ 854 public function getSource(): string 855 { 856 return $this->source; 857 } 858 859 /** 860 * returns a unique value to force a CSS reload if either the config 861 * or the theme changes 862 * 863 * @return int Summary of unix timestamps, to be unique on theme parameters 864 * change 865 */ 866 public function getThemeUniqueValue(): int 867 { 868 global $PMA_Theme; 869 870 return crc32( 871 $this->sourceMtime . 872 $this->defaultSourceMtime . 873 $this->get('user_preferences_mtime') . 874 ($PMA_Theme->mtimeInfo ?? 0) . 875 ($PMA_Theme->filesizeInfo ?? 0) 876 ); 877 } 878 879 /** 880 * checks if upload is enabled 881 */ 882 public function checkUpload(): void 883 { 884 if (! ini_get('file_uploads')) { 885 $this->set('enable_upload', false); 886 887 return; 888 } 889 890 $this->set('enable_upload', true); 891 // if set "php_admin_value file_uploads Off" in httpd.conf 892 // ini_get() also returns the string "Off" in this case: 893 if (strtolower((string) ini_get('file_uploads')) !== 'off') { 894 return; 895 } 896 897 $this->set('enable_upload', false); 898 } 899 900 /** 901 * Maximum upload size as limited by PHP 902 * Used with permission from Moodle (https://moodle.org/) by Martin Dougiamas 903 * 904 * this section generates $max_upload_size in bytes 905 */ 906 public function checkUploadSize(): void 907 { 908 $fileSize = ini_get('upload_max_filesize'); 909 910 if (! $fileSize) { 911 $fileSize = '5M'; 912 } 913 914 $size = Core::getRealSize($fileSize); 915 $postSize = ini_get('post_max_size'); 916 917 if ($postSize) { 918 $size = min($size, Core::getRealSize($postSize)); 919 } 920 921 $this->set('max_upload_size', $size); 922 } 923 924 /** 925 * Checks if protocol is https 926 * 927 * This function checks if the https protocol on the active connection. 928 */ 929 public function isHttps(): bool 930 { 931 if ($this->get('is_https') !== null) { 932 return $this->get('is_https'); 933 } 934 935 $url = $this->get('PmaAbsoluteUri'); 936 937 $is_https = false; 938 if (! empty($url) && parse_url($url, PHP_URL_SCHEME) === 'https') { 939 $is_https = true; 940 } elseif (strtolower(Core::getenv('HTTP_SCHEME')) === 'https') { 941 $is_https = true; 942 } elseif (strtolower(Core::getenv('HTTPS')) === 'on') { 943 $is_https = true; 944 } elseif (strtolower(substr(Core::getenv('REQUEST_URI'), 0, 6)) === 'https:') { 945 $is_https = true; 946 } elseif (strtolower(Core::getenv('HTTP_HTTPS_FROM_LB')) === 'on') { 947 // A10 Networks load balancer 948 $is_https = true; 949 } elseif (strtolower(Core::getenv('HTTP_FRONT_END_HTTPS')) === 'on') { 950 $is_https = true; 951 } elseif (strtolower(Core::getenv('HTTP_X_FORWARDED_PROTO')) === 'https') { 952 $is_https = true; 953 } elseif (strtolower(Core::getenv('HTTP_CLOUDFRONT_FORWARDED_PROTO')) === 'https') { 954 // Amazon CloudFront, issue #15621 955 $is_https = true; 956 } elseif (Util::getProtoFromForwardedHeader(Core::getenv('HTTP_FORWARDED')) === 'https') { 957 // RFC 7239 Forwarded header 958 $is_https = true; 959 } elseif (Core::getenv('SERVER_PORT') == 443) { 960 $is_https = true; 961 } 962 963 $this->set('is_https', $is_https); 964 965 return $is_https; 966 } 967 968 /** 969 * Get phpMyAdmin root path 970 */ 971 public function getRootPath(): string 972 { 973 static $cookie_path = null; 974 975 if ($cookie_path !== null && ! defined('TESTSUITE')) { 976 return $cookie_path; 977 } 978 979 $url = $this->get('PmaAbsoluteUri'); 980 981 if (! empty($url)) { 982 $path = parse_url($url, PHP_URL_PATH); 983 if (! empty($path)) { 984 if (substr($path, -1) !== '/') { 985 return $path . '/'; 986 } 987 988 return $path; 989 } 990 } 991 992 $parsedUrlPath = parse_url($GLOBALS['PMA_PHP_SELF'], PHP_URL_PATH); 993 994 $parts = explode( 995 '/', 996 rtrim(str_replace('\\', '/', $parsedUrlPath), '/') 997 ); 998 999 /* Remove filename */ 1000 if (substr($parts[count($parts) - 1], -4) === '.php') { 1001 $parts = array_slice($parts, 0, count($parts) - 1); 1002 } 1003 1004 /* Remove extra path from javascript calls */ 1005 if (defined('PMA_PATH_TO_BASEDIR')) { 1006 $parts = array_slice($parts, 0, count($parts) - 1); 1007 } 1008 1009 $parts[] = ''; 1010 1011 return implode('/', $parts); 1012 } 1013 1014 /** 1015 * enables backward compatibility 1016 */ 1017 public function enableBc(): void 1018 { 1019 $GLOBALS['cfg'] = $this->settings; 1020 $GLOBALS['default_server'] = $this->defaultServer; 1021 unset($this->defaultServer); 1022 $GLOBALS['is_upload'] = $this->get('enable_upload'); 1023 $GLOBALS['max_upload_size'] = $this->get('max_upload_size'); 1024 $GLOBALS['is_https'] = $this->get('is_https'); 1025 1026 $defines = [ 1027 'PMA_VERSION', 1028 'PMA_MAJOR_VERSION', 1029 'PMA_THEME_VERSION', 1030 'PMA_THEME_GENERATION', 1031 'PMA_IS_WINDOWS', 1032 'PMA_IS_GD2', 1033 'PMA_USR_OS', 1034 'PMA_USR_BROWSER_VER', 1035 'PMA_USR_BROWSER_AGENT', 1036 ]; 1037 1038 foreach ($defines as $define) { 1039 if (defined($define)) { 1040 continue; 1041 } 1042 1043 define($define, $this->get($define)); 1044 } 1045 } 1046 1047 /** 1048 * removes cookie 1049 * 1050 * @param string $cookieName name of cookie to remove 1051 * 1052 * @return bool result of setcookie() 1053 */ 1054 public function removeCookie(string $cookieName): bool 1055 { 1056 $httpCookieName = $this->getCookieName($cookieName); 1057 1058 if ($this->issetCookie($cookieName)) { 1059 unset($_COOKIE[$httpCookieName]); 1060 } 1061 if (defined('TESTSUITE')) { 1062 return true; 1063 } 1064 1065 return setcookie( 1066 $httpCookieName, 1067 '', 1068 time() - 3600, 1069 $this->getRootPath(), 1070 '', 1071 $this->isHttps() 1072 ); 1073 } 1074 1075 /** 1076 * sets cookie if value is different from current cookie value, 1077 * or removes if value is equal to default 1078 * 1079 * @param string $cookie name of cookie to remove 1080 * @param string $value new cookie value 1081 * @param string $default default value 1082 * @param int $validity validity of cookie in seconds (default is one month) 1083 * @param bool $httponly whether cookie is only for HTTP (and not for scripts) 1084 * 1085 * @return bool result of setcookie() 1086 */ 1087 public function setCookie( 1088 string $cookie, 1089 string $value, 1090 ?string $default = null, 1091 ?int $validity = null, 1092 bool $httponly = true 1093 ): bool { 1094 global $cfg; 1095 1096 if (strlen($value) > 0 && $default !== null && $value === $default 1097 ) { 1098 // default value is used 1099 if ($this->issetCookie($cookie)) { 1100 // remove cookie 1101 return $this->removeCookie($cookie); 1102 } 1103 1104 return false; 1105 } 1106 1107 if (strlen($value) === 0 && $this->issetCookie($cookie)) { 1108 // remove cookie, value is empty 1109 return $this->removeCookie($cookie); 1110 } 1111 1112 $httpCookieName = $this->getCookieName($cookie); 1113 1114 if (! $this->issetCookie($cookie) || $this->getCookie($cookie) !== $value) { 1115 // set cookie with new value 1116 /* Calculate cookie validity */ 1117 if ($validity === null) { 1118 /* Valid for one month */ 1119 $validity = time() + 2592000; 1120 } elseif ($validity == 0) { 1121 /* Valid for session */ 1122 $validity = 0; 1123 } else { 1124 $validity = time() + $validity; 1125 } 1126 if (defined('TESTSUITE')) { 1127 $_COOKIE[$httpCookieName] = $value; 1128 1129 return true; 1130 } 1131 1132 if (PHP_VERSION_ID < 70300) { 1133 return setcookie( 1134 $httpCookieName, 1135 $value, 1136 $validity, 1137 $this->getRootPath() . '; samesite=' . $cfg['CookieSameSite'], 1138 '', 1139 $this->isHttps(), 1140 $httponly 1141 ); 1142 } 1143 $optionalParams = [ 1144 'expires' => $validity, 1145 'path' => $this->getRootPath(), 1146 'domain' => '', 1147 'secure' => $this->isHttps(), 1148 'httponly' => $httponly, 1149 'samesite' => $cfg['CookieSameSite'], 1150 ]; 1151 1152 return setcookie( 1153 $httpCookieName, 1154 $value, 1155 $optionalParams 1156 ); 1157 } 1158 1159 // cookie has already $value as value 1160 return true; 1161 } 1162 1163 /** 1164 * get cookie 1165 * 1166 * @param string $cookieName The name of the cookie to get 1167 * 1168 * @return mixed|null result of getCookie() 1169 */ 1170 public function getCookie(string $cookieName) 1171 { 1172 if (isset($_COOKIE[$this->getCookieName($cookieName)])) { 1173 return $_COOKIE[$this->getCookieName($cookieName)]; 1174 } 1175 1176 return null; 1177 } 1178 1179 /** 1180 * Get the real cookie name 1181 * 1182 * @param string $cookieName The name of the cookie 1183 */ 1184 public function getCookieName(string $cookieName): string 1185 { 1186 return $cookieName . ( $this->isHttps() ? '_https' : '' ); 1187 } 1188 1189 /** 1190 * isset cookie 1191 * 1192 * @param string $cookieName The name of the cookie to check 1193 * 1194 * @return bool result of issetCookie() 1195 */ 1196 public function issetCookie(string $cookieName): bool 1197 { 1198 return isset($_COOKIE[$this->getCookieName($cookieName)]); 1199 } 1200 1201 /** 1202 * Error handler to catch fatal errors when loading configuration 1203 * file 1204 */ 1205 public static function fatalErrorHandler(): void 1206 { 1207 global $isConfigLoading; 1208 1209 if (! isset($isConfigLoading) || ! $isConfigLoading) { 1210 return; 1211 } 1212 1213 $error = error_get_last(); 1214 if ($error === null) { 1215 return; 1216 } 1217 1218 Core::fatalError( 1219 sprintf( 1220 'Failed to load phpMyAdmin configuration (%s:%s): %s', 1221 Error::relPath($error['file']), 1222 $error['line'], 1223 $error['message'] 1224 ) 1225 ); 1226 } 1227 1228 /** 1229 * Wrapper for footer/header rendering 1230 * 1231 * @param string $filename File to check and render 1232 * @param string $id Div ID 1233 */ 1234 private static function renderCustom(string $filename, string $id): string 1235 { 1236 $retval = ''; 1237 if (@file_exists($filename)) { 1238 $retval .= '<div id="' . $id . '">'; 1239 ob_start(); 1240 include $filename; 1241 $retval .= ob_get_clean(); 1242 $retval .= '</div>'; 1243 } 1244 1245 return $retval; 1246 } 1247 1248 /** 1249 * Renders user configured footer 1250 */ 1251 public static function renderFooter(): string 1252 { 1253 return self::renderCustom(CUSTOM_FOOTER_FILE, 'pma_footer'); 1254 } 1255 1256 /** 1257 * Renders user configured footer 1258 */ 1259 public static function renderHeader(): string 1260 { 1261 return self::renderCustom(CUSTOM_HEADER_FILE, 'pma_header'); 1262 } 1263 1264 /** 1265 * Returns temporary dir path 1266 * 1267 * @param string $name Directory name 1268 */ 1269 public function getTempDir(string $name): ?string 1270 { 1271 static $temp_dir = []; 1272 1273 if (isset($temp_dir[$name]) && ! defined('TESTSUITE')) { 1274 return $temp_dir[$name]; 1275 } 1276 1277 $path = $this->get('TempDir'); 1278 if (empty($path)) { 1279 $path = null; 1280 } else { 1281 $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name; 1282 if (! @is_dir($path)) { 1283 @mkdir($path, 0770, true); 1284 } 1285 if (! @is_dir($path) || ! @is_writable($path)) { 1286 $path = null; 1287 } 1288 } 1289 1290 $temp_dir[$name] = $path; 1291 1292 return $path; 1293 } 1294 1295 /** 1296 * Returns temporary directory 1297 */ 1298 public function getUploadTempDir(): ?string 1299 { 1300 // First try configured temp dir 1301 // Fallback to PHP upload_tmp_dir 1302 $dirs = [ 1303 $this->getTempDir('upload'), 1304 ini_get('upload_tmp_dir'), 1305 sys_get_temp_dir(), 1306 ]; 1307 1308 foreach ($dirs as $dir) { 1309 if (! empty($dir) && @is_writable($dir)) { 1310 return realpath($dir); 1311 } 1312 } 1313 1314 return null; 1315 } 1316 1317 /** 1318 * Selects server based on request parameters. 1319 */ 1320 public function selectServer(): int 1321 { 1322 $request = empty($_REQUEST['server']) ? 0 : $_REQUEST['server']; 1323 1324 /** 1325 * Lookup server by name 1326 * (see FAQ 4.8) 1327 */ 1328 if (! is_numeric($request)) { 1329 foreach ($this->settings['Servers'] as $i => $server) { 1330 $verboseToLower = mb_strtolower($server['verbose']); 1331 $serverToLower = mb_strtolower($request); 1332 if ($server['host'] == $request 1333 || $server['verbose'] == $request 1334 || $verboseToLower == $serverToLower 1335 || md5($verboseToLower) === $serverToLower 1336 ) { 1337 $request = $i; 1338 break; 1339 } 1340 } 1341 if (is_string($request)) { 1342 $request = 0; 1343 } 1344 } 1345 1346 /** 1347 * If no server is selected, make sure that $this->settings['Server'] is empty (so 1348 * that nothing will work), and skip server authentication. 1349 * We do NOT exit here, but continue on without logging into any server. 1350 * This way, the welcome page will still come up (with no server info) and 1351 * present a choice of servers in the case that there are multiple servers 1352 * and '$this->settings['ServerDefault'] = 0' is set. 1353 */ 1354 1355 if (is_numeric($request) && ! empty($request) && ! empty($this->settings['Servers'][$request])) { 1356 $server = $request; 1357 $this->settings['Server'] = $this->settings['Servers'][$server]; 1358 } else { 1359 if (! empty($this->settings['Servers'][$this->settings['ServerDefault']])) { 1360 $server = $this->settings['ServerDefault']; 1361 $this->settings['Server'] = $this->settings['Servers'][$server]; 1362 } else { 1363 $server = 0; 1364 $this->settings['Server'] = []; 1365 } 1366 } 1367 1368 return (int) $server; 1369 } 1370 1371 /** 1372 * Checks whether Servers configuration is valid and possibly apply fixups. 1373 */ 1374 public function checkServers(): void 1375 { 1376 // Do we have some server? 1377 if (! isset($this->settings['Servers']) || count($this->settings['Servers']) === 0) { 1378 // No server => create one with defaults 1379 $this->settings['Servers'] = [1 => $this->defaultServer]; 1380 } else { 1381 // We have server(s) => apply default configuration 1382 $new_servers = []; 1383 1384 foreach ($this->settings['Servers'] as $server_index => $each_server) { 1385 // Detect wrong configuration 1386 if (! is_int($server_index) || $server_index < 1) { 1387 trigger_error( 1388 sprintf(__('Invalid server index: %s'), $server_index), 1389 E_USER_ERROR 1390 ); 1391 } 1392 1393 $each_server = array_merge($this->defaultServer, $each_server); 1394 1395 // Final solution to bug #582890 1396 // If we are using a socket connection 1397 // and there is nothing in the verbose server name 1398 // or the host field, then generate a name for the server 1399 // in the form of "Server 2", localized of course! 1400 if (empty($each_server['host']) && empty($each_server['verbose'])) { 1401 $each_server['verbose'] = sprintf(__('Server %d'), $server_index); 1402 } 1403 1404 $new_servers[$server_index] = $each_server; 1405 } 1406 $this->settings['Servers'] = $new_servers; 1407 } 1408 } 1409 1410 /** 1411 * Return connection parameters for the database server 1412 * 1413 * @param int $mode Connection mode on of CONNECT_USER, CONNECT_CONTROL 1414 * or CONNECT_AUXILIARY. 1415 * @param array|null $server Server information like host/port/socket/persistent 1416 * 1417 * @return array user, host and server settings array 1418 */ 1419 public static function getConnectionParams(int $mode, ?array $server = null): array 1420 { 1421 global $cfg; 1422 1423 $user = null; 1424 $password = null; 1425 1426 if ($mode == DatabaseInterface::CONNECT_USER) { 1427 $user = $cfg['Server']['user']; 1428 $password = $cfg['Server']['password']; 1429 $server = $cfg['Server']; 1430 } elseif ($mode == DatabaseInterface::CONNECT_CONTROL) { 1431 $user = $cfg['Server']['controluser']; 1432 $password = $cfg['Server']['controlpass']; 1433 1434 $server = []; 1435 1436 if (! empty($cfg['Server']['controlhost'])) { 1437 $server['host'] = $cfg['Server']['controlhost']; 1438 } else { 1439 $server['host'] = $cfg['Server']['host']; 1440 } 1441 // Share the settings if the host is same 1442 if ($server['host'] == $cfg['Server']['host']) { 1443 $shared = [ 1444 'port', 1445 'socket', 1446 'compress', 1447 'ssl', 1448 'ssl_key', 1449 'ssl_cert', 1450 'ssl_ca', 1451 'ssl_ca_path', 1452 'ssl_ciphers', 1453 'ssl_verify', 1454 ]; 1455 foreach ($shared as $item) { 1456 if (! isset($cfg['Server'][$item])) { 1457 continue; 1458 } 1459 1460 $server[$item] = $cfg['Server'][$item]; 1461 } 1462 } 1463 // Set configured port 1464 if (! empty($cfg['Server']['controlport'])) { 1465 $server['port'] = $cfg['Server']['controlport']; 1466 } 1467 // Set any configuration with control_ prefix 1468 foreach ($cfg['Server'] as $key => $val) { 1469 if (substr($key, 0, 8) !== 'control_') { 1470 continue; 1471 } 1472 1473 $server[substr($key, 8)] = $val; 1474 } 1475 } else { 1476 if ($server === null) { 1477 return [ 1478 null, 1479 null, 1480 null, 1481 ]; 1482 } 1483 if (isset($server['user'])) { 1484 $user = $server['user']; 1485 } 1486 if (isset($server['password'])) { 1487 $password = $server['password']; 1488 } 1489 } 1490 1491 // Perform sanity checks on some variables 1492 $server['port'] = empty($server['port']) ? 0 : (int) $server['port']; 1493 1494 if (empty($server['socket'])) { 1495 $server['socket'] = null; 1496 } 1497 if (empty($server['host'])) { 1498 $server['host'] = 'localhost'; 1499 } 1500 if (! isset($server['ssl'])) { 1501 $server['ssl'] = false; 1502 } 1503 if (! isset($server['compress'])) { 1504 $server['compress'] = false; 1505 } 1506 1507 return [ 1508 $user, 1509 $password, 1510 $server, 1511 ]; 1512 } 1513 1514 /** 1515 * Get LoginCookieValidity from preferences cache. 1516 * 1517 * No generic solution for loading preferences from cache as some settings 1518 * need to be kept for processing in loadUserPreferences(). 1519 * 1520 * @see loadUserPreferences() 1521 */ 1522 public function getLoginCookieValidityFromCache(int $server): void 1523 { 1524 global $cfg; 1525 1526 $cacheKey = 'server_' . $server; 1527 1528 if (! isset($_SESSION['cache'][$cacheKey]['userprefs']['LoginCookieValidity'])) { 1529 return; 1530 } 1531 1532 $value = $_SESSION['cache'][$cacheKey]['userprefs']['LoginCookieValidity']; 1533 $this->set('LoginCookieValidity', $value); 1534 $cfg['LoginCookieValidity'] = $value; 1535 } 1536} 1537