3 declare(strict_types
=1);
7 use PhpMyAdmin\ConfigStorage\UserGroupLevel
;
8 use PhpMyAdmin\Html\Generator
;
9 use PhpMyAdmin\Http\ServerRequest
;
10 use PhpMyAdmin\Query\Compatibility
;
11 use PhpMyAdmin\SqlParser\Context
;
12 use PhpMyAdmin\SqlParser\Token
;
13 use PhpMyAdmin\Utils\SessionCache
;
17 use function _pgettext
;
19 use function array_key_exists
;
20 use function array_map
;
21 use function array_unique
;
25 use function ctype_digit
;
29 use function extension_loaded
;
33 use function function_exists
;
34 use function htmlentities
;
35 use function htmlspecialchars
;
36 use function htmlspecialchars_decode
;
38 use function in_array
;
39 use function is_array
;
40 use function is_numeric
;
41 use function is_object
;
42 use function is_scalar
;
43 use function is_string
;
45 use function mb_detect_encoding
;
46 use function mb_strlen
;
47 use function mb_strpos
;
48 use function mb_strrpos
;
49 use function mb_strtolower
;
50 use function mb_substr
;
51 use function number_format
;
53 use function parse_url
;
54 use function preg_match
;
55 use function preg_quote
;
56 use function preg_replace
;
57 use function random_bytes
;
62 use function set_time_limit
;
65 use function str_contains
;
66 use function str_ends_with
;
67 use function str_getcsv
;
69 use function str_replace
;
70 use function str_starts_with
;
71 use function strftime
;
73 use function strnatcasecmp
;
75 use function strtolower
;
83 use const PHP_INT_SIZE
;
84 use const STR_PAD_LEFT
;
87 * Misc functions used all over the scripts.
92 * Checks whether configuration value tells to show icons.
94 * @param string $value Configuration option name
96 public static function showIcons(string $value): bool
98 return in_array(Config
::getInstance()->settings
[$value], ['icons', 'both'], true);
102 * Checks whether configuration value tells to show text.
104 * @param string $value Configuration option name
106 public static function showText(string $value): bool
108 return in_array(Config
::getInstance()->settings
[$value], ['text', 'both'], true);
112 * Returns the formatted maximum size for an upload
114 * @param int|float|string $maxUploadSize the size
116 * @return string the message
118 public static function getFormattedMaximumUploadSize(int|
float|
string $maxUploadSize): string
120 // I have to reduce the second parameter (sensitiveness) from 6 to 4
121 // to avoid weird results like 512 kKib
122 [$maxSize, $maxUnit] = self
::formatByteDown($maxUploadSize, 4);
124 return '(' . sprintf(__('Max: %s%s'), $maxSize, $maxUnit) . ')';
128 * removes quotes (',",`) from a quoted string
130 * checks if the string is quoted and removes this quotes
132 * @param string $quotedString string to remove quotes from
133 * @param string|null $quote type of quote to remove
135 * @return string unquoted string
137 public static function unQuote(string $quotedString, string|
null $quote = null): string
141 if ($quote === null) {
149 foreach ($quotes as $quote) {
150 if (str_starts_with($quotedString, $quote) && str_ends_with($quotedString, $quote)) {
151 // replace escaped quotes
152 return str_replace($quote . $quote, $quote, mb_substr($quotedString, 1, -1));
156 return $quotedString;
160 * Get a URL link to the official MySQL documentation
162 * @param string $link contains name of page/anchor that is being linked
163 * @param string $anchor anchor to page part
165 * @return string the URL link
167 public static function getMySQLDocuURL(string $link, string $anchor = ''): string
169 // Fixup for newly used names:
170 $link = str_replace('_', '-', mb_strtolower($link));
178 $dbi = DatabaseInterface
::getInstance();
179 if ($dbi->isConnected()) {
180 $serverVersion = $dbi->getVersion();
181 if ($serverVersion >= 80000) {
183 } elseif ($serverVersion >= 50700) {
185 } elseif ($serverVersion >= 50600) {
190 $url = 'https://dev.mysql.com/doc/refman/'
191 . $mysql . '/' . $lang . '/' . $link . '.html';
192 if ($anchor !== '') {
193 $url .= '#' . $anchor;
196 return Core
::linkURL($url);
200 * Get a URL link to the official documentation page of either MySQL
201 * or MariaDB depending on the database server
204 * @param bool $isMariaDB if the database server is MariaDB
206 * @return string The URL link
208 public static function getDocuURL(bool $isMariaDB = false): string
211 $url = 'https://mariadb.com/kb/en/documentation/';
213 return Core
::linkURL($url);
216 return self
::getMySQLDocuURL('');
219 /* ----------------------- Set of misc functions ----------------------- */
222 * Adds backquotes on both sides of a database, table or field name.
223 * and escapes backquotes inside the name with another backquote
227 * echo backquote('owner`s db'); // `owner``s db`
231 * @param Stringable|string|null $identifier the database, table or field name to "backquote"
233 public static function backquote(Stringable|
string|
null $identifier): string
235 return static::backquoteCompat($identifier, 'NONE');
239 * Adds backquotes on both sides of a database, table or field name.
240 * in compatibility mode
244 * echo backquoteCompat('owner`s db'); // `owner``s db`
248 * @param Stringable|string|null $identifier the database, table or field name to "backquote"
249 * @param string $compatibility string compatibility mode (used by dump functions)
250 * @param bool $doIt a flag to bypass this function (used by dump functions)
252 public static function backquoteCompat(
253 Stringable|
string|
null $identifier,
254 string $compatibility = 'MSSQL',
257 $identifier = (string) $identifier;
258 if ($identifier === '' ||
$identifier === '*') {
262 if (! $doIt && ! ((int) Context
::isKeyword($identifier) & Token
::FLAG_KEYWORD_RESERVED
)) {
268 if ($compatibility === 'MSSQL') {
273 return $quote . str_replace($quote, $escapeChar . $quote, $identifier) . $quote;
277 * Formats $value to byte view
279 * @param float|int|string|null $value the value to format
280 * @param int $limes the sensitiveness
281 * @param int $comma the number of decimals to retain
283 * @return string[]|null the formatted value and its unit
284 * @psalm-return ($value is null ? null : array{string, string})
286 public static function formatByteDown(float|
int|
string|
null $value, int $limes = 6, int $comma = 0): array|
null
288 if ($value === null) {
292 if (is_string($value)) {
293 $value = (float) $value;
297 /* l10n: shortcuts for Byte */
299 /* l10n: shortcuts for Kilobyte */
301 /* l10n: shortcuts for Megabyte */
303 /* l10n: shortcuts for Gigabyte */
305 /* l10n: shortcuts for Terabyte */
307 /* l10n: shortcuts for Petabyte */
309 /* l10n: shortcuts for Exabyte */
315 $unit = $byteUnits[0];
317 /** @infection-ignore-all */
318 for ($d = 6, $ex = 15; $d >= 1; $d--, $ex -= 3) {
319 $unitSize = $li * 10 ** $ex;
320 if (isset($byteUnits[$d]) && $value >= $unitSize) {
321 // use 1024.0 to avoid integer overflow on 64-bit machines
322 $value = round($value / (1024 ** $d / $dh)) / $dh;
323 $unit = $byteUnits[$d];
328 if ($unit !== $byteUnits[0]) {
329 // if the unit is not bytes (as represented in current language)
330 // reformat with max length of 5
331 // 4th parameter=true means do not reformat if value < 1
332 $returnValue = self
::formatNumber($value, 5, $comma, true, false);
334 // do not reformat, just handle the locale
335 $returnValue = self
::formatNumber($value, 0);
338 return [trim($returnValue), $unit];
342 * Formats $value to the given length and appends SI prefixes
343 * with a $length of 0 no truncation occurs, number is only formatted
344 * to the current locale
348 * echo formatNumber(123456789, 6); // 123,457 k
349 * echo formatNumber(-123456789, 4, 2); // -123.46 M
350 * echo formatNumber(-0.003, 6); // -3 m
351 * echo formatNumber(0.003, 3, 3); // 0.003
352 * echo formatNumber(0.00003, 3, 2); // 0.03 m
353 * echo formatNumber(0, 6); // 0
356 * @param float|int|string $value the value to format
357 * @param int $digitsLeft number of digits left of the comma
358 * @param int $digitsRight number of digits right of the comma
359 * @param bool $onlyDown do not reformat numbers below 1
360 * @param bool $noTrailingZero removes trailing zeros right of the comma (default: true)
362 * @return string the formatted value and its unit
364 public static function formatNumber(
365 float|
int|
string $value,
367 int $digitsRight = 0,
368 bool $onlyDown = false,
369 bool $noTrailingZero = true,
375 if (is_string($value)) {
376 $value = (float) $value;
379 $originalValue = $value;
380 //number_format is not multibyte safe, str_replace is safe
381 if ($digitsLeft === 0) {
382 $value = number_format(
385 /* l10n: Decimal separator */
387 /* l10n: Thousands separator */
390 if ($originalValue != 0 && (float) $value == 0) {
391 return ' <' . (1 / 10 ** $digitsRight);
397 // this units needs no translation, ISO
417 /* l10n: Decimal separator */
418 $decimalSep = __('.');
419 /* l10n: Thousands separator */
420 $thousandsSep = __(',');
422 // check for negative value to retain sign
425 $value = abs($value);
430 $dh = 10 ** $digitsRight;
432 // This gives us the right SI prefix already, but $digits_left parameter not incorporated
433 $d = floor(log10((float) $value) / 3);
434 // Lowering the SI prefix by 1 gives us an additional 3 zeros
435 // So if we have 3,6,9,12.. free digits ($digits_left - $cur_digits) to use, then lower the SI prefix
436 $curDigits = floor(log10($value / 1000 ** $d) +
1);
437 if ($digitsLeft > $curDigits) {
438 $d -= floor(($digitsLeft - $curDigits) / 3);
441 if ($d < 0 && $onlyDown) {
445 $value = round($value / (1000 ** $d / $dh)) / $dh;
448 // number_format is not multibyte safe, str_replace is safe
449 $formattedValue = number_format($value, $digitsRight, $decimalSep, $thousandsSep);
450 // If we don't want any zeros, remove them now
451 if ($noTrailingZero && str_contains($formattedValue, $decimalSep)) {
452 $formattedValue = preg_replace('/' . preg_quote($decimalSep, '/') . '?0+$/', '', $formattedValue);
455 if ($originalValue != 0 && $value == 0) {
456 return ' <' . number_format(1 / 10 ** $digitsRight, $digitsRight, $decimalSep, $thousandsSep) . ' ' . $unit;
459 return $sign . $formattedValue . ' ' . $unit;
463 * Returns the number of bytes when a formatted size is given
465 * @param string|int $formattedSize the size expression (for example 8MB)
467 * @return int|float The numerical part of the expression (for example 8)
469 public static function extractValueFromFormattedSize(string|
int $formattedSize): int|
float
473 $formattedSize = (string) $formattedSize;
475 if (preg_match('/^[0-9]+GB$/', $formattedSize)) {
476 $returnValue = (int) mb_substr($formattedSize, 0, -2) * 1024 ** 3;
477 } elseif (preg_match('/^[0-9]+MB$/', $formattedSize)) {
478 $returnValue = (int) mb_substr($formattedSize, 0, -2) * 1024 ** 2;
479 } elseif (preg_match('/^[0-9]+K$/', $formattedSize)) {
480 $returnValue = (int) mb_substr($formattedSize, 0, -1) * 1024 ** 1;
487 * Writes localised date
489 * @param int $timestamp the current timestamp
490 * @param string $format format
492 * @return string the formatted date
494 public static function localisedDate(int $timestamp = -1, string $format = ''): string
497 _pgettext('Short month name for January', 'Jan'),
498 _pgettext('Short month name for February', 'Feb'),
499 _pgettext('Short month name for March', 'Mar'),
500 _pgettext('Short month name for April', 'Apr'),
501 _pgettext('Short month name for May', 'May'),
502 _pgettext('Short month name for June', 'Jun'),
503 _pgettext('Short month name for July', 'Jul'),
504 _pgettext('Short month name for August', 'Aug'),
505 _pgettext('Short month name for September', 'Sep'),
506 _pgettext('Short month name for October', 'Oct'),
507 _pgettext('Short month name for November', 'Nov'),
508 _pgettext('Short month name for December', 'Dec'),
511 _pgettext('Short week day name for Sunday', 'Sun'),
512 _pgettext('Short week day name for Monday', 'Mon'),
513 _pgettext('Short week day name for Tuesday', 'Tue'),
514 _pgettext('Short week day name for Wednesday', 'Wed'),
515 _pgettext('Short week day name for Thursday', 'Thu'),
516 _pgettext('Short week day name for Friday', 'Fri'),
517 _pgettext('Short week day name for Saturday', 'Sat'),
520 if ($format === '') {
521 /* l10n: See https://www.php.net/manual/en/function.strftime.php */
522 $format = __('%B %d, %Y at %I:%M %p');
525 if ($timestamp === -1) {
529 $date = (string) preg_replace(
531 // phpcs:ignore Generic.PHP.DeprecatedFunctions
532 $dayOfWeek[(int) @strftime
('%w', $timestamp)],
535 $date = (string) preg_replace(
537 // phpcs:ignore Generic.PHP.DeprecatedFunctions
538 $month[(int) @strftime
('%m', $timestamp) - 1],
543 $hours = (int) date('H', $timestamp);
545 $amPm = _pgettext('AM/PM indication in time', 'PM');
547 $amPm = _pgettext('AM/PM indication in time', 'AM');
550 $date = (string) preg_replace('@%[pP]@', $amPm, $date);
552 // Can return false on windows for Japanese language
553 // See https://github.com/phpmyadmin/phpmyadmin/issues/15830
554 // phpcs:ignore Generic.PHP.DeprecatedFunctions
555 $ret = @strftime
($date, $timestamp);
556 // Some OSes such as Win8.1 Traditional Chinese version did not produce UTF-8
557 // output here. See https://github.com/phpmyadmin/phpmyadmin/issues/10598
558 if ($ret === false ||
mb_detect_encoding($ret, 'UTF-8', true) !== 'UTF-8') {
559 return date('Y-m-d H:i:s', $timestamp);
566 * Splits a URL string by parameter
568 * @param string $url the URL
570 * @return array<int, string> the parameter/value pairs, for example [0] db=sakila
572 public static function splitURLQuery(string $url): array
574 // decode encoded url separators
575 $separator = Url
::getArgSeparator();
576 // on most places separator is still hard coded ...
577 if ($separator !== '&') {
578 // ... so always replace & with $separator
579 $url = str_replace([htmlentities('&'), '&'], [$separator, $separator], $url);
582 $url = str_replace(htmlentities($separator), $separator, $url);
585 $urlParts = parse_url($url);
587 if (is_array($urlParts) && isset($urlParts['query']) && $separator !== '') {
588 return explode($separator, $urlParts['query']);
595 * Returns a given timespan value in a readable format.
597 * @param int $seconds the timespan
599 * @return string the formatted value
601 public static function timespanFormat(int $seconds): string
603 $days = floor($seconds / 86400);
605 $seconds -= $days * 86400;
608 $hours = floor($seconds / 3600);
609 if ($days > 0 ||
$hours > 0) {
610 $seconds -= $hours * 3600;
613 $minutes = floor($seconds / 60);
614 if ($days > 0 ||
$hours > 0 ||
$minutes > 0) {
615 $seconds -= $minutes * 60;
619 __('%s days, %s hours, %s minutes and %s seconds'),
628 * Generate the charset query part
630 * @param string $collation Collation
631 * @param bool $override (optional) force 'CHARACTER SET' keyword
633 public static function getCharsetQueryPart(string $collation, bool $override = false): string
635 [$charset] = explode('_', $collation);
636 $keyword = ' CHARSET=';
639 $keyword = ' CHARACTER SET ';
642 return $keyword . $charset
643 . ($charset === $collation ?
'' : ' COLLATE ' . $collation);
647 * Generate a pagination selector for browsing resultsets
649 * @param string $name The name for the request parameter
650 * @param int $rows Number of rows in the pagination set
651 * @param int $pageNow current page number
652 * @param int $nbTotalPage number of total pages
653 * @param int $showAll If the number of pages is lower than this
654 * variable, no pages will be omitted in pagination
655 * @param int $sliceStart How many rows at the beginning should always
657 * @param int $sliceEnd How many rows at the end should always be shown?
658 * @param int $percent Percentage of calculation page offsets to hop to a
660 * @param int $range Near the current page, how many pages should
661 * be considered "nearby" and displayed as well?
662 * @param string $prompt The prompt to display (sometimes empty)
664 public static function pageselector(
668 int $nbTotalPage = 1,
676 $increment = floor($nbTotalPage / $percent);
677 $pageNowMinusRange = $pageNow - $range;
678 $pageNowPlusRange = $pageNow +
$range;
680 $gotoPage = $prompt . ' <select class="pageselector ajax"';
682 $gotoPage .= ' name="' . $name . '" >';
683 if ($nbTotalPage < $showAll) {
684 $pages = range(1, $nbTotalPage);
688 // Always show first X pages
689 for ($i = 1; $i <= $sliceStart; $i++
) {
693 // Always show last X pages
694 for ($i = $nbTotalPage - $sliceEnd; $i <= $nbTotalPage; $i++
) {
698 // Based on the number of results we add the specified
699 // $percent percentage to each page number,
700 // so that we have a representing page number every now and then to
701 // immediately jump to specific pages.
702 // As soon as we get near our currently chosen page ($pageNow -
703 // $range), every page number will be shown.
705 $x = $nbTotalPage - $sliceEnd;
706 $metBoundary = false;
709 if ($i >= $pageNowMinusRange && $i <= $pageNowPlusRange) {
710 // If our pageselector comes near the current page, we use 1
711 // counter increments
715 // We add the percentage increment to our current page to
716 // hop to the next one in range
719 // Make sure that we do not cross our boundaries.
720 if ($i > $pageNowMinusRange && ! $metBoundary) {
721 $i = $pageNowMinusRange;
725 if ($i <= 0 ||
$i > $x) {
733 Add page numbers with "geometrically increasing" distances.
735 This helps me a lot when navigating through giant tables.
737 Test case: table with 2.28 million sets, 76190 pages. Page of interest
738 is between 72376 and 76190.
739 Selecting page 72376.
740 Now, old version enumerated only +/- 10 pages around 72376 and the
741 percentage increment produced steps of about 3000.
743 The following code adds page numbers +/- 2,4,8,16,32,64,128,256 etc.
744 around the current page.
750 $i = $pageNow +
$dist;
751 if ($i <= 0 ||
$i > $x) {
762 $i = $pageNow - $dist;
763 if ($i <= 0 ||
$i > $x) {
770 // Since because of ellipsing of the current page some numbers may be
771 // double, we unify our array:
773 $pages = array_unique($pages);
776 if ($pageNow > $nbTotalPage) {
780 foreach ($pages as $i) {
781 if ($i == $pageNow) {
782 $selected = 'selected="selected" style="font-weight: bold"';
787 $gotoPage .= ' <option ' . $selected
788 . ' value="' . (($i - 1) * $rows) . '">' . $i . '</option>' . "\n";
791 $gotoPage .= ' </select>';
797 * Calculate page number through position
799 * @param int $pos position of first item
800 * @param int $maxCount number of items per page
802 * @return int $page_num
804 public static function getPageFromPosition(int $pos, int $maxCount): int
806 return (int) floor($pos / $maxCount) +
1;
810 * replaces %u in given path with current user name
814 * $user_dir = userDir('/var/pma_tmp/%u/'); // '/var/pma_tmp/root/'
818 * @param string $dir with wildcard for user
820 * @return string per user directory
822 public static function userDir(string $dir): string
824 // add trailing slash
825 if (! str_ends_with($dir, '/')) {
829 return str_replace('%u', Core
::securePath(Config
::getInstance()->selectedServer
['user']), $dir);
833 * Clears cache content which needs to be refreshed on user change.
835 public static function clearUserCache(): void
837 SessionCache
::remove('is_superuser');
838 SessionCache
::remove('is_createuser');
839 SessionCache
::remove('is_grantuser');
840 SessionCache
::remove('mysql_cur_user');
841 SessionCache
::remove('mysql_cur_role');
845 * Converts a bit value to printable format;
846 * in MySQL a BIT field can be from 1 to 64 bits so we need this
847 * function because in PHP, decbin() supports only 32 bits
850 * @param int $value coming from a BIT field
851 * @param int $length length
853 * @return string the printable value
855 public static function printableBitValue(int $value, int $length): string
857 // if running on a 64-bit server or the length is safe for decbin()
858 if (PHP_INT_SIZE
== 8 ||
$length < 33) {
859 $printable = decbin($value);
861 // FIXME: does not work for the leftmost bit of a 64-bit value
864 while ($value >= 2 ** $i) {
873 if ($value - 2 ** $i < 0) {
874 $printable = '0' . $printable;
876 $printable = '1' . $printable;
883 $printable = strrev($printable);
886 return str_pad($printable, $length, '0', STR_PAD_LEFT
);
890 * Converts a BIT type default value
891 * for example, b'010' becomes 010
893 * @param string|null $bitDefaultValue value
895 * @return string the converted value
897 public static function convertBitDefaultValue(string|
null $bitDefaultValue): string
899 return (string) preg_replace(
902 htmlspecialchars_decode((string) $bitDefaultValue, ENT_QUOTES
),
908 * Extracts the various parts from a column spec
910 * @param string $columnSpecification Column specification
912 * @return mixed[] associative array containing type, spec_in_brackets
913 * and possibly enum_set_values (another array)
915 public static function extractColumnSpec(string $columnSpecification): array
917 $firstBracketPos = mb_strpos($columnSpecification, '(');
918 if ($firstBracketPos) {
919 $specInBrackets = rtrim(
921 $columnSpecification,
922 $firstBracketPos +
1,
923 mb_strrpos($columnSpecification, ')') - $firstBracketPos - 1,
926 // convert to lowercase just to be sure
927 $type = mb_strtolower(
928 rtrim(mb_substr($columnSpecification, 0, $firstBracketPos)),
931 // Split trailing attributes such as unsigned,
932 // binary, zerofill and get data type name
933 $typeParts = explode(' ', $columnSpecification);
934 $type = mb_strtolower($typeParts[0]);
935 $specInBrackets = '';
938 if ($type === 'enum' ||
$type === 'set') {
939 // Define our working vars
940 $enumSetValues = self
::parseEnumSetValues($columnSpecification, false);
942 . '(' . str_replace("','", "', '", $specInBrackets) . ')';
950 /* Create printable type name */
951 $printType = mb_strtolower($columnSpecification);
953 // Strip the "BINARY" attribute, except if we find "BINARY(" because
954 // this would be a BINARY or VARBINARY column type;
955 // by the way, a BLOB should not show the BINARY attribute
956 // because this is not accepted in MySQL syntax.
957 if (str_contains($printType, 'binary') && ! preg_match('@binary[\(]@', $printType)) {
958 $printType = str_replace('binary', '', $printType);
964 $printType = (string) preg_replace('@zerofill@', '', $printType, -1, $zerofillCount);
965 $zerofill = $zerofillCount > 0;
966 $printType = (string) preg_replace('@unsigned@', '', $printType, -1, $unsignedCount);
967 $unsigned = $unsignedCount > 0;
968 $printType = (string) preg_replace('@\/\*!100301 compressed\*\/@', '', $printType, -1, $compressedCount);
969 $compressed = $compressedCount > 0;
970 $printType = trim($printType);
975 $attribute = 'BINARY';
979 $attribute = 'UNSIGNED';
983 $attribute = 'UNSIGNED ZEROFILL';
987 // With InnoDB page compression, multiple compression algorithms are supported.
988 // In contrast, with InnoDB's COMPRESSED row format, zlib is the only supported compression algorithm.
989 // This means that the COMPRESSED row format has less compression options than InnoDB page compression does.
990 // @see https://mariadb.com/kb/en/innodb-page-compression/#comparison-with-the-compressed-row-format
991 $attribute = 'COMPRESSED=zlib';
994 $canContainCollation = false;
995 if (! $binary && preg_match('@^(char|varchar|text|tinytext|mediumtext|longtext|set|enum)@', $type)) {
996 $canContainCollation = true;
999 // for the case ENUM('–','“')
1000 $displayedType = htmlspecialchars($printType, ENT_COMPAT
);
1001 $config = Config
::getInstance();
1002 if (mb_strlen($printType) > $config->settings
['LimitChars']) {
1003 $displayedType = '<abbr title="' . htmlspecialchars($printType) . '">';
1004 $displayedType .= htmlspecialchars(
1008 $config->settings
['LimitChars'],
1012 $displayedType .= '</abbr>';
1017 'spec_in_brackets' => $specInBrackets,
1018 'enum_set_values' => $enumSetValues,
1019 'print_type' => $printType,
1020 'binary' => $binary,
1021 'unsigned' => $unsigned,
1022 'zerofill' => $zerofill,
1023 'attribute' => $attribute,
1024 'can_contain_collation' => $canContainCollation,
1025 'displayed_type' => $displayedType,
1030 * If the string starts with a \r\n pair (0x0d0a) add an extra \n
1032 * @return string with the chars replaced
1034 public static function duplicateFirstNewline(string $string): string
1036 $firstOccurrence = mb_strpos($string, "\r\n");
1037 if ($firstOccurrence === 0) {
1038 return "\n" . $string;
1045 * Get the action word corresponding to a script name
1046 * in order to display it as a title in navigation panel
1048 * @param string $target a valid value for $cfg['NavigationTreeDefaultTabTable'],
1049 * $cfg['NavigationTreeDefaultTabTable2'],
1050 * $cfg['DefaultTabTable'] or $cfg['DefaultTabDatabase']
1052 * @return string|bool Title for the $cfg value
1054 public static function getTitleForTarget(string $target): string|
bool
1057 'structure' => __('Structure'),
1059 'search' => __('Search'),
1060 'insert' => __('Insert'),
1061 'browse' => __('Browse'),
1062 'operations' => __('Operations'),
1065 return $mapping[$target] ??
false;
1069 * Get the script name corresponding to a plain English config word
1070 * in order to append in links on navigation and main panel
1072 * @param string $target a valid value for
1073 * $cfg['NavigationTreeDefaultTabTable'],
1074 * $cfg['NavigationTreeDefaultTabTable2'],
1075 * $cfg['DefaultTabTable'], $cfg['DefaultTabDatabase'] or
1076 * $cfg['DefaultTabServer']
1077 * @param string $location one out of 'server', 'table', 'database'
1079 * @return string script name corresponding to the config word
1081 public static function getScriptNameForOption(string $target, string $location): string
1083 return Url
::getFromRoute(self
::getUrlForOption($target, $location));
1087 * Get the URL corresponding to a plain English config word
1088 * in order to append in links on navigation and main panel
1090 * @param string $target a valid value for
1091 * $cfg['NavigationTreeDefaultTabTable'],
1092 * $cfg['NavigationTreeDefaultTabTable2'],
1093 * $cfg['DefaultTabTable'], $cfg['DefaultTabDatabase'] or
1094 * $cfg['DefaultTabServer']
1095 * @param string $location one out of 'server', 'table', 'database'
1097 * @return string The URL corresponding to the config word
1099 public static function getUrlForOption(string $target, string $location): string
1101 if ($location === 'server') {
1102 // Values for $cfg['DefaultTabServer']
1109 case 'server_databases.php':
1110 return '/server/databases';
1113 case 'server_status.php':
1114 return '/server/status';
1117 case 'server_variables.php':
1118 return '/server/variables';
1121 case 'server_privileges.php':
1122 return '/server/privileges';
1124 } elseif ($location === 'database') {
1125 // Values for $cfg['DefaultTabDatabase']
1128 case 'db_structure.php':
1129 return '/database/structure';
1133 return '/database/sql';
1136 case 'db_search.php':
1137 return '/database/search';
1140 case 'db_operations.php':
1141 return '/database/operations';
1143 } elseif ($location === 'table') {
1144 // Values for $cfg['DefaultTabTable'],
1145 // $cfg['NavigationTreeDefaultTabTable'] and
1146 // $cfg['NavigationTreeDefaultTabTable2']
1149 case 'tbl_structure.php':
1150 return '/table/structure';
1154 return '/table/sql';
1157 case 'tbl_select.php':
1158 return '/table/search';
1161 case 'tbl_change.php':
1162 return '/table/change';
1174 * Formats user string, expanding @VARIABLES@, accepting strftime format
1177 * @param string $string Text where to do expansion.
1178 * @param callable|null $escape Function to call for escaping variable values.
1179 * @param array<string, string|null> $updates Array with overrides for default parameters (obtained from GLOBALS).
1180 * @psalm-param callable(string):string|null $escape
1182 public static function expandUserString(
1184 callable|
null $escape = null,
1185 array $updates = [],
1189 $vars['http_host'] = Core
::getEnv('HTTP_HOST');
1190 $config = Config
::getInstance();
1191 $vars['server_name'] = $config->selectedServer
['host'];
1192 $vars['server_verbose'] = $config->selectedServer
['verbose'];
1194 if (empty($config->selectedServer
['verbose'])) {
1195 $vars['server_verbose_or_name'] = $config->selectedServer
['host'];
1197 $vars['server_verbose_or_name'] = $config->selectedServer
['verbose'];
1200 $vars['database'] = Current
::$database;
1201 $vars['table'] = Current
::$table;
1202 $vars['phpmyadmin_version'] = 'phpMyAdmin ' . Version
::VERSION
;
1204 /* Update forced variables */
1205 foreach ($updates as $key => $val) {
1210 * Replacement mapping
1212 * The __VAR__ ones are for backward compatibility, because user might still have it in cookies.
1215 '@HTTP_HOST@' => $vars['http_host'],
1216 '@SERVER@' => $vars['server_name'],
1217 '__SERVER__' => $vars['server_name'],
1218 '@VERBOSE@' => $vars['server_verbose'],
1219 '@VSERVER@' => $vars['server_verbose_or_name'],
1220 '@DATABASE@' => $vars['database'],
1221 '__DB__' => $vars['database'],
1222 '@TABLE@' => $vars['table'],
1223 '__TABLE__' => $vars['table'],
1224 '@PHPMYADMIN@' => $vars['phpmyadmin_version'],
1227 /* Optional escaping */
1228 if ($escape !== null) {
1229 $replace = array_map($escape, $replace);
1232 /* Backward compatibility in 3.5.x */
1233 if (str_contains($string, '@FIELDS@')) {
1234 $string = strtr($string, ['@FIELDS@' => '@COLUMNS@']);
1237 /* Fetch columns list if required */
1238 if (str_contains($string, '@COLUMNS@')) {
1239 $columnsList = DatabaseInterface
::getInstance()->getColumnNames(Current
::$database, Current
::$table);
1242 if ($escape !== null) {
1243 foreach ($columnsList as $column) {
1244 $columnNames[] = self
::$escape($column);
1247 $columnNames = $columnsList;
1250 $replace['@COLUMNS@'] = implode(',', $columnNames);
1253 /* Do the replacement */
1254 // phpcs:ignore Generic.PHP.DeprecatedFunctions
1255 return strtr((string) @strftime
($string), $replace);
1259 * This function processes the datatypes supported by the DB,
1260 * as specified in Types->getColumns() and returns an array
1261 * (useful for quickly checking if a datatype is supported).
1263 * @return string[] An array of datatypes.
1265 public static function getSupportedDatatypes(): array
1268 foreach (DatabaseInterface
::getInstance()->types
->getColumns() as $value) {
1269 if (is_array($value)) {
1270 foreach ($value as $subvalue) {
1271 if ($subvalue === '-') {
1275 $retval[] = $subvalue;
1277 } elseif ($value !== '-') {
1286 * Returns a list of datatypes that are not (yet) handled by PMA.
1287 * Used by: /table/change and libraries/Routines.php
1289 * @return string[] list of datatypes
1291 public static function unsupportedDatatypes(): array
1297 * This function is to check whether database support UUID
1299 public static function isUUIDSupported(): bool
1301 return Compatibility
::isUUIDSupported(DatabaseInterface
::getInstance());
1305 * Checks if the current user has a specific privilege and returns true if the
1306 * user indeed has that privilege or false if they don't. This function must
1307 * only be used for features that are available since MySQL 5, because it
1308 * relies on the INFORMATION_SCHEMA database to be present.
1310 * Example: currentUserHasPrivilege('CREATE ROUTINE', 'mydb');
1311 * // Checks if the currently logged in user has the global
1312 * // 'CREATE ROUTINE' privilege or, if not, checks if the
1313 * // user has this privilege on database 'mydb'.
1315 * @param string $priv The privilege to check
1316 * @param string|null $db null, to only check global privileges
1317 * string, db name where to also check
1319 * @param string|null $tbl null, to only check global/db privileges
1320 * string, table name where to also check
1323 public static function currentUserHasPrivilege(string $priv, string|
null $db = null, string|
null $tbl = null): bool
1325 $dbi = DatabaseInterface
::getInstance();
1326 // Get the username for the current user in the format
1327 // required to use in the information schema database.
1328 [$user, $host] = $dbi->getCurrentUserAndHost();
1330 // MySQL is started with --skip-grant-tables
1336 $username .= str_replace("'", "''", $user);
1337 $username .= "''@''";
1338 $username .= str_replace("'", "''", $host);
1341 // Prepare the query
1342 $query = 'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`%s` '
1343 . "WHERE GRANTEE='%s' AND PRIVILEGE_TYPE='%s'";
1345 // Check global privileges first.
1346 $userPrivileges = $dbi->fetchValue(
1354 if ($userPrivileges) {
1358 // If a database name was provided and user does not have the
1359 // required global privilege, try database-wise permissions.
1361 // There was no database name provided and the user
1362 // does not have the correct global privilege.
1366 $query .= ' AND %s LIKE `TABLE_SCHEMA`';
1367 $schemaPrivileges = $dbi->fetchValue(
1370 'SCHEMA_PRIVILEGES',
1373 $dbi->quoteString($db),
1376 if ($schemaPrivileges) {
1380 // If a table name was also provided and we still didn't
1381 // find any valid privileges, try table-wise privileges.
1382 if ($tbl !== null) {
1383 $query .= ' AND TABLE_NAME=%s';
1384 $tablePrivileges = $dbi->fetchValue(
1390 $dbi->quoteString($db),
1391 $dbi->quoteString($tbl),
1394 if ($tablePrivileges) {
1400 * If we reached this point, the user does not
1401 * have even valid table-wise privileges.
1407 * Returns server type for current connection
1409 * Known types are: MariaDB, Percona Server and MySQL (default)
1411 * @phpstan-return 'MariaDB'|'Percona Server'|'MySQL'
1413 public static function getServerType(): string
1415 $dbi = DatabaseInterface
::getInstance();
1416 if ($dbi->isMariaDB()) {
1420 if ($dbi->isPercona()) {
1421 return 'Percona Server';
1428 * Parses ENUM/SET values
1430 * @param string $definition The definition of the column
1431 * for which to parse the values
1432 * @param bool $escapeHtml Whether to escape html entities
1436 public static function parseEnumSetValues(string $definition, bool $escapeHtml = true): array
1438 // There is a JS port of the below parser in functions.js
1439 // If you are fixing something here,
1440 // you need to also update the JS port.
1442 // This should really be delegated to MySQL but since we also want to HTML encode it,
1443 // it is easier this way.
1444 // It future replace str_getcsv with $dbi->fetchSingleRow('SELECT '.$expressionInBrackets[1]);
1446 preg_match('/\((.*)\)/', $definition, $expressionInBrackets);
1447 $matches = str_getcsv($expressionInBrackets[1], ',', "'");
1450 foreach ($matches as $value) {
1451 $value = strtr($value, ['\\\\' => '\\']); // str_getcsv doesn't unescape backslashes so we do it ourselves
1452 $values[] = $escapeHtml ?
htmlspecialchars($value, ENT_QUOTES
, 'UTF-8') : $value;
1459 * Return the list of tabs for the menu with corresponding names
1461 * @return array<string, string> list of tabs for the menu
1463 public static function getMenuTabList(UserGroupLevel
$level): array
1465 return match ($level) {
1466 UserGroupLevel
::Server
=> [
1467 'databases' => __('Databases'),
1469 'status' => __('Status'),
1470 'rights' => __('Users'),
1471 'export' => __('Export'),
1472 'import' => __('Import'),
1473 'settings' => __('Settings'),
1474 'binlog' => __('Binary log'),
1475 'replication' => __('Replication'),
1476 'vars' => __('Variables'),
1477 'charset' => __('Charsets'),
1478 'plugins' => __('Plugins'),
1479 'engine' => __('Engines'),
1481 UserGroupLevel
::Database
=> [
1482 'structure' => __('Structure'),
1484 'search' => __('Search'),
1485 'query' => __('Query'),
1486 'export' => __('Export'),
1487 'import' => __('Import'),
1488 'operation' => __('Operations'),
1489 'privileges' => __('Privileges'),
1490 'routines' => __('Routines'),
1491 'events' => __('Events'),
1492 'triggers' => __('Triggers'),
1493 'tracking' => __('Tracking'),
1494 'designer' => __('Designer'),
1495 'central_columns' => __('Central columns'),
1497 UserGroupLevel
::Table
=> [
1498 'browse' => __('Browse'),
1499 'structure' => __('Structure'),
1501 'search' => __('Search'),
1502 'insert' => __('Insert'),
1503 'export' => __('Export'),
1504 'import' => __('Import'),
1505 'privileges' => __('Privileges'),
1506 'operation' => __('Operations'),
1507 'tracking' => __('Tracking'),
1508 'triggers' => __('Triggers'),
1514 * Add fractional seconds to time, datetime and timestamp strings.
1515 * If the string contains fractional seconds,
1516 * pads it with 0s up to 6 decimal places.
1518 * @param string $value time, datetime or timestamp strings
1520 * @return string time, datetime or timestamp strings with fractional seconds
1522 public static function addMicroseconds(string $value): string
1524 if ($value === '' ||
$value === 'CURRENT_TIMESTAMP' ||
$value === 'current_timestamp()') {
1528 if (! str_contains($value, '.')) {
1529 return $value . '.000000';
1537 mb_strpos($value, '.') +
7,
1542 * Reads the file, detects the compression MIME type, closes the file
1543 * and returns the MIME type
1545 * @param resource $file the file handle
1547 * @return string the MIME type for compression, or 'none'
1549 public static function getCompressionMimeType($file): string
1551 $test = fread($file, 4);
1553 if ($test === false) {
1559 $len = strlen($test);
1561 if ($len >= 2 && $test[0] === chr(31) && $test[1] === chr(139)) {
1562 return 'application/gzip';
1565 if ($len >= 3 && str_starts_with($test, 'BZh')) {
1566 return 'application/bzip2';
1569 if ($len >= 4 && $test == "PK\003\004") {
1570 return 'application/zip';
1577 * Provide COLLATE clause, if required, to perform case sensitive comparisons
1578 * for queries on information_schema.
1580 * @return string COLLATE clause if needed or empty string.
1582 public static function getCollateForIS(): string
1584 $names = DatabaseInterface
::getInstance()->getLowerCaseNames();
1586 return 'COLLATE utf8_bin';
1590 return 'COLLATE utf8_general_ci';
1597 * Process the index data.
1599 * @param mixed[] $indexes index data
1601 * @return mixed[] processes index data
1603 public static function processIndexData(array $indexes): array
1608 $pkArray = []; // will be use to emphasis prim. keys in the table
1613 foreach ($indexes as $row) {
1614 // Backups the list of primary keys
1615 if ($row['Key_name'] === 'PRIMARY') {
1616 $primary .= $row['Column_name'] . ', ';
1617 $pkArray[$row['Column_name']] = 1;
1620 // Retains keys informations
1621 if ($row['Key_name'] != $lastIndex) {
1622 $lastIndex = $row['Key_name'];
1625 $indexesInfo[$row['Key_name']]['Sequences'][] = $row['Seq_in_index'];
1626 $indexesInfo[$row['Key_name']]['Non_unique'] = $row['Non_unique'];
1627 if (isset($row['Cardinality'])) {
1628 $indexesInfo[$row['Key_name']]['Cardinality'] = $row['Cardinality'];
1631 // I don't know what does following column mean....
1632 // $indexes_info[$row['Key_name']]['Packed'] = $row['Packed'];
1634 $indexesInfo[$row['Key_name']]['Comment'] = $row['Comment'];
1636 $indexesData[$row['Key_name']][$row['Seq_in_index']]['Column_name'] = $row['Column_name'];
1637 if (! isset($row['Sub_part'])) {
1641 $indexesData[$row['Key_name']][$row['Seq_in_index']]['Sub_part'] = $row['Sub_part'];
1644 return [$primary, $pkArray, $indexesInfo, $indexesData];
1648 * Gets the list of tables in the current db and information about these tables if possible.
1650 * @return array<int, array|int>
1651 * @psalm-return array{array, int}
1653 public static function getDbInfo(ServerRequest
$request, string $db, bool $isResultLimited = true): array
1656 * information about tables in db
1660 $dbi = DatabaseInterface
::getInstance();
1661 $config = Config
::getInstance();
1662 // Special speedup for newer MySQL Versions (in 4.0 format changed)
1663 if ($config->settings
['SkipLockedTables'] === true) {
1664 $tables = self
::getTablesWhenOpen($db);
1667 $totalNumTables = null;
1668 if ($tables === []) {
1669 // Set some sorting defaults
1673 /** @var mixed $sortParam */
1674 $sortParam = $request->getParam('sort');
1675 if (is_string($sortParam)) {
1676 $sortableNameMappings = [
1678 'records' => 'Rows',
1680 'collation' => 'Collation',
1681 'size' => 'Data_length',
1682 'overhead' => 'Data_free',
1683 'creation' => 'Create_time',
1684 'last_update' => 'Update_time',
1685 'last_check' => 'Check_time',
1686 'comment' => 'Comment',
1689 // Make sure the sort type is implemented
1690 if (isset($sortableNameMappings[$sortParam])) {
1691 $sort = $sortableNameMappings[$sortParam];
1692 if ($request->getParam('sort_order') === 'DESC') {
1693 $sortOrder = 'DESC';
1698 $groupWithSeparator = false;
1701 $limitCount = false;
1704 /** @var mixed $tableGroupParam */
1705 $tableGroupParam = $request->getParam('tbl_group');
1706 /** @var mixed $tableTypeParam */
1707 $tableTypeParam = $request->getParam('tbl_type');
1709 is_string($tableGroupParam) && $tableGroupParam !== ''
1710 ||
is_string($tableTypeParam) && $tableTypeParam !== ''
1712 if (is_string($tableTypeParam) && $tableTypeParam !== '') {
1713 // only tables for selected type
1714 $tableType = $tableTypeParam;
1717 if (is_string($tableGroupParam) && $tableGroupParam !== '') {
1718 // only tables for selected group
1719 // include the table with the exact name of the group if such exists
1720 $groupTable = $dbi->getTablesFull(
1730 $groupWithSeparator = $tableGroupParam . $config->settings
['NavigationTreeTableSeparator'];
1734 // - get the total number of tables
1735 // (needed for proper working of the MaxTableList feature)
1736 $tables = $dbi->getTables($db);
1737 $totalNumTables = count($tables);
1738 if ($isResultLimited) {
1739 // fetch the details for a possible limited subset
1740 $limitOffset = self
::getTableListPosition($request, $db);
1745 // We must use union operator here instead of array_merge to preserve numerical keys
1746 $tables = $groupTable +
$dbi->getTablesFull(
1748 $groupWithSeparator !== false ?
$groupWithSeparator : $tables,
1749 $groupWithSeparator !== false,
1760 $totalNumTables ??
count($tables), // needed for proper working of the MaxTableList feature
1765 * Gets the list of tables in the current db, taking into account
1766 * that they might be "in use"
1768 * @return mixed[] list of tables
1770 private static function getTablesWhenOpen(string $db): array
1772 $dbi = DatabaseInterface
::getInstance();
1774 $openTables = $dbi->query(
1775 'SHOW OPEN TABLES FROM ' . self
::backquote($db) . ' WHERE In_use > 0;',
1778 // Blending out tables in use
1779 $openTableNames = [];
1781 /** @var string $tableName */
1782 foreach ($openTables as ['Table' => $tableName]) {
1783 $openTableNames[] = $tableName;
1786 // is there at least one "in use" table?
1787 if ($openTableNames === []) {
1793 $whereAdded = false;
1794 $config = Config
::getInstance();
1796 isset($_REQUEST['tbl_group'])
1797 && is_scalar($_REQUEST['tbl_group'])
1798 && (string) $_REQUEST['tbl_group'] !== ''
1800 $group = $dbi->escapeMysqlWildcards((string) $_REQUEST['tbl_group']);
1801 $groupWithSeparator = $dbi->escapeMysqlWildcards(
1802 $_REQUEST['tbl_group'] . $config->settings
['NavigationTreeTableSeparator'],
1804 $tblGroupSql .= ' WHERE ('
1805 . self
::backquote('Tables_in_' . $db)
1806 . ' LIKE ' . $dbi->quoteString($groupWithSeparator . '%')
1808 . self
::backquote('Tables_in_' . $db)
1809 . ' LIKE ' . $dbi->quoteString($group) . ')';
1813 if (isset($_REQUEST['tbl_type']) && in_array($_REQUEST['tbl_type'], ['table', 'view'], true)) {
1814 $tblGroupSql .= $whereAdded ?
' AND' : ' WHERE';
1815 if ($_REQUEST['tbl_type'] === 'view') {
1816 $tblGroupSql .= " `Table_type` NOT IN ('BASE TABLE', 'SYSTEM VERSIONED')";
1818 $tblGroupSql .= " `Table_type` IN ('BASE TABLE', 'SYSTEM VERSIONED')";
1822 $dbInfoResult = $dbi->query('SHOW FULL TABLES FROM ' . self
::backquote($db) . $tblGroupSql);
1824 if ($dbInfoResult->numRows() > 0) {
1826 while ($tableName = $dbInfoResult->fetchValue()) {
1827 if (! in_array($tableName, $openTableNames, true)) {
1828 $names[] = $tableName;
1829 } else { // table in use
1830 $tables[$tableName] = [
1831 'TABLE_NAME' => $tableName,
1835 'TABLE_COMMENT' => '',
1840 if ($names !== []) {
1841 $tables +
= $dbi->getTablesFull($db, $names);
1844 if ($config->settings
['NaturalOrder']) {
1845 uksort($tables, strnatcasecmp(...));
1853 * Returns list of used PHP extensions.
1857 public static function listPHPExtensions(): array
1860 if (function_exists('mysqli_connect')) {
1861 $result[] = 'mysqli';
1864 if (extension_loaded('curl')) {
1868 if (extension_loaded('mbstring')) {
1869 $result[] = 'mbstring';
1872 if (extension_loaded('sodium')) {
1873 $result[] = 'sodium';
1880 * Converts given (request) parameter to string
1882 * @param mixed $value Value to convert
1884 public static function requestString(mixed $value): string
1886 while (is_array($value) ||
is_object($value)) {
1887 if (is_object($value)) {
1888 $value = (array) $value;
1891 $value = reset($value);
1894 return trim((string) $value);
1898 * Generates random string consisting of ASCII chars
1900 * @param int $length Length of string
1901 * @param bool $asHex (optional) Send the result as hex
1903 public static function generateRandom(int $length, bool $asHex = false): string
1907 /** @infection-ignore-all */
1908 while (strlen($result) < $length) {
1909 // Get random byte and strip highest bit
1910 // to get ASCII only range
1911 $byte = ord(random_bytes(1)) & 0x7f;
1912 // We want only ASCII chars and no DEL character (127)
1913 if ($byte <= 32 ||
$byte === 127) {
1917 $result .= chr($byte);
1920 return $asHex ?
bin2hex($result) : $result;
1924 * Wrapper around PHP date function
1926 * @param string $format Date format string
1928 public static function date(string $format): string
1930 return date($format);
1934 * Wrapper around php's set_time_limit
1936 public static function setTimeLimit(): void
1938 // The function can be disabled in php.ini
1939 if (! function_exists('set_time_limit')) {
1943 @set_time_limit
(Config
::getInstance()->settings
['ExecTimeLimit']);
1947 * Access to a multidimensional array recursively by the keys specified in $path
1949 * @param mixed[] $array List of values
1950 * @param (int|string)[] $path Path to searched value
1951 * @param mixed $default Default value
1953 * @return mixed Searched value
1955 public static function getValueByKey(array $array, array $path, mixed $default = null): mixed
1957 foreach ($path as $key) {
1958 if (! array_key_exists($key, $array)) {
1962 $array = $array[$key];
1969 * Creates a clickable column header for table information
1971 * @param string $title Title to use for the link
1972 * @param string $sort Corresponds to sortable data name mapped
1973 * in Util::getDbInfo
1974 * @param string $initialSortOrder Initial sort order
1976 * @return string Link to be displayed in the table header
1978 public static function sortableTableHeader(string $title, string $sort, string $initialSortOrder = 'ASC'): string
1980 $requestedSort = 'table';
1981 $requestedSortOrder = $futureSortOrder = $initialSortOrder;
1982 // If the user requested a sort
1983 if (isset($_REQUEST['sort'])) {
1984 $requestedSort = $_REQUEST['sort'];
1985 if (isset($_REQUEST['sort_order'])) {
1986 $requestedSortOrder = $_REQUEST['sort_order'];
1991 $orderLinkParams = [];
1992 $orderLinkParams['title'] = __('Sort');
1993 // If this column was requested to be sorted.
1994 if ($requestedSort == $sort) {
1995 if ($requestedSortOrder === 'ASC') {
1996 $futureSortOrder = 'DESC';
1997 // current sort order is ASC
1998 $orderImg = ' ' . Generator
::getImage(
2001 ['class' => 'sort_arrow', 'title' => ''],
2003 $orderImg .= ' ' . Generator
::getImage(
2006 ['class' => 'sort_arrow hide', 'title' => ''],
2008 // but on mouse over, show the reverse order (DESC)
2009 $orderLinkParams['onmouseover'] = "$('.sort_arrow').toggle();";
2010 // on mouse out, show current sort order (ASC)
2011 $orderLinkParams['onmouseout'] = "$('.sort_arrow').toggle();";
2013 $futureSortOrder = 'ASC';
2014 // current sort order is DESC
2015 $orderImg = ' ' . Generator
::getImage(
2018 ['class' => 'sort_arrow hide', 'title' => ''],
2020 $orderImg .= ' ' . Generator
::getImage(
2023 ['class' => 'sort_arrow', 'title' => ''],
2025 // but on mouse over, show the reverse order (ASC)
2026 $orderLinkParams['onmouseover'] = "$('.sort_arrow').toggle();";
2027 // on mouse out, show current sort order (DESC)
2028 $orderLinkParams['onmouseout'] = "$('.sort_arrow').toggle();";
2033 'db' => $_REQUEST['db'],
2034 'pos' => 0, // We set the position back to 0 every time they sort.
2036 'sort_order' => $futureSortOrder,
2039 if (isset($_REQUEST['tbl_type']) && in_array($_REQUEST['tbl_type'], ['view', 'table'], true)) {
2040 $urlParams['tbl_type'] = $_REQUEST['tbl_type'];
2043 if (! empty($_REQUEST['tbl_group'])) {
2044 $urlParams['tbl_group'] = $_REQUEST['tbl_group'];
2047 $url = Url
::getFromRoute('/database/structure');
2049 return Generator
::linkOrButton($url, $urlParams, $title . $orderImg, $orderLinkParams);
2053 * Check that input is an int or an int in a string
2055 * @param mixed $input input to check
2057 public static function isInteger(mixed $input): bool
2059 return is_scalar($input) && ctype_digit((string) $input);
2063 * Get the protocol from the RFC 7239 Forwarded header
2065 * @param string $headerContents The Forwarded header contents
2067 * @return string the protocol http/https
2069 public static function getProtoFromForwardedHeader(string $headerContents): string
2071 if (str_contains($headerContents, '=')) {// does not contain any equal sign
2072 $hops = explode(',', $headerContents);
2073 $parts = explode(';', $hops[0]);
2074 foreach ($parts as $part) {
2075 $keyValueArray = explode('=', $part, 2);
2076 if (count($keyValueArray) !== 2) {
2080 [$keyName, $value] = $keyValueArray;
2081 $value = strtolower(trim($value));
2082 if (strtolower(trim($keyName)) === 'proto' && in_array($value, ['http', 'https'], true)) {
2091 public static function getTableListPosition(ServerRequest
$request, string $db): int
2093 if (! isset($_SESSION['tmpval']['table_limit_offset']) ||
$_SESSION['tmpval']['table_limit_offset_db'] != $db) {
2094 $_SESSION['tmpval']['table_limit_offset'] = 0;
2095 $_SESSION['tmpval']['table_limit_offset_db'] = $db;
2098 /** @var string|null $posParam */
2099 $posParam = $request->getParam('pos');
2100 if (is_numeric($posParam)) {
2101 $_SESSION['tmpval']['table_limit_offset'] = (int) $posParam;
2104 return $_SESSION['tmpval']['table_limit_offset'];