| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680 | <?php/** * Used to render the header of PMA's pages */declare(strict_types=1);namespace PhpMyAdmin;use PhpMyAdmin\ConfigStorage\Relation;use PhpMyAdmin\Html\Generator;use PhpMyAdmin\Navigation\Navigation;use function array_merge;use function defined;use function gmdate;use function header;use function htmlspecialchars;use function implode;use function ini_get;use function is_bool;use function sprintf;use function strlen;use function strtolower;use function urlencode;/** * Class used to output the HTTP and HTML headers */class Header{    /**     * Scripts instance     *     * @var Scripts     */    private $scripts;    /**     * PhpMyAdmin\Console instance     *     * @var Console     */    private $console;    /**     * Menu instance     *     * @var Menu     */    private $menu;    /**     * The page title     *     * @var string     */    private $title;    /**     * The value for the id attribute for the body tag     *     * @var string     */    private $bodyId;    /**     * Whether to show the top menu     *     * @var bool     */    private $menuEnabled;    /**     * Whether to show the warnings     *     * @var bool     */    private $warningsEnabled;    /**     * Whether we are servicing an ajax request.     *     * @var bool     */    private $isAjax = false;    /**     * Whether to display anything     *     * @var bool     */    private $isEnabled;    /**     * Whether the HTTP headers (and possibly some HTML)     * have already been sent to the browser     *     * @var bool     */    private $headerIsSent;    /** @var UserPreferences */    private $userPreferences;    /** @var Template */    private $template;    /**     * Creates a new class instance     */    public function __construct()    {        global $db, $table, $dbi;        $this->template = new Template();        $this->isEnabled = true;        $this->bodyId = '';        $this->title = '';        $this->console = new Console();        $this->menuEnabled = false;        if ($dbi !== null) {            $this->menuEnabled = true;            $this->menu = new Menu($dbi, $db ?? '', $table ?? '');        }        $this->warningsEnabled = true;        $this->scripts = new Scripts();        $this->addDefaultScripts();        $this->headerIsSent = false;        $this->userPreferences = new UserPreferences();    }    /**     * Loads common scripts     */    private function addDefaultScripts(): void    {        // Localised strings        $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('ajax.js');        $this->scripts->addFile('keyhandler.js');        $this->scripts->addFile('vendor/jquery/jquery-ui.min.js');        $this->scripts->addFile('name-conflict-fixes.js');        $this->scripts->addFile('vendor/bootstrap/bootstrap.bundle.min.js');        $this->scripts->addFile('vendor/js.cookie.js');        $this->scripts->addFile('vendor/jquery/jquery.validate.min.js');        $this->scripts->addFile('vendor/jquery/jquery-ui-timepicker-addon.js');        $this->scripts->addFile('vendor/jquery/jquery.debounce-1.0.6.js');        $this->scripts->addFile('menu_resizer.js');        // Cross-framing protection        // At this point browser settings are not merged        // this is good that we only use file configuration for this protection        if ($GLOBALS['cfg']['AllowThirdPartyFraming'] === false) {            $this->scripts->addFile('cross_framing_protection.js');        }        // Here would not be a good place to add CodeMirror because        // the user preferences have not been merged at this point        $this->scripts->addFile('messages.php', ['l' => $GLOBALS['lang']]);        $this->scripts->addFile('config.js');        $this->scripts->addFile('doclinks.js');        $this->scripts->addFile('functions.js');        $this->scripts->addFile('navigation.js');        $this->scripts->addFile('indexes.js');        $this->scripts->addFile('common.js');        $this->scripts->addFile('page_settings.js');        $this->scripts->addCode($this->getJsParamsCode());    }    /**     * Returns, as an array, a list of parameters     * used on the client side     *     * @return array     */    public function getJsParams(): array    {        global $db, $table, $dbi;        $pftext = $_SESSION['tmpval']['pftext'] ?? '';        $params = [            // Do not add any separator, JS code will decide            'common_query' => Url::getCommonRaw([], ''),            'opendb_url' => Util::getScriptNameForOption($GLOBALS['cfg']['DefaultTabDatabase'], 'database'),            'lang' => $GLOBALS['lang'],            'server' => $GLOBALS['server'],            'table' => $table ?? '',            'db' => $db ?? '',            'token' => $_SESSION[' PMA_token '],            'text_dir' => $GLOBALS['text_dir'],            'LimitChars' => $GLOBALS['cfg']['LimitChars'],            'pftext' => $pftext,            'confirm' => $GLOBALS['cfg']['Confirm'],            'LoginCookieValidity' => $GLOBALS['cfg']['LoginCookieValidity'],            'session_gc_maxlifetime' => (int) ini_get('session.gc_maxlifetime'),            'logged_in' => isset($dbi) ? $dbi->isConnected() : false,            'is_https' => $GLOBALS['config'] !== null && $GLOBALS['config']->isHttps(),            'rootPath' => $GLOBALS['config'] !== null && $GLOBALS['config']->getRootPath(),            'arg_separator' => Url::getArgSeparator(),            'version' => Version::VERSION,        ];        if (isset($GLOBALS['cfg']['Server'], $GLOBALS['cfg']['Server']['auth_type'])) {            $params['auth_type'] = $GLOBALS['cfg']['Server']['auth_type'];            if (isset($GLOBALS['cfg']['Server']['user'])) {                $params['user'] = $GLOBALS['cfg']['Server']['user'];            }        }        return $params;    }    /**     * Returns, as a string, a list of parameters     * used on the client side     */    public function getJsParamsCode(): string    {        $params = $this->getJsParams();        foreach ($params as $key => $value) {            if (is_bool($value)) {                $params[$key] = $key . ':' . ($value ? 'true' : 'false') . '';            } else {                $params[$key] = $key . ':"' . Sanitize::escapeJsString($value) . '"';            }        }        return 'CommonParams.setAll({' . implode(',', $params) . '});';    }    /**     * Disables the rendering of the header     */    public function disable(): void    {        $this->isEnabled = false;    }    /**     * Set the ajax flag to indicate whether     * we are servicing an ajax request     *     * @param bool $isAjax Whether we are servicing an ajax request     */    public function setAjax(bool $isAjax): void    {        $this->isAjax = $isAjax;        $this->console->setAjax($isAjax);    }    /**     * Returns the Scripts object     *     * @return Scripts object     */    public function getScripts(): Scripts    {        return $this->scripts;    }    /**     * Returns the Menu object     *     * @return Menu object     */    public function getMenu(): Menu    {        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->menuEnabled = false;        $this->console->disable();    }    /**     * Disables the display of the top menu     */    public function disableWarnings(): void    {        $this->warningsEnabled = false;    }    /**     * Generates the header     *     * @return string The header     */    public function getDisplay(): string    {        global $db, $table, $theme, $dbi;        if ($this->headerIsSent || ! $this->isEnabled) {            return '';        }        $recentTable = '';        if (empty($_REQUEST['recent_table'])) {            $recentTable = $this->addRecentTable($db, $table);        }        if ($this->isAjax) {            return $recentTable;        }        $this->sendHttpHeaders();        $baseDir = defined('PMA_PATH_TO_BASEDIR') ? PMA_PATH_TO_BASEDIR : '';        $themePath = $theme instanceof Theme ? $theme->getPath() : '';        $version = self::getVersionParameter();        // The user preferences have been merged at this point        // so we can conditionally add CodeMirror, other scripts and settings        if ($GLOBALS['cfg']['CodemirrorEnable']) {            $this->scripts->addFile('vendor/codemirror/lib/codemirror.js');            $this->scripts->addFile('vendor/codemirror/mode/sql/sql.js');            $this->scripts->addFile('vendor/codemirror/addon/runmode/runmode.js');            $this->scripts->addFile('vendor/codemirror/addon/hint/show-hint.js');            $this->scripts->addFile('vendor/codemirror/addon/hint/sql-hint.js');            if ($GLOBALS['cfg']['LintEnable']) {                $this->scripts->addFile('vendor/codemirror/addon/lint/lint.js');                $this->scripts->addFile('codemirror/addon/lint/sql-lint.js');            }        }        if ($GLOBALS['cfg']['SendErrorReports'] !== 'never') {            $this->scripts->addFile('vendor/tracekit.js');            $this->scripts->addFile('error_report.js');        }        if ($GLOBALS['cfg']['enable_drag_drop_import'] === true) {            $this->scripts->addFile('drag_drop_import.js');        }        if (! $GLOBALS['config']->get('DisableShortcutKeys')) {            $this->scripts->addFile('shortcuts_handler.js');        }        $this->scripts->addCode($this->getVariablesForJavaScript());        $this->scripts->addCode('ConsoleEnterExecutes=' . ($GLOBALS['cfg']['ConsoleEnterExecutes'] ? 'true' : 'false'));        $this->scripts->addFiles($this->console->getScripts());        // if database storage for user preferences is transient,        // offer to load exported settings from localStorage        // (detection will be done in JavaScript)        $userprefsOfferImport = false;        if (            $GLOBALS['config']->get('user_preferences') === 'session'            && ! isset($_SESSION['userprefs_autoload'])        ) {            $userprefsOfferImport = true;        }        if ($userprefsOfferImport) {            $this->scripts->addFile('config.js');        }        if ($this->menuEnabled && $GLOBALS['server'] > 0) {            $nav = new Navigation(                $this->template,                new Relation($dbi),                $dbi            );            $navigation = $nav->getDisplay();        }        $customHeader = Config::renderHeader();        // offer to load user preferences from localStorage        if ($userprefsOfferImport) {            $loadUserPreferences = $this->userPreferences->autoloadGetHeader();        }        if ($this->menuEnabled && $GLOBALS['server'] > 0) {            $menu = $this->menu->getDisplay();        }        $console = $this->console->getDisplay();        $messages = $this->getMessage();        $isLoggedIn = isset($GLOBALS['dbi']) && $GLOBALS['dbi']->isConnected();        return $this->template->render('header', [            'lang' => $GLOBALS['lang'],            'allow_third_party_framing' => $GLOBALS['cfg']['AllowThirdPartyFraming'],            'base_dir' => $baseDir,            'theme_path' => $themePath,            'version' => $version,            'text_dir' => $GLOBALS['text_dir'],            'server' => $GLOBALS['server'] ?? null,            'title' => $this->getPageTitle(),            'scripts' => $this->scripts->getDisplay(),            'body_id' => $this->bodyId,            'navigation' => $navigation ?? '',            'custom_header' => $customHeader,            'load_user_preferences' => $loadUserPreferences ?? '',            'show_hint' => $GLOBALS['cfg']['ShowHint'],            'is_warnings_enabled' => $this->warningsEnabled,            'is_menu_enabled' => $this->menuEnabled,            'is_logged_in' => $isLoggedIn,            'menu' => $menu ?? '',            'console' => $console,            'messages' => $messages,            'recent_table' => $recentTable,        ]);    }    /**     * 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 (! empty($GLOBALS['message'])) {            $message = $GLOBALS['message'];            unset($GLOBALS['message']);        } elseif (! empty($_REQUEST['message'])) {            $message = $_REQUEST['message'];        }        if (! empty($message)) {            if (isset($GLOBALS['buffer_message'])) {                $bufferMessage = $GLOBALS['buffer_message'];            }            $retval .= Generator::getMessage($message);            if (isset($bufferMessage)) {                $GLOBALS['buffer_message'] = $bufferMessage;            }        }        return $retval;    }    /**     * Sends out the HTTP headers     */    public function sendHttpHeaders(): void    {        if (defined('TESTSUITE')) {            return;        }        /**         * Sends http headers         */        $GLOBALS['now'] = gmdate('D, d M Y H:i:s') . ' GMT';        $headers = $this->getHttpHeaders();        foreach ($headers as $name => $value) {            header(sprintf('%s: %s', $name, $value));        }        $this->headerIsSent = true;    }    /**     * @return array<string, string>     */    private function getHttpHeaders(): array    {        $headers = [];        /* Prevent against ClickJacking by disabling framing */        if (strtolower((string) $GLOBALS['cfg']['AllowThirdPartyFraming']) === 'sameorigin') {            $headers['X-Frame-Options'] = 'SAMEORIGIN';        } elseif ($GLOBALS['cfg']['AllowThirdPartyFraming'] !== true) {            $headers['X-Frame-Options'] = 'DENY';        }        $headers['Referrer-Policy'] = 'same-origin';        $headers = array_merge($headers, $this->getCspHeaders());        /**         * Re-enable possible disabled XSS filters.         *         * @see https://developer.mozilla.org/docs/Web/HTTP/Headers/X-XSS-Protection         */        $headers['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         */        $headers['X-Content-Type-Options'] = 'nosniff';        /**         * Adobe cross-domain-policies.         *         * @see https://www.sentrium.co.uk/labs/application-security-101-http-headers         */        $headers['X-Permitted-Cross-Domain-Policies'] = 'none';        /**         * Robots meta tag.         *         * @see https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag         */        $headers['X-Robots-Tag'] = 'noindex, nofollow';        $headers = array_merge($headers, Core::getNoCacheHeaders());        if (! defined('IS_TRANSFORMATION_WRAPPER')) {            // 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 (strlen($this->title) == 0) {            if ($GLOBALS['server'] > 0) {                if (strlen($GLOBALS['table'])) {                    $tempTitle = $GLOBALS['cfg']['TitleTable'];                } elseif (strlen($GLOBALS['db'])) {                    $tempTitle = $GLOBALS['cfg']['TitleDatabase'];                } elseif (strlen($GLOBALS['cfg']['Server']['host'])) {                    $tempTitle = $GLOBALS['cfg']['TitleServer'];                } else {                    $tempTitle = $GLOBALS['cfg']['TitleDefault'];                }                $this->title = htmlspecialchars(                    Util::expandUserString($tempTitle)                );            } else {                $this->title = 'phpMyAdmin';            }        }        return $this->title;    }    /**     * Get all the CSP allow policy headers     *     * @return array<string, string>     */    private function getCspHeaders(): array    {        global $cfg;        $mapTileUrls = ' *.tile.openstreetmap.org';        $captchaUrl = '';        $cspAllow = $cfg['CSPAllow'];        if (            ! empty($cfg['CaptchaLoginPrivateKey'])            && ! empty($cfg['CaptchaLoginPublicKey'])            && ! empty($cfg['CaptchaApi'])            && ! empty($cfg['CaptchaRequestParam'])            && ! empty($cfg['CaptchaResponseParam'])        ) {            $captchaUrl = ' ' . $cfg['CaptchaCsp'] . ' ';        }        $headers = [];        $headers['Content-Security-Policy'] = sprintf(            'default-src \'self\' %s%s;script-src \'self\' \'unsafe-inline\' \'unsafe-eval\' %s%s;'                . 'style-src \'self\' \'unsafe-inline\' %s%s;img-src \'self\' data: %s%s%s;object-src \'none\';',            $captchaUrl,            $cspAllow,            $captchaUrl,            $cspAllow,            $captchaUrl,            $cspAllow,            $cspAllow,            $mapTileUrls,            $captchaUrl        );        $headers['X-Content-Security-Policy'] = sprintf(            'default-src \'self\' %s%s;options inline-script eval-script;'                . 'referrer no-referrer;img-src \'self\' data: %s%s%s;object-src \'none\';',            $captchaUrl,            $cspAllow,            $cspAllow,            $mapTileUrls,            $captchaUrl        );        $headers['X-WebKit-CSP'] = sprintf(            'default-src \'self\' %s%s;script-src \'self\' %s%s \'unsafe-inline\' \'unsafe-eval\';'                . 'referrer no-referrer;style-src \'self\' \'unsafe-inline\' %s;'                . 'img-src \'self\' data: %s%s%s;object-src \'none\';',            $captchaUrl,            $cspAllow,            $captchaUrl,            $cspAllow,            $captchaUrl,            $cspAllow,            $mapTileUrls,            $captchaUrl        );        return $headers;    }    /**     * Add recently used table and reload the navigation.     *     * @param string $db    Database name where the table is located.     * @param string $table The table name     */    private function addRecentTable(string $db, string $table): string    {        $retval = '';        if ($this->menuEnabled && strlen($table) > 0 && $GLOBALS['cfg']['NumRecentTables'] > 0) {            $tmpResult = RecentFavoriteTable::getInstance('recent')->add($db, $table);            if ($tmpResult === true) {                $retval = RecentFavoriteTable::getHtmlUpdateRecentTables();            } else {                $error = $tmpResult;                $retval = $error->getDisplay();            }        }        return $retval;    }    /**     * Returns the phpMyAdmin version to be appended to the url to avoid caching     * between versions     *     * @return string urlencoded pma version as a parameter     */    public static function getVersionParameter(): string    {        return 'v=' . urlencode(Version::VERSION);    }    private function getVariablesForJavaScript(): string    {        global $cfg;        $maxInputVars = ini_get('max_input_vars');        $maxInputVarsValue = $maxInputVars === false || $maxInputVars === '' ? 'false' : (int) $maxInputVars;        return $this->template->render('javascript/variables', [            'first_day_of_calendar' => $cfg['FirstDayOfCalendar'] ?? 0,            'max_input_vars' => $maxInputVarsValue,        ]);    }}
 |