Header::getVersionParameter()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Used to render the header of PMA's pages
4
 */
5
6
declare(strict_types=1);
7
8
namespace PhpMyAdmin;
9
10
use PhpMyAdmin\ConfigStorage\Relation;
11
use PhpMyAdmin\Container\ContainerBuilder;
12
use PhpMyAdmin\Html\Generator;
13
use PhpMyAdmin\Navigation\Navigation;
14
use PhpMyAdmin\Theme\ThemeManager;
15
16
use function array_merge;
17
use function defined;
18
use function htmlspecialchars;
19
use function ini_get;
20
use function json_encode;
21
use function sprintf;
22
use function strtolower;
23
use function urlencode;
24
25
use const JSON_HEX_TAG;
26
27
/**
28
 * Class used to output the HTTP and HTML headers
29
 */
30
class Header
31
{
32
    /**
33
     * Scripts instance
34
     */
35
    private Scripts $scripts;
36
    /**
37
     * Menu instance
38
     */
39
    private Menu $menu;
40
    /**
41
     * The page title
42
     */
43
    private string $title = '';
44
    /**
45
     * The value for the id attribute for the body tag
46
     */
47
    private string $bodyId = '';
48
    /**
49
     * Whether to show the top menu
50
     */
51
    private bool $menuEnabled;
52
    /**
53
     * Whether to show the warnings
54
     */
55
    private bool $warningsEnabled = true;
56
57
    private UserPreferences $userPreferences;
58
59
    private bool $isTransformationWrapper = false;
60
61 36
    public function __construct(
62
        private readonly Template $template,
63
        private readonly Console $console,
64
        private readonly Config $config,
65
    ) {
66 36
        $dbi = DatabaseInterface::getInstance();
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\DatabaseInterface::getInstance() has been deprecated: Use dependency injection instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

66
        $dbi = /** @scrutinizer ignore-deprecated */ DatabaseInterface::getInstance();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
67 36
        $this->menuEnabled = $dbi->isConnected();
68 36
        $relation = new Relation($dbi);
69 36
        $this->menu = new Menu($dbi, $this->template, $this->config, $relation, Current::$database, Current::$table);
70 36
        $this->scripts = new Scripts($this->template);
71 36
        $this->addDefaultScripts();
72
73 36
        $this->userPreferences = new UserPreferences($dbi, $relation, $this->template);
74
    }
75
76
    /**
77
     * Loads common scripts
78
     */
79 36
    private function addDefaultScripts(): void
80
    {
81 36
        $this->scripts->addFile('runtime.js');
82 36
        $this->scripts->addFile('vendor/jquery/jquery.min.js');
83 36
        $this->scripts->addFile('vendor/jquery/jquery-migrate.min.js');
84 36
        $this->scripts->addFile('vendor/sprintf.js');
85 36
        $this->scripts->addFile('vendor/jquery/jquery-ui.min.js');
86 36
        $this->scripts->addFile('name-conflict-fixes.js');
87 36
        $this->scripts->addFile('vendor/bootstrap/bootstrap.bundle.min.js');
88 36
        $this->scripts->addFile('vendor/js.cookie.min.js');
89 36
        $this->scripts->addFile('vendor/jquery/jquery.validate.min.js');
90 36
        $this->scripts->addFile('vendor/jquery/jquery-ui-timepicker-addon.js');
91 36
        $this->scripts->addFile('index.php', ['route' => '/messages', 'l' => $GLOBALS['lang']]);
92 36
        $this->scripts->addFile('shared.js');
93 36
        $this->scripts->addFile('menu_resizer.js');
94 36
        $this->scripts->addFile('main.js');
95
96 36
        $this->scripts->addCode($this->getJsParamsCode());
97
    }
98
99
    /**
100
     * Returns, as an array, a list of parameters
101
     * used on the client side
102
     *
103
     * @return mixed[]
104
     */
105 36
    public function getJsParams(): array
106
    {
107 36
        $pftext = $_SESSION['tmpval']['pftext'] ?? '';
108
109 36
        $params = [
110
            // Do not add any separator, JS code will decide
111 36
            'common_query' => Url::getCommonRaw([], ''),
112 36
            'opendb_url' => Util::getScriptNameForOption($this->config->settings['DefaultTabDatabase'], 'database'),
113 36
            'lang' => $GLOBALS['lang'],
114 36
            'server' => Current::$server,
115 36
            'table' => Current::$table,
116 36
            'db' => Current::$database,
117 36
            'token' => $_SESSION[' PMA_token '],
118 36
            'text_dir' => LanguageManager::$textDir,
119 36
            'LimitChars' => $this->config->settings['LimitChars'],
120 36
            'pftext' => $pftext,
121 36
            'confirm' => $this->config->settings['Confirm'],
122 36
            'LoginCookieValidity' => $this->config->settings['LoginCookieValidity'],
123 36
            'session_gc_maxlifetime' => (int) ini_get('session.gc_maxlifetime'),
124 36
            'logged_in' => DatabaseInterface::getInstance()->isConnected(),
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\DatabaseInterface::getInstance() has been deprecated: Use dependency injection instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

124
            'logged_in' => /** @scrutinizer ignore-deprecated */ DatabaseInterface::getInstance()->isConnected(),

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
125 36
            'is_https' => $this->config->isHttps(),
126 36
            'rootPath' => $this->config->getRootPath(),
127 36
            'arg_separator' => Url::getArgSeparator(),
128 36
            'version' => Version::VERSION,
129 36
        ];
130 36
        if ($this->config->hasSelectedServer()) {
131
            $params['auth_type'] = $this->config->selectedServer['auth_type'];
132
            if (isset($this->config->selectedServer['user'])) {
133
                $params['user'] = $this->config->selectedServer['user'];
134
            }
135
        }
136
137 36
        return $params;
138
    }
139
140
    /**
141
     * Returns, as a string, a list of parameters
142
     * used on the client side
143
     */
144 36
    public function getJsParamsCode(): string
145
    {
146 36
        $params = $this->getJsParams();
147
148 36
        return 'window.Navigation.update(window.CommonParams.setAll(' . json_encode($params, JSON_HEX_TAG) . '));';
149
    }
150
151
    /**
152
     * Returns the Scripts object
153
     *
154
     * @return Scripts object
155
     */
156 8
    public function getScripts(): Scripts
157
    {
158 8
        return $this->scripts;
159
    }
160
161
    /**
162
     * Returns the Menu object
163
     *
164
     * @return Menu object
165
     */
166
    public function getMenu(): Menu
167
    {
168
        return $this->menu;
169
    }
170
171
    /**
172
     * Setter for the ID attribute in the BODY tag
173
     *
174
     * @param string $id Value for the ID attribute
175
     */
176 4
    public function setBodyId(string $id): void
177
    {
178 4
        $this->bodyId = htmlspecialchars($id);
179
    }
180
181
    /**
182
     * Setter for the title of the page
183
     *
184
     * @param string $title New title
185
     */
186
    public function setTitle(string $title): void
187
    {
188
        $this->title = htmlspecialchars($title);
189
    }
190
191
    /**
192
     * Disables the display of the top menu
193
     */
194
    public function disableMenuAndConsole(): void
195
    {
196
        $this->menuEnabled = false;
197
        $this->console->disable();
198
    }
199
200
    /**
201
     * Disables the display of the top menu
202
     */
203 4
    public function disableWarnings(): void
204
    {
205 4
        $this->warningsEnabled = false;
206
    }
207
208
    /** @return mixed[] */
209 4
    public function getDisplay(): array
210
    {
211 4
        $baseDir = defined('PMA_PATH_TO_BASEDIR') ? PMA_PATH_TO_BASEDIR : '';
212
213
        /** @var ThemeManager $themeManager */
214 4
        $themeManager = ContainerBuilder::getContainer()->get(ThemeManager::class);
215 4
        $theme = $themeManager->theme;
216
217 4
        $version = self::getVersionParameter();
218
219
        // The user preferences have been merged at this point
220
        // so we can conditionally add CodeMirror, other scripts and settings
221 4
        if ($this->config->settings['CodemirrorEnable']) {
222
            $this->scripts->addFile('vendor/codemirror/lib/codemirror.js');
223
            $this->scripts->addFile('vendor/codemirror/mode/sql/sql.js');
224
            $this->scripts->addFile('vendor/codemirror/addon/runmode/runmode.js');
225
            $this->scripts->addFile('vendor/codemirror/addon/hint/show-hint.js');
226
            $this->scripts->addFile('vendor/codemirror/addon/hint/sql-hint.js');
227
            if ($this->config->settings['LintEnable']) {
228
                $this->scripts->addFile('vendor/codemirror/addon/lint/lint.js');
229
                $this->scripts->addFile('codemirror/addon/lint/sql-lint.js');
230
            }
231
        }
232
233 4
        if ($this->config->settings['SendErrorReports'] !== 'never') {
234
            $this->scripts->addFile('vendor/tracekit.js');
235
            $this->scripts->addFile('error_report.js');
236
        }
237
238 4
        if ($this->config->settings['enable_drag_drop_import'] === true) {
239
            $this->scripts->addFile('drag_drop_import.js');
240
        }
241
242 4
        if (! $this->config->get('DisableShortcutKeys')) {
243
            $this->scripts->addFile('shortcuts_handler.js');
244
        }
245
246 4
        $this->scripts->addCode($this->getVariablesForJavaScript());
247
248 4
        $this->scripts->addCode(
249 4
            'ConsoleEnterExecutes=' . ($this->config->settings['ConsoleEnterExecutes'] ? 'true' : 'false'),
250 4
        );
251 4
        $this->scripts->addFiles($this->console->getScripts());
252
253 4
        $dbi = DatabaseInterface::getInstance();
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\DatabaseInterface::getInstance() has been deprecated: Use dependency injection instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

253
        $dbi = /** @scrutinizer ignore-deprecated */ DatabaseInterface::getInstance();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
254 4
        if ($this->menuEnabled && Current::$server > 0) {
255
            $navigation = (new Navigation($this->template, new Relation($dbi), $dbi, $this->config))->getDisplay();
256
        }
257
258 4
        $customHeader = Config::renderHeader();
259
260
        // offer to load user preferences from localStorage
261
        if (
262 4
            $this->config->get('user_preferences') === 'session'
263 4
            && ! isset($_SESSION['userprefs_autoload'])
264
        ) {
265
            $loadUserPreferences = $this->userPreferences->autoloadGetHeader();
266
        }
267
268 4
        if ($this->menuEnabled && Current::$server > 0) {
269
            $menu = $this->menu->getDisplay();
270
        }
271
272 4
        $console = $this->console->getDisplay();
273 4
        $messages = $this->getMessage();
274 4
        $isLoggedIn = $dbi->isConnected();
275
276 4
        $this->scripts->addFile('datetimepicker.js');
277 4
        $this->scripts->addFile('validator-messages.js');
278
279 4
        return [
280 4
            'lang' => $GLOBALS['lang'],
281 4
            'allow_third_party_framing' => $this->config->settings['AllowThirdPartyFraming'],
282 4
            'base_dir' => $baseDir,
283 4
            'theme_path' => $theme->getPath(),
284 4
            'version' => $version,
285 4
            'text_dir' => LanguageManager::$textDir,
286 4
            'server' => Current::$server,
287 4
            'title' => $this->getPageTitle(),
288 4
            'scripts' => $this->scripts->getDisplay(),
289 4
            'body_id' => $this->bodyId,
290 4
            'navigation' => $navigation ?? '',
291 4
            'custom_header' => $customHeader,
292 4
            'load_user_preferences' => $loadUserPreferences ?? '',
293 4
            'show_hint' => $this->config->settings['ShowHint'],
294 4
            'is_warnings_enabled' => $this->warningsEnabled,
295 4
            'is_menu_enabled' => $this->menuEnabled,
296 4
            'is_logged_in' => $isLoggedIn,
297 4
            'menu' => $menu ?? '',
298 4
            'console' => $console,
299 4
            'messages' => $messages,
300 4
            'theme_color_mode' => $theme->getColorMode(),
301 4
            'theme_color_modes' => $theme->getColorModes(),
302 4
            'theme_id' => $theme->getId(),
303 4
            'current_user' => $dbi->getCurrentUserAndHost(),
304 4
            'is_mariadb' => $dbi->isMariaDB(),
305 4
        ];
306
    }
307
308
    /**
309
     * Returns the message to be displayed at the top of
310
     * the page, including the executed SQL query, if any.
311
     */
312 8
    public function getMessage(): string
313
    {
314 8
        $retval = '';
315 8
        $message = '';
316 8
        if (! empty($GLOBALS['message'])) {
317 4
            $message = $GLOBALS['message'];
318 4
            unset($GLOBALS['message']);
319 4
        } elseif (! empty($_REQUEST['message'])) {
320
            $message = $_REQUEST['message'];
321
        }
322
323 8
        if ($message !== '') {
324 4
            if (isset($GLOBALS['buffer_message'])) {
325
                $bufferMessage = $GLOBALS['buffer_message'];
326
            }
327
328 4
            $retval .= Generator::getMessage($message);
329 4
            if (isset($bufferMessage)) {
330
                $GLOBALS['buffer_message'] = $bufferMessage;
331
            }
332
        }
333
334 8
        return $retval;
335
    }
336
337
    /** @return array<string, string> */
338 12
    public function getHttpHeaders(): array
339
    {
340 12
        $headers = [];
341
342
        /* Prevent against ClickJacking by disabling framing */
343 12
        if (strtolower((string) $this->config->settings['AllowThirdPartyFraming']) === 'sameorigin') {
344 4
            $headers['X-Frame-Options'] = 'SAMEORIGIN';
345 8
        } elseif ($this->config->settings['AllowThirdPartyFraming'] !== true) {
346 4
            $headers['X-Frame-Options'] = 'DENY';
347
        }
348
349 12
        $headers['Referrer-Policy'] = 'same-origin';
350
351 12
        $headers = array_merge($headers, $this->getCspHeaders());
352
353
        /**
354
         * Re-enable possible disabled XSS filters.
355
         *
356
         * @see https://developer.mozilla.org/docs/Web/HTTP/Headers/X-XSS-Protection
357
         */
358 12
        $headers['X-XSS-Protection'] = '1; mode=block';
359
360
        /**
361
         * "nosniff", prevents Internet Explorer and Google Chrome from MIME-sniffing
362
         * a response away from the declared content-type.
363
         *
364
         * @see https://developer.mozilla.org/docs/Web/HTTP/Headers/X-Content-Type-Options
365
         */
366 12
        $headers['X-Content-Type-Options'] = 'nosniff';
367
368
        /**
369
         * Adobe cross-domain-policies.
370
         *
371
         * @see https://www.sentrium.co.uk/labs/application-security-101-http-headers
372
         */
373 12
        $headers['X-Permitted-Cross-Domain-Policies'] = 'none';
374
375
        /**
376
         * Robots meta tag.
377
         *
378
         * @see https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag
379
         */
380 12
        $headers['X-Robots-Tag'] = 'noindex, nofollow';
381
382
        /**
383
         * The HTTP Permissions-Policy header provides a mechanism to allow and deny
384
         * the use of browser features in a document
385
         * or within any <iframe> elements in the document.
386
         *
387
         * @see https://developer.mozilla.org/docs/Web/HTTP/Headers/Permissions-Policy
388
         */
389 12
        $headers['Permissions-Policy'] = 'fullscreen=(self), oversized-images=(self), interest-cohort=()';
390
391 12
        $headers = array_merge($headers, Core::getNoCacheHeaders());
392
393
        /**
394
         * A different Content-Type is set in {@see \PhpMyAdmin\Controllers\Transformation\WrapperController}.
395
         */
396 12
        if (! $this->isTransformationWrapper) {
397
            // Define the charset to be used
398 12
            $headers['Content-Type'] = 'text/html; charset=utf-8';
399
        }
400
401 12
        return $headers;
402
    }
403
404
    /**
405
     * If the page is missing the title, this function
406
     * will set it to something reasonable
407
     */
408 4
    public function getPageTitle(): string
409
    {
410 4
        if ($this->title === '') {
411 4
            if (Current::$server > 0) {
412
                if (Current::$table !== '') {
413
                    $tempTitle = $this->config->settings['TitleTable'];
414
                } elseif (Current::$database !== '') {
415
                    $tempTitle = $this->config->settings['TitleDatabase'];
416
                } elseif ($this->config->selectedServer['host'] !== '') {
417
                    $tempTitle = $this->config->settings['TitleServer'];
418
                } else {
419
                    $tempTitle = $this->config->settings['TitleDefault'];
420
                }
421
422
                $this->title = htmlspecialchars(
423
                    Util::expandUserString($tempTitle),
424
                );
425
            } else {
426 4
                $this->title = 'phpMyAdmin';
427
            }
428
        }
429
430 4
        return $this->title;
431
    }
432
433
    /**
434
     * Get all the CSP allow policy headers
435
     *
436
     * @return array<string, string>
437
     */
438 12
    private function getCspHeaders(): array
439
    {
440 12
        $mapTileUrl = ' tile.openstreetmap.org';
441 12
        $captchaUrl = '';
442 12
        $cspAllow = $this->config->settings['CSPAllow'];
443
444
        if (
445 12
            ! empty($this->config->settings['CaptchaLoginPrivateKey'])
446 12
            && ! empty($this->config->settings['CaptchaLoginPublicKey'])
447 12
            && ! empty($this->config->settings['CaptchaApi'])
448 12
            && ! empty($this->config->settings['CaptchaRequestParam'])
449 12
            && ! empty($this->config->settings['CaptchaResponseParam'])
450
        ) {
451 8
            $captchaUrl = ' ' . $this->config->settings['CaptchaCsp'] . ' ';
452
        }
453
454 12
        $headers = [];
455
456 12
        $headers['Content-Security-Policy'] = sprintf(
457 12
            'default-src \'self\' %s%s;script-src \'self\' \'unsafe-inline\' \'unsafe-eval\' %s%s;'
458 12
                . 'style-src \'self\' \'unsafe-inline\' %s%s;img-src \'self\' data: %s%s%s;object-src \'none\';',
459 12
            $captchaUrl,
460 12
            $cspAllow,
461 12
            $captchaUrl,
462 12
            $cspAllow,
463 12
            $captchaUrl,
464 12
            $cspAllow,
465 12
            $cspAllow,
466 12
            $mapTileUrl,
467 12
            $captchaUrl,
468 12
        );
469
470 12
        $headers['X-Content-Security-Policy'] = sprintf(
471 12
            'default-src \'self\' %s%s;options inline-script eval-script;'
472 12
                . 'referrer no-referrer;img-src \'self\' data: %s%s%s;object-src \'none\';',
473 12
            $captchaUrl,
474 12
            $cspAllow,
475 12
            $cspAllow,
476 12
            $mapTileUrl,
477 12
            $captchaUrl,
478 12
        );
479
480 12
        $headers['X-WebKit-CSP'] = sprintf(
481 12
            'default-src \'self\' %s%s;script-src \'self\' %s%s \'unsafe-inline\' \'unsafe-eval\';'
482 12
                . 'referrer no-referrer;style-src \'self\' \'unsafe-inline\' %s;'
483 12
                . 'img-src \'self\' data: %s%s%s;object-src \'none\';',
484 12
            $captchaUrl,
485 12
            $cspAllow,
486 12
            $captchaUrl,
487 12
            $cspAllow,
488 12
            $captchaUrl,
489 12
            $cspAllow,
490 12
            $mapTileUrl,
491 12
            $captchaUrl,
492 12
        );
493
494 12
        return $headers;
495
    }
496
497
    /**
498
     * Returns the phpMyAdmin version to be appended to the url to avoid caching
499
     * between versions
500
     *
501
     * @return string urlencoded pma version as a parameter
502
     */
503 4
    public static function getVersionParameter(): string
504
    {
505 4
        return 'v=' . urlencode(Version::VERSION);
506
    }
507
508 4
    private function getVariablesForJavaScript(): string
509
    {
510 4
        $maxInputVars = ini_get('max_input_vars');
511 4
        $maxInputVarsValue = $maxInputVars === false || $maxInputVars === '' ? 'false' : (int) $maxInputVars;
512
513 4
        return $this->template->render('javascript/variables', [
514 4
            'first_day_of_calendar' => $this->config->settings['FirstDayOfCalendar'] ?? 0,
515 4
            'max_input_vars' => $maxInputVarsValue,
516 4
        ]);
517
    }
518
519
    public function setIsTransformationWrapper(bool $isTransformationWrapper): void
520
    {
521
        $this->isTransformationWrapper = $isTransformationWrapper;
522
    }
523
524
    public function getConsole(): Console
525
    {
526
        return $this->console;
527
    }
528
}
529