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