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
;
13 use PhpMyAdmin\Theme\ThemeManager
;
15 use function array_merge
;
19 use function htmlspecialchars
;
21 use function json_encode
;
24 use function strtolower
;
25 use function urlencode
;
27 use const JSON_HEX_TAG
;
30 * Class used to output the HTTP and HTML headers
37 private Scripts
$scripts;
39 * PhpMyAdmin\Console instance
41 private Console
$console;
49 private string $title = '';
51 * The value for the id attribute for the body tag
53 private string $bodyId = '';
55 * Whether to show the top menu
57 private bool $menuEnabled;
59 * Whether to show the warnings
61 private bool $warningsEnabled = true;
63 * Whether we are servicing an ajax request.
65 private bool $isAjax = false;
67 * Whether to display anything
69 private bool $isEnabled = true;
71 * Whether the HTTP headers (and possibly some HTML)
72 * have already been sent to the browser
74 private bool $headerIsSent = false;
76 private UserPreferences
$userPreferences;
78 private bool $isTransformationWrapper = false;
81 * Creates a new class instance
83 public function __construct(private readonly Template
$template, Console
$console)
85 $dbi = DatabaseInterface
::getInstance();
86 $this->console
= $console;
87 $this->menuEnabled
= $dbi->isConnected();
88 $this->menu
= new Menu($dbi, $this->template
, $GLOBALS['db'] ??
'', $GLOBALS['table'] ??
'');
89 $this->scripts
= new Scripts($this->template
);
90 $this->addDefaultScripts();
92 $this->userPreferences
= new UserPreferences($dbi, new Relation($dbi), $this->template
);
96 * Loads common scripts
98 private function addDefaultScripts(): void
100 $this->scripts
->addFile('runtime.js');
101 $this->scripts
->addFile('vendor/jquery/jquery.min.js');
102 $this->scripts
->addFile('vendor/jquery/jquery-migrate.min.js');
103 $this->scripts
->addFile('vendor/sprintf.js');
104 $this->scripts
->addFile('vendor/jquery/jquery-ui.min.js');
105 $this->scripts
->addFile('name-conflict-fixes.js');
106 $this->scripts
->addFile('vendor/bootstrap/bootstrap.bundle.min.js');
107 $this->scripts
->addFile('vendor/js.cookie.min.js');
108 $this->scripts
->addFile('vendor/jquery/jquery.validate.min.js');
109 $this->scripts
->addFile('vendor/jquery/jquery-ui-timepicker-addon.js');
110 $this->scripts
->addFile('index.php', ['route' => '/messages', 'l' => $GLOBALS['lang']]);
111 $this->scripts
->addFile('shared.js');
112 $this->scripts
->addFile('menu_resizer.js');
113 $this->scripts
->addFile('main.js');
115 $this->scripts
->addCode($this->getJsParamsCode());
119 * Returns, as an array, a list of parameters
120 * used on the client side
124 public function getJsParams(): array
126 $pftext = $_SESSION['tmpval']['pftext'] ??
'';
128 $config = Config
::getInstance();
130 // Do not add any separator, JS code will decide
131 'common_query' => Url
::getCommonRaw([], ''),
132 'opendb_url' => Util
::getScriptNameForOption($config->settings
['DefaultTabDatabase'], 'database'),
133 'lang' => $GLOBALS['lang'],
134 'server' => $GLOBALS['server'],
135 'table' => $GLOBALS['table'] ??
'',
136 'db' => $GLOBALS['db'] ??
'',
137 'token' => $_SESSION[' PMA_token '],
138 'text_dir' => $GLOBALS['text_dir'],
139 'LimitChars' => $config->settings
['LimitChars'],
141 'confirm' => $config->settings
['Confirm'],
142 'LoginCookieValidity' => $config->settings
['LoginCookieValidity'],
143 'session_gc_maxlifetime' => (int) ini_get('session.gc_maxlifetime'),
144 'logged_in' => DatabaseInterface
::getInstance()->isConnected(),
145 'is_https' => $config->isHttps(),
146 'rootPath' => $config->getRootPath(),
147 'arg_separator' => Url
::getArgSeparator(),
148 'version' => Version
::VERSION
,
150 if ($config->hasSelectedServer()) {
151 $params['auth_type'] = $config->selectedServer
['auth_type'];
152 if (isset($config->selectedServer
['user'])) {
153 $params['user'] = $config->selectedServer
['user'];
161 * Returns, as a string, a list of parameters
162 * used on the client side
164 public function getJsParamsCode(): string
166 $params = $this->getJsParams();
168 return 'window.Navigation.update(window.CommonParams.setAll(' . json_encode($params, JSON_HEX_TAG
) . '));';
172 * Disables the rendering of the header
174 public function disable(): void
176 $this->isEnabled
= false;
180 * Set the ajax flag to indicate whether
181 * we are servicing an ajax request
183 * @param bool $isAjax Whether we are servicing an ajax request
185 public function setAjax(bool $isAjax): void
187 $this->isAjax
= $isAjax;
188 $this->console
->setAjax($isAjax);
192 * Returns the Scripts object
194 * @return Scripts object
196 public function getScripts(): Scripts
198 return $this->scripts
;
202 * Returns the Menu object
204 * @return Menu object
206 public function getMenu(): Menu
212 * Setter for the ID attribute in the BODY tag
214 * @param string $id Value for the ID attribute
216 public function setBodyId(string $id): void
218 $this->bodyId
= htmlspecialchars($id);
222 * Setter for the title of the page
224 * @param string $title New title
226 public function setTitle(string $title): void
228 $this->title
= htmlspecialchars($title);
232 * Disables the display of the top menu
234 public function disableMenuAndConsole(): void
236 $this->menuEnabled
= false;
237 $this->console
->disable();
241 * Disables the display of the top menu
243 public function disableWarnings(): void
245 $this->warningsEnabled
= false;
249 * Generates the header
251 * @return string The header
253 public function getDisplay(): string
255 if ($this->headerIsSent ||
! $this->isEnabled
) {
260 if (empty($_REQUEST['recent_table'])) {
261 $recentTable = $this->addRecentTable($GLOBALS['db'], $GLOBALS['table']);
268 $this->sendHttpHeaders();
270 $baseDir = defined('PMA_PATH_TO_BASEDIR') ? PMA_PATH_TO_BASEDIR
: '';
272 /** @var ThemeManager $themeManager */
273 $themeManager = Core
::getContainerBuilder()->get(ThemeManager
::class);
274 $theme = $themeManager->theme
;
276 $version = self
::getVersionParameter();
278 // The user preferences have been merged at this point
279 // so we can conditionally add CodeMirror, other scripts and settings
280 $config = Config
::getInstance();
281 if ($config->settings
['CodemirrorEnable']) {
282 $this->scripts
->addFile('vendor/codemirror/lib/codemirror.js');
283 $this->scripts
->addFile('vendor/codemirror/mode/sql/sql.js');
284 $this->scripts
->addFile('vendor/codemirror/addon/runmode/runmode.js');
285 $this->scripts
->addFile('vendor/codemirror/addon/hint/show-hint.js');
286 $this->scripts
->addFile('vendor/codemirror/addon/hint/sql-hint.js');
287 if ($config->settings
['LintEnable']) {
288 $this->scripts
->addFile('vendor/codemirror/addon/lint/lint.js');
289 $this->scripts
->addFile('codemirror/addon/lint/sql-lint.js');
293 if ($config->settings
['SendErrorReports'] !== 'never') {
294 $this->scripts
->addFile('vendor/tracekit.js');
295 $this->scripts
->addFile('error_report.js');
298 if ($config->settings
['enable_drag_drop_import'] === true) {
299 $this->scripts
->addFile('drag_drop_import.js');
302 if (! $config->get('DisableShortcutKeys')) {
303 $this->scripts
->addFile('shortcuts_handler.js');
306 $this->scripts
->addCode($this->getVariablesForJavaScript());
308 $this->scripts
->addCode(
309 'ConsoleEnterExecutes=' . ($config->settings
['ConsoleEnterExecutes'] ?
'true' : 'false'),
311 $this->scripts
->addFiles($this->console
->getScripts());
313 $dbi = DatabaseInterface
::getInstance();
314 if ($this->menuEnabled
&& $GLOBALS['server'] > 0) {
315 $nav = new Navigation(
320 $navigation = $nav->getDisplay();
323 $customHeader = Config
::renderHeader();
325 // offer to load user preferences from localStorage
327 $config->get('user_preferences') === 'session'
328 && ! isset($_SESSION['userprefs_autoload'])
330 $loadUserPreferences = $this->userPreferences
->autoloadGetHeader();
333 if ($this->menuEnabled
&& $GLOBALS['server'] > 0) {
334 $menu = $this->menu
->getDisplay();
337 $console = $this->console
->getDisplay();
338 $messages = $this->getMessage();
339 $isLoggedIn = $dbi->isConnected();
341 $this->scripts
->addFile('datetimepicker.js');
342 $this->scripts
->addFile('validator-messages.js');
344 return $this->template
->render('header', [
345 'lang' => $GLOBALS['lang'],
346 'allow_third_party_framing' => $config->settings
['AllowThirdPartyFraming'],
347 'base_dir' => $baseDir,
348 'theme_path' => $theme->getPath(),
349 'version' => $version,
350 'text_dir' => $GLOBALS['text_dir'],
351 'server' => $GLOBALS['server'] ??
null,
352 'title' => $this->getPageTitle(),
353 'scripts' => $this->scripts
->getDisplay(),
354 'body_id' => $this->bodyId
,
355 'navigation' => $navigation ??
'',
356 'custom_header' => $customHeader,
357 'load_user_preferences' => $loadUserPreferences ??
'',
358 'show_hint' => $config->settings
['ShowHint'],
359 'is_warnings_enabled' => $this->warningsEnabled
,
360 'is_menu_enabled' => $this->menuEnabled
,
361 'is_logged_in' => $isLoggedIn,
362 'menu' => $menu ??
'',
363 'console' => $console,
364 'messages' => $messages,
365 'recent_table' => $recentTable,
366 'theme_color_mode' => $theme->getColorMode(),
367 'theme_color_modes' => $theme->getColorModes(),
368 'theme_id' => $theme->getId(),
369 'current_user' => $dbi->getCurrentUserAndHost(),
370 'is_mariadb' => $dbi->isMariaDB(),
375 * Returns the message to be displayed at the top of
376 * the page, including the executed SQL query, if any.
378 public function getMessage(): string
382 if (! empty($GLOBALS['message'])) {
383 $message = $GLOBALS['message'];
384 unset($GLOBALS['message']);
385 } elseif (! empty($_REQUEST['message'])) {
386 $message = $_REQUEST['message'];
389 if (! empty($message)) {
390 if (isset($GLOBALS['buffer_message'])) {
391 $bufferMessage = $GLOBALS['buffer_message'];
394 $retval .= Generator
::getMessage($message);
395 if (isset($bufferMessage)) {
396 $GLOBALS['buffer_message'] = $bufferMessage;
404 * Sends out the HTTP headers
406 public function sendHttpHeaders(): void
408 if (defined('TESTSUITE')) {
415 $GLOBALS['now'] = gmdate('D, d M Y H:i:s') . ' GMT';
417 $headers = $this->getHttpHeaders();
419 foreach ($headers as $name => $value) {
420 header(sprintf('%s: %s', $name, $value));
423 $this->headerIsSent
= true;
426 /** @return array<string, string> */
427 public function getHttpHeaders(): array
431 /* Prevent against ClickJacking by disabling framing */
432 $config = Config
::getInstance();
433 if (strtolower((string) $config->settings
['AllowThirdPartyFraming']) === 'sameorigin') {
434 $headers['X-Frame-Options'] = 'SAMEORIGIN';
435 } elseif ($config->settings
['AllowThirdPartyFraming'] !== true) {
436 $headers['X-Frame-Options'] = 'DENY';
439 $headers['Referrer-Policy'] = 'no-referrer';
441 $headers = array_merge($headers, $this->getCspHeaders());
444 * Re-enable possible disabled XSS filters.
446 * @see https://developer.mozilla.org/docs/Web/HTTP/Headers/X-XSS-Protection
448 $headers['X-XSS-Protection'] = '1; mode=block';
451 * "nosniff", prevents Internet Explorer and Google Chrome from MIME-sniffing
452 * a response away from the declared content-type.
454 * @see https://developer.mozilla.org/docs/Web/HTTP/Headers/X-Content-Type-Options
456 $headers['X-Content-Type-Options'] = 'nosniff';
459 * Adobe cross-domain-policies.
461 * @see https://www.sentrium.co.uk/labs/application-security-101-http-headers
463 $headers['X-Permitted-Cross-Domain-Policies'] = 'none';
468 * @see https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag
470 $headers['X-Robots-Tag'] = 'noindex, nofollow';
473 * The HTTP Permissions-Policy header provides a mechanism to allow and deny
474 * the use of browser features in a document
475 * or within any <iframe> elements in the document.
477 * @see https://developer.mozilla.org/docs/Web/HTTP/Headers/Permissions-Policy
479 $headers['Permissions-Policy'] = 'fullscreen=(self), oversized-images=(self), interest-cohort=()';
481 $headers = array_merge($headers, Core
::getNoCacheHeaders());
484 * A different Content-Type is set in {@see \PhpMyAdmin\Controllers\Transformation\WrapperController}.
486 if (! $this->isTransformationWrapper
) {
487 // Define the charset to be used
488 $headers['Content-Type'] = 'text/html; charset=utf-8';
495 * If the page is missing the title, this function
496 * will set it to something reasonable
498 public function getPageTitle(): string
500 if (strlen($this->title
) == 0) {
501 if ($GLOBALS['server'] > 0) {
502 $config = Config
::getInstance();
503 if ($GLOBALS['table'] !== '') {
504 $tempTitle = $config->settings
['TitleTable'];
505 } elseif ($GLOBALS['db'] !== '') {
506 $tempTitle = $config->settings
['TitleDatabase'];
507 } elseif ($config->selectedServer
['host'] !== '') {
508 $tempTitle = $config->settings
['TitleServer'];
510 $tempTitle = $config->settings
['TitleDefault'];
513 $this->title
= htmlspecialchars(
514 Util
::expandUserString($tempTitle),
517 $this->title
= 'phpMyAdmin';
525 * Get all the CSP allow policy headers
527 * @return array<string, string>
529 private function getCspHeaders(): array
531 $mapTileUrl = ' tile.openstreetmap.org';
533 $config = Config
::getInstance();
534 $cspAllow = $config->settings
['CSPAllow'];
537 ! empty($config->settings
['CaptchaLoginPrivateKey'])
538 && ! empty($config->settings
['CaptchaLoginPublicKey'])
539 && ! empty($config->settings
['CaptchaApi'])
540 && ! empty($config->settings
['CaptchaRequestParam'])
541 && ! empty($config->settings
['CaptchaResponseParam'])
543 $captchaUrl = ' ' . $config->settings
['CaptchaCsp'] . ' ';
548 $headers['Content-Security-Policy'] = sprintf(
549 'default-src \'self\' %s%s;script-src \'self\' \'unsafe-inline\' \'unsafe-eval\' %s%s;'
550 . 'style-src \'self\' \'unsafe-inline\' %s%s;img-src \'self\' data: %s%s%s;object-src \'none\';',
562 $headers['X-Content-Security-Policy'] = sprintf(
563 'default-src \'self\' %s%s;options inline-script eval-script;'
564 . 'referrer no-referrer;img-src \'self\' data: %s%s%s;object-src \'none\';',
572 $headers['X-WebKit-CSP'] = sprintf(
573 'default-src \'self\' %s%s;script-src \'self\' %s%s \'unsafe-inline\' \'unsafe-eval\';'
574 . 'referrer no-referrer;style-src \'self\' \'unsafe-inline\' %s;'
575 . 'img-src \'self\' data: %s%s%s;object-src \'none\';',
590 * Add recently used table and reload the navigation.
592 * @param string $db Database name where the table is located.
593 * @param string $table The table name
595 private function addRecentTable(string $db, string $table): string
597 if ($this->menuEnabled
&& $table !== '' && Config
::getInstance()->settings
['NumRecentTables'] > 0) {
598 $error = RecentFavoriteTable
::getInstance('recent')->add($db, $table);
599 if ($error === true) {
600 return RecentFavoriteTable
::getHtmlUpdateRecentTables();
603 return $error->getDisplay();
610 * Returns the phpMyAdmin version to be appended to the url to avoid caching
613 * @return string urlencoded pma version as a parameter
615 public static function getVersionParameter(): string
617 return 'v=' . urlencode(Version
::VERSION
);
620 private function getVariablesForJavaScript(): string
622 $maxInputVars = ini_get('max_input_vars');
623 $maxInputVarsValue = $maxInputVars === false ||
$maxInputVars === '' ?
'false' : (int) $maxInputVars;
625 return $this->template
->render('javascript/variables', [
626 'first_day_of_calendar' => Config
::getInstance()->settings
['FirstDayOfCalendar'] ??
0,
627 'max_input_vars' => $maxInputVarsValue,
631 public function setIsTransformationWrapper(bool $isTransformationWrapper): void
633 $this->isTransformationWrapper
= $isTransformationWrapper;