phpmyadmin/src/Header.php
Maximilian Krög 76be859196
Restrict map tile url to https protocol
Signed-off-by: Maximilian Krög <maxi_kroeg@web.de>
2026-03-08 03:23:08 +01:00

480 lines
16 KiB
PHP

<?php
/**
* Used to render the header of PMA's pages
*/
declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\Clock\Clock;
use PhpMyAdmin\Config\UserPreferences;
use PhpMyAdmin\Config\UserPreferencesHandler;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\Console\Console;
use PhpMyAdmin\Container\ContainerBuilder;
use PhpMyAdmin\Dbal\DatabaseInterface;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\I18n\LanguageManager;
use PhpMyAdmin\Navigation\Navigation;
use PhpMyAdmin\Theme\ThemeManager;
use Psr\Clock\ClockInterface;
use function array_merge;
use function htmlspecialchars;
use function implode;
use function ini_get;
use function json_encode;
use const JSON_HEX_TAG;
/**
* Class used to output the HTTP and HTML headers
*/
class Header
{
private Scripts|null $scripts = null;
private Menu|null $menu = null;
/**
* The page title
*/
private string $title = '';
/**
* The value for the id attribute for the body tag
*/
private string $bodyId = '';
/** Whether to show the top menu */
private bool|null $isMenuEnabled = null;
/**
* Whether to show the warnings
*/
private bool $warningsEnabled = true;
private bool $isTransformationWrapper = false;
public function __construct(
private readonly Template $template,
private readonly Console $console,
private readonly Config $config,
private readonly DatabaseInterface $dbi,
private readonly Relation $relation,
private readonly UserPreferences $userPreferences,
private readonly UserPreferencesHandler $userPreferencesHandler,
) {
}
private function isMenuEnabled(): bool
{
if ($this->isMenuEnabled !== null) {
return $this->isMenuEnabled;
}
$this->isMenuEnabled = $this->dbi->isConnected();
return $this->isMenuEnabled;
}
public function getScripts(): Scripts
{
if ($this->scripts !== null) {
return $this->scripts;
}
$this->scripts = new Scripts($this->template);
$this->scripts->addFile('runtime.js');
$this->scripts->addFile('vendor/jquery/jquery.min.js');
$this->scripts->addFile('vendor/jquery/jquery-migrate.min.js');
$this->scripts->addFile('vendor/sprintf.js');
$this->scripts->addFile('vendor/jquery/jquery-ui.min.js');
$this->scripts->addFile('vendor/bootstrap/bootstrap.js');
$this->scripts->addFile('vendor/js.cookie.min.js');
$this->scripts->addFile('vendor/jquery/jquery.validate.min.js');
$this->scripts->addFile('vendor/jquery/jquery-ui-timepicker-addon.min.js');
$this->scripts->addFile('index.php', ['route' => '/messages', 'l' => Current::$lang]);
$this->scripts->addFile('shared.js');
$this->scripts->addFile('menu_resizer.js');
$this->scripts->addFile('main.js');
$this->scripts->addCode($this->getJsParamsCode());
return $this->scripts;
}
/**
* Returns, as an array, a list of parameters
* used on the client side
*
* @return mixed[]
*/
public function getJsParams(): array
{
$pftext = $_SESSION['tmpval']['pftext'] ?? '';
$params = [
// Do not add any separator, JS code will decide
'common_query' => Url::getCommonRaw([], ''),
'opendb_url' => Url::getFromRoute($this->config->config->DefaultTabDatabase),
'lang' => Current::$lang,
'server' => Current::$server,
'table' => Current::$table,
'db' => Current::$database,
'token' => Session::getToken(),
'text_dir' => LanguageManager::$textDirection->value,
'LimitChars' => $this->config->config->limitChars,
'pftext' => $pftext,
'confirm' => $this->config->config->Confirm,
'LoginCookieValidity' => $this->config->config->LoginCookieValidity,
'session_gc_maxlifetime' => (int) ini_get('session.gc_maxlifetime'),
'logged_in' => $this->dbi->isConnected(),
'is_https' => $this->config->isHttps(),
'rootPath' => $this->config->getRootPath(),
'arg_separator' => Url::getArgSeparator(),
'version' => Version::VERSION,
];
if ($this->config->hasSelectedServer()) {
$params['auth_type'] = $this->config->selectedServer['auth_type'];
if (isset($this->config->selectedServer['user'])) {
$params['user'] = $this->config->selectedServer['user'];
}
}
return $params;
}
/**
* Returns, as a string, a list of parameters
* used on the client side
*/
public function getJsParamsCode(): string
{
$params = $this->getJsParams();
return 'window.Navigation.update(window.CommonParams.setAll(' . json_encode($params, JSON_HEX_TAG) . '));';
}
public function getMenu(): Menu
{
if ($this->menu !== null) {
return $this->menu;
}
$this->menu = new Menu(
$this->dbi,
$this->template,
$this->config,
$this->relation,
Current::$database,
Current::$table,
);
return $this->menu;
}
/**
* Setter for the ID attribute in the BODY tag
*
* @param string $id Value for the ID attribute
*/
public function setBodyId(string $id): void
{
$this->bodyId = htmlspecialchars($id);
}
/**
* Setter for the title of the page
*
* @param string $title New title
*/
public function setTitle(string $title): void
{
$this->title = htmlspecialchars($title);
}
/**
* Disables the display of the top menu
*/
public function disableMenuAndConsole(): void
{
$this->isMenuEnabled = false;
$this->console->disable();
}
/**
* Disables the display of the top menu
*/
public function disableWarnings(): void
{
$this->warningsEnabled = false;
}
/** @return array<string, mixed> */
public function getDisplay(ResponseRenderer $responseRenderer): array
{
$themeManager = ContainerBuilder::getContainer()->get(ThemeManager::class);
$theme = $themeManager->theme;
$scripts = $this->getScripts();
$userAgent = Core::getEnv('HTTP_USER_AGENT');
// The user preferences have been merged at this point
// so we can conditionally add CodeMirror, other scripts and settings
// See #20159, why we need to check for HTTP_USER_AGENT here
if ($this->config->config->CodemirrorEnable && $userAgent !== '') {
$scripts->addFile('vendor/codemirror/lib/codemirror.js');
$scripts->addFile('vendor/codemirror/mode/sql/sql.js');
$scripts->addFile('vendor/codemirror/addon/runmode/runmode.js');
$scripts->addFile('vendor/codemirror/addon/hint/show-hint.js');
$scripts->addFile('vendor/codemirror/addon/hint/sql-hint.js');
if ($this->config->config->LintEnable) {
$scripts->addFile('vendor/codemirror/addon/lint/lint.js');
$scripts->addFile('codemirror/addon/lint/sql-lint.js');
}
}
if ($this->config->config->SendErrorReports !== 'never') {
$scripts->addFile('vendor/tracekit.js');
$scripts->addFile('error_report.js');
}
if ($this->config->config->enable_drag_drop_import) {
$scripts->addFile('drag_drop_import.js');
}
if (! $this->config->config->DisableShortcutKeys) {
$scripts->addFile('shortcuts_handler.js');
}
$scripts->addCode($this->getVariablesForJavaScript());
$scripts->addCode('ConsoleEnterExecutes=' . ($this->config->config->ConsoleEnterExecutes ? 'true' : 'false'));
$scripts->addFiles($this->console->getScripts());
if ($this->isMenuEnabled() && Current::$server > 0) {
$navigation = (new Navigation($this->template, $this->relation, $this->dbi, $this->config))
->getDisplay($responseRenderer);
}
$customHeader = self::renderHeader();
// offer to load user preferences from localStorage
if (
$this->userPreferencesHandler->storageType === 'session'
&& ! isset($_SESSION['userprefs_autoload'])
) {
$loadUserPreferences = $this->userPreferences->autoloadGetHeader();
}
$menu = '';
if ($this->isMenuEnabled() && Current::$server > 0) {
$menu = $this->getMenu()->getDisplay();
}
$console = $this->console->getDisplay();
$messages = $this->getMessage();
$isLoggedIn = $this->dbi->isConnected();
$scripts->addFile('datetimepicker.js');
$scripts->addFile('validator-messages.js');
return [
'lang' => Current::$lang,
'allow_third_party_framing' => $this->config->config->AllowThirdPartyFraming,
'codemirror_enable' => $this->config->config->CodemirrorEnable,
'lint_enable' => $this->config->config->LintEnable,
'theme_path' => $theme->getPath(),
'server' => Current::$server,
'title' => $this->getPageTitle(),
'scripts' => $scripts->getDisplay(),
'body_id' => $this->bodyId,
'navigation' => $navigation ?? '',
'custom_header' => $customHeader,
'load_user_preferences' => $loadUserPreferences ?? '',
'show_hint' => $this->config->config->ShowHint,
'is_warnings_enabled' => $this->warningsEnabled,
'is_menu_enabled' => $this->isMenuEnabled(),
'is_logged_in' => $isLoggedIn,
'menu' => $menu,
'console' => $console,
'messages' => $messages,
'theme_color_mode' => $theme->getColorMode(),
'theme_color_modes' => $theme->getColorModes(),
'theme_id' => $theme->getId(),
'current_user' => $this->dbi->getCurrentUserAndHost(),
'is_mariadb' => $this->dbi->isMariaDB(),
];
}
/**
* Returns the message to be displayed at the top of
* the page, including the executed SQL query, if any.
*/
public function getMessage(): string
{
$retval = '';
$message = '';
if (Current::$message !== null) {
$message = Current::$message;
Current::$message = null;
} elseif (! empty($_REQUEST['message'])) {
$message = $_REQUEST['message'];
}
if ($message !== '') {
$retval .= Generator::getMessage($message);
}
return $retval;
}
/** @return array<string, string> */
public function getHttpHeaders(ClockInterface|null $clock = null): array
{
$headers = [
'Referrer-Policy' => 'same-origin',
'Content-Security-Policy' => $this->getCspHeader(),
/**
* Re-enable possible disabled XSS filters.
*
* @see https://developer.mozilla.org/docs/Web/HTTP/Headers/X-XSS-Protection
*/
'X-XSS-Protection' => '1; mode=block',
/**
* "nosniff", prevents Internet Explorer and Google Chrome from MIME-sniffing
* a response away from the declared content-type.
*
* @see https://developer.mozilla.org/docs/Web/HTTP/Headers/X-Content-Type-Options
*/
'X-Content-Type-Options' => 'nosniff',
/**
* Adobe cross-domain-policies.
*
* @see https://www.sentrium.co.uk/labs/application-security-101-http-headers
*/
'X-Permitted-Cross-Domain-Policies' => 'none',
/**
* Robots meta tag.
*
* @see https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag
*/
'X-Robots-Tag' => 'noindex, nofollow',
/**
* The HTTP Permissions-Policy header provides a mechanism to allow and deny
* the use of browser features in a document
* or within any <iframe> elements in the document.
*
* @see https://developer.mozilla.org/docs/Web/HTTP/Headers/Permissions-Policy
*/
'Permissions-Policy' => 'fullscreen=(self), interest-cohort=()',
];
$headers = array_merge($headers, Core::getNoCacheHeaders($clock ?? new Clock()));
/**
* A different Content-Type is set in {@see \PhpMyAdmin\Controllers\Transformation\WrapperController}.
*/
if (! $this->isTransformationWrapper) {
// Define the charset to be used
$headers['Content-Type'] = 'text/html; charset=utf-8';
}
return $headers;
}
/**
* If the page is missing the title, this function
* will set it to something reasonable
*/
public function getPageTitle(): string
{
if ($this->title === '') {
if (Current::$server > 0) {
if (Current::$table !== '') {
$tempTitle = $this->config->config->TitleTable;
} elseif (Current::$database !== '') {
$tempTitle = $this->config->config->TitleDatabase;
} elseif ($this->config->selectedServer['host'] !== '') {
$tempTitle = $this->config->config->TitleServer;
} else {
$tempTitle = $this->config->config->TitleDefault;
}
$this->title = htmlspecialchars(Util::expandUserString($this->dbi, $this->config, $tempTitle));
} else {
$this->title = 'phpMyAdmin';
}
}
return $this->title;
}
/** Get the Content-Security-Policy header */
private function getCspHeader(): string
{
$mapTileUrl = ' https://tile.openstreetmap.org';
$cspAllow = $this->config->config->CSPAllow === '' ? '' : ' ' . $this->config->config->CSPAllow;
$captchaUrl =
$this->config->config->CaptchaLoginPrivateKey === '' ||
$this->config->config->CaptchaLoginPublicKey === '' ||
$this->config->config->CaptchaApi === '' ||
$this->config->config->CaptchaRequestParam === '' ||
$this->config->config->CaptchaResponseParam === ''
? ''
: ' ' . $this->config->config->CaptchaCsp;
$csp = [
"default-src 'self'" . $captchaUrl . $cspAllow,
"img-src 'self' data:" . $captchaUrl . $cspAllow . $mapTileUrl,
"object-src 'none'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'" . $captchaUrl . $cspAllow,
"style-src 'self' 'unsafe-inline'" . $captchaUrl . $cspAllow,
];
// Prevent click-jacking by disabling inline-framing
if ($this->config->config->AllowThirdPartyFraming === 'sameorigin') {
$csp[] = "frame-ancestors 'self'";
} elseif ($this->config->config->AllowThirdPartyFraming !== true) {
$csp[] = "frame-ancestors 'none'";
}
return implode('; ', $csp) . ';';
}
private function getVariablesForJavaScript(): string
{
$maxInputVars = ini_get('max_input_vars');
$maxInputVarsValue = $maxInputVars === false || $maxInputVars === '' ? 'false' : (int) $maxInputVars;
return $this->template->render('javascript/variables', [
'first_day_of_calendar' => $this->config->config->FirstDayOfCalendar,
'max_input_vars' => $maxInputVarsValue,
]);
}
public function setIsTransformationWrapper(bool $isTransformationWrapper): void
{
$this->isTransformationWrapper = $isTransformationWrapper;
}
public function getConsole(): Console
{
return $this->console;
}
/**
* Renders user configured footer
*/
public static function renderHeader(): string
{
return Generator::renderCustom(CUSTOM_HEADER_FILE, 'pma_header');
}
}