Passed
Push — master ( 0ee393...41622c )
by Maurício
09:55
created

Core::checkTokenRequestParam()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 37
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6.3949

Importance

Changes 0
Metric Value
cc 6
eloc 18
c 0
b 0
f 0
nc 7
nop 0
dl 0
loc 37
rs 9.0444
ccs 14
cts 18
cp 0.7778
crap 6.3949
1
<?php
2
/**
3
 * Core functions used all over the scripts.
4
 * This script is distinct from libraries/common.inc.php because this
5
 * script is called from /test.
6
 */
7
8
declare(strict_types=1);
9
10
namespace PhpMyAdmin;
11
12
use PhpMyAdmin\Display\Error as DisplayError;
13
use Symfony\Component\DependencyInjection\ContainerBuilder;
14
use const DATE_RFC1123;
15
use const E_USER_ERROR;
16
use const E_USER_WARNING;
17
use const FILTER_VALIDATE_IP;
18
use function array_keys;
19
use function array_pop;
20
use function array_walk_recursive;
21
use function chr;
22
use function count;
23
use function date_default_timezone_get;
24
use function date_default_timezone_set;
25
use function defined;
26
use function explode;
27
use function extension_loaded;
28
use function filter_var;
29
use function function_exists;
30
use function getenv;
31
use function gettype;
32
use function gmdate;
33
use function hash_equals;
34
use function hash_hmac;
35
use function header;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, PhpMyAdmin\header. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
36
use function htmlspecialchars;
37
use function http_build_query;
38
use function implode;
39
use function in_array;
40
use function ini_get;
41
use function ini_set;
42
use function intval;
43
use function is_array;
44
use function is_numeric;
45
use function is_scalar;
46
use function is_string;
47
use function json_encode;
48
use function mb_internal_encoding;
49
use function mb_strlen;
50
use function mb_strpos;
51
use function mb_strrpos;
52
use function mb_substr;
53
use function parse_str;
54
use function parse_url;
55
use function preg_match;
56
use function preg_replace;
57
use function session_id;
58
use function session_write_close;
59
use function sprintf;
60
use function str_replace;
61
use function strlen;
62
use function strpos;
63
use function strtolower;
64
use function strtr;
65
use function substr;
66
use function trigger_error;
67
use function unserialize;
68
use function urldecode;
69
use function vsprintf;
70
71
/**
72
 * Core class
73
 */
74
class Core
75
{
76
    /**
77
     * checks given $var and returns it if valid, or $default of not valid
78
     * given $var is also checked for type being 'similar' as $default
79
     * or against any other type if $type is provided
80
     *
81
     * <code>
82
     * // $_REQUEST['db'] not set
83
     * echo Core::ifSetOr($_REQUEST['db'], ''); // ''
84
     * // $_POST['sql_query'] not set
85
     * echo Core::ifSetOr($_POST['sql_query']); // null
86
     * // $cfg['EnableFoo'] not set
87
     * echo Core::ifSetOr($cfg['EnableFoo'], false, 'boolean'); // false
88
     * echo Core::ifSetOr($cfg['EnableFoo']); // null
89
     * // $cfg['EnableFoo'] set to 1
90
     * echo Core::ifSetOr($cfg['EnableFoo'], false, 'boolean'); // false
91
     * echo Core::ifSetOr($cfg['EnableFoo'], false, 'similar'); // 1
92
     * echo Core::ifSetOr($cfg['EnableFoo'], false); // 1
93
     * // $cfg['EnableFoo'] set to true
94
     * echo Core::ifSetOr($cfg['EnableFoo'], false, 'boolean'); // true
95
     * </code>
96
     *
97
     * @see self::isValid()
98
     *
99
     * @param mixed $var     param to check
100
     * @param mixed $default default value
101
     * @param mixed $type    var type or array of values to check against $var
102
     *
103
     * @return mixed $var or $default
104
     */
105 92
    public static function ifSetOr(&$var, $default = null, $type = 'similar')
106
    {
107 92
        if (! self::isValid($var, $type, $default)) {
108 80
            return $default;
109
        }
110
111 12
        return $var;
112
    }
113
114
    /**
115
     * checks given $var against $type or $compare
116
     *
117
     * $type can be:
118
     * - false       : no type checking
119
     * - 'scalar'    : whether type of $var is integer, float, string or boolean
120
     * - 'numeric'   : whether type of $var is any number representation
121
     * - 'length'    : whether type of $var is scalar with a string length > 0
122
     * - 'similar'   : whether type of $var is similar to type of $compare
123
     * - 'equal'     : whether type of $var is identical to type of $compare
124
     * - 'identical' : whether $var is identical to $compare, not only the type!
125
     * - or any other valid PHP variable type
126
     *
127
     * <code>
128
     * // $_REQUEST['doit'] = true;
129
     * Core::isValid($_REQUEST['doit'], 'identical', 'true'); // false
130
     * // $_REQUEST['doit'] = 'true';
131
     * Core::isValid($_REQUEST['doit'], 'identical', 'true'); // true
132
     * </code>
133
     *
134
     * NOTE: call-by-reference is used to not get NOTICE on undefined vars,
135
     * but the var is not altered inside this function, also after checking a var
136
     * this var exists nut is not set, example:
137
     * <code>
138
     * // $var is not set
139
     * isset($var); // false
140
     * functionCallByReference($var); // false
141
     * isset($var); // true
142
     * functionCallByReference($var); // true
143
     * </code>
144
     *
145
     * to avoid this we set this var to null if not isset
146
     *
147
     * @see https://secure.php.net/gettype
148
     *
149
     * @param mixed $var     variable to check
150
     * @param mixed $type    var type or array of valid values to check against $var
151
     * @param mixed $compare var to compare with $var
152
     *
153
     * @return bool whether valid or not
154
     *
155
     * @todo add some more var types like hex, bin, ...?
156
     */
157 280
    public static function isValid(&$var, $type = 'length', $compare = null): bool
158
    {
159 280
        if (! isset($var)) {
160
            // var is not even set
161 128
            return false;
162
        }
163
164 164
        if ($type === false) {
165
            // no vartype requested
166 48
            return true;
167
        }
168
169 116
        if (is_array($type)) {
170 8
            return in_array($var, $type);
171
        }
172
173
        // allow some aliases of var types
174 108
        $type = strtolower($type);
175 81
        switch ($type) {
176 108
            case 'identic':
177 4
                $type = 'identical';
178 4
                break;
179 104
            case 'len':
180 4
                $type = 'length';
181 4
                break;
182 104
            case 'bool':
183 4
                $type = 'boolean';
184 4
                break;
185 104
            case 'float':
186 4
                $type = 'double';
187 4
                break;
188 104
            case 'int':
189 8
                $type = 'integer';
190 8
                break;
191 104
            case 'null':
192 4
                $type = 'NULL';
193 4
                break;
194
        }
195
196 108
        if ($type === 'identical') {
197 4
            return $var === $compare;
198
        }
199
200
        // whether we should check against given $compare
201 104
        if ($type === 'similar') {
202 28
            switch (gettype($compare)) {
203 28
                case 'string':
204 20
                case 'boolean':
205 12
                    $type = 'scalar';
206 12
                    break;
207 16
                case 'integer':
208 12
                case 'double':
209 8
                    $type = 'numeric';
210 8
                    break;
211
                default:
212 28
                    $type = gettype($compare);
213
            }
214 100
        } elseif ($type === 'equal') {
215 24
            $type = gettype($compare);
216
        }
217
218
        // do the check
219 104
        if ($type === 'length' || $type === 'scalar') {
220 60
            $is_scalar = is_scalar($var);
221 60
            if ($is_scalar && $type === 'length') {
222 32
                return strlen((string) $var) > 0;
223
            }
224
225 32
            return $is_scalar;
226
        }
227
228 68
        if ($type === 'numeric') {
229 24
            return is_numeric($var);
230
        }
231
232 52
        return gettype($var) === $type;
233
    }
234
235
    /**
236
     * Removes insecure parts in a path; used before include() or
237
     * require() when a part of the path comes from an insecure source
238
     * like a cookie or form.
239
     *
240
     * @param string $path The path to check
241
     *
242
     * @return string  The secured path
243
     *
244
     * @access public
245
     */
246 8
    public static function securePath(string $path): string
247
    {
248
        // change .. to .
249 8
        return preg_replace('@\.\.*@', '.', $path);
250
    } // end function
251
252
    /**
253
     * displays the given error message on phpMyAdmin error page in foreign language,
254
     * ends script execution and closes session
255
     *
256
     * loads language file if not loaded already
257
     *
258
     * @param string       $error_message the error message or named error message
259
     * @param string|array $message_args  arguments applied to $error_message
260
     */
261 24
    public static function fatalError(
262
        string $error_message,
263
        $message_args = null
264
    ): void {
265
        /* Use format string if applicable */
266 24
        if (is_string($message_args)) {
267 4
            $error_message = sprintf($error_message, $message_args);
268 24
        } elseif (is_array($message_args)) {
269 4
            $error_message = vsprintf($error_message, $message_args);
270
        }
271
272
        /*
273
         * Avoid using Response class as config does not have to be loaded yet
274
         * (this can happen on early fatal error)
275
         */
276 24
        if (isset($GLOBALS['dbi'], $GLOBALS['PMA_Config']) && $GLOBALS['dbi'] !== null
277 24
            && $GLOBALS['PMA_Config']->get('is_setup') === false
278 24
            && Response::getInstance()->isAjax()) {
279
            $response = Response::getInstance();
280
            $response->setRequestStatus(false);
281
            $response->addJSON('message', Message::error($error_message));
282 24
        } elseif (! empty($_REQUEST['ajax_request'])) {
283
            // Generate JSON manually
284
            self::headerJSON();
285
            echo json_encode(
286
                [
287
                    'success' => false,
288
                    'message' => Message::error($error_message)->getDisplay(),
289
                ]
290
            );
291
        } else {
292 24
            $error_message = strtr($error_message, ['<br>' => '[br]']);
293 24
            $error_header = __('Error');
294 24
            $lang = $GLOBALS['lang'] ?? 'en';
295 24
            $dir = $GLOBALS['text_dir'] ?? 'ltr';
296
297 24
            echo DisplayError::display(new Template(), $lang, $dir, $error_header, $error_message);
298
        }
299 24
        if (! defined('TESTSUITE')) {
300
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
301
        }
302 24
    }
303
304
    /**
305
     * Returns a link to the PHP documentation
306
     *
307
     * @param string $target anchor in documentation
308
     *
309
     * @return string  the URL
310
     *
311
     * @access public
312
     */
313 24
    public static function getPHPDocLink(string $target): string
314
    {
315
        /* List of PHP documentation translations */
316
        $php_doc_languages = [
317 24
            'pt_BR',
318
            'zh',
319
            'fr',
320
            'de',
321
            'it',
322
            'ja',
323
            'pl',
324
            'ro',
325
            'ru',
326
            'fa',
327
            'es',
328
            'tr',
329
        ];
330
331 24
        $lang = 'en';
332 24
        if (isset($GLOBALS['lang']) && in_array($GLOBALS['lang'], $php_doc_languages)) {
333
            $lang = $GLOBALS['lang'];
334
        }
335
336 24
        return self::linkURL('https://secure.php.net/manual/' . $lang . '/' . $target);
337
    }
338
339
    /**
340
     * Warn or fail on missing extension.
341
     *
342
     * @param string $extension Extension name
343
     * @param bool   $fatal     Whether the error is fatal.
344
     * @param string $extra     Extra string to append to message.
345
     */
346 8
    public static function warnMissingExtension(
347
        string $extension,
348
        bool $fatal = false,
349
        string $extra = ''
350
    ): void {
351
        /** @var ErrorHandler $error_handler */
352 8
        global $error_handler;
353
354
        /* Gettext does not have to be loaded yet here */
355 8
        if (function_exists('__')) {
356 8
            $message = __(
357 8
                'The %s extension is missing. Please check your PHP configuration.'
358
            );
359
        } else {
360
            $message
361
                = 'The %s extension is missing. Please check your PHP configuration.';
362
        }
363 8
        $doclink = self::getPHPDocLink('book.' . $extension . '.php');
364 8
        $message = sprintf(
365 8
            $message,
366 8
            '[a@' . $doclink . '@Documentation][em]' . $extension . '[/em][/a]'
367
        );
368 8
        if ($extra != '') {
369 4
            $message .= ' ' . $extra;
370
        }
371 8
        if ($fatal) {
372 8
            self::fatalError($message);
373
374 8
            return;
375
        }
376
377
        $error_handler->addError(
378
            $message,
379
            E_USER_WARNING,
380
            '',
381
            0,
382
            false
383
        );
384
    }
385
386
    /**
387
     * returns count of tables in given db
388
     *
389
     * @param string $db database to count tables for
390
     *
391
     * @return int count of tables in $db
392
     */
393
    public static function getTableCount(string $db): int
394
    {
395
        $tables = $GLOBALS['dbi']->tryQuery(
396
            'SHOW TABLES FROM ' . Util::backquote($db) . ';',
397
            DatabaseInterface::CONNECT_USER,
398
            DatabaseInterface::QUERY_STORE
399
        );
400
        if ($tables) {
401
            $num_tables = $GLOBALS['dbi']->numRows($tables);
402
            $GLOBALS['dbi']->freeResult($tables);
403
        } else {
404
            $num_tables = 0;
405
        }
406
407
        return $num_tables;
408
    }
409
410
    /**
411
     * Converts numbers like 10M into bytes
412
     * Used with permission from Moodle (https://moodle.org) by Martin Dougiamas
413
     * (renamed with PMA prefix to avoid double definition when embedded
414
     * in Moodle)
415
     *
416
     * @param string|int $size size (Default = 0)
417
     */
418 9008
    public static function getRealSize($size = 0): int
419
    {
420 9008
        if (! $size) {
421 4
            return 0;
422
        }
423
424
        $binaryprefixes = [
425 9008
            'T' => 1099511627776,
426
            't' => 1099511627776,
427
            'G' =>    1073741824,
428
            'g' =>    1073741824,
429
            'M' =>       1048576,
430
            'm' =>       1048576,
431
            'K' =>          1024,
432
            'k' =>          1024,
433
        ];
434
435 9008
        if (preg_match('/^([0-9]+)([KMGT])/i', $size, $matches)) {
436 9008
            return (int) ($matches[1] * $binaryprefixes[$matches[2]]);
437
        }
438
439 4
        return (int) $size;
440
    } // end getRealSize()
441
442
    /**
443
     * Checks given $page against given $allowList and returns true if valid
444
     * it optionally ignores query parameters in $page (script.php?ignored)
445
     *
446
     * @param string $page      page to check
447
     * @param array  $allowList allow list to check page against
448
     * @param bool   $include   whether the page is going to be included
449
     *
450
     * @return bool whether $page is valid or not (in $allowList or not)
451
     */
452 32
    public static function checkPageValidity(&$page, array $allowList = [], $include = false): bool
453
    {
454 32
        if (empty($allowList)) {
455 8
            $allowList = ['index.php'];
456
        }
457 32
        if (empty($page)) {
458 8
            return false;
459
        }
460
461 24
        if (in_array($page, $allowList)) {
462
            return true;
463
        }
464 24
        if ($include) {
465 12
            return false;
466
        }
467
468 12
        $_page = mb_substr(
469 12
            $page,
470 12
            0,
471 12
            mb_strpos($page . '?', '?')
472
        );
473 12
        if (in_array($_page, $allowList)) {
474 4
            return true;
475
        }
476
477 8
        $_page = urldecode($page);
478 8
        $_page = mb_substr(
479 8
            $_page,
480 8
            0,
481 8
            mb_strpos($_page . '?', '?')
482
        );
483
484 8
        return in_array($_page, $allowList);
485
    }
486
487
    /**
488
     * tries to find the value for the given environment variable name
489
     *
490
     * searches in $_SERVER, $_ENV then tries getenv() and apache_getenv()
491
     * in this order
492
     *
493
     * @param string $var_name variable name
494
     *
495
     * @return string  value of $var or empty string
496
     */
497 9008
    public static function getenv(string $var_name): string
498
    {
499 9008
        if (isset($_SERVER[$var_name])) {
500 508
            return (string) $_SERVER[$var_name];
501
        }
502
503 9008
        if (isset($_ENV[$var_name])) {
504
            return (string) $_ENV[$var_name];
505
        }
506
507 9008
        if (getenv($var_name)) {
508
            return getenv($var_name);
509
        }
510
511 9008
        if (function_exists('apache_getenv')
512 9008
            && apache_getenv($var_name, true)
513
        ) {
514
            return apache_getenv($var_name, true);
515
        }
516
517 9008
        return '';
518
    }
519
520
    /**
521
     * Send HTTP header, taking IIS limits into account (600 seems ok)
522
     *
523
     * @param string $uri         the header to send
524
     * @param bool   $use_refresh whether to use Refresh: header when running on IIS
525
     */
526 52
    public static function sendHeaderLocation(string $uri, bool $use_refresh = false): void
527
    {
528 52
        if ($GLOBALS['PMA_Config']->get('PMA_IS_IIS') && mb_strlen($uri) > 600) {
529 4
            Response::getInstance()->disable();
530
531 4
            $template = new Template();
532 4
            echo $template->render('header_location', ['uri' => $uri]);
533
534 4
            return;
535
        }
536
537
        /*
538
         * Avoid relative path redirect problems in case user entered URL
539
         * like /phpmyadmin/index.php/ which some web servers happily accept.
540
         */
541 48
        if ($uri[0] == '.') {
542 20
            $uri = $GLOBALS['PMA_Config']->getRootPath() . substr($uri, 2);
543
        }
544
545 48
        $response = Response::getInstance();
546
547 48
        session_write_close();
548 48
        if ($response->headersSent()) {
549
            trigger_error(
550
                'Core::sendHeaderLocation called when headers are already sent!',
551
                E_USER_ERROR
552
            );
553
        }
554
        // bug #1523784: IE6 does not like 'Refresh: 0', it
555
        // results in a blank page
556
        // but we need it when coming from the cookie login panel)
557 48
        if ($GLOBALS['PMA_Config']->get('PMA_IS_IIS') && $use_refresh) {
558 4
            $response->header('Refresh: 0; ' . $uri);
559
        } else {
560 48
            $response->header('Location: ' . $uri);
561
        }
562 48
    }
563
564
    /**
565
     * Outputs application/json headers. This includes no caching.
566
     */
567
    public static function headerJSON(): void
568
    {
569
        if (defined('TESTSUITE')) {
570
            return;
571
        }
572
        // No caching
573
        self::noCacheHeader();
574
        // MIME type
575
        header('Content-Type: application/json; charset=UTF-8');
576
        // Disable content sniffing in browser
577
        // This is needed in case we include HTML in JSON, browser might assume it's
578
        // html to display
579
        header('X-Content-Type-Options: nosniff');
580
    }
581
582
    /**
583
     * Outputs headers to prevent caching in browser (and on the way).
584
     */
585
    public static function noCacheHeader(): void
586
    {
587
        if (defined('TESTSUITE')) {
588
            return;
589
        }
590
        // rfc2616 - Section 14.21
591
        header('Expires: ' . gmdate(DATE_RFC1123));
592
        // HTTP/1.1
593
        header(
594
            'Cache-Control: no-store, no-cache, must-revalidate,'
595
            . '  pre-check=0, post-check=0, max-age=0'
596
        );
597
598
        header('Pragma: no-cache'); // HTTP/1.0
599
        // test case: exporting a database into a .gz file with Safari
600
        // would produce files not having the current time
601
        // (added this header for Safari but should not harm other browsers)
602
        header('Last-Modified: ' . gmdate(DATE_RFC1123));
603
    }
604
605
    /**
606
     * Sends header indicating file download.
607
     *
608
     * @param string $filename Filename to include in headers if empty,
609
     *                         none Content-Disposition header will be sent.
610
     * @param string $mimetype MIME type to include in headers.
611
     * @param int    $length   Length of content (optional)
612
     * @param bool   $no_cache Whether to include no-caching headers.
613
     */
614
    public static function downloadHeader(
615
        string $filename,
616
        string $mimetype,
617
        int $length = 0,
618
        bool $no_cache = true
619
    ): void {
620
        if ($no_cache) {
621
            self::noCacheHeader();
622
        }
623
        /* Replace all possibly dangerous chars in filename */
624
        $filename = Sanitize::sanitizeFilename($filename);
625
        if (! empty($filename)) {
626
            header('Content-Description: File Transfer');
627
            header('Content-Disposition: attachment; filename="' . $filename . '"');
628
        }
629
        header('Content-Type: ' . $mimetype);
630
        // inform the server that compression has been done,
631
        // to avoid a double compression (for example with Apache + mod_deflate)
632
        $notChromeOrLessThan43 = PMA_USR_BROWSER_AGENT != 'CHROME' // see bug #4942
0 ignored issues
show
introduced by
The condition PhpMyAdmin\PMA_USR_BROWSER_AGENT != 'CHROME' is always true.
Loading history...
633
            || (PMA_USR_BROWSER_AGENT == 'CHROME' && PMA_USR_BROWSER_VER < 43);
634
        if (strpos($mimetype, 'gzip') !== false && $notChromeOrLessThan43) {
635
            header('Content-Encoding: gzip');
636
        }
637
        header('Content-Transfer-Encoding: binary');
638
        if ($length <= 0) {
639
            return;
640
        }
641
642
        header('Content-Length: ' . $length);
643
    }
644
645
    /**
646
     * Returns value of an element in $array given by $path.
647
     * $path is a string describing position of an element in an associative array,
648
     * eg. Servers/1/host refers to $array[Servers][1][host]
649
     *
650
     * @param string $path    path in the array
651
     * @param array  $array   the array
652
     * @param mixed  $default default value
653
     *
654
     * @return array|mixed|null array element or $default
655
     */
656 159
    public static function arrayRead(string $path, array $array, $default = null)
657
    {
658 159
        $keys = explode('/', $path);
659 159
        $value =& $array;
660 159
        foreach ($keys as $key) {
661 159
            if (! isset($value[$key])) {
662 132
                return $default;
663
            }
664 115
            $value =& $value[$key];
665
        }
666
667 103
        return $value;
668
    }
669
670
    /**
671
     * Stores value in an array
672
     *
673
     * @param string $path  path in the array
674
     * @param array  $array the array
675
     * @param mixed  $value value to store
676
     */
677 80
    public static function arrayWrite(string $path, array &$array, $value): void
678
    {
679 80
        $keys = explode('/', $path);
680 80
        $last_key = array_pop($keys);
681 80
        $a =& $array;
682 80
        foreach ($keys as $key) {
683 52
            if (! isset($a[$key])) {
684 52
                $a[$key] = [];
685
            }
686 52
            $a =& $a[$key];
687
        }
688 80
        $a[$last_key] = $value;
689 80
    }
690
691
    /**
692
     * Removes value from an array
693
     *
694
     * @param string $path  path in the array
695
     * @param array  $array the array
696
     */
697 32
    public static function arrayRemove(string $path, array &$array): void
698
    {
699 32
        $keys = explode('/', $path);
700 32
        $keys_last = array_pop($keys);
701 32
        $path = [];
702 32
        $depth = 0;
703
704 32
        $path[0] =& $array;
705 32
        $found = true;
706
        // go as deep as required or possible
707 32
        foreach ($keys as $key) {
708 24
            if (! isset($path[$depth][$key])) {
709 20
                $found = false;
710 20
                break;
711
            }
712 12
            $depth++;
713 12
            $path[$depth] =& $path[$depth - 1][$key];
714
        }
715
        // if element found, remove it
716 32
        if ($found) {
717 28
            unset($path[$depth][$keys_last]);
718 28
            $depth--;
719
        }
720
721
        // remove empty nested arrays
722 32
        for (; $depth >= 0; $depth--) {
723 24
            if (isset($path[$depth + 1]) && count($path[$depth + 1]) !== 0) {
724 12
                break;
725
            }
726
727 20
            unset($path[$depth][$keys[$depth]]);
728
        }
729 32
    }
730
731
    /**
732
     * Returns link to (possibly) external site using defined redirector.
733
     *
734
     * @param string $url URL where to go.
735
     *
736
     * @return string URL for a link.
737
     */
738 422
    public static function linkURL(string $url): string
739
    {
740 422
        if (! preg_match('#^https?://#', $url)) {
741 12
            return $url;
742
        }
743
744 414
        $params = [];
745 414
        $params['url'] = $url;
746
747 414
        $url = Url::getCommon($params);
748
        //strip off token and such sensitive information. Just keep url.
749 414
        $arr = parse_url($url);
750
751 414
        if (! is_array($arr)) {
0 ignored issues
show
introduced by
The condition is_array($arr) is always true.
Loading history...
752
            $arr = [];
753
        }
754
755 414
        parse_str($arr['query'] ?? '', $vars);
756 414
        $query = http_build_query(['url' => $vars['url']]);
757
758 414
        if ($GLOBALS['PMA_Config'] !== null && $GLOBALS['PMA_Config']->get('is_setup')) {
759
            $url = '../url.php?' . $query;
760
        } else {
761 414
            $url = './url.php?' . $query;
762
        }
763
764 414
        return $url;
765
    }
766
767
    /**
768
     * Checks whether domain of URL is an allowed domain or not.
769
     * Use only for URLs of external sites.
770
     *
771
     * @param string $url URL of external site.
772
     *
773
     * @return bool True: if domain of $url is allowed domain,
774
     * False: otherwise.
775
     */
776 32
    public static function isAllowedDomain(string $url): bool
777
    {
778 32
        $arr = parse_url($url);
779
780 32
        if (! is_array($arr)) {
0 ignored issues
show
introduced by
The condition is_array($arr) is always true.
Loading history...
781
            $arr = [];
782
        }
783
784
        // We need host to be set
785 32
        if (! isset($arr['host']) || strlen($arr['host']) == 0) {
786 4
            return false;
787
        }
788
        // We do not want these to be present
789
        $blocked = [
790 28
            'user',
791
            'pass',
792
            'port',
793
        ];
794 28
        foreach ($blocked as $part) {
795 28
            if (isset($arr[$part]) && strlen((string) $arr[$part]) != 0) {
796 19
                return false;
797
            }
798
        }
799 12
        $domain = $arr['host'];
800
        $domainAllowList = [
801
            /* Include current domain */
802 12
            $_SERVER['SERVER_NAME'],
803
            /* phpMyAdmin domains */
804 12
            'wiki.phpmyadmin.net',
805 12
            'www.phpmyadmin.net',
806 12
            'phpmyadmin.net',
807 12
            'demo.phpmyadmin.net',
808 12
            'docs.phpmyadmin.net',
809
            /* mysql.com domains */
810 12
            'dev.mysql.com',
811 12
            'bugs.mysql.com',
812
            /* mariadb domains */
813 12
            'mariadb.org',
814 12
            'mariadb.com',
815
            /* php.net domains */
816 12
            'php.net',
817 12
            'secure.php.net',
818
            /* Github domains*/
819 12
            'github.com',
820 12
            'www.github.com',
821
            /* Percona domains */
822 12
            'www.percona.com',
823
            /* Following are doubtful ones. */
824 12
            'mysqldatabaseadministration.blogspot.com',
825
        ];
826
827 12
        return in_array($domain, $domainAllowList);
828
    }
829
830
    /**
831
     * Replace some html-unfriendly stuff
832
     *
833
     * @param string $buffer String to process
834
     *
835
     * @return string Escaped and cleaned up text suitable for html
836
     */
837 20
    public static function mimeDefaultFunction(string $buffer): string
838
    {
839 20
        $buffer = htmlspecialchars($buffer);
840 20
        $buffer = str_replace('  ', ' &nbsp;', $buffer);
841
842 20
        return preg_replace("@((\015\012)|(\015)|(\012))@", '<br>' . "\n", $buffer);
843
    }
844
845
    /**
846
     * Displays SQL query before executing.
847
     *
848
     * @param array|string $query_data Array containing queries or query itself
849
     */
850
    public static function previewSQL($query_data): void
851
    {
852
        $retval = '<div class="preview_sql">';
853
        if (empty($query_data)) {
854
            $retval .= __('No change');
855
        } elseif (is_array($query_data)) {
856
            foreach ($query_data as $query) {
857
                $retval .= Html\Generator::formatSql($query);
858
            }
859
        } else {
860
            $retval .= Html\Generator::formatSql($query_data);
861
        }
862
        $retval .= '</div>';
863
        $response = Response::getInstance();
864
        $response->addJSON('sql_data', $retval);
865
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
866
    }
867
868
    /**
869
     * recursively check if variable is empty
870
     *
871
     * @param mixed $value the variable
872
     *
873
     * @return bool true if empty
874
     */
875
    public static function emptyRecursive($value): bool
876
    {
877
        $empty = true;
878
        if (is_array($value)) {
879
            array_walk_recursive(
880
                $value,
881
                /**
882
                 * @param mixed $item
883
                 */
884
                static function ($item) use (&$empty) {
885
                    $empty = $empty && empty($item);
886
                }
887
            );
888
        } else {
889
            $empty = empty($value);
890
        }
891
892
        return $empty;
893
    }
894
895
    /**
896
     * Creates some globals from $_POST variables matching a pattern
897
     *
898
     * @param array $post_patterns The patterns to search for
899
     */
900
    public static function setPostAsGlobal(array $post_patterns): void
901
    {
902
        global $containerBuilder;
903
904
        foreach (array_keys($_POST) as $post_key) {
905
            foreach ($post_patterns as $one_post_pattern) {
906
                if (! preg_match($one_post_pattern, $post_key)) {
907
                    continue;
908
                }
909
910
                $GLOBALS[$post_key] = $_POST[$post_key];
911
                $containerBuilder->setParameter($post_key, $GLOBALS[$post_key]);
912
            }
913
        }
914
    }
915
916
    public static function setDatabaseAndTableFromRequest(ContainerBuilder $containerBuilder): void
917
    {
918
        global $db, $table, $url_params;
919
920
        $databaseFromRequest = $_POST['db'] ?? $_GET['db'] ?? $_REQUEST['db'] ?? null;
921
        $tableFromRequest = $_POST['table'] ?? $_GET['table'] ?? $_REQUEST['table'] ?? null;
922
923
        $db = self::isValid($databaseFromRequest) ? $databaseFromRequest : '';
924
        $table = self::isValid($tableFromRequest) ? $tableFromRequest : '';
925
926
        $url_params['db'] = $db;
927
        $url_params['table'] = $table;
928
        $containerBuilder->setParameter('db', $db);
929
        $containerBuilder->setParameter('table', $table);
930
        $containerBuilder->setParameter('url_params', $url_params);
931
    }
932
933
    /**
934
     * PATH_INFO could be compromised if set, so remove it from PHP_SELF
935
     * and provide a clean PHP_SELF here
936
     */
937 32
    public static function cleanupPathInfo(): void
938
    {
939 32
        global $PMA_PHP_SELF;
940
941 32
        $PMA_PHP_SELF = self::getenv('PHP_SELF');
942 32
        if (empty($PMA_PHP_SELF)) {
943 20
            $PMA_PHP_SELF = urldecode(self::getenv('REQUEST_URI'));
944
        }
945 32
        $_PATH_INFO = self::getenv('PATH_INFO');
946 32
        if (! empty($_PATH_INFO) && ! empty($PMA_PHP_SELF)) {
947 12
            $question_pos = mb_strpos($PMA_PHP_SELF, '?');
948 12
            if ($question_pos != false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $question_pos of type integer to the boolean false. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
949 4
                $PMA_PHP_SELF = mb_substr($PMA_PHP_SELF, 0, $question_pos);
950
            }
951 12
            $path_info_pos = mb_strrpos($PMA_PHP_SELF, $_PATH_INFO);
952 12
            if ($path_info_pos !== false) {
953 12
                $path_info_part = mb_substr($PMA_PHP_SELF, $path_info_pos, mb_strlen($_PATH_INFO));
954 12
                if ($path_info_part == $_PATH_INFO) {
955 12
                    $PMA_PHP_SELF = mb_substr($PMA_PHP_SELF, 0, $path_info_pos);
956
                }
957
            }
958
        }
959
960 32
        $path = [];
961 32
        foreach (explode('/', $PMA_PHP_SELF) as $part) {
962
            // ignore parts that have no value
963 32
            if (empty($part) || $part === '.') {
964 32
                continue;
965
            }
966
967 32
            if ($part !== '..') {
968
                // cool, we found a new part
969 32
                $path[] = $part;
970 8
            } elseif (count($path) > 0) {
971
                // going back up? sure
972 14
                array_pop($path);
973
            }
974
            // Here we intentionall ignore case where we go too up
975
            // as there is nothing sane to do
976
        }
977
978 32
        $PMA_PHP_SELF = htmlspecialchars('/' . implode('/', $path));
979 32
    }
980
981
    /**
982
     * Checks that required PHP extensions are there.
983
     */
984
    public static function checkExtensions(): void
985
    {
986
        /**
987
         * Warning about mbstring.
988
         */
989
        if (! function_exists('mb_detect_encoding')) {
990
            self::warnMissingExtension('mbstring');
991
        }
992
993
        /**
994
         * We really need this one!
995
         */
996
        if (! function_exists('preg_replace')) {
997
            self::warnMissingExtension('pcre', true);
998
        }
999
1000
        /**
1001
         * JSON is required in several places.
1002
         */
1003
        if (! function_exists('json_encode')) {
1004
            self::warnMissingExtension('json', true);
1005
        }
1006
1007
        /**
1008
         * ctype is required for Twig.
1009
         */
1010
        if (! function_exists('ctype_alpha')) {
1011
            self::warnMissingExtension('ctype', true);
1012
        }
1013
1014
        /**
1015
         * hash is required for cookie authentication.
1016
         */
1017
        if (function_exists('hash_hmac')) {
1018
            return;
1019
        }
1020
1021
        self::warnMissingExtension('hash', true);
1022
    }
1023
1024
    /**
1025
     * Gets the "true" IP address of the current user
1026
     *
1027
     * @return string|bool the ip of the user
1028
     *
1029
     * @access private
1030
     */
1031 108
    public static function getIp()
1032
    {
1033
        /* Get the address of user */
1034 108
        if (empty($_SERVER['REMOTE_ADDR'])) {
1035
            /* We do not know remote IP */
1036 56
            return false;
1037
        }
1038
1039 56
        $direct_ip = $_SERVER['REMOTE_ADDR'];
1040
1041
        /* Do we trust this IP as a proxy? If yes we will use it's header. */
1042 56
        if (! isset($GLOBALS['cfg']['TrustedProxies'][$direct_ip])) {
1043
            /* Return true IP */
1044 44
            return $direct_ip;
1045
        }
1046
1047
        /**
1048
         * Parse header in form:
1049
         * X-Forwarded-For: client, proxy1, proxy2
1050
         */
1051
        // Get header content
1052 12
        $value = self::getenv($GLOBALS['cfg']['TrustedProxies'][$direct_ip]);
1053
        // Grab first element what is client adddress
1054 12
        $value = explode(',', $value)[0];
1055
        // checks that the header contains only one IP address,
1056 12
        $is_ip = filter_var($value, FILTER_VALIDATE_IP);
1057
1058 12
        if ($is_ip !== false) {
1059
            // True IP behind a proxy
1060 8
            return $value;
1061
        }
1062
1063
        // We could not parse header
1064 4
        return false;
1065
    } // end of the 'getIp()' function
1066
1067
    /**
1068
     * Sanitizes MySQL hostname
1069
     *
1070
     * * strips p: prefix(es)
1071
     *
1072
     * @param string $name User given hostname
1073
     */
1074 20
    public static function sanitizeMySQLHost(string $name): string
1075
    {
1076 20
        while (strtolower(substr($name, 0, 2)) == 'p:') {
1077 12
            $name = substr($name, 2);
1078
        }
1079
1080 20
        return $name;
1081
    }
1082
1083
    /**
1084
     * Sanitizes MySQL username
1085
     *
1086
     * * strips part behind null byte
1087
     *
1088
     * @param string $name User given username
1089
     */
1090 28
    public static function sanitizeMySQLUser(string $name): string
1091
    {
1092 28
        $position = strpos($name, chr(0));
1093 28
        if ($position !== false) {
1094
            return substr($name, 0, $position);
1095
        }
1096
1097 28
        return $name;
1098
    }
1099
1100
    /**
1101
     * Safe unserializer wrapper
1102
     *
1103
     * It does not unserialize data containing objects
1104
     *
1105
     * @param string $data Data to unserialize
1106
     *
1107
     * @return mixed|null
1108
     */
1109 40
    public static function safeUnserialize(string $data)
1110
    {
1111 40
        if (! is_string($data)) {
0 ignored issues
show
introduced by
The condition is_string($data) is always true.
Loading history...
1112
            return null;
1113
        }
1114
1115
        /* validate serialized data */
1116 40
        $length = strlen($data);
1117 40
        $depth = 0;
1118 40
        for ($i = 0; $i < $length; $i++) {
1119 40
            $value = $data[$i];
1120
1121 30
            switch ($value) {
1122 40
                case '}':
1123
                    /* end of array */
1124 8
                    if ($depth <= 0) {
1125
                        return null;
1126
                    }
1127 8
                    $depth--;
1128 8
                    break;
1129 40
                case 's':
1130
                    /* string */
1131
                    // parse sting length
1132 20
                    $strlen = intval(substr($data, $i + 2));
1133
                    // string start
1134 20
                    $i = strpos($data, ':', $i + 2);
1135 20
                    if ($i === false) {
1136
                        return null;
1137
                    }
1138
                    // skip string, quotes and ;
1139 20
                    $i += 2 + $strlen + 1;
1140 20
                    if ($data[$i] != ';') {
1141
                        return null;
1142
                    }
1143 20
                    break;
1144
1145 32
                case 'b':
1146 28
                case 'i':
1147 28
                case 'd':
1148
                    /* bool, integer or double */
1149
                    // skip value to sepearator
1150 16
                    $i = strpos($data, ';', $i);
1151 16
                    if ($i === false) {
1152
                        return null;
1153
                    }
1154 16
                    break;
1155 28
                case 'a':
1156
                    /* array */
1157
                    // find array start
1158 16
                    $i = strpos($data, '{', $i);
1159 16
                    if ($i === false) {
1160
                        return null;
1161
                    }
1162
                    // remember nesting
1163 16
                    $depth++;
1164 16
                    break;
1165 20
                case 'N':
1166
                    /* null */
1167
                    // skip to end
1168
                    $i = strpos($data, ';', $i);
1169
                    if ($i === false) {
1170
                        return null;
1171
                    }
1172
                    break;
1173
                default:
1174
                    /* any other elements are not wanted */
1175 20
                    return null;
1176
            }
1177
        }
1178
1179
        // check unterminated arrays
1180 20
        if ($depth > 0) {
1181
            return null;
1182
        }
1183
1184 20
        return unserialize($data);
1185
    }
1186
1187
    /**
1188
     * Applies changes to PHP configuration.
1189
     */
1190
    public static function configure(): void
1191
    {
1192
        /**
1193
         * Set utf-8 encoding for PHP
1194
         */
1195
        ini_set('default_charset', 'utf-8');
1196
        mb_internal_encoding('utf-8');
1197
1198
        /**
1199
         * Set precision to sane value, with higher values
1200
         * things behave slightly unexpectedly, for example
1201
         * round(1.2, 2) returns 1.199999999999999956.
1202
         */
1203
        ini_set('precision', '14');
1204
1205
        /**
1206
         * check timezone setting
1207
         * this could produce an E_WARNING - but only once,
1208
         * if not done here it will produce E_WARNING on every date/time function
1209
         */
1210
        date_default_timezone_set(@date_default_timezone_get());
1211
    }
1212
1213
    /**
1214
     * Check whether PHP configuration matches our needs.
1215
     */
1216
    public static function checkConfiguration(): void
1217
    {
1218
        /**
1219
         * As we try to handle charsets by ourself, mbstring overloads just
1220
         * break it, see bug 1063821.
1221
         *
1222
         * We specifically use empty here as we are looking for anything else than
1223
         * empty value or 0.
1224
         */
1225
        if (extension_loaded('mbstring') && ! empty(ini_get('mbstring.func_overload'))) {
1226
            self::fatalError(
1227
                __(
1228
                    'You have enabled mbstring.func_overload in your PHP '
1229
                    . 'configuration. This option is incompatible with phpMyAdmin '
1230
                    . 'and might cause some data to be corrupted!'
1231
                )
1232
            );
1233
        }
1234
1235
        /**
1236
         * The ini_set and ini_get functions can be disabled using
1237
         * disable_functions but we're relying quite a lot of them.
1238
         */
1239
        if (function_exists('ini_get') && function_exists('ini_set')) {
1240
            return;
1241
        }
1242
1243
        self::fatalError(
1244
            __(
1245
                'The ini_get and/or ini_set functions are disabled in php.ini. '
1246
                . 'phpMyAdmin requires these functions!'
1247
            )
1248
        );
1249
    }
1250
1251
    /**
1252
     * Checks request and fails with fatal error if something problematic is found
1253
     */
1254
    public static function checkRequest(): void
1255
    {
1256
        if (isset($_REQUEST['GLOBALS']) || isset($_FILES['GLOBALS'])) {
1257
            self::fatalError(__('GLOBALS overwrite attempt'));
1258
        }
1259
1260
        /**
1261
         * protect against possible exploits - there is no need to have so much variables
1262
         */
1263
        if (count($_REQUEST) <= 1000) {
1264
            return;
1265
        }
1266
1267
        self::fatalError(__('possible exploit'));
1268
    }
1269
1270
    /**
1271
     * Sign the sql query using hmac using the session token
1272
     *
1273
     * @param string $sqlQuery The sql query
1274
     *
1275
     * @return string
1276
     */
1277 32
    public static function signSqlQuery($sqlQuery)
1278
    {
1279 32
        global $cfg;
1280
1281 32
        $secret = $_SESSION[' HMAC_secret '] ?? '';
1282
1283 32
        return hash_hmac('sha256', $sqlQuery, $secret . $cfg['blowfish_secret']);
1284
    }
1285
1286
    /**
1287
     * Check that the sql query has a valid hmac signature
1288
     *
1289
     * @param string $sqlQuery  The sql query
1290
     * @param string $signature The Signature to check
1291
     *
1292
     * @return bool
1293
     */
1294 24
    public static function checkSqlQuerySignature($sqlQuery, $signature)
1295
    {
1296 24
        global $cfg;
1297
1298 24
        $secret = $_SESSION[' HMAC_secret '] ?? '';
1299 24
        $hmac = hash_hmac('sha256', $sqlQuery, $secret . $cfg['blowfish_secret']);
1300
1301 24
        return hash_equals($hmac, $signature);
1302
    }
1303
1304
    /**
1305
     * Check whether user supplied token is valid, if not remove any possibly
1306
     * dangerous stuff from request.
1307
     *
1308
     * Check for token mismatch only if the Request method is POST.
1309
     * GET Requests would never have token and therefore checking
1310
     * mis-match does not make sense.
1311
     */
1312 4
    public static function checkTokenRequestParam(): void
1313
    {
1314 4
        global $token_mismatch, $token_provided;
1315
1316 4
        $token_mismatch = true;
1317 4
        $token_provided = false;
1318
1319 4
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
1320 4
            return;
1321
        }
1322
1323 4
        if (self::isValid($_POST['token'])) {
1324 4
            $token_provided = true;
1325 4
            $token_mismatch = ! @hash_equals($_SESSION[' PMA_token '], $_POST['token']);
1326
        }
1327
1328 4
        if (! $token_mismatch) {
1329 4
            return;
1330
        }
1331
1332
        // Warn in case the mismatch is result of failed setting of session cookie
1333 4
        if (isset($_POST['set_session']) && $_POST['set_session'] !== session_id()) {
1334
            trigger_error(
1335
                __(
1336
                    'Failed to set session cookie. Maybe you are using '
1337
                    . 'HTTP instead of HTTPS to access phpMyAdmin.'
1338
                ),
1339
                E_USER_ERROR
1340
            );
1341
        }
1342
1343
        /**
1344
         * We don't allow any POST operation parameters if the token is mismatched
1345
         * or is not provided.
1346
         */
1347 4
        $allowList = ['ajax_request'];
1348 4
        Sanitize::removeRequestVars($allowList);
1349 4
    }
1350
}
1351