Replace `global` keyword with `$GLOBALS`
[phpmyadmin.git] / libraries / classes / Header.php
blob48e2f6e21b4c8669be2526cf8dd65e5b633fb6ac
1 <?php
2 /**
3 * Used to render the header of PMA's pages
4 */
6 declare(strict_types=1);
8 namespace PhpMyAdmin;
10 use PhpMyAdmin\ConfigStorage\Relation;
11 use PhpMyAdmin\Html\Generator;
12 use PhpMyAdmin\Navigation\Navigation;
14 use function array_merge;
15 use function defined;
16 use function gmdate;
17 use function header;
18 use function htmlspecialchars;
19 use function implode;
20 use function ini_get;
21 use function is_bool;
22 use function sprintf;
23 use function strlen;
24 use function strtolower;
25 use function urlencode;
27 /**
28 * Class used to output the HTTP and HTML headers
30 class Header
32 /**
33 * Scripts instance
35 * @var Scripts
37 private $scripts;
38 /**
39 * PhpMyAdmin\Console instance
41 * @var Console
43 private $console;
44 /**
45 * Menu instance
47 * @var Menu
49 private $menu;
50 /**
51 * The page title
53 * @var string
55 private $title;
56 /**
57 * The value for the id attribute for the body tag
59 * @var string
61 private $bodyId;
62 /**
63 * Whether to show the top menu
65 * @var bool
67 private $menuEnabled;
68 /**
69 * Whether to show the warnings
71 * @var bool
73 private $warningsEnabled;
74 /**
75 * Whether we are servicing an ajax request.
77 * @var bool
79 private $isAjax;
80 /**
81 * Whether to display anything
83 * @var bool
85 private $isEnabled;
86 /**
87 * Whether the HTTP headers (and possibly some HTML)
88 * have already been sent to the browser
90 * @var bool
92 private $headerIsSent;
94 /** @var UserPreferences */
95 private $userPreferences;
97 /** @var Template */
98 private $template;
101 * Creates a new class instance
103 public function __construct()
105 $this->template = new Template();
107 $this->isEnabled = true;
108 $this->isAjax = false;
109 $this->bodyId = '';
110 $this->title = '';
111 $this->console = new Console();
112 $this->menu = new Menu($GLOBALS['dbi'], $GLOBALS['db'] ?? '', $GLOBALS['table'] ?? '');
113 $this->menuEnabled = true;
114 $this->warningsEnabled = true;
115 $this->scripts = new Scripts();
116 $this->addDefaultScripts();
117 $this->headerIsSent = false;
119 $this->userPreferences = new UserPreferences();
123 * Loads common scripts
125 private function addDefaultScripts(): void
127 // Localised strings
128 $this->scripts->addFile('vendor/jquery/jquery.min.js');
129 $this->scripts->addFile('vendor/jquery/jquery-migrate.js');
130 $this->scripts->addFile('vendor/sprintf.js');
131 $this->scripts->addFile('ajax.js');
132 $this->scripts->addFile('keyhandler.js');
133 $this->scripts->addFile('vendor/jquery/jquery-ui.min.js');
134 $this->scripts->addFile('name-conflict-fixes.js');
135 $this->scripts->addFile('vendor/bootstrap/bootstrap.bundle.min.js');
136 $this->scripts->addFile('vendor/js.cookie.js');
137 $this->scripts->addFile('vendor/jquery/jquery.mousewheel.js');
138 $this->scripts->addFile('vendor/jquery/jquery.validate.js');
139 $this->scripts->addFile('vendor/jquery/jquery-ui-timepicker-addon.js');
140 $this->scripts->addFile('vendor/jquery/jquery.debounce-1.0.6.js');
141 $this->scripts->addFile('menu_resizer.js');
143 // Cross-framing protection
144 // At this point browser settings are not merged
145 // this is good that we only use file configuration for this protection
146 if ($GLOBALS['cfg']['AllowThirdPartyFraming'] === false) {
147 $this->scripts->addFile('cross_framing_protection.js');
150 // Here would not be a good place to add CodeMirror because
151 // the user preferences have not been merged at this point
153 $this->scripts->addFile('messages.php', ['l' => $GLOBALS['lang']]);
154 $this->scripts->addFile('config.js');
155 $this->scripts->addFile('doclinks.js');
156 $this->scripts->addFile('functions.js');
157 $this->scripts->addFile('navigation.js');
158 $this->scripts->addFile('indexes.js');
159 $this->scripts->addFile('common.js');
160 $this->scripts->addFile('page_settings.js');
162 $this->scripts->addCode($this->getJsParamsCode());
166 * Returns, as an array, a list of parameters
167 * used on the client side
169 * @return array
171 public function getJsParams(): array
173 $pftext = $_SESSION['tmpval']['pftext'] ?? '';
175 $params = [
176 // Do not add any separator, JS code will decide
177 'common_query' => Url::getCommonRaw([], ''),
178 'opendb_url' => Util::getScriptNameForOption($GLOBALS['cfg']['DefaultTabDatabase'], 'database'),
179 'lang' => $GLOBALS['lang'],
180 'server' => $GLOBALS['server'],
181 'table' => $GLOBALS['table'] ?? '',
182 'db' => $GLOBALS['db'] ?? '',
183 'token' => $_SESSION[' PMA_token '],
184 'text_dir' => $GLOBALS['text_dir'],
185 'LimitChars' => $GLOBALS['cfg']['LimitChars'],
186 'pftext' => $pftext,
187 'confirm' => $GLOBALS['cfg']['Confirm'],
188 'LoginCookieValidity' => $GLOBALS['cfg']['LoginCookieValidity'],
189 'session_gc_maxlifetime' => (int) ini_get('session.gc_maxlifetime'),
190 'logged_in' => isset($GLOBALS['dbi']) ? $GLOBALS['dbi']->isConnected() : false,
191 'is_https' => $GLOBALS['config']->isHttps(),
192 'rootPath' => $GLOBALS['config']->getRootPath(),
193 'arg_separator' => Url::getArgSeparator(),
194 'version' => Version::VERSION,
196 if (isset($GLOBALS['cfg']['Server'], $GLOBALS['cfg']['Server']['auth_type'])) {
197 $params['auth_type'] = $GLOBALS['cfg']['Server']['auth_type'];
198 if (isset($GLOBALS['cfg']['Server']['user'])) {
199 $params['user'] = $GLOBALS['cfg']['Server']['user'];
203 return $params;
207 * Returns, as a string, a list of parameters
208 * used on the client side
210 public function getJsParamsCode(): string
212 $params = $this->getJsParams();
213 foreach ($params as $key => $value) {
214 if (is_bool($value)) {
215 $params[$key] = $key . ':' . ($value ? 'true' : 'false') . '';
216 } else {
217 $params[$key] = $key . ':"' . Sanitize::escapeJsString($value) . '"';
221 return 'CommonParams.setAll({' . implode(',', $params) . '});';
225 * Disables the rendering of the header
227 public function disable(): void
229 $this->isEnabled = false;
233 * Set the ajax flag to indicate whether
234 * we are servicing an ajax request
236 * @param bool $isAjax Whether we are servicing an ajax request
238 public function setAjax(bool $isAjax): void
240 $this->isAjax = $isAjax;
241 $this->console->setAjax($isAjax);
245 * Returns the Scripts object
247 * @return Scripts object
249 public function getScripts(): Scripts
251 return $this->scripts;
255 * Returns the Menu object
257 * @return Menu object
259 public function getMenu(): Menu
261 return $this->menu;
265 * Setter for the ID attribute in the BODY tag
267 * @param string $id Value for the ID attribute
269 public function setBodyId(string $id): void
271 $this->bodyId = htmlspecialchars($id);
275 * Setter for the title of the page
277 * @param string $title New title
279 public function setTitle(string $title): void
281 $this->title = htmlspecialchars($title);
285 * Disables the display of the top menu
287 public function disableMenuAndConsole(): void
289 $this->menuEnabled = false;
290 $this->console->disable();
294 * Disables the display of the top menu
296 public function disableWarnings(): void
298 $this->warningsEnabled = false;
302 * Generates the header
304 * @return string The header
306 public function getDisplay(): string
308 if ($this->headerIsSent || ! $this->isEnabled) {
309 return '';
312 $recentTable = '';
313 if (empty($_REQUEST['recent_table'])) {
314 $recentTable = $this->addRecentTable($GLOBALS['db'], $GLOBALS['table']);
317 if ($this->isAjax) {
318 return $recentTable;
321 $this->sendHttpHeaders();
323 $baseDir = defined('PMA_PATH_TO_BASEDIR') ? PMA_PATH_TO_BASEDIR : '';
324 $themePath = $GLOBALS['theme'] instanceof Theme ? $GLOBALS['theme']->getPath() : '';
325 $version = self::getVersionParameter();
327 // The user preferences have been merged at this point
328 // so we can conditionally add CodeMirror, other scripts and settings
329 if ($GLOBALS['cfg']['CodemirrorEnable']) {
330 $this->scripts->addFile('vendor/codemirror/lib/codemirror.js');
331 $this->scripts->addFile('vendor/codemirror/mode/sql/sql.js');
332 $this->scripts->addFile('vendor/codemirror/addon/runmode/runmode.js');
333 $this->scripts->addFile('vendor/codemirror/addon/hint/show-hint.js');
334 $this->scripts->addFile('vendor/codemirror/addon/hint/sql-hint.js');
335 if ($GLOBALS['cfg']['LintEnable']) {
336 $this->scripts->addFile('vendor/codemirror/addon/lint/lint.js');
337 $this->scripts->addFile('codemirror/addon/lint/sql-lint.js');
341 if ($GLOBALS['cfg']['SendErrorReports'] !== 'never') {
342 $this->scripts->addFile('vendor/tracekit.js');
343 $this->scripts->addFile('error_report.js');
346 if ($GLOBALS['cfg']['enable_drag_drop_import'] === true) {
347 $this->scripts->addFile('drag_drop_import.js');
350 if (! $GLOBALS['config']->get('DisableShortcutKeys')) {
351 $this->scripts->addFile('shortcuts_handler.js');
354 $this->scripts->addCode($this->getVariablesForJavaScript());
356 $this->scripts->addCode('ConsoleEnterExecutes=' . ($GLOBALS['cfg']['ConsoleEnterExecutes'] ? 'true' : 'false'));
357 $this->scripts->addFiles($this->console->getScripts());
359 // if database storage for user preferences is transient,
360 // offer to load exported settings from localStorage
361 // (detection will be done in JavaScript)
362 $userprefsOfferImport = false;
363 if (
364 $GLOBALS['config']->get('user_preferences') === 'session'
365 && ! isset($_SESSION['userprefs_autoload'])
367 $userprefsOfferImport = true;
370 if ($userprefsOfferImport) {
371 $this->scripts->addFile('config.js');
374 if ($this->menuEnabled && $GLOBALS['server'] > 0) {
375 $nav = new Navigation(
376 $this->template,
377 new Relation($GLOBALS['dbi']),
378 $GLOBALS['dbi']
380 $navigation = $nav->getDisplay();
383 $customHeader = Config::renderHeader();
385 // offer to load user preferences from localStorage
386 if ($userprefsOfferImport) {
387 $loadUserPreferences = $this->userPreferences->autoloadGetHeader();
390 if ($this->menuEnabled && $GLOBALS['server'] > 0) {
391 $menu = $this->menu->getDisplay();
394 $console = $this->console->getDisplay();
395 $messages = $this->getMessage();
397 $this->scripts->addFile('datetimepicker.js');
398 $this->scripts->addFile('validator-messages.js');
400 return $this->template->render('header', [
401 'lang' => $GLOBALS['lang'],
402 'allow_third_party_framing' => $GLOBALS['cfg']['AllowThirdPartyFraming'],
403 'base_dir' => $baseDir,
404 'theme_path' => $themePath,
405 'version' => $version,
406 'text_dir' => $GLOBALS['text_dir'],
407 'server' => $GLOBALS['server'] ?? null,
408 'title' => $this->getPageTitle(),
409 'scripts' => $this->scripts->getDisplay(),
410 'body_id' => $this->bodyId,
411 'navigation' => $navigation ?? '',
412 'custom_header' => $customHeader,
413 'load_user_preferences' => $loadUserPreferences ?? '',
414 'show_hint' => $GLOBALS['cfg']['ShowHint'],
415 'is_warnings_enabled' => $this->warningsEnabled,
416 'is_menu_enabled' => $this->menuEnabled,
417 'menu' => $menu ?? '',
418 'console' => $console,
419 'messages' => $messages,
420 'recent_table' => $recentTable,
425 * Returns the message to be displayed at the top of
426 * the page, including the executed SQL query, if any.
428 public function getMessage(): string
430 $retval = '';
431 $message = '';
432 if (! empty($GLOBALS['message'])) {
433 $message = $GLOBALS['message'];
434 unset($GLOBALS['message']);
435 } elseif (! empty($_REQUEST['message'])) {
436 $message = $_REQUEST['message'];
439 if (! empty($message)) {
440 if (isset($GLOBALS['buffer_message'])) {
441 $bufferMessage = $GLOBALS['buffer_message'];
444 $retval .= Generator::getMessage($message);
445 if (isset($bufferMessage)) {
446 $GLOBALS['buffer_message'] = $bufferMessage;
450 return $retval;
454 * Sends out the HTTP headers
456 public function sendHttpHeaders(): void
458 if (defined('TESTSUITE')) {
459 return;
463 * Sends http headers
465 $GLOBALS['now'] = gmdate('D, d M Y H:i:s') . ' GMT';
467 $headers = $this->getHttpHeaders();
469 foreach ($headers as $name => $value) {
470 header(sprintf('%s: %s', $name, $value));
473 $this->headerIsSent = true;
477 * @return array<string, string>
479 private function getHttpHeaders(): array
481 $headers = [];
483 /* Prevent against ClickJacking by disabling framing */
484 if (strtolower((string) $GLOBALS['cfg']['AllowThirdPartyFraming']) === 'sameorigin') {
485 $headers['X-Frame-Options'] = 'SAMEORIGIN';
486 } elseif ($GLOBALS['cfg']['AllowThirdPartyFraming'] !== true) {
487 $headers['X-Frame-Options'] = 'DENY';
490 $headers['Referrer-Policy'] = 'no-referrer';
492 $headers = array_merge($headers, $this->getCspHeaders());
495 * Re-enable possible disabled XSS filters.
497 * @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
499 $headers['X-XSS-Protection'] = '1; mode=block';
502 * "nosniff", prevents Internet Explorer and Google Chrome from MIME-sniffing
503 * a response away from the declared content-type.
505 * @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
507 $headers['X-Content-Type-Options'] = 'nosniff';
510 * Adobe cross-domain-policies.
512 * @see https://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html
514 $headers['X-Permitted-Cross-Domain-Policies'] = 'none';
517 * Robots meta tag.
519 * @see https://developers.google.com/webmasters/control-crawl-index/docs/robots_meta_tag
521 $headers['X-Robots-Tag'] = 'noindex, nofollow';
523 $headers = array_merge($headers, Core::getNoCacheHeaders());
525 if (! defined('IS_TRANSFORMATION_WRAPPER')) {
526 // Define the charset to be used
527 $headers['Content-Type'] = 'text/html; charset=utf-8';
530 return $headers;
534 * If the page is missing the title, this function
535 * will set it to something reasonable
537 public function getPageTitle(): string
539 if (strlen($this->title) == 0) {
540 if ($GLOBALS['server'] > 0) {
541 if (strlen($GLOBALS['table'])) {
542 $tempTitle = $GLOBALS['cfg']['TitleTable'];
543 } elseif (strlen($GLOBALS['db'])) {
544 $tempTitle = $GLOBALS['cfg']['TitleDatabase'];
545 } elseif (strlen($GLOBALS['cfg']['Server']['host'])) {
546 $tempTitle = $GLOBALS['cfg']['TitleServer'];
547 } else {
548 $tempTitle = $GLOBALS['cfg']['TitleDefault'];
551 $this->title = htmlspecialchars(
552 Util::expandUserString($tempTitle)
554 } else {
555 $this->title = 'phpMyAdmin';
559 return $this->title;
563 * Get all the CSP allow policy headers
565 * @return array<string, string>
567 private function getCspHeaders(): array
569 $mapTileUrls = ' *.tile.openstreetmap.org';
570 $captchaUrl = '';
571 $cspAllow = $GLOBALS['cfg']['CSPAllow'];
573 if (
574 ! empty($GLOBALS['cfg']['CaptchaLoginPrivateKey'])
575 && ! empty($GLOBALS['cfg']['CaptchaLoginPublicKey'])
576 && ! empty($GLOBALS['cfg']['CaptchaApi'])
577 && ! empty($GLOBALS['cfg']['CaptchaRequestParam'])
578 && ! empty($GLOBALS['cfg']['CaptchaResponseParam'])
580 $captchaUrl = ' ' . $GLOBALS['cfg']['CaptchaCsp'] . ' ';
583 $headers = [];
585 $headers['Content-Security-Policy'] = sprintf(
586 'default-src \'self\' %s%s;script-src \'self\' \'unsafe-inline\' \'unsafe-eval\' %s%s;'
587 . 'style-src \'self\' \'unsafe-inline\' %s%s;img-src \'self\' data: %s%s%s;object-src \'none\';',
588 $captchaUrl,
589 $cspAllow,
590 $captchaUrl,
591 $cspAllow,
592 $captchaUrl,
593 $cspAllow,
594 $cspAllow,
595 $mapTileUrls,
596 $captchaUrl
599 $headers['X-Content-Security-Policy'] = sprintf(
600 'default-src \'self\' %s%s;options inline-script eval-script;'
601 . 'referrer no-referrer;img-src \'self\' data: %s%s%s;object-src \'none\';',
602 $captchaUrl,
603 $cspAllow,
604 $cspAllow,
605 $mapTileUrls,
606 $captchaUrl
609 $headers['X-WebKit-CSP'] = sprintf(
610 'default-src \'self\' %s%s;script-src \'self\' %s%s \'unsafe-inline\' \'unsafe-eval\';'
611 . 'referrer no-referrer;style-src \'self\' \'unsafe-inline\' %s;'
612 . 'img-src \'self\' data: %s%s%s;object-src \'none\';',
613 $captchaUrl,
614 $cspAllow,
615 $captchaUrl,
616 $cspAllow,
617 $captchaUrl,
618 $cspAllow,
619 $mapTileUrls,
620 $captchaUrl
623 return $headers;
627 * Add recently used table and reload the navigation.
629 * @param string $db Database name where the table is located.
630 * @param string $table The table name
632 private function addRecentTable(string $db, string $table): string
634 $retval = '';
635 if ($this->menuEnabled && strlen($table) > 0 && $GLOBALS['cfg']['NumRecentTables'] > 0) {
636 $tmpResult = RecentFavoriteTable::getInstance('recent')->add($db, $table);
637 if ($tmpResult === true) {
638 $retval = RecentFavoriteTable::getHtmlUpdateRecentTables();
639 } else {
640 $error = $tmpResult;
641 $retval = $error->getDisplay();
645 return $retval;
649 * Returns the phpMyAdmin version to be appended to the url to avoid caching
650 * between versions
652 * @return string urlencoded pma version as a parameter
654 public static function getVersionParameter(): string
656 return 'v=' . urlencode(Version::VERSION);
659 private function getVariablesForJavaScript(): string
661 $maxInputVars = ini_get('max_input_vars');
662 $maxInputVarsValue = $maxInputVars === false || $maxInputVars === '' ? 'false' : (int) $maxInputVars;
664 return $this->template->render('javascript/variables', [
665 'first_day_of_calendar' => $GLOBALS['cfg']['FirstDayOfCalendar'] ?? 0,
666 'max_input_vars' => $maxInputVarsValue,