Translated using Weblate (Portuguese)
[phpmyadmin.git] / src / Util.php
blob907be406ee88d710750f453adcac82c630ad991f
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin;
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;
14 use Stringable;
16 use function __;
17 use function _pgettext;
18 use function abs;
19 use function array_key_exists;
20 use function array_map;
21 use function array_unique;
22 use function bin2hex;
23 use function chr;
24 use function count;
25 use function ctype_digit;
26 use function date;
27 use function decbin;
28 use function explode;
29 use function extension_loaded;
30 use function fclose;
31 use function floor;
32 use function fread;
33 use function function_exists;
34 use function htmlentities;
35 use function htmlspecialchars;
36 use function htmlspecialchars_decode;
37 use function implode;
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;
44 use function log10;
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;
52 use function ord;
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;
58 use function range;
59 use function reset;
60 use function round;
61 use function rtrim;
62 use function set_time_limit;
63 use function sort;
64 use function sprintf;
65 use function str_contains;
66 use function str_ends_with;
67 use function str_getcsv;
68 use function str_pad;
69 use function str_replace;
70 use function str_starts_with;
71 use function strftime;
72 use function strlen;
73 use function strnatcasecmp;
74 use function strrev;
75 use function strtolower;
76 use function strtr;
77 use function time;
78 use function trim;
79 use function uksort;
81 use const ENT_COMPAT;
82 use const ENT_QUOTES;
83 use const PHP_INT_SIZE;
84 use const STR_PAD_LEFT;
86 /**
87 * Misc functions used all over the scripts.
89 class Util
91 /**
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
139 $quotes = [];
141 if ($quote === null) {
142 $quotes[] = '`';
143 $quotes[] = '"';
144 $quotes[] = "'";
145 } else {
146 $quotes[] = $quote;
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));
172 if ($link === '') {
173 $link = 'index';
176 $mysql = '5.5';
177 $lang = 'en';
178 $dbi = DatabaseInterface::getInstance();
179 if ($dbi->isConnected()) {
180 $serverVersion = $dbi->getVersion();
181 if ($serverVersion >= 80000) {
182 $mysql = '8.0';
183 } elseif ($serverVersion >= 50700) {
184 $mysql = '5.7';
185 } elseif ($serverVersion >= 50600) {
186 $mysql = '5.6';
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
202 * of the user.
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
210 if ($isMariaDB) {
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
225 * example:
226 * <code>
227 * echo backquote('owner`s db'); // `owner``s db`
229 * </code>
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
242 * example:
243 * <code>
244 * echo backquoteCompat('owner`s db'); // `owner``s db`
246 * </code>
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',
255 bool $doIt = true,
256 ): string {
257 $identifier = (string) $identifier;
258 if ($identifier === '' || $identifier === '*') {
259 return $identifier;
262 if (! $doIt && ! ((int) Context::isKeyword($identifier) & Token::FLAG_KEYWORD_RESERVED)) {
263 return $identifier;
266 $quote = '`';
267 $escapeChar = '`';
268 if ($compatibility === 'MSSQL') {
269 $quote = '"';
270 $escapeChar = '\\';
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) {
289 return null;
292 if (is_string($value)) {
293 $value = (float) $value;
296 $byteUnits = [
297 /* l10n: shortcuts for Byte */
298 __('B'),
299 /* l10n: shortcuts for Kilobyte */
300 __('KiB'),
301 /* l10n: shortcuts for Megabyte */
302 __('MiB'),
303 /* l10n: shortcuts for Gigabyte */
304 __('GiB'),
305 /* l10n: shortcuts for Terabyte */
306 __('TiB'),
307 /* l10n: shortcuts for Petabyte */
308 __('PiB'),
309 /* l10n: shortcuts for Exabyte */
310 __('EiB'),
313 $dh = 10 ** $comma;
314 $li = 10 ** $limes;
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];
324 break 1;
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);
333 } else {
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
346 * examples:
347 * <code>
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
354 * </code>
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,
366 int $digitsLeft = 3,
367 int $digitsRight = 0,
368 bool $onlyDown = false,
369 bool $noTrailingZero = true,
370 ): string {
371 if ($value == 0) {
372 return '0';
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(
383 (float) $value,
384 $digitsRight,
385 /* l10n: Decimal separator */
386 __('.'),
387 /* l10n: Thousands separator */
388 __(','),
390 if ($originalValue != 0 && (float) $value == 0) {
391 return ' <' . (1 / 10 ** $digitsRight);
394 return $value;
397 // this units needs no translation, ISO
398 $units = [
399 -8 => 'y',
400 -7 => 'z',
401 -6 => 'a',
402 -5 => 'f',
403 -4 => 'p',
404 -3 => 'n',
405 -2 => 'µ',
406 -1 => 'm',
407 0 => ' ',
408 1 => 'k',
409 2 => 'M',
410 3 => 'G',
411 4 => 'T',
412 5 => 'P',
413 6 => 'E',
414 7 => 'Z',
415 8 => 'Y',
417 /* l10n: Decimal separator */
418 $decimalSep = __('.');
419 /* l10n: Thousands separator */
420 $thousandsSep = __(',');
422 // check for negative value to retain sign
423 if ($value < 0) {
424 $sign = '-';
425 $value = abs($value);
426 } else {
427 $sign = '';
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) {
442 $d = 0;
445 $value = round($value / (1000 ** $d / $dh)) / $dh;
446 $unit = $units[$d];
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
471 $returnValue = -1;
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;
483 return $returnValue;
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
496 $month = [
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'),
510 $dayOfWeek = [
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) {
526 $timestamp = time();
529 $date = (string) preg_replace(
530 '@%[aA]@',
531 // phpcs:ignore Generic.PHP.DeprecatedFunctions
532 $dayOfWeek[(int) @strftime('%w', $timestamp)],
533 $format,
535 $date = (string) preg_replace(
536 '@%[bB]@',
537 // phpcs:ignore Generic.PHP.DeprecatedFunctions
538 $month[(int) @strftime('%m', $timestamp) - 1],
539 $date,
542 /* Fill in AM/PM */
543 $hours = (int) date('H', $timestamp);
544 if ($hours >= 12) {
545 $amPm = _pgettext('AM/PM indication in time', 'PM');
546 } else {
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);
562 return $ret;
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);
583 // end decode
585 $urlParts = parse_url($url);
587 if (is_array($urlParts) && isset($urlParts['query']) && $separator !== '') {
588 return explode($separator, $urlParts['query']);
591 return [];
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);
604 if ($days > 0) {
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;
618 return sprintf(
619 __('%s days, %s hours, %s minutes and %s seconds'),
620 (string) $days,
621 (string) $hours,
622 (string) $minutes,
623 (string) $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=';
638 if ($override) {
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
656 * be shown?
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
659 * next page
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(
665 string $name,
666 int $rows,
667 int $pageNow = 1,
668 int $nbTotalPage = 1,
669 int $showAll = 200,
670 int $sliceStart = 5,
671 int $sliceEnd = 5,
672 int $percent = 20,
673 int $range = 10,
674 string $prompt = '',
675 ): string {
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);
685 } else {
686 $pages = [];
688 // Always show first X pages
689 for ($i = 1; $i <= $sliceStart; $i++) {
690 $pages[] = $i;
693 // Always show last X pages
694 for ($i = $nbTotalPage - $sliceEnd; $i <= $nbTotalPage; $i++) {
695 $pages[] = $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.
704 $i = $sliceStart;
705 $x = $nbTotalPage - $sliceEnd;
706 $metBoundary = false;
708 while ($i <= $x) {
709 if ($i >= $pageNowMinusRange && $i <= $pageNowPlusRange) {
710 // If our pageselector comes near the current page, we use 1
711 // counter increments
712 $i++;
713 $metBoundary = true;
714 } else {
715 // We add the percentage increment to our current page to
716 // hop to the next one in range
717 $i += $increment;
719 // Make sure that we do not cross our boundaries.
720 if ($i > $pageNowMinusRange && ! $metBoundary) {
721 $i = $pageNowMinusRange;
725 if ($i <= 0 || $i > $x) {
726 continue;
729 $pages[] = $i;
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.
746 $i = $pageNow;
747 $dist = 1;
748 while ($i < $x) {
749 $dist *= 2;
750 $i = $pageNow + $dist;
751 if ($i <= 0 || $i > $x) {
752 continue;
755 $pages[] = $i;
758 $i = $pageNow;
759 $dist = 1;
760 while ($i > 0) {
761 $dist *= 2;
762 $i = $pageNow - $dist;
763 if ($i <= 0 || $i > $x) {
764 continue;
767 $pages[] = $i;
770 // Since because of ellipsing of the current page some numbers may be
771 // double, we unify our array:
772 sort($pages);
773 $pages = array_unique($pages);
776 if ($pageNow > $nbTotalPage) {
777 $pages[] = $pageNow;
780 foreach ($pages as $i) {
781 if ($i == $pageNow) {
782 $selected = 'selected="selected" style="font-weight: bold"';
783 } else {
784 $selected = '';
787 $gotoPage .= ' <option ' . $selected
788 . ' value="' . (($i - 1) * $rows) . '">' . $i . '</option>' . "\n";
791 $gotoPage .= ' </select>';
793 return $gotoPage;
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
812 * example:
813 * <code>
814 * $user_dir = userDir('/var/pma_tmp/%u/'); // '/var/pma_tmp/root/'
816 * </code>
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, '/')) {
826 $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
848 * on 32-bit servers
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);
860 } else {
861 // FIXME: does not work for the leftmost bit of a 64-bit value
862 $i = 0;
863 $printable = '';
864 while ($value >= 2 ** $i) {
865 ++$i;
868 if ($i !== 0) {
869 --$i;
872 while ($i >= 0) {
873 if ($value - 2 ** $i < 0) {
874 $printable = '0' . $printable;
875 } else {
876 $printable = '1' . $printable;
877 $value -= 2 ** $i;
880 --$i;
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(
900 "/^b'(\d*)'?$/",
901 '$1',
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(
920 mb_substr(
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)),
930 } else {
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);
941 $printType = $type
942 . '(' . str_replace("','", "', '", $specInBrackets) . ')';
943 $binary = false;
944 $unsigned = false;
945 $zerofill = false;
946 $compressed = false;
947 } else {
948 $enumSetValues = [];
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);
959 $binary = true;
960 } else {
961 $binary = false;
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);
973 $attribute = ' ';
974 if ($binary) {
975 $attribute = 'BINARY';
978 if ($unsigned) {
979 $attribute = 'UNSIGNED';
982 if ($zerofill) {
983 $attribute = 'UNSIGNED ZEROFILL';
986 if ($compressed) {
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('&#8211;','&ldquo;')
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(
1005 mb_substr(
1006 $printType,
1008 $config->settings['LimitChars'],
1009 ) . '...',
1010 ENT_COMPAT,
1012 $displayedType .= '</abbr>';
1015 return [
1016 'type' => $type,
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;
1041 return $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
1056 $mapping = [
1057 'structure' => __('Structure'),
1058 'sql' => __('SQL'),
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']
1103 switch ($target) {
1104 case 'welcome':
1105 case 'index.php':
1106 return '/';
1108 case 'databases':
1109 case 'server_databases.php':
1110 return '/server/databases';
1112 case 'status':
1113 case 'server_status.php':
1114 return '/server/status';
1116 case 'variables':
1117 case 'server_variables.php':
1118 return '/server/variables';
1120 case 'privileges':
1121 case 'server_privileges.php':
1122 return '/server/privileges';
1124 } elseif ($location === 'database') {
1125 // Values for $cfg['DefaultTabDatabase']
1126 switch ($target) {
1127 case 'structure':
1128 case 'db_structure.php':
1129 return '/database/structure';
1131 case 'sql':
1132 case 'db_sql.php':
1133 return '/database/sql';
1135 case 'search':
1136 case 'db_search.php':
1137 return '/database/search';
1139 case 'operations':
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']
1147 switch ($target) {
1148 case 'structure':
1149 case 'tbl_structure.php':
1150 return '/table/structure';
1152 case 'sql':
1153 case 'tbl_sql.php':
1154 return '/table/sql';
1156 case 'search':
1157 case 'tbl_select.php':
1158 return '/table/search';
1160 case 'insert':
1161 case 'tbl_change.php':
1162 return '/table/change';
1164 case 'browse':
1165 case 'sql.php':
1166 return '/sql';
1170 return '/';
1174 * Formats user string, expanding @VARIABLES@, accepting strftime format
1175 * string.
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(
1183 string $string,
1184 callable|null $escape = null,
1185 array $updates = [],
1186 ): string {
1187 /* Content */
1188 $vars = [];
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'];
1196 } else {
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) {
1206 $vars[$key] = $val;
1210 * Replacement mapping
1212 * The __VAR__ ones are for backward compatibility, because user might still have it in cookies.
1214 $replace = [
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);
1241 $columnNames = [];
1242 if ($escape !== null) {
1243 foreach ($columnsList as $column) {
1244 $columnNames[] = self::$escape($column);
1246 } else {
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
1267 $retval = [];
1268 foreach (DatabaseInterface::getInstance()->types->getColumns() as $value) {
1269 if (is_array($value)) {
1270 foreach ($value as $subvalue) {
1271 if ($subvalue === '-') {
1272 continue;
1275 $retval[] = $subvalue;
1277 } elseif ($value !== '-') {
1278 $retval[] = $value;
1282 return $retval;
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
1293 return [];
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
1318 * for privileges
1319 * @param string|null $tbl null, to only check global/db privileges
1320 * string, table name where to also check
1321 * for privileges
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
1331 if ($user === '') {
1332 return true;
1335 $username = "''";
1336 $username .= str_replace("'", "''", $user);
1337 $username .= "''@''";
1338 $username .= str_replace("'", "''", $host);
1339 $username .= "''";
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(
1347 sprintf(
1348 $query,
1349 'USER_PRIVILEGES',
1350 $username,
1351 $priv,
1354 if ($userPrivileges) {
1355 return true;
1358 // If a database name was provided and user does not have the
1359 // required global privilege, try database-wise permissions.
1360 if ($db === null) {
1361 // There was no database name provided and the user
1362 // does not have the correct global privilege.
1363 return false;
1366 $query .= ' AND %s LIKE `TABLE_SCHEMA`';
1367 $schemaPrivileges = $dbi->fetchValue(
1368 sprintf(
1369 $query,
1370 'SCHEMA_PRIVILEGES',
1371 $username,
1372 $priv,
1373 $dbi->quoteString($db),
1376 if ($schemaPrivileges) {
1377 return true;
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(
1385 sprintf(
1386 $query,
1387 'TABLE_PRIVILEGES',
1388 $username,
1389 $priv,
1390 $dbi->quoteString($db),
1391 $dbi->quoteString($tbl),
1394 if ($tablePrivileges) {
1395 return true;
1400 * If we reached this point, the user does not
1401 * have even valid table-wise privileges.
1403 return false;
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()) {
1417 return 'MariaDB';
1420 if ($dbi->isPercona()) {
1421 return 'Percona Server';
1424 return 'MySQL';
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
1434 * @return string[]
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], ',', "'");
1449 $values = [];
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;
1455 return $values;
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'),
1468 'sql' => __('SQL'),
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'),
1483 'sql' => __('SQL'),
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'),
1500 'sql' => __('SQL'),
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()') {
1525 return $value;
1528 if (! str_contains($value, '.')) {
1529 return $value . '.000000';
1532 $value .= '000000';
1534 return mb_substr(
1535 $value,
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) {
1554 fclose($file);
1556 return 'none';
1559 $len = strlen($test);
1560 fclose($file);
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';
1573 return 'none';
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();
1585 if ($names === 0) {
1586 return 'COLLATE utf8_bin';
1589 if ($names === 2) {
1590 return 'COLLATE utf8_general_ci';
1593 return '';
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
1605 $lastIndex = '';
1607 $primary = '';
1608 $pkArray = []; // will be use to emphasis prim. keys in the table
1609 $indexesInfo = [];
1610 $indexesData = [];
1612 // view
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'])) {
1638 continue;
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
1658 $tables = [];
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
1670 $sort = 'Name';
1671 $sortOrder = 'ASC';
1673 /** @var mixed $sortParam */
1674 $sortParam = $request->getParam('sort');
1675 if (is_string($sortParam)) {
1676 $sortableNameMappings = [
1677 'table' => 'Name',
1678 'records' => 'Rows',
1679 'type' => 'Engine',
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;
1699 $tableType = null;
1700 $limitOffset = 0;
1701 $limitCount = false;
1702 $groupTable = [];
1704 /** @var mixed $tableGroupParam */
1705 $tableGroupParam = $request->getParam('tbl_group');
1706 /** @var mixed $tableTypeParam */
1707 $tableTypeParam = $request->getParam('tbl_type');
1708 if (
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(
1721 $db,
1722 $tableGroupParam,
1723 false,
1725 false,
1726 $sort,
1727 $sortOrder,
1728 $tableType,
1730 $groupWithSeparator = $tableGroupParam . $config->settings['NavigationTreeTableSeparator'];
1732 } else {
1733 // all tables in db
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);
1741 $limitCount = true;
1745 // We must use union operator here instead of array_merge to preserve numerical keys
1746 $tables = $groupTable + $dbi->getTablesFull(
1747 $db,
1748 $groupWithSeparator !== false ? $groupWithSeparator : $tables,
1749 $groupWithSeparator !== false,
1750 $limitOffset,
1751 $limitCount,
1752 $sort,
1753 $sortOrder,
1754 $tableType,
1758 return [
1759 $tables,
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 === []) {
1788 return [];
1791 $tables = [];
1792 $tblGroupSql = '';
1793 $whereAdded = false;
1794 $config = Config::getInstance();
1795 if (
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 . '%')
1807 . ' OR '
1808 . self::backquote('Tables_in_' . $db)
1809 . ' LIKE ' . $dbi->quoteString($group) . ')';
1810 $whereAdded = true;
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')";
1817 } else {
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) {
1825 $names = [];
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,
1832 'ENGINE' => '',
1833 'TABLE_TYPE' => '',
1834 'TABLE_ROWS' => 0,
1835 'TABLE_COMMENT' => '',
1840 if ($names !== []) {
1841 $tables += $dbi->getTablesFull($db, $names);
1844 if ($config->settings['NaturalOrder']) {
1845 uksort($tables, strnatcasecmp(...));
1849 return $tables;
1853 * Returns list of used PHP extensions.
1855 * @return string[]
1857 public static function listPHPExtensions(): array
1859 $result = [];
1860 if (function_exists('mysqli_connect')) {
1861 $result[] = 'mysqli';
1864 if (extension_loaded('curl')) {
1865 $result[] = 'curl';
1868 if (extension_loaded('mbstring')) {
1869 $result[] = 'mbstring';
1872 if (extension_loaded('sodium')) {
1873 $result[] = 'sodium';
1876 return $result;
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
1905 $result = '';
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) {
1914 continue;
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')) {
1940 return;
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)) {
1959 return $default;
1962 $array = $array[$key];
1965 return $array;
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'];
1990 $orderImg = '';
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(
1999 's_asc',
2000 __('Ascending'),
2001 ['class' => 'sort_arrow', 'title' => ''],
2003 $orderImg .= ' ' . Generator::getImage(
2004 's_desc',
2005 __('Descending'),
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();";
2012 } else {
2013 $futureSortOrder = 'ASC';
2014 // current sort order is DESC
2015 $orderImg = ' ' . Generator::getImage(
2016 's_asc',
2017 __('Ascending'),
2018 ['class' => 'sort_arrow hide', 'title' => ''],
2020 $orderImg .= ' ' . Generator::getImage(
2021 's_desc',
2022 __('Descending'),
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();";
2032 $urlParams = [
2033 'db' => $_REQUEST['db'],
2034 'pos' => 0, // We set the position back to 0 every time they sort.
2035 'sort' => $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) {
2077 continue;
2080 [$keyName, $value] = $keyValueArray;
2081 $value = strtolower(trim($value));
2082 if (strtolower(trim($keyName)) === 'proto' && in_array($value, ['http', 'https'], true)) {
2083 return $value;
2088 return '';
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'];