Translated using Weblate (Portuguese (Brazil))
[phpmyadmin.git] / src / Util.php
blob414144050905200088fb354707f2bed46d60bc3e
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin;
7 use PhpMyAdmin\Html\Generator;
8 use PhpMyAdmin\Http\ServerRequest;
9 use PhpMyAdmin\Query\Compatibility;
10 use PhpMyAdmin\SqlParser\Context;
11 use PhpMyAdmin\SqlParser\Token;
12 use PhpMyAdmin\Utils\SessionCache;
13 use Stringable;
15 use function __;
16 use function _pgettext;
17 use function abs;
18 use function array_key_exists;
19 use function array_map;
20 use function array_unique;
21 use function bin2hex;
22 use function chr;
23 use function count;
24 use function ctype_digit;
25 use function date;
26 use function decbin;
27 use function explode;
28 use function extension_loaded;
29 use function fclose;
30 use function floatval;
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_getcsv;
67 use function str_pad;
68 use function str_replace;
69 use function str_starts_with;
70 use function strftime;
71 use function strlen;
72 use function strnatcasecmp;
73 use function strrev;
74 use function strtolower;
75 use function strtr;
76 use function time;
77 use function trim;
78 use function uksort;
80 use const ENT_COMPAT;
81 use const ENT_QUOTES;
82 use const PHP_INT_SIZE;
83 use const STR_PAD_LEFT;
85 /**
86 * Misc functions used all over the scripts.
88 class Util
90 /**
91 * Checks whether configuration value tells to show icons.
93 * @param string $value Configuration option name
95 public static function showIcons(string $value): bool
97 return in_array(Config::getInstance()->settings[$value], ['icons', 'both'], true);
101 * Checks whether configuration value tells to show text.
103 * @param string $value Configuration option name
105 public static function showText(string $value): bool
107 return in_array(Config::getInstance()->settings[$value], ['text', 'both'], true);
111 * Returns the formatted maximum size for an upload
113 * @param int|float|string $maxUploadSize the size
115 * @return string the message
117 public static function getFormattedMaximumUploadSize(int|float|string $maxUploadSize): string
119 // I have to reduce the second parameter (sensitiveness) from 6 to 4
120 // to avoid weird results like 512 kKib
121 [$maxSize, $maxUnit] = self::formatByteDown($maxUploadSize, 4);
123 return '(' . sprintf(__('Max: %s%s'), $maxSize, $maxUnit) . ')';
127 * removes quotes (',",`) from a quoted string
129 * checks if the string is quoted and removes this quotes
131 * @param string $quotedString string to remove quotes from
132 * @param string $quote type of quote to remove
134 * @return string unquoted string
136 public static function unQuote(string $quotedString, string|null $quote = null): string
138 $quotes = [];
140 if ($quote === null) {
141 $quotes[] = '`';
142 $quotes[] = '"';
143 $quotes[] = "'";
144 } else {
145 $quotes[] = $quote;
148 foreach ($quotes as $quote) {
149 if (mb_substr($quotedString, 0, 1) === $quote && mb_substr($quotedString, -1, 1) === $quote) {
150 // replace escaped quotes
151 return str_replace($quote . $quote, $quote, mb_substr($quotedString, 1, -1));
155 return $quotedString;
159 * Get a URL link to the official MySQL documentation
161 * @param string $link contains name of page/anchor that is being linked
162 * @param string $anchor anchor to page part
164 * @return string the URL link
166 public static function getMySQLDocuURL(string $link, string $anchor = ''): string
168 // Fixup for newly used names:
169 $link = str_replace('_', '-', mb_strtolower($link));
171 if ($link === '') {
172 $link = 'index';
175 $mysql = '5.5';
176 $lang = 'en';
177 $dbi = DatabaseInterface::getInstance();
178 if ($dbi->isConnected()) {
179 $serverVersion = $dbi->getVersion();
180 if ($serverVersion >= 80000) {
181 $mysql = '8.0';
182 } elseif ($serverVersion >= 50700) {
183 $mysql = '5.7';
184 } elseif ($serverVersion >= 50600) {
185 $mysql = '5.6';
189 $url = 'https://dev.mysql.com/doc/refman/'
190 . $mysql . '/' . $lang . '/' . $link . '.html';
191 if ($anchor !== '') {
192 $url .= '#' . $anchor;
195 return Core::linkURL($url);
199 * Get a URL link to the official documentation page of either MySQL
200 * or MariaDB depending on the database server
201 * of the user.
203 * @param bool $isMariaDB if the database server is MariaDB
205 * @return string The URL link
207 public static function getDocuURL(bool $isMariaDB = false): string
209 if ($isMariaDB) {
210 $url = 'https://mariadb.com/kb/en/documentation/';
212 return Core::linkURL($url);
215 return self::getMySQLDocuURL('');
218 /* ----------------------- Set of misc functions ----------------------- */
221 * Adds backquotes on both sides of a database, table or field name.
222 * and escapes backquotes inside the name with another backquote
224 * example:
225 * <code>
226 * echo backquote('owner`s db'); // `owner``s db`
228 * </code>
230 * @param Stringable|string|null $identifier the database, table or field name to "backquote"
232 public static function backquote(Stringable|string|null $identifier): string
234 return static::backquoteCompat($identifier, 'NONE');
238 * Adds backquotes on both sides of a database, table or field name.
239 * in compatibility mode
241 * example:
242 * <code>
243 * echo backquoteCompat('owner`s db'); // `owner``s db`
245 * </code>
247 * @param Stringable|string|null $identifier the database, table or field name to "backquote"
248 * @param string $compatibility string compatibility mode (used by dump functions)
249 * @param bool|null $doIt a flag to bypass this function (used by dump functions)
251 public static function backquoteCompat(
252 Stringable|string|null $identifier,
253 string $compatibility = 'MSSQL',
254 bool|null $doIt = true,
255 ): string {
256 $identifier = (string) $identifier;
257 if ($identifier === '' || $identifier === '*') {
258 return $identifier;
261 if (! $doIt && ! ((int) Context::isKeyword($identifier) & Token::FLAG_KEYWORD_RESERVED)) {
262 return $identifier;
265 $quote = '`';
266 $escapeChar = '`';
267 if ($compatibility === 'MSSQL') {
268 $quote = '"';
269 $escapeChar = '\\';
272 return $quote . str_replace($quote, $escapeChar . $quote, $identifier) . $quote;
276 * Formats $value to byte view
278 * @param float|int|string|null $value the value to format
279 * @param int $limes the sensitiveness
280 * @param int $comma the number of decimals to retain
282 * @return string[]|null the formatted value and its unit
283 * @psalm-return ($value is null ? null : array{string, string})
285 public static function formatByteDown(float|int|string|null $value, int $limes = 6, int $comma = 0): array|null
287 if ($value === null) {
288 return null;
291 if (is_string($value)) {
292 $value = (float) $value;
295 $byteUnits = [
296 /* l10n: shortcuts for Byte */
297 __('B'),
298 /* l10n: shortcuts for Kilobyte */
299 __('KiB'),
300 /* l10n: shortcuts for Megabyte */
301 __('MiB'),
302 /* l10n: shortcuts for Gigabyte */
303 __('GiB'),
304 /* l10n: shortcuts for Terabyte */
305 __('TiB'),
306 /* l10n: shortcuts for Petabyte */
307 __('PiB'),
308 /* l10n: shortcuts for Exabyte */
309 __('EiB'),
312 $dh = 10 ** $comma;
313 $li = 10 ** $limes;
314 $unit = $byteUnits[0];
316 /** @infection-ignore-all */
317 for ($d = 6, $ex = 15; $d >= 1; $d--, $ex -= 3) {
318 $unitSize = $li * 10 ** $ex;
319 if (isset($byteUnits[$d]) && $value >= $unitSize) {
320 // use 1024.0 to avoid integer overflow on 64-bit machines
321 $value = round($value / (1024 ** $d / $dh)) / $dh;
322 $unit = $byteUnits[$d];
323 break 1;
327 if ($unit !== $byteUnits[0]) {
328 // if the unit is not bytes (as represented in current language)
329 // reformat with max length of 5
330 // 4th parameter=true means do not reformat if value < 1
331 $returnValue = self::formatNumber($value, 5, $comma, true, false);
332 } else {
333 // do not reformat, just handle the locale
334 $returnValue = self::formatNumber($value, 0);
337 return [trim($returnValue), $unit];
341 * Formats $value to the given length and appends SI prefixes
342 * with a $length of 0 no truncation occurs, number is only formatted
343 * to the current locale
345 * examples:
346 * <code>
347 * echo formatNumber(123456789, 6); // 123,457 k
348 * echo formatNumber(-123456789, 4, 2); // -123.46 M
349 * echo formatNumber(-0.003, 6); // -3 m
350 * echo formatNumber(0.003, 3, 3); // 0.003
351 * echo formatNumber(0.00003, 3, 2); // 0.03 m
352 * echo formatNumber(0, 6); // 0
353 * </code>
355 * @param float|int|string $value the value to format
356 * @param int $digitsLeft number of digits left of the comma
357 * @param int $digitsRight number of digits right of the comma
358 * @param bool $onlyDown do not reformat numbers below 1
359 * @param bool $noTrailingZero removes trailing zeros right of the comma (default: true)
361 * @return string the formatted value and its unit
363 public static function formatNumber(
364 float|int|string $value,
365 int $digitsLeft = 3,
366 int $digitsRight = 0,
367 bool $onlyDown = false,
368 bool $noTrailingZero = true,
369 ): string {
370 if ($value == 0) {
371 return '0';
374 if (is_string($value)) {
375 $value = (float) $value;
378 $originalValue = $value;
379 //number_format is not multibyte safe, str_replace is safe
380 if ($digitsLeft === 0) {
381 $value = number_format(
382 (float) $value,
383 $digitsRight,
384 /* l10n: Decimal separator */
385 __('.'),
386 /* l10n: Thousands separator */
387 __(','),
389 if (($originalValue != 0) && (floatval($value) == 0)) {
390 return ' <' . (1 / 10 ** $digitsRight);
393 return $value;
396 // this units needs no translation, ISO
397 $units = [
398 -8 => 'y',
399 -7 => 'z',
400 -6 => 'a',
401 -5 => 'f',
402 -4 => 'p',
403 -3 => 'n',
404 -2 => 'µ',
405 -1 => 'm',
406 0 => ' ',
407 1 => 'k',
408 2 => 'M',
409 3 => 'G',
410 4 => 'T',
411 5 => 'P',
412 6 => 'E',
413 7 => 'Z',
414 8 => 'Y',
416 /* l10n: Decimal separator */
417 $decimalSep = __('.');
418 /* l10n: Thousands separator */
419 $thousandsSep = __(',');
421 // check for negative value to retain sign
422 if ($value < 0) {
423 $sign = '-';
424 $value = abs($value);
425 } else {
426 $sign = '';
429 $dh = 10 ** $digitsRight;
431 // This gives us the right SI prefix already, but $digits_left parameter not incorporated
432 $d = floor(log10((float) $value) / 3);
433 // Lowering the SI prefix by 1 gives us an additional 3 zeros
434 // So if we have 3,6,9,12.. free digits ($digits_left - $cur_digits) to use, then lower the SI prefix
435 $curDigits = floor(log10($value / 1000 ** $d) + 1);
436 if ($digitsLeft > $curDigits) {
437 $d -= floor(($digitsLeft - $curDigits) / 3);
440 if ($d < 0 && $onlyDown) {
441 $d = 0;
444 $value = round($value / (1000 ** $d / $dh)) / $dh;
445 $unit = $units[$d];
447 // number_format is not multibyte safe, str_replace is safe
448 $formattedValue = number_format($value, $digitsRight, $decimalSep, $thousandsSep);
449 // If we don't want any zeros, remove them now
450 if ($noTrailingZero && str_contains($formattedValue, $decimalSep)) {
451 $formattedValue = preg_replace('/' . preg_quote($decimalSep, '/') . '?0+$/', '', $formattedValue);
454 if ($originalValue != 0 && floatval($value) == 0) {
455 return ' <' . number_format(1 / 10 ** $digitsRight, $digitsRight, $decimalSep, $thousandsSep) . ' ' . $unit;
458 return $sign . $formattedValue . ' ' . $unit;
462 * Returns the number of bytes when a formatted size is given
464 * @param string|int $formattedSize the size expression (for example 8MB)
466 * @return int|float The numerical part of the expression (for example 8)
468 public static function extractValueFromFormattedSize(string|int $formattedSize): int|float
470 $returnValue = -1;
472 $formattedSize = (string) $formattedSize;
474 if (preg_match('/^[0-9]+GB$/', $formattedSize)) {
475 $returnValue = (int) mb_substr($formattedSize, 0, -2) * 1024 ** 3;
476 } elseif (preg_match('/^[0-9]+MB$/', $formattedSize)) {
477 $returnValue = (int) mb_substr($formattedSize, 0, -2) * 1024 ** 2;
478 } elseif (preg_match('/^[0-9]+K$/', $formattedSize)) {
479 $returnValue = (int) mb_substr($formattedSize, 0, -1) * 1024 ** 1;
482 return $returnValue;
486 * Writes localised date
488 * @param int $timestamp the current timestamp
489 * @param string $format format
491 * @return string the formatted date
493 public static function localisedDate(int $timestamp = -1, string $format = ''): string
495 $month = [
496 _pgettext('Short month name for January', 'Jan'),
497 _pgettext('Short month name for February', 'Feb'),
498 _pgettext('Short month name for March', 'Mar'),
499 _pgettext('Short month name for April', 'Apr'),
500 _pgettext('Short month name for May', 'May'),
501 _pgettext('Short month name for June', 'Jun'),
502 _pgettext('Short month name for July', 'Jul'),
503 _pgettext('Short month name for August', 'Aug'),
504 _pgettext('Short month name for September', 'Sep'),
505 _pgettext('Short month name for October', 'Oct'),
506 _pgettext('Short month name for November', 'Nov'),
507 _pgettext('Short month name for December', 'Dec'),
509 $dayOfWeek = [
510 _pgettext('Short week day name for Sunday', 'Sun'),
511 _pgettext('Short week day name for Monday', 'Mon'),
512 _pgettext('Short week day name for Tuesday', 'Tue'),
513 _pgettext('Short week day name for Wednesday', 'Wed'),
514 _pgettext('Short week day name for Thursday', 'Thu'),
515 _pgettext('Short week day name for Friday', 'Fri'),
516 _pgettext('Short week day name for Saturday', 'Sat'),
519 if ($format == '') {
520 /* l10n: See https://www.php.net/manual/en/function.strftime.php */
521 $format = __('%B %d, %Y at %I:%M %p');
524 if ($timestamp == -1) {
525 $timestamp = time();
528 $date = (string) preg_replace(
529 '@%[aA]@',
530 // phpcs:ignore Generic.PHP.DeprecatedFunctions
531 $dayOfWeek[(int) @strftime('%w', $timestamp)],
532 $format,
534 $date = (string) preg_replace(
535 '@%[bB]@',
536 // phpcs:ignore Generic.PHP.DeprecatedFunctions
537 $month[(int) @strftime('%m', $timestamp) - 1],
538 $date,
541 /* Fill in AM/PM */
542 $hours = (int) date('H', $timestamp);
543 if ($hours >= 12) {
544 $amPm = _pgettext('AM/PM indication in time', 'PM');
545 } else {
546 $amPm = _pgettext('AM/PM indication in time', 'AM');
549 $date = (string) preg_replace('@%[pP]@', $amPm, $date);
551 // Can return false on windows for Japanese language
552 // See https://github.com/phpmyadmin/phpmyadmin/issues/15830
553 // phpcs:ignore Generic.PHP.DeprecatedFunctions
554 $ret = @strftime($date, $timestamp);
555 // Some OSes such as Win8.1 Traditional Chinese version did not produce UTF-8
556 // output here. See https://github.com/phpmyadmin/phpmyadmin/issues/10598
557 if ($ret === false || mb_detect_encoding($ret, 'UTF-8', true) !== 'UTF-8') {
558 return date('Y-m-d H:i:s', $timestamp);
561 return $ret;
565 * Splits a URL string by parameter
567 * @param string $url the URL
569 * @return array<int, string> the parameter/value pairs, for example [0] db=sakila
571 public static function splitURLQuery(string $url): array
573 // decode encoded url separators
574 $separator = Url::getArgSeparator();
575 // on most places separator is still hard coded ...
576 if ($separator !== '&') {
577 // ... so always replace & with $separator
578 $url = str_replace([htmlentities('&'), '&'], [$separator, $separator], $url);
581 $url = str_replace(htmlentities($separator), $separator, $url);
582 // end decode
584 $urlParts = parse_url($url);
586 if (is_array($urlParts) && isset($urlParts['query']) && strlen($separator) > 0) {
587 return explode($separator, $urlParts['query']);
590 return [];
594 * Returns a given timespan value in a readable format.
596 * @param int $seconds the timespan
598 * @return string the formatted value
600 public static function timespanFormat(int $seconds): string
602 $days = floor($seconds / 86400);
603 if ($days > 0) {
604 $seconds -= $days * 86400;
607 $hours = floor($seconds / 3600);
608 if ($days > 0 || $hours > 0) {
609 $seconds -= $hours * 3600;
612 $minutes = floor($seconds / 60);
613 if ($days > 0 || $hours > 0 || $minutes > 0) {
614 $seconds -= $minutes * 60;
617 return sprintf(
618 __('%s days, %s hours, %s minutes and %s seconds'),
619 (string) $days,
620 (string) $hours,
621 (string) $minutes,
622 (string) $seconds,
627 * Generate the charset query part
629 * @param string $collation Collation
630 * @param bool $override (optional) force 'CHARACTER SET' keyword
632 public static function getCharsetQueryPart(string $collation, bool $override = false): string
634 [$charset] = explode('_', $collation);
635 $keyword = ' CHARSET=';
637 if ($override) {
638 $keyword = ' CHARACTER SET ';
641 return $keyword . $charset
642 . ($charset === $collation ? '' : ' COLLATE ' . $collation);
646 * Generate a pagination selector for browsing resultsets
648 * @param string $name The name for the request parameter
649 * @param int $rows Number of rows in the pagination set
650 * @param int $pageNow current page number
651 * @param int $nbTotalPage number of total pages
652 * @param int $showAll If the number of pages is lower than this
653 * variable, no pages will be omitted in pagination
654 * @param int $sliceStart How many rows at the beginning should always
655 * be shown?
656 * @param int $sliceEnd How many rows at the end should always be shown?
657 * @param int $percent Percentage of calculation page offsets to hop to a
658 * next page
659 * @param int $range Near the current page, how many pages should
660 * be considered "nearby" and displayed as well?
661 * @param string $prompt The prompt to display (sometimes empty)
663 public static function pageselector(
664 string $name,
665 int $rows,
666 int $pageNow = 1,
667 int $nbTotalPage = 1,
668 int $showAll = 200,
669 int $sliceStart = 5,
670 int $sliceEnd = 5,
671 int $percent = 20,
672 int $range = 10,
673 string $prompt = '',
674 ): string {
675 $increment = floor($nbTotalPage / $percent);
676 $pageNowMinusRange = $pageNow - $range;
677 $pageNowPlusRange = $pageNow + $range;
679 $gotoPage = $prompt . ' <select class="pageselector ajax"';
681 $gotoPage .= ' name="' . $name . '" >';
682 if ($nbTotalPage < $showAll) {
683 $pages = range(1, $nbTotalPage);
684 } else {
685 $pages = [];
687 // Always show first X pages
688 for ($i = 1; $i <= $sliceStart; $i++) {
689 $pages[] = $i;
692 // Always show last X pages
693 for ($i = $nbTotalPage - $sliceEnd; $i <= $nbTotalPage; $i++) {
694 $pages[] = $i;
697 // Based on the number of results we add the specified
698 // $percent percentage to each page number,
699 // so that we have a representing page number every now and then to
700 // immediately jump to specific pages.
701 // As soon as we get near our currently chosen page ($pageNow -
702 // $range), every page number will be shown.
703 $i = $sliceStart;
704 $x = $nbTotalPage - $sliceEnd;
705 $metBoundary = false;
707 while ($i <= $x) {
708 if ($i >= $pageNowMinusRange && $i <= $pageNowPlusRange) {
709 // If our pageselector comes near the current page, we use 1
710 // counter increments
711 $i++;
712 $metBoundary = true;
713 } else {
714 // We add the percentage increment to our current page to
715 // hop to the next one in range
716 $i += $increment;
718 // Make sure that we do not cross our boundaries.
719 if ($i > $pageNowMinusRange && ! $metBoundary) {
720 $i = $pageNowMinusRange;
724 if ($i <= 0 || $i > $x) {
725 continue;
728 $pages[] = $i;
732 Add page numbers with "geometrically increasing" distances.
734 This helps me a lot when navigating through giant tables.
736 Test case: table with 2.28 million sets, 76190 pages. Page of interest
737 is between 72376 and 76190.
738 Selecting page 72376.
739 Now, old version enumerated only +/- 10 pages around 72376 and the
740 percentage increment produced steps of about 3000.
742 The following code adds page numbers +/- 2,4,8,16,32,64,128,256 etc.
743 around the current page.
745 $i = $pageNow;
746 $dist = 1;
747 while ($i < $x) {
748 $dist = 2 * $dist;
749 $i = $pageNow + $dist;
750 if ($i <= 0 || $i > $x) {
751 continue;
754 $pages[] = $i;
757 $i = $pageNow;
758 $dist = 1;
759 while ($i > 0) {
760 $dist = 2 * $dist;
761 $i = $pageNow - $dist;
762 if ($i <= 0 || $i > $x) {
763 continue;
766 $pages[] = $i;
769 // Since because of ellipsing of the current page some numbers may be
770 // double, we unify our array:
771 sort($pages);
772 $pages = array_unique($pages);
775 if ($pageNow > $nbTotalPage) {
776 $pages[] = $pageNow;
779 foreach ($pages as $i) {
780 if ($i == $pageNow) {
781 $selected = 'selected="selected" style="font-weight: bold"';
782 } else {
783 $selected = '';
786 $gotoPage .= ' <option ' . $selected
787 . ' value="' . (($i - 1) * $rows) . '">' . $i . '</option>' . "\n";
790 $gotoPage .= ' </select>';
792 return $gotoPage;
796 * Calculate page number through position
798 * @param int $pos position of first item
799 * @param int $maxCount number of items per page
801 * @return int $page_num
803 public static function getPageFromPosition(int $pos, int $maxCount): int
805 return (int) floor($pos / $maxCount) + 1;
809 * replaces %u in given path with current user name
811 * example:
812 * <code>
813 * $user_dir = userDir('/var/pma_tmp/%u/'); // '/var/pma_tmp/root/'
815 * </code>
817 * @param string $dir with wildcard for user
819 * @return string per user directory
821 public static function userDir(string $dir): string
823 // add trailing slash
824 if (mb_substr($dir, -1) !== '/') {
825 $dir .= '/';
828 return str_replace('%u', Core::securePath(Config::getInstance()->selectedServer['user']), $dir);
832 * Clears cache content which needs to be refreshed on user change.
834 public static function clearUserCache(): void
836 SessionCache::remove('is_superuser');
837 SessionCache::remove('is_createuser');
838 SessionCache::remove('is_grantuser');
839 SessionCache::remove('mysql_cur_user');
843 * Converts a bit value to printable format;
844 * in MySQL a BIT field can be from 1 to 64 bits so we need this
845 * function because in PHP, decbin() supports only 32 bits
846 * on 32-bit servers
848 * @param int $value coming from a BIT field
849 * @param int $length length
851 * @return string the printable value
853 public static function printableBitValue(int $value, int $length): string
855 // if running on a 64-bit server or the length is safe for decbin()
856 if (PHP_INT_SIZE == 8 || $length < 33) {
857 $printable = decbin($value);
858 } else {
859 // FIXME: does not work for the leftmost bit of a 64-bit value
860 $i = 0;
861 $printable = '';
862 while ($value >= 2 ** $i) {
863 ++$i;
866 if ($i != 0) {
867 --$i;
870 while ($i >= 0) {
871 if ($value - 2 ** $i < 0) {
872 $printable = '0' . $printable;
873 } else {
874 $printable = '1' . $printable;
875 $value -= 2 ** $i;
878 --$i;
881 $printable = strrev($printable);
884 return str_pad($printable, $length, '0', STR_PAD_LEFT);
888 * Converts a BIT type default value
889 * for example, b'010' becomes 010
891 * @param string|null $bitDefaultValue value
893 * @return string the converted value
895 public static function convertBitDefaultValue(string|null $bitDefaultValue): string
897 return (string) preg_replace(
898 "/^b'(\d*)'?$/",
899 '$1',
900 htmlspecialchars_decode((string) $bitDefaultValue, ENT_QUOTES),
906 * Extracts the various parts from a column spec
908 * @param string $columnSpecification Column specification
910 * @return mixed[] associative array containing type, spec_in_brackets
911 * and possibly enum_set_values (another array)
913 public static function extractColumnSpec(string $columnSpecification): array
915 $firstBracketPos = mb_strpos($columnSpecification, '(');
916 if ($firstBracketPos) {
917 $specInBrackets = rtrim(
918 mb_substr(
919 $columnSpecification,
920 $firstBracketPos + 1,
921 mb_strrpos($columnSpecification, ')') - $firstBracketPos - 1,
924 // convert to lowercase just to be sure
925 $type = mb_strtolower(
926 rtrim(mb_substr($columnSpecification, 0, $firstBracketPos)),
928 } else {
929 // Split trailing attributes such as unsigned,
930 // binary, zerofill and get data type name
931 $typeParts = explode(' ', $columnSpecification);
932 $type = mb_strtolower($typeParts[0]);
933 $specInBrackets = '';
936 if ($type === 'enum' || $type === 'set') {
937 // Define our working vars
938 $enumSetValues = self::parseEnumSetValues($columnSpecification, false);
939 $printType = $type
940 . '(' . str_replace("','", "', '", $specInBrackets) . ')';
941 $binary = false;
942 $unsigned = false;
943 $zerofill = false;
944 $compressed = false;
945 } else {
946 $enumSetValues = [];
948 /* Create printable type name */
949 $printType = mb_strtolower($columnSpecification);
951 // Strip the "BINARY" attribute, except if we find "BINARY(" because
952 // this would be a BINARY or VARBINARY column type;
953 // by the way, a BLOB should not show the BINARY attribute
954 // because this is not accepted in MySQL syntax.
955 if (str_contains($printType, 'binary') && ! preg_match('@binary[\(]@', $printType)) {
956 $printType = str_replace('binary', '', $printType);
957 $binary = true;
958 } else {
959 $binary = false;
962 $printType = (string) preg_replace('@zerofill@', '', $printType, -1, $zerofillCount);
963 $zerofill = ($zerofillCount > 0);
964 $printType = (string) preg_replace('@unsigned@', '', $printType, -1, $unsignedCount);
965 $unsigned = ($unsignedCount > 0);
966 $printType = (string) preg_replace('@\/\*!100301 compressed\*\/@', '', $printType, -1, $compressedCount);
967 $compressed = ($compressedCount > 0);
968 $printType = trim($printType);
971 $attribute = ' ';
972 if ($binary) {
973 $attribute = 'BINARY';
976 if ($unsigned) {
977 $attribute = 'UNSIGNED';
980 if ($zerofill) {
981 $attribute = 'UNSIGNED ZEROFILL';
984 if ($compressed) {
985 // With InnoDB page compression, multiple compression algorithms are supported.
986 // In contrast, with InnoDB's COMPRESSED row format, zlib is the only supported compression algorithm.
987 // This means that the COMPRESSED row format has less compression options than InnoDB page compression does.
988 // @see https://mariadb.com/kb/en/innodb-page-compression/#comparison-with-the-compressed-row-format
989 $attribute = 'COMPRESSED=zlib';
992 $canContainCollation = false;
993 if (! $binary && preg_match('@^(char|varchar|text|tinytext|mediumtext|longtext|set|enum)@', $type)) {
994 $canContainCollation = true;
997 // for the case ENUM('&#8211;','&ldquo;')
998 $displayedType = htmlspecialchars($printType, ENT_COMPAT);
999 $config = Config::getInstance();
1000 if (mb_strlen($printType) > $config->settings['LimitChars']) {
1001 $displayedType = '<abbr title="' . htmlspecialchars($printType) . '">';
1002 $displayedType .= htmlspecialchars(
1003 mb_substr(
1004 $printType,
1006 $config->settings['LimitChars'],
1007 ) . '...',
1008 ENT_COMPAT,
1010 $displayedType .= '</abbr>';
1013 return [
1014 'type' => $type,
1015 'spec_in_brackets' => $specInBrackets,
1016 'enum_set_values' => $enumSetValues,
1017 'print_type' => $printType,
1018 'binary' => $binary,
1019 'unsigned' => $unsigned,
1020 'zerofill' => $zerofill,
1021 'attribute' => $attribute,
1022 'can_contain_collation' => $canContainCollation,
1023 'displayed_type' => $displayedType,
1028 * If the string starts with a \r\n pair (0x0d0a) add an extra \n
1030 * @return string with the chars replaced
1032 public static function duplicateFirstNewline(string $string): string
1034 $firstOccurrence = mb_strpos($string, "\r\n");
1035 if ($firstOccurrence === 0) {
1036 return "\n" . $string;
1039 return $string;
1043 * Get the action word corresponding to a script name
1044 * in order to display it as a title in navigation panel
1046 * @param string $target a valid value for $cfg['NavigationTreeDefaultTabTable'],
1047 * $cfg['NavigationTreeDefaultTabTable2'],
1048 * $cfg['DefaultTabTable'] or $cfg['DefaultTabDatabase']
1050 * @return string|bool Title for the $cfg value
1052 public static function getTitleForTarget(string $target): string|bool
1054 $mapping = [
1055 'structure' => __('Structure'),
1056 'sql' => __('SQL'),
1057 'search' => __('Search'),
1058 'insert' => __('Insert'),
1059 'browse' => __('Browse'),
1060 'operations' => __('Operations'),
1063 return $mapping[$target] ?? false;
1067 * Get the script name corresponding to a plain English config word
1068 * in order to append in links on navigation and main panel
1070 * @param string $target a valid value for
1071 * $cfg['NavigationTreeDefaultTabTable'],
1072 * $cfg['NavigationTreeDefaultTabTable2'],
1073 * $cfg['DefaultTabTable'], $cfg['DefaultTabDatabase'] or
1074 * $cfg['DefaultTabServer']
1075 * @param string $location one out of 'server', 'table', 'database'
1077 * @return string script name corresponding to the config word
1079 public static function getScriptNameForOption(string $target, string $location): string
1081 return Url::getFromRoute(self::getUrlForOption($target, $location));
1085 * Get the URL corresponding to a plain English config word
1086 * in order to append in links on navigation and main panel
1088 * @param string $target a valid value for
1089 * $cfg['NavigationTreeDefaultTabTable'],
1090 * $cfg['NavigationTreeDefaultTabTable2'],
1091 * $cfg['DefaultTabTable'], $cfg['DefaultTabDatabase'] or
1092 * $cfg['DefaultTabServer']
1093 * @param string $location one out of 'server', 'table', 'database'
1095 * @return string The URL corresponding to the config word
1097 public static function getUrlForOption(string $target, string $location): string
1099 if ($location === 'server') {
1100 // Values for $cfg['DefaultTabServer']
1101 switch ($target) {
1102 case 'welcome':
1103 case 'index.php':
1104 return '/';
1106 case 'databases':
1107 case 'server_databases.php':
1108 return '/server/databases';
1110 case 'status':
1111 case 'server_status.php':
1112 return '/server/status';
1114 case 'variables':
1115 case 'server_variables.php':
1116 return '/server/variables';
1118 case 'privileges':
1119 case 'server_privileges.php':
1120 return '/server/privileges';
1122 } elseif ($location === 'database') {
1123 // Values for $cfg['DefaultTabDatabase']
1124 switch ($target) {
1125 case 'structure':
1126 case 'db_structure.php':
1127 return '/database/structure';
1129 case 'sql':
1130 case 'db_sql.php':
1131 return '/database/sql';
1133 case 'search':
1134 case 'db_search.php':
1135 return '/database/search';
1137 case 'operations':
1138 case 'db_operations.php':
1139 return '/database/operations';
1141 } elseif ($location === 'table') {
1142 // Values for $cfg['DefaultTabTable'],
1143 // $cfg['NavigationTreeDefaultTabTable'] and
1144 // $cfg['NavigationTreeDefaultTabTable2']
1145 switch ($target) {
1146 case 'structure':
1147 case 'tbl_structure.php':
1148 return '/table/structure';
1150 case 'sql':
1151 case 'tbl_sql.php':
1152 return '/table/sql';
1154 case 'search':
1155 case 'tbl_select.php':
1156 return '/table/search';
1158 case 'insert':
1159 case 'tbl_change.php':
1160 return '/table/change';
1162 case 'browse':
1163 case 'sql.php':
1164 return '/sql';
1168 return '/';
1172 * Formats user string, expanding @VARIABLES@, accepting strftime format
1173 * string.
1175 * @param string $string Text where to do expansion.
1176 * @param callable|null $escape Function to call for escaping variable values.
1177 * @param array<string, string|null> $updates Array with overrides for default parameters (obtained from GLOBALS).
1178 * @psalm-param callable(string):string|null $escape
1180 public static function expandUserString(
1181 string $string,
1182 callable|null $escape = null,
1183 array $updates = [],
1184 ): string {
1185 /* Content */
1186 $vars = [];
1187 $vars['http_host'] = Core::getEnv('HTTP_HOST');
1188 $config = Config::getInstance();
1189 $vars['server_name'] = $config->selectedServer['host'];
1190 $vars['server_verbose'] = $config->selectedServer['verbose'];
1192 if (empty($config->selectedServer['verbose'])) {
1193 $vars['server_verbose_or_name'] = $config->selectedServer['host'];
1194 } else {
1195 $vars['server_verbose_or_name'] = $config->selectedServer['verbose'];
1198 $vars['database'] = Current::$database;
1199 $vars['table'] = Current::$table;
1200 $vars['phpmyadmin_version'] = 'phpMyAdmin ' . Version::VERSION;
1202 /* Update forced variables */
1203 foreach ($updates as $key => $val) {
1204 $vars[$key] = $val;
1208 * Replacement mapping
1210 * The __VAR__ ones are for backward compatibility, because user might still have it in cookies.
1212 $replace = [
1213 '@HTTP_HOST@' => $vars['http_host'],
1214 '@SERVER@' => $vars['server_name'],
1215 '__SERVER__' => $vars['server_name'],
1216 '@VERBOSE@' => $vars['server_verbose'],
1217 '@VSERVER@' => $vars['server_verbose_or_name'],
1218 '@DATABASE@' => $vars['database'],
1219 '__DB__' => $vars['database'],
1220 '@TABLE@' => $vars['table'],
1221 '__TABLE__' => $vars['table'],
1222 '@PHPMYADMIN@' => $vars['phpmyadmin_version'],
1225 /* Optional escaping */
1226 if ($escape !== null) {
1227 $replace = array_map($escape, $replace);
1230 /* Backward compatibility in 3.5.x */
1231 if (str_contains($string, '@FIELDS@')) {
1232 $string = strtr($string, ['@FIELDS@' => '@COLUMNS@']);
1235 /* Fetch columns list if required */
1236 if (str_contains($string, '@COLUMNS@')) {
1237 $columnsList = DatabaseInterface::getInstance()->getColumnNames(Current::$database, Current::$table);
1239 $columnNames = [];
1240 if ($escape !== null) {
1241 foreach ($columnsList as $column) {
1242 $columnNames[] = self::$escape($column);
1244 } else {
1245 $columnNames = $columnsList;
1248 $replace['@COLUMNS@'] = implode(',', $columnNames);
1251 /* Do the replacement */
1252 // phpcs:ignore Generic.PHP.DeprecatedFunctions
1253 return strtr((string) @strftime($string), $replace);
1257 * This function processes the datatypes supported by the DB,
1258 * as specified in Types->getColumns() and returns an array
1259 * (useful for quickly checking if a datatype is supported).
1261 * @return string[] An array of datatypes.
1263 public static function getSupportedDatatypes(): array
1265 $retval = [];
1266 foreach (DatabaseInterface::getInstance()->types->getColumns() as $value) {
1267 if (is_array($value)) {
1268 foreach ($value as $subvalue) {
1269 if ($subvalue === '-') {
1270 continue;
1273 $retval[] = $subvalue;
1275 } elseif ($value !== '-') {
1276 $retval[] = $value;
1280 return $retval;
1284 * Returns a list of datatypes that are not (yet) handled by PMA.
1285 * Used by: /table/change and libraries/Routines.php
1287 * @return string[] list of datatypes
1289 public static function unsupportedDatatypes(): array
1291 return [];
1295 * This function is to check whether database support UUID
1297 public static function isUUIDSupported(): bool
1299 return Compatibility::isUUIDSupported(DatabaseInterface::getInstance());
1303 * Checks if the current user has a specific privilege and returns true if the
1304 * user indeed has that privilege or false if they don't. This function must
1305 * only be used for features that are available since MySQL 5, because it
1306 * relies on the INFORMATION_SCHEMA database to be present.
1308 * Example: currentUserHasPrivilege('CREATE ROUTINE', 'mydb');
1309 * // Checks if the currently logged in user has the global
1310 * // 'CREATE ROUTINE' privilege or, if not, checks if the
1311 * // user has this privilege on database 'mydb'.
1313 * @param string $priv The privilege to check
1314 * @param string|null $db null, to only check global privileges
1315 * string, db name where to also check
1316 * for privileges
1317 * @param string|null $tbl null, to only check global/db privileges
1318 * string, table name where to also check
1319 * for privileges
1321 public static function currentUserHasPrivilege(string $priv, string|null $db = null, string|null $tbl = null): bool
1323 $dbi = DatabaseInterface::getInstance();
1324 // Get the username for the current user in the format
1325 // required to use in the information schema database.
1326 [$user, $host] = $dbi->getCurrentUserAndHost();
1328 // MySQL is started with --skip-grant-tables
1329 if ($user === '') {
1330 return true;
1333 $username = "''";
1334 $username .= str_replace("'", "''", $user);
1335 $username .= "''@''";
1336 $username .= str_replace("'", "''", $host);
1337 $username .= "''";
1339 // Prepare the query
1340 $query = 'SELECT `PRIVILEGE_TYPE` FROM `INFORMATION_SCHEMA`.`%s` '
1341 . "WHERE GRANTEE='%s' AND PRIVILEGE_TYPE='%s'";
1343 // Check global privileges first.
1344 $userPrivileges = $dbi->fetchValue(
1345 sprintf(
1346 $query,
1347 'USER_PRIVILEGES',
1348 $username,
1349 $priv,
1352 if ($userPrivileges) {
1353 return true;
1356 // If a database name was provided and user does not have the
1357 // required global privilege, try database-wise permissions.
1358 if ($db === null) {
1359 // There was no database name provided and the user
1360 // does not have the correct global privilege.
1361 return false;
1364 $query .= ' AND %s LIKE `TABLE_SCHEMA`';
1365 $schemaPrivileges = $dbi->fetchValue(
1366 sprintf(
1367 $query,
1368 'SCHEMA_PRIVILEGES',
1369 $username,
1370 $priv,
1371 $dbi->quoteString($db),
1374 if ($schemaPrivileges) {
1375 return true;
1378 // If a table name was also provided and we still didn't
1379 // find any valid privileges, try table-wise privileges.
1380 if ($tbl !== null) {
1381 $query .= ' AND TABLE_NAME=%s';
1382 $tablePrivileges = $dbi->fetchValue(
1383 sprintf(
1384 $query,
1385 'TABLE_PRIVILEGES',
1386 $username,
1387 $priv,
1388 $dbi->quoteString($db),
1389 $dbi->quoteString($tbl),
1392 if ($tablePrivileges) {
1393 return true;
1398 * If we reached this point, the user does not
1399 * have even valid table-wise privileges.
1401 return false;
1405 * Returns server type for current connection
1407 * Known types are: MariaDB, Percona Server and MySQL (default)
1409 * @phpstan-return 'MariaDB'|'Percona Server'|'MySQL'
1411 public static function getServerType(): string
1413 $dbi = DatabaseInterface::getInstance();
1414 if ($dbi->isMariaDB()) {
1415 return 'MariaDB';
1418 if ($dbi->isPercona()) {
1419 return 'Percona Server';
1422 return 'MySQL';
1426 * Parses ENUM/SET values
1428 * @param string $definition The definition of the column
1429 * for which to parse the values
1430 * @param bool $escapeHtml Whether to escape html entities
1432 * @return string[]
1434 public static function parseEnumSetValues(string $definition, bool $escapeHtml = true): array
1436 // There is a JS port of the below parser in functions.js
1437 // If you are fixing something here,
1438 // you need to also update the JS port.
1440 // This should really be delegated to MySQL but since we also want to HTML encode it,
1441 // it is easier this way.
1442 // It future replace str_getcsv with $dbi->fetchSingleRow('SELECT '.$expressionInBrackets[1]);
1444 preg_match('/\((.*)\)/', $definition, $expressionInBrackets);
1445 $matches = str_getcsv($expressionInBrackets[1], ',', "'");
1447 $values = [];
1448 foreach ($matches as $value) {
1449 $value = strtr($value, ['\\\\' => '\\']); // str_getcsv doesn't unescape backslashes so we do it ourselves
1450 $values[] = $escapeHtml ? htmlspecialchars($value, ENT_QUOTES, 'UTF-8') : $value;
1453 return $values;
1457 * Return the list of tabs for the menu with corresponding names
1459 * @param string|null $level 'server', 'db' or 'table' level
1461 * @return mixed[]|null list of tabs for the menu
1463 public static function getMenuTabList(string|null $level = null): array|null
1465 $tabList = [
1466 '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 'db' => [
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 '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'),
1512 if ($level === null) {
1513 return $tabList;
1516 if (array_key_exists($level, $tabList)) {
1517 return $tabList[$level];
1520 return null;
1524 * Add fractional seconds to time, datetime and timestamp strings.
1525 * If the string contains fractional seconds,
1526 * pads it with 0s up to 6 decimal places.
1528 * @param string $value time, datetime or timestamp strings
1530 * @return string time, datetime or timestamp strings with fractional seconds
1532 public static function addMicroseconds(string $value): string
1534 if ($value === '' || $value === 'CURRENT_TIMESTAMP' || $value === 'current_timestamp()') {
1535 return $value;
1538 if (! str_contains($value, '.')) {
1539 return $value . '.000000';
1542 $value .= '000000';
1544 return mb_substr(
1545 $value,
1547 mb_strpos($value, '.') + 7,
1552 * Reads the file, detects the compression MIME type, closes the file
1553 * and returns the MIME type
1555 * @param resource $file the file handle
1557 * @return string the MIME type for compression, or 'none'
1559 public static function getCompressionMimeType($file): string
1561 $test = fread($file, 4);
1563 if ($test === false) {
1564 fclose($file);
1566 return 'none';
1569 $len = strlen($test);
1570 fclose($file);
1571 if ($len >= 2 && $test[0] === chr(31) && $test[1] === chr(139)) {
1572 return 'application/gzip';
1575 if ($len >= 3 && str_starts_with($test, 'BZh')) {
1576 return 'application/bzip2';
1579 if ($len >= 4 && $test == "PK\003\004") {
1580 return 'application/zip';
1583 return 'none';
1587 * Provide COLLATE clause, if required, to perform case sensitive comparisons
1588 * for queries on information_schema.
1590 * @return string COLLATE clause if needed or empty string.
1592 public static function getCollateForIS(): string
1594 $names = DatabaseInterface::getInstance()->getLowerCaseNames();
1595 if ($names === 0) {
1596 return 'COLLATE utf8_bin';
1599 if ($names === 2) {
1600 return 'COLLATE utf8_general_ci';
1603 return '';
1607 * Process the index data.
1609 * @param mixed[] $indexes index data
1611 * @return mixed[] processes index data
1613 public static function processIndexData(array $indexes): array
1615 $lastIndex = '';
1617 $primary = '';
1618 $pkArray = []; // will be use to emphasis prim. keys in the table
1619 $indexesInfo = [];
1620 $indexesData = [];
1622 // view
1623 foreach ($indexes as $row) {
1624 // Backups the list of primary keys
1625 if ($row['Key_name'] === 'PRIMARY') {
1626 $primary .= $row['Column_name'] . ', ';
1627 $pkArray[$row['Column_name']] = 1;
1630 // Retains keys informations
1631 if ($row['Key_name'] != $lastIndex) {
1632 $lastIndex = $row['Key_name'];
1635 $indexesInfo[$row['Key_name']]['Sequences'][] = $row['Seq_in_index'];
1636 $indexesInfo[$row['Key_name']]['Non_unique'] = $row['Non_unique'];
1637 if (isset($row['Cardinality'])) {
1638 $indexesInfo[$row['Key_name']]['Cardinality'] = $row['Cardinality'];
1641 // I don't know what does following column mean....
1642 // $indexes_info[$row['Key_name']]['Packed'] = $row['Packed'];
1644 $indexesInfo[$row['Key_name']]['Comment'] = $row['Comment'];
1646 $indexesData[$row['Key_name']][$row['Seq_in_index']]['Column_name'] = $row['Column_name'];
1647 if (! isset($row['Sub_part'])) {
1648 continue;
1651 $indexesData[$row['Key_name']][$row['Seq_in_index']]['Sub_part'] = $row['Sub_part'];
1654 return [$primary, $pkArray, $indexesInfo, $indexesData];
1658 * Gets the list of tables in the current db and information about these tables if possible.
1660 * @return array<int, array|int>
1661 * @psalm-return array{array, int}
1663 public static function getDbInfo(ServerRequest $request, string $db, bool $isResultLimited = true): array
1666 * information about tables in db
1668 $tables = [];
1670 $dbi = DatabaseInterface::getInstance();
1671 $config = Config::getInstance();
1672 // Special speedup for newer MySQL Versions (in 4.0 format changed)
1673 if ($config->settings['SkipLockedTables'] === true) {
1674 $tables = self::getTablesWhenOpen($db);
1677 $totalNumTables = null;
1678 if ($tables === []) {
1679 // Set some sorting defaults
1680 $sort = 'Name';
1681 $sortOrder = 'ASC';
1683 /** @var mixed $sortParam */
1684 $sortParam = $request->getParam('sort');
1685 if (is_string($sortParam)) {
1686 $sortableNameMappings = [
1687 'table' => 'Name',
1688 'records' => 'Rows',
1689 'type' => 'Engine',
1690 'collation' => 'Collation',
1691 'size' => 'Data_length',
1692 'overhead' => 'Data_free',
1693 'creation' => 'Create_time',
1694 'last_update' => 'Update_time',
1695 'last_check' => 'Check_time',
1696 'comment' => 'Comment',
1699 // Make sure the sort type is implemented
1700 if (isset($sortableNameMappings[$sortParam])) {
1701 $sort = $sortableNameMappings[$sortParam];
1702 if ($request->getParam('sort_order') === 'DESC') {
1703 $sortOrder = 'DESC';
1708 $groupWithSeparator = false;
1709 $tableType = null;
1710 $limitOffset = 0;
1711 $limitCount = false;
1712 $groupTable = [];
1714 /** @var mixed $tableGroupParam */
1715 $tableGroupParam = $request->getParam('tbl_group');
1716 /** @var mixed $tableTypeParam */
1717 $tableTypeParam = $request->getParam('tbl_type');
1718 if (
1719 is_string($tableGroupParam) && $tableGroupParam !== ''
1720 || is_string($tableTypeParam) && $tableTypeParam !== ''
1722 if (is_string($tableTypeParam) && $tableTypeParam !== '') {
1723 // only tables for selected type
1724 $tableType = $tableTypeParam;
1727 if (is_string($tableGroupParam) && $tableGroupParam !== '') {
1728 // only tables for selected group
1729 // include the table with the exact name of the group if such exists
1730 $groupTable = $dbi->getTablesFull(
1731 $db,
1732 $tableGroupParam,
1733 false,
1735 false,
1736 $sort,
1737 $sortOrder,
1738 $tableType,
1740 $groupWithSeparator = $tableGroupParam . $config->settings['NavigationTreeTableSeparator'];
1742 } else {
1743 // all tables in db
1744 // - get the total number of tables
1745 // (needed for proper working of the MaxTableList feature)
1746 $totalNumTables = count($dbi->getTables($db));
1747 if ($isResultLimited) {
1748 // fetch the details for a possible limited subset
1749 $limitOffset = self::getTableListPosition($request, $db);
1750 $limitCount = true;
1754 // We must use union operator here instead of array_merge to preserve numerical keys
1755 $tables = $groupTable + $dbi->getTablesFull(
1756 $db,
1757 $groupWithSeparator !== false ? $groupWithSeparator : '',
1758 $groupWithSeparator !== false,
1759 $limitOffset,
1760 $limitCount,
1761 $sort,
1762 $sortOrder,
1763 $tableType,
1767 return [
1768 $tables,
1769 $totalNumTables ?? count($tables), // needed for proper working of the MaxTableList feature
1774 * Gets the list of tables in the current db, taking into account
1775 * that they might be "in use"
1777 * @return mixed[] list of tables
1779 private static function getTablesWhenOpen(string $db): array
1781 $dbi = DatabaseInterface::getInstance();
1783 $openTables = $dbi->query(
1784 'SHOW OPEN TABLES FROM ' . self::backquote($db) . ' WHERE In_use > 0;',
1787 // Blending out tables in use
1788 $openTableNames = [];
1790 /** @var string $tableName */
1791 foreach ($openTables as ['Table' => $tableName]) {
1792 $openTableNames[] = $tableName;
1795 // is there at least one "in use" table?
1796 if ($openTableNames === []) {
1797 return [];
1800 $tables = [];
1801 $tblGroupSql = '';
1802 $whereAdded = false;
1803 $config = Config::getInstance();
1804 if (
1805 isset($_REQUEST['tbl_group'])
1806 && is_scalar($_REQUEST['tbl_group'])
1807 && strlen((string) $_REQUEST['tbl_group']) > 0
1809 $group = $dbi->escapeMysqlWildcards((string) $_REQUEST['tbl_group']);
1810 $groupWithSeparator = $dbi->escapeMysqlWildcards(
1811 $_REQUEST['tbl_group'] . $config->settings['NavigationTreeTableSeparator'],
1813 $tblGroupSql .= ' WHERE ('
1814 . self::backquote('Tables_in_' . $db)
1815 . ' LIKE ' . $dbi->quoteString($groupWithSeparator . '%')
1816 . ' OR '
1817 . self::backquote('Tables_in_' . $db)
1818 . ' LIKE ' . $dbi->quoteString($group) . ')';
1819 $whereAdded = true;
1822 if (isset($_REQUEST['tbl_type']) && in_array($_REQUEST['tbl_type'], ['table', 'view'], true)) {
1823 $tblGroupSql .= $whereAdded ? ' AND' : ' WHERE';
1824 if ($_REQUEST['tbl_type'] === 'view') {
1825 $tblGroupSql .= " `Table_type` NOT IN ('BASE TABLE', 'SYSTEM VERSIONED')";
1826 } else {
1827 $tblGroupSql .= " `Table_type` IN ('BASE TABLE', 'SYSTEM VERSIONED')";
1831 $dbInfoResult = $dbi->query('SHOW FULL TABLES FROM ' . self::backquote($db) . $tblGroupSql);
1833 if ($dbInfoResult->numRows() > 0) {
1834 $names = [];
1835 while ($tableName = $dbInfoResult->fetchValue()) {
1836 if (! in_array($tableName, $openTableNames, true)) {
1837 $names[] = $tableName;
1838 } else { // table in use
1839 $tables[$tableName] = [
1840 'TABLE_NAME' => $tableName,
1841 'ENGINE' => '',
1842 'TABLE_TYPE' => '',
1843 'TABLE_ROWS' => 0,
1844 'TABLE_COMMENT' => '',
1849 if ($names !== []) {
1850 $tables += $dbi->getTablesFull($db, $names);
1853 if ($config->settings['NaturalOrder']) {
1854 uksort($tables, strnatcasecmp(...));
1858 return $tables;
1862 * Returns list of used PHP extensions.
1864 * @return string[]
1866 public static function listPHPExtensions(): array
1868 $result = [];
1869 if (function_exists('mysqli_connect')) {
1870 $result[] = 'mysqli';
1873 if (extension_loaded('curl')) {
1874 $result[] = 'curl';
1877 if (extension_loaded('mbstring')) {
1878 $result[] = 'mbstring';
1881 if (extension_loaded('sodium')) {
1882 $result[] = 'sodium';
1885 return $result;
1889 * Converts given (request) parameter to string
1891 * @param mixed $value Value to convert
1893 public static function requestString(mixed $value): string
1895 while (is_array($value) || is_object($value)) {
1896 if (is_object($value)) {
1897 $value = (array) $value;
1900 $value = reset($value);
1903 return trim((string) $value);
1907 * Generates random string consisting of ASCII chars
1909 * @param int $length Length of string
1910 * @param bool $asHex (optional) Send the result as hex
1912 public static function generateRandom(int $length, bool $asHex = false): string
1914 $result = '';
1916 /** @infection-ignore-all */
1917 while (strlen($result) < $length) {
1918 // Get random byte and strip highest bit
1919 // to get ASCII only range
1920 $byte = ord(random_bytes(1)) & 0x7f;
1921 // We want only ASCII chars and no DEL character (127)
1922 if ($byte <= 32 || $byte === 127) {
1923 continue;
1926 $result .= chr($byte);
1929 return $asHex ? bin2hex($result) : $result;
1933 * Wrapper around PHP date function
1935 * @param string $format Date format string
1937 public static function date(string $format): string
1939 return date($format);
1943 * Wrapper around php's set_time_limit
1945 public static function setTimeLimit(): void
1947 // The function can be disabled in php.ini
1948 if (! function_exists('set_time_limit')) {
1949 return;
1952 @set_time_limit(Config::getInstance()->settings['ExecTimeLimit']);
1956 * Access to a multidimensional array recursively by the keys specified in $path
1958 * @param mixed[] $array List of values
1959 * @param (int|string)[] $path Path to searched value
1960 * @param mixed $default Default value
1962 * @return mixed Searched value
1964 public static function getValueByKey(array $array, array $path, mixed $default = null): mixed
1966 foreach ($path as $key) {
1967 if (! array_key_exists($key, $array)) {
1968 return $default;
1971 $array = $array[$key];
1974 return $array;
1978 * Creates a clickable column header for table information
1980 * @param string $title Title to use for the link
1981 * @param string $sort Corresponds to sortable data name mapped
1982 * in Util::getDbInfo
1983 * @param string $initialSortOrder Initial sort order
1985 * @return string Link to be displayed in the table header
1987 public static function sortableTableHeader(string $title, string $sort, string $initialSortOrder = 'ASC'): string
1989 $requestedSort = 'table';
1990 $requestedSortOrder = $futureSortOrder = $initialSortOrder;
1991 // If the user requested a sort
1992 if (isset($_REQUEST['sort'])) {
1993 $requestedSort = $_REQUEST['sort'];
1994 if (isset($_REQUEST['sort_order'])) {
1995 $requestedSortOrder = $_REQUEST['sort_order'];
1999 $orderImg = '';
2000 $orderLinkParams = [];
2001 $orderLinkParams['title'] = __('Sort');
2002 // If this column was requested to be sorted.
2003 if ($requestedSort == $sort) {
2004 if ($requestedSortOrder === 'ASC') {
2005 $futureSortOrder = 'DESC';
2006 // current sort order is ASC
2007 $orderImg = ' ' . Generator::getImage(
2008 's_asc',
2009 __('Ascending'),
2010 ['class' => 'sort_arrow', 'title' => ''],
2012 $orderImg .= ' ' . Generator::getImage(
2013 's_desc',
2014 __('Descending'),
2015 ['class' => 'sort_arrow hide', 'title' => ''],
2017 // but on mouse over, show the reverse order (DESC)
2018 $orderLinkParams['onmouseover'] = "$('.sort_arrow').toggle();";
2019 // on mouse out, show current sort order (ASC)
2020 $orderLinkParams['onmouseout'] = "$('.sort_arrow').toggle();";
2021 } else {
2022 $futureSortOrder = 'ASC';
2023 // current sort order is DESC
2024 $orderImg = ' ' . Generator::getImage(
2025 's_asc',
2026 __('Ascending'),
2027 ['class' => 'sort_arrow hide', 'title' => ''],
2029 $orderImg .= ' ' . Generator::getImage(
2030 's_desc',
2031 __('Descending'),
2032 ['class' => 'sort_arrow', 'title' => ''],
2034 // but on mouse over, show the reverse order (ASC)
2035 $orderLinkParams['onmouseover'] = "$('.sort_arrow').toggle();";
2036 // on mouse out, show current sort order (DESC)
2037 $orderLinkParams['onmouseout'] = "$('.sort_arrow').toggle();";
2041 $urlParams = [
2042 'db' => $_REQUEST['db'],
2043 'pos' => 0, // We set the position back to 0 every time they sort.
2044 'sort' => $sort,
2045 'sort_order' => $futureSortOrder,
2048 if (isset($_REQUEST['tbl_type']) && in_array($_REQUEST['tbl_type'], ['view', 'table'], true)) {
2049 $urlParams['tbl_type'] = $_REQUEST['tbl_type'];
2052 if (! empty($_REQUEST['tbl_group'])) {
2053 $urlParams['tbl_group'] = $_REQUEST['tbl_group'];
2056 $url = Url::getFromRoute('/database/structure');
2058 return Generator::linkOrButton($url, $urlParams, $title . $orderImg, $orderLinkParams);
2062 * Check that input is an int or an int in a string
2064 * @param mixed $input input to check
2066 public static function isInteger(mixed $input): bool
2068 return is_scalar($input) && ctype_digit((string) $input);
2072 * Get the protocol from the RFC 7239 Forwarded header
2074 * @param string $headerContents The Forwarded header contents
2076 * @return string the protocol http/https
2078 public static function getProtoFromForwardedHeader(string $headerContents): string
2080 if (str_contains($headerContents, '=')) {// does not contain any equal sign
2081 $hops = explode(',', $headerContents);
2082 $parts = explode(';', $hops[0]);
2083 foreach ($parts as $part) {
2084 $keyValueArray = explode('=', $part, 2);
2085 if (count($keyValueArray) !== 2) {
2086 continue;
2089 [$keyName, $value] = $keyValueArray;
2090 $value = trim(strtolower($value));
2091 if (strtolower(trim($keyName)) === 'proto' && in_array($value, ['http', 'https'], true)) {
2092 return $value;
2097 return '';
2100 public static function getTableListPosition(ServerRequest $request, string $db): int
2102 if (! isset($_SESSION['tmpval']['table_limit_offset']) || $_SESSION['tmpval']['table_limit_offset_db'] != $db) {
2103 $_SESSION['tmpval']['table_limit_offset'] = 0;
2104 $_SESSION['tmpval']['table_limit_offset_db'] = $db;
2107 /** @var string|null $posParam */
2108 $posParam = $request->getParam('pos');
2109 if (is_numeric($posParam)) {
2110 $_SESSION['tmpval']['table_limit_offset'] = (int) $posParam;
2113 return $_SESSION['tmpval']['table_limit_offset'];