3 * Used to render the header of PMA's pages
6 declare(strict_types
=1);
10 use PhpMyAdmin\ConfigStorage\Relation
;
11 use PhpMyAdmin\Html\Generator
;
12 use PhpMyAdmin\Navigation\Navigation
;
14 use function array_merge
;
18 use function htmlspecialchars
;
24 use function strtolower
;
25 use function urlencode
;
28 * Class used to output the HTTP and HTML headers
39 * PhpMyAdmin\Console instance
57 * The value for the id attribute for the body tag
63 * Whether to show the top menu
69 * Whether to show the warnings
73 private $warningsEnabled;
75 * Whether we are servicing an ajax request.
81 * Whether to display anything
87 * Whether the HTTP headers (and possibly some HTML)
88 * have already been sent to the browser
92 private $headerIsSent;
94 /** @var UserPreferences */
95 private $userPreferences;
101 * Creates a new class instance
103 public function __construct()
105 $this->template
= new Template();
107 $this->isEnabled
= true;
108 $this->isAjax
= false;
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
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
171 public function getJsParams(): array
173 $pftext = $_SESSION['tmpval']['pftext'] ??
'';
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'],
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'];
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') . '';
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
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
) {
313 if (empty($_REQUEST['recent_table'])) {
314 $recentTable = $this->addRecentTable($GLOBALS['db'], $GLOBALS['table']);
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;
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(
377 new Relation($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
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;
454 * Sends out the HTTP headers
456 public function sendHttpHeaders(): void
458 if (defined('TESTSUITE')) {
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
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';
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';
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'];
548 $tempTitle = $GLOBALS['cfg']['TitleDefault'];
551 $this->title
= htmlspecialchars(
552 Util
::expandUserString($tempTitle)
555 $this->title
= 'phpMyAdmin';
563 * Get all the CSP allow policy headers
565 * @return array<string, string>
567 private function getCspHeaders(): array
569 $mapTileUrls = ' *.tile.openstreetmap.org';
571 $cspAllow = $GLOBALS['cfg']['CSPAllow'];
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'] . ' ';
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\';',
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\';',
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\';',
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
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();
641 $retval = $error->getDisplay();
649 * Returns the phpMyAdmin version to be appended to the url to avoid caching
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,