1<?php
2
3declare(strict_types=1);
4
5namespace PhpMyAdmin;
6
7use PhpMyAdmin\Html\Generator;
8use PhpMyAdmin\Html\MySQLDocumentation;
9use PhpMyAdmin\Query\Utilities;
10use PhpMyAdmin\SqlParser\Components\Expression;
11use PhpMyAdmin\SqlParser\Context;
12use PhpMyAdmin\SqlParser\Token;
13use PhpMyAdmin\Utils\SessionCache;
14use phpseclib\Crypt\Random;
15use stdClass;
16use const ENT_COMPAT;
17use const ENT_QUOTES;
18use const PHP_INT_SIZE;
19use const PHP_MAJOR_VERSION;
20use const PREG_OFFSET_CAPTURE;
21use const STR_PAD_LEFT;
22use function abs;
23use function array_key_exists;
24use function array_map;
25use function array_merge;
26use function array_shift;
27use function array_unique;
28use function basename;
29use function bin2hex;
30use function chr;
31use function class_exists;
32use function count;
33use function ctype_digit;
34use function date;
35use function decbin;
36use function defined;
37use function explode;
38use function extension_loaded;
39use function fclose;
40use function floatval;
41use function floor;
42use function fread;
43use function function_exists;
44use function html_entity_decode;
45use function htmlentities;
46use function htmlspecialchars;
47use function htmlspecialchars_decode;
48use function implode;
49use function in_array;
50use function ini_get;
51use function is_array;
52use function is_callable;
53use function is_object;
54use function is_string;
55use function log10;
56use function mb_detect_encoding;
57use function mb_strlen;
58use function mb_strpos;
59use function mb_strrpos;
60use function mb_strstr;
61use function mb_strtolower;
62use function mb_substr;
63use function number_format;
64use function ord;
65use function parse_url;
66use function pow;
67use function preg_match;
68use function preg_quote;
69use function preg_replace;
70use function range;
71use function reset;
72use function round;
73use function rtrim;
74use function set_time_limit;
75use function sort;
76use function sprintf;
77use function str_pad;
78use function str_replace;
79use function strcasecmp;
80use function strftime;
81use function stripos;
82use function strlen;
83use function strpos;
84use function strrev;
85use function strtolower;
86use function strtoupper;
87use function strtr;
88use function substr;
89use function time;
90use function trim;
91use function uksort;
92use function version_compare;
93
94/**
95 * Misc functions used all over the scripts.
96 */
97class Util
98{
99    /**
100     * Checks whether configuration value tells to show icons.
101     *
102     * @param string $value Configuration option name
103     *
104     * @return bool Whether to show icons.
105     */
106    public static function showIcons($value): bool
107    {
108        return in_array($GLOBALS['cfg'][$value], ['icons', 'both']);
109    }
110
111    /**
112     * Checks whether configuration value tells to show text.
113     *
114     * @param string $value Configuration option name
115     *
116     * @return bool Whether to show text.
117     */
118    public static function showText($value): bool
119    {
120        return in_array($GLOBALS['cfg'][$value], ['text', 'both']);
121    }
122
123    /**
124     * Returns the formatted maximum size for an upload
125     *
126     * @param int|float|string $max_upload_size the size
127     *
128     * @return string the message
129     *
130     * @access public
131     */
132    public static function getFormattedMaximumUploadSize($max_upload_size): string
133    {
134        // I have to reduce the second parameter (sensitiveness) from 6 to 4
135        // to avoid weird results like 512 kKib
136        [$max_size, $max_unit] = self::formatByteDown($max_upload_size, 4);
137
138        return '(' . sprintf(__('Max: %s%s'), $max_size, $max_unit) . ')';
139    }
140
141    /**
142     * Add slashes before "_" and "%" characters for using them in MySQL
143     * database, table and field names.
144     * Note: This function does not escape backslashes!
145     *
146     * @param string $name the string to escape
147     *
148     * @return string the escaped string
149     *
150     * @access public
151     */
152    public static function escapeMysqlWildcards($name): string
153    {
154        return strtr($name, ['_' => '\\_', '%' => '\\%']);
155    }
156
157    /**
158     * removes slashes before "_" and "%" characters
159     * Note: This function does not unescape backslashes!
160     *
161     * @param string $name the string to escape
162     *
163     * @return string the escaped string
164     *
165     * @access public
166     */
167    public static function unescapeMysqlWildcards($name): string
168    {
169        return strtr($name, ['\\_' => '_', '\\%' => '%']);
170    }
171
172    /**
173     * removes quotes (',",`) from a quoted string
174     *
175     * checks if the string is quoted and removes this quotes
176     *
177     * @param string $quoted_string string to remove quotes from
178     * @param string $quote         type of quote to remove
179     *
180     * @return string unquoted string
181     */
182    public static function unQuote(string $quoted_string, ?string $quote = null): string
183    {
184        $quotes = [];
185
186        if ($quote === null) {
187            $quotes[] = '`';
188            $quotes[] = '"';
189            $quotes[] = "'";
190        } else {
191            $quotes[] = $quote;
192        }
193
194        foreach ($quotes as $quote) {
195            if (mb_substr($quoted_string, 0, 1) === $quote
196                && mb_substr($quoted_string, -1, 1) === $quote
197            ) {
198                $unquoted_string = mb_substr($quoted_string, 1, -1);
199                // replace escaped quotes
200                $unquoted_string = str_replace(
201                    $quote . $quote,
202                    $quote,
203                    $unquoted_string
204                );
205
206                return $unquoted_string;
207            }
208        }
209
210        return $quoted_string;
211    }
212
213    /**
214     * Get a URL link to the official MySQL documentation
215     *
216     * @param string $link   contains name of page/anchor that is being linked
217     * @param string $anchor anchor to page part
218     *
219     * @return string  the URL link
220     *
221     * @access public
222     */
223    public static function getMySQLDocuURL(string $link, string $anchor = ''): string
224    {
225        global $dbi;
226
227        // Fixup for newly used names:
228        $link = str_replace('_', '-', mb_strtolower($link));
229
230        if (empty($link)) {
231            $link = 'index';
232        }
233        $mysql = '5.5';
234        $lang = 'en';
235        if (isset($dbi)) {
236            $serverVersion = $dbi->getVersion();
237            if ($serverVersion >= 80000) {
238                $mysql = '8.0';
239            } elseif ($serverVersion >= 50700) {
240                $mysql = '5.7';
241            } elseif ($serverVersion >= 50600) {
242                $mysql = '5.6';
243            } elseif ($serverVersion >= 50500) {
244                $mysql = '5.5';
245            }
246        }
247        $url = 'https://dev.mysql.com/doc/refman/'
248            . $mysql . '/' . $lang . '/' . $link . '.html';
249        if (! empty($anchor)) {
250            $url .= '#' . $anchor;
251        }
252
253        return Core::linkURL($url);
254    }
255
256    /**
257     * Get a URL link to the official documentation page of either MySQL
258     * or MariaDB depending on the database server
259     * of the user.
260     *
261     * @param bool $isMariaDB if the database server is MariaDB
262     *
263     * @return string The URL link
264     */
265    public static function getDocuURL(bool $isMariaDB = false): string
266    {
267        if ($isMariaDB) {
268            $url = 'https://mariadb.com/kb/en/documentation/';
269
270            return Core::linkURL($url);
271        }
272
273        return self::getMySQLDocuURL('');
274    }
275
276    /**
277     * Check the correct row count
278     *
279     * @param string $db    the db name
280     * @param array  $table the table infos
281     *
282     * @return int the possibly modified row count
283     */
284    private static function checkRowCount($db, array $table)
285    {
286        global $dbi;
287
288        $rowCount = 0;
289
290        if ($table['Rows'] === null) {
291            // Do not check exact row count here,
292            // if row count is invalid possibly the table is defect
293            // and this would break the navigation panel;
294            // but we can check row count if this is a view or the
295            // information_schema database
296            // since Table::countRecords() returns a limited row count
297            // in this case.
298
299            // set this because Table::countRecords() can use it
300            $tbl_is_view = $table['TABLE_TYPE'] === 'VIEW';
301
302            if ($tbl_is_view || Utilities::isSystemSchema($db)) {
303                $rowCount = $dbi
304                    ->getTable($db, $table['Name'])
305                    ->countRecords();
306            }
307        }
308
309        return $rowCount;
310    }
311
312    /**
313     * returns array with tables of given db with extended information and grouped
314     *
315     * @param string $db
316     *
317     * @return array (recursive) grouped table list
318     */
319    public static function getTableList($db): array
320    {
321        global $dbi;
322
323        $sep = $GLOBALS['cfg']['NavigationTreeTableSeparator'];
324
325        $tables = $dbi->getTablesFull($db);
326
327        if ($GLOBALS['cfg']['NaturalOrder']) {
328            uksort($tables, 'strnatcasecmp');
329        }
330
331        if (count($tables) < 1) {
332            return $tables;
333        }
334
335        $default = [
336            'Name'      => '',
337            'Rows'      => 0,
338            'Comment'   => '',
339            'disp_name' => '',
340        ];
341
342        $table_groups = [];
343
344        foreach ($tables as $table_name => $table) {
345            $table['Rows'] = self::checkRowCount($db, $table);
346
347            // in $group we save the reference to the place in $table_groups
348            // where to store the table info
349            if ($GLOBALS['cfg']['NavigationTreeEnableGrouping']
350                && $sep && mb_strstr($table_name, $sep)
351            ) {
352                $parts = explode($sep, $table_name);
353
354                $group =& $table_groups;
355                $i = 0;
356                $group_name_full = '';
357                $parts_cnt = count($parts) - 1;
358
359                while (($i < $parts_cnt)
360                    && ($i < $GLOBALS['cfg']['NavigationTreeTableLevel'])
361                ) {
362                    $group_name = $parts[$i] . $sep;
363                    $group_name_full .= $group_name;
364
365                    if (! isset($group[$group_name])) {
366                        $group[$group_name] = [];
367                        $group[$group_name]['is' . $sep . 'group'] = true;
368                        $group[$group_name]['tab' . $sep . 'count'] = 1;
369                        $group[$group_name]['tab' . $sep . 'group']
370                            = $group_name_full;
371                    } elseif (! isset($group[$group_name]['is' . $sep . 'group'])) {
372                        $table = $group[$group_name];
373                        $group[$group_name] = [];
374                        $group[$group_name][$group_name] = $table;
375                        $group[$group_name]['is' . $sep . 'group'] = true;
376                        $group[$group_name]['tab' . $sep . 'count'] = 1;
377                        $group[$group_name]['tab' . $sep . 'group']
378                            = $group_name_full;
379                    } else {
380                        $group[$group_name]['tab' . $sep . 'count']++;
381                    }
382
383                    $group =& $group[$group_name];
384                    $i++;
385                }
386            } else {
387                if (! isset($table_groups[$table_name])) {
388                    $table_groups[$table_name] = [];
389                }
390                $group =& $table_groups;
391            }
392
393            $table['disp_name'] = $table['Name'];
394            $group[$table_name] = array_merge($default, $table);
395        }
396
397        return $table_groups;
398    }
399
400    /* ----------------------- Set of misc functions ----------------------- */
401
402    /**
403     * Adds backquotes on both sides of a database, table or field name.
404     * and escapes backquotes inside the name with another backquote
405     *
406     * example:
407     * <code>
408     * echo backquote('owner`s db'); // `owner``s db`
409     *
410     * </code>
411     *
412     * @param array|string $a_name the database, table or field name to "backquote"
413     *                             or array of it
414     * @param bool         $do_it  a flag to bypass this function (used by dump
415     *                             functions)
416     *
417     * @return mixed the "backquoted" database, table or field name
418     *
419     * @access public
420     */
421    public static function backquote($a_name, $do_it = true)
422    {
423        return static::backquoteCompat($a_name, 'NONE', $do_it);
424    }
425
426    /**
427     * Adds backquotes on both sides of a database, table or field name.
428     * in compatibility mode
429     *
430     * example:
431     * <code>
432     * echo backquoteCompat('owner`s db'); // `owner``s db`
433     *
434     * </code>
435     *
436     * @param array|string $a_name        the database, table or field name to
437     *                                    "backquote" or array of it
438     * @param string       $compatibility string compatibility mode (used by dump
439     *                                    functions)
440     * @param bool         $do_it         a flag to bypass this function (used by dump
441     *                                    functions)
442     *
443     * @return mixed the "backquoted" database, table or field name
444     *
445     * @access public
446     */
447    public static function backquoteCompat(
448        $a_name,
449        string $compatibility = 'MSSQL',
450        $do_it = true
451    ) {
452        if (is_array($a_name)) {
453            foreach ($a_name as &$data) {
454                $data = self::backquoteCompat($data, $compatibility, $do_it);
455            }
456
457            return $a_name;
458        }
459
460        if (! $do_it) {
461            if (! (Context::isKeyword($a_name) & Token::FLAG_KEYWORD_RESERVED)) {
462                return $a_name;
463            }
464        }
465
466        // @todo add more compatibility cases (ORACLE for example)
467        switch ($compatibility) {
468            case 'MSSQL':
469                $quote = '"';
470                $escapeChar = '\\';
471                break;
472            default:
473                $quote = '`';
474                $escapeChar = '`';
475                break;
476        }
477
478        // '0' is also empty for php :-(
479        if (strlen((string) $a_name) > 0 && $a_name !== '*') {
480            return $quote . str_replace($quote, $escapeChar . $quote, (string) $a_name) . $quote;
481        }
482
483        return $a_name;
484    }
485
486    /**
487     * Formats $value to byte view
488     *
489     * @param float|int|string|null $value the value to format
490     * @param int                   $limes the sensitiveness
491     * @param int                   $comma the number of decimals to retain
492     *
493     * @return array|null the formatted value and its unit
494     *
495     * @access public
496     */
497    public static function formatByteDown($value, $limes = 6, $comma = 0): ?array
498    {
499        if ($value === null) {
500            return null;
501        }
502
503        if (is_string($value)) {
504            $value = (float) $value;
505        }
506
507        $byteUnits = [
508            /* l10n: shortcuts for Byte */
509            __('B'),
510            /* l10n: shortcuts for Kilobyte */
511            __('KiB'),
512            /* l10n: shortcuts for Megabyte */
513            __('MiB'),
514            /* l10n: shortcuts for Gigabyte */
515            __('GiB'),
516            /* l10n: shortcuts for Terabyte */
517            __('TiB'),
518            /* l10n: shortcuts for Petabyte */
519            __('PiB'),
520            /* l10n: shortcuts for Exabyte */
521            __('EiB'),
522        ];
523
524        $dh = pow(10, $comma);
525        $li = pow(10, $limes);
526        $unit = $byteUnits[0];
527
528        for ($d = 6, $ex = 15; $d >= 1; $d--, $ex -= 3) {
529            $unitSize = $li * pow(10, $ex);
530            if (isset($byteUnits[$d]) && $value >= $unitSize) {
531                // use 1024.0 to avoid integer overflow on 64-bit machines
532                $value = round($value / (pow(1024, $d) / $dh)) / $dh;
533                $unit = $byteUnits[$d];
534                break 1;
535            }
536        }
537
538        if ($unit != $byteUnits[0]) {
539            // if the unit is not bytes (as represented in current language)
540            // reformat with max length of 5
541            // 4th parameter=true means do not reformat if value < 1
542            $return_value = self::formatNumber($value, 5, $comma, true, false);
543        } else {
544            // do not reformat, just handle the locale
545            $return_value = self::formatNumber($value, 0);
546        }
547
548        return [
549            trim($return_value),
550            $unit,
551        ];
552    }
553
554    /**
555     * Formats $value to the given length and appends SI prefixes
556     * with a $length of 0 no truncation occurs, number is only formatted
557     * to the current locale
558     *
559     * examples:
560     * <code>
561     * echo formatNumber(123456789, 6);     // 123,457 k
562     * echo formatNumber(-123456789, 4, 2); //    -123.46 M
563     * echo formatNumber(-0.003, 6);        //      -3 m
564     * echo formatNumber(0.003, 3, 3);      //       0.003
565     * echo formatNumber(0.00003, 3, 2);    //       0.03 m
566     * echo formatNumber(0, 6);             //       0
567     * </code>
568     *
569     * @param float|int|string $value          the value to format
570     * @param int              $digits_left    number of digits left of the comma
571     * @param int              $digits_right   number of digits right of the comma
572     * @param bool             $only_down      do not reformat numbers below 1
573     * @param bool             $noTrailingZero removes trailing zeros right of the comma (default: true)
574     *
575     * @return string   the formatted value and its unit
576     *
577     * @access public
578     */
579    public static function formatNumber(
580        $value,
581        $digits_left = 3,
582        $digits_right = 0,
583        $only_down = false,
584        $noTrailingZero = true
585    ) {
586        if ($value == 0) {
587            return '0';
588        }
589
590        if (is_string($value)) {
591            $value = (float) $value;
592        }
593
594        $originalValue = $value;
595        //number_format is not multibyte safe, str_replace is safe
596        if ($digits_left === 0) {
597            $value = number_format(
598                (float) $value,
599                $digits_right,
600                /* l10n: Decimal separator */
601                __('.'),
602                /* l10n: Thousands separator */
603                __(',')
604            );
605            if (($originalValue != 0) && (floatval($value) == 0)) {
606                $value = ' <' . (1 / pow(10, $digits_right));
607            }
608
609            return $value;
610        }
611
612        // this units needs no translation, ISO
613        $units = [
614            -8 => 'y',
615            -7 => 'z',
616            -6 => 'a',
617            -5 => 'f',
618            -4 => 'p',
619            -3 => 'n',
620            -2 => 'µ',
621            -1 => 'm',
622            0 => ' ',
623            1 => 'k',
624            2 => 'M',
625            3 => 'G',
626            4 => 'T',
627            5 => 'P',
628            6 => 'E',
629            7 => 'Z',
630            8 => 'Y',
631        ];
632        /* l10n: Decimal separator */
633        $decimal_sep = __('.');
634        /* l10n: Thousands separator */
635        $thousands_sep = __(',');
636
637        // check for negative value to retain sign
638        if ($value < 0) {
639            $sign = '-';
640            $value = abs($value);
641        } else {
642            $sign = '';
643        }
644
645        $dh = pow(10, $digits_right);
646
647        /*
648         * This gives us the right SI prefix already,
649         * but $digits_left parameter not incorporated
650         */
651        $d = floor(log10((float) $value) / 3);
652        /*
653         * Lowering the SI prefix by 1 gives us an additional 3 zeros
654         * So if we have 3,6,9,12.. free digits ($digits_left - $cur_digits)
655         * to use, then lower the SI prefix
656         */
657        $cur_digits = floor(log10($value / pow(1000, $d)) + 1);
658        if ($digits_left > $cur_digits) {
659            $d -= floor(($digits_left - $cur_digits) / 3);
660        }
661
662        if ($d < 0 && $only_down) {
663            $d = 0;
664        }
665
666        $value = round($value / (pow(1000, $d) / $dh)) / $dh;
667        $unit = $units[$d];
668
669        // number_format is not multibyte safe, str_replace is safe
670        $formattedValue = number_format(
671            $value,
672            $digits_right,
673            $decimal_sep,
674            $thousands_sep
675        );
676        // If we don't want any zeros, remove them now
677        if ($noTrailingZero && strpos($formattedValue, $decimal_sep) !== false) {
678            $formattedValue = preg_replace('/' . preg_quote($decimal_sep, '/') . '?0+$/', '', $formattedValue);
679        }
680
681        if ($originalValue != 0 && floatval($value) == 0) {
682            return ' <' . number_format(
683                1 / pow(10, $digits_right),
684                $digits_right,
685                $decimal_sep,
686                $thousands_sep
687            )
688            . ' ' . $unit;
689        }
690
691        return $sign . $formattedValue . ' ' . $unit;
692    }
693
694    /**
695     * Returns the number of bytes when a formatted size is given
696     *
697     * @param string|int $formatted_size the size expression (for example 8MB)
698     *
699     * @return int|float The numerical part of the expression (for example 8)
700     */
701    public static function extractValueFromFormattedSize($formatted_size)
702    {
703        $return_value = -1;
704
705        $formatted_size = (string) $formatted_size;
706
707        if (preg_match('/^[0-9]+GB$/', $formatted_size)) {
708            $return_value = (int) mb_substr(
709                $formatted_size,
710                0,
711                -2
712            ) * pow(1024, 3);
713        } elseif (preg_match('/^[0-9]+MB$/', $formatted_size)) {
714            $return_value = (int) mb_substr(
715                $formatted_size,
716                0,
717                -2
718            ) * pow(1024, 2);
719        } elseif (preg_match('/^[0-9]+K$/', $formatted_size)) {
720            $return_value = (int) mb_substr(
721                $formatted_size,
722                0,
723                -1
724            ) * pow(1024, 1);
725        }
726
727        return $return_value;
728    }
729
730    /**
731     * Writes localised date
732     *
733     * @param int    $timestamp the current timestamp
734     * @param string $format    format
735     *
736     * @return string   the formatted date
737     *
738     * @access public
739     */
740    public static function localisedDate($timestamp = -1, $format = '')
741    {
742        $month = [
743            /* l10n: Short month name */
744            __('Jan'),
745            /* l10n: Short month name */
746            __('Feb'),
747            /* l10n: Short month name */
748            __('Mar'),
749            /* l10n: Short month name */
750            __('Apr'),
751            /* l10n: Short month name */
752            _pgettext('Short month name', 'May'),
753            /* l10n: Short month name */
754            __('Jun'),
755            /* l10n: Short month name */
756            __('Jul'),
757            /* l10n: Short month name */
758            __('Aug'),
759            /* l10n: Short month name */
760            __('Sep'),
761            /* l10n: Short month name */
762            __('Oct'),
763            /* l10n: Short month name */
764            __('Nov'),
765            /* l10n: Short month name */
766            __('Dec'),
767        ];
768        $day_of_week = [
769            /* l10n: Short week day name for Sunday */
770            _pgettext('Short week day name for Sunday', 'Sun'),
771            /* l10n: Short week day name for Monday */
772            __('Mon'),
773            /* l10n: Short week day name for Tuesday */
774            __('Tue'),
775            /* l10n: Short week day name for Wednesday */
776            __('Wed'),
777            /* l10n: Short week day name for Thursday */
778            __('Thu'),
779            /* l10n: Short week day name for Friday */
780            __('Fri'),
781            /* l10n: Short week day name for Saturday */
782            __('Sat'),
783        ];
784
785        if ($format == '') {
786            /* l10n: See https://www.php.net/manual/en/function.strftime.php */
787            $format = __('%B %d, %Y at %I:%M %p');
788        }
789
790        if ($timestamp == -1) {
791            $timestamp = time();
792        }
793
794        $date = (string) preg_replace(
795            '@%[aA]@',
796            $day_of_week[(int) @strftime('%w', (int) $timestamp)],
797            $format
798        );
799        $date = (string) preg_replace(
800            '@%[bB]@',
801            $month[(int) @strftime('%m', (int) $timestamp) - 1],
802            $date
803        );
804
805        /* Fill in AM/PM */
806        $hours = (int) date('H', (int) $timestamp);
807        if ($hours >= 12) {
808            $am_pm = _pgettext('AM/PM indication in time', 'PM');
809        } else {
810            $am_pm = _pgettext('AM/PM indication in time', 'AM');
811        }
812        $date = (string) preg_replace('@%[pP]@', $am_pm, $date);
813
814        // Can return false on windows for Japanese language
815        // See https://github.com/phpmyadmin/phpmyadmin/issues/15830
816        $ret = @strftime($date, (int) $timestamp);
817        // Some OSes such as Win8.1 Traditional Chinese version did not produce UTF-8
818        // output here. See https://github.com/phpmyadmin/phpmyadmin/issues/10598
819        if ($ret === false
820            || mb_detect_encoding($ret, 'UTF-8', true) !== 'UTF-8'
821        ) {
822            $ret = date('Y-m-d H:i:s', (int) $timestamp);
823        }
824
825        return $ret;
826    }
827
828    /**
829     * Splits a URL string by parameter
830     *
831     * @param string $url the URL
832     *
833     * @return array<int, string> the parameter/value pairs, for example [0] db=sakila
834     */
835    public static function splitURLQuery($url): array
836    {
837        // decode encoded url separators
838        $separator = Url::getArgSeparator();
839        // on most places separator is still hard coded ...
840        if ($separator !== '&') {
841            // ... so always replace & with $separator
842            $url = str_replace([htmlentities('&'), '&'], [$separator, $separator], $url);
843        }
844
845        $url = str_replace(htmlentities($separator), $separator, $url);
846        // end decode
847
848        $url_parts = parse_url($url);
849
850        if (is_array($url_parts) && isset($url_parts['query'])) {
851            $array = explode($separator, $url_parts['query']);
852
853            return is_array($array) ? $array : [];
854        }
855
856        return [];
857    }
858
859    /**
860     * Returns a given timespan value in a readable format.
861     *
862     * @param int $seconds the timespan
863     *
864     * @return string the formatted value
865     */
866    public static function timespanFormat($seconds): string
867    {
868        $days = floor($seconds / 86400);
869        if ($days > 0) {
870            $seconds -= $days * 86400;
871        }
872
873        $hours = floor($seconds / 3600);
874        if ($days > 0 || $hours > 0) {
875            $seconds -= $hours * 3600;
876        }
877
878        $minutes = floor($seconds / 60);
879        if ($days > 0 || $hours > 0 || $minutes > 0) {
880            $seconds -= $minutes * 60;
881        }
882
883        return sprintf(
884            __('%s days, %s hours, %s minutes and %s seconds'),
885            (string) $days,
886            (string) $hours,
887            (string) $minutes,
888            (string) $seconds
889        );
890    }
891
892    /**
893     * Function added to avoid path disclosures.
894     * Called by each script that needs parameters, it displays
895     * an error message and, by default, stops the execution.
896     *
897     * @param string[] $params  The names of the parameters needed by the calling
898     *                          script
899     * @param bool     $request Check parameters in request
900     *
901     * @access public
902     */
903    public static function checkParameters($params, $request = false): void
904    {
905        $reported_script_name = basename($GLOBALS['PMA_PHP_SELF']);
906        $found_error = false;
907        $error_message = '';
908        if ($request) {
909            $array = $_REQUEST;
910        } else {
911            $array = $GLOBALS;
912        }
913
914        foreach ($params as $param) {
915            if (isset($array[$param])) {
916                continue;
917            }
918
919            $error_message .= $reported_script_name
920                . ': ' . __('Missing parameter:') . ' '
921                . $param
922                . MySQLDocumentation::showDocumentation('faq', 'faqmissingparameters', true)
923                . '[br]';
924            $found_error = true;
925        }
926        if (! $found_error) {
927            return;
928        }
929
930        Core::fatalError($error_message);
931    }
932
933    /**
934     * Build a condition and with a value
935     *
936     * @param string|int|float|null $row          The row value
937     * @param stdClass              $meta         The field metadata
938     * @param string                $fieldFlags   The field flags
939     * @param int                   $fieldsCount  A number of fields
940     * @param string                $conditionKey A key used for BINARY fields functions
941     * @param string                $condition    The condition
942     *
943     * @return array<int,string|null>
944     */
945    private static function getConditionValue(
946        $row,
947        stdClass $meta,
948        string $fieldFlags,
949        int $fieldsCount,
950        string $conditionKey,
951        string $condition
952    ): array {
953        global $dbi;
954
955        if ($row === null) {
956            return ['IS NULL', $condition];
957        }
958
959        $conditionValue = '';
960        $isBinaryString = $meta->type === 'string' && stripos($fieldFlags, 'BINARY') !== false;
961        // 63 is the binary charset, see: https://dev.mysql.com/doc/internals/en/charsets.html
962        $isBlobAndIsBinaryCharset = $meta->type === 'blob' && $meta->charsetnr === 63;
963        // timestamp is numeric on some MySQL 4.1
964        // for real we use CONCAT above and it should compare to string
965        if ($meta->numeric
966            && ($meta->type !== 'timestamp')
967            && ($meta->type !== 'real')
968        ) {
969            $conditionValue = '= ' . $row;
970        } elseif ($isBlobAndIsBinaryCharset || (! empty($row) && $isBinaryString)) {
971            // hexify only if this is a true not empty BLOB or a BINARY
972
973            // do not waste memory building a too big condition
974            $rowLength = mb_strlen((string) $row);
975            if ($rowLength > 0 && $rowLength < 1000) {
976                // use a CAST if possible, to avoid problems
977                // if the field contains wildcard characters % or _
978                $conditionValue = '= CAST(0x' . bin2hex((string) $row) . ' AS BINARY)';
979            } elseif ($fieldsCount === 1) {
980                // when this blob is the only field present
981                // try settling with length comparison
982                $condition = ' CHAR_LENGTH(' . $conditionKey . ') ';
983                $conditionValue = ' = ' . $rowLength;
984            } else {
985                // this blob won't be part of the final condition
986                $conditionValue = null;
987            }
988        } elseif (in_array($meta->type, self::getGISDatatypes())
989            && ! empty($row)
990        ) {
991            // do not build a too big condition
992            if (mb_strlen((string) $row) < 5000) {
993                $condition .= '=0x' . bin2hex((string) $row) . ' AND';
994            } else {
995                $condition = '';
996            }
997        } elseif ($meta->type === 'bit') {
998            $conditionValue = "= b'"
999                . self::printableBitValue((int) $row, (int) $meta->length) . "'";
1000        } else {
1001            $conditionValue = '= \''
1002                . $dbi->escapeString($row) . '\'';
1003        }
1004
1005        return [$conditionValue, $condition];
1006    }
1007
1008    /**
1009     * Function to generate unique condition for specified row.
1010     *
1011     * @param resource|int $handle            current query result
1012     * @param int          $fields_cnt        number of fields
1013     * @param stdClass[]   $fields_meta       meta information about fields
1014     * @param array        $row               current row
1015     * @param bool         $force_unique      generate condition only on pk or unique
1016     * @param string|bool  $restrict_to_table restrict the unique condition to this table or false if none
1017     * @param Expression[] $expressions       An array of Expression instances.
1018     *
1019     * @return array the calculated condition and whether condition is unique
1020     */
1021    public static function getUniqueCondition(
1022        $handle,
1023        $fields_cnt,
1024        array $fields_meta,
1025        array $row,
1026        $force_unique = false,
1027        $restrict_to_table = false,
1028        array $expressions = []
1029    ): array {
1030        global $dbi;
1031
1032        $primary_key          = '';
1033        $unique_key           = '';
1034        $nonprimary_condition = '';
1035        $preferred_condition = '';
1036        $primary_key_array    = [];
1037        $unique_key_array     = [];
1038        $nonprimary_condition_array = [];
1039        $condition_array = [];
1040
1041        for ($i = 0; $i < $fields_cnt; ++$i) {
1042            $meta        = $fields_meta[$i];
1043
1044            // do not use a column alias in a condition
1045            if (! isset($meta->orgname) || strlen($meta->orgname) === 0) {
1046                $meta->orgname = $meta->name;
1047
1048                foreach ($expressions as $expression) {
1049                    if (empty($expression->alias) || empty($expression->column)) {
1050                        continue;
1051                    }
1052                    if (strcasecmp($meta->name, $expression->alias) == 0) {
1053                        $meta->orgname = $expression->column;
1054                        break;
1055                    }
1056                }
1057            }
1058
1059            // Do not use a table alias in a condition.
1060            // Test case is:
1061            // select * from galerie x WHERE
1062            //(select count(*) from galerie y where y.datum=x.datum)>1
1063            //
1064            // But orgtable is present only with mysqli extension so the
1065            // fix is only for mysqli.
1066            // Also, do not use the original table name if we are dealing with
1067            // a view because this view might be updatable.
1068            // (The isView() verification should not be costly in most cases
1069            // because there is some caching in the function).
1070            if (isset($meta->orgtable)
1071                && ($meta->table != $meta->orgtable)
1072                && ! $dbi->getTable($GLOBALS['db'], $meta->table)->isView()
1073            ) {
1074                $meta->table = $meta->orgtable;
1075            }
1076
1077            // If this field is not from the table which the unique clause needs
1078            // to be restricted to.
1079            if ($restrict_to_table && $restrict_to_table != $meta->table) {
1080                continue;
1081            }
1082
1083            // to fix the bug where float fields (primary or not)
1084            // can't be matched because of the imprecision of
1085            // floating comparison, use CONCAT
1086            // (also, the syntax "CONCAT(field) IS NULL"
1087            // that we need on the next "if" will work)
1088            if ($meta->type === 'real') {
1089                $con_key = 'CONCAT(' . self::backquote($meta->table) . '.'
1090                    . self::backquote($meta->orgname) . ')';
1091            } else {
1092                $con_key = self::backquote($meta->table) . '.'
1093                    . self::backquote($meta->orgname);
1094            }
1095            $condition = ' ' . $con_key . ' ';
1096
1097            [$con_val, $condition] = self::getConditionValue(
1098                $row[$i] ?? null,
1099                $meta,
1100                $dbi->fieldFlags($handle, $i),
1101                $fields_cnt,
1102                $con_key,
1103                $condition
1104            );
1105
1106            if ($con_val === null) {
1107                continue;
1108            }
1109
1110            $condition .= $con_val . ' AND';
1111
1112            if ($meta->primary_key > 0) {
1113                $primary_key .= $condition;
1114                $primary_key_array[$con_key] = $con_val;
1115            } elseif ($meta->unique_key > 0) {
1116                $unique_key  .= $condition;
1117                $unique_key_array[$con_key] = $con_val;
1118            }
1119
1120            $nonprimary_condition .= $condition;
1121            $nonprimary_condition_array[$con_key] = $con_val;
1122        }
1123
1124        // Correction University of Virginia 19991216:
1125        // prefer primary or unique keys for condition,
1126        // but use conjunction of all values if no primary key
1127        $clause_is_unique = true;
1128
1129        if ($primary_key) {
1130            $preferred_condition = $primary_key;
1131            $condition_array = $primary_key_array;
1132        } elseif ($unique_key) {
1133            $preferred_condition = $unique_key;
1134            $condition_array = $unique_key_array;
1135        } elseif (! $force_unique) {
1136            $preferred_condition = $nonprimary_condition;
1137            $condition_array = $nonprimary_condition_array;
1138            $clause_is_unique = false;
1139        }
1140
1141        $where_clause = trim((string) preg_replace('|\s?AND$|', '', $preferred_condition));
1142
1143        return [
1144            $where_clause,
1145            $clause_is_unique,
1146            $condition_array,
1147        ];
1148    }
1149
1150    /**
1151     * Generate the charset query part
1152     *
1153     * @param string $collation Collation
1154     * @param bool   $override  (optional) force 'CHARACTER SET' keyword
1155     */
1156    public static function getCharsetQueryPart(string $collation, bool $override = false): string
1157    {
1158        [$charset] = explode('_', $collation);
1159        $keyword = ' CHARSET=';
1160
1161        if ($override) {
1162            $keyword = ' CHARACTER SET ';
1163        }
1164
1165        return $keyword . $charset
1166            . ($charset == $collation ? '' : ' COLLATE ' . $collation);
1167    }
1168
1169    /**
1170     * Generate a pagination selector for browsing resultsets
1171     *
1172     * @param string $name        The name for the request parameter
1173     * @param int    $rows        Number of rows in the pagination set
1174     * @param int    $pageNow     current page number
1175     * @param int    $nbTotalPage number of total pages
1176     * @param int    $showAll     If the number of pages is lower than this
1177     *                            variable, no pages will be omitted in pagination
1178     * @param int    $sliceStart  How many rows at the beginning should always
1179     *                            be shown?
1180     * @param int    $sliceEnd    How many rows at the end should always be shown?
1181     * @param int    $percent     Percentage of calculation page offsets to hop to a
1182     *                            next page
1183     * @param int    $range       Near the current page, how many pages should
1184     *                            be considered "nearby" and displayed as well?
1185     * @param string $prompt      The prompt to display (sometimes empty)
1186     *
1187     * @access public
1188     */
1189    public static function pageselector(
1190        $name,
1191        $rows,
1192        $pageNow = 1,
1193        $nbTotalPage = 1,
1194        $showAll = 200,
1195        $sliceStart = 5,
1196        $sliceEnd = 5,
1197        $percent = 20,
1198        $range = 10,
1199        $prompt = ''
1200    ): string {
1201        $increment = floor($nbTotalPage / $percent);
1202        $pageNowMinusRange = $pageNow - $range;
1203        $pageNowPlusRange = $pageNow + $range;
1204
1205        $gotopage = $prompt . ' <select class="pageselector ajax"';
1206
1207        $gotopage .= ' name="' . $name . '" >';
1208        if ($nbTotalPage < $showAll) {
1209            $pages = range(1, $nbTotalPage);
1210        } else {
1211            $pages = [];
1212
1213            // Always show first X pages
1214            for ($i = 1; $i <= $sliceStart; $i++) {
1215                $pages[] = $i;
1216            }
1217
1218            // Always show last X pages
1219            for ($i = $nbTotalPage - $sliceEnd; $i <= $nbTotalPage; $i++) {
1220                $pages[] = $i;
1221            }
1222
1223            // Based on the number of results we add the specified
1224            // $percent percentage to each page number,
1225            // so that we have a representing page number every now and then to
1226            // immediately jump to specific pages.
1227            // As soon as we get near our currently chosen page ($pageNow -
1228            // $range), every page number will be shown.
1229            $i = $sliceStart;
1230            $x = $nbTotalPage - $sliceEnd;
1231            $met_boundary = false;
1232
1233            while ($i <= $x) {
1234                if ($i >= $pageNowMinusRange && $i <= $pageNowPlusRange) {
1235                    // If our pageselector comes near the current page, we use 1
1236                    // counter increments
1237                    $i++;
1238                    $met_boundary = true;
1239                } else {
1240                    // We add the percentage increment to our current page to
1241                    // hop to the next one in range
1242                    $i += $increment;
1243
1244                    // Make sure that we do not cross our boundaries.
1245                    if ($i > $pageNowMinusRange && ! $met_boundary) {
1246                        $i = $pageNowMinusRange;
1247                    }
1248                }
1249
1250                if ($i <= 0 || $i > $x) {
1251                    continue;
1252                }
1253
1254                $pages[] = $i;
1255            }
1256
1257            /*
1258            Add page numbers with "geometrically increasing" distances.
1259
1260            This helps me a lot when navigating through giant tables.
1261
1262            Test case: table with 2.28 million sets, 76190 pages. Page of interest
1263            is between 72376 and 76190.
1264            Selecting page 72376.
1265            Now, old version enumerated only +/- 10 pages around 72376 and the
1266            percentage increment produced steps of about 3000.
1267
1268            The following code adds page numbers +/- 2,4,8,16,32,64,128,256 etc.
1269            around the current page.
1270            */
1271            $i = $pageNow;
1272            $dist = 1;
1273            while ($i < $x) {
1274                $dist = 2 * $dist;
1275                $i = $pageNow + $dist;
1276                if ($i <= 0 || $i > $x) {
1277                    continue;
1278                }
1279
1280                $pages[] = $i;
1281            }
1282
1283            $i = $pageNow;
1284            $dist = 1;
1285            while ($i > 0) {
1286                $dist = 2 * $dist;
1287                $i = $pageNow - $dist;
1288                if ($i <= 0 || $i > $x) {
1289                    continue;
1290                }
1291
1292                $pages[] = $i;
1293            }
1294
1295            // Since because of ellipsing of the current page some numbers may be
1296            // double, we unify our array:
1297            sort($pages);
1298            $pages = array_unique($pages);
1299        }
1300
1301        if ($pageNow > $nbTotalPage) {
1302            $pages[] = $pageNow;
1303        }
1304
1305        foreach ($pages as $i) {
1306            if ($i == $pageNow) {
1307                $selected = 'selected="selected" style="font-weight: bold"';
1308            } else {
1309                $selected = '';
1310            }
1311            $gotopage .= '                <option ' . $selected
1312                . ' value="' . (($i - 1) * $rows) . '">' . $i . '</option>' . "\n";
1313        }
1314
1315        $gotopage .= ' </select>';
1316
1317        return $gotopage;
1318    }
1319
1320    /**
1321     * Calculate page number through position
1322     *
1323     * @param int $pos       position of first item
1324     * @param int $max_count number of items per page
1325     *
1326     * @return int $page_num
1327     *
1328     * @access public
1329     */
1330    public static function getPageFromPosition($pos, $max_count)
1331    {
1332        return (int) floor($pos / $max_count) + 1;
1333    }
1334
1335    /**
1336     * replaces %u in given path with current user name
1337     *
1338     * example:
1339     * <code>
1340     * $user_dir = userDir('/var/pma_tmp/%u/'); // '/var/pma_tmp/root/'
1341     *
1342     * </code>
1343     *
1344     * @param string $dir with wildcard for user
1345     *
1346     * @return string per user directory
1347     */
1348    public static function userDir(string $dir): string
1349    {
1350        // add trailing slash
1351        if (mb_substr($dir, -1) !== '/') {
1352            $dir .= '/';
1353        }
1354
1355        return str_replace('%u', Core::securePath($GLOBALS['cfg']['Server']['user']), $dir);
1356    }
1357
1358    /**
1359     * Clears cache content which needs to be refreshed on user change.
1360     */
1361    public static function clearUserCache(): void
1362    {
1363        SessionCache::remove('is_superuser');
1364        SessionCache::remove('is_createuser');
1365        SessionCache::remove('is_grantuser');
1366    }
1367
1368    /**
1369     * Converts a bit value to printable format;
1370     * in MySQL a BIT field can be from 1 to 64 bits so we need this
1371     * function because in PHP, decbin() supports only 32 bits
1372     * on 32-bit servers
1373     *
1374     * @param int $value  coming from a BIT field
1375     * @param int $length length
1376     *
1377     * @return string the printable value
1378     */
1379    public static function printableBitValue(int $value, int $length): string
1380    {
1381        // if running on a 64-bit server or the length is safe for decbin()
1382        if (PHP_INT_SIZE == 8 || $length < 33) {
1383            $printable = decbin($value);
1384        } else {
1385            // FIXME: does not work for the leftmost bit of a 64-bit value
1386            $i = 0;
1387            $printable = '';
1388            while ($value >= pow(2, $i)) {
1389                ++$i;
1390            }
1391            if ($i != 0) {
1392                --$i;
1393            }
1394
1395            while ($i >= 0) {
1396                if ($value - pow(2, $i) < 0) {
1397                    $printable = '0' . $printable;
1398                } else {
1399                    $printable = '1' . $printable;
1400                    $value -= pow(2, $i);
1401                }
1402                --$i;
1403            }
1404            $printable = strrev($printable);
1405        }
1406        $printable = str_pad($printable, $length, '0', STR_PAD_LEFT);
1407
1408        return $printable;
1409    }
1410
1411    /**
1412     * Converts a BIT type default value
1413     * for example, b'010' becomes 010
1414     *
1415     * @param string|null $bitDefaultValue value
1416     *
1417     * @return string the converted value
1418     */
1419    public static function convertBitDefaultValue(?string $bitDefaultValue): string
1420    {
1421        return (string) preg_replace(
1422            "/^b'(\d*)'?$/",
1423            '$1',
1424            htmlspecialchars_decode((string) $bitDefaultValue, ENT_QUOTES),
1425            1
1426        );
1427    }
1428
1429    /**
1430     * Extracts the various parts from a column spec
1431     *
1432     * @param string $columnspec Column specification
1433     *
1434     * @return array associative array containing type, spec_in_brackets
1435     *          and possibly enum_set_values (another array)
1436     */
1437    public static function extractColumnSpec($columnspec)
1438    {
1439        $first_bracket_pos = mb_strpos($columnspec, '(');
1440        if ($first_bracket_pos) {
1441            $spec_in_brackets = rtrim(
1442                mb_substr(
1443                    $columnspec,
1444                    $first_bracket_pos + 1,
1445                    mb_strrpos($columnspec, ')') - $first_bracket_pos - 1
1446                )
1447            );
1448            // convert to lowercase just to be sure
1449            $type = mb_strtolower(
1450                rtrim(mb_substr($columnspec, 0, $first_bracket_pos))
1451            );
1452        } else {
1453            // Split trailing attributes such as unsigned,
1454            // binary, zerofill and get data type name
1455            $type_parts = explode(' ', $columnspec);
1456            $type = mb_strtolower($type_parts[0]);
1457            $spec_in_brackets = '';
1458        }
1459
1460        if ($type === 'enum' || $type === 'set') {
1461            // Define our working vars
1462            $enum_set_values = self::parseEnumSetValues($columnspec, false);
1463            $printtype = $type
1464                . '(' . str_replace("','", "', '", $spec_in_brackets) . ')';
1465            $binary = false;
1466            $unsigned = false;
1467            $zerofill = false;
1468        } else {
1469            $enum_set_values = [];
1470
1471            /* Create printable type name */
1472            $printtype = mb_strtolower($columnspec);
1473
1474            // Strip the "BINARY" attribute, except if we find "BINARY(" because
1475            // this would be a BINARY or VARBINARY column type;
1476            // by the way, a BLOB should not show the BINARY attribute
1477            // because this is not accepted in MySQL syntax.
1478            if (strpos($printtype, 'binary') !== false
1479                && ! preg_match('@binary[\(]@', $printtype)
1480            ) {
1481                $printtype = str_replace('binary', '', $printtype);
1482                $binary = true;
1483            } else {
1484                $binary = false;
1485            }
1486
1487            $printtype = (string) preg_replace(
1488                '@zerofill@',
1489                '',
1490                $printtype,
1491                -1,
1492                $zerofill_cnt
1493            );
1494            $zerofill = ($zerofill_cnt > 0);
1495            $printtype = (string) preg_replace(
1496                '@unsigned@',
1497                '',
1498                $printtype,
1499                -1,
1500                $unsigned_cnt
1501            );
1502            $unsigned = ($unsigned_cnt > 0);
1503            $printtype = trim($printtype);
1504        }
1505
1506        $attribute     = ' ';
1507        if ($binary) {
1508            $attribute = 'BINARY';
1509        }
1510        if ($unsigned) {
1511            $attribute = 'UNSIGNED';
1512        }
1513        if ($zerofill) {
1514            $attribute = 'UNSIGNED ZEROFILL';
1515        }
1516
1517        $can_contain_collation = false;
1518        if (! $binary
1519            && preg_match(
1520                '@^(char|varchar|text|tinytext|mediumtext|longtext|set|enum)@',
1521                $type
1522            )
1523        ) {
1524            $can_contain_collation = true;
1525        }
1526
1527        // for the case ENUM('&#8211;','&ldquo;')
1528        $displayed_type = htmlspecialchars($printtype, ENT_COMPAT);
1529        if (mb_strlen($printtype) > $GLOBALS['cfg']['LimitChars']) {
1530            $displayed_type  = '<abbr title="' . htmlspecialchars($printtype) . '">';
1531            $displayed_type .= htmlspecialchars(
1532                mb_substr(
1533                    $printtype,
1534                    0,
1535                    (int) $GLOBALS['cfg']['LimitChars']
1536                ) . '...',
1537                ENT_COMPAT
1538            );
1539            $displayed_type .= '</abbr>';
1540        }
1541
1542        return [
1543            'type' => $type,
1544            'spec_in_brackets' => $spec_in_brackets,
1545            'enum_set_values'  => $enum_set_values,
1546            'print_type' => $printtype,
1547            'binary' => $binary,
1548            'unsigned' => $unsigned,
1549            'zerofill' => $zerofill,
1550            'attribute' => $attribute,
1551            'can_contain_collation' => $can_contain_collation,
1552            'displayed_type' => $displayed_type,
1553        ];
1554    }
1555
1556    /**
1557     * Verifies if this table's engine supports foreign keys
1558     *
1559     * @param string $engine engine
1560     */
1561    public static function isForeignKeySupported($engine): bool
1562    {
1563        global $dbi;
1564
1565        $engine = strtoupper((string) $engine);
1566        if (($engine === 'INNODB') || ($engine === 'PBXT')) {
1567            return true;
1568        }
1569
1570        if ($engine === 'NDBCLUSTER' || $engine === 'NDB') {
1571            $ndbver = strtolower(
1572                $dbi->fetchValue('SELECT @@ndb_version_string')
1573            );
1574            if (substr($ndbver, 0, 4) === 'ndb-') {
1575                $ndbver = substr($ndbver, 4);
1576            }
1577
1578            return version_compare($ndbver, '7.3', '>=');
1579        }
1580
1581        return false;
1582    }
1583
1584    /**
1585     * Is Foreign key check enabled?
1586     */
1587    public static function isForeignKeyCheck(): bool
1588    {
1589        global $dbi;
1590
1591        if ($GLOBALS['cfg']['DefaultForeignKeyChecks'] === 'enable') {
1592            return true;
1593        }
1594
1595        if ($GLOBALS['cfg']['DefaultForeignKeyChecks'] === 'disable') {
1596            return false;
1597        }
1598
1599        return $dbi->getVariable('FOREIGN_KEY_CHECKS') === 'ON';
1600    }
1601
1602    /**
1603     * Handle foreign key check request
1604     *
1605     * @return bool Default foreign key checks value
1606     */
1607    public static function handleDisableFKCheckInit(): bool
1608    {
1609        /** @var DatabaseInterface $dbi */
1610        global $dbi;
1611
1612        $default_fk_check_value = $dbi->getVariable('FOREIGN_KEY_CHECKS') === 'ON';
1613        if (isset($_REQUEST['fk_checks'])) {
1614            if (empty($_REQUEST['fk_checks'])) {
1615                // Disable foreign key checks
1616                $dbi->setVariable('FOREIGN_KEY_CHECKS', 'OFF');
1617            } else {
1618                // Enable foreign key checks
1619                $dbi->setVariable('FOREIGN_KEY_CHECKS', 'ON');
1620            }
1621        }
1622
1623        return $default_fk_check_value;
1624    }
1625
1626    /**
1627     * Cleanup changes done for foreign key check
1628     *
1629     * @param bool $default_fk_check_value original value for 'FOREIGN_KEY_CHECKS'
1630     */
1631    public static function handleDisableFKCheckCleanup(bool $default_fk_check_value): void
1632    {
1633        /** @var DatabaseInterface $dbi */
1634        global $dbi;
1635
1636        $dbi->setVariable(
1637            'FOREIGN_KEY_CHECKS',
1638            $default_fk_check_value ? 'ON' : 'OFF'
1639        );
1640    }
1641
1642    /**
1643     * Converts GIS data to Well Known Text format
1644     *
1645     * @param string $data        GIS data
1646     * @param bool   $includeSRID Add SRID to the WKT
1647     *
1648     * @return string GIS data in Well Know Text format
1649     */
1650    public static function asWKT($data, $includeSRID = false)
1651    {
1652        global $dbi;
1653
1654        // Convert to WKT format
1655        $hex = bin2hex($data);
1656        $spatialAsText = 'ASTEXT';
1657        $spatialSrid = 'SRID';
1658        $axisOrder = '';
1659        $mysqlVersionInt = $dbi->getVersion();
1660        if ($mysqlVersionInt >= 50600) {
1661            $spatialAsText = 'ST_ASTEXT';
1662            $spatialSrid = 'ST_SRID';
1663        }
1664
1665        if ($mysqlVersionInt >= 80001 && ! $dbi->isMariaDb()) {
1666            $axisOrder = ', \'axis-order=long-lat\'';
1667        }
1668
1669        $wktsql     = 'SELECT ' . $spatialAsText . "(x'" . $hex . "'" . $axisOrder . ')';
1670        if ($includeSRID) {
1671            $wktsql .= ', ' . $spatialSrid . "(x'" . $hex . "')";
1672        }
1673
1674        $wktresult  = $dbi->tryQuery(
1675            $wktsql
1676        );
1677        $wktarr     = $dbi->fetchRow($wktresult);
1678        $wktval     = $wktarr[0] ?? null;
1679
1680        if ($includeSRID) {
1681            $srid = $wktarr[1] ?? null;
1682            $wktval = "'" . $wktval . "'," . $srid;
1683        }
1684        @$dbi->freeResult($wktresult);
1685
1686        return $wktval;
1687    }
1688
1689    /**
1690     * If the string starts with a \r\n pair (0x0d0a) add an extra \n
1691     *
1692     * @param string $string string
1693     *
1694     * @return string with the chars replaced
1695     */
1696    public static function duplicateFirstNewline(string $string): string
1697    {
1698        $first_occurence = mb_strpos($string, "\r\n");
1699        if ($first_occurence === 0) {
1700            $string = "\n" . $string;
1701        }
1702
1703        return $string;
1704    }
1705
1706    /**
1707     * Get the action word corresponding to a script name
1708     * in order to display it as a title in navigation panel
1709     *
1710     * @param string $target a valid value for $cfg['NavigationTreeDefaultTabTable'],
1711     *                       $cfg['NavigationTreeDefaultTabTable2'],
1712     *                       $cfg['DefaultTabTable'] or $cfg['DefaultTabDatabase']
1713     *
1714     * @return string|bool Title for the $cfg value
1715     */
1716    public static function getTitleForTarget($target)
1717    {
1718        $mapping = [
1719            'structure' =>  __('Structure'),
1720            'sql' => __('SQL'),
1721            'search' => __('Search'),
1722            'insert' => __('Insert'),
1723            'browse' => __('Browse'),
1724            'operations' => __('Operations'),
1725        ];
1726
1727        return $mapping[$target] ?? false;
1728    }
1729
1730    /**
1731     * Get the script name corresponding to a plain English config word
1732     * in order to append in links on navigation and main panel
1733     *
1734     * @param string $target   a valid value for
1735     *                         $cfg['NavigationTreeDefaultTabTable'],
1736     *                         $cfg['NavigationTreeDefaultTabTable2'],
1737     *                         $cfg['DefaultTabTable'], $cfg['DefaultTabDatabase'] or
1738     *                         $cfg['DefaultTabServer']
1739     * @param string $location one out of 'server', 'table', 'database'
1740     *
1741     * @return string script name corresponding to the config word
1742     */
1743    public static function getScriptNameForOption($target, string $location): string
1744    {
1745        $url = self::getUrlForOption($target, $location);
1746        if ($url === null) {
1747            return './';
1748        }
1749
1750        return Url::getFromRoute($url);
1751    }
1752
1753    /**
1754     * Get the URL corresponding to a plain English config word
1755     * in order to append in links on navigation and main panel
1756     *
1757     * @param string $target   a valid value for
1758     *                         $cfg['NavigationTreeDefaultTabTable'],
1759     *                         $cfg['NavigationTreeDefaultTabTable2'],
1760     *                         $cfg['DefaultTabTable'], $cfg['DefaultTabDatabase'] or
1761     *                         $cfg['DefaultTabServer']
1762     * @param string $location one out of 'server', 'table', 'database'
1763     *
1764     * @return string The URL corresponding to the config word or null if nothing was found
1765     */
1766    public static function getUrlForOption($target, string $location): ?string
1767    {
1768        if ($location === 'server') {
1769            // Values for $cfg['DefaultTabServer']
1770            switch ($target) {
1771                case 'welcome':
1772                case 'index.php':
1773                    return '/';
1774                case 'databases':
1775                case 'server_databases.php':
1776                    return '/server/databases';
1777                case 'status':
1778                case 'server_status.php':
1779                    return '/server/status';
1780                case 'variables':
1781                case 'server_variables.php':
1782                    return '/server/variables';
1783                case 'privileges':
1784                case 'server_privileges.php':
1785                    return '/server/privileges';
1786            }
1787        } elseif ($location === 'database') {
1788            // Values for $cfg['DefaultTabDatabase']
1789            switch ($target) {
1790                case 'structure':
1791                case 'db_structure.php':
1792                    return '/database/structure';
1793                case 'sql':
1794                case 'db_sql.php':
1795                    return '/database/sql';
1796                case 'search':
1797                case 'db_search.php':
1798                    return '/database/search';
1799                case 'operations':
1800                case 'db_operations.php':
1801                    return '/database/operations';
1802            }
1803        } elseif ($location === 'table') {
1804            // Values for $cfg['DefaultTabTable'],
1805            // $cfg['NavigationTreeDefaultTabTable'] and
1806            // $cfg['NavigationTreeDefaultTabTable2']
1807            switch ($target) {
1808                case 'structure':
1809                case 'tbl_structure.php':
1810                    return '/table/structure';
1811                case 'sql':
1812                case 'tbl_sql.php':
1813                    return '/table/sql';
1814                case 'search':
1815                case 'tbl_select.php':
1816                    return '/table/search';
1817                case 'insert':
1818                case 'tbl_change.php':
1819                    return '/table/change';
1820                case 'browse':
1821                case 'sql.php':
1822                    return '/sql';
1823            }
1824        }
1825
1826        return null;
1827    }
1828
1829    /**
1830     * Formats user string, expanding @VARIABLES@, accepting strftime format
1831     * string.
1832     *
1833     * @param string       $string  Text where to do expansion.
1834     * @param array|string $escape  Function to call for escaping variable values.
1835     *                              Can also be an array of:
1836     *                              - the escape method name
1837     *                              - the class that contains the method
1838     *                              - location of the class (for inclusion)
1839     * @param array        $updates Array with overrides for default parameters
1840     *                              (obtained from GLOBALS).
1841     *
1842     * @return string
1843     */
1844    public static function expandUserString(
1845        $string,
1846        $escape = null,
1847        array $updates = []
1848    ) {
1849        global $dbi;
1850
1851        /* Content */
1852        $vars = [];
1853        $vars['http_host'] = Core::getenv('HTTP_HOST');
1854        $vars['server_name'] = $GLOBALS['cfg']['Server']['host'];
1855        $vars['server_verbose'] = $GLOBALS['cfg']['Server']['verbose'];
1856
1857        if (empty($GLOBALS['cfg']['Server']['verbose'])) {
1858            $vars['server_verbose_or_name'] = $GLOBALS['cfg']['Server']['host'];
1859        } else {
1860            $vars['server_verbose_or_name'] = $GLOBALS['cfg']['Server']['verbose'];
1861        }
1862
1863        $vars['database'] = $GLOBALS['db'];
1864        $vars['table'] = $GLOBALS['table'];
1865        $vars['phpmyadmin_version'] = 'phpMyAdmin ' . PMA_VERSION;
1866
1867        /* Update forced variables */
1868        foreach ($updates as $key => $val) {
1869            $vars[$key] = $val;
1870        }
1871
1872        /* Replacement mapping */
1873        /*
1874         * The __VAR__ ones are for backward compatibility, because user
1875         * might still have it in cookies.
1876         */
1877        $replace = [
1878            '@HTTP_HOST@' => $vars['http_host'],
1879            '@SERVER@' => $vars['server_name'],
1880            '__SERVER__' => $vars['server_name'],
1881            '@VERBOSE@' => $vars['server_verbose'],
1882            '@VSERVER@' => $vars['server_verbose_or_name'],
1883            '@DATABASE@' => $vars['database'],
1884            '__DB__' => $vars['database'],
1885            '@TABLE@' => $vars['table'],
1886            '__TABLE__' => $vars['table'],
1887            '@PHPMYADMIN@' => $vars['phpmyadmin_version'],
1888        ];
1889
1890        /* Optional escaping */
1891        if ($escape !== null) {
1892            if (is_array($escape)) {
1893                $escape_class = new $escape[1]();
1894                $escape_method = $escape[0];
1895            }
1896            foreach ($replace as $key => $val) {
1897                if (isset($escape_class, $escape_method)) {
1898                    $replace[$key] = $escape_class->$escape_method($val);
1899                } elseif ($escape === 'backquote') {
1900                    $replace[$key] = self::backquote($val);
1901                } elseif (is_callable($escape)) {
1902                    $replace[$key] = $escape($val);
1903                }
1904            }
1905        }
1906
1907        /* Backward compatibility in 3.5.x */
1908        if (mb_strpos($string, '@FIELDS@') !== false) {
1909            $string = strtr($string, ['@FIELDS@' => '@COLUMNS@']);
1910        }
1911
1912        /* Fetch columns list if required */
1913        if (mb_strpos($string, '@COLUMNS@') !== false) {
1914            $columns_list = $dbi->getColumns(
1915                $GLOBALS['db'],
1916                $GLOBALS['table']
1917            );
1918
1919            // sometimes the table no longer exists at this point
1920            if ($columns_list !== null) {
1921                $column_names = [];
1922                foreach ($columns_list as $column) {
1923                    if ($escape !== null) {
1924                        $column_names[] = self::$escape($column['Field']);
1925                    } else {
1926                        $column_names[] = $column['Field'];
1927                    }
1928                }
1929                $replace['@COLUMNS@'] = implode(',', $column_names);
1930            } else {
1931                $replace['@COLUMNS@'] = '*';
1932            }
1933        }
1934
1935        /* Do the replacement */
1936        return strtr((string) @strftime($string), $replace);
1937    }
1938
1939    /**
1940     * This function processes the datatypes supported by the DB,
1941     * as specified in Types->getColumns() and either returns an array
1942     * (useful for quickly checking if a datatype is supported)
1943     * or an HTML snippet that creates a drop-down list.
1944     *
1945     * @param bool   $html     Whether to generate an html snippet or an array
1946     * @param string $selected The value to mark as selected in HTML mode
1947     *
1948     * @return mixed   An HTML snippet or an array of datatypes.
1949     */
1950    public static function getSupportedDatatypes($html = false, $selected = '')
1951    {
1952        global $dbi;
1953
1954        if ($html) {
1955            $retval = Generator::getSupportedDatatypes($selected);
1956        } else {
1957            $retval = [];
1958            foreach ($dbi->types->getColumns() as $value) {
1959                if (is_array($value)) {
1960                    foreach ($value as $subvalue) {
1961                        if ($subvalue === '-') {
1962                            continue;
1963                        }
1964
1965                        $retval[] = $subvalue;
1966                    }
1967                } else {
1968                    if ($value !== '-') {
1969                        $retval[] = $value;
1970                    }
1971                }
1972            }
1973        }
1974
1975        return $retval;
1976    }
1977
1978    /**
1979     * Returns a list of datatypes that are not (yet) handled by PMA.
1980     * Used by: /table/change and libraries/Routines.php
1981     *
1982     * @return array list of datatypes
1983     */
1984    public static function unsupportedDatatypes(): array
1985    {
1986        return [];
1987    }
1988
1989    /**
1990     * Return GIS data types
1991     *
1992     * @param bool $upper_case whether to return values in upper case
1993     *
1994     * @return string[] GIS data types
1995     */
1996    public static function getGISDatatypes($upper_case = false): array
1997    {
1998        $gis_data_types = [
1999            'geometry',
2000            'point',
2001            'linestring',
2002            'polygon',
2003            'multipoint',
2004            'multilinestring',
2005            'multipolygon',
2006            'geometrycollection',
2007        ];
2008        if ($upper_case) {
2009            $gis_data_types = array_map('mb_strtoupper', $gis_data_types);
2010        }
2011
2012        return $gis_data_types;
2013    }
2014
2015    /**
2016     * Generates GIS data based on the string passed.
2017     *
2018     * @param string $gis_string   GIS string
2019     * @param int    $mysqlVersion The mysql version as int
2020     *
2021     * @return string GIS data enclosed in 'ST_GeomFromText' or 'GeomFromText' function
2022     */
2023    public static function createGISData($gis_string, $mysqlVersion)
2024    {
2025        $geomFromText = $mysqlVersion >= 50600 ? 'ST_GeomFromText' : 'GeomFromText';
2026        $gis_string = trim($gis_string);
2027        $geom_types = '(POINT|MULTIPOINT|LINESTRING|MULTILINESTRING|'
2028            . 'POLYGON|MULTIPOLYGON|GEOMETRYCOLLECTION)';
2029        if (preg_match("/^'" . $geom_types . "\(.*\)',[0-9]*$/i", $gis_string)) {
2030            return $geomFromText . '(' . $gis_string . ')';
2031        }
2032
2033        if (preg_match('/^' . $geom_types . '\(.*\)$/i', $gis_string)) {
2034            return $geomFromText . "('" . $gis_string . "')";
2035        }
2036
2037        return $gis_string;
2038    }
2039
2040    /**
2041     * Returns the names and details of the functions
2042     * that can be applied on geometry data types.
2043     *
2044     * @param string $geom_type if provided the output is limited to the functions
2045     *                          that are applicable to the provided geometry type.
2046     * @param bool   $binary    if set to false functions that take two geometries
2047     *                          as arguments will not be included.
2048     * @param bool   $display   if set to true separators will be added to the
2049     *                          output array.
2050     *
2051     * @return array<int|string,array<string,int|string>> names and details of the functions that can be applied on
2052     *                                                    geometry data types.
2053     */
2054    public static function getGISFunctions(
2055        $geom_type = null,
2056        $binary = true,
2057        $display = false
2058    ): array {
2059        global $dbi;
2060
2061        $funcs = [];
2062        if ($display) {
2063            $funcs[] = ['display' => ' '];
2064        }
2065
2066        // Unary functions common to all geometry types
2067        $funcs['Dimension']    = [
2068            'params' => 1,
2069            'type' => 'int',
2070        ];
2071        $funcs['Envelope']     = [
2072            'params' => 1,
2073            'type' => 'Polygon',
2074        ];
2075        $funcs['GeometryType'] = [
2076            'params' => 1,
2077            'type' => 'text',
2078        ];
2079        $funcs['SRID']         = [
2080            'params' => 1,
2081            'type' => 'int',
2082        ];
2083        $funcs['IsEmpty']      = [
2084            'params' => 1,
2085            'type' => 'int',
2086        ];
2087        $funcs['IsSimple']     = [
2088            'params' => 1,
2089            'type' => 'int',
2090        ];
2091
2092        $geom_type = mb_strtolower(trim((string) $geom_type));
2093        if ($display && $geom_type !== 'geometry' && $geom_type !== 'multipoint') {
2094            $funcs[] = ['display' => '--------'];
2095        }
2096
2097        // Unary functions that are specific to each geometry type
2098        if ($geom_type === 'point') {
2099            $funcs['X'] = [
2100                'params' => 1,
2101                'type' => 'float',
2102            ];
2103            $funcs['Y'] = [
2104                'params' => 1,
2105                'type' => 'float',
2106            ];
2107        } elseif ($geom_type === 'linestring') {
2108            $funcs['EndPoint']   = [
2109                'params' => 1,
2110                'type' => 'point',
2111            ];
2112            $funcs['GLength']    = [
2113                'params' => 1,
2114                'type' => 'float',
2115            ];
2116            $funcs['NumPoints']  = [
2117                'params' => 1,
2118                'type' => 'int',
2119            ];
2120            $funcs['StartPoint'] = [
2121                'params' => 1,
2122                'type' => 'point',
2123            ];
2124            $funcs['IsRing']     = [
2125                'params' => 1,
2126                'type' => 'int',
2127            ];
2128        } elseif ($geom_type === 'multilinestring') {
2129            $funcs['GLength']  = [
2130                'params' => 1,
2131                'type' => 'float',
2132            ];
2133            $funcs['IsClosed'] = [
2134                'params' => 1,
2135                'type' => 'int',
2136            ];
2137        } elseif ($geom_type === 'polygon') {
2138            $funcs['Area']         = [
2139                'params' => 1,
2140                'type' => 'float',
2141            ];
2142            $funcs['ExteriorRing'] = [
2143                'params' => 1,
2144                'type' => 'linestring',
2145            ];
2146            $funcs['NumInteriorRings'] = [
2147                'params' => 1,
2148                'type' => 'int',
2149            ];
2150        } elseif ($geom_type === 'multipolygon') {
2151            $funcs['Area']     = [
2152                'params' => 1,
2153                'type' => 'float',
2154            ];
2155            $funcs['Centroid'] = [
2156                'params' => 1,
2157                'type' => 'point',
2158            ];
2159            // Not yet implemented in MySQL
2160            //$funcs['PointOnSurface'] = array('params' => 1, 'type' => 'point');
2161        } elseif ($geom_type === 'geometrycollection') {
2162            $funcs['NumGeometries'] = [
2163                'params' => 1,
2164                'type' => 'int',
2165            ];
2166        }
2167
2168        // If we are asked for binary functions as well
2169        if ($binary) {
2170            // section separator
2171            if ($display) {
2172                $funcs[] = ['display' => '--------'];
2173            }
2174
2175            $spatialPrefix = '';
2176            if ($dbi->getVersion() >= 50601) {
2177                // If MySQL version is greater than or equal 5.6.1,
2178                // use the ST_ prefix.
2179                $spatialPrefix = 'ST_';
2180            }
2181            $funcs[$spatialPrefix . 'Crosses']    = [
2182                'params' => 2,
2183                'type' => 'int',
2184            ];
2185            $funcs[$spatialPrefix . 'Contains']   = [
2186                'params' => 2,
2187                'type' => 'int',
2188            ];
2189            $funcs[$spatialPrefix . 'Disjoint']   = [
2190                'params' => 2,
2191                'type' => 'int',
2192            ];
2193            $funcs[$spatialPrefix . 'Equals']     = [
2194                'params' => 2,
2195                'type' => 'int',
2196            ];
2197            $funcs[$spatialPrefix . 'Intersects'] = [
2198                'params' => 2,
2199                'type' => 'int',
2200            ];
2201            $funcs[$spatialPrefix . 'Overlaps']   = [
2202                'params' => 2,
2203                'type' => 'int',
2204            ];
2205            $funcs[$spatialPrefix . 'Touches']    = [
2206                'params' => 2,
2207                'type' => 'int',
2208            ];
2209            $funcs[$spatialPrefix . 'Within']     = [
2210                'params' => 2,
2211                'type' => 'int',
2212            ];
2213
2214            if ($display) {
2215                $funcs[] = ['display' => '--------'];
2216            }
2217            // Minimum bounding rectangle functions
2218            $funcs['MBRContains']   = [
2219                'params' => 2,
2220                'type' => 'int',
2221            ];
2222            $funcs['MBRDisjoint']   = [
2223                'params' => 2,
2224                'type' => 'int',
2225            ];
2226            $funcs['MBREquals']     = [
2227                'params' => 2,
2228                'type' => 'int',
2229            ];
2230            $funcs['MBRIntersects'] = [
2231                'params' => 2,
2232                'type' => 'int',
2233            ];
2234            $funcs['MBROverlaps']   = [
2235                'params' => 2,
2236                'type' => 'int',
2237            ];
2238            $funcs['MBRTouches']    = [
2239                'params' => 2,
2240                'type' => 'int',
2241            ];
2242            $funcs['MBRWithin']     = [
2243                'params' => 2,
2244                'type' => 'int',
2245            ];
2246        }
2247
2248        return $funcs;
2249    }
2250
2251    /**
2252     * Checks if the current user has a specific privilege and returns true if the
2253     * user indeed has that privilege or false if they don't. This function must
2254     * only be used for features that are available since MySQL 5, because it
2255     * relies on the INFORMATION_SCHEMA database to be present.
2256     *
2257     * Example:   currentUserHasPrivilege('CREATE ROUTINE', 'mydb');
2258     *            // Checks if the currently logged in user has the global
2259     *            // 'CREATE ROUTINE' privilege or, if not, checks if the
2260     *            // user has this privilege on database 'mydb'.
2261     *
2262     * @param string      $priv The privilege to check
2263     * @param string|null $db   null, to only check global privileges
2264     *                          string, db name where to also check
2265     *                          for privileges
2266     * @param string|null $tbl  null, to only check global/db privileges
2267     *                          string, table name where to also check
2268     *                          for privileges
2269     */
2270    public static function currentUserHasPrivilege(string $priv, ?string $db = null, ?string $tbl = null): bool
2271    {
2272        global $dbi;
2273
2274        // Get the username for the current user in the format
2275        // required to use in the information schema database.
2276        [$user, $host] = $dbi->getCurrentUserAndHost();
2277
2278        // MySQL is started with --skip-grant-tables
2279        if ($user === '') {
2280            return true;
2281        }
2282
2283        $username  = "''";
2284        $username .= str_replace("'", "''", $user);
2285        $username .= "''@''";
2286        $username .= str_replace("'", "''", $host);
2287        $username .= "''";
2288
2289        // Prepare the query
2290        $query = 'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`%s` '
2291               . "WHERE GRANTEE='%s' AND PRIVILEGE_TYPE='%s'";
2292
2293        // Check global privileges first.
2294        $user_privileges = $dbi->fetchValue(
2295            sprintf(
2296                $query,
2297                'USER_PRIVILEGES',
2298                $username,
2299                $priv
2300            )
2301        );
2302        if ($user_privileges) {
2303            return true;
2304        }
2305        // If a database name was provided and user does not have the
2306        // required global privilege, try database-wise permissions.
2307        if ($db === null) {
2308            // There was no database name provided and the user
2309            // does not have the correct global privilege.
2310            return false;
2311        }
2312
2313        $query .= " AND '%s' LIKE `TABLE_SCHEMA`";
2314        $schema_privileges = $dbi->fetchValue(
2315            sprintf(
2316                $query,
2317                'SCHEMA_PRIVILEGES',
2318                $username,
2319                $priv,
2320                $dbi->escapeString($db)
2321            )
2322        );
2323        if ($schema_privileges) {
2324            return true;
2325        }
2326        // If a table name was also provided and we still didn't
2327        // find any valid privileges, try table-wise privileges.
2328        if ($tbl !== null) {
2329            $query .= " AND TABLE_NAME='%s'";
2330            $table_privileges = $dbi->fetchValue(
2331                sprintf(
2332                    $query,
2333                    'TABLE_PRIVILEGES',
2334                    $username,
2335                    $priv,
2336                    $dbi->escapeString($db),
2337                    $dbi->escapeString($tbl)
2338                )
2339            );
2340            if ($table_privileges) {
2341                return true;
2342            }
2343        }
2344
2345        /**
2346         * If we reached this point, the user does not
2347         * have even valid table-wise privileges.
2348         */
2349        return false;
2350    }
2351
2352    /**
2353     * Returns server type for current connection
2354     *
2355     * Known types are: MariaDB, PerconaDB and MySQL (default)
2356     *
2357     * @return string
2358     */
2359    public static function getServerType()
2360    {
2361        global $dbi;
2362
2363        if ($dbi->isMariaDB()) {
2364            return 'MariaDB';
2365        }
2366
2367        if ($dbi->isPercona()) {
2368            return 'Percona Server';
2369        }
2370
2371        return 'MySQL';
2372    }
2373
2374    /**
2375     * Parses ENUM/SET values
2376     *
2377     * @param string $definition The definition of the column
2378     *                           for which to parse the values
2379     * @param bool   $escapeHtml Whether to escape html entities
2380     *
2381     * @return array
2382     */
2383    public static function parseEnumSetValues($definition, $escapeHtml = true)
2384    {
2385        $values_string = htmlentities($definition, ENT_COMPAT, 'UTF-8');
2386        // There is a JS port of the below parser in functions.js
2387        // If you are fixing something here,
2388        // you need to also update the JS port.
2389        $values = [];
2390        $in_string = false;
2391        $buffer = '';
2392
2393        for ($i = 0, $length = mb_strlen($values_string); $i < $length; $i++) {
2394            $curr = mb_substr($values_string, $i, 1);
2395            $next = $i == mb_strlen($values_string) - 1
2396                ? ''
2397                : mb_substr($values_string, $i + 1, 1);
2398
2399            if (! $in_string && $curr == "'") {
2400                $in_string = true;
2401            } elseif (($in_string && $curr === '\\') && $next === '\\') {
2402                $buffer .= '&#92;';
2403                $i++;
2404            } elseif (($in_string && $next == "'")
2405                && ($curr == "'" || $curr === '\\')
2406            ) {
2407                $buffer .= '&#39;';
2408                $i++;
2409            } elseif ($in_string && $curr == "'") {
2410                $in_string = false;
2411                $values[] = $buffer;
2412                $buffer = '';
2413            } elseif ($in_string) {
2414                 $buffer .= $curr;
2415            }
2416        }
2417
2418        if (strlen($buffer) > 0) {
2419            // The leftovers in the buffer are the last value (if any)
2420            $values[] = $buffer;
2421        }
2422
2423        if (! $escapeHtml) {
2424            foreach ($values as $key => $value) {
2425                $values[$key] = html_entity_decode($value, ENT_QUOTES, 'UTF-8');
2426            }
2427        }
2428
2429        return $values;
2430    }
2431
2432    /**
2433     * Get regular expression which occur first inside the given sql query.
2434     *
2435     * @param array  $regex_array Comparing regular expressions.
2436     * @param string $query       SQL query to be checked.
2437     *
2438     * @return string Matching regular expression.
2439     */
2440    public static function getFirstOccurringRegularExpression(array $regex_array, $query): string
2441    {
2442        $minimum_first_occurence_index = null;
2443        $regex = null;
2444
2445        foreach ($regex_array as $test_regex) {
2446            if (! preg_match($test_regex, $query, $matches, PREG_OFFSET_CAPTURE)) {
2447                continue;
2448            }
2449
2450            if ($minimum_first_occurence_index !== null
2451                && ($matches[0][1] >= $minimum_first_occurence_index)
2452            ) {
2453                continue;
2454            }
2455
2456            $regex = $test_regex;
2457            $minimum_first_occurence_index = $matches[0][1];
2458        }
2459
2460        return $regex;
2461    }
2462
2463    /**
2464     * Return the list of tabs for the menu with corresponding names
2465     *
2466     * @param string $level 'server', 'db' or 'table' level
2467     *
2468     * @return array|null list of tabs for the menu
2469     */
2470    public static function getMenuTabList($level = null)
2471    {
2472        $tabList = [
2473            'server' => [
2474                'databases'   => __('Databases'),
2475                'sql'         => __('SQL'),
2476                'status'      => __('Status'),
2477                'rights'      => __('Users'),
2478                'export'      => __('Export'),
2479                'import'      => __('Import'),
2480                'settings'    => __('Settings'),
2481                'binlog'      => __('Binary log'),
2482                'replication' => __('Replication'),
2483                'vars'        => __('Variables'),
2484                'charset'     => __('Charsets'),
2485                'plugins'     => __('Plugins'),
2486                'engine'      => __('Engines'),
2487            ],
2488            'db'     => [
2489                'structure'   => __('Structure'),
2490                'sql'         => __('SQL'),
2491                'search'      => __('Search'),
2492                'query'       => __('Query'),
2493                'export'      => __('Export'),
2494                'import'      => __('Import'),
2495                'operation'   => __('Operations'),
2496                'privileges'  => __('Privileges'),
2497                'routines'    => __('Routines'),
2498                'events'      => __('Events'),
2499                'triggers'    => __('Triggers'),
2500                'tracking'    => __('Tracking'),
2501                'designer'    => __('Designer'),
2502                'central_columns' => __('Central columns'),
2503            ],
2504            'table'  => [
2505                'browse'      => __('Browse'),
2506                'structure'   => __('Structure'),
2507                'sql'         => __('SQL'),
2508                'search'      => __('Search'),
2509                'insert'      => __('Insert'),
2510                'export'      => __('Export'),
2511                'import'      => __('Import'),
2512                'privileges'  => __('Privileges'),
2513                'operation'   => __('Operations'),
2514                'tracking'    => __('Tracking'),
2515                'triggers'    => __('Triggers'),
2516            ],
2517        ];
2518
2519        if ($level == null) {
2520            return $tabList;
2521        }
2522
2523        if (array_key_exists($level, $tabList)) {
2524            return $tabList[$level];
2525        }
2526
2527        return null;
2528    }
2529
2530    /**
2531     * Add fractional seconds to time, datetime and timestamp strings.
2532     * If the string contains fractional seconds,
2533     * pads it with 0s up to 6 decimal places.
2534     *
2535     * @param string $value time, datetime or timestamp strings
2536     *
2537     * @return string time, datetime or timestamp strings with fractional seconds
2538     */
2539    public static function addMicroseconds($value)
2540    {
2541        if (empty($value) || $value === 'CURRENT_TIMESTAMP'
2542            || $value === 'current_timestamp()'
2543        ) {
2544            return $value;
2545        }
2546
2547        if (mb_strpos($value, '.') === false) {
2548            return $value . '.000000';
2549        }
2550
2551        $value .= '000000';
2552
2553        return mb_substr(
2554            $value,
2555            0,
2556            mb_strpos($value, '.') + 7
2557        );
2558    }
2559
2560    /**
2561     * Reads the file, detects the compression MIME type, closes the file
2562     * and returns the MIME type
2563     *
2564     * @param resource $file the file handle
2565     *
2566     * @return string the MIME type for compression, or 'none'
2567     */
2568    public static function getCompressionMimeType($file)
2569    {
2570        $test = fread($file, 4);
2571
2572        if ($test === false) {
2573            fclose($file);
2574
2575            return 'none';
2576        }
2577
2578        $len = strlen($test);
2579        fclose($file);
2580        if ($len >= 2 && $test[0] == chr(31) && $test[1] == chr(139)) {
2581            return 'application/gzip';
2582        }
2583        if ($len >= 3 && substr($test, 0, 3) === 'BZh') {
2584            return 'application/bzip2';
2585        }
2586        if ($len >= 4 && $test == "PK\003\004") {
2587            return 'application/zip';
2588        }
2589
2590        return 'none';
2591    }
2592
2593    /**
2594     * Provide COLLATE clause, if required, to perform case sensitive comparisons
2595     * for queries on information_schema.
2596     *
2597     * @return string COLLATE clause if needed or empty string.
2598     */
2599    public static function getCollateForIS()
2600    {
2601        global $dbi;
2602
2603        $names = $dbi->getLowerCaseNames();
2604        if ($names === '0') {
2605            return 'COLLATE utf8_bin';
2606        }
2607
2608        if ($names === '2') {
2609            return 'COLLATE utf8_general_ci';
2610        }
2611
2612        return '';
2613    }
2614
2615    /**
2616     * Process the index data.
2617     *
2618     * @param array $indexes index data
2619     *
2620     * @return array processes index data
2621     */
2622    public static function processIndexData(array $indexes)
2623    {
2624        $lastIndex    = '';
2625
2626        $primary      = '';
2627        $pk_array     = []; // will be use to emphasis prim. keys in the table
2628        $indexes_info = [];
2629        $indexes_data = [];
2630
2631        // view
2632        foreach ($indexes as $row) {
2633            // Backups the list of primary keys
2634            if ($row['Key_name'] === 'PRIMARY') {
2635                $primary   .= $row['Column_name'] . ', ';
2636                $pk_array[$row['Column_name']] = 1;
2637            }
2638            // Retains keys informations
2639            if ($row['Key_name'] != $lastIndex) {
2640                $indexes[] = $row['Key_name'];
2641                $lastIndex = $row['Key_name'];
2642            }
2643            $indexes_info[$row['Key_name']]['Sequences'][] = $row['Seq_in_index'];
2644            $indexes_info[$row['Key_name']]['Non_unique'] = $row['Non_unique'];
2645            if (isset($row['Cardinality'])) {
2646                $indexes_info[$row['Key_name']]['Cardinality'] = $row['Cardinality'];
2647            }
2648            // I don't know what does following column mean....
2649            // $indexes_info[$row['Key_name']]['Packed']          = $row['Packed'];
2650
2651            $indexes_info[$row['Key_name']]['Comment'] = $row['Comment'];
2652
2653            $indexes_data[$row['Key_name']][$row['Seq_in_index']]['Column_name']
2654                = $row['Column_name'];
2655            if (! isset($row['Sub_part'])) {
2656                continue;
2657            }
2658
2659            $indexes_data[$row['Key_name']][$row['Seq_in_index']]['Sub_part']
2660                = $row['Sub_part'];
2661        }
2662
2663        return [
2664            $primary,
2665            $pk_array,
2666            $indexes_info,
2667            $indexes_data,
2668        ];
2669    }
2670
2671    /**
2672     * Returns whether the database server supports virtual columns
2673     *
2674     * @return bool
2675     */
2676    public static function isVirtualColumnsSupported()
2677    {
2678        global $dbi;
2679
2680        $serverType = self::getServerType();
2681        $serverVersion = $dbi->getVersion();
2682
2683        return in_array($serverType, ['MySQL', 'Percona Server']) && $serverVersion >= 50705
2684             || ($serverType === 'MariaDB' && $serverVersion >= 50200);
2685    }
2686
2687    /**
2688     * Gets the list of tables in the current db and information about these
2689     * tables if possible
2690     *
2691     * @param string      $db       database name
2692     * @param string|null $sub_part part of script name
2693     *
2694     * @return array
2695     */
2696    public static function getDbInfo($db, ?string $sub_part)
2697    {
2698        global $cfg, $dbi;
2699
2700        /**
2701         * limits for table list
2702         */
2703        if (! isset($_SESSION['tmpval']['table_limit_offset'])
2704            || $_SESSION['tmpval']['table_limit_offset_db'] != $db
2705        ) {
2706            $_SESSION['tmpval']['table_limit_offset'] = 0;
2707            $_SESSION['tmpval']['table_limit_offset_db'] = $db;
2708        }
2709        if (isset($_REQUEST['pos'])) {
2710            $_SESSION['tmpval']['table_limit_offset'] = (int) $_REQUEST['pos'];
2711        }
2712        $pos = $_SESSION['tmpval']['table_limit_offset'];
2713
2714        /**
2715         * whether to display extended stats
2716         */
2717        $isShowStats = $cfg['ShowStats'];
2718
2719        /**
2720         * whether selected db is information_schema
2721         */
2722        $isSystemSchema = false;
2723
2724        if (Utilities::isSystemSchema($db)) {
2725            $isShowStats = false;
2726            $isSystemSchema = true;
2727        }
2728
2729        /**
2730         * information about tables in db
2731         */
2732        $tables = [];
2733
2734        $tooltip_truename = [];
2735        $tooltip_aliasname = [];
2736
2737        // Special speedup for newer MySQL Versions (in 4.0 format changed)
2738        if ($cfg['SkipLockedTables'] === true) {
2739            $db_info_result = $dbi->query(
2740                'SHOW OPEN TABLES FROM ' . self::backquote($db) . ' WHERE In_use > 0;'
2741            );
2742
2743            // Blending out tables in use
2744            if ($db_info_result && $dbi->numRows($db_info_result) > 0) {
2745                $tables = self::getTablesWhenOpen($db, $db_info_result);
2746            } elseif ($db_info_result) {
2747                $dbi->freeResult($db_info_result);
2748            }
2749        }
2750
2751        if (empty($tables)) {
2752            // Set some sorting defaults
2753            $sort = 'Name';
2754            $sort_order = 'ASC';
2755
2756            if (isset($_REQUEST['sort'])) {
2757                $sortable_name_mappings = [
2758                    'table'       => 'Name',
2759                    'records'     => 'Rows',
2760                    'type'        => 'Engine',
2761                    'collation'   => 'Collation',
2762                    'size'        => 'Data_length',
2763                    'overhead'    => 'Data_free',
2764                    'creation'    => 'Create_time',
2765                    'last_update' => 'Update_time',
2766                    'last_check'  => 'Check_time',
2767                    'comment'     => 'Comment',
2768                ];
2769
2770                // Make sure the sort type is implemented
2771                if (isset($sortable_name_mappings[$_REQUEST['sort']])) {
2772                    $sort = $sortable_name_mappings[$_REQUEST['sort']];
2773                    if ($_REQUEST['sort_order'] === 'DESC') {
2774                        $sort_order = 'DESC';
2775                    }
2776                }
2777            }
2778
2779            $groupWithSeparator = false;
2780            $tbl_type = null;
2781            $limit_offset = 0;
2782            $limit_count = false;
2783            $groupTable = [];
2784
2785            if (! empty($_REQUEST['tbl_group']) || ! empty($_REQUEST['tbl_type'])) {
2786                if (! empty($_REQUEST['tbl_type'])) {
2787                    // only tables for selected type
2788                    $tbl_type = $_REQUEST['tbl_type'];
2789                }
2790                if (! empty($_REQUEST['tbl_group'])) {
2791                    // only tables for selected group
2792                    $tbl_group = $_REQUEST['tbl_group'];
2793                    // include the table with the exact name of the group if such
2794                    // exists
2795                    $groupTable = $dbi->getTablesFull(
2796                        $db,
2797                        $tbl_group,
2798                        false,
2799                        $limit_offset,
2800                        $limit_count,
2801                        $sort,
2802                        $sort_order,
2803                        $tbl_type
2804                    );
2805                    $groupWithSeparator = $tbl_group
2806                        . $GLOBALS['cfg']['NavigationTreeTableSeparator'];
2807                }
2808            } else {
2809                // all tables in db
2810                // - get the total number of tables
2811                //  (needed for proper working of the MaxTableList feature)
2812                $tables = $dbi->getTables($db);
2813                $total_num_tables = count($tables);
2814                if (! (isset($sub_part) && $sub_part === '_export')) {
2815                    // fetch the details for a possible limited subset
2816                    $limit_offset = $pos;
2817                    $limit_count = true;
2818                }
2819            }
2820            $tables = array_merge(
2821                $groupTable,
2822                $dbi->getTablesFull(
2823                    $db,
2824                    $groupWithSeparator !== false ? $groupWithSeparator : '',
2825                    $groupWithSeparator !== false,
2826                    $limit_offset,
2827                    $limit_count,
2828                    $sort,
2829                    $sort_order,
2830                    $tbl_type
2831                )
2832            );
2833        }
2834
2835        $num_tables = count($tables);
2836        //  (needed for proper working of the MaxTableList feature)
2837        if (! isset($total_num_tables)) {
2838            $total_num_tables = $num_tables;
2839        }
2840
2841        /**
2842         * If coming from a Show MySQL link on the home page,
2843         * put something in $sub_part
2844         */
2845        if (empty($sub_part)) {
2846            $sub_part = '_structure';
2847        }
2848
2849        return [
2850            $tables,
2851            $num_tables,
2852            $total_num_tables,
2853            $sub_part,
2854            $isShowStats,
2855            $isSystemSchema,
2856            $tooltip_truename,
2857            $tooltip_aliasname,
2858            $pos,
2859        ];
2860    }
2861
2862    /**
2863     * Gets the list of tables in the current db, taking into account
2864     * that they might be "in use"
2865     *
2866     * @param string $db             database name
2867     * @param object $db_info_result result set
2868     *
2869     * @return array list of tables
2870     */
2871    public static function getTablesWhenOpen($db, $db_info_result): array
2872    {
2873        global $dbi;
2874
2875        $sot_cache = [];
2876        $tables = [];
2877
2878        while ($tmp = $dbi->fetchAssoc($db_info_result)) {
2879            $sot_cache[$tmp['Table']] = true;
2880        }
2881        $dbi->freeResult($db_info_result);
2882
2883        // is there at least one "in use" table?
2884        if (count($sot_cache) > 0) {
2885            $tblGroupSql = '';
2886            $whereAdded = false;
2887            if (Core::isValid($_REQUEST['tbl_group'])) {
2888                $group = self::escapeMysqlWildcards($_REQUEST['tbl_group']);
2889                $groupWithSeparator = self::escapeMysqlWildcards(
2890                    $_REQUEST['tbl_group']
2891                    . $GLOBALS['cfg']['NavigationTreeTableSeparator']
2892                );
2893                $tblGroupSql .= ' WHERE ('
2894                    . self::backquote('Tables_in_' . $db)
2895                    . " LIKE '" . $groupWithSeparator . "%'"
2896                    . ' OR '
2897                    . self::backquote('Tables_in_' . $db)
2898                    . " LIKE '" . $group . "')";
2899                $whereAdded = true;
2900            }
2901            if (Core::isValid($_REQUEST['tbl_type'], ['table', 'view'])) {
2902                $tblGroupSql .= $whereAdded ? ' AND' : ' WHERE';
2903                if ($_REQUEST['tbl_type'] === 'view') {
2904                    $tblGroupSql .= " `Table_type` NOT IN ('BASE TABLE', 'SYSTEM VERSIONED')";
2905                } else {
2906                    $tblGroupSql .= " `Table_type` IN ('BASE TABLE', 'SYSTEM VERSIONED')";
2907                }
2908            }
2909            $db_info_result = $dbi->query(
2910                'SHOW FULL TABLES FROM ' . self::backquote($db) . $tblGroupSql,
2911                DatabaseInterface::CONNECT_USER,
2912                DatabaseInterface::QUERY_STORE
2913            );
2914            unset($tblGroupSql, $whereAdded);
2915
2916            if ($db_info_result && $dbi->numRows($db_info_result) > 0) {
2917                $names = [];
2918                while ($tmp = $dbi->fetchRow($db_info_result)) {
2919                    if (! isset($sot_cache[$tmp[0]])) {
2920                        $names[] = $tmp[0];
2921                    } else { // table in use
2922                        $tables[$tmp[0]] = [
2923                            'TABLE_NAME' => $tmp[0],
2924                            'ENGINE' => '',
2925                            'TABLE_TYPE' => '',
2926                            'TABLE_ROWS' => 0,
2927                            'TABLE_COMMENT' => '',
2928                        ];
2929                    }
2930                }
2931                if (count($names) > 0) {
2932                    $tables = array_merge(
2933                        $tables,
2934                        $dbi->getTablesFull($db, $names)
2935                    );
2936                }
2937                if ($GLOBALS['cfg']['NaturalOrder']) {
2938                    uksort($tables, 'strnatcasecmp');
2939                }
2940            } elseif ($db_info_result) {
2941                $dbi->freeResult($db_info_result);
2942            }
2943            unset($sot_cache);
2944        }
2945
2946        return $tables;
2947    }
2948
2949    /**
2950     * Checks whether database extension is loaded
2951     *
2952     * @param string $extension mysql extension to check
2953     */
2954    public static function checkDbExtension(string $extension = 'mysqli'): bool
2955    {
2956        return function_exists($extension . '_connect');
2957    }
2958
2959    /**
2960     * Returns list of used PHP extensions.
2961     *
2962     * @return string[]
2963     */
2964    public static function listPHPExtensions(): array
2965    {
2966        $result = [];
2967        if (self::checkDbExtension('mysqli')) {
2968            $result[] = 'mysqli';
2969        }
2970
2971        if (extension_loaded('curl')) {
2972            $result[] = 'curl';
2973        }
2974
2975        if (extension_loaded('mbstring')) {
2976            $result[] = 'mbstring';
2977        }
2978
2979        return $result;
2980    }
2981
2982    /**
2983     * Converts given (request) parameter to string
2984     *
2985     * @param mixed $value Value to convert
2986     */
2987    public static function requestString($value): string
2988    {
2989        while (is_array($value) || is_object($value)) {
2990            if (is_object($value)) {
2991                $value = (array) $value;
2992            }
2993            $value = reset($value);
2994        }
2995
2996        return trim((string) $value);
2997    }
2998
2999    /**
3000     * Generates random string consisting of ASCII chars
3001     *
3002     * @param int  $length Length of string
3003     * @param bool $asHex  (optional) Send the result as hex
3004     */
3005    public static function generateRandom(int $length, bool $asHex = false): string
3006    {
3007        $result = '';
3008        if (class_exists(Random::class)) {
3009            $random_func = [
3010                Random::class,
3011                'string',
3012            ];
3013        } else {
3014            $random_func = 'openssl_random_pseudo_bytes';
3015        }
3016        while (strlen($result) < $length) {
3017            // Get random byte and strip highest bit
3018            // to get ASCII only range
3019            $byte = ord((string) $random_func(1)) & 0x7f;
3020            // We want only ASCII chars and no DEL character (127)
3021            if ($byte <= 32 || $byte === 127) {
3022                continue;
3023            }
3024
3025            $result .= chr($byte);
3026        }
3027
3028        return $asHex ? bin2hex($result) : $result;
3029    }
3030
3031    /**
3032     * Wrapper around PHP date function
3033     *
3034     * @param string $format Date format string
3035     *
3036     * @return string
3037     */
3038    public static function date($format)
3039    {
3040        if (defined('TESTSUITE')) {
3041            return '0000-00-00 00:00:00';
3042        }
3043
3044        return date($format);
3045    }
3046
3047    /**
3048     * Wrapper around php's set_time_limit
3049     */
3050    public static function setTimeLimit(): void
3051    {
3052        // The function can be disabled in php.ini
3053        if (! function_exists('set_time_limit')) {
3054            return;
3055        }
3056
3057        @set_time_limit((int) $GLOBALS['cfg']['ExecTimeLimit']);
3058    }
3059
3060    /**
3061     * Access to a multidimensional array by dot notation
3062     *
3063     * @param array        $array   List of values
3064     * @param string|array $path    Path to searched value
3065     * @param mixed        $default Default value
3066     *
3067     * @return mixed Searched value
3068     */
3069    public static function getValueByKey(array $array, $path, $default = null)
3070    {
3071        if (is_string($path)) {
3072            $path = explode('.', $path);
3073        }
3074        $p = array_shift($path);
3075        while (isset($p)) {
3076            if (! isset($array[$p])) {
3077                return $default;
3078            }
3079            $array = $array[$p];
3080            $p = array_shift($path);
3081        }
3082
3083        return $array;
3084    }
3085
3086    /**
3087     * Creates a clickable column header for table information
3088     *
3089     * @param string $title            Title to use for the link
3090     * @param string $sort             Corresponds to sortable data name mapped
3091     *                                 in Util::getDbInfo
3092     * @param string $initialSortOrder Initial sort order
3093     *
3094     * @return string Link to be displayed in the table header
3095     */
3096    public static function sortableTableHeader($title, $sort, $initialSortOrder = 'ASC')
3097    {
3098        $requestedSort = 'table';
3099        $requestedSortOrder = $futureSortOrder = $initialSortOrder;
3100        // If the user requested a sort
3101        if (isset($_REQUEST['sort'])) {
3102            $requestedSort = $_REQUEST['sort'];
3103            if (isset($_REQUEST['sort_order'])) {
3104                $requestedSortOrder = $_REQUEST['sort_order'];
3105            }
3106        }
3107        $orderImg = '';
3108        $orderLinkParams = [];
3109        $orderLinkParams['title'] = __('Sort');
3110        // If this column was requested to be sorted.
3111        if ($requestedSort == $sort) {
3112            if ($requestedSortOrder === 'ASC') {
3113                $futureSortOrder = 'DESC';
3114                // current sort order is ASC
3115                $orderImg = ' ' . Generator::getImage(
3116                    's_asc',
3117                    __('Ascending'),
3118                    [
3119                        'class' => 'sort_arrow',
3120                        'title' => '',
3121                    ]
3122                );
3123                $orderImg .= ' ' . Generator::getImage(
3124                    's_desc',
3125                    __('Descending'),
3126                    [
3127                        'class' => 'sort_arrow hide',
3128                        'title' => '',
3129                    ]
3130                );
3131                // but on mouse over, show the reverse order (DESC)
3132                $orderLinkParams['onmouseover'] = "$('.sort_arrow').toggle();";
3133                // on mouse out, show current sort order (ASC)
3134                $orderLinkParams['onmouseout'] = "$('.sort_arrow').toggle();";
3135            } else {
3136                $futureSortOrder = 'ASC';
3137                // current sort order is DESC
3138                $orderImg = ' ' . Generator::getImage(
3139                    's_asc',
3140                    __('Ascending'),
3141                    [
3142                        'class' => 'sort_arrow hide',
3143                        'title' => '',
3144                    ]
3145                );
3146                $orderImg .= ' ' . Generator::getImage(
3147                    's_desc',
3148                    __('Descending'),
3149                    [
3150                        'class' => 'sort_arrow',
3151                        'title' => '',
3152                    ]
3153                );
3154                // but on mouse over, show the reverse order (ASC)
3155                $orderLinkParams['onmouseover'] = "$('.sort_arrow').toggle();";
3156                // on mouse out, show current sort order (DESC)
3157                $orderLinkParams['onmouseout'] = "$('.sort_arrow').toggle();";
3158            }
3159        }
3160        $urlParams = [
3161            'db' => $_REQUEST['db'],
3162            'pos' => 0, // We set the position back to 0 every time they sort.
3163            'sort' => $sort,
3164            'sort_order' => $futureSortOrder,
3165        ];
3166
3167        if (Core::isValid($_REQUEST['tbl_type'], ['view', 'table'])) {
3168            $urlParams['tbl_type'] = $_REQUEST['tbl_type'];
3169        }
3170        if (! empty($_REQUEST['tbl_group'])) {
3171            $urlParams['tbl_group'] = $_REQUEST['tbl_group'];
3172        }
3173
3174        $url = Url::getFromRoute('/database/structure');
3175
3176        return Generator::linkOrButton($url, $urlParams, $title . $orderImg, $orderLinkParams);
3177    }
3178
3179    /**
3180     * Check that input is an int or an int in a string
3181     *
3182     * @param mixed $input input to check
3183     */
3184    public static function isInteger($input): bool
3185    {
3186        return ctype_digit((string) $input);
3187    }
3188
3189    /**
3190     * Get the protocol from the RFC 7239 Forwarded header
3191     *
3192     * @param string $headerContents The Forwarded header contents
3193     *
3194     * @return string the protocol http/https
3195     */
3196    public static function getProtoFromForwardedHeader(string $headerContents): string
3197    {
3198        if (strpos($headerContents, '=') !== false) {// does not contain any equal sign
3199            $hops = explode(',', $headerContents);
3200            $parts = explode(';', $hops[0]);
3201            foreach ($parts as $part) {
3202                $keyValueArray = explode('=', $part, 2);
3203                if (count($keyValueArray) !== 2) {
3204                    continue;
3205                }
3206
3207                [
3208                    $keyName,
3209                    $value,
3210                ] = $keyValueArray;
3211                $value = trim(strtolower($value));
3212                if (strtolower(trim($keyName)) === 'proto' && in_array($value, ['http', 'https'])) {
3213                    return $value;
3214                }
3215            }
3216        }
3217
3218        return '';
3219    }
3220
3221    /**
3222     * Check if error reporting is available
3223     */
3224    public static function isErrorReportingAvailable(): bool
3225    {
3226        // issue #16256 - PHP 7.x does not return false for a core function
3227        if (PHP_MAJOR_VERSION < 8) {
3228            $disabled = ini_get('disable_functions');
3229            if (is_string($disabled)) {
3230                $disabled = explode(',', $disabled);
3231                $disabled = array_map(static function (string $part) {
3232                    return trim($part);
3233                }, $disabled);
3234
3235                return ! in_array('error_reporting', $disabled);
3236            }
3237        }
3238
3239        return function_exists('error_reporting');
3240    }
3241}
3242