"Fossies" - the Fresh Open Source Software Archive

Member "phpMyAdmin-5.1.0-english/libraries/classes/Util.php" (24 Feb 2021, 104070 Bytes) of package /linux/www/phpMyAdmin-5.1.0-english.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "Util.php" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 5.0.4-english_vs_5.1.0-english.

    1 <?php
    2 
    3 declare(strict_types=1);
    4 
    5 namespace PhpMyAdmin;
    6 
    7 use PhpMyAdmin\Html\Generator;
    8 use PhpMyAdmin\Html\MySQLDocumentation;
    9 use PhpMyAdmin\Query\Utilities;
   10 use PhpMyAdmin\SqlParser\Components\Expression;
   11 use PhpMyAdmin\SqlParser\Context;
   12 use PhpMyAdmin\SqlParser\Token;
   13 use PhpMyAdmin\Utils\SessionCache;
   14 use phpseclib\Crypt\Random;
   15 use stdClass;
   16 use const ENT_COMPAT;
   17 use const ENT_QUOTES;
   18 use const PHP_INT_SIZE;
   19 use const PHP_MAJOR_VERSION;
   20 use const PREG_OFFSET_CAPTURE;
   21 use const STR_PAD_LEFT;
   22 use function abs;
   23 use function array_key_exists;
   24 use function array_map;
   25 use function array_merge;
   26 use function array_shift;
   27 use function array_unique;
   28 use function basename;
   29 use function bin2hex;
   30 use function chr;
   31 use function class_exists;
   32 use function count;
   33 use function ctype_digit;
   34 use function date;
   35 use function decbin;
   36 use function defined;
   37 use function explode;
   38 use function extension_loaded;
   39 use function fclose;
   40 use function floatval;
   41 use function floor;
   42 use function fread;
   43 use function function_exists;
   44 use function html_entity_decode;
   45 use function htmlentities;
   46 use function htmlspecialchars;
   47 use function htmlspecialchars_decode;
   48 use function implode;
   49 use function in_array;
   50 use function ini_get;
   51 use function is_array;
   52 use function is_callable;
   53 use function is_object;
   54 use function is_string;
   55 use function log10;
   56 use function mb_detect_encoding;
   57 use function mb_strlen;
   58 use function mb_strpos;
   59 use function mb_strrpos;
   60 use function mb_strstr;
   61 use function mb_strtolower;
   62 use function mb_substr;
   63 use function number_format;
   64 use function ord;
   65 use function parse_url;
   66 use function pow;
   67 use function preg_match;
   68 use function preg_quote;
   69 use function preg_replace;
   70 use function range;
   71 use function reset;
   72 use function round;
   73 use function rtrim;
   74 use function set_time_limit;
   75 use function sort;
   76 use function sprintf;
   77 use function str_pad;
   78 use function str_replace;
   79 use function strcasecmp;
   80 use function strftime;
   81 use function stripos;
   82 use function strlen;
   83 use function strpos;
   84 use function strrev;
   85 use function strtolower;
   86 use function strtoupper;
   87 use function strtr;
   88 use function substr;
   89 use function time;
   90 use function trim;
   91 use function uksort;
   92 use function version_compare;
   93 
   94 /**
   95  * Misc functions used all over the scripts.
   96  */
   97 class 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|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 The numerical part of the expression (for example 8)
  700      */
  701     public static function extractValueFromFormattedSize($formatted_size): int
  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         // timestamp is numeric on some MySQL 4.1
  961         // for real we use CONCAT above and it should compare to string
  962         if ($meta->numeric
  963             && ($meta->type !== 'timestamp')
  964             && ($meta->type !== 'real')
  965         ) {
  966             $conditionValue = '= ' . $row;
  967         } elseif (($meta->type === 'blob') || ($meta->type === 'string')
  968             && stripos($fieldFlags, 'BINARY') !== false
  969             && ! empty($row)
  970         ) {
  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             if (mb_strlen((string) $row) < 1000) {
  975                 // use a CAST if possible, to avoid problems
  976                 // if the field contains wildcard characters % or _
  977                 $conditionValue = '= CAST(0x' . bin2hex((string) $row) . ' AS BINARY)';
  978             } elseif ($fieldsCount === 1) {
  979                 // when this blob is the only field present
  980                 // try settling with length comparison
  981                 $condition = ' CHAR_LENGTH(' . $conditionKey . ') ';
  982                 $conditionValue = ' = ' . mb_strlen((string) $row);
  983             } else {
  984                 // this blob won't be part of the final condition
  985                 $conditionValue = null;
  986             }
  987         } elseif (in_array($meta->type, self::getGISDatatypes())
  988             && ! empty($row)
  989         ) {
  990             // do not build a too big condition
  991             if (mb_strlen((string) $row) < 5000) {
  992                 $condition .= '=0x' . bin2hex((string) $row) . ' AND';
  993             } else {
  994                 $condition = '';
  995             }
  996         } elseif ($meta->type === 'bit') {
  997             $conditionValue = "= b'"
  998                 . self::printableBitValue((int) $row, (int) $meta->length) . "'";
  999         } else {
 1000             $conditionValue = '= \''
 1001                 . $dbi->escapeString($row) . '\'';
 1002         }
 1003 
 1004         return [$conditionValue, $condition];
 1005     }
 1006 
 1007     /**
 1008      * Function to generate unique condition for specified row.
 1009      *
 1010      * @param resource|int $handle            current query result
 1011      * @param int          $fields_cnt        number of fields
 1012      * @param stdClass[]   $fields_meta       meta information about fields
 1013      * @param array        $row               current row
 1014      * @param bool         $force_unique      generate condition only on pk or unique
 1015      * @param string|bool  $restrict_to_table restrict the unique condition to this table or false if none
 1016      * @param Expression[] $expressions       An array of Expression instances.
 1017      *
 1018      * @return array the calculated condition and whether condition is unique
 1019      */
 1020     public static function getUniqueCondition(
 1021         $handle,
 1022         $fields_cnt,
 1023         array $fields_meta,
 1024         array $row,
 1025         $force_unique = false,
 1026         $restrict_to_table = false,
 1027         array $expressions = []
 1028     ): array {
 1029         global $dbi;
 1030 
 1031         $primary_key          = '';
 1032         $unique_key           = '';
 1033         $nonprimary_condition = '';
 1034         $preferred_condition = '';
 1035         $primary_key_array    = [];
 1036         $unique_key_array     = [];
 1037         $nonprimary_condition_array = [];
 1038         $condition_array = [];
 1039 
 1040         for ($i = 0; $i < $fields_cnt; ++$i) {
 1041             $meta        = $fields_meta[$i];
 1042 
 1043             // do not use a column alias in a condition
 1044             if (! isset($meta->orgname) || strlen($meta->orgname) === 0) {
 1045                 $meta->orgname = $meta->name;
 1046 
 1047                 foreach ($expressions as $expression) {
 1048                     if (empty($expression->alias) || empty($expression->column)) {
 1049                         continue;
 1050                     }
 1051                     if (strcasecmp($meta->name, $expression->alias) == 0) {
 1052                         $meta->orgname = $expression->column;
 1053                         break;
 1054                     }
 1055                 }
 1056             }
 1057 
 1058             // Do not use a table alias in a condition.
 1059             // Test case is:
 1060             // select * from galerie x WHERE
 1061             //(select count(*) from galerie y where y.datum=x.datum)>1
 1062             //
 1063             // But orgtable is present only with mysqli extension so the
 1064             // fix is only for mysqli.
 1065             // Also, do not use the original table name if we are dealing with
 1066             // a view because this view might be updatable.
 1067             // (The isView() verification should not be costly in most cases
 1068             // because there is some caching in the function).
 1069             if (isset($meta->orgtable)
 1070                 && ($meta->table != $meta->orgtable)
 1071                 && ! $dbi->getTable($GLOBALS['db'], $meta->table)->isView()
 1072             ) {
 1073                 $meta->table = $meta->orgtable;
 1074             }
 1075 
 1076             // If this field is not from the table which the unique clause needs
 1077             // to be restricted to.
 1078             if ($restrict_to_table && $restrict_to_table != $meta->table) {
 1079                 continue;
 1080             }
 1081 
 1082             // to fix the bug where float fields (primary or not)
 1083             // can't be matched because of the imprecision of
 1084             // floating comparison, use CONCAT
 1085             // (also, the syntax "CONCAT(field) IS NULL"
 1086             // that we need on the next "if" will work)
 1087             if ($meta->type === 'real') {
 1088                 $con_key = 'CONCAT(' . self::backquote($meta->table) . '.'
 1089                     . self::backquote($meta->orgname) . ')';
 1090             } else {
 1091                 $con_key = self::backquote($meta->table) . '.'
 1092                     . self::backquote($meta->orgname);
 1093             }
 1094             $condition = ' ' . $con_key . ' ';
 1095 
 1096             [$con_val, $condition] = self::getConditionValue(
 1097                 $row[$i] ?? null,
 1098                 $meta,
 1099                 $dbi->fieldFlags($handle, $i),
 1100                 $fields_cnt,
 1101                 $con_key,
 1102                 $condition
 1103             );
 1104 
 1105             if ($con_val === null) {
 1106                 continue;
 1107             }
 1108 
 1109             $condition .= $con_val . ' AND';
 1110 
 1111             if ($meta->primary_key > 0) {
 1112                 $primary_key .= $condition;
 1113                 $primary_key_array[$con_key] = $con_val;
 1114             } elseif ($meta->unique_key > 0) {
 1115                 $unique_key  .= $condition;
 1116                 $unique_key_array[$con_key] = $con_val;
 1117             }
 1118 
 1119             $nonprimary_condition .= $condition;
 1120             $nonprimary_condition_array[$con_key] = $con_val;
 1121         }
 1122 
 1123         // Correction University of Virginia 19991216:
 1124         // prefer primary or unique keys for condition,
 1125         // but use conjunction of all values if no primary key
 1126         $clause_is_unique = true;
 1127 
 1128         if ($primary_key) {
 1129             $preferred_condition = $primary_key;
 1130             $condition_array = $primary_key_array;
 1131         } elseif ($unique_key) {
 1132             $preferred_condition = $unique_key;
 1133             $condition_array = $unique_key_array;
 1134         } elseif (! $force_unique) {
 1135             $preferred_condition = $nonprimary_condition;
 1136             $condition_array = $nonprimary_condition_array;
 1137             $clause_is_unique = false;
 1138         }
 1139 
 1140         $where_clause = trim((string) preg_replace('|\s?AND$|', '', $preferred_condition));
 1141 
 1142         return [
 1143             $where_clause,
 1144             $clause_is_unique,
 1145             $condition_array,
 1146         ];
 1147     }
 1148 
 1149     /**
 1150      * Generate the charset query part
 1151      *
 1152      * @param string $collation Collation
 1153      * @param bool   $override  (optional) force 'CHARACTER SET' keyword
 1154      */
 1155     public static function getCharsetQueryPart(string $collation, bool $override = false): string
 1156     {
 1157         [$charset] = explode('_', $collation);
 1158         $keyword = ' CHARSET=';
 1159 
 1160         if ($override) {
 1161             $keyword = ' CHARACTER SET ';
 1162         }
 1163 
 1164         return $keyword . $charset
 1165             . ($charset == $collation ? '' : ' COLLATE ' . $collation);
 1166     }
 1167 
 1168     /**
 1169      * Generate a pagination selector for browsing resultsets
 1170      *
 1171      * @param string $name        The name for the request parameter
 1172      * @param int    $rows        Number of rows in the pagination set
 1173      * @param int    $pageNow     current page number
 1174      * @param int    $nbTotalPage number of total pages
 1175      * @param int    $showAll     If the number of pages is lower than this
 1176      *                            variable, no pages will be omitted in pagination
 1177      * @param int    $sliceStart  How many rows at the beginning should always
 1178      *                            be shown?
 1179      * @param int    $sliceEnd    How many rows at the end should always be shown?
 1180      * @param int    $percent     Percentage of calculation page offsets to hop to a
 1181      *                            next page
 1182      * @param int    $range       Near the current page, how many pages should
 1183      *                            be considered "nearby" and displayed as well?
 1184      * @param string $prompt      The prompt to display (sometimes empty)
 1185      *
 1186      * @access public
 1187      */
 1188     public static function pageselector(
 1189         $name,
 1190         $rows,
 1191         $pageNow = 1,
 1192         $nbTotalPage = 1,
 1193         $showAll = 200,
 1194         $sliceStart = 5,
 1195         $sliceEnd = 5,
 1196         $percent = 20,
 1197         $range = 10,
 1198         $prompt = ''
 1199     ): string {
 1200         $increment = floor($nbTotalPage / $percent);
 1201         $pageNowMinusRange = $pageNow - $range;
 1202         $pageNowPlusRange = $pageNow + $range;
 1203 
 1204         $gotopage = $prompt . ' <select class="pageselector ajax"';
 1205 
 1206         $gotopage .= ' name="' . $name . '" >';
 1207         if ($nbTotalPage < $showAll) {
 1208             $pages = range(1, $nbTotalPage);
 1209         } else {
 1210             $pages = [];
 1211 
 1212             // Always show first X pages
 1213             for ($i = 1; $i <= $sliceStart; $i++) {
 1214                 $pages[] = $i;
 1215             }
 1216 
 1217             // Always show last X pages
 1218             for ($i = $nbTotalPage - $sliceEnd; $i <= $nbTotalPage; $i++) {
 1219                 $pages[] = $i;
 1220             }
 1221 
 1222             // Based on the number of results we add the specified
 1223             // $percent percentage to each page number,
 1224             // so that we have a representing page number every now and then to
 1225             // immediately jump to specific pages.
 1226             // As soon as we get near our currently chosen page ($pageNow -
 1227             // $range), every page number will be shown.
 1228             $i = $sliceStart;
 1229             $x = $nbTotalPage - $sliceEnd;
 1230             $met_boundary = false;
 1231 
 1232             while ($i <= $x) {
 1233                 if ($i >= $pageNowMinusRange && $i <= $pageNowPlusRange) {
 1234                     // If our pageselector comes near the current page, we use 1
 1235                     // counter increments
 1236                     $i++;
 1237                     $met_boundary = true;
 1238                 } else {
 1239                     // We add the percentage increment to our current page to
 1240                     // hop to the next one in range
 1241                     $i += $increment;
 1242 
 1243                     // Make sure that we do not cross our boundaries.
 1244                     if ($i > $pageNowMinusRange && ! $met_boundary) {
 1245                         $i = $pageNowMinusRange;
 1246                     }
 1247                 }
 1248 
 1249                 if ($i <= 0 || $i > $x) {
 1250                     continue;
 1251                 }
 1252 
 1253                 $pages[] = $i;
 1254             }
 1255 
 1256             /*
 1257             Add page numbers with "geometrically increasing" distances.
 1258 
 1259             This helps me a lot when navigating through giant tables.
 1260 
 1261             Test case: table with 2.28 million sets, 76190 pages. Page of interest
 1262             is between 72376 and 76190.
 1263             Selecting page 72376.
 1264             Now, old version enumerated only +/- 10 pages around 72376 and the
 1265             percentage increment produced steps of about 3000.
 1266 
 1267             The following code adds page numbers +/- 2,4,8,16,32,64,128,256 etc.
 1268             around the current page.
 1269             */
 1270             $i = $pageNow;
 1271             $dist = 1;
 1272             while ($i < $x) {
 1273                 $dist = 2 * $dist;
 1274                 $i = $pageNow + $dist;
 1275                 if ($i <= 0 || $i > $x) {
 1276                     continue;
 1277                 }
 1278 
 1279                 $pages[] = $i;
 1280             }
 1281 
 1282             $i = $pageNow;
 1283             $dist = 1;
 1284             while ($i > 0) {
 1285                 $dist = 2 * $dist;
 1286                 $i = $pageNow - $dist;
 1287                 if ($i <= 0 || $i > $x) {
 1288                     continue;
 1289                 }
 1290 
 1291                 $pages[] = $i;
 1292             }
 1293 
 1294             // Since because of ellipsing of the current page some numbers may be
 1295             // double, we unify our array:
 1296             sort($pages);
 1297             $pages = array_unique($pages);
 1298         }
 1299 
 1300         if ($pageNow > $nbTotalPage) {
 1301             $pages[] = $pageNow;
 1302         }
 1303 
 1304         foreach ($pages as $i) {
 1305             if ($i == $pageNow) {
 1306                 $selected = 'selected="selected" style="font-weight: bold"';
 1307             } else {
 1308                 $selected = '';
 1309             }
 1310             $gotopage .= '                <option ' . $selected
 1311                 . ' value="' . (($i - 1) * $rows) . '">' . $i . '</option>' . "\n";
 1312         }
 1313 
 1314         $gotopage .= ' </select>';
 1315 
 1316         return $gotopage;
 1317     }
 1318 
 1319     /**
 1320      * Calculate page number through position
 1321      *
 1322      * @param int $pos       position of first item
 1323      * @param int $max_count number of items per page
 1324      *
 1325      * @return int $page_num
 1326      *
 1327      * @access public
 1328      */
 1329     public static function getPageFromPosition($pos, $max_count)
 1330     {
 1331         return (int) floor($pos / $max_count) + 1;
 1332     }
 1333 
 1334     /**
 1335      * replaces %u in given path with current user name
 1336      *
 1337      * example:
 1338      * <code>
 1339      * $user_dir = userDir('/var/pma_tmp/%u/'); // '/var/pma_tmp/root/'
 1340      *
 1341      * </code>
 1342      *
 1343      * @param string $dir with wildcard for user
 1344      *
 1345      * @return string per user directory
 1346      */
 1347     public static function userDir($dir): string
 1348     {
 1349         // add trailing slash
 1350         if (mb_substr($dir, -1) !== '/') {
 1351             $dir .= '/';
 1352         }
 1353 
 1354         return str_replace('%u', Core::securePath($GLOBALS['cfg']['Server']['user']), $dir);
 1355     }
 1356 
 1357     /**
 1358      * Clears cache content which needs to be refreshed on user change.
 1359      */
 1360     public static function clearUserCache(): void
 1361     {
 1362         SessionCache::remove('is_superuser');
 1363         SessionCache::remove('is_createuser');
 1364         SessionCache::remove('is_grantuser');
 1365     }
 1366 
 1367     /**
 1368      * Converts a bit value to printable format;
 1369      * in MySQL a BIT field can be from 1 to 64 bits so we need this
 1370      * function because in PHP, decbin() supports only 32 bits
 1371      * on 32-bit servers
 1372      *
 1373      * @param int $value  coming from a BIT field
 1374      * @param int $length length
 1375      *
 1376      * @return string the printable value
 1377      */
 1378     public static function printableBitValue(int $value, int $length): string
 1379     {
 1380         // if running on a 64-bit server or the length is safe for decbin()
 1381         if (PHP_INT_SIZE == 8 || $length < 33) {
 1382             $printable = decbin($value);
 1383         } else {
 1384             // FIXME: does not work for the leftmost bit of a 64-bit value
 1385             $i = 0;
 1386             $printable = '';
 1387             while ($value >= pow(2, $i)) {
 1388                 ++$i;
 1389             }
 1390             if ($i != 0) {
 1391                 --$i;
 1392             }
 1393 
 1394             while ($i >= 0) {
 1395                 if ($value - pow(2, $i) < 0) {
 1396                     $printable = '0' . $printable;
 1397                 } else {
 1398                     $printable = '1' . $printable;
 1399                     $value -= pow(2, $i);
 1400                 }
 1401                 --$i;
 1402             }
 1403             $printable = strrev($printable);
 1404         }
 1405         $printable = str_pad($printable, $length, '0', STR_PAD_LEFT);
 1406 
 1407         return $printable;
 1408     }
 1409 
 1410     /**
 1411      * Converts a BIT type default value
 1412      * for example, b'010' becomes 010
 1413      *
 1414      * @param string|null $bitDefaultValue value
 1415      *
 1416      * @return string the converted value
 1417      */
 1418     public static function convertBitDefaultValue(?string $bitDefaultValue): string
 1419     {
 1420         return (string) preg_replace(
 1421             "/^b'(\d*)'?$/",
 1422             '$1',
 1423             htmlspecialchars_decode((string) $bitDefaultValue, ENT_QUOTES),
 1424             1
 1425         );
 1426     }
 1427 
 1428     /**
 1429      * Extracts the various parts from a column spec
 1430      *
 1431      * @param string $columnspec Column specification
 1432      *
 1433      * @return array associative array containing type, spec_in_brackets
 1434      *          and possibly enum_set_values (another array)
 1435      */
 1436     public static function extractColumnSpec($columnspec)
 1437     {
 1438         $first_bracket_pos = mb_strpos($columnspec, '(');
 1439         if ($first_bracket_pos) {
 1440             $spec_in_brackets = rtrim(
 1441                 mb_substr(
 1442                     $columnspec,
 1443                     $first_bracket_pos + 1,
 1444                     mb_strrpos($columnspec, ')') - $first_bracket_pos - 1
 1445                 )
 1446             );
 1447             // convert to lowercase just to be sure
 1448             $type = mb_strtolower(
 1449                 rtrim(mb_substr($columnspec, 0, $first_bracket_pos))
 1450             );
 1451         } else {
 1452             // Split trailing attributes such as unsigned,
 1453             // binary, zerofill and get data type name
 1454             $type_parts = explode(' ', $columnspec);
 1455             $type = mb_strtolower($type_parts[0]);
 1456             $spec_in_brackets = '';
 1457         }
 1458 
 1459         if ($type === 'enum' || $type === 'set') {
 1460             // Define our working vars
 1461             $enum_set_values = self::parseEnumSetValues($columnspec, false);
 1462             $printtype = $type
 1463                 . '(' . str_replace("','", "', '", $spec_in_brackets) . ')';
 1464             $binary = false;
 1465             $unsigned = false;
 1466             $zerofill = false;
 1467         } else {
 1468             $enum_set_values = [];
 1469 
 1470             /* Create printable type name */
 1471             $printtype = mb_strtolower($columnspec);
 1472 
 1473             // Strip the "BINARY" attribute, except if we find "BINARY(" because
 1474             // this would be a BINARY or VARBINARY column type;
 1475             // by the way, a BLOB should not show the BINARY attribute
 1476             // because this is not accepted in MySQL syntax.
 1477             if (strpos($printtype, 'binary') !== false
 1478                 && ! preg_match('@binary[\(]@', $printtype)
 1479             ) {
 1480                 $printtype = str_replace('binary', '', $printtype);
 1481                 $binary = true;
 1482             } else {
 1483                 $binary = false;
 1484             }
 1485 
 1486             $printtype = (string) preg_replace(
 1487                 '@zerofill@',
 1488                 '',
 1489                 $printtype,
 1490                 -1,
 1491                 $zerofill_cnt
 1492             );
 1493             $zerofill = ($zerofill_cnt > 0);
 1494             $printtype = (string) preg_replace(
 1495                 '@unsigned@',
 1496                 '',
 1497                 $printtype,
 1498                 -1,
 1499                 $unsigned_cnt
 1500             );
 1501             $unsigned = ($unsigned_cnt > 0);
 1502             $printtype = trim($printtype);
 1503         }
 1504 
 1505         $attribute     = ' ';
 1506         if ($binary) {
 1507             $attribute = 'BINARY';
 1508         }
 1509         if ($unsigned) {
 1510             $attribute = 'UNSIGNED';
 1511         }
 1512         if ($zerofill) {
 1513             $attribute = 'UNSIGNED ZEROFILL';
 1514         }
 1515 
 1516         $can_contain_collation = false;
 1517         if (! $binary
 1518             && preg_match(
 1519                 '@^(char|varchar|text|tinytext|mediumtext|longtext|set|enum)@',
 1520                 $type
 1521             )
 1522         ) {
 1523             $can_contain_collation = true;
 1524         }
 1525 
 1526         // for the case ENUM('&#8211;','&ldquo;')
 1527         $displayed_type = htmlspecialchars($printtype);
 1528         if (mb_strlen($printtype) > $GLOBALS['cfg']['LimitChars']) {
 1529             $displayed_type  = '<abbr title="' . htmlspecialchars($printtype) . '">';
 1530             $displayed_type .= htmlspecialchars(
 1531                 mb_substr(
 1532                     $printtype,
 1533                     0,
 1534                     (int) $GLOBALS['cfg']['LimitChars']
 1535                 ) . '...'
 1536             );
 1537             $displayed_type .= '</abbr>';
 1538         }
 1539 
 1540         return [
 1541             'type' => $type,
 1542             'spec_in_brackets' => $spec_in_brackets,
 1543             'enum_set_values'  => $enum_set_values,
 1544             'print_type' => $printtype,
 1545             'binary' => $binary,
 1546             'unsigned' => $unsigned,
 1547             'zerofill' => $zerofill,
 1548             'attribute' => $attribute,
 1549             'can_contain_collation' => $can_contain_collation,
 1550             'displayed_type' => $displayed_type,
 1551         ];
 1552     }
 1553 
 1554     /**
 1555      * Verifies if this table's engine supports foreign keys
 1556      *
 1557      * @param string $engine engine
 1558      */
 1559     public static function isForeignKeySupported($engine): bool
 1560     {
 1561         global $dbi;
 1562 
 1563         $engine = strtoupper((string) $engine);
 1564         if (($engine === 'INNODB') || ($engine === 'PBXT')) {
 1565             return true;
 1566         }
 1567 
 1568         if ($engine === 'NDBCLUSTER' || $engine === 'NDB') {
 1569             $ndbver = strtolower(
 1570                 $dbi->fetchValue('SELECT @@ndb_version_string')
 1571             );
 1572             if (substr($ndbver, 0, 4) === 'ndb-') {
 1573                 $ndbver = substr($ndbver, 4);
 1574             }
 1575 
 1576             return version_compare($ndbver, '7.3', '>=');
 1577         }
 1578 
 1579         return false;
 1580     }
 1581 
 1582     /**
 1583      * Is Foreign key check enabled?
 1584      */
 1585     public static function isForeignKeyCheck(): bool
 1586     {
 1587         global $dbi;
 1588 
 1589         if ($GLOBALS['cfg']['DefaultForeignKeyChecks'] === 'enable') {
 1590             return true;
 1591         }
 1592 
 1593         if ($GLOBALS['cfg']['DefaultForeignKeyChecks'] === 'disable') {
 1594             return false;
 1595         }
 1596 
 1597         return $dbi->getVariable('FOREIGN_KEY_CHECKS') === 'ON';
 1598     }
 1599 
 1600     /**
 1601      * Handle foreign key check request
 1602      *
 1603      * @return bool Default foreign key checks value
 1604      */
 1605     public static function handleDisableFKCheckInit(): bool
 1606     {
 1607         /** @var DatabaseInterface $dbi */
 1608         global $dbi;
 1609 
 1610         $default_fk_check_value = $dbi->getVariable('FOREIGN_KEY_CHECKS') === 'ON';
 1611         if (isset($_REQUEST['fk_checks'])) {
 1612             if (empty($_REQUEST['fk_checks'])) {
 1613                 // Disable foreign key checks
 1614                 $dbi->setVariable('FOREIGN_KEY_CHECKS', 'OFF');
 1615             } else {
 1616                 // Enable foreign key checks
 1617                 $dbi->setVariable('FOREIGN_KEY_CHECKS', 'ON');
 1618             }
 1619         }
 1620 
 1621         return $default_fk_check_value;
 1622     }
 1623 
 1624     /**
 1625      * Cleanup changes done for foreign key check
 1626      *
 1627      * @param bool $default_fk_check_value original value for 'FOREIGN_KEY_CHECKS'
 1628      */
 1629     public static function handleDisableFKCheckCleanup(bool $default_fk_check_value): void
 1630     {
 1631         /** @var DatabaseInterface $dbi */
 1632         global $dbi;
 1633 
 1634         $dbi->setVariable(
 1635             'FOREIGN_KEY_CHECKS',
 1636             $default_fk_check_value ? 'ON' : 'OFF'
 1637         );
 1638     }
 1639 
 1640     /**
 1641      * Converts GIS data to Well Known Text format
 1642      *
 1643      * @param string $data        GIS data
 1644      * @param bool   $includeSRID Add SRID to the WKT
 1645      *
 1646      * @return string GIS data in Well Know Text format
 1647      */
 1648     public static function asWKT($data, $includeSRID = false)
 1649     {
 1650         global $dbi;
 1651 
 1652         // Convert to WKT format
 1653         $hex = bin2hex($data);
 1654         $spatialAsText = 'ASTEXT';
 1655         $spatialSrid = 'SRID';
 1656         $axisOrder = '';
 1657         $mysqlVersionInt = $dbi->getVersion();
 1658         if ($mysqlVersionInt >= 50600) {
 1659             $spatialAsText = 'ST_ASTEXT';
 1660             $spatialSrid = 'ST_SRID';
 1661         }
 1662 
 1663         if ($mysqlVersionInt >= 80010 && ! $dbi->isMariaDb()) {
 1664             $axisOrder = ', \'axis-order=long-lat\'';
 1665         }
 1666 
 1667         $wktsql     = 'SELECT ' . $spatialAsText . "(x'" . $hex . "'" . $axisOrder . ')';
 1668         if ($includeSRID) {
 1669             $wktsql .= ', ' . $spatialSrid . "(x'" . $hex . "')";
 1670         }
 1671 
 1672         $wktresult  = $dbi->tryQuery(
 1673             $wktsql
 1674         );
 1675         $wktarr     = $dbi->fetchRow($wktresult, 0);
 1676         $wktval     = $wktarr[0] ?? null;
 1677 
 1678         if ($includeSRID) {
 1679             $srid = $wktarr[1] ?? null;
 1680             $wktval = "'" . $wktval . "'," . $srid;
 1681         }
 1682         @$dbi->freeResult($wktresult);
 1683 
 1684         return $wktval;
 1685     }
 1686 
 1687     /**
 1688      * If the string starts with a \r\n pair (0x0d0a) add an extra \n
 1689      *
 1690      * @param string $string string
 1691      *
 1692      * @return string with the chars replaced
 1693      */
 1694     public static function duplicateFirstNewline(string $string): string
 1695     {
 1696         $first_occurence = mb_strpos($string, "\r\n");
 1697         if ($first_occurence === 0) {
 1698             $string = "\n" . $string;
 1699         }
 1700 
 1701         return $string;
 1702     }
 1703 
 1704     /**
 1705      * Get the action word corresponding to a script name
 1706      * in order to display it as a title in navigation panel
 1707      *
 1708      * @param string $target a valid value for $cfg['NavigationTreeDefaultTabTable'],
 1709      *                       $cfg['NavigationTreeDefaultTabTable2'],
 1710      *                       $cfg['DefaultTabTable'] or $cfg['DefaultTabDatabase']
 1711      *
 1712      * @return string|bool Title for the $cfg value
 1713      */
 1714     public static function getTitleForTarget($target)
 1715     {
 1716         $mapping = [
 1717             'structure' =>  __('Structure'),
 1718             'sql' => __('SQL'),
 1719             'search' => __('Search'),
 1720             'insert' => __('Insert'),
 1721             'browse' => __('Browse'),
 1722             'operations' => __('Operations'),
 1723         ];
 1724 
 1725         return $mapping[$target] ?? false;
 1726     }
 1727 
 1728     /**
 1729      * Get the script name corresponding to a plain English config word
 1730      * in order to append in links on navigation and main panel
 1731      *
 1732      * @param string $target   a valid value for
 1733      *                         $cfg['NavigationTreeDefaultTabTable'],
 1734      *                         $cfg['NavigationTreeDefaultTabTable2'],
 1735      *                         $cfg['DefaultTabTable'], $cfg['DefaultTabDatabase'] or
 1736      *                         $cfg['DefaultTabServer']
 1737      * @param string $location one out of 'server', 'table', 'database'
 1738      *
 1739      * @return string script name corresponding to the config word
 1740      */
 1741     public static function getScriptNameForOption($target, string $location): string
 1742     {
 1743         $url = self::getUrlForOption($target, $location);
 1744         if ($url === null) {
 1745             return '/';
 1746         }
 1747 
 1748         return Url::getFromRoute($url);
 1749     }
 1750 
 1751     /**
 1752      * Get the URL corresponding to a plain English config word
 1753      * in order to append in links on navigation and main panel
 1754      *
 1755      * @param string $target   a valid value for
 1756      *                         $cfg['NavigationTreeDefaultTabTable'],
 1757      *                         $cfg['NavigationTreeDefaultTabTable2'],
 1758      *                         $cfg['DefaultTabTable'], $cfg['DefaultTabDatabase'] or
 1759      *                         $cfg['DefaultTabServer']
 1760      * @param string $location one out of 'server', 'table', 'database'
 1761      *
 1762      * @return string The URL corresponding to the config word or null if nothing was found
 1763      */
 1764     public static function getUrlForOption($target, string $location): ?string
 1765     {
 1766         if ($location === 'server') {
 1767             // Values for $cfg['DefaultTabServer']
 1768             switch ($target) {
 1769                 case 'welcome':
 1770                     return '/';
 1771                 case 'databases':
 1772                     return '/server/databases';
 1773                 case 'status':
 1774                     return '/server/status';
 1775                 case 'variables':
 1776                     return '/server/variables';
 1777                 case 'privileges':
 1778                     return '/server/privileges';
 1779             }
 1780         } elseif ($location === 'database') {
 1781             // Values for $cfg['DefaultTabDatabase']
 1782             switch ($target) {
 1783                 case 'structure':
 1784                     return '/database/structure';
 1785                 case 'sql':
 1786                     return '/database/sql';
 1787                 case 'search':
 1788                     return '/database/search';
 1789                 case 'operations':
 1790                     return '/database/operations';
 1791             }
 1792         } elseif ($location === 'table') {
 1793             // Values for $cfg['DefaultTabTable'],
 1794             // $cfg['NavigationTreeDefaultTabTable'] and
 1795             // $cfg['NavigationTreeDefaultTabTable2']
 1796             switch ($target) {
 1797                 case 'structure':
 1798                     return '/table/structure';
 1799                 case 'sql':
 1800                     return '/table/sql';
 1801                 case 'search':
 1802                     return '/table/search';
 1803                 case 'insert':
 1804                     return '/table/change';
 1805                 case 'browse':
 1806                     return '/sql';
 1807             }
 1808         }
 1809 
 1810         return null;
 1811     }
 1812 
 1813     /**
 1814      * Formats user string, expanding @VARIABLES@, accepting strftime format
 1815      * string.
 1816      *
 1817      * @param string       $string  Text where to do expansion.
 1818      * @param array|string $escape  Function to call for escaping variable values.
 1819      *                              Can also be an array of:
 1820      *                              - the escape method name
 1821      *                              - the class that contains the method
 1822      *                              - location of the class (for inclusion)
 1823      * @param array        $updates Array with overrides for default parameters
 1824      *                              (obtained from GLOBALS).
 1825      *
 1826      * @return string
 1827      */
 1828     public static function expandUserString(
 1829         $string,
 1830         $escape = null,
 1831         array $updates = []
 1832     ) {
 1833         global $dbi;
 1834 
 1835         /* Content */
 1836         $vars = [];
 1837         $vars['http_host'] = Core::getenv('HTTP_HOST');
 1838         $vars['server_name'] = $GLOBALS['cfg']['Server']['host'];
 1839         $vars['server_verbose'] = $GLOBALS['cfg']['Server']['verbose'];
 1840 
 1841         if (empty($GLOBALS['cfg']['Server']['verbose'])) {
 1842             $vars['server_verbose_or_name'] = $GLOBALS['cfg']['Server']['host'];
 1843         } else {
 1844             $vars['server_verbose_or_name'] = $GLOBALS['cfg']['Server']['verbose'];
 1845         }
 1846 
 1847         $vars['database'] = $GLOBALS['db'];
 1848         $vars['table'] = $GLOBALS['table'];
 1849         $vars['phpmyadmin_version'] = 'phpMyAdmin ' . PMA_VERSION;
 1850 
 1851         /* Update forced variables */
 1852         foreach ($updates as $key => $val) {
 1853             $vars[$key] = $val;
 1854         }
 1855 
 1856         /* Replacement mapping */
 1857         /*
 1858          * The __VAR__ ones are for backward compatibility, because user
 1859          * might still have it in cookies.
 1860          */
 1861         $replace = [
 1862             '@HTTP_HOST@' => $vars['http_host'],
 1863             '@SERVER@' => $vars['server_name'],
 1864             '__SERVER__' => $vars['server_name'],
 1865             '@VERBOSE@' => $vars['server_verbose'],
 1866             '@VSERVER@' => $vars['server_verbose_or_name'],
 1867             '@DATABASE@' => $vars['database'],
 1868             '__DB__' => $vars['database'],
 1869             '@TABLE@' => $vars['table'],
 1870             '__TABLE__' => $vars['table'],
 1871             '@PHPMYADMIN@' => $vars['phpmyadmin_version'],
 1872         ];
 1873 
 1874         /* Optional escaping */
 1875         if ($escape !== null) {
 1876             if (is_array($escape)) {
 1877                 $escape_class = new $escape[1]();
 1878                 $escape_method = $escape[0];
 1879             }
 1880             foreach ($replace as $key => $val) {
 1881                 if (isset($escape_class, $escape_method)) {
 1882                     $replace[$key] = $escape_class->$escape_method($val);
 1883                 } elseif ($escape === 'backquote') {
 1884                     $replace[$key] = self::backquote($val);
 1885                 } elseif (is_callable($escape)) {
 1886                     $replace[$key] = $escape($val);
 1887                 }
 1888             }
 1889         }
 1890 
 1891         /* Backward compatibility in 3.5.x */
 1892         if (mb_strpos($string, '@FIELDS@') !== false) {
 1893             $string = strtr($string, ['@FIELDS@' => '@COLUMNS@']);
 1894         }
 1895 
 1896         /* Fetch columns list if required */
 1897         if (mb_strpos($string, '@COLUMNS@') !== false) {
 1898             $columns_list = $dbi->getColumns(
 1899                 $GLOBALS['db'],
 1900                 $GLOBALS['table']
 1901             );
 1902 
 1903             // sometimes the table no longer exists at this point
 1904             if ($columns_list !== null) {
 1905                 $column_names = [];
 1906                 foreach ($columns_list as $column) {
 1907                     if ($escape !== null) {
 1908                         $column_names[] = self::$escape($column['Field']);
 1909                     } else {
 1910                         $column_names[] = $column['Field'];
 1911                     }
 1912                 }
 1913                 $replace['@COLUMNS@'] = implode(',', $column_names);
 1914             } else {
 1915                 $replace['@COLUMNS@'] = '*';
 1916             }
 1917         }
 1918 
 1919         /* Do the replacement */
 1920         return strtr((string) strftime($string), $replace);
 1921     }
 1922 
 1923     /**
 1924      * This function processes the datatypes supported by the DB,
 1925      * as specified in Types->getColumns() and either returns an array
 1926      * (useful for quickly checking if a datatype is supported)
 1927      * or an HTML snippet that creates a drop-down list.
 1928      *
 1929      * @param bool   $html     Whether to generate an html snippet or an array
 1930      * @param string $selected The value to mark as selected in HTML mode
 1931      *
 1932      * @return mixed   An HTML snippet or an array of datatypes.
 1933      */
 1934     public static function getSupportedDatatypes($html = false, $selected = '')
 1935     {
 1936         global $dbi;
 1937 
 1938         if ($html) {
 1939             $retval = Generator::getSupportedDatatypes($selected);
 1940         } else {
 1941             $retval = [];
 1942             foreach ($dbi->types->getColumns() as $value) {
 1943                 if (is_array($value)) {
 1944                     foreach ($value as $subvalue) {
 1945                         if ($subvalue === '-') {
 1946                             continue;
 1947                         }
 1948 
 1949                         $retval[] = $subvalue;
 1950                     }
 1951                 } else {
 1952                     if ($value !== '-') {
 1953                         $retval[] = $value;
 1954                     }
 1955                 }
 1956             }
 1957         }
 1958 
 1959         return $retval;
 1960     }
 1961 
 1962     /**
 1963      * Returns a list of datatypes that are not (yet) handled by PMA.
 1964      * Used by: /table/change and libraries/Routines.php
 1965      *
 1966      * @return array list of datatypes
 1967      */
 1968     public static function unsupportedDatatypes(): array
 1969     {
 1970         return [];
 1971     }
 1972 
 1973     /**
 1974      * Return GIS data types
 1975      *
 1976      * @param bool $upper_case whether to return values in upper case
 1977      *
 1978      * @return string[] GIS data types
 1979      */
 1980     public static function getGISDatatypes($upper_case = false): array
 1981     {
 1982         $gis_data_types = [
 1983             'geometry',
 1984             'point',
 1985             'linestring',
 1986             'polygon',
 1987             'multipoint',
 1988             'multilinestring',
 1989             'multipolygon',
 1990             'geometrycollection',
 1991         ];
 1992         if ($upper_case) {
 1993             $gis_data_types = array_map('mb_strtoupper', $gis_data_types);
 1994         }
 1995 
 1996         return $gis_data_types;
 1997     }
 1998 
 1999     /**
 2000      * Generates GIS data based on the string passed.
 2001      *
 2002      * @param string $gis_string   GIS string
 2003      * @param int    $mysqlVersion The mysql version as int
 2004      *
 2005      * @return string GIS data enclosed in 'ST_GeomFromText' or 'GeomFromText' function
 2006      */
 2007     public static function createGISData($gis_string, $mysqlVersion)
 2008     {
 2009         $geomFromText = $mysqlVersion >= 50600 ? 'ST_GeomFromText' : 'GeomFromText';
 2010         $gis_string = trim($gis_string);
 2011         $geom_types = '(POINT|MULTIPOINT|LINESTRING|MULTILINESTRING|'
 2012             . 'POLYGON|MULTIPOLYGON|GEOMETRYCOLLECTION)';
 2013         if (preg_match("/^'" . $geom_types . "\(.*\)',[0-9]*$/i", $gis_string)) {
 2014             return $geomFromText . '(' . $gis_string . ')';
 2015         }
 2016 
 2017         if (preg_match('/^' . $geom_types . '\(.*\)$/i', $gis_string)) {
 2018             return $geomFromText . "('" . $gis_string . "')";
 2019         }
 2020 
 2021         return $gis_string;
 2022     }
 2023 
 2024     /**
 2025      * Returns the names and details of the functions
 2026      * that can be applied on geometry data types.
 2027      *
 2028      * @param string $geom_type if provided the output is limited to the functions
 2029      *                          that are applicable to the provided geometry type.
 2030      * @param bool   $binary    if set to false functions that take two geometries
 2031      *                          as arguments will not be included.
 2032      * @param bool   $display   if set to true separators will be added to the
 2033      *                          output array.
 2034      *
 2035      * @return array<int|string,array<string,int|string>> names and details of the functions that can be applied on
 2036      *                                                    geometry data types.
 2037      */
 2038     public static function getGISFunctions(
 2039         $geom_type = null,
 2040         $binary = true,
 2041         $display = false
 2042     ): array {
 2043         global $dbi;
 2044 
 2045         $funcs = [];
 2046         if ($display) {
 2047             $funcs[] = ['display' => ' '];
 2048         }
 2049 
 2050         // Unary functions common to all geometry types
 2051         $funcs['Dimension']    = [
 2052             'params' => 1,
 2053             'type' => 'int',
 2054         ];
 2055         $funcs['Envelope']     = [
 2056             'params' => 1,
 2057             'type' => 'Polygon',
 2058         ];
 2059         $funcs['GeometryType'] = [
 2060             'params' => 1,
 2061             'type' => 'text',
 2062         ];
 2063         $funcs['SRID']         = [
 2064             'params' => 1,
 2065             'type' => 'int',
 2066         ];
 2067         $funcs['IsEmpty']      = [
 2068             'params' => 1,
 2069             'type' => 'int',
 2070         ];
 2071         $funcs['IsSimple']     = [
 2072             'params' => 1,
 2073             'type' => 'int',
 2074         ];
 2075 
 2076         $geom_type = mb_strtolower(trim((string) $geom_type));
 2077         if ($display && $geom_type !== 'geometry' && $geom_type !== 'multipoint') {
 2078             $funcs[] = ['display' => '--------'];
 2079         }
 2080 
 2081         // Unary functions that are specific to each geometry type
 2082         if ($geom_type === 'point') {
 2083             $funcs['X'] = [
 2084                 'params' => 1,
 2085                 'type' => 'float',
 2086             ];
 2087             $funcs['Y'] = [
 2088                 'params' => 1,
 2089                 'type' => 'float',
 2090             ];
 2091         } elseif ($geom_type === 'linestring') {
 2092             $funcs['EndPoint']   = [
 2093                 'params' => 1,
 2094                 'type' => 'point',
 2095             ];
 2096             $funcs['GLength']    = [
 2097                 'params' => 1,
 2098                 'type' => 'float',
 2099             ];
 2100             $funcs['NumPoints']  = [
 2101                 'params' => 1,
 2102                 'type' => 'int',
 2103             ];
 2104             $funcs['StartPoint'] = [
 2105                 'params' => 1,
 2106                 'type' => 'point',
 2107             ];
 2108             $funcs['IsRing']     = [
 2109                 'params' => 1,
 2110                 'type' => 'int',
 2111             ];
 2112         } elseif ($geom_type === 'multilinestring') {
 2113             $funcs['GLength']  = [
 2114                 'params' => 1,
 2115                 'type' => 'float',
 2116             ];
 2117             $funcs['IsClosed'] = [
 2118                 'params' => 1,
 2119                 'type' => 'int',
 2120             ];
 2121         } elseif ($geom_type === 'polygon') {
 2122             $funcs['Area']         = [
 2123                 'params' => 1,
 2124                 'type' => 'float',
 2125             ];
 2126             $funcs['ExteriorRing'] = [
 2127                 'params' => 1,
 2128                 'type' => 'linestring',
 2129             ];
 2130             $funcs['NumInteriorRings'] = [
 2131                 'params' => 1,
 2132                 'type' => 'int',
 2133             ];
 2134         } elseif ($geom_type === 'multipolygon') {
 2135             $funcs['Area']     = [
 2136                 'params' => 1,
 2137                 'type' => 'float',
 2138             ];
 2139             $funcs['Centroid'] = [
 2140                 'params' => 1,
 2141                 'type' => 'point',
 2142             ];
 2143             // Not yet implemented in MySQL
 2144             //$funcs['PointOnSurface'] = array('params' => 1, 'type' => 'point');
 2145         } elseif ($geom_type === 'geometrycollection') {
 2146             $funcs['NumGeometries'] = [
 2147                 'params' => 1,
 2148                 'type' => 'int',
 2149             ];
 2150         }
 2151 
 2152         // If we are asked for binary functions as well
 2153         if ($binary) {
 2154             // section separator
 2155             if ($display) {
 2156                 $funcs[] = ['display' => '--------'];
 2157             }
 2158 
 2159             $spatialPrefix = '';
 2160             if ($dbi->getVersion() >= 50601) {
 2161                 // If MySQL version is greater than or equal 5.6.1,
 2162                 // use the ST_ prefix.
 2163                 $spatialPrefix = 'ST_';
 2164             }
 2165             $funcs[$spatialPrefix . 'Crosses']    = [
 2166                 'params' => 2,
 2167                 'type' => 'int',
 2168             ];
 2169             $funcs[$spatialPrefix . 'Contains']   = [
 2170                 'params' => 2,
 2171                 'type' => 'int',
 2172             ];
 2173             $funcs[$spatialPrefix . 'Disjoint']   = [
 2174                 'params' => 2,
 2175                 'type' => 'int',
 2176             ];
 2177             $funcs[$spatialPrefix . 'Equals']     = [
 2178                 'params' => 2,
 2179                 'type' => 'int',
 2180             ];
 2181             $funcs[$spatialPrefix . 'Intersects'] = [
 2182                 'params' => 2,
 2183                 'type' => 'int',
 2184             ];
 2185             $funcs[$spatialPrefix . 'Overlaps']   = [
 2186                 'params' => 2,
 2187                 'type' => 'int',
 2188             ];
 2189             $funcs[$spatialPrefix . 'Touches']    = [
 2190                 'params' => 2,
 2191                 'type' => 'int',
 2192             ];
 2193             $funcs[$spatialPrefix . 'Within']     = [
 2194                 'params' => 2,
 2195                 'type' => 'int',
 2196             ];
 2197 
 2198             if ($display) {
 2199                 $funcs[] = ['display' => '--------'];
 2200             }
 2201             // Minimum bounding rectangle functions
 2202             $funcs['MBRContains']   = [
 2203                 'params' => 2,
 2204                 'type' => 'int',
 2205             ];
 2206             $funcs['MBRDisjoint']   = [
 2207                 'params' => 2,
 2208                 'type' => 'int',
 2209             ];
 2210             $funcs['MBREquals']     = [
 2211                 'params' => 2,
 2212                 'type' => 'int',
 2213             ];
 2214             $funcs['MBRIntersects'] = [
 2215                 'params' => 2,
 2216                 'type' => 'int',
 2217             ];
 2218             $funcs['MBROverlaps']   = [
 2219                 'params' => 2,
 2220                 'type' => 'int',
 2221             ];
 2222             $funcs['MBRTouches']    = [
 2223                 'params' => 2,
 2224                 'type' => 'int',
 2225             ];
 2226             $funcs['MBRWithin']     = [
 2227                 'params' => 2,
 2228                 'type' => 'int',
 2229             ];
 2230         }
 2231 
 2232         return $funcs;
 2233     }
 2234 
 2235     /**
 2236      * Checks if the current user has a specific privilege and returns true if the
 2237      * user indeed has that privilege or false if they don't. This function must
 2238      * only be used for features that are available since MySQL 5, because it
 2239      * relies on the INFORMATION_SCHEMA database to be present.
 2240      *
 2241      * Example:   currentUserHasPrivilege('CREATE ROUTINE', 'mydb');
 2242      *            // Checks if the currently logged in user has the global
 2243      *            // 'CREATE ROUTINE' privilege or, if not, checks if the
 2244      *            // user has this privilege on database 'mydb'.
 2245      *
 2246      * @param string      $priv The privilege to check
 2247      * @param string|null $db   null, to only check global privileges
 2248      *                          string, db name where to also check
 2249      *                          for privileges
 2250      * @param string|null $tbl  null, to only check global/db privileges
 2251      *                          string, table name where to also check
 2252      *                          for privileges
 2253      */
 2254     public static function currentUserHasPrivilege(string $priv, ?string $db = null, ?string $tbl = null): bool
 2255     {
 2256         global $dbi;
 2257 
 2258         // Get the username for the current user in the format
 2259         // required to use in the information schema database.
 2260         [$user, $host] = $dbi->getCurrentUserAndHost();
 2261 
 2262         // MySQL is started with --skip-grant-tables
 2263         if ($user === '') {
 2264             return true;
 2265         }
 2266 
 2267         $username  = "''";
 2268         $username .= str_replace("'", "''", $user);
 2269         $username .= "''@''";
 2270         $username .= str_replace("'", "''", $host);
 2271         $username .= "''";
 2272 
 2273         // Prepare the query
 2274         $query = 'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`%s` '
 2275                . "WHERE GRANTEE='%s' AND PRIVILEGE_TYPE='%s'";
 2276 
 2277         // Check global privileges first.
 2278         $user_privileges = $dbi->fetchValue(
 2279             sprintf(
 2280                 $query,
 2281                 'USER_PRIVILEGES',
 2282                 $username,
 2283                 $priv
 2284             )
 2285         );
 2286         if ($user_privileges) {
 2287             return true;
 2288         }
 2289         // If a database name was provided and user does not have the
 2290         // required global privilege, try database-wise permissions.
 2291         if ($db === null) {
 2292             // There was no database name provided and the user
 2293             // does not have the correct global privilege.
 2294             return false;
 2295         }
 2296 
 2297         $query .= " AND '%s' LIKE `TABLE_SCHEMA`";
 2298         $schema_privileges = $dbi->fetchValue(
 2299             sprintf(
 2300                 $query,
 2301                 'SCHEMA_PRIVILEGES',
 2302                 $username,
 2303                 $priv,
 2304                 $dbi->escapeString($db)
 2305             )
 2306         );
 2307         if ($schema_privileges) {
 2308             return true;
 2309         }
 2310         // If a table name was also provided and we still didn't
 2311         // find any valid privileges, try table-wise privileges.
 2312         if ($tbl !== null) {
 2313             // need to escape wildcards in db and table names, see bug #3518484
 2314             $tbl = str_replace(['%', '_'], ['\%', '\_'], $tbl);
 2315             $query .= " AND TABLE_NAME='%s'";
 2316             $table_privileges = $dbi->fetchValue(
 2317                 sprintf(
 2318                     $query,
 2319                     'TABLE_PRIVILEGES',
 2320                     $username,
 2321                     $priv,
 2322                     $dbi->escapeString($db),
 2323                     $dbi->escapeString($tbl)
 2324                 )
 2325             );
 2326             if ($table_privileges) {
 2327                 return true;
 2328             }
 2329         }
 2330 
 2331         /**
 2332          * If we reached this point, the user does not
 2333          * have even valid table-wise privileges.
 2334          */
 2335         return false;
 2336     }
 2337 
 2338     /**
 2339      * Returns server type for current connection
 2340      *
 2341      * Known types are: MariaDB, PerconaDB and MySQL (default)
 2342      *
 2343      * @return string
 2344      */
 2345     public static function getServerType()
 2346     {
 2347         global $dbi;
 2348 
 2349         if ($dbi->isMariaDB()) {
 2350             return 'MariaDB';
 2351         }
 2352 
 2353         if ($dbi->isPercona()) {
 2354             return 'Percona Server';
 2355         }
 2356 
 2357         return 'MySQL';
 2358     }
 2359 
 2360     /**
 2361      * Parses ENUM/SET values
 2362      *
 2363      * @param string $definition The definition of the column
 2364      *                           for which to parse the values
 2365      * @param bool   $escapeHtml Whether to escape html entities
 2366      *
 2367      * @return array
 2368      */
 2369     public static function parseEnumSetValues($definition, $escapeHtml = true)
 2370     {
 2371         $values_string = htmlentities($definition, ENT_COMPAT, 'UTF-8');
 2372         // There is a JS port of the below parser in functions.js
 2373         // If you are fixing something here,
 2374         // you need to also update the JS port.
 2375         $values = [];
 2376         $in_string = false;
 2377         $buffer = '';
 2378 
 2379         for ($i = 0, $length = mb_strlen($values_string); $i < $length; $i++) {
 2380             $curr = mb_substr($values_string, $i, 1);
 2381             $next = $i == mb_strlen($values_string) - 1
 2382                 ? ''
 2383                 : mb_substr($values_string, $i + 1, 1);
 2384 
 2385             if (! $in_string && $curr == "'") {
 2386                 $in_string = true;
 2387             } elseif (($in_string && $curr === '\\') && $next === '\\') {
 2388                 $buffer .= '&#92;';
 2389                 $i++;
 2390             } elseif (($in_string && $next == "'")
 2391                 && ($curr == "'" || $curr === '\\')
 2392             ) {
 2393                 $buffer .= '&#39;';
 2394                 $i++;
 2395             } elseif ($in_string && $curr == "'") {
 2396                 $in_string = false;
 2397                 $values[] = $buffer;
 2398                 $buffer = '';
 2399             } elseif ($in_string) {
 2400                  $buffer .= $curr;
 2401             }
 2402         }
 2403 
 2404         if (strlen($buffer) > 0) {
 2405             // The leftovers in the buffer are the last value (if any)
 2406             $values[] = $buffer;
 2407         }
 2408 
 2409         if (! $escapeHtml) {
 2410             foreach ($values as $key => $value) {
 2411                 $values[$key] = html_entity_decode($value, ENT_QUOTES, 'UTF-8');
 2412             }
 2413         }
 2414 
 2415         return $values;
 2416     }
 2417 
 2418     /**
 2419      * Get regular expression which occur first inside the given sql query.
 2420      *
 2421      * @param array  $regex_array Comparing regular expressions.
 2422      * @param string $query       SQL query to be checked.
 2423      *
 2424      * @return string Matching regular expression.
 2425      */
 2426     public static function getFirstOccurringRegularExpression(array $regex_array, $query): string
 2427     {
 2428         $minimum_first_occurence_index = null;
 2429         $regex = null;
 2430 
 2431         foreach ($regex_array as $test_regex) {
 2432             if (! preg_match($test_regex, $query, $matches, PREG_OFFSET_CAPTURE)) {
 2433                 continue;
 2434             }
 2435 
 2436             if ($minimum_first_occurence_index !== null
 2437                 && ($matches[0][1] >= $minimum_first_occurence_index)
 2438             ) {
 2439                 continue;
 2440             }
 2441 
 2442             $regex = $test_regex;
 2443             $minimum_first_occurence_index = $matches[0][1];
 2444         }
 2445 
 2446         return $regex;
 2447     }
 2448 
 2449     /**
 2450      * Return the list of tabs for the menu with corresponding names
 2451      *
 2452      * @param string $level 'server', 'db' or 'table' level
 2453      *
 2454      * @return array|null list of tabs for the menu
 2455      */
 2456     public static function getMenuTabList($level = null)
 2457     {
 2458         $tabList = [
 2459             'server' => [
 2460                 'databases'   => __('Databases'),
 2461                 'sql'         => __('SQL'),
 2462                 'status'      => __('Status'),
 2463                 'rights'      => __('Users'),
 2464                 'export'      => __('Export'),
 2465                 'import'      => __('Import'),
 2466                 'settings'    => __('Settings'),
 2467                 'binlog'      => __('Binary log'),
 2468                 'replication' => __('Replication'),
 2469                 'vars'        => __('Variables'),
 2470                 'charset'     => __('Charsets'),
 2471                 'plugins'     => __('Plugins'),
 2472                 'engine'      => __('Engines'),
 2473             ],
 2474             'db'     => [
 2475                 'structure'   => __('Structure'),
 2476                 'sql'         => __('SQL'),
 2477                 'search'      => __('Search'),
 2478                 'query'       => __('Query'),
 2479                 'export'      => __('Export'),
 2480                 'import'      => __('Import'),
 2481                 'operation'   => __('Operations'),
 2482                 'privileges'  => __('Privileges'),
 2483                 'routines'    => __('Routines'),
 2484                 'events'      => __('Events'),
 2485                 'triggers'    => __('Triggers'),
 2486                 'tracking'    => __('Tracking'),
 2487                 'designer'    => __('Designer'),
 2488                 'central_columns' => __('Central columns'),
 2489             ],
 2490             'table'  => [
 2491                 'browse'      => __('Browse'),
 2492                 'structure'   => __('Structure'),
 2493                 'sql'         => __('SQL'),
 2494                 'search'      => __('Search'),
 2495                 'insert'      => __('Insert'),
 2496                 'export'      => __('Export'),
 2497                 'import'      => __('Import'),
 2498                 'privileges'  => __('Privileges'),
 2499                 'operation'   => __('Operations'),
 2500                 'tracking'    => __('Tracking'),
 2501                 'triggers'    => __('Triggers'),
 2502             ],
 2503         ];
 2504 
 2505         if ($level == null) {
 2506             return $tabList;
 2507         }
 2508 
 2509         if (array_key_exists($level, $tabList)) {
 2510             return $tabList[$level];
 2511         }
 2512 
 2513         return null;
 2514     }
 2515 
 2516     /**
 2517      * Add fractional seconds to time, datetime and timestamp strings.
 2518      * If the string contains fractional seconds,
 2519      * pads it with 0s up to 6 decimal places.
 2520      *
 2521      * @param string $value time, datetime or timestamp strings
 2522      *
 2523      * @return string time, datetime or timestamp strings with fractional seconds
 2524      */
 2525     public static function addMicroseconds($value)
 2526     {
 2527         if (empty($value) || $value === 'CURRENT_TIMESTAMP'
 2528             || $value === 'current_timestamp()'
 2529         ) {
 2530             return $value;
 2531         }
 2532 
 2533         if (mb_strpos($value, '.') === false) {
 2534             return $value . '.000000';
 2535         }
 2536 
 2537         $value .= '000000';
 2538 
 2539         return mb_substr(
 2540             $value,
 2541             0,
 2542             mb_strpos($value, '.') + 7
 2543         );
 2544     }
 2545 
 2546     /**
 2547      * Reads the file, detects the compression MIME type, closes the file
 2548      * and returns the MIME type
 2549      *
 2550      * @param resource $file the file handle
 2551      *
 2552      * @return string the MIME type for compression, or 'none'
 2553      */
 2554     public static function getCompressionMimeType($file)
 2555     {
 2556         $test = fread($file, 4);
 2557 
 2558         if ($test === false) {
 2559             fclose($file);
 2560 
 2561             return 'none';
 2562         }
 2563 
 2564         $len = strlen($test);
 2565         fclose($file);
 2566         if ($len >= 2 && $test[0] == chr(31) && $test[1] == chr(139)) {
 2567             return 'application/gzip';
 2568         }
 2569         if ($len >= 3 && substr($test, 0, 3) === 'BZh') {
 2570             return 'application/bzip2';
 2571         }
 2572         if ($len >= 4 && $test == "PK\003\004") {
 2573             return 'application/zip';
 2574         }
 2575 
 2576         return 'none';
 2577     }
 2578 
 2579     /**
 2580      * Provide COLLATE clause, if required, to perform case sensitive comparisons
 2581      * for queries on information_schema.
 2582      *
 2583      * @return string COLLATE clause if needed or empty string.
 2584      */
 2585     public static function getCollateForIS()
 2586     {
 2587         global $dbi;
 2588 
 2589         $names = $dbi->getLowerCaseNames();
 2590         if ($names === '0') {
 2591             return 'COLLATE utf8_bin';
 2592         }
 2593 
 2594         if ($names === '2') {
 2595             return 'COLLATE utf8_general_ci';
 2596         }
 2597 
 2598         return '';
 2599     }
 2600 
 2601     /**
 2602      * Process the index data.
 2603      *
 2604      * @param array $indexes index data
 2605      *
 2606      * @return array processes index data
 2607      */
 2608     public static function processIndexData(array $indexes)
 2609     {
 2610         $lastIndex    = '';
 2611 
 2612         $primary      = '';
 2613         $pk_array     = []; // will be use to emphasis prim. keys in the table
 2614         $indexes_info = [];
 2615         $indexes_data = [];
 2616 
 2617         // view
 2618         foreach ($indexes as $row) {
 2619             // Backups the list of primary keys
 2620             if ($row['Key_name'] === 'PRIMARY') {
 2621                 $primary   .= $row['Column_name'] . ', ';
 2622                 $pk_array[$row['Column_name']] = 1;
 2623             }
 2624             // Retains keys informations
 2625             if ($row['Key_name'] != $lastIndex) {
 2626                 $indexes[] = $row['Key_name'];
 2627                 $lastIndex = $row['Key_name'];
 2628             }
 2629             $indexes_info[$row['Key_name']]['Sequences'][] = $row['Seq_in_index'];
 2630             $indexes_info[$row['Key_name']]['Non_unique'] = $row['Non_unique'];
 2631             if (isset($row['Cardinality'])) {
 2632                 $indexes_info[$row['Key_name']]['Cardinality'] = $row['Cardinality'];
 2633             }
 2634             // I don't know what does following column mean....
 2635             // $indexes_info[$row['Key_name']]['Packed']          = $row['Packed'];
 2636 
 2637             $indexes_info[$row['Key_name']]['Comment'] = $row['Comment'];
 2638 
 2639             $indexes_data[$row['Key_name']][$row['Seq_in_index']]['Column_name']
 2640                 = $row['Column_name'];
 2641             if (! isset($row['Sub_part'])) {
 2642                 continue;
 2643             }
 2644 
 2645             $indexes_data[$row['Key_name']][$row['Seq_in_index']]['Sub_part']
 2646                 = $row['Sub_part'];
 2647         }
 2648 
 2649         return [
 2650             $primary,
 2651             $pk_array,
 2652             $indexes_info,
 2653             $indexes_data,
 2654         ];
 2655     }
 2656 
 2657     /**
 2658      * Returns whether the database server supports virtual columns
 2659      *
 2660      * @return bool
 2661      */
 2662     public static function isVirtualColumnsSupported()
 2663     {
 2664         global $dbi;
 2665 
 2666         $serverType = self::getServerType();
 2667         $serverVersion = $dbi->getVersion();
 2668 
 2669         return in_array($serverType, ['MySQL', 'Percona Server']) && $serverVersion >= 50705
 2670              || ($serverType === 'MariaDB' && $serverVersion >= 50200);
 2671     }
 2672 
 2673     /**
 2674      * Gets the list of tables in the current db and information about these
 2675      * tables if possible
 2676      *
 2677      * @param string      $db       database name
 2678      * @param string|null $sub_part part of script name
 2679      *
 2680      * @return array
 2681      */
 2682     public static function getDbInfo($db, ?string $sub_part)
 2683     {
 2684         global $cfg, $dbi;
 2685 
 2686         /**
 2687          * limits for table list
 2688          */
 2689         if (! isset($_SESSION['tmpval']['table_limit_offset'])
 2690             || $_SESSION['tmpval']['table_limit_offset_db'] != $db
 2691         ) {
 2692             $_SESSION['tmpval']['table_limit_offset'] = 0;
 2693             $_SESSION['tmpval']['table_limit_offset_db'] = $db;
 2694         }
 2695         if (isset($_REQUEST['pos'])) {
 2696             $_SESSION['tmpval']['table_limit_offset'] = (int) $_REQUEST['pos'];
 2697         }
 2698         $pos = $_SESSION['tmpval']['table_limit_offset'];
 2699 
 2700         /**
 2701          * whether to display extended stats
 2702          */
 2703         $isShowStats = $cfg['ShowStats'];
 2704 
 2705         /**
 2706          * whether selected db is information_schema
 2707          */
 2708         $isSystemSchema = false;
 2709 
 2710         if (Utilities::isSystemSchema($db)) {
 2711             $isShowStats = false;
 2712             $isSystemSchema = true;
 2713         }
 2714 
 2715         /**
 2716          * information about tables in db
 2717          */
 2718         $tables = [];
 2719 
 2720         $tooltip_truename = [];
 2721         $tooltip_aliasname = [];
 2722 
 2723         // Special speedup for newer MySQL Versions (in 4.0 format changed)
 2724         if ($cfg['SkipLockedTables'] === true) {
 2725             $db_info_result = $dbi->query(
 2726                 'SHOW OPEN TABLES FROM ' . self::backquote($db) . ' WHERE In_use > 0;'
 2727             );
 2728 
 2729             // Blending out tables in use
 2730             if ($db_info_result && $dbi->numRows($db_info_result) > 0) {
 2731                 $tables = self::getTablesWhenOpen($db, $db_info_result);
 2732             } elseif ($db_info_result) {
 2733                 $dbi->freeResult($db_info_result);
 2734             }
 2735         }
 2736 
 2737         if (empty($tables)) {
 2738             // Set some sorting defaults
 2739             $sort = 'Name';
 2740             $sort_order = 'ASC';
 2741 
 2742             if (isset($_REQUEST['sort'])) {
 2743                 $sortable_name_mappings = [
 2744                     'table'       => 'Name',
 2745                     'records'     => 'Rows',
 2746                     'type'        => 'Engine',
 2747                     'collation'   => 'Collation',
 2748                     'size'        => 'Data_length',
 2749                     'overhead'    => 'Data_free',
 2750                     'creation'    => 'Create_time',
 2751                     'last_update' => 'Update_time',
 2752                     'last_check'  => 'Check_time',
 2753                     'comment'     => 'Comment',
 2754                 ];
 2755 
 2756                 // Make sure the sort type is implemented
 2757                 if (isset($sortable_name_mappings[$_REQUEST['sort']])) {
 2758                     $sort = $sortable_name_mappings[$_REQUEST['sort']];
 2759                     if ($_REQUEST['sort_order'] === 'DESC') {
 2760                         $sort_order = 'DESC';
 2761                     }
 2762                 }
 2763             }
 2764 
 2765             $groupWithSeparator = false;
 2766             $tbl_type = null;
 2767             $limit_offset = 0;
 2768             $limit_count = false;
 2769             $groupTable = [];
 2770 
 2771             if (! empty($_REQUEST['tbl_group']) || ! empty($_REQUEST['tbl_type'])) {
 2772                 if (! empty($_REQUEST['tbl_type'])) {
 2773                     // only tables for selected type
 2774                     $tbl_type = $_REQUEST['tbl_type'];
 2775                 }
 2776                 if (! empty($_REQUEST['tbl_group'])) {
 2777                     // only tables for selected group
 2778                     $tbl_group = $_REQUEST['tbl_group'];
 2779                     // include the table with the exact name of the group if such
 2780                     // exists
 2781                     $groupTable = $dbi->getTablesFull(
 2782                         $db,
 2783                         $tbl_group,
 2784                         false,
 2785                         $limit_offset,
 2786                         $limit_count,
 2787                         $sort,
 2788                         $sort_order,
 2789                         $tbl_type
 2790                     );
 2791                     $groupWithSeparator = $tbl_group
 2792                         . $GLOBALS['cfg']['NavigationTreeTableSeparator'];
 2793                 }
 2794             } else {
 2795                 // all tables in db
 2796                 // - get the total number of tables
 2797                 //  (needed for proper working of the MaxTableList feature)
 2798                 $tables = $dbi->getTables($db);
 2799                 $total_num_tables = count($tables);
 2800                 if (! (isset($sub_part) && $sub_part === '_export')) {
 2801                     // fetch the details for a possible limited subset
 2802                     $limit_offset = $pos;
 2803                     $limit_count = true;
 2804                 }
 2805             }
 2806             $tables = array_merge(
 2807                 $groupTable,
 2808                 $dbi->getTablesFull(
 2809                     $db,
 2810                     $groupWithSeparator !== false ? $groupWithSeparator : '',
 2811                     $groupWithSeparator !== false,
 2812                     $limit_offset,
 2813                     $limit_count,
 2814                     $sort,
 2815                     $sort_order,
 2816                     $tbl_type
 2817                 )
 2818             );
 2819         }
 2820 
 2821         $num_tables = count($tables);
 2822         //  (needed for proper working of the MaxTableList feature)
 2823         if (! isset($total_num_tables)) {
 2824             $total_num_tables = $num_tables;
 2825         }
 2826 
 2827         /**
 2828          * If coming from a Show MySQL link on the home page,
 2829          * put something in $sub_part
 2830          */
 2831         if (empty($sub_part)) {
 2832             $sub_part = '_structure';
 2833         }
 2834 
 2835         return [
 2836             $tables,
 2837             $num_tables,
 2838             $total_num_tables,
 2839             $sub_part,
 2840             $isShowStats,
 2841             $isSystemSchema,
 2842             $tooltip_truename,
 2843             $tooltip_aliasname,
 2844             $pos,
 2845         ];
 2846     }
 2847 
 2848     /**
 2849      * Gets the list of tables in the current db, taking into account
 2850      * that they might be "in use"
 2851      *
 2852      * @param string $db             database name
 2853      * @param object $db_info_result result set
 2854      *
 2855      * @return array list of tables
 2856      */
 2857     public static function getTablesWhenOpen($db, $db_info_result): array
 2858     {
 2859         global $dbi;
 2860 
 2861         $sot_cache = [];
 2862         $tables = [];
 2863 
 2864         while ($tmp = $dbi->fetchAssoc($db_info_result)) {
 2865             $sot_cache[$tmp['Table']] = true;
 2866         }
 2867         $dbi->freeResult($db_info_result);
 2868 
 2869         // is there at least one "in use" table?
 2870         if (count($sot_cache) > 0) {
 2871             $tblGroupSql = '';
 2872             $whereAdded = false;
 2873             if (Core::isValid($_REQUEST['tbl_group'])) {
 2874                 $group = self::escapeMysqlWildcards($_REQUEST['tbl_group']);
 2875                 $groupWithSeparator = self::escapeMysqlWildcards(
 2876                     $_REQUEST['tbl_group']
 2877                     . $GLOBALS['cfg']['NavigationTreeTableSeparator']
 2878                 );
 2879                 $tblGroupSql .= ' WHERE ('
 2880                     . self::backquote('Tables_in_' . $db)
 2881                     . " LIKE '" . $groupWithSeparator . "%'"
 2882                     . ' OR '
 2883                     . self::backquote('Tables_in_' . $db)
 2884                     . " LIKE '" . $group . "')";
 2885                 $whereAdded = true;
 2886             }
 2887             if (Core::isValid($_REQUEST['tbl_type'], ['table', 'view'])) {
 2888                 $tblGroupSql .= $whereAdded ? ' AND' : ' WHERE';
 2889                 if ($_REQUEST['tbl_type'] === 'view') {
 2890                     $tblGroupSql .= " `Table_type` NOT IN ('BASE TABLE', 'SYSTEM VERSIONED')";
 2891                 } else {
 2892                     $tblGroupSql .= " `Table_type` IN ('BASE TABLE', 'SYSTEM VERSIONED')";
 2893                 }
 2894             }
 2895             $db_info_result = $dbi->query(
 2896                 'SHOW FULL TABLES FROM ' . self::backquote($db) . $tblGroupSql,
 2897                 DatabaseInterface::CONNECT_USER,
 2898                 DatabaseInterface::QUERY_STORE
 2899             );
 2900             unset($tblGroupSql, $whereAdded);
 2901 
 2902             if ($db_info_result && $dbi->numRows($db_info_result) > 0) {
 2903                 $names = [];
 2904                 while ($tmp = $dbi->fetchRow($db_info_result)) {
 2905                     if (! isset($sot_cache[$tmp[0]])) {
 2906                         $names[] = $tmp[0];
 2907                     } else { // table in use
 2908                         $tables[$tmp[0]] = [
 2909                             'TABLE_NAME' => $tmp[0],
 2910                             'ENGINE' => '',
 2911                             'TABLE_TYPE' => '',
 2912                             'TABLE_ROWS' => 0,
 2913                             'TABLE_COMMENT' => '',
 2914                         ];
 2915                     }
 2916                 }
 2917                 if (count($names) > 0) {
 2918                     $tables = array_merge(
 2919                         $tables,
 2920                         $dbi->getTablesFull($db, $names)
 2921                     );
 2922                 }
 2923                 if ($GLOBALS['cfg']['NaturalOrder']) {
 2924                     uksort($tables, 'strnatcasecmp');
 2925                 }
 2926             } elseif ($db_info_result) {
 2927                 $dbi->freeResult($db_info_result);
 2928             }
 2929             unset($sot_cache);
 2930         }
 2931 
 2932         return $tables;
 2933     }
 2934 
 2935     /**
 2936      * Checks whether database extension is loaded
 2937      *
 2938      * @param string $extension mysql extension to check
 2939      */
 2940     public static function checkDbExtension(string $extension = 'mysqli'): bool
 2941     {
 2942         return function_exists($extension . '_connect');
 2943     }
 2944 
 2945     /**
 2946      * Returns list of used PHP extensions.
 2947      *
 2948      * @return string[]
 2949      */
 2950     public static function listPHPExtensions(): array
 2951     {
 2952         $result = [];
 2953         if (self::checkDbExtension('mysqli')) {
 2954             $result[] = 'mysqli';
 2955         }
 2956 
 2957         if (extension_loaded('curl')) {
 2958             $result[] = 'curl';
 2959         }
 2960 
 2961         if (extension_loaded('mbstring')) {
 2962             $result[] = 'mbstring';
 2963         }
 2964 
 2965         return $result;
 2966     }
 2967 
 2968     /**
 2969      * Converts given (request) parameter to string
 2970      *
 2971      * @param mixed $value Value to convert
 2972      */
 2973     public static function requestString($value): string
 2974     {
 2975         while (is_array($value) || is_object($value)) {
 2976             if (is_object($value)) {
 2977                 $value = (array) $value;
 2978             }
 2979             $value = reset($value);
 2980         }
 2981 
 2982         return trim((string) $value);
 2983     }
 2984 
 2985     /**
 2986      * Generates random string consisting of ASCII chars
 2987      *
 2988      * @param int  $length Length of string
 2989      * @param bool $asHex  (optional) Send the result as hex
 2990      */
 2991     public static function generateRandom(int $length, bool $asHex = false): string
 2992     {
 2993         $result = '';
 2994         if (class_exists(Random::class)) {
 2995             $random_func = [
 2996                 Random::class,
 2997                 'string',
 2998             ];
 2999         } else {
 3000             $random_func = 'openssl_random_pseudo_bytes';
 3001         }
 3002         while (strlen($result) < $length) {
 3003             // Get random byte and strip highest bit
 3004             // to get ASCII only range
 3005             $byte = ord((string) $random_func(1)) & 0x7f;
 3006             // We want only ASCII chars
 3007             if ($byte <= 32) {
 3008                 continue;
 3009             }
 3010 
 3011             $result .= chr($byte);
 3012         }
 3013 
 3014         return $asHex ? bin2hex($result) : $result;
 3015     }
 3016 
 3017     /**
 3018      * Wrapper around PHP date function
 3019      *
 3020      * @param string $format Date format string
 3021      *
 3022      * @return string
 3023      */
 3024     public static function date($format)
 3025     {
 3026         if (defined('TESTSUITE')) {
 3027             return '0000-00-00 00:00:00';
 3028         }
 3029 
 3030         return date($format);
 3031     }
 3032 
 3033     /**
 3034      * Wrapper around php's set_time_limit
 3035      */
 3036     public static function setTimeLimit(): void
 3037     {
 3038         // The function can be disabled in php.ini
 3039         if (! function_exists('set_time_limit')) {
 3040             return;
 3041         }
 3042 
 3043         @set_time_limit((int) $GLOBALS['cfg']['ExecTimeLimit']);
 3044     }
 3045 
 3046     /**
 3047      * Access to a multidimensional array by dot notation
 3048      *
 3049      * @param array        $array   List of values
 3050      * @param string|array $path    Path to searched value
 3051      * @param mixed        $default Default value
 3052      *
 3053      * @return mixed Searched value
 3054      */
 3055     public static function getValueByKey(array $array, $path, $default = null)
 3056     {
 3057         if (is_string($path)) {
 3058             $path = explode('.', $path);
 3059         }
 3060         $p = array_shift($path);
 3061         while (isset($p)) {
 3062             if (! isset($array[$p])) {
 3063                 return $default;
 3064             }
 3065             $array = $array[$p];
 3066             $p = array_shift($path);
 3067         }
 3068 
 3069         return $array;
 3070     }
 3071 
 3072     /**
 3073      * Creates a clickable column header for table information
 3074      *
 3075      * @param string $title            Title to use for the link
 3076      * @param string $sort             Corresponds to sortable data name mapped
 3077      *                                 in Util::getDbInfo
 3078      * @param string $initialSortOrder Initial sort order
 3079      *
 3080      * @return string Link to be displayed in the table header
 3081      */
 3082     public static function sortableTableHeader($title, $sort, $initialSortOrder = 'ASC')
 3083     {
 3084         $requestedSort = 'table';
 3085         $requestedSortOrder = $futureSortOrder = $initialSortOrder;
 3086         // If the user requested a sort
 3087         if (isset($_REQUEST['sort'])) {
 3088             $requestedSort = $_REQUEST['sort'];
 3089             if (isset($_REQUEST['sort_order'])) {
 3090                 $requestedSortOrder = $_REQUEST['sort_order'];
 3091             }
 3092         }
 3093         $orderImg = '';
 3094         $orderLinkParams = [];
 3095         $orderLinkParams['title'] = __('Sort');
 3096         // If this column was requested to be sorted.
 3097         if ($requestedSort == $sort) {
 3098             if ($requestedSortOrder === 'ASC') {
 3099                 $futureSortOrder = 'DESC';
 3100                 // current sort order is ASC
 3101                 $orderImg = ' ' . Generator::getImage(
 3102                     's_asc',
 3103                     __('Ascending'),
 3104                     [
 3105                         'class' => 'sort_arrow',
 3106                         'title' => '',
 3107                     ]
 3108                 );
 3109                 $orderImg .= ' ' . Generator::getImage(
 3110                     's_desc',
 3111                     __('Descending'),
 3112                     [
 3113                         'class' => 'sort_arrow hide',
 3114                         'title' => '',
 3115                     ]
 3116                 );
 3117                 // but on mouse over, show the reverse order (DESC)
 3118                 $orderLinkParams['onmouseover'] = "$('.sort_arrow').toggle();";
 3119                 // on mouse out, show current sort order (ASC)
 3120                 $orderLinkParams['onmouseout'] = "$('.sort_arrow').toggle();";
 3121             } else {
 3122                 $futureSortOrder = 'ASC';
 3123                 // current sort order is DESC
 3124                 $orderImg = ' ' . Generator::getImage(
 3125                     's_asc',
 3126                     __('Ascending'),
 3127                     [
 3128                         'class' => 'sort_arrow hide',
 3129                         'title' => '',
 3130                     ]
 3131                 );
 3132                 $orderImg .= ' ' . Generator::getImage(
 3133                     's_desc',
 3134                     __('Descending'),
 3135                     [
 3136                         'class' => 'sort_arrow',
 3137                         'title' => '',
 3138                     ]
 3139                 );
 3140                 // but on mouse over, show the reverse order (ASC)
 3141                 $orderLinkParams['onmouseover'] = "$('.sort_arrow').toggle();";
 3142                 // on mouse out, show current sort order (DESC)
 3143                 $orderLinkParams['onmouseout'] = "$('.sort_arrow').toggle();";
 3144             }
 3145         }
 3146         $urlParams = [
 3147             'db' => $_REQUEST['db'],
 3148             'pos' => 0, // We set the position back to 0 every time they sort.
 3149             'sort' => $sort,
 3150             'sort_order' => $futureSortOrder,
 3151         ];
 3152 
 3153         if (Core::isValid($_REQUEST['tbl_type'], ['view', 'table'])) {
 3154             $urlParams['tbl_type'] = $_REQUEST['tbl_type'];
 3155         }
 3156         if (! empty($_REQUEST['tbl_group'])) {
 3157             $urlParams['tbl_group'] = $_REQUEST['tbl_group'];
 3158         }
 3159 
 3160         $url = Url::getFromRoute('/database/structure', $urlParams);
 3161 
 3162         return Generator::linkOrButton($url, $title . $orderImg, $orderLinkParams);
 3163     }
 3164 
 3165     /**
 3166      * Check that input is an int or an int in a string
 3167      *
 3168      * @param mixed $input input to check
 3169      */
 3170     public static function isInteger($input): bool
 3171     {
 3172         return ctype_digit((string) $input);
 3173     }
 3174 
 3175     /**
 3176      * Get the protocol from the RFC 7239 Forwarded header
 3177      *
 3178      * @param string $headerContents The Forwarded header contents
 3179      *
 3180      * @return string the protocol http/https
 3181      */
 3182     public static function getProtoFromForwardedHeader(string $headerContents): string
 3183     {
 3184         if (strpos($headerContents, '=') !== false) {// does not contain any equal sign
 3185             $hops = explode(',', $headerContents);
 3186             $parts = explode(';', $hops[0]);
 3187             foreach ($parts as $part) {
 3188                 $keyValueArray = explode('=', $part, 2);
 3189                 if (count($keyValueArray) !== 2) {
 3190                     continue;
 3191                 }
 3192 
 3193                 [
 3194                     $keyName,
 3195                     $value,
 3196                 ] = $keyValueArray;
 3197                 $value = trim(strtolower($value));
 3198                 if (strtolower(trim($keyName)) === 'proto' && in_array($value, ['http', 'https'])) {
 3199                     return $value;
 3200                 }
 3201             }
 3202         }
 3203 
 3204         return '';
 3205     }
 3206 
 3207     /**
 3208      * Check if error reporting is available
 3209      */
 3210     public static function isErrorReportingAvailable(): bool
 3211     {
 3212         // issue #16256 - PHP 7.x does not return false for a core function
 3213         if (PHP_MAJOR_VERSION < 8) {
 3214             $disabled = ini_get('disable_functions');
 3215             if (is_string($disabled)) {
 3216                 $disabled = explode(',', $disabled);
 3217                 $disabled = array_map(static function (string $part) {
 3218                     return trim($part);
 3219                 }, $disabled);
 3220 
 3221                 return ! in_array('error_reporting', $disabled);
 3222             }
 3223         }
 3224 
 3225         return function_exists('error_reporting');
 3226     }
 3227 }