1<?php 2/** 3 * Used to render the header of PMA's pages 4 */ 5 6declare(strict_types=1); 7 8namespace PhpMyAdmin; 9 10use PhpMyAdmin\Html\Generator; 11use PhpMyAdmin\Navigation\Navigation; 12use function defined; 13use function gmdate; 14use function header; 15use function htmlspecialchars; 16use function implode; 17use function ini_get; 18use function is_bool; 19use function strlen; 20use function strtolower; 21use function urlencode; 22 23/** 24 * Class used to output the HTTP and HTML headers 25 */ 26class Header 27{ 28 /** 29 * Scripts instance 30 * 31 * @access private 32 * @var Scripts 33 */ 34 private $scripts; 35 /** 36 * PhpMyAdmin\Console instance 37 * 38 * @access private 39 * @var Console 40 */ 41 private $console; 42 /** 43 * Menu instance 44 * 45 * @access private 46 * @var Menu 47 */ 48 private $menu; 49 /** 50 * Whether to offer the option of importing user settings 51 * 52 * @access private 53 * @var bool 54 */ 55 private $userprefsOfferImport; 56 /** 57 * The page title 58 * 59 * @access private 60 * @var string 61 */ 62 private $title; 63 /** 64 * The value for the id attribute for the body tag 65 * 66 * @access private 67 * @var string 68 */ 69 private $bodyId; 70 /** 71 * Whether to show the top menu 72 * 73 * @access private 74 * @var bool 75 */ 76 private $menuEnabled; 77 /** 78 * Whether to show the warnings 79 * 80 * @access private 81 * @var bool 82 */ 83 private $warningsEnabled; 84 /** 85 * Whether the page is in 'print view' mode 86 * 87 * @access private 88 * @var bool 89 */ 90 private $isPrintView; 91 /** 92 * Whether we are servicing an ajax request. 93 * 94 * @access private 95 * @var bool 96 */ 97 private $isAjax; 98 /** 99 * Whether to display anything 100 * 101 * @access private 102 * @var bool 103 */ 104 private $isEnabled; 105 /** 106 * Whether the HTTP headers (and possibly some HTML) 107 * have already been sent to the browser 108 * 109 * @access private 110 * @var bool 111 */ 112 private $headerIsSent; 113 114 /** @var UserPreferences */ 115 private $userPreferences; 116 117 /** @var Template */ 118 private $template; 119 120 /** 121 * Creates a new class instance 122 */ 123 public function __construct() 124 { 125 global $db, $table; 126 127 $this->template = new Template(); 128 129 $this->isEnabled = true; 130 $this->isAjax = false; 131 $this->bodyId = ''; 132 $this->title = ''; 133 $this->console = new Console(); 134 $this->menu = new Menu( 135 $db ?? '', 136 $table ?? '' 137 ); 138 $this->menuEnabled = true; 139 $this->warningsEnabled = true; 140 $this->isPrintView = false; 141 $this->scripts = new Scripts(); 142 $this->addDefaultScripts(); 143 $this->headerIsSent = false; 144 // if database storage for user preferences is transient, 145 // offer to load exported settings from localStorage 146 // (detection will be done in JavaScript) 147 $this->userprefsOfferImport = false; 148 if ($GLOBALS['PMA_Config']->get('user_preferences') === 'session' 149 && ! isset($_SESSION['userprefs_autoload']) 150 ) { 151 $this->userprefsOfferImport = true; 152 } 153 154 $this->userPreferences = new UserPreferences(); 155 } 156 157 /** 158 * Loads common scripts 159 */ 160 private function addDefaultScripts(): void 161 { 162 // Localised strings 163 $this->scripts->addFile('vendor/jquery/jquery.min.js'); 164 $this->scripts->addFile('vendor/jquery/jquery-migrate.js'); 165 $this->scripts->addFile('vendor/sprintf.js'); 166 $this->scripts->addFile('ajax.js'); 167 $this->scripts->addFile('keyhandler.js'); 168 $this->scripts->addFile('vendor/bootstrap/bootstrap.bundle.min.js'); 169 $this->scripts->addFile('vendor/jquery/jquery-ui.min.js'); 170 $this->scripts->addFile('vendor/js.cookie.js'); 171 $this->scripts->addFile('vendor/jquery/jquery.mousewheel.js'); 172 $this->scripts->addFile('vendor/jquery/jquery.validate.js'); 173 $this->scripts->addFile('vendor/jquery/jquery-ui-timepicker-addon.js'); 174 $this->scripts->addFile('vendor/jquery/jquery.ba-hashchange-2.0.js'); 175 $this->scripts->addFile('vendor/jquery/jquery.debounce-1.0.6.js'); 176 $this->scripts->addFile('menu_resizer.js'); 177 178 // Cross-framing protection 179 if ($GLOBALS['cfg']['AllowThirdPartyFraming'] === false) { 180 $this->scripts->addFile('cross_framing_protection.js'); 181 } 182 183 $this->scripts->addFile('rte.js'); 184 if ($GLOBALS['cfg']['SendErrorReports'] !== 'never') { 185 $this->scripts->addFile('vendor/tracekit.js'); 186 $this->scripts->addFile('error_report.js'); 187 } 188 189 // Here would not be a good place to add CodeMirror because 190 // the user preferences have not been merged at this point 191 192 $this->scripts->addFile('messages.php', ['l' => $GLOBALS['lang']]); 193 $this->scripts->addCode($this->getVariablesForJavaScript()); 194 $this->scripts->addFile('config.js'); 195 $this->scripts->addFile('doclinks.js'); 196 $this->scripts->addFile('functions.js'); 197 $this->scripts->addFile('navigation.js'); 198 $this->scripts->addFile('indexes.js'); 199 $this->scripts->addFile('common.js'); 200 $this->scripts->addFile('page_settings.js'); 201 if ($GLOBALS['cfg']['enable_drag_drop_import'] === true) { 202 $this->scripts->addFile('drag_drop_import.js'); 203 } 204 if (! $GLOBALS['PMA_Config']->get('DisableShortcutKeys')) { 205 $this->scripts->addFile('shortcuts_handler.js'); 206 } 207 $this->scripts->addCode($this->getJsParamsCode()); 208 } 209 210 /** 211 * Returns, as an array, a list of parameters 212 * used on the client side 213 * 214 * @return array 215 */ 216 public function getJsParams(): array 217 { 218 global $db, $table, $dbi; 219 220 $pftext = $_SESSION['tmpval']['pftext'] ?? ''; 221 222 $params = [ 223 // Do not add any separator, JS code will decide 224 'common_query' => Url::getCommonRaw([], ''), 225 'opendb_url' => Util::getScriptNameForOption( 226 $GLOBALS['cfg']['DefaultTabDatabase'], 227 'database' 228 ), 229 'lang' => $GLOBALS['lang'], 230 'server' => $GLOBALS['server'], 231 'table' => $table ?? '', 232 'db' => $db ?? '', 233 'token' => $_SESSION[' PMA_token '], 234 'text_dir' => $GLOBALS['text_dir'], 235 'show_databases_navigation_as_tree' => $GLOBALS['cfg']['ShowDatabasesNavigationAsTree'], 236 'pma_text_default_tab' => Util::getTitleForTarget( 237 $GLOBALS['cfg']['DefaultTabTable'] 238 ), 239 'pma_text_left_default_tab' => Util::getTitleForTarget( 240 $GLOBALS['cfg']['NavigationTreeDefaultTabTable'] 241 ), 242 'pma_text_left_default_tab2' => Util::getTitleForTarget( 243 $GLOBALS['cfg']['NavigationTreeDefaultTabTable2'] 244 ), 245 'LimitChars' => $GLOBALS['cfg']['LimitChars'], 246 'pftext' => $pftext, 247 'confirm' => $GLOBALS['cfg']['Confirm'], 248 'LoginCookieValidity' => $GLOBALS['cfg']['LoginCookieValidity'], 249 'session_gc_maxlifetime' => (int) ini_get('session.gc_maxlifetime'), 250 'logged_in' => isset($dbi) ? $dbi->isConnected() : false, 251 'is_https' => $GLOBALS['PMA_Config']->isHttps(), 252 'rootPath' => $GLOBALS['PMA_Config']->getRootPath(), 253 'arg_separator' => Url::getArgSeparator(), 254 'PMA_VERSION' => PMA_VERSION, 255 ]; 256 if (isset($GLOBALS['cfg']['Server'], $GLOBALS['cfg']['Server']['auth_type'])) { 257 $params['auth_type'] = $GLOBALS['cfg']['Server']['auth_type']; 258 if (isset($GLOBALS['cfg']['Server']['user'])) { 259 $params['user'] = $GLOBALS['cfg']['Server']['user']; 260 } 261 } 262 263 return $params; 264 } 265 266 /** 267 * Returns, as a string, a list of parameters 268 * used on the client side 269 */ 270 public function getJsParamsCode(): string 271 { 272 $params = $this->getJsParams(); 273 foreach ($params as $key => $value) { 274 if (is_bool($value)) { 275 $params[$key] = $key . ':' . ($value ? 'true' : 'false') . ''; 276 } else { 277 $params[$key] = $key . ':"' . Sanitize::escapeJsString($value) . '"'; 278 } 279 } 280 281 return 'CommonParams.setAll({' . implode(',', $params) . '});'; 282 } 283 284 /** 285 * Disables the rendering of the header 286 */ 287 public function disable(): void 288 { 289 $this->isEnabled = false; 290 } 291 292 /** 293 * Set the ajax flag to indicate whether 294 * we are servicing an ajax request 295 * 296 * @param bool $isAjax Whether we are servicing an ajax request 297 */ 298 public function setAjax(bool $isAjax): void 299 { 300 $this->isAjax = $isAjax; 301 $this->console->setAjax($isAjax); 302 } 303 304 /** 305 * Returns the Scripts object 306 * 307 * @return Scripts object 308 */ 309 public function getScripts(): Scripts 310 { 311 return $this->scripts; 312 } 313 314 /** 315 * Returns the Menu object 316 * 317 * @return Menu object 318 */ 319 public function getMenu(): Menu 320 { 321 return $this->menu; 322 } 323 324 /** 325 * Setter for the ID attribute in the BODY tag 326 * 327 * @param string $id Value for the ID attribute 328 */ 329 public function setBodyId(string $id): void 330 { 331 $this->bodyId = htmlspecialchars($id); 332 } 333 334 /** 335 * Setter for the title of the page 336 * 337 * @param string $title New title 338 */ 339 public function setTitle(string $title): void 340 { 341 $this->title = htmlspecialchars($title); 342 } 343 344 /** 345 * Disables the display of the top menu 346 */ 347 public function disableMenuAndConsole(): void 348 { 349 $this->menuEnabled = false; 350 $this->console->disable(); 351 } 352 353 /** 354 * Disables the display of the top menu 355 */ 356 public function disableWarnings(): void 357 { 358 $this->warningsEnabled = false; 359 } 360 361 /** 362 * Turns on 'print view' mode 363 */ 364 public function enablePrintView(): void 365 { 366 $this->disableMenuAndConsole(); 367 $this->setTitle(__('Print view') . ' - phpMyAdmin ' . PMA_VERSION); 368 $this->isPrintView = true; 369 } 370 371 /** 372 * Generates the header 373 * 374 * @return string The header 375 */ 376 public function getDisplay(): string 377 { 378 global $db, $table, $PMA_Theme, $dbi; 379 380 if ($this->headerIsSent || ! $this->isEnabled) { 381 return ''; 382 } 383 384 $recentTable = ''; 385 if (empty($_REQUEST['recent_table'])) { 386 $recentTable = $this->addRecentTable($db, $table); 387 } 388 389 if ($this->isAjax) { 390 return $recentTable; 391 } 392 393 $this->sendHttpHeaders(); 394 395 $baseDir = defined('PMA_PATH_TO_BASEDIR') ? PMA_PATH_TO_BASEDIR : ''; 396 $uniqueValue = $GLOBALS['PMA_Config']->getThemeUniqueValue(); 397 $themePath = $PMA_Theme !== null ? $PMA_Theme->getPath() : ''; 398 $version = self::getVersionParameter(); 399 400 // The user preferences have been merged at this point 401 // so we can conditionally add CodeMirror 402 if ($GLOBALS['cfg']['CodemirrorEnable']) { 403 $this->scripts->addFile('vendor/codemirror/lib/codemirror.js'); 404 $this->scripts->addFile('vendor/codemirror/mode/sql/sql.js'); 405 $this->scripts->addFile('vendor/codemirror/addon/runmode/runmode.js'); 406 $this->scripts->addFile('vendor/codemirror/addon/hint/show-hint.js'); 407 $this->scripts->addFile('vendor/codemirror/addon/hint/sql-hint.js'); 408 if ($GLOBALS['cfg']['LintEnable']) { 409 $this->scripts->addFile('vendor/codemirror/addon/lint/lint.js'); 410 $this->scripts->addFile( 411 'codemirror/addon/lint/sql-lint.js' 412 ); 413 } 414 } 415 416 $this->scripts->addCode( 417 'ConsoleEnterExecutes=' 418 . ($GLOBALS['cfg']['ConsoleEnterExecutes'] ? 'true' : 'false') 419 ); 420 $this->scripts->addFiles($this->console->getScripts()); 421 422 if ($this->userprefsOfferImport) { 423 $this->scripts->addFile('config.js'); 424 } 425 426 if ($this->menuEnabled && $GLOBALS['server'] > 0) { 427 $nav = new Navigation( 428 $this->template, 429 new Relation($dbi), 430 $dbi 431 ); 432 $navigation = $nav->getDisplay(); 433 } 434 435 $customHeader = Config::renderHeader(); 436 437 // offer to load user preferences from localStorage 438 if ($this->userprefsOfferImport) { 439 $loadUserPreferences = $this->userPreferences->autoloadGetHeader(); 440 } 441 442 if ($this->menuEnabled && $GLOBALS['server'] > 0) { 443 $menu = $this->menu->getDisplay(); 444 } 445 446 $console = $this->console->getDisplay(); 447 $messages = $this->getMessage(); 448 449 return $this->template->render('header', [ 450 'lang' => $GLOBALS['lang'], 451 'allow_third_party_framing' => $GLOBALS['cfg']['AllowThirdPartyFraming'], 452 'is_print_view' => $this->isPrintView, 453 'base_dir' => $baseDir, 454 'unique_value' => $uniqueValue, 455 'theme_path' => $themePath, 456 'version' => $version, 457 'text_dir' => $GLOBALS['text_dir'], 458 'server' => $GLOBALS['server'] ?? null, 459 'title' => $this->getPageTitle(), 460 'scripts' => $this->scripts->getDisplay(), 461 'body_id' => $this->bodyId, 462 'navigation' => $navigation ?? '', 463 'custom_header' => $customHeader, 464 'load_user_preferences' => $loadUserPreferences ?? '', 465 'show_hint' => $GLOBALS['cfg']['ShowHint'], 466 'is_warnings_enabled' => $this->warningsEnabled, 467 'is_menu_enabled' => $this->menuEnabled, 468 'menu' => $menu ?? '', 469 'console' => $console, 470 'messages' => $messages, 471 'recent_table' => $recentTable, 472 ]); 473 } 474 475 /** 476 * Returns the message to be displayed at the top of 477 * the page, including the executed SQL query, if any. 478 */ 479 public function getMessage(): string 480 { 481 $retval = ''; 482 $message = ''; 483 if (! empty($GLOBALS['message'])) { 484 $message = $GLOBALS['message']; 485 unset($GLOBALS['message']); 486 } elseif (! empty($_REQUEST['message'])) { 487 $message = $_REQUEST['message']; 488 } 489 if (! empty($message)) { 490 if (isset($GLOBALS['buffer_message'])) { 491 $buffer_message = $GLOBALS['buffer_message']; 492 } 493 $retval .= Generator::getMessage($message); 494 if (isset($buffer_message)) { 495 $GLOBALS['buffer_message'] = $buffer_message; 496 } 497 } 498 499 return $retval; 500 } 501 502 /** 503 * Sends out the HTTP headers 504 */ 505 public function sendHttpHeaders(): void 506 { 507 if (defined('TESTSUITE')) { 508 return; 509 } 510 511 /** 512 * Sends http headers 513 */ 514 $GLOBALS['now'] = gmdate('D, d M Y H:i:s') . ' GMT'; 515 516 /* Prevent against ClickJacking by disabling framing */ 517 if (strtolower((string) $GLOBALS['cfg']['AllowThirdPartyFraming']) === 'sameorigin') { 518 header( 519 'X-Frame-Options: SAMEORIGIN' 520 ); 521 } elseif ($GLOBALS['cfg']['AllowThirdPartyFraming'] !== true) { 522 header( 523 'X-Frame-Options: DENY' 524 ); 525 } 526 header( 527 'Referrer-Policy: no-referrer' 528 ); 529 530 $cspHeaders = $this->getCspHeaders(); 531 foreach ($cspHeaders as $cspHeader) { 532 header($cspHeader); 533 } 534 535 // Re-enable possible disabled XSS filters 536 // see https://www.owasp.org/index.php/List_of_useful_HTTP_headers 537 header( 538 'X-XSS-Protection: 1; mode=block' 539 ); 540 // "nosniff", prevents Internet Explorer and Google Chrome from MIME-sniffing 541 // a response away from the declared content-type 542 // see https://www.owasp.org/index.php/List_of_useful_HTTP_headers 543 header( 544 'X-Content-Type-Options: nosniff' 545 ); 546 // Adobe cross-domain-policies 547 // see https://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html 548 header( 549 'X-Permitted-Cross-Domain-Policies: none' 550 ); 551 // Robots meta tag 552 // see https://developers.google.com/webmasters/control-crawl-index/docs/robots_meta_tag 553 header( 554 'X-Robots-Tag: noindex, nofollow' 555 ); 556 Core::noCacheHeader(); 557 if (! defined('IS_TRANSFORMATION_WRAPPER')) { 558 // Define the charset to be used 559 header('Content-Type: text/html; charset=utf-8'); 560 } 561 $this->headerIsSent = true; 562 } 563 564 /** 565 * If the page is missing the title, this function 566 * will set it to something reasonable 567 */ 568 public function getPageTitle(): string 569 { 570 if (strlen($this->title) == 0) { 571 if ($GLOBALS['server'] > 0) { 572 if (strlen($GLOBALS['table'])) { 573 $temp_title = $GLOBALS['cfg']['TitleTable']; 574 } elseif (strlen($GLOBALS['db'])) { 575 $temp_title = $GLOBALS['cfg']['TitleDatabase']; 576 } elseif (strlen($GLOBALS['cfg']['Server']['host'])) { 577 $temp_title = $GLOBALS['cfg']['TitleServer']; 578 } else { 579 $temp_title = $GLOBALS['cfg']['TitleDefault']; 580 } 581 $this->title = htmlspecialchars( 582 Util::expandUserString($temp_title) 583 ); 584 } else { 585 $this->title = 'phpMyAdmin'; 586 } 587 } 588 589 return $this->title; 590 } 591 592 /** 593 * Get all the CSP allow policy headers 594 * 595 * @return string[] 596 */ 597 private function getCspHeaders(): array 598 { 599 global $cfg; 600 601 $mapTileUrls = ' *.tile.openstreetmap.org'; 602 $captchaUrl = ''; 603 $cspAllow = $cfg['CSPAllow']; 604 605 if (! empty($cfg['CaptchaApi']) 606 && ! empty($cfg['CaptchaRequestParam']) 607 && ! empty($cfg['CaptchaResponseParam']) 608 && ! empty($cfg['CaptchaLoginPrivateKey']) 609 && ! empty($cfg['CaptchaLoginPublicKey']) 610 ) { 611 $captchaUrl = ' ' . $cfg['CaptchaCsp'] . ' '; 612 } 613 614 return [ 615 616 "Content-Security-Policy: default-src 'self' " 617 . $captchaUrl 618 . $cspAllow . ';' 619 . "script-src 'self' 'unsafe-inline' 'unsafe-eval' " 620 . $captchaUrl 621 . $cspAllow . ';' 622 . "style-src 'self' 'unsafe-inline' " 623 . $captchaUrl 624 . $cspAllow 625 . ';' 626 . "img-src 'self' data: " 627 . $cspAllow 628 . $mapTileUrls 629 . $captchaUrl 630 . ';' 631 . "object-src 'none';", 632 633 "X-Content-Security-Policy: default-src 'self' " 634 . $captchaUrl 635 . $cspAllow . ';' 636 . 'options inline-script eval-script;' 637 . 'referrer no-referrer;' 638 . "img-src 'self' data: " 639 . $cspAllow 640 . $mapTileUrls 641 . $captchaUrl 642 . ';' 643 . "object-src 'none';", 644 645 "X-WebKit-CSP: default-src 'self' " 646 . $captchaUrl 647 . $cspAllow . ';' 648 . "script-src 'self' " 649 . $captchaUrl 650 . $cspAllow 651 . " 'unsafe-inline' 'unsafe-eval';" 652 . 'referrer no-referrer;' 653 . "style-src 'self' 'unsafe-inline' " 654 . $captchaUrl 655 . ';' 656 . "img-src 'self' data: " 657 . $cspAllow 658 . $mapTileUrls 659 . $captchaUrl 660 . ';' 661 . "object-src 'none';", 662 ]; 663 } 664 665 /** 666 * Add recently used table and reload the navigation. 667 * 668 * @param string $db Database name where the table is located. 669 * @param string $table The table name 670 */ 671 private function addRecentTable(string $db, string $table): string 672 { 673 $retval = ''; 674 if ($this->menuEnabled 675 && strlen($table) > 0 676 && $GLOBALS['cfg']['NumRecentTables'] > 0 677 ) { 678 $tmp_result = RecentFavoriteTable::getInstance('recent')->add( 679 $db, 680 $table 681 ); 682 if ($tmp_result === true) { 683 $retval = RecentFavoriteTable::getHtmlUpdateRecentTables(); 684 } else { 685 $error = $tmp_result; 686 $retval = $error->getDisplay(); 687 } 688 } 689 690 return $retval; 691 } 692 693 /** 694 * Returns the phpMyAdmin version to be appended to the url to avoid caching 695 * between versions 696 * 697 * @return string urlencoded pma version as a parameter 698 */ 699 public static function getVersionParameter(): string 700 { 701 return 'v=' . urlencode(PMA_VERSION); 702 } 703 704 private function getVariablesForJavaScript(): string 705 { 706 global $cfg, $PMA_Theme; 707 708 $maxInputVars = ini_get('max_input_vars'); 709 $maxInputVarsValue = $maxInputVars === false || $maxInputVars === '' ? 'false' : (int) $maxInputVars; 710 711 return $this->template->render('javascript/variables', [ 712 'first_day_of_calendar' => $cfg['FirstDayOfCalendar'] ?? 0, 713 'theme_image_path' => $PMA_Theme !== null ? $PMA_Theme->getImgPath() : '', 714 'max_input_vars' => $maxInputVarsValue, 715 ]); 716 } 717} 718