1<?php 2 3declare(strict_types=1); 4 5namespace PhpMyAdmin\Display; 6 7use PhpMyAdmin\Config\SpecialSchemaLinks; 8use PhpMyAdmin\Core; 9use PhpMyAdmin\DatabaseInterface; 10use PhpMyAdmin\Html\Generator; 11use PhpMyAdmin\Index; 12use PhpMyAdmin\Message; 13use PhpMyAdmin\Plugins\Transformations\Output\Text_Octetstream_Sql; 14use PhpMyAdmin\Plugins\Transformations\Output\Text_Plain_Json; 15use PhpMyAdmin\Plugins\Transformations\Output\Text_Plain_Sql; 16use PhpMyAdmin\Plugins\Transformations\Text_Plain_Link; 17use PhpMyAdmin\Plugins\TransformationsPlugin; 18use PhpMyAdmin\Relation; 19use PhpMyAdmin\Response; 20use PhpMyAdmin\Sanitize; 21use PhpMyAdmin\Sql; 22use PhpMyAdmin\SqlParser\Parser; 23use PhpMyAdmin\SqlParser\Statements\SelectStatement; 24use PhpMyAdmin\SqlParser\Utils\Query; 25use PhpMyAdmin\Table; 26use PhpMyAdmin\Template; 27use PhpMyAdmin\Transformations; 28use PhpMyAdmin\Url; 29use PhpMyAdmin\Util; 30use stdClass; 31use const MYSQLI_TYPE_BIT; 32use function array_filter; 33use function array_keys; 34use function array_merge; 35use function array_shift; 36use function bin2hex; 37use function ceil; 38use function class_exists; 39use function count; 40use function explode; 41use function file_exists; 42use function floor; 43use function htmlspecialchars; 44use function implode; 45use function intval; 46use function is_array; 47use function is_object; 48use function json_encode; 49use function mb_check_encoding; 50use function mb_strlen; 51use function mb_strpos; 52use function mb_strtolower; 53use function mb_strtoupper; 54use function mb_substr; 55use function md5; 56use function method_exists; 57use function mt_rand; 58use function pack; 59use function preg_match; 60use function preg_replace; 61use function str_replace; 62use function strcasecmp; 63use function strip_tags; 64use function stripos; 65use function strlen; 66use function strpos; 67use function strtoupper; 68use function substr; 69use function trim; 70 71/** 72 * Handle all the functionalities related to displaying results 73 * of sql queries, stored procedure, browsing sql processes or 74 * displaying binary log. 75 */ 76class Results 77{ 78 // Define constants 79 public const NO_EDIT_OR_DELETE = 'nn'; 80 public const UPDATE_ROW = 'ur'; 81 public const DELETE_ROW = 'dr'; 82 public const KILL_PROCESS = 'kp'; 83 84 public const POSITION_LEFT = 'left'; 85 public const POSITION_RIGHT = 'right'; 86 public const POSITION_BOTH = 'both'; 87 public const POSITION_NONE = 'none'; 88 89 public const DISPLAY_FULL_TEXT = 'F'; 90 public const DISPLAY_PARTIAL_TEXT = 'P'; 91 92 public const HEADER_FLIP_TYPE_AUTO = 'auto'; 93 public const HEADER_FLIP_TYPE_CSS = 'css'; 94 public const HEADER_FLIP_TYPE_FAKE = 'fake'; 95 96 public const DATE_FIELD = 'date'; 97 public const DATETIME_FIELD = 'datetime'; 98 public const TIMESTAMP_FIELD = 'timestamp'; 99 public const TIME_FIELD = 'time'; 100 public const STRING_FIELD = 'string'; 101 public const GEOMETRY_FIELD = 'geometry'; 102 public const BLOB_FIELD = 'BLOB'; 103 public const BINARY_FIELD = 'BINARY'; 104 105 public const RELATIONAL_KEY = 'K'; 106 public const RELATIONAL_DISPLAY_COLUMN = 'D'; 107 108 public const GEOMETRY_DISP_GEOM = 'GEOM'; 109 public const GEOMETRY_DISP_WKT = 'WKT'; 110 public const GEOMETRY_DISP_WKB = 'WKB'; 111 112 public const SMART_SORT_ORDER = 'SMART'; 113 public const ASCENDING_SORT_DIR = 'ASC'; 114 public const DESCENDING_SORT_DIR = 'DESC'; 115 116 public const TABLE_TYPE_INNO_DB = 'InnoDB'; 117 public const ALL_ROWS = 'all'; 118 public const QUERY_TYPE_SELECT = 'SELECT'; 119 120 public const ROUTINE_PROCEDURE = 'procedure'; 121 public const ROUTINE_FUNCTION = 'function'; 122 123 public const ACTION_LINK_CONTENT_ICONS = 'icons'; 124 public const ACTION_LINK_CONTENT_TEXT = 'text'; 125 126 // Declare global fields 127 128 /** @var array<string, mixed> */ 129 public $properties = [ 130 /* integer server id */ 131 'server' => null, 132 133 /* string Database name */ 134 'db' => null, 135 136 /* string Table name */ 137 'table' => null, 138 139 /* string the URL to go back in case of errors */ 140 'goto' => null, 141 142 /* string the SQL query */ 143 'sql_query' => null, 144 145 /* 146 * integer the total number of rows returned by the SQL query without any 147 * appended "LIMIT" clause programmatically 148 */ 149 'unlim_num_rows' => null, 150 151 /* array meta information about fields */ 152 'fields_meta' => null, 153 154 /* boolean */ 155 'is_count' => null, 156 157 /* integer */ 158 'is_export' => null, 159 160 /* boolean */ 161 'is_func' => null, 162 163 /* integer */ 164 'is_analyse' => null, 165 166 /* integer the total number of rows returned by the SQL query */ 167 'num_rows' => null, 168 169 /* integer the total number of fields returned by the SQL query */ 170 'fields_cnt' => null, 171 172 /* double time taken for execute the SQL query */ 173 'querytime' => null, 174 175 /* string path for theme images directory */ 176 'theme_image_path' => null, 177 178 /* string */ 179 'text_dir' => null, 180 181 /* boolean */ 182 'is_maint' => null, 183 184 /* boolean */ 185 'is_explain' => null, 186 187 /* boolean */ 188 'is_show' => null, 189 190 /* boolean */ 191 'is_browse_distinct' => null, 192 193 /* array table definitions */ 194 'showtable' => null, 195 196 /* string */ 197 'printview' => null, 198 199 /* array column names to highlight */ 200 'highlight_columns' => null, 201 202 /* array holding various display information */ 203 'display_params' => null, 204 205 /* array mime types information of fields */ 206 'mime_map' => null, 207 208 /* boolean */ 209 'editable' => null, 210 211 /* random unique ID to distinguish result set */ 212 'unique_id' => null, 213 214 /* where clauses for each row, each table in the row */ 215 'whereClauseMap' => [], 216 ]; 217 218 /** 219 * This variable contains the column transformation information 220 * for some of the system databases. 221 * One element of this array represent all relevant columns in all tables in 222 * one specific database 223 * 224 * @var array 225 */ 226 public $transformationInfo; 227 228 /** @var Relation */ 229 private $relation; 230 231 /** @var Transformations */ 232 private $transformations; 233 234 /** @var Template */ 235 public $template; 236 237 /** 238 * @param string $db the database name 239 * @param string $table the table name 240 * @param int $server the server id 241 * @param string $goto the URL to go back in case of errors 242 * @param string $sql_query the SQL query 243 * 244 * @access public 245 */ 246 public function __construct($db, $table, $server, $goto, $sql_query) 247 { 248 global $dbi; 249 250 $this->relation = new Relation($dbi); 251 $this->transformations = new Transformations(); 252 $this->template = new Template(); 253 254 $this->setDefaultTransformations(); 255 256 $this->properties['db'] = $db; 257 $this->properties['table'] = $table; 258 $this->properties['server'] = $server; 259 $this->properties['goto'] = $goto; 260 $this->properties['sql_query'] = $sql_query; 261 $this->properties['unique_id'] = mt_rand(); 262 } 263 264 /** 265 * Sets default transformations for some columns 266 * 267 * @return void 268 */ 269 private function setDefaultTransformations() 270 { 271 $json_highlighting_data = [ 272 'libraries/classes/Plugins/Transformations/Output/Text_Plain_Json.php', 273 Text_Plain_Json::class, 274 'Text_Plain', 275 ]; 276 $sql_highlighting_data = [ 277 'libraries/classes/Plugins/Transformations/Output/Text_Plain_Sql.php', 278 Text_Plain_Sql::class, 279 'Text_Plain', 280 ]; 281 $blob_sql_highlighting_data = [ 282 'libraries/classes/Plugins/Transformations/Output/Text_Octetstream_Sql.php', 283 Text_Octetstream_Sql::class, 284 'Text_Octetstream', 285 ]; 286 $link_data = [ 287 'libraries/classes/Plugins/Transformations/Text_Plain_Link.php', 288 Text_Plain_Link::class, 289 'Text_Plain', 290 ]; 291 $this->transformationInfo = [ 292 'information_schema' => [ 293 'events' => ['event_definition' => $sql_highlighting_data], 294 'processlist' => ['info' => $sql_highlighting_data], 295 'routines' => ['routine_definition' => $sql_highlighting_data], 296 'triggers' => ['action_statement' => $sql_highlighting_data], 297 'views' => ['view_definition' => $sql_highlighting_data], 298 ], 299 'mysql' => [ 300 'event' => [ 301 'body' => $blob_sql_highlighting_data, 302 'body_utf8' => $blob_sql_highlighting_data, 303 ], 304 'general_log' => ['argument' => $sql_highlighting_data], 305 'help_category' => ['url' => $link_data], 306 'help_topic' => [ 307 'example' => $sql_highlighting_data, 308 'url' => $link_data, 309 ], 310 'proc' => [ 311 'param_list' => $blob_sql_highlighting_data, 312 'returns' => $blob_sql_highlighting_data, 313 'body' => $blob_sql_highlighting_data, 314 'body_utf8' => $blob_sql_highlighting_data, 315 ], 316 'slow_log' => ['sql_text' => $sql_highlighting_data], 317 ], 318 ]; 319 320 $cfgRelation = $this->relation->getRelationsParam(); 321 if (! $cfgRelation['db']) { 322 return; 323 } 324 325 $this->transformationInfo[$cfgRelation['db']] = []; 326 $relDb = &$this->transformationInfo[$cfgRelation['db']]; 327 if (! empty($cfgRelation['history'])) { 328 $relDb[$cfgRelation['history']] = ['sqlquery' => $sql_highlighting_data]; 329 } 330 if (! empty($cfgRelation['bookmark'])) { 331 $relDb[$cfgRelation['bookmark']] = ['query' => $sql_highlighting_data]; 332 } 333 if (! empty($cfgRelation['tracking'])) { 334 $relDb[$cfgRelation['tracking']] = [ 335 'schema_sql' => $sql_highlighting_data, 336 'data_sql' => $sql_highlighting_data, 337 ]; 338 } 339 if (! empty($cfgRelation['favorite'])) { 340 $relDb[$cfgRelation['favorite']] = ['tables' => $json_highlighting_data]; 341 } 342 if (! empty($cfgRelation['recent'])) { 343 $relDb[$cfgRelation['recent']] = ['tables' => $json_highlighting_data]; 344 } 345 if (! empty($cfgRelation['savedsearches'])) { 346 $relDb[$cfgRelation['savedsearches']] = ['search_data' => $json_highlighting_data]; 347 } 348 if (! empty($cfgRelation['designer_settings'])) { 349 $relDb[$cfgRelation['designer_settings']] = ['settings_data' => $json_highlighting_data]; 350 } 351 if (! empty($cfgRelation['table_uiprefs'])) { 352 $relDb[$cfgRelation['table_uiprefs']] = ['prefs' => $json_highlighting_data]; 353 } 354 if (! empty($cfgRelation['userconfig'])) { 355 $relDb[$cfgRelation['userconfig']] = ['config_data' => $json_highlighting_data]; 356 } 357 if (empty($cfgRelation['export_templates'])) { 358 return; 359 } 360 361 $relDb[$cfgRelation['export_templates']] = ['template_data' => $json_highlighting_data]; 362 } 363 364 /** 365 * Set properties which were not initialized at the constructor 366 * 367 * @param int $unlim_num_rows the total number of rows returned by 368 * the SQL query without any appended 369 * "LIMIT" clause programmatically 370 * @param stdClass $fields_meta meta information about fields 371 * @param bool $is_count statement is SELECT COUNT 372 * @param int $is_export statement contains INTO OUTFILE 373 * @param bool $is_func statement contains a function like SUM() 374 * @param int $is_analyse statement contains PROCEDURE ANALYSE 375 * @param int $num_rows total no. of rows returned by SQL query 376 * @param int $fields_cnt total no.of fields returned by SQL query 377 * @param double $querytime time taken for execute the SQL query 378 * @param string $themeImagePath path for theme images directory 379 * @param string $text_dir text direction 380 * @param bool $is_maint statement contains a maintenance command 381 * @param bool $is_explain statement contains EXPLAIN 382 * @param bool $is_show statement contains SHOW 383 * @param array $showtable table definitions 384 * @param string $printview print view was requested 385 * @param bool $editable whether the results set is editable 386 * @param bool $is_browse_dist whether browsing distinct values 387 * 388 * @return void 389 */ 390 public function setProperties( 391 $unlim_num_rows, 392 $fields_meta, 393 $is_count, 394 $is_export, 395 $is_func, 396 $is_analyse, 397 $num_rows, 398 $fields_cnt, 399 $querytime, 400 $themeImagePath, 401 $text_dir, 402 $is_maint, 403 $is_explain, 404 $is_show, 405 $showtable, 406 $printview, 407 $editable, 408 $is_browse_dist 409 ) { 410 $this->properties['unlim_num_rows'] = $unlim_num_rows; 411 $this->properties['fields_meta'] = $fields_meta; 412 $this->properties['is_count'] = $is_count; 413 $this->properties['is_export'] = $is_export; 414 $this->properties['is_func'] = $is_func; 415 $this->properties['is_analyse'] = $is_analyse; 416 $this->properties['num_rows'] = $num_rows; 417 $this->properties['fields_cnt'] = $fields_cnt; 418 $this->properties['querytime'] = $querytime; 419 $this->properties['theme_image_path'] = $themeImagePath; 420 $this->properties['text_dir'] = $text_dir; 421 $this->properties['is_maint'] = $is_maint; 422 $this->properties['is_explain'] = $is_explain; 423 $this->properties['is_show'] = $is_show; 424 $this->properties['showtable'] = $showtable; 425 $this->properties['printview'] = $printview; 426 $this->properties['editable'] = $editable; 427 $this->properties['is_browse_distinct'] = $is_browse_dist; 428 } 429 430 /** 431 * Defines the parts to display for a print view 432 * 433 * @param array $displayParts the parts to display 434 * 435 * @return array the modified display parts 436 * 437 * @access private 438 */ 439 private function setDisplayPartsForPrintView(array $displayParts) 440 { 441 // set all elements to false! 442 $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; // no edit link 443 $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE; // no delete link 444 $displayParts['sort_lnk'] = (string) '0'; 445 $displayParts['nav_bar'] = (string) '0'; 446 $displayParts['bkm_form'] = (string) '0'; 447 $displayParts['text_btn'] = (string) '0'; 448 $displayParts['pview_lnk'] = (string) '0'; 449 450 return $displayParts; 451 } 452 453 /** 454 * Defines the parts to display for a SHOW statement 455 * 456 * @param array $displayParts the parts to display 457 * 458 * @return array the modified display parts 459 * 460 * @access private 461 */ 462 private function setDisplayPartsForShow(array $displayParts) 463 { 464 preg_match( 465 '@^SHOW[[:space:]]+(VARIABLES|(FULL[[:space:]]+)?' 466 . 'PROCESSLIST|STATUS|TABLE|GRANTS|CREATE|LOGS|DATABASES|FIELDS' 467 . ')@i', 468 $this->properties['sql_query'], 469 $which 470 ); 471 472 $bIsProcessList = isset($which[1]); 473 if ($bIsProcessList) { 474 $str = ' ' . strtoupper($which[1]); 475 $bIsProcessList = $bIsProcessList 476 && strpos($str, 'PROCESSLIST') > 0; 477 } 478 479 if ($bIsProcessList) { 480 // no edit link 481 $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; 482 // "kill process" type edit link 483 $displayParts['del_lnk'] = self::KILL_PROCESS; 484 } else { 485 // Default case -> no links 486 // no edit link 487 $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; 488 // no delete link 489 $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE; 490 } 491 // Other settings 492 $displayParts['sort_lnk'] = (string) '0'; 493 $displayParts['nav_bar'] = (string) '0'; 494 $displayParts['bkm_form'] = (string) '1'; 495 $displayParts['text_btn'] = (string) '1'; 496 $displayParts['pview_lnk'] = (string) '1'; 497 498 return $displayParts; 499 } 500 501 /** 502 * Defines the parts to display for statements not related to data 503 * 504 * @param array $displayParts the parts to display 505 * 506 * @return array the modified display parts 507 * 508 * @access private 509 */ 510 private function setDisplayPartsForNonData(array $displayParts) 511 { 512 // Statement is a "SELECT COUNT", a 513 // "CHECK/ANALYZE/REPAIR/OPTIMIZE/CHECKSUM", an "EXPLAIN" one or 514 // contains a "PROC ANALYSE" part 515 $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; // no edit link 516 $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE; // no delete link 517 $displayParts['sort_lnk'] = (string) '0'; 518 $displayParts['nav_bar'] = (string) '0'; 519 $displayParts['bkm_form'] = (string) '1'; 520 521 if ($this->properties['is_maint']) { 522 $displayParts['text_btn'] = (string) '1'; 523 } else { 524 $displayParts['text_btn'] = (string) '0'; 525 } 526 $displayParts['pview_lnk'] = (string) '1'; 527 528 return $displayParts; 529 } 530 531 /** 532 * Defines the parts to display for other statements (probably SELECT) 533 * 534 * @param array $displayParts the parts to display 535 * 536 * @return array the modified display parts 537 * 538 * @access private 539 */ 540 private function setDisplayPartsForSelect(array $displayParts) 541 { 542 // Other statements (ie "SELECT" ones) -> updates 543 // $displayParts['edit_lnk'], $displayParts['del_lnk'] and 544 // $displayParts['text_btn'] (keeps other default values) 545 546 $fields_meta = $this->properties['fields_meta']; 547 $prev_table = ''; 548 $displayParts['text_btn'] = (string) '1'; 549 $number_of_columns = $this->properties['fields_cnt']; 550 551 for ($i = 0; $i < $number_of_columns; $i++) { 552 $is_link = ($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) 553 || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE) 554 || ($displayParts['sort_lnk'] != '0'); 555 556 // Displays edit/delete/sort/insert links? 557 if ($is_link 558 && $prev_table != '' 559 && $fields_meta[$i]->table != '' 560 && $fields_meta[$i]->table != $prev_table 561 ) { 562 // don't display links 563 $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; 564 $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE; 565 /** 566 * @todo May be problematic with same field names 567 * in two joined table. 568 */ 569 // $displayParts['sort_lnk'] = (string) '0'; 570 if ($displayParts['text_btn'] == '1') { 571 break; 572 } 573 } 574 575 // Always display print view link 576 $displayParts['pview_lnk'] = (string) '1'; 577 if ($fields_meta[$i]->table == '') { 578 continue; 579 } 580 581 $prev_table = $fields_meta[$i]->table; 582 } 583 584 if ($prev_table == '') { // no table for any of the columns 585 // don't display links 586 $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; 587 $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE; 588 } 589 590 return $displayParts; 591 } 592 593 /** 594 * Defines the parts to display for the results of a SQL query 595 * and the total number of rows 596 * 597 * @see getTable() 598 * 599 * @param array $displayParts the parts to display (see a few 600 * lines above for explanations) 601 * 602 * @return array the first element is an array with explicit indexes 603 * for all the display elements 604 * the second element is the total number of rows returned 605 * by the SQL query without any programmatically appended 606 * LIMIT clause (just a copy of $unlim_num_rows if it exists, 607 * else computed inside this function) 608 * 609 * @access private 610 */ 611 private function setDisplayPartsAndTotal(array $displayParts) 612 { 613 global $dbi; 614 615 $the_total = 0; 616 617 // 1. Following variables are needed for use in isset/empty or 618 // use with array indexes or safe use in foreach 619 $db = $this->properties['db']; 620 $table = $this->properties['table']; 621 $unlim_num_rows = $this->properties['unlim_num_rows']; 622 $num_rows = $this->properties['num_rows']; 623 $printview = $this->properties['printview']; 624 625 // 2. Updates the display parts 626 if ($printview == '1') { 627 $displayParts = $this->setDisplayPartsForPrintView($displayParts); 628 } elseif ($this->properties['is_count'] || $this->properties['is_analyse'] 629 || $this->properties['is_maint'] || $this->properties['is_explain'] 630 ) { 631 $displayParts = $this->setDisplayPartsForNonData($displayParts); 632 } elseif ($this->properties['is_show']) { 633 $displayParts = $this->setDisplayPartsForShow($displayParts); 634 } else { 635 $displayParts = $this->setDisplayPartsForSelect($displayParts); 636 } 637 638 // 3. Gets the total number of rows if it is unknown 639 if (isset($unlim_num_rows) && $unlim_num_rows != '') { 640 $the_total = $unlim_num_rows; 641 } elseif (($displayParts['nav_bar'] == '1') 642 || ($displayParts['sort_lnk'] == '1') 643 && (strlen($db) > 0 && strlen($table) > 0) 644 ) { 645 $the_total = $dbi->getTable($db, $table)->countRecords(); 646 } 647 648 // if for COUNT query, number of rows returned more than 1 649 // (may be being used GROUP BY) 650 if ($this->properties['is_count'] && isset($num_rows) && $num_rows > 1) { 651 $displayParts['nav_bar'] = (string) '1'; 652 $displayParts['sort_lnk'] = (string) '1'; 653 } 654 // 4. If navigation bar or sorting fields names URLs should be 655 // displayed but there is only one row, change these settings to 656 // false 657 if ($displayParts['nav_bar'] == '1' || $displayParts['sort_lnk'] == '1') { 658 // - Do not display sort links if less than 2 rows. 659 // - For a VIEW we (probably) did not count the number of rows 660 // so don't test this number here, it would remove the possibility 661 // of sorting VIEW results. 662 $_table = new Table($table, $db); 663 if (isset($unlim_num_rows) 664 && ($unlim_num_rows < 2) 665 && ! $_table->isView() 666 ) { 667 $displayParts['sort_lnk'] = (string) '0'; 668 } 669 } 670 671 return [ 672 $displayParts, 673 $the_total, 674 ]; 675 } 676 677 /** 678 * Return true if we are executing a query in the form of 679 * "SELECT * FROM <a table> ..." 680 * 681 * @see getTableHeaders(), getColumnParams() 682 * 683 * @param array $analyzed_sql_results analyzed sql results 684 * 685 * @return bool 686 * 687 * @access private 688 */ 689 private function isSelect(array $analyzed_sql_results) 690 { 691 return ! ($this->properties['is_count'] 692 || $this->properties['is_export'] 693 || $this->properties['is_func'] 694 || $this->properties['is_analyse']) 695 && ! empty($analyzed_sql_results['select_from']) 696 && ! empty($analyzed_sql_results['statement']->from) 697 && (count($analyzed_sql_results['statement']->from) === 1) 698 && ! empty($analyzed_sql_results['statement']->from[0]->table); 699 } 700 701 /** 702 * Get a navigation button 703 * 704 * @see getMoveBackwardButtonsForTableNavigation(), 705 * getMoveForwardButtonsForTableNavigation() 706 * 707 * @param string $caption iconic caption for button 708 * @param string $title text for button 709 * @param int $pos position for next query 710 * @param string $html_sql_query query ready for display 711 * @param bool $back whether 'begin' or 'previous' 712 * @param string $onsubmit optional onsubmit clause 713 * @param string $input_for_real_end optional hidden field for special treatment 714 * @param string $onclick optional onclick clause 715 * 716 * @return string html content 717 * 718 * @access private 719 */ 720 private function getTableNavigationButton( 721 $caption, 722 $title, 723 $pos, 724 $html_sql_query, 725 $back, 726 $onsubmit = '', 727 $input_for_real_end = '', 728 $onclick = '' 729 ) { 730 $caption_output = ''; 731 if ($back) { 732 if (Util::showIcons('TableNavigationLinksMode')) { 733 $caption_output .= $caption; 734 } 735 if (Util::showText('TableNavigationLinksMode')) { 736 $caption_output .= ' ' . $title; 737 } 738 } else { 739 if (Util::showText('TableNavigationLinksMode')) { 740 $caption_output .= $title; 741 } 742 if (Util::showIcons('TableNavigationLinksMode')) { 743 $caption_output .= ' ' . $caption; 744 } 745 } 746 747 return $this->template->render('display/results/table_navigation_button', [ 748 'db' => $this->properties['db'], 749 'table' => $this->properties['table'], 750 'sql_query' => $html_sql_query, 751 'pos' => $pos, 752 'is_browse_distinct' => $this->properties['is_browse_distinct'], 753 'goto' => $this->properties['goto'], 754 'input_for_real_end' => $input_for_real_end, 755 'caption_output' => $caption_output, 756 'title' => $title, 757 'onsubmit' => $onsubmit, 758 'onclick' => $onclick, 759 ]); 760 } 761 762 /** 763 * Possibly return a page selector for table navigation 764 * 765 * @return array ($output, $nbTotalPage) 766 * 767 * @access private 768 */ 769 private function getHtmlPageSelector(): array 770 { 771 $pageNow = (int) floor( 772 $_SESSION['tmpval']['pos'] 773 / $_SESSION['tmpval']['max_rows'] 774 ) + 1; 775 776 $nbTotalPage = (int) ceil( 777 $this->properties['unlim_num_rows'] 778 / $_SESSION['tmpval']['max_rows'] 779 ); 780 781 $output = ''; 782 if ($nbTotalPage > 1) { 783 $_url_params = [ 784 'db' => $this->properties['db'], 785 'table' => $this->properties['table'], 786 'sql_query' => $this->properties['sql_query'], 787 'goto' => $this->properties['goto'], 788 'is_browse_distinct' => $this->properties['is_browse_distinct'], 789 ]; 790 791 $output = $this->template->render('display/results/page_selector', [ 792 'url_params' => $_url_params, 793 'page_selector' => Util::pageselector( 794 'pos', 795 $_SESSION['tmpval']['max_rows'], 796 $pageNow, 797 $nbTotalPage, 798 200, 799 5, 800 5, 801 20, 802 10 803 ), 804 ]); 805 } 806 807 return [ 808 $output, 809 $nbTotalPage, 810 ]; 811 } 812 813 /** 814 * Get a navigation bar to browse among the results of a SQL query 815 * 816 * @see getTable() 817 * 818 * @param int $posNext the offset for the "next" page 819 * @param int $posPrevious the offset for the "previous" page 820 * @param bool $isInnodb whether its InnoDB or not 821 * @param string $sortByKeyHtml the sort by key dialog 822 * 823 * @return array 824 */ 825 private function getTableNavigation( 826 $posNext, 827 $posPrevious, 828 $isInnodb, 829 $sortByKeyHtml 830 ): array { 831 $isShowingAll = $_SESSION['tmpval']['max_rows'] === self::ALL_ROWS; 832 833 // Move to the beginning or to the previous page 834 $moveBackwardButtons = ''; 835 if ($_SESSION['tmpval']['pos'] && ! $isShowingAll) { 836 $moveBackwardButtons = $this->getMoveBackwardButtonsForTableNavigation( 837 htmlspecialchars($this->properties['sql_query']), 838 $posPrevious 839 ); 840 } 841 842 $pageSelector = ''; 843 $numberTotalPage = 1; 844 if (! $isShowingAll) { 845 [ 846 $pageSelector, 847 $numberTotalPage, 848 ] = $this->getHtmlPageSelector(); 849 } 850 851 // Move to the next page or to the last one 852 $moveForwardButtons = ''; 853 if ($this->properties['unlim_num_rows'] === false // view with unknown number of rows 854 || (! $isShowingAll 855 && $_SESSION['tmpval']['pos'] + $_SESSION['tmpval']['max_rows'] < $this->properties['unlim_num_rows'] 856 && $this->properties['num_rows'] >= $_SESSION['tmpval']['max_rows']) 857 ) { 858 $moveForwardButtons = $this->getMoveForwardButtonsForTableNavigation( 859 htmlspecialchars($this->properties['sql_query']), 860 $posNext, 861 $isInnodb 862 ); 863 } 864 865 $hiddenFields = [ 866 'db' => $this->properties['db'], 867 'table' => $this->properties['table'], 868 'server' => $this->properties['server'], 869 'sql_query' => $this->properties['sql_query'], 870 'is_browse_distinct' => $this->properties['is_browse_distinct'], 871 'goto' => $this->properties['goto'], 872 ]; 873 874 return [ 875 'move_backward_buttons' => $moveBackwardButtons, 876 'page_selector' => $pageSelector, 877 'move_forward_buttons' => $moveForwardButtons, 878 'number_total_page' => $numberTotalPage, 879 'has_show_all' => $GLOBALS['cfg']['ShowAll'] || ($this->properties['unlim_num_rows'] <= 500), 880 'hidden_fields' => $hiddenFields, 881 'session_max_rows' => $isShowingAll ? $GLOBALS['cfg']['MaxRows'] : 'all', 882 'is_showing_all' => $isShowingAll, 883 'max_rows' => $_SESSION['tmpval']['max_rows'], 884 'pos' => $_SESSION['tmpval']['pos'], 885 'sort_by_key' => $sortByKeyHtml, 886 ]; 887 } 888 889 /** 890 * Prepare move backward buttons - previous and first 891 * 892 * @see getTableNavigation() 893 * 894 * @param string $html_sql_query the sql encoded by html special characters 895 * @param int $pos_prev the offset for the "previous" page 896 * 897 * @return string html content 898 * 899 * @access private 900 */ 901 private function getMoveBackwardButtonsForTableNavigation( 902 $html_sql_query, 903 $pos_prev 904 ) { 905 return $this->getTableNavigationButton( 906 '<<', 907 _pgettext('First page', 'Begin'), 908 0, 909 $html_sql_query, 910 true 911 ) 912 . $this->getTableNavigationButton( 913 '<', 914 _pgettext('Previous page', 'Previous'), 915 $pos_prev, 916 $html_sql_query, 917 true 918 ); 919 } 920 921 /** 922 * Prepare move forward buttons - next and last 923 * 924 * @see getTableNavigation() 925 * 926 * @param string $html_sql_query the sql encoded by htmlspecialchars() 927 * @param int $pos_next the offset for the "next" page 928 * @param bool $is_innodb whether it's InnoDB or not 929 * 930 * @return string html content 931 * 932 * @access private 933 */ 934 private function getMoveForwardButtonsForTableNavigation( 935 $html_sql_query, 936 $pos_next, 937 $is_innodb 938 ) { 939 // display the Next button 940 $buttons_html = $this->getTableNavigationButton( 941 '>', 942 _pgettext('Next page', 'Next'), 943 $pos_next, 944 $html_sql_query, 945 false 946 ); 947 948 // prepare some options for the End button 949 if ($is_innodb 950 && $this->properties['unlim_num_rows'] > $GLOBALS['cfg']['MaxExactCount'] 951 ) { 952 $input_for_real_end = '<input id="real_end_input" type="hidden" ' 953 . 'name="find_real_end" value="1">'; 954 // no backquote around this message 955 $onclick = ''; 956 } else { 957 $input_for_real_end = $onclick = ''; 958 } 959 960 $maxRows = $_SESSION['tmpval']['max_rows']; 961 $onsubmit = 'onsubmit="return ' 962 . ($_SESSION['tmpval']['pos'] 963 + $maxRows 964 < $this->properties['unlim_num_rows'] 965 && $this->properties['num_rows'] >= $maxRows 966 ? 'true' 967 : 'false') . '"'; 968 969 // display the End button 970 return $buttons_html . $this->getTableNavigationButton( 971 '>>', 972 _pgettext('Last page', 'End'), 973 @((int) ceil( 974 $this->properties['unlim_num_rows'] 975 / $_SESSION['tmpval']['max_rows'] 976 ) - 1) * $maxRows, 977 $html_sql_query, 978 false, 979 $onsubmit, 980 $input_for_real_end, 981 $onclick 982 ); 983 } 984 985 /** 986 * Get the headers of the results table, for all of the columns 987 * 988 * @see getTableHeaders() 989 * 990 * @param array $displayParts which elements to display 991 * @param array $analyzed_sql_results analyzed sql results 992 * @param array $sort_expression sort expression 993 * @param array $sort_expression_nodirection sort expression 994 * without direction 995 * @param array $sort_direction sort direction 996 * @param bool $is_limited_display with limited operations 997 * or not 998 * @param string $unsorted_sql_query query without the sort part 999 * 1000 * @return string html content 1001 * 1002 * @access private 1003 */ 1004 private function getTableHeadersForColumns( 1005 array $displayParts, 1006 array $analyzed_sql_results, 1007 array $sort_expression, 1008 array $sort_expression_nodirection, 1009 array $sort_direction, 1010 $is_limited_display, 1011 $unsorted_sql_query 1012 ) { 1013 $html = ''; 1014 1015 // required to generate sort links that will remember whether the 1016 // "Show all" button has been clicked 1017 $sql_md5 = md5( 1018 $this->properties['server'] 1019 . $this->properties['db'] 1020 . $this->properties['sql_query'] 1021 ); 1022 $session_max_rows = $is_limited_display 1023 ? 0 1024 : $_SESSION['tmpval']['query'][$sql_md5]['max_rows']; 1025 1026 // Following variable are needed for use in isset/empty or 1027 // use with array indexes/safe use in the for loop 1028 $highlight_columns = $this->properties['highlight_columns']; 1029 $fields_meta = $this->properties['fields_meta']; 1030 1031 // Prepare Display column comments if enabled 1032 // ($GLOBALS['cfg']['ShowBrowseComments']). 1033 $comments_map = $this->getTableCommentsArray($analyzed_sql_results); 1034 1035 [$col_order, $col_visib] = $this->getColumnParams( 1036 $analyzed_sql_results 1037 ); 1038 1039 // optimize: avoid calling a method on each iteration 1040 $number_of_columns = $this->properties['fields_cnt']; 1041 1042 for ($j = 0; $j < $number_of_columns; $j++) { 1043 // PHP 7.4 fix for accessing array offset on bool 1044 $col_visib_current = is_array($col_visib) && isset($col_visib[$j]) ? $col_visib[$j] : null; 1045 1046 // assign $i with the appropriate column order 1047 $i = $col_order ? $col_order[$j] : $j; 1048 1049 // See if this column should get highlight because it's used in the 1050 // where-query. 1051 $name = $fields_meta[$i]->name; 1052 $condition_field = isset($highlight_columns[$name]) 1053 || isset($highlight_columns[Util::backquote($name)]); 1054 1055 // Prepare comment-HTML-wrappers for each row, if defined/enabled. 1056 $comments = $this->getCommentForRow($comments_map, $fields_meta[$i]); 1057 $display_params = $this->properties['display_params']; 1058 1059 if (($displayParts['sort_lnk'] == '1') && ! $is_limited_display) { 1060 [$order_link, $sorted_header_html] 1061 = $this->getOrderLinkAndSortedHeaderHtml( 1062 $fields_meta[$i], 1063 $sort_expression, 1064 $sort_expression_nodirection, 1065 $i, 1066 $unsorted_sql_query, 1067 $session_max_rows, 1068 $comments, 1069 $sort_direction, 1070 $col_visib, 1071 $col_visib_current 1072 ); 1073 1074 $html .= $sorted_header_html; 1075 1076 $display_params['desc'][] = ' <th ' 1077 . 'class="draggable' 1078 . ($condition_field ? ' condition' : '') 1079 . '" data-column="' . htmlspecialchars($fields_meta[$i]->name) 1080 . '">' . "\n" . $order_link . $comments . ' </th>' . "\n"; 1081 } else { 1082 // Results can't be sorted 1083 $html 1084 .= $this->getDraggableClassForNonSortableColumns( 1085 $col_visib, 1086 $col_visib_current, 1087 $condition_field, 1088 $fields_meta[$i], 1089 $comments 1090 ); 1091 1092 $display_params['desc'][] = ' <th ' 1093 . 'class="draggable' 1094 . ($condition_field ? ' condition"' : '') 1095 . '" data-column="' . htmlspecialchars((string) $fields_meta[$i]->name) 1096 . '"> ' 1097 . htmlspecialchars((string) $fields_meta[$i]->name) 1098 . $comments . ' </th>'; 1099 } 1100 1101 $this->properties['display_params'] = $display_params; 1102 } 1103 1104 return $html; 1105 } 1106 1107 /** 1108 * Get the headers of the results table 1109 * 1110 * @see getTable() 1111 * 1112 * @param array $displayParts which elements to display 1113 * @param array $analyzedSqlResults analyzed sql results 1114 * @param string $unsortedSqlQuery the unsorted sql query 1115 * @param array $sortExpression sort expression 1116 * @param array|string $sortExpressionNoDirection sort expression without direction 1117 * @param array $sortDirection sort direction 1118 * @param bool $isLimitedDisplay with limited operations or not 1119 * 1120 * @return array 1121 */ 1122 private function getTableHeaders( 1123 array &$displayParts, 1124 array $analyzedSqlResults, 1125 $unsortedSqlQuery, 1126 array $sortExpression = [], 1127 $sortExpressionNoDirection = '', 1128 array $sortDirection = [], 1129 $isLimitedDisplay = false 1130 ): array { 1131 // Needed for use in isset/empty or 1132 // use with array indexes/safe use in foreach 1133 $printView = $this->properties['printview']; 1134 $displayParams = $this->properties['display_params']; 1135 1136 // Output data needed for column reordering and show/hide column 1137 $columnOrder = $this->getDataForResettingColumnOrder($analyzedSqlResults); 1138 1139 $displayParams['emptypre'] = 0; 1140 $displayParams['emptyafter'] = 0; 1141 $displayParams['textbtn'] = ''; 1142 $fullOrPartialTextLink = ''; 1143 1144 $this->properties['display_params'] = $displayParams; 1145 1146 // Display options (if we are not in print view) 1147 $optionsBlock = []; 1148 if (! (isset($printView) && ($printView == '1')) && ! $isLimitedDisplay) { 1149 $optionsBlock = $this->getOptionsBlock(); 1150 1151 // prepare full/partial text button or link 1152 $fullOrPartialTextLink = $this->getFullOrPartialTextButtonOrLink(); 1153 } 1154 1155 // 1. Set $colspan and generate html with full/partial 1156 // text button or link 1157 [$colspan, $buttonHtml] = $this->getFieldVisibilityParams( 1158 $displayParts, 1159 $fullOrPartialTextLink 1160 ); 1161 1162 // 2. Displays the fields' name 1163 // 2.0 If sorting links should be used, checks if the query is a "JOIN" 1164 // statement (see 2.1.3) 1165 1166 // See if we have to highlight any header fields of a WHERE query. 1167 // Uses SQL-Parser results. 1168 $this->setHighlightedColumnGlobalField($analyzedSqlResults); 1169 1170 // Get the headers for all of the columns 1171 $tableHeadersForColumns = $this->getTableHeadersForColumns( 1172 $displayParts, 1173 $analyzedSqlResults, 1174 $sortExpression, 1175 $sortExpressionNoDirection, 1176 $sortDirection, 1177 $isLimitedDisplay, 1178 $unsortedSqlQuery 1179 ); 1180 1181 // Display column at rightside - checkboxes or empty column 1182 $columnAtRightSide = ''; 1183 if (! $printView) { 1184 $columnAtRightSide = $this->getColumnAtRightSide( 1185 $displayParts, 1186 $fullOrPartialTextLink, 1187 $colspan 1188 ); 1189 } 1190 1191 return [ 1192 'column_order' => $columnOrder, 1193 'options' => $optionsBlock, 1194 'has_bulk_actions_form' => $displayParts['del_lnk'] === self::DELETE_ROW 1195 || $displayParts['del_lnk'] === self::KILL_PROCESS, 1196 'button' => $buttonHtml, 1197 'table_headers_for_columns' => $tableHeadersForColumns, 1198 'column_at_right_side' => $columnAtRightSide, 1199 ]; 1200 } 1201 1202 /** 1203 * Prepare unsorted sql query and sort by key drop down 1204 * 1205 * @see getTableHeaders() 1206 * 1207 * @param array $analyzed_sql_results analyzed sql results 1208 * @param array|null $sort_expression sort expression 1209 * 1210 * @return array two element array - $unsorted_sql_query, $drop_down_html 1211 * 1212 * @access private 1213 */ 1214 private function getUnsortedSqlAndSortByKeyDropDown( 1215 array $analyzed_sql_results, 1216 ?array $sort_expression 1217 ) { 1218 $drop_down_html = ''; 1219 1220 $unsorted_sql_query = Query::replaceClause( 1221 $analyzed_sql_results['statement'], 1222 $analyzed_sql_results['parser']->list, 1223 'ORDER BY', 1224 '' 1225 ); 1226 1227 // Data is sorted by indexes only if it there is only one table. 1228 if ($this->isSelect($analyzed_sql_results)) { 1229 // grab indexes data: 1230 $indexes = Index::getFromTable( 1231 $this->properties['table'], 1232 $this->properties['db'] 1233 ); 1234 1235 // do we have any index? 1236 if (! empty($indexes)) { 1237 $drop_down_html = $this->getSortByKeyDropDown( 1238 $indexes, 1239 $sort_expression, 1240 $unsorted_sql_query 1241 ); 1242 } 1243 } 1244 1245 return [ 1246 $unsorted_sql_query, 1247 $drop_down_html, 1248 ]; 1249 } 1250 1251 /** 1252 * Prepare sort by key dropdown - html code segment 1253 * 1254 * @see getTableHeaders() 1255 * 1256 * @param Index[] $indexes the indexes of the table for sort criteria 1257 * @param array|null $sortExpression the sort expression 1258 * @param string $unsortedSqlQuery the unsorted sql query 1259 * 1260 * @return string html content 1261 * 1262 * @access private 1263 */ 1264 private function getSortByKeyDropDown( 1265 $indexes, 1266 ?array $sortExpression, 1267 $unsortedSqlQuery 1268 ): string { 1269 $hiddenFields = [ 1270 'db' => $this->properties['db'], 1271 'table' => $this->properties['table'], 1272 'server' => $this->properties['server'], 1273 'sort_by_key' => '1', 1274 ]; 1275 1276 // Keep the number of rows (25, 50, 100, ...) when changing sort key value 1277 if (isset($_SESSION['tmpval']) && isset($_SESSION['tmpval']['max_rows'])) { 1278 $hiddenFields['session_max_rows'] = $_SESSION['tmpval']['max_rows']; 1279 } 1280 1281 $isIndexUsed = false; 1282 $localOrder = is_array($sortExpression) ? implode(', ', $sortExpression) : ''; 1283 1284 $options = []; 1285 foreach ($indexes as $index) { 1286 $ascSort = '`' 1287 . implode('` ASC, `', array_keys($index->getColumns())) 1288 . '` ASC'; 1289 1290 $descSort = '`' 1291 . implode('` DESC, `', array_keys($index->getColumns())) 1292 . '` DESC'; 1293 1294 $isIndexUsed = $isIndexUsed 1295 || $localOrder === $ascSort 1296 || $localOrder === $descSort; 1297 1298 $unsortedSqlQueryFirstPart = $unsortedSqlQuery; 1299 $unsortedSqlQuerySecondPart = ''; 1300 if (preg_match( 1301 '@(.*)([[:space:]](LIMIT (.*)|PROCEDURE (.*)|' 1302 . 'FOR UPDATE|LOCK IN SHARE MODE))@is', 1303 $unsortedSqlQuery, 1304 $myReg 1305 )) { 1306 $unsortedSqlQueryFirstPart = $myReg[1]; 1307 $unsortedSqlQuerySecondPart = $myReg[2]; 1308 } 1309 1310 $options[] = [ 1311 'value' => $unsortedSqlQueryFirstPart . ' ORDER BY ' 1312 . $ascSort . $unsortedSqlQuerySecondPart, 1313 'content' => $index->getName() . ' (ASC)', 1314 'is_selected' => $localOrder === $ascSort, 1315 ]; 1316 $options[] = [ 1317 'value' => $unsortedSqlQueryFirstPart . ' ORDER BY ' 1318 . $descSort . $unsortedSqlQuerySecondPart, 1319 'content' => $index->getName() . ' (DESC)', 1320 'is_selected' => $localOrder === $descSort, 1321 ]; 1322 } 1323 $options[] = [ 1324 'value' => $unsortedSqlQuery, 1325 'content' => __('None'), 1326 'is_selected' => ! $isIndexUsed, 1327 ]; 1328 1329 return $this->template->render('display/results/sort_by_key', [ 1330 'hidden_fields' => $hiddenFields, 1331 'options' => $options, 1332 ]); 1333 } 1334 1335 /** 1336 * Set column span, row span and prepare html with full/partial 1337 * text button or link 1338 * 1339 * @see getTableHeaders() 1340 * 1341 * @param array $displayParts which elements to display 1342 * @param string $full_or_partial_text_link full/partial link or text button 1343 * 1344 * @return array 2 element array - $colspan, $button_html 1345 * 1346 * @access private 1347 */ 1348 private function getFieldVisibilityParams( 1349 array &$displayParts, 1350 $full_or_partial_text_link 1351 ) { 1352 $button_html = ''; 1353 $display_params = $this->properties['display_params']; 1354 1355 // 1. Displays the full/partial text button (part 1)... 1356 $button_html .= '<thead class="thead-light"><tr>' . "\n"; 1357 1358 $emptyPreCondition = $displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE 1359 && $displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE; 1360 1361 $colspan = $emptyPreCondition ? ' colspan="4"' 1362 : ''; 1363 1364 $leftOrBoth = $GLOBALS['cfg']['RowActionLinks'] === self::POSITION_LEFT 1365 || $GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH; 1366 1367 // ... before the result table 1368 if (($displayParts['edit_lnk'] === self::NO_EDIT_OR_DELETE) 1369 && ($displayParts['del_lnk'] === self::NO_EDIT_OR_DELETE) 1370 && ($displayParts['text_btn'] == '1') 1371 ) { 1372 $display_params['emptypre'] = $emptyPreCondition ? 4 : 0; 1373 } elseif ($leftOrBoth && ($displayParts['text_btn'] == '1') 1374 ) { 1375 // ... at the left column of the result table header if possible 1376 // and required 1377 1378 $display_params['emptypre'] = $emptyPreCondition ? 4 : 0; 1379 1380 $button_html .= '<th class="column_action sticky print_ignore" ' . $colspan 1381 . '>' . $full_or_partial_text_link . '</th>'; 1382 } elseif ($leftOrBoth 1383 && (($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) 1384 || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE)) 1385 ) { 1386 // ... elseif no button, displays empty(ies) col(s) if required 1387 1388 $display_params['emptypre'] = $emptyPreCondition ? 4 : 0; 1389 1390 $button_html .= '<td ' . $colspan . '></td>'; 1391 } elseif ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_NONE) { 1392 // ... elseif display an empty column if the actions links are 1393 // disabled to match the rest of the table 1394 $button_html .= '<th class="column_action sticky"></th>'; 1395 } 1396 1397 $this->properties['display_params'] = $display_params; 1398 1399 return [ 1400 $colspan, 1401 $button_html, 1402 ]; 1403 } 1404 1405 /** 1406 * Get table comments as array 1407 * 1408 * @see getTableHeaders() 1409 * 1410 * @param array $analyzed_sql_results analyzed sql results 1411 * 1412 * @return array table comments 1413 * 1414 * @access private 1415 */ 1416 private function getTableCommentsArray(array $analyzed_sql_results) 1417 { 1418 if (! $GLOBALS['cfg']['ShowBrowseComments'] 1419 || empty($analyzed_sql_results['statement']->from) 1420 ) { 1421 return []; 1422 } 1423 1424 $ret = []; 1425 foreach ($analyzed_sql_results['statement']->from as $field) { 1426 if (empty($field->table)) { 1427 continue; 1428 } 1429 $ret[$field->table] = $this->relation->getComments( 1430 empty($field->database) ? $this->properties['db'] : $field->database, 1431 $field->table 1432 ); 1433 } 1434 1435 return $ret; 1436 } 1437 1438 /** 1439 * Set global array for store highlighted header fields 1440 * 1441 * @see getTableHeaders() 1442 * 1443 * @param array $analyzed_sql_results analyzed sql results 1444 * 1445 * @return void 1446 * 1447 * @access private 1448 */ 1449 private function setHighlightedColumnGlobalField(array $analyzed_sql_results) 1450 { 1451 $highlight_columns = []; 1452 1453 if (! empty($analyzed_sql_results['statement']->where)) { 1454 foreach ($analyzed_sql_results['statement']->where as $expr) { 1455 foreach ($expr->identifiers as $identifier) { 1456 $highlight_columns[$identifier] = 'true'; 1457 } 1458 } 1459 } 1460 1461 $this->properties['highlight_columns'] = $highlight_columns; 1462 } 1463 1464 /** 1465 * Prepare data for column restoring and show/hide 1466 * 1467 * @see getTableHeaders() 1468 * 1469 * @param array $analyzedSqlResults analyzed sql results 1470 * 1471 * @return array 1472 */ 1473 private function getDataForResettingColumnOrder(array $analyzedSqlResults): array 1474 { 1475 global $dbi; 1476 1477 if (! $this->isSelect($analyzedSqlResults)) { 1478 return []; 1479 } 1480 1481 [$columnOrder, $columnVisibility] = $this->getColumnParams( 1482 $analyzedSqlResults 1483 ); 1484 1485 $tableCreateTime = ''; 1486 $table = new Table($this->properties['table'], $this->properties['db']); 1487 if (! $table->isView()) { 1488 $tableCreateTime = $dbi->getTable( 1489 $this->properties['db'], 1490 $this->properties['table'] 1491 )->getStatusInfo('Create_time'); 1492 } 1493 1494 return [ 1495 'order' => $columnOrder, 1496 'visibility' => $columnVisibility, 1497 'is_view' => $table->isView(), 1498 'table_create_time' => $tableCreateTime, 1499 ]; 1500 } 1501 1502 /** 1503 * Prepare option fields block 1504 * 1505 * @see getTableHeaders() 1506 * 1507 * @return array 1508 */ 1509 private function getOptionsBlock(): array 1510 { 1511 if (isset($_SESSION['tmpval']['possible_as_geometry']) 1512 && $_SESSION['tmpval']['possible_as_geometry'] == false 1513 ) { 1514 if ($_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_GEOM) { 1515 $_SESSION['tmpval']['geoOption'] = self::GEOMETRY_DISP_WKT; 1516 } 1517 } 1518 1519 return [ 1520 'geo_option' => $_SESSION['tmpval']['geoOption'], 1521 'hide_transformation' => $_SESSION['tmpval']['hide_transformation'], 1522 'display_blob' => $_SESSION['tmpval']['display_blob'], 1523 'display_binary' => $_SESSION['tmpval']['display_binary'], 1524 'relational_display' => $_SESSION['tmpval']['relational_display'], 1525 'possible_as_geometry' => $_SESSION['tmpval']['possible_as_geometry'], 1526 'pftext' => $_SESSION['tmpval']['pftext'], 1527 ]; 1528 } 1529 1530 /** 1531 * Get full/partial text button or link 1532 * 1533 * @see getTableHeaders() 1534 * 1535 * @return string html content 1536 * 1537 * @access private 1538 */ 1539 private function getFullOrPartialTextButtonOrLink() 1540 { 1541 $url_params_full_text = [ 1542 'db' => $this->properties['db'], 1543 'table' => $this->properties['table'], 1544 'sql_query' => $this->properties['sql_query'], 1545 'goto' => $this->properties['goto'], 1546 'full_text_button' => 1, 1547 ]; 1548 1549 if ($_SESSION['tmpval']['pftext'] === self::DISPLAY_FULL_TEXT) { 1550 // currently in fulltext mode so show the opposite link 1551 $tmp_image_file = $this->properties['theme_image_path'] . 's_partialtext.png'; 1552 $tmp_txt = __('Partial texts'); 1553 $url_params_full_text['pftext'] = self::DISPLAY_PARTIAL_TEXT; 1554 } else { 1555 $tmp_image_file = $this->properties['theme_image_path'] . 's_fulltext.png'; 1556 $tmp_txt = __('Full texts'); 1557 $url_params_full_text['pftext'] = self::DISPLAY_FULL_TEXT; 1558 } 1559 1560 $tmp_image = '<img class="fulltext" src="' . $tmp_image_file . '" alt="' 1561 . $tmp_txt . '" title="' . $tmp_txt . '">'; 1562 1563 return Generator::linkOrButton(Url::getFromRoute('/sql'), $url_params_full_text, $tmp_image); 1564 } 1565 1566 /** 1567 * Get comment for row 1568 * 1569 * @see getTableHeaders() 1570 * 1571 * @param array $commentsMap comments array 1572 * @param array $fieldsMeta set of field properties 1573 * 1574 * @return string html content 1575 * 1576 * @access private 1577 */ 1578 private function getCommentForRow(array $commentsMap, $fieldsMeta) 1579 { 1580 return $this->template->render('display/results/comment_for_row', [ 1581 'comments_map' => $commentsMap, 1582 'fields_meta' => $fieldsMeta, 1583 'limit_chars' => $GLOBALS['cfg']['LimitChars'], 1584 ]); 1585 } 1586 1587 /** 1588 * Prepare parameters and html for sorted table header fields 1589 * 1590 * @see getTableHeaders() 1591 * 1592 * @param stdClass $fields_meta set of field properties 1593 * @param array $sort_expression sort expression 1594 * @param array $sort_expression_nodirection sort expression without direction 1595 * @param int $column_index the index of the column 1596 * @param string $unsorted_sql_query the unsorted sql query 1597 * @param int $session_max_rows maximum rows resulted by sql 1598 * @param string $comments comment for row 1599 * @param array $sort_direction sort direction 1600 * @param bool $col_visib column is visible(false) or column isn't visible(string array) 1601 * @param string $col_visib_j element of $col_visib array 1602 * 1603 * @return array 2 element array - $order_link, $sorted_header_html 1604 * 1605 * @access private 1606 */ 1607 private function getOrderLinkAndSortedHeaderHtml( 1608 $fields_meta, 1609 array $sort_expression, 1610 array $sort_expression_nodirection, 1611 $column_index, 1612 $unsorted_sql_query, 1613 $session_max_rows, 1614 $comments, 1615 array $sort_direction, 1616 $col_visib, 1617 $col_visib_j 1618 ) { 1619 $sorted_header_html = ''; 1620 1621 // Checks if the table name is required; it's the case 1622 // for a query with a "JOIN" statement and if the column 1623 // isn't aliased, or in queries like 1624 // SELECT `1`.`master_field` , `2`.`master_field` 1625 // FROM `PMA_relation` AS `1` , `PMA_relation` AS `2` 1626 1627 $sort_tbl = isset($fields_meta->table) 1628 && strlen($fields_meta->table) > 0 1629 && $fields_meta->orgname == $fields_meta->name 1630 ? Util::backquote( 1631 $fields_meta->table 1632 ) . '.' 1633 : ''; 1634 1635 $name_to_use_in_sort = $fields_meta->name; 1636 1637 // Generates the orderby clause part of the query which is part 1638 // of URL 1639 [$single_sort_order, $multi_sort_order, $order_img] 1640 = $this->getSingleAndMultiSortUrls( 1641 $sort_expression, 1642 $sort_expression_nodirection, 1643 $sort_tbl, 1644 $name_to_use_in_sort, 1645 $sort_direction, 1646 $fields_meta 1647 ); 1648 1649 if (preg_match( 1650 '@(.*)([[:space:]](LIMIT (.*)|PROCEDURE (.*)|FOR UPDATE|' 1651 . 'LOCK IN SHARE MODE))@is', 1652 $unsorted_sql_query, 1653 $regs3 1654 )) { 1655 $single_sorted_sql_query = $regs3[1] . $single_sort_order . $regs3[2]; 1656 $multi_sorted_sql_query = $regs3[1] . $multi_sort_order . $regs3[2]; 1657 } else { 1658 $single_sorted_sql_query = $unsorted_sql_query . $single_sort_order; 1659 $multi_sorted_sql_query = $unsorted_sql_query . $multi_sort_order; 1660 } 1661 1662 $_single_url_params = [ 1663 'db' => $this->properties['db'], 1664 'table' => $this->properties['table'], 1665 'sql_query' => $single_sorted_sql_query, 1666 'sql_signature' => Core::signSqlQuery($single_sorted_sql_query), 1667 'session_max_rows' => $session_max_rows, 1668 'is_browse_distinct' => $this->properties['is_browse_distinct'], 1669 ]; 1670 1671 $_multi_url_params = [ 1672 'db' => $this->properties['db'], 1673 'table' => $this->properties['table'], 1674 'sql_query' => $multi_sorted_sql_query, 1675 'sql_signature' => Core::signSqlQuery($multi_sorted_sql_query), 1676 'session_max_rows' => $session_max_rows, 1677 'is_browse_distinct' => $this->properties['is_browse_distinct'], 1678 ]; 1679 1680 // Displays the sorting URL 1681 // enable sort order swapping for image 1682 $order_link = $this->getSortOrderLink( 1683 $order_img, 1684 $fields_meta, 1685 $_single_url_params, 1686 $_multi_url_params 1687 ); 1688 1689 $order_link .= $this->getSortOrderHiddenInputs( 1690 $_multi_url_params, 1691 $name_to_use_in_sort 1692 ); 1693 1694 $sorted_header_html .= $this->getDraggableClassForSortableColumns( 1695 $col_visib, 1696 $col_visib_j, 1697 $fields_meta, 1698 $order_link, 1699 $comments 1700 ); 1701 1702 return [ 1703 $order_link, 1704 $sorted_header_html, 1705 ]; 1706 } 1707 1708 /** 1709 * Prepare parameters and html for sorted table header fields 1710 * 1711 * @see getOrderLinkAndSortedHeaderHtml() 1712 * 1713 * @param array $sort_expression sort expression 1714 * @param array $sort_expression_nodirection sort expression without direction 1715 * @param string $sort_tbl The name of the table to which 1716 * the current column belongs to 1717 * @param string $name_to_use_in_sort The current column under 1718 * consideration 1719 * @param array $sort_direction sort direction 1720 * @param stdClass $fields_meta set of field properties 1721 * 1722 * @return array 3 element array - $single_sort_order, $sort_order, $order_img 1723 * 1724 * @access private 1725 */ 1726 private function getSingleAndMultiSortUrls( 1727 array $sort_expression, 1728 array $sort_expression_nodirection, 1729 $sort_tbl, 1730 $name_to_use_in_sort, 1731 array $sort_direction, 1732 $fields_meta 1733 ) { 1734 $sort_order = ''; 1735 // Check if the current column is in the order by clause 1736 $is_in_sort = $this->isInSorted( 1737 $sort_expression, 1738 $sort_expression_nodirection, 1739 $sort_tbl, 1740 $name_to_use_in_sort 1741 ); 1742 $current_name = $name_to_use_in_sort; 1743 if ($sort_expression_nodirection[0] == '' || ! $is_in_sort) { 1744 $special_index = $sort_expression_nodirection[0] == '' 1745 ? 0 1746 : count($sort_expression_nodirection); 1747 $sort_expression_nodirection[$special_index] 1748 = Util::backquote( 1749 $current_name 1750 ); 1751 $sort_direction[$special_index] = preg_match( 1752 '@time|date@i', 1753 $fields_meta->type ?? '' 1754 ) ? self::DESCENDING_SORT_DIR : self::ASCENDING_SORT_DIR; 1755 } 1756 1757 $sort_expression_nodirection = array_filter($sort_expression_nodirection); 1758 $single_sort_order = null; 1759 foreach ($sort_expression_nodirection as $index => $expression) { 1760 // check if this is the first clause, 1761 // if it is then we have to add "order by" 1762 $is_first_clause = ($index == 0); 1763 $name_to_use_in_sort = $expression; 1764 $sort_tbl_new = $sort_tbl; 1765 // Test to detect if the column name is a standard name 1766 // Standard name has the table name prefixed to the column name 1767 if (mb_strpos($name_to_use_in_sort, '.') !== false) { 1768 $matches = explode('.', $name_to_use_in_sort); 1769 // Matches[0] has the table name 1770 // Matches[1] has the column name 1771 $name_to_use_in_sort = $matches[1]; 1772 $sort_tbl_new = $matches[0]; 1773 } 1774 1775 // $name_to_use_in_sort might contain a space due to 1776 // formatting of function expressions like "COUNT(name )" 1777 // so we remove the space in this situation 1778 $name_to_use_in_sort = str_replace([' )', '``'], [')', '`'], $name_to_use_in_sort); 1779 $name_to_use_in_sort = trim($name_to_use_in_sort, '`'); 1780 1781 // If this the first column name in the order by clause add 1782 // order by clause to the column name 1783 $query_head = $is_first_clause ? "\nORDER BY " : ''; 1784 // Again a check to see if the given column is a aggregate column 1785 if (mb_strpos($name_to_use_in_sort, '(') !== false) { 1786 $sort_order .= $query_head . $name_to_use_in_sort . ' '; 1787 } else { 1788 if (strlen($sort_tbl_new) > 0) { 1789 $sort_tbl_new .= '.'; 1790 } 1791 $sort_order .= $query_head . $sort_tbl_new 1792 . Util::backquote( 1793 $name_to_use_in_sort 1794 ) . ' '; 1795 } 1796 1797 // For a special case where the code generates two dots between 1798 // column name and table name. 1799 $sort_order = preg_replace('/\.\./', '.', $sort_order); 1800 // Incase this is the current column save $single_sort_order 1801 if ($current_name == $name_to_use_in_sort) { 1802 if (mb_strpos($current_name, '(') !== false) { 1803 $single_sort_order = "\n" . 'ORDER BY ' . Util::backquote($current_name) . ' '; 1804 } else { 1805 $single_sort_order = "\n" . 'ORDER BY ' . $sort_tbl 1806 . Util::backquote( 1807 $current_name 1808 ) . ' '; 1809 } 1810 if ($is_in_sort) { 1811 [$single_sort_order, $order_img] 1812 = $this->getSortingUrlParams( 1813 $sort_direction, 1814 $single_sort_order, 1815 $index 1816 ); 1817 } else { 1818 $single_sort_order .= strtoupper($sort_direction[$index]); 1819 } 1820 } 1821 if ($current_name == $name_to_use_in_sort && $is_in_sort) { 1822 // We need to generate the arrow button and related html 1823 [$sort_order, $order_img] = $this->getSortingUrlParams( 1824 $sort_direction, 1825 $sort_order, 1826 $index 1827 ); 1828 $order_img .= ' <small>' . ($index + 1) . '</small>'; 1829 } else { 1830 $sort_order .= strtoupper($sort_direction[$index]); 1831 } 1832 // Separate columns by a comma 1833 $sort_order .= ', '; 1834 } 1835 // remove the comma from the last column name in the newly 1836 // constructed clause 1837 $sort_order = mb_substr( 1838 $sort_order, 1839 0, 1840 mb_strlen($sort_order) - 2 1841 ); 1842 if (empty($order_img)) { 1843 $order_img = ''; 1844 } 1845 1846 return [ 1847 $single_sort_order, 1848 $sort_order, 1849 $order_img, 1850 ]; 1851 } 1852 1853 /** 1854 * Check whether the column is sorted 1855 * 1856 * @see getTableHeaders() 1857 * 1858 * @param array $sort_expression sort expression 1859 * @param array $sort_expression_nodirection sort expression without direction 1860 * @param string $sort_tbl the table name 1861 * @param string $name_to_use_in_sort the sorting column name 1862 * 1863 * @return bool the column sorted or not 1864 * 1865 * @access private 1866 */ 1867 private function isInSorted( 1868 array $sort_expression, 1869 array $sort_expression_nodirection, 1870 $sort_tbl, 1871 $name_to_use_in_sort 1872 ) { 1873 $index_in_expression = 0; 1874 1875 foreach ($sort_expression_nodirection as $index => $clause) { 1876 if (mb_strpos($clause, '.') !== false) { 1877 $fragments = explode('.', $clause); 1878 $clause2 = $fragments[0] . '.' . str_replace('`', '', $fragments[1]); 1879 } else { 1880 $clause2 = $sort_tbl . str_replace('`', '', $clause); 1881 } 1882 if ($clause2 === $sort_tbl . $name_to_use_in_sort) { 1883 $index_in_expression = $index; 1884 break; 1885 } 1886 } 1887 if (empty($sort_expression[$index_in_expression])) { 1888 $is_in_sort = false; 1889 } else { 1890 // Field name may be preceded by a space, or any number 1891 // of characters followed by a dot (tablename.fieldname) 1892 // so do a direct comparison for the sort expression; 1893 // this avoids problems with queries like 1894 // "SELECT id, count(id)..." and clicking to sort 1895 // on id or on count(id). 1896 // Another query to test this: 1897 // SELECT p.*, FROM_UNIXTIME(p.temps) FROM mytable AS p 1898 // (and try clicking on each column's header twice) 1899 $noSortTable = empty($sort_tbl) || mb_strpos( 1900 $sort_expression_nodirection[$index_in_expression], 1901 $sort_tbl 1902 ) === false; 1903 $noOpenParenthesis = mb_strpos( 1904 $sort_expression_nodirection[$index_in_expression], 1905 '(' 1906 ) === false; 1907 if (! empty($sort_tbl) && $noSortTable && $noOpenParenthesis) { 1908 $new_sort_expression_nodirection = $sort_tbl 1909 . $sort_expression_nodirection[$index_in_expression]; 1910 } else { 1911 $new_sort_expression_nodirection 1912 = $sort_expression_nodirection[$index_in_expression]; 1913 } 1914 1915 //Back quotes are removed in next comparison, so remove them from value 1916 //to compare. 1917 $name_to_use_in_sort = str_replace('`', '', $name_to_use_in_sort); 1918 1919 $is_in_sort = false; 1920 $sort_name = str_replace('`', '', $sort_tbl) . $name_to_use_in_sort; 1921 1922 if ($sort_name == str_replace('`', '', $new_sort_expression_nodirection) 1923 || $sort_name == str_replace('`', '', $sort_expression_nodirection[$index_in_expression]) 1924 ) { 1925 $is_in_sort = true; 1926 } 1927 } 1928 1929 return $is_in_sort; 1930 } 1931 1932 /** 1933 * Get sort url parameters - sort order and order image 1934 * 1935 * @see getSingleAndMultiSortUrls() 1936 * 1937 * @param array $sort_direction the sort direction 1938 * @param string $sort_order the sorting order 1939 * @param int $index the index of sort direction array. 1940 * 1941 * @return array 2 element array - $sort_order, $order_img 1942 * 1943 * @access private 1944 */ 1945 private function getSortingUrlParams(array $sort_direction, $sort_order, $index) 1946 { 1947 if (strtoupper(trim($sort_direction[$index])) === self::DESCENDING_SORT_DIR) { 1948 $sort_order .= ' ASC'; 1949 $order_img = ' ' . Generator::getImage( 1950 's_desc', 1951 __('Descending'), 1952 [ 1953 'class' => 'soimg', 1954 'title' => '', 1955 ] 1956 ); 1957 $order_img .= ' ' . Generator::getImage( 1958 's_asc', 1959 __('Ascending'), 1960 [ 1961 'class' => 'soimg hide', 1962 'title' => '', 1963 ] 1964 ); 1965 } else { 1966 $sort_order .= ' DESC'; 1967 $order_img = ' ' . Generator::getImage( 1968 's_asc', 1969 __('Ascending'), 1970 [ 1971 'class' => 'soimg', 1972 'title' => '', 1973 ] 1974 ); 1975 $order_img .= ' ' . Generator::getImage( 1976 's_desc', 1977 __('Descending'), 1978 [ 1979 'class' => 'soimg hide', 1980 'title' => '', 1981 ] 1982 ); 1983 } 1984 1985 return [ 1986 $sort_order, 1987 $order_img, 1988 ]; 1989 } 1990 1991 /** 1992 * Get sort order link 1993 * 1994 * @see getTableHeaders() 1995 * 1996 * @param string $order_img the sort order image 1997 * @param stdClass $fields_meta set of field properties 1998 * @param array $order_url_params the url params for sort 1999 * @param array $multi_order_url_params the url params for sort 2000 * 2001 * @return string the sort order link 2002 * 2003 * @access private 2004 */ 2005 private function getSortOrderLink( 2006 $order_img, 2007 $fields_meta, 2008 $order_url_params, 2009 $multi_order_url_params 2010 ) { 2011 $order_link_params = ['class' => 'sortlink']; 2012 2013 $order_link_content = htmlspecialchars($fields_meta->name ?? ''); 2014 $inner_link_content = $order_link_content . $order_img 2015 . '<input type="hidden" value="' 2016 . Url::getFromRoute('/sql') 2017 . Url::getCommon($multi_order_url_params, '?', false) 2018 . '">'; 2019 2020 return Generator::linkOrButton( 2021 Url::getFromRoute('/sql'), 2022 $order_url_params, 2023 $inner_link_content, 2024 $order_link_params 2025 ); 2026 } 2027 2028 private function getSortOrderHiddenInputs( 2029 array $multipleUrlParams, 2030 string $nameToUseInSort 2031 ): string { 2032 $sqlQuery = $multipleUrlParams['sql_query']; 2033 $sqlQueryAdd = $sqlQuery; 2034 $sqlQueryRemove = null; 2035 $parser = new Parser($sqlQuery); 2036 2037 $firstStatement = $parser->statements[0] ?? null; 2038 $numberOfClausesFound = null; 2039 if ($firstStatement instanceof SelectStatement) { 2040 $orderClauses = $firstStatement->order ?? []; 2041 foreach ($orderClauses as $key => $order) { 2042 // If this is the column name, then remove it from the order clause 2043 if ($order->expr->column !== $nameToUseInSort) { 2044 continue; 2045 } 2046 // remove the order clause for this column and from the counted array 2047 unset($firstStatement->order[$key], $orderClauses[$key]); 2048 } 2049 $numberOfClausesFound = count($orderClauses); 2050 $sqlQueryRemove = $firstStatement->build(); 2051 } 2052 2053 $multipleUrlParams['sql_query'] = $sqlQueryRemove ?? $sqlQuery; 2054 $multipleUrlParams['sql_signature'] = Core::signSqlQuery($multipleUrlParams['sql_query']); 2055 2056 $urlRemoveOrder = Url::getFromRoute('/sql', $multipleUrlParams); 2057 if ($numberOfClausesFound !== null && $numberOfClausesFound === 0) { 2058 $urlRemoveOrder .= '&discard_remembered_sort=1'; 2059 } 2060 2061 $multipleUrlParams['sql_query'] = $sqlQueryAdd; 2062 $multipleUrlParams['sql_signature'] = Core::signSqlQuery($multipleUrlParams['sql_query']); 2063 2064 $urlAddOrder = Url::getFromRoute('/sql', $multipleUrlParams); 2065 2066 return '<input type="hidden" name="url-remove-order" value="' . $urlRemoveOrder . '">' . "\n" 2067 . '<input type="hidden" name="url-add-order" value="' . $urlAddOrder . '">'; 2068 } 2069 2070 /** 2071 * Check if the column contains numeric data. If yes, then set the 2072 * column header's alignment right 2073 * 2074 * @see getDraggableClassForSortableColumns() 2075 * 2076 * @param stdClass $fields_meta set of field properties 2077 * @param array $th_class array containing classes 2078 * 2079 * @return void 2080 */ 2081 private function getClassForNumericColumnType($fields_meta, array &$th_class) 2082 { 2083 if (! preg_match( 2084 '@int|decimal|float|double|real|bit|boolean|serial@i', 2085 (string) $fields_meta->type 2086 )) { 2087 return; 2088 } 2089 2090 $th_class[] = 'text-right'; 2091 } 2092 2093 /** 2094 * Prepare columns to draggable effect for sortable columns 2095 * 2096 * @see getTableHeaders() 2097 * 2098 * @param bool $col_visib the column is visible (false) 2099 * array the column is not visible (string array) 2100 * @param string $col_visib_j element of $col_visib array 2101 * @param stdClass $fields_meta set of field properties 2102 * @param string $order_link the order link 2103 * @param string $comments the comment for the column 2104 * 2105 * @return string html content 2106 * 2107 * @access private 2108 */ 2109 private function getDraggableClassForSortableColumns( 2110 $col_visib, 2111 $col_visib_j, 2112 $fields_meta, 2113 $order_link, 2114 $comments 2115 ) { 2116 $draggable_html = '<th'; 2117 $th_class = []; 2118 $th_class[] = 'draggable'; 2119 $this->getClassForNumericColumnType($fields_meta, $th_class); 2120 if ($col_visib && ! $col_visib_j) { 2121 $th_class[] = 'hide'; 2122 } 2123 2124 $th_class[] = 'column_heading'; 2125 $th_class[] = 'sticky'; 2126 if ($GLOBALS['cfg']['BrowsePointerEnable'] == true) { 2127 $th_class[] = 'pointer'; 2128 } 2129 2130 if ($GLOBALS['cfg']['BrowseMarkerEnable'] == true) { 2131 $th_class[] = 'marker'; 2132 } 2133 2134 $draggable_html .= ' class="' . implode(' ', $th_class) . '"'; 2135 2136 $draggable_html .= ' data-column="' . htmlspecialchars((string) $fields_meta->name) 2137 . '">' . $order_link . $comments . '</th>'; 2138 2139 return $draggable_html; 2140 } 2141 2142 /** 2143 * Prepare columns to draggable effect for non sortable columns 2144 * 2145 * @see getTableHeaders() 2146 * 2147 * @param bool $col_visib the column is visible (false) 2148 * array the column is not visible (string array) 2149 * @param string $col_visib_j element of $col_visib array 2150 * @param bool $condition_field whether to add CSS class condition 2151 * @param stdClass $fields_meta set of field properties 2152 * @param string $comments the comment for the column 2153 * 2154 * @return string html content 2155 * 2156 * @access private 2157 */ 2158 private function getDraggableClassForNonSortableColumns( 2159 $col_visib, 2160 $col_visib_j, 2161 $condition_field, 2162 $fields_meta, 2163 $comments 2164 ) { 2165 $draggable_html = '<th'; 2166 $th_class = []; 2167 $th_class[] = 'draggable'; 2168 $th_class[] = 'sticky'; 2169 $this->getClassForNumericColumnType($fields_meta, $th_class); 2170 if ($col_visib && ! $col_visib_j) { 2171 $th_class[] = 'hide'; 2172 } 2173 2174 if ($condition_field) { 2175 $th_class[] = 'condition'; 2176 } 2177 2178 $draggable_html .= ' class="' . implode(' ', $th_class) . '"'; 2179 2180 $draggable_html .= ' data-column="' 2181 . htmlspecialchars((string) $fields_meta->name) . '">'; 2182 2183 $draggable_html .= htmlspecialchars((string) $fields_meta->name); 2184 2185 $draggable_html .= "\n" . $comments . '</th>'; 2186 2187 return $draggable_html; 2188 } 2189 2190 /** 2191 * Prepare column to show at right side - check boxes or empty column 2192 * 2193 * @see getTableHeaders() 2194 * 2195 * @param array $displayParts which elements to display 2196 * @param string $full_or_partial_text_link full/partial link or text button 2197 * @param string $colspan column span of table header 2198 * 2199 * @return string html content 2200 * 2201 * @access private 2202 */ 2203 private function getColumnAtRightSide( 2204 array &$displayParts, 2205 $full_or_partial_text_link, 2206 $colspan 2207 ) { 2208 $right_column_html = ''; 2209 $display_params = $this->properties['display_params']; 2210 2211 // Displays the needed checkboxes at the right 2212 // column of the result table header if possible and required... 2213 if (($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_RIGHT) 2214 || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH) 2215 && (($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) 2216 || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE)) 2217 && ($displayParts['text_btn'] == '1') 2218 ) { 2219 $display_params['emptyafter'] 2220 = ($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) 2221 && ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE) ? 4 : 1; 2222 2223 $right_column_html .= "\n" 2224 . '<th class="column_action print_ignore" ' . $colspan . '>' 2225 . $full_or_partial_text_link 2226 . '</th>'; 2227 } elseif (($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_LEFT) 2228 || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH) 2229 && (($displayParts['edit_lnk'] === self::NO_EDIT_OR_DELETE) 2230 && ($displayParts['del_lnk'] === self::NO_EDIT_OR_DELETE)) 2231 && (! isset($GLOBALS['is_header_sent']) || ! $GLOBALS['is_header_sent']) 2232 ) { 2233 // ... elseif no button, displays empty columns if required 2234 // (unless coming from Browse mode print view) 2235 2236 $display_params['emptyafter'] 2237 = ($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) 2238 && ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE) ? 4 : 1; 2239 2240 $right_column_html .= "\n" . '<td class="print_ignore" ' . $colspan 2241 . '></td>'; 2242 } 2243 2244 $this->properties['display_params'] = $display_params; 2245 2246 return $right_column_html; 2247 } 2248 2249 /** 2250 * Prepares the display for a value 2251 * 2252 * @see getDataCellForGeometryColumns(), 2253 * getDataCellForNonNumericColumns() 2254 * 2255 * @param string $class class of table cell 2256 * @param bool $conditionField whether to add CSS class condition 2257 * @param string $value value to display 2258 * 2259 * @return string the td 2260 * 2261 * @access private 2262 */ 2263 private function buildValueDisplay($class, $conditionField, $value) 2264 { 2265 return $this->template->render('display/results/value_display', [ 2266 'class' => $class, 2267 'condition_field' => $conditionField, 2268 'value' => $value, 2269 ]); 2270 } 2271 2272 /** 2273 * Prepares the display for a null value 2274 * 2275 * @see getDataCellForNumericColumns(), 2276 * getDataCellForGeometryColumns(), 2277 * getDataCellForNonNumericColumns() 2278 * 2279 * @param string $class class of table cell 2280 * @param bool $conditionField whether to add CSS class condition 2281 * @param stdClass $meta the meta-information about this field 2282 * @param string $align cell alignment 2283 * 2284 * @return string the td 2285 * 2286 * @access private 2287 */ 2288 private function buildNullDisplay($class, $conditionField, $meta, $align = '') 2289 { 2290 $classes = $this->addClass($class, $conditionField, $meta, ''); 2291 2292 return $this->template->render('display/results/null_display', [ 2293 'align' => $align, 2294 'meta' => $meta, 2295 'classes' => $classes, 2296 ]); 2297 } 2298 2299 /** 2300 * Prepares the display for an empty value 2301 * 2302 * @see getDataCellForNumericColumns(), 2303 * getDataCellForGeometryColumns(), 2304 * getDataCellForNonNumericColumns() 2305 * 2306 * @param string $class class of table cell 2307 * @param bool $conditionField whether to add CSS class condition 2308 * @param stdClass $meta the meta-information about this field 2309 * @param string $align cell alignment 2310 * 2311 * @return string the td 2312 * 2313 * @access private 2314 */ 2315 private function buildEmptyDisplay($class, $conditionField, $meta, $align = '') 2316 { 2317 $classes = $this->addClass($class, $conditionField, $meta, 'nowrap'); 2318 2319 return $this->template->render('display/results/empty_display', [ 2320 'align' => $align, 2321 'classes' => $classes, 2322 ]); 2323 } 2324 2325 /** 2326 * Adds the relevant classes. 2327 * 2328 * @see buildNullDisplay(), getRowData() 2329 * 2330 * @param string $class class of table cell 2331 * @param bool $condition_field whether to add CSS class 2332 * condition 2333 * @param stdClass $meta the meta-information about the 2334 * field 2335 * @param string $nowrap avoid wrapping 2336 * @param bool $is_field_truncated is field truncated (display ...) 2337 * @param TransformationsPlugin|string $transformation_plugin transformation plugin. 2338 * Can also be the default function: 2339 * Core::mimeDefaultFunction 2340 * @param string $default_function default transformation function 2341 * 2342 * @return string the list of classes 2343 * 2344 * @access private 2345 */ 2346 private function addClass( 2347 $class, 2348 $condition_field, 2349 $meta, 2350 $nowrap, 2351 $is_field_truncated = false, 2352 $transformation_plugin = '', 2353 $default_function = '' 2354 ) { 2355 $classes = [ 2356 $class, 2357 $nowrap, 2358 ]; 2359 2360 if (isset($meta->mimetype)) { 2361 $classes[] = preg_replace('/\//', '_', $meta->mimetype); 2362 } 2363 2364 if ($condition_field) { 2365 $classes[] = 'condition'; 2366 } 2367 2368 if ($is_field_truncated) { 2369 $classes[] = 'truncated'; 2370 } 2371 2372 $mime_map = $this->properties['mime_map']; 2373 $orgFullColName = $this->properties['db'] . '.' . $meta->orgtable 2374 . '.' . $meta->orgname; 2375 if ($transformation_plugin != $default_function 2376 || ! empty($mime_map[$orgFullColName]['input_transformation']) 2377 ) { 2378 $classes[] = 'transformed'; 2379 } 2380 2381 // Define classes to be added to this data field based on the type of data 2382 $matches = [ 2383 'enum' => 'enum', 2384 'set' => 'set', 2385 'binary' => 'hex', 2386 ]; 2387 2388 foreach ($matches as $key => $value) { 2389 if (mb_strpos($meta->flags, $key) === false) { 2390 continue; 2391 } 2392 2393 $classes[] = $value; 2394 } 2395 2396 if (mb_strpos($meta->type, 'bit') !== false) { 2397 $classes[] = 'bit'; 2398 } 2399 2400 return implode(' ', $classes); 2401 } 2402 2403 /** 2404 * Prepare the body of the results table 2405 * 2406 * @see getTable() 2407 * 2408 * @param int $dt_result the link id associated to the query 2409 * which results have to be displayed 2410 * @param array $displayParts which elements to display 2411 * @param array $map the list of relations 2412 * @param array $analyzed_sql_results analyzed sql results 2413 * @param bool $is_limited_display with limited operations or not 2414 * 2415 * @return string html content 2416 * 2417 * @global array $row current row data 2418 * @access private 2419 */ 2420 private function getTableBody( 2421 &$dt_result, 2422 array &$displayParts, 2423 array $map, 2424 array $analyzed_sql_results, 2425 $is_limited_display = false 2426 ) { 2427 global $dbi; 2428 2429 // Mostly because of browser transformations, to make the row-data accessible in a plugin. 2430 global $row; 2431 2432 $table_body_html = ''; 2433 2434 // query without conditions to shorten URLs when needed, 200 is just 2435 // guess, it should depend on remaining URL length 2436 $url_sql_query = $this->getUrlSqlQuery($analyzed_sql_results); 2437 2438 $display_params = $this->properties['display_params']; 2439 2440 if (! is_array($map)) { 2441 $map = []; 2442 } 2443 2444 $row_no = 0; 2445 $display_params['edit'] = []; 2446 $display_params['copy'] = []; 2447 $display_params['delete'] = []; 2448 $display_params['data'] = []; 2449 $display_params['row_delete'] = []; 2450 $this->properties['display_params'] = $display_params; 2451 2452 // name of the class added to all grid editable elements; 2453 // if we don't have all the columns of a unique key in the result set, 2454 // do not permit grid editing 2455 if ($is_limited_display || ! $this->properties['editable']) { 2456 $grid_edit_class = ''; 2457 } else { 2458 switch ($GLOBALS['cfg']['GridEditing']) { 2459 case 'double-click': 2460 // trying to reduce generated HTML by using shorter 2461 // classes like click1 and click2 2462 $grid_edit_class = 'grid_edit click2'; 2463 break; 2464 case 'click': 2465 $grid_edit_class = 'grid_edit click1'; 2466 break; 2467 default: // 'disabled' 2468 $grid_edit_class = ''; 2469 break; 2470 } 2471 } 2472 2473 // prepare to get the column order, if available 2474 [$col_order, $col_visib] = $this->getColumnParams( 2475 $analyzed_sql_results 2476 ); 2477 2478 // Correction University of Virginia 19991216 in the while below 2479 // Previous code assumed that all tables have keys, specifically that 2480 // the phpMyAdmin GUI should support row delete/edit only for such 2481 // tables. 2482 // Although always using keys is arguably the prescribed way of 2483 // defining a relational table, it is not required. This will in 2484 // particular be violated by the novice. 2485 // We want to encourage phpMyAdmin usage by such novices. So the code 2486 // below has been changed to conditionally work as before when the 2487 // table being displayed has one or more keys; but to display 2488 // delete/edit options correctly for tables without keys. 2489 2490 $whereClauseMap = $this->properties['whereClauseMap']; 2491 while ($row = $dbi->fetchRow($dt_result)) { 2492 // add repeating headers 2493 if (($row_no != 0) && ($_SESSION['tmpval']['repeat_cells'] != 0) 2494 && ! $row_no % $_SESSION['tmpval']['repeat_cells'] 2495 ) { 2496 $table_body_html .= $this->getRepeatingHeaders( 2497 $display_params 2498 ); 2499 } 2500 2501 $tr_class = []; 2502 if ($GLOBALS['cfg']['BrowsePointerEnable'] != true) { 2503 $tr_class[] = 'nopointer'; 2504 } 2505 if ($GLOBALS['cfg']['BrowseMarkerEnable'] != true) { 2506 $tr_class[] = 'nomarker'; 2507 } 2508 2509 // pointer code part 2510 $classes = (empty($tr_class) ? ' ' : 'class="' . implode(' ', $tr_class) . '"'); 2511 $table_body_html .= '<tr ' . $classes . ' >'; 2512 2513 // 1. Prepares the row 2514 2515 // In print view these variable needs to be initialized 2516 $del_url = null; 2517 $del_str = null; 2518 $edit_str = null; 2519 $js_conf = null; 2520 $copy_url = null; 2521 $copy_str = null; 2522 $edit_url = null; 2523 $editCopyUrlParams = null; 2524 $delUrlParams = null; 2525 2526 // 1.2 Defines the URLs for the modify/delete link(s) 2527 2528 if (($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) 2529 || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE) 2530 ) { 2531 $expressions = []; 2532 2533 if (isset($analyzed_sql_results['statement']) 2534 && $analyzed_sql_results['statement'] instanceof SelectStatement 2535 ) { 2536 $expressions = $analyzed_sql_results['statement']->expr; 2537 } 2538 2539 // Results from a "SELECT" statement -> builds the 2540 // WHERE clause to use in links (a unique key if possible) 2541 /** 2542 * @todo $where_clause could be empty, for example a table 2543 * with only one field and it's a BLOB; in this case, 2544 * avoid to display the delete and edit links 2545 */ 2546 [$where_clause, $clause_is_unique, $condition_array] = Util::getUniqueCondition( 2547 $dt_result, 2548 $this->properties['fields_cnt'], 2549 $this->properties['fields_meta'], 2550 $row, 2551 false, 2552 $this->properties['table'], 2553 $expressions 2554 ); 2555 $whereClauseMap[$row_no][$this->properties['table']] = $where_clause; 2556 $this->properties['whereClauseMap'] = $whereClauseMap; 2557 2558 // 1.2.1 Modify link(s) - update row case 2559 if ($displayParts['edit_lnk'] === self::UPDATE_ROW) { 2560 [ 2561 $edit_url, 2562 $copy_url, 2563 $edit_str, 2564 $copy_str, 2565 $editCopyUrlParams, 2566 ] 2567 = $this->getModifiedLinks( 2568 $where_clause, 2569 $clause_is_unique, 2570 $url_sql_query 2571 ); 2572 } 2573 2574 // 1.2.2 Delete/Kill link(s) 2575 [$del_url, $del_str, $js_conf, $delUrlParams] 2576 = $this->getDeleteAndKillLinks( 2577 $where_clause, 2578 $clause_is_unique, 2579 $url_sql_query, 2580 $displayParts['del_lnk'], 2581 $row 2582 ); 2583 2584 // 1.3 Displays the links at left if required 2585 if (($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_LEFT) 2586 || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH) 2587 ) { 2588 $table_body_html .= $this->template->render('display/results/checkbox_and_links', [ 2589 'position' => self::POSITION_LEFT, 2590 'has_checkbox' => ! empty($del_url) && $displayParts['del_lnk'] !== self::KILL_PROCESS, 2591 'edit' => [ 2592 'url' => $edit_url, 2593 'params' => $editCopyUrlParams + ['default_action' => 'update'], 2594 'string' => $edit_str, 2595 'clause_is_unique' => $clause_is_unique, 2596 ], 2597 'copy' => [ 2598 'url' => $copy_url, 2599 'params' => $editCopyUrlParams + ['default_action' => 'insert'], 2600 'string' => $copy_str, 2601 ], 2602 'delete' => ['url' => $del_url, 'params' => $delUrlParams, 'string' => $del_str], 2603 'row_number' => $row_no, 2604 'where_clause' => $where_clause, 2605 'condition' => json_encode($condition_array), 2606 'is_ajax' => Response::getInstance()->isAjax(), 2607 'js_conf' => $js_conf ?? '', 2608 ]); 2609 } elseif ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_NONE) { 2610 $table_body_html .= $this->template->render('display/results/checkbox_and_links', [ 2611 'position' => self::POSITION_NONE, 2612 'has_checkbox' => ! empty($del_url) && $displayParts['del_lnk'] !== self::KILL_PROCESS, 2613 'edit' => [ 2614 'url' => $edit_url, 2615 'params' => $editCopyUrlParams + ['default_action' => 'update'], 2616 'string' => $edit_str, 2617 'clause_is_unique' => $clause_is_unique, 2618 ], 2619 'copy' => [ 2620 'url' => $copy_url, 2621 'params' => $editCopyUrlParams + ['default_action' => 'insert'], 2622 'string' => $copy_str, 2623 ], 2624 'delete' => ['url' => $del_url, 'params' => $delUrlParams, 'string' => $del_str], 2625 'row_number' => $row_no, 2626 'where_clause' => $where_clause, 2627 'condition' => json_encode($condition_array), 2628 'is_ajax' => Response::getInstance()->isAjax(), 2629 'js_conf' => $js_conf ?? '', 2630 ]); 2631 } 2632 } 2633 2634 // 2. Displays the rows' values 2635 if ($this->properties['mime_map'] === null) { 2636 $this->setMimeMap(); 2637 } 2638 $table_body_html .= $this->getRowValues( 2639 $dt_result, 2640 $row, 2641 $row_no, 2642 $col_order, 2643 $map, 2644 $grid_edit_class, 2645 $col_visib, 2646 $url_sql_query, 2647 $analyzed_sql_results 2648 ); 2649 2650 // 3. Displays the modify/delete links on the right if required 2651 if (($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) 2652 || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE) 2653 ) { 2654 if (($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_RIGHT) 2655 || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH) 2656 ) { 2657 $table_body_html .= $this->template->render('display/results/checkbox_and_links', [ 2658 'position' => self::POSITION_RIGHT, 2659 'has_checkbox' => ! empty($del_url) && $displayParts['del_lnk'] !== self::KILL_PROCESS, 2660 'edit' => [ 2661 'url' => $edit_url, 2662 'params' => $editCopyUrlParams + ['default_action' => 'update'], 2663 'string' => $edit_str, 2664 'clause_is_unique' => $clause_is_unique ?? true, 2665 ], 2666 'copy' => [ 2667 'url' => $copy_url, 2668 'params' => $editCopyUrlParams + ['default_action' => 'insert'], 2669 'string' => $copy_str, 2670 ], 2671 'delete' => ['url' => $del_url, 'params' => $delUrlParams, 'string' => $del_str], 2672 'row_number' => $row_no, 2673 'where_clause' => $where_clause ?? '', 2674 'condition' => json_encode($condition_array ?? []), 2675 'is_ajax' => Response::getInstance()->isAjax(), 2676 'js_conf' => $js_conf ?? '', 2677 ]); 2678 } 2679 } 2680 2681 $table_body_html .= '</tr>'; 2682 $table_body_html .= "\n"; 2683 $row_no++; 2684 } 2685 2686 return $table_body_html; 2687 } 2688 2689 /** 2690 * Sets the MIME details of the columns in the results set 2691 * 2692 * @return void 2693 */ 2694 private function setMimeMap() 2695 { 2696 $fields_meta = $this->properties['fields_meta']; 2697 $mimeMap = []; 2698 $added = []; 2699 2700 for ($currentColumn = 0; $currentColumn < $this->properties['fields_cnt']; ++$currentColumn) { 2701 $meta = $fields_meta[$currentColumn]; 2702 $orgFullTableName = $this->properties['db'] . '.' . $meta->orgtable; 2703 2704 if (! $GLOBALS['cfgRelation']['commwork'] 2705 || ! $GLOBALS['cfgRelation']['mimework'] 2706 || ! $GLOBALS['cfg']['BrowseMIME'] 2707 || $_SESSION['tmpval']['hide_transformation'] 2708 || ! empty($added[$orgFullTableName]) 2709 ) { 2710 continue; 2711 } 2712 2713 $mimeMap = array_merge( 2714 $mimeMap, 2715 $this->transformations->getMime($this->properties['db'], $meta->orgtable, false, true) ?? [] 2716 ); 2717 $added[$orgFullTableName] = true; 2718 } 2719 2720 // special browser transformation for some SHOW statements 2721 if ($this->properties['is_show'] 2722 && ! $_SESSION['tmpval']['hide_transformation'] 2723 ) { 2724 preg_match( 2725 '@^SHOW[[:space:]]+(VARIABLES|(FULL[[:space:]]+)?' 2726 . 'PROCESSLIST|STATUS|TABLE|GRANTS|CREATE|LOGS|DATABASES|FIELDS' 2727 . ')@i', 2728 $this->properties['sql_query'], 2729 $which 2730 ); 2731 2732 if (isset($which[1])) { 2733 $str = ' ' . strtoupper($which[1]); 2734 $isShowProcessList = strpos($str, 'PROCESSLIST') > 0; 2735 if ($isShowProcessList) { 2736 $mimeMap['..Info'] = [ 2737 'mimetype' => 'Text_Plain', 2738 'transformation' => 'output/Text_Plain_Sql.php', 2739 ]; 2740 } 2741 2742 $isShowCreateTable = preg_match( 2743 '@CREATE[[:space:]]+TABLE@i', 2744 $this->properties['sql_query'] 2745 ); 2746 if ($isShowCreateTable) { 2747 $mimeMap['..Create Table'] = [ 2748 'mimetype' => 'Text_Plain', 2749 'transformation' => 'output/Text_Plain_Sql.php', 2750 ]; 2751 } 2752 } 2753 } 2754 2755 $this->properties['mime_map'] = $mimeMap; 2756 } 2757 2758 /** 2759 * Get the values for one data row 2760 * 2761 * @see getTableBody() 2762 * 2763 * @param int $dt_result the link id associated to the query 2764 * which results have to be displayed 2765 * @param array $row current row data 2766 * @param int $row_no the index of current row 2767 * @param array|false $col_order the column order false when 2768 * a property not found false 2769 * when a property not found 2770 * @param array $map the list of relations 2771 * @param string $grid_edit_class the class for all editable 2772 * columns 2773 * @param bool|array|string $col_visib column is visible(false); 2774 * column isn't visible(string 2775 * array) 2776 * @param string $url_sql_query the analyzed sql query 2777 * @param array $analyzed_sql_results analyzed sql results 2778 * 2779 * @return string html content 2780 * 2781 * @access private 2782 */ 2783 private function getRowValues( 2784 &$dt_result, 2785 array $row, 2786 $row_no, 2787 $col_order, 2788 array $map, 2789 $grid_edit_class, 2790 $col_visib, 2791 $url_sql_query, 2792 array $analyzed_sql_results 2793 ) { 2794 $row_values_html = ''; 2795 2796 // Following variable are needed for use in isset/empty or 2797 // use with array indexes/safe use in foreach 2798 $sql_query = $this->properties['sql_query']; 2799 $fields_meta = $this->properties['fields_meta']; 2800 $highlight_columns = $this->properties['highlight_columns']; 2801 $mime_map = $this->properties['mime_map']; 2802 2803 $row_info = $this->getRowInfoForSpecialLinks($row, $col_order); 2804 2805 $whereClauseMap = $this->properties['whereClauseMap']; 2806 2807 $columnCount = $this->properties['fields_cnt']; 2808 2809 // Load SpecialSchemaLinks for all rows 2810 $specialSchemaLinks = SpecialSchemaLinks::get(); 2811 2812 for ($currentColumn = 0; $currentColumn < $columnCount; ++$currentColumn) { 2813 // assign $i with appropriate column order 2814 $i = is_array($col_order) ? $col_order[$currentColumn] : $currentColumn; 2815 2816 $meta = $fields_meta[$i]; 2817 $orgFullColName 2818 = $this->properties['db'] . '.' . $meta->orgtable . '.' . $meta->orgname; 2819 2820 $not_null_class = $meta->not_null ? 'not_null' : ''; 2821 $relation_class = isset($map[$meta->name]) ? 'relation' : ''; 2822 $hide_class = is_array($col_visib) && isset($col_visib[$currentColumn]) && ! $col_visib[$currentColumn] 2823 ? 'hide' 2824 : ''; 2825 $grid_edit = $meta->orgtable != '' ? $grid_edit_class : ''; 2826 2827 // handle datetime-related class, for grid editing 2828 $field_type_class 2829 = $this->getClassForDateTimeRelatedFields($meta->type); 2830 2831 $is_field_truncated = false; 2832 // combine all the classes applicable to this column's value 2833 $class = $this->getClassesForColumn( 2834 $grid_edit, 2835 $not_null_class, 2836 $relation_class, 2837 $hide_class, 2838 $field_type_class 2839 ); 2840 2841 // See if this column should get highlight because it's used in the 2842 // where-query. 2843 $condition_field = isset($highlight_columns) 2844 && (isset($highlight_columns[$meta->name]) 2845 || isset($highlight_columns[Util::backquote($meta->name)])); 2846 2847 // Wrap MIME-transformations. [MIME] 2848 $default_function = [ 2849 Core::class, 2850 'mimeDefaultFunction', 2851 ]; // default_function 2852 $transformation_plugin = $default_function; 2853 $transform_options = []; 2854 2855 if ($GLOBALS['cfgRelation']['mimework'] 2856 && $GLOBALS['cfg']['BrowseMIME'] 2857 ) { 2858 if (isset($mime_map[$orgFullColName]['mimetype']) 2859 && ! empty($mime_map[$orgFullColName]['transformation']) 2860 ) { 2861 $file = $mime_map[$orgFullColName]['transformation']; 2862 $include_file = 'libraries/classes/Plugins/Transformations/' . $file; 2863 2864 if (@file_exists(ROOT_PATH . $include_file)) { 2865 $class_name = $this->transformations->getClassName($include_file); 2866 if (class_exists($class_name)) { 2867 // todo add $plugin_manager 2868 $plugin_manager = null; 2869 $transformation_plugin = new $class_name( 2870 $plugin_manager 2871 ); 2872 2873 $transform_options = $this->transformations->getOptions( 2874 $mime_map[$orgFullColName]['transformation_options'] ?? '' 2875 ); 2876 2877 $meta->mimetype = str_replace( 2878 '_', 2879 '/', 2880 $mime_map[$orgFullColName]['mimetype'] 2881 ); 2882 } 2883 } 2884 } 2885 } 2886 2887 // Check whether the field needs to display with syntax highlighting 2888 2889 $dbLower = mb_strtolower($this->properties['db']); 2890 $tblLower = mb_strtolower($meta->orgtable); 2891 $nameLower = mb_strtolower($meta->orgname); 2892 if (! empty($this->transformationInfo[$dbLower][$tblLower][$nameLower]) 2893 && isset($row[$i]) 2894 && (trim($row[$i]) != '') 2895 && ! $_SESSION['tmpval']['hide_transformation'] 2896 ) { 2897 include_once ROOT_PATH . $this->transformationInfo[$dbLower][$tblLower][$nameLower][0]; 2898 $transformation_plugin = new $this->transformationInfo[$dbLower][$tblLower][$nameLower][1](null); 2899 2900 $transform_options = $this->transformations->getOptions( 2901 $mime_map[$orgFullColName]['transformation_options'] ?? '' 2902 ); 2903 2904 $orgTable = mb_strtolower($meta->orgtable); 2905 $orgName = mb_strtolower($meta->orgname); 2906 2907 $meta->mimetype = str_replace( 2908 '_', 2909 '/', 2910 $this->transformationInfo[$dbLower][$orgTable][$orgName][2] 2911 ); 2912 } 2913 2914 // Check for the predefined fields need to show as link in schemas 2915 if (! empty($specialSchemaLinks[$dbLower][$tblLower][$nameLower])) { 2916 $linking_url = $this->getSpecialLinkUrl( 2917 $specialSchemaLinks[$dbLower][$tblLower][$nameLower], 2918 $row[$i], 2919 $row_info 2920 ); 2921 $transformation_plugin = new Text_Plain_Link(); 2922 2923 $transform_options = [ 2924 0 => $linking_url, 2925 2 => true, 2926 ]; 2927 2928 $meta->mimetype = str_replace( 2929 '_', 2930 '/', 2931 'Text/Plain' 2932 ); 2933 } 2934 2935 $expressions = []; 2936 2937 if (isset($analyzed_sql_results['statement']) 2938 && $analyzed_sql_results['statement'] instanceof SelectStatement 2939 ) { 2940 $expressions = $analyzed_sql_results['statement']->expr; 2941 } 2942 2943 /** 2944 * The result set can have columns from more than one table, 2945 * this is why we have to check for the unique conditions 2946 * related to this table; however getUniqueCondition() is 2947 * costly and does not need to be called if we already know 2948 * the conditions for the current table. 2949 */ 2950 if (! isset($whereClauseMap[$row_no][$meta->orgtable])) { 2951 $unique_conditions = Util::getUniqueCondition( 2952 $dt_result, 2953 $this->properties['fields_cnt'], 2954 $this->properties['fields_meta'], 2955 $row, 2956 false, 2957 $meta->orgtable, 2958 $expressions 2959 ); 2960 $whereClauseMap[$row_no][$meta->orgtable] = $unique_conditions[0]; 2961 } 2962 2963 $_url_params = [ 2964 'db' => $this->properties['db'], 2965 'table' => $meta->orgtable, 2966 'where_clause_sign' => Core::signSqlQuery($whereClauseMap[$row_no][$meta->orgtable]), 2967 'where_clause' => $whereClauseMap[$row_no][$meta->orgtable], 2968 'transform_key' => $meta->orgname, 2969 ]; 2970 2971 if (! empty($sql_query)) { 2972 $_url_params['sql_query'] = $url_sql_query; 2973 } 2974 2975 $transform_options['wrapper_link'] = Url::getCommon($_url_params); 2976 $transform_options['wrapper_params'] = $_url_params; 2977 2978 $display_params = $this->properties['display_params']; 2979 2980 // in some situations (issue 11406), numeric returns 1 2981 // even for a string type 2982 // for decimal numeric is returning 1 2983 // have to improve logic 2984 // Nullable text fields and text fields have the blob flag (issue 16896) 2985 $isNumericAndNotBlob = $meta->numeric == 1 && $meta->blob == 0; 2986 if (($isNumericAndNotBlob && $meta->type !== 'string') || $meta->type === 'real') { 2987 // n u m e r i c 2988 2989 $display_params['data'][$row_no][$i] 2990 = $this->getDataCellForNumericColumns( 2991 $row[$i] === null ? null : (string) $row[$i], 2992 $class, 2993 $condition_field, 2994 $meta, 2995 $map, 2996 $is_field_truncated, 2997 $analyzed_sql_results, 2998 $transformation_plugin, 2999 $default_function, 3000 $transform_options 3001 ); 3002 } elseif ($meta->type === self::GEOMETRY_FIELD) { 3003 // g e o m e t r y 3004 3005 // Remove 'grid_edit' from $class as we do not allow to 3006 // inline-edit geometry data. 3007 $class = str_replace('grid_edit', '', $class); 3008 3009 $display_params['data'][$row_no][$i] 3010 = $this->getDataCellForGeometryColumns( 3011 $row[$i] === null ? null : (string) $row[$i], 3012 $class, 3013 $meta, 3014 $map, 3015 $_url_params, 3016 $condition_field, 3017 $transformation_plugin, 3018 $default_function, 3019 $transform_options, 3020 $analyzed_sql_results 3021 ); 3022 } else { 3023 // n o t n u m e r i c 3024 3025 $display_params['data'][$row_no][$i] 3026 = $this->getDataCellForNonNumericColumns( 3027 $row[$i] === null ? null : (string) $row[$i], 3028 $class, 3029 $meta, 3030 $map, 3031 $_url_params, 3032 $condition_field, 3033 $transformation_plugin, 3034 $default_function, 3035 $transform_options, 3036 $is_field_truncated, 3037 $analyzed_sql_results, 3038 $dt_result, 3039 $i 3040 ); 3041 } 3042 3043 // output stored cell 3044 $row_values_html .= $display_params['data'][$row_no][$i]; 3045 3046 if (isset($display_params['rowdata'][$i][$row_no])) { 3047 $display_params['rowdata'][$i][$row_no] 3048 .= $display_params['data'][$row_no][$i]; 3049 } else { 3050 $display_params['rowdata'][$i][$row_no] 3051 = $display_params['data'][$row_no][$i]; 3052 } 3053 3054 $this->properties['display_params'] = $display_params; 3055 } 3056 3057 return $row_values_html; 3058 } 3059 3060 /** 3061 * Get link for display special schema links 3062 * 3063 * @param array<string,array<int,array<string,string>>|string> $link_relations 3064 * @param string $column_value column value 3065 * @param array $row_info information about row 3066 * 3067 * @return string generated link 3068 * 3069 * @phpstan-param array{ 3070 * 'link_param': string, 3071 * 'link_dependancy_params'?: array< 3072 * int, 3073 * array{'param_info': string, 'column_name': string} 3074 * >, 3075 * 'default_page': string 3076 * } $link_relations 3077 */ 3078 private function getSpecialLinkUrl( 3079 array $link_relations, 3080 $column_value, 3081 array $row_info 3082 ) { 3083 $linking_url_params = []; 3084 3085 $linking_url_params[$link_relations['link_param']] = $column_value; 3086 3087 $divider = strpos($link_relations['default_page'], '?') ? '&' : '?'; 3088 if (empty($link_relations['link_dependancy_params'])) { 3089 return $link_relations['default_page'] 3090 . Url::getCommonRaw($linking_url_params, $divider); 3091 } 3092 3093 foreach ($link_relations['link_dependancy_params'] as $new_param) { 3094 $columnName = mb_strtolower($new_param['column_name']); 3095 3096 // If there is a value for this column name in the row_info provided 3097 if (isset($row_info[$columnName])) { 3098 $urlParameterName = $new_param['param_info']; 3099 $linking_url_params[$urlParameterName] = $row_info[$columnName]; 3100 } 3101 3102 // Special case 1 - when executing routines, according 3103 // to the type of the routine, url param changes 3104 if (empty($row_info['routine_type'])) { 3105 continue; 3106 } 3107 } 3108 3109 return $link_relations['default_page'] 3110 . Url::getCommonRaw($linking_url_params, $divider); 3111 } 3112 3113 /** 3114 * Prepare row information for display special links 3115 * 3116 * @param array $row current row data 3117 * @param array|bool $col_order the column order 3118 * 3119 * @return array associative array with column nama -> value 3120 */ 3121 private function getRowInfoForSpecialLinks(array $row, $col_order) 3122 { 3123 $row_info = []; 3124 $fields_meta = $this->properties['fields_meta']; 3125 3126 for ($n = 0; $n < $this->properties['fields_cnt']; ++$n) { 3127 $m = is_array($col_order) ? $col_order[$n] : $n; 3128 $row_info[mb_strtolower($fields_meta[$m]->orgname)] 3129 = $row[$m]; 3130 } 3131 3132 return $row_info; 3133 } 3134 3135 /** 3136 * Get url sql query without conditions to shorten URLs 3137 * 3138 * @see getTableBody() 3139 * 3140 * @param array $analyzed_sql_results analyzed sql results 3141 * 3142 * @return string analyzed sql query 3143 * 3144 * @access private 3145 */ 3146 private function getUrlSqlQuery(array $analyzed_sql_results) 3147 { 3148 if (($analyzed_sql_results['querytype'] !== 'SELECT') 3149 || (mb_strlen($this->properties['sql_query']) < 200) 3150 ) { 3151 return $this->properties['sql_query']; 3152 } 3153 3154 $query = 'SELECT ' . Query::getClause( 3155 $analyzed_sql_results['statement'], 3156 $analyzed_sql_results['parser']->list, 3157 'SELECT' 3158 ); 3159 3160 $from_clause = Query::getClause( 3161 $analyzed_sql_results['statement'], 3162 $analyzed_sql_results['parser']->list, 3163 'FROM' 3164 ); 3165 3166 if (! empty($from_clause)) { 3167 $query .= ' FROM ' . $from_clause; 3168 } 3169 3170 return $query; 3171 } 3172 3173 /** 3174 * Get column order and column visibility 3175 * 3176 * @see getTableBody() 3177 * 3178 * @param array $analyzed_sql_results analyzed sql results 3179 * 3180 * @return array 2 element array - $col_order, $col_visib 3181 * 3182 * @access private 3183 */ 3184 private function getColumnParams(array $analyzed_sql_results) 3185 { 3186 if ($this->isSelect($analyzed_sql_results)) { 3187 $pmatable = new Table($this->properties['table'], $this->properties['db']); 3188 $col_order = $pmatable->getUiProp(Table::PROP_COLUMN_ORDER); 3189 /* Validate the value */ 3190 if ($col_order !== false) { 3191 $fields_cnt = $this->properties['fields_cnt']; 3192 foreach ($col_order as $value) { 3193 if ($value < $fields_cnt) { 3194 continue; 3195 } 3196 3197 $pmatable->removeUiProp(Table::PROP_COLUMN_ORDER); 3198 $fields_cnt = false; 3199 } 3200 } 3201 $col_visib = $pmatable->getUiProp(Table::PROP_COLUMN_VISIB); 3202 } else { 3203 $col_order = false; 3204 $col_visib = false; 3205 } 3206 3207 return [ 3208 $col_order, 3209 $col_visib, 3210 ]; 3211 } 3212 3213 /** 3214 * Get HTML for repeating headers 3215 * 3216 * @see getTableBody() 3217 * 3218 * @param array $display_params holds various display info 3219 * 3220 * @return string html content 3221 * 3222 * @access private 3223 */ 3224 private function getRepeatingHeaders( 3225 array $display_params 3226 ) { 3227 $header_html = '<tr>' . "\n"; 3228 3229 if ($display_params['emptypre'] > 0) { 3230 $header_html .= ' <th colspan="' 3231 . $display_params['emptypre'] . '">' 3232 . "\n" . ' </th>' . "\n"; 3233 } elseif ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_NONE) { 3234 $header_html .= ' <th></th>' . "\n"; 3235 } 3236 3237 foreach ($display_params['desc'] as $val) { 3238 $header_html .= $val; 3239 } 3240 3241 if ($display_params['emptyafter'] > 0) { 3242 $header_html 3243 .= ' <th colspan="' . $display_params['emptyafter'] 3244 . '">' 3245 . "\n" . ' </th>' . "\n"; 3246 } 3247 $header_html .= '</tr>' . "\n"; 3248 3249 return $header_html; 3250 } 3251 3252 /** 3253 * Get modified links 3254 * 3255 * @see getTableBody() 3256 * 3257 * @param string $where_clause the where clause of the sql 3258 * @param bool $clause_is_unique the unique condition of clause 3259 * @param string $url_sql_query the analyzed sql query 3260 * 3261 * @return array<int,string|array> 5 element array - $edit_url, $copy_url, 3262 * $edit_str, $copy_str 3263 * 3264 * @access private 3265 */ 3266 private function getModifiedLinks( 3267 $where_clause, 3268 $clause_is_unique, 3269 $url_sql_query 3270 ) { 3271 $_url_params = [ 3272 'db' => $this->properties['db'], 3273 'table' => $this->properties['table'], 3274 'where_clause' => $where_clause, 3275 'clause_is_unique' => $clause_is_unique, 3276 'sql_query' => $url_sql_query, 3277 'goto' => Url::getFromRoute('/sql'), 3278 ]; 3279 3280 $edit_url = Url::getFromRoute('/table/change'); 3281 3282 $copy_url = Url::getFromRoute('/table/change'); 3283 3284 $edit_str = $this->getActionLinkContent( 3285 'b_edit', 3286 __('Edit') 3287 ); 3288 $copy_str = $this->getActionLinkContent( 3289 'b_insrow', 3290 __('Copy') 3291 ); 3292 3293 return [ 3294 $edit_url, 3295 $copy_url, 3296 $edit_str, 3297 $copy_str, 3298 $_url_params, 3299 ]; 3300 } 3301 3302 /** 3303 * Get delete and kill links 3304 * 3305 * @see getTableBody() 3306 * 3307 * @param string $where_clause the where clause of the sql 3308 * @param bool $clause_is_unique the unique condition of clause 3309 * @param string $url_sql_query the analyzed sql query 3310 * @param string $del_lnk the delete link of current row 3311 * @param array $row the current row 3312 * 3313 * @return array 3 element array 3314 * $del_url, $del_str, $js_conf 3315 * 3316 * @access private 3317 */ 3318 private function getDeleteAndKillLinks( 3319 $where_clause, 3320 $clause_is_unique, 3321 $url_sql_query, 3322 $del_lnk, 3323 array $row 3324 ) { 3325 global $dbi; 3326 3327 $goto = $this->properties['goto']; 3328 3329 if ($del_lnk === self::DELETE_ROW) { // delete row case 3330 $_url_params = [ 3331 'db' => $this->properties['db'], 3332 'table' => $this->properties['table'], 3333 'sql_query' => $url_sql_query, 3334 'message_to_show' => __('The row has been deleted.'), 3335 'goto' => empty($goto) ? Url::getFromRoute('/table/sql') : $goto, 3336 ]; 3337 3338 $lnk_goto = Url::getFromRoute('/sql', $_url_params); 3339 3340 $del_query = 'DELETE FROM ' 3341 . Util::backquote($this->properties['table']) 3342 . ' WHERE ' . $where_clause . 3343 ($clause_is_unique ? '' : ' LIMIT 1'); 3344 3345 $_url_params = [ 3346 'db' => $this->properties['db'], 3347 'table' => $this->properties['table'], 3348 'sql_query' => $del_query, 3349 'message_to_show' => __('The row has been deleted.'), 3350 'goto' => $lnk_goto, 3351 ]; 3352 $del_url = Url::getFromRoute('/sql'); 3353 3354 $js_conf = 'DELETE FROM ' . $this->properties['table'] 3355 . ' WHERE ' . $where_clause 3356 . ($clause_is_unique ? '' : ' LIMIT 1'); 3357 3358 $del_str = $this->getActionLinkContent('b_drop', __('Delete')); 3359 } elseif ($del_lnk === self::KILL_PROCESS) { // kill process case 3360 $_url_params = [ 3361 'db' => $this->properties['db'], 3362 'table' => $this->properties['table'], 3363 'sql_query' => $url_sql_query, 3364 'goto' => Url::getFromRoute('/'), 3365 ]; 3366 3367 $lnk_goto = Url::getFromRoute('/sql', $_url_params); 3368 3369 $kill = $dbi->getKillQuery((int) $row[0]); 3370 3371 $_url_params = [ 3372 'db' => 'mysql', 3373 'sql_query' => $kill, 3374 'goto' => $lnk_goto, 3375 ]; 3376 3377 $del_url = Url::getFromRoute('/sql'); 3378 $js_conf = $kill; 3379 $del_str = Generator::getIcon( 3380 'b_drop', 3381 __('Kill') 3382 ); 3383 } else { 3384 $del_url = $del_str = $js_conf = $_url_params = null; 3385 } 3386 3387 return [ 3388 $del_url, 3389 $del_str, 3390 $js_conf, 3391 $_url_params, 3392 ]; 3393 } 3394 3395 /** 3396 * Get content inside the table row action links (Edit/Copy/Delete) 3397 * 3398 * @see getModifiedLinks(), getDeleteAndKillLinks() 3399 * 3400 * @param string $icon The name of the file to get 3401 * @param string $display_text The text displaying after the image icon 3402 * 3403 * @return string 3404 * 3405 * @access private 3406 */ 3407 private function getActionLinkContent($icon, $display_text) 3408 { 3409 $linkContent = ''; 3410 3411 if (isset($GLOBALS['cfg']['RowActionType']) 3412 && $GLOBALS['cfg']['RowActionType'] === self::ACTION_LINK_CONTENT_ICONS 3413 ) { 3414 $linkContent .= '<span class="nowrap">' 3415 . Generator::getImage( 3416 $icon, 3417 $display_text 3418 ) 3419 . '</span>'; 3420 } elseif (isset($GLOBALS['cfg']['RowActionType']) 3421 && $GLOBALS['cfg']['RowActionType'] === self::ACTION_LINK_CONTENT_TEXT 3422 ) { 3423 $linkContent .= '<span class="nowrap">' . $display_text . '</span>'; 3424 } else { 3425 $linkContent .= Generator::getIcon( 3426 $icon, 3427 $display_text 3428 ); 3429 } 3430 3431 return $linkContent; 3432 } 3433 3434 /** 3435 * Get the combined classes for a column 3436 * 3437 * @see getTableBody() 3438 * 3439 * @param string $grid_edit_class the class for all editable columns 3440 * @param string $not_null_class the class for not null columns 3441 * @param string $relation_class the class for relations in a column 3442 * @param string $hide_class the class for visibility of a column 3443 * @param string $field_type_class the class related to type of the field 3444 * 3445 * @return string the combined classes 3446 * 3447 * @access private 3448 */ 3449 private function getClassesForColumn( 3450 $grid_edit_class, 3451 $not_null_class, 3452 $relation_class, 3453 $hide_class, 3454 $field_type_class 3455 ) { 3456 return 'data ' . $grid_edit_class . ' ' . $not_null_class . ' ' 3457 . $relation_class . ' ' . $hide_class . ' ' . $field_type_class; 3458 } 3459 3460 /** 3461 * Get class for datetime related fields 3462 * 3463 * @see getTableBody() 3464 * 3465 * @param string $type the type of the column field 3466 * 3467 * @return string the class for the column 3468 * 3469 * @access private 3470 */ 3471 private function getClassForDateTimeRelatedFields($type) 3472 { 3473 if ((substr($type, 0, 9) === self::TIMESTAMP_FIELD) 3474 || ($type === self::DATETIME_FIELD) 3475 ) { 3476 $field_type_class = 'datetimefield'; 3477 } elseif ($type === self::DATE_FIELD) { 3478 $field_type_class = 'datefield'; 3479 } elseif ($type === self::TIME_FIELD) { 3480 $field_type_class = 'timefield'; 3481 } elseif ($type === self::STRING_FIELD) { 3482 $field_type_class = 'text'; 3483 } else { 3484 $field_type_class = ''; 3485 } 3486 3487 return $field_type_class; 3488 } 3489 3490 /** 3491 * Prepare data cell for numeric type fields 3492 * 3493 * @see getTableBody() 3494 * 3495 * @param string|null $column the column's value 3496 * @param string $class the html class for column 3497 * @param bool $condition_field the column should highlighted 3498 * or not 3499 * @param stdClass $meta the meta-information about this 3500 * field 3501 * @param array $map the list of relations 3502 * @param bool $is_field_truncated the condition for blob data 3503 * replacements 3504 * @param array $analyzed_sql_results the analyzed query 3505 * @param TransformationsPlugin $transformation_plugin the name of transformation plugin 3506 * @param string $default_function the default transformation 3507 * function 3508 * @param array $transform_options the transformation parameters 3509 * 3510 * @return string the prepared cell, html content 3511 * 3512 * @access private 3513 */ 3514 private function getDataCellForNumericColumns( 3515 ?string $column, 3516 $class, 3517 $condition_field, 3518 $meta, 3519 array $map, 3520 $is_field_truncated, 3521 array $analyzed_sql_results, 3522 $transformation_plugin, 3523 $default_function, 3524 array $transform_options 3525 ) { 3526 if (! isset($column) || $column === null) { 3527 $cell = $this->buildNullDisplay( 3528 'text-right ' . $class, 3529 $condition_field, 3530 $meta, 3531 '' 3532 ); 3533 } elseif ($column != '') { 3534 $nowrap = ' nowrap'; 3535 $where_comparison = ' = ' . $column; 3536 3537 $cell = $this->getRowData( 3538 'text-right ' . $class, 3539 $condition_field, 3540 $analyzed_sql_results, 3541 $meta, 3542 $map, 3543 $column, 3544 $column, 3545 $transformation_plugin, 3546 $default_function, 3547 $nowrap, 3548 $where_comparison, 3549 $transform_options, 3550 $is_field_truncated, 3551 '' 3552 ); 3553 } else { 3554 $cell = $this->buildEmptyDisplay( 3555 'text-right ' . $class, 3556 $condition_field, 3557 $meta, 3558 '' 3559 ); 3560 } 3561 3562 return $cell; 3563 } 3564 3565 /** 3566 * Get data cell for geometry type fields 3567 * 3568 * @see getTableBody() 3569 * 3570 * @param string|null $column the relevant column in data row 3571 * @param string $class the html class for column 3572 * @param stdClass $meta the meta-information about 3573 * this field 3574 * @param array $map the list of relations 3575 * @param array $_url_params the parameters for generate url 3576 * @param bool $condition_field the column should highlighted 3577 * or not 3578 * @param TransformationsPlugin $transformation_plugin the name of transformation 3579 * function 3580 * @param string $default_function the default transformation 3581 * function 3582 * @param array $transform_options the transformation parameters 3583 * @param array $analyzed_sql_results the analyzed query 3584 * 3585 * @return string the prepared data cell, html content 3586 * 3587 * @access private 3588 */ 3589 private function getDataCellForGeometryColumns( 3590 ?string $column, 3591 $class, 3592 $meta, 3593 array $map, 3594 array $_url_params, 3595 $condition_field, 3596 $transformation_plugin, 3597 $default_function, 3598 $transform_options, 3599 array $analyzed_sql_results 3600 ) { 3601 if (! isset($column) || $column === null) { 3602 return $this->buildNullDisplay($class, $condition_field, $meta); 3603 } 3604 3605 if ($column == '') { 3606 return $this->buildEmptyDisplay($class, $condition_field, $meta); 3607 } 3608 3609 // Display as [GEOMETRY - (size)] 3610 if ($_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_GEOM) { 3611 $geometry_text = $this->handleNonPrintableContents( 3612 strtoupper(self::GEOMETRY_FIELD), 3613 $column, 3614 $transformation_plugin, 3615 $transform_options, 3616 $default_function, 3617 $meta, 3618 $_url_params 3619 ); 3620 3621 return $this->buildValueDisplay( 3622 $class, 3623 $condition_field, 3624 $geometry_text 3625 ); 3626 } 3627 3628 if ($_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_WKT) { 3629 // Prepare in Well Known Text(WKT) format. 3630 $where_comparison = ' = ' . $column; 3631 3632 // Convert to WKT format 3633 $wktval = Util::asWKT($column); 3634 [ 3635 $is_field_truncated, 3636 $displayedColumn, 3637 // skip 3rd param 3638 ] = $this->getPartialText($wktval); 3639 3640 return $this->getRowData( 3641 $class, 3642 $condition_field, 3643 $analyzed_sql_results, 3644 $meta, 3645 $map, 3646 $wktval, 3647 $displayedColumn, 3648 $transformation_plugin, 3649 $default_function, 3650 '', 3651 $where_comparison, 3652 $transform_options, 3653 $is_field_truncated, 3654 '' 3655 ); 3656 } 3657 3658 // Prepare in Well Known Binary (WKB) format. 3659 3660 if ($_SESSION['tmpval']['display_binary']) { 3661 $where_comparison = ' = ' . $column; 3662 3663 $wkbval = substr(bin2hex($column), 8); 3664 [ 3665 $is_field_truncated, 3666 $displayedColumn, 3667 // skip 3rd param 3668 ] = $this->getPartialText($wkbval); 3669 3670 return $this->getRowData( 3671 $class, 3672 $condition_field, 3673 $analyzed_sql_results, 3674 $meta, 3675 $map, 3676 $wkbval, 3677 $displayedColumn, 3678 $transformation_plugin, 3679 $default_function, 3680 '', 3681 $where_comparison, 3682 $transform_options, 3683 $is_field_truncated, 3684 '' 3685 ); 3686 } 3687 3688 $wkbval = $this->handleNonPrintableContents( 3689 self::BINARY_FIELD, 3690 $column, 3691 $transformation_plugin, 3692 $transform_options, 3693 $default_function, 3694 $meta, 3695 $_url_params 3696 ); 3697 3698 return $this->buildValueDisplay( 3699 $class, 3700 $condition_field, 3701 $wkbval 3702 ); 3703 } 3704 3705 /** 3706 * Get data cell for non numeric type fields 3707 * 3708 * @see getTableBody() 3709 * 3710 * @param string|null $column the relevant column in data row 3711 * @param string $class the html class for column 3712 * @param stdClass $meta the meta-information about 3713 * the field 3714 * @param array $map the list of relations 3715 * @param array $_url_params the parameters for generate 3716 * url 3717 * @param bool $condition_field the column should highlighted 3718 * or not 3719 * @param TransformationsPlugin $transformation_plugin the name of transformation 3720 * function 3721 * @param string $default_function the default transformation 3722 * function 3723 * @param array $transform_options the transformation parameters 3724 * @param bool $is_field_truncated is data truncated due to 3725 * LimitChars 3726 * @param array $analyzed_sql_results the analyzed query 3727 * @param int $dt_result the link id associated to 3728 * the query which results 3729 * have to be displayed 3730 * @param int $col_index the column index 3731 * 3732 * @return string the prepared data cell, html content 3733 * 3734 * @access private 3735 */ 3736 private function getDataCellForNonNumericColumns( 3737 ?string $column, 3738 $class, 3739 $meta, 3740 array $map, 3741 array $_url_params, 3742 $condition_field, 3743 $transformation_plugin, 3744 $default_function, 3745 $transform_options, 3746 $is_field_truncated, 3747 array $analyzed_sql_results, 3748 &$dt_result, 3749 $col_index 3750 ) { 3751 global $dbi; 3752 3753 $original_length = 0; 3754 3755 $is_analyse = $this->properties['is_analyse']; 3756 $field_flags = $dbi->fieldFlags($dt_result, $col_index); 3757 3758 $bIsText = is_object($transformation_plugin) 3759 && strpos($transformation_plugin->getMIMEType(), 'Text') 3760 === false; 3761 3762 // disable inline grid editing 3763 // if binary fields are protected 3764 // or transformation plugin is of non text type 3765 // such as image 3766 if ((stripos($field_flags, self::BINARY_FIELD) !== false 3767 && ($GLOBALS['cfg']['ProtectBinary'] === 'all' 3768 || ($GLOBALS['cfg']['ProtectBinary'] === 'noblob' 3769 && stripos($meta->type, self::BLOB_FIELD) === false) 3770 || ($GLOBALS['cfg']['ProtectBinary'] === 'blob' 3771 && stripos($meta->type, self::BLOB_FIELD) !== false))) 3772 || $bIsText 3773 ) { 3774 $class = str_replace('grid_edit', '', $class); 3775 } 3776 3777 if (! isset($column) || $column === null) { 3778 return $this->buildNullDisplay($class, $condition_field, $meta); 3779 } 3780 3781 if ($column == '') { 3782 return $this->buildEmptyDisplay($class, $condition_field, $meta); 3783 } 3784 3785 // Cut all fields to $GLOBALS['cfg']['LimitChars'] 3786 // (unless it's a link-type transformation or binary) 3787 $originalDataForWhereClause = $column; 3788 $displayedColumn = $column; 3789 if (! (is_object($transformation_plugin) 3790 && strpos($transformation_plugin->getName(), 'Link') !== false) 3791 && stripos($field_flags, self::BINARY_FIELD) === false 3792 ) { 3793 [ 3794 $is_field_truncated, 3795 $column, 3796 $original_length, 3797 ] = $this->getPartialText($column); 3798 } 3799 3800 $formatted = false; 3801 if (isset($meta->_type) && $meta->_type === MYSQLI_TYPE_BIT) { 3802 $displayedColumn = Util::printableBitValue( 3803 (int) $displayedColumn, 3804 (int) $meta->length 3805 ); 3806 3807 // some results of PROCEDURE ANALYSE() are reported as 3808 // being BINARY but they are quite readable, 3809 // so don't treat them as BINARY 3810 } elseif (stripos($field_flags, self::BINARY_FIELD) !== false 3811 && ! (isset($is_analyse) && $is_analyse) 3812 ) { 3813 // we show the BINARY or BLOB message and field's size 3814 // (or maybe use a transformation) 3815 $binary_or_blob = self::BLOB_FIELD; 3816 if ($meta->type === self::STRING_FIELD) { 3817 $binary_or_blob = self::BINARY_FIELD; 3818 } 3819 $displayedColumn = $this->handleNonPrintableContents( 3820 $binary_or_blob, 3821 $displayedColumn, 3822 $transformation_plugin, 3823 $transform_options, 3824 $default_function, 3825 $meta, 3826 $_url_params, 3827 $is_field_truncated 3828 ); 3829 $class = $this->addClass( 3830 $class, 3831 $condition_field, 3832 $meta, 3833 '', 3834 $is_field_truncated, 3835 $transformation_plugin, 3836 $default_function 3837 ); 3838 $result = strip_tags($column); 3839 // disable inline grid editing 3840 // if binary or blob data is not shown 3841 if (stripos($result, $binary_or_blob) !== false) { 3842 $class = str_replace('grid_edit', '', $class); 3843 } 3844 $formatted = true; 3845 } 3846 3847 if ($formatted) { 3848 return $this->buildValueDisplay( 3849 $class, 3850 $condition_field, 3851 $displayedColumn 3852 ); 3853 } 3854 3855 // transform functions may enable no-wrapping: 3856 $function_nowrap = 'applyTransformationNoWrap'; 3857 3858 $bool_nowrap = ($default_function != $transformation_plugin) 3859 && method_exists($transformation_plugin, $function_nowrap) 3860 ? $transformation_plugin->$function_nowrap($transform_options) 3861 : false; 3862 3863 // do not wrap if date field type or if no-wrapping enabled by transform functions 3864 // otherwise, preserve whitespaces and wrap 3865 $nowrap = preg_match('@DATE|TIME@i', $meta->type) 3866 || $bool_nowrap ? 'nowrap' : 'pre_wrap'; 3867 3868 $where_comparison = ' = \'' 3869 . $dbi->escapeString($originalDataForWhereClause) 3870 . '\''; 3871 3872 return $this->getRowData( 3873 $class, 3874 $condition_field, 3875 $analyzed_sql_results, 3876 $meta, 3877 $map, 3878 $column, 3879 $displayedColumn, 3880 $transformation_plugin, 3881 $default_function, 3882 $nowrap, 3883 $where_comparison, 3884 $transform_options, 3885 $is_field_truncated, 3886 $original_length 3887 ); 3888 } 3889 3890 /** 3891 * Checks the posted options for viewing query results 3892 * and sets appropriate values in the session. 3893 * 3894 * @return void 3895 * 3896 * @todo make maximum remembered queries configurable 3897 * @todo move/split into SQL class!? 3898 * @todo currently this is called twice unnecessary 3899 * @todo ignore LIMIT and ORDER in query!? 3900 * @access public 3901 */ 3902 public function setConfigParamsForDisplayTable() 3903 { 3904 $sql_md5 = md5( 3905 $this->properties['server'] 3906 . $this->properties['db'] 3907 . $this->properties['sql_query'] 3908 ); 3909 $query = []; 3910 if (isset($_SESSION['tmpval']['query'][$sql_md5])) { 3911 $query = $_SESSION['tmpval']['query'][$sql_md5]; 3912 } 3913 3914 $query['sql'] = $this->properties['sql_query']; 3915 3916 if (empty($query['repeat_cells'])) { 3917 $query['repeat_cells'] = $GLOBALS['cfg']['RepeatCells']; 3918 } 3919 3920 // The value can also be from _GET as described on issue #16146 when sorting results 3921 $sessionMaxRows = $_GET['session_max_rows'] ?? $_POST['session_max_rows'] ?? ''; 3922 3923 // as this is a form value, the type is always string so we cannot 3924 // use Core::isValid($_POST['session_max_rows'], 'integer') 3925 if (Core::isValid($sessionMaxRows, 'numeric')) { 3926 $query['max_rows'] = (int) $sessionMaxRows; 3927 unset($_GET['session_max_rows'], $_POST['session_max_rows']); 3928 } elseif ($sessionMaxRows === self::ALL_ROWS) { 3929 $query['max_rows'] = self::ALL_ROWS; 3930 unset($_GET['session_max_rows'], $_POST['session_max_rows']); 3931 } elseif (empty($query['max_rows'])) { 3932 $query['max_rows'] = intval($GLOBALS['cfg']['MaxRows']); 3933 } 3934 3935 if (Core::isValid($_REQUEST['pos'], 'numeric')) { 3936 $query['pos'] = $_REQUEST['pos']; 3937 unset($_REQUEST['pos']); 3938 } elseif (empty($query['pos'])) { 3939 $query['pos'] = 0; 3940 } 3941 3942 if (Core::isValid( 3943 $_REQUEST['pftext'], 3944 [ 3945 self::DISPLAY_PARTIAL_TEXT, 3946 self::DISPLAY_FULL_TEXT, 3947 ] 3948 ) 3949 ) { 3950 $query['pftext'] = $_REQUEST['pftext']; 3951 unset($_REQUEST['pftext']); 3952 } elseif (empty($query['pftext'])) { 3953 $query['pftext'] = self::DISPLAY_PARTIAL_TEXT; 3954 } 3955 3956 if (Core::isValid( 3957 $_REQUEST['relational_display'], 3958 [ 3959 self::RELATIONAL_KEY, 3960 self::RELATIONAL_DISPLAY_COLUMN, 3961 ] 3962 ) 3963 ) { 3964 $query['relational_display'] = $_REQUEST['relational_display']; 3965 unset($_REQUEST['relational_display']); 3966 } elseif (empty($query['relational_display'])) { 3967 // The current session value has priority over a 3968 // change via Settings; this change will be apparent 3969 // starting from the next session 3970 $query['relational_display'] = $GLOBALS['cfg']['RelationalDisplay']; 3971 } 3972 3973 if (Core::isValid( 3974 $_REQUEST['geoOption'], 3975 [ 3976 self::GEOMETRY_DISP_WKT, 3977 self::GEOMETRY_DISP_WKB, 3978 self::GEOMETRY_DISP_GEOM, 3979 ] 3980 ) 3981 ) { 3982 $query['geoOption'] = $_REQUEST['geoOption']; 3983 unset($_REQUEST['geoOption']); 3984 } elseif (empty($query['geoOption'])) { 3985 $query['geoOption'] = self::GEOMETRY_DISP_GEOM; 3986 } 3987 3988 if (isset($_REQUEST['display_binary'])) { 3989 $query['display_binary'] = true; 3990 unset($_REQUEST['display_binary']); 3991 } elseif (isset($_REQUEST['display_options_form'])) { 3992 // we know that the checkbox was unchecked 3993 unset($query['display_binary']); 3994 } elseif (! isset($_REQUEST['full_text_button'])) { 3995 // selected by default because some operations like OPTIMIZE TABLE 3996 // and all queries involving functions return "binary" contents, 3997 // according to low-level field flags 3998 $query['display_binary'] = true; 3999 } 4000 4001 if (isset($_REQUEST['display_blob'])) { 4002 $query['display_blob'] = true; 4003 unset($_REQUEST['display_blob']); 4004 } elseif (isset($_REQUEST['display_options_form'])) { 4005 // we know that the checkbox was unchecked 4006 unset($query['display_blob']); 4007 } 4008 4009 if (isset($_REQUEST['hide_transformation'])) { 4010 $query['hide_transformation'] = true; 4011 unset($_REQUEST['hide_transformation']); 4012 } elseif (isset($_REQUEST['display_options_form'])) { 4013 // we know that the checkbox was unchecked 4014 unset($query['hide_transformation']); 4015 } 4016 4017 // move current query to the last position, to be removed last 4018 // so only least executed query will be removed if maximum remembered 4019 // queries limit is reached 4020 unset($_SESSION['tmpval']['query'][$sql_md5]); 4021 $_SESSION['tmpval']['query'][$sql_md5] = $query; 4022 4023 // do not exceed a maximum number of queries to remember 4024 if (count($_SESSION['tmpval']['query']) > 10) { 4025 array_shift($_SESSION['tmpval']['query']); 4026 //echo 'deleting one element ...'; 4027 } 4028 4029 // populate query configuration 4030 $_SESSION['tmpval']['pftext'] 4031 = $query['pftext']; 4032 $_SESSION['tmpval']['relational_display'] 4033 = $query['relational_display']; 4034 $_SESSION['tmpval']['geoOption'] 4035 = $query['geoOption']; 4036 $_SESSION['tmpval']['display_binary'] = isset( 4037 $query['display_binary'] 4038 ); 4039 $_SESSION['tmpval']['display_blob'] = isset( 4040 $query['display_blob'] 4041 ); 4042 $_SESSION['tmpval']['hide_transformation'] = isset( 4043 $query['hide_transformation'] 4044 ); 4045 $_SESSION['tmpval']['pos'] 4046 = $query['pos']; 4047 $_SESSION['tmpval']['max_rows'] 4048 = $query['max_rows']; 4049 $_SESSION['tmpval']['repeat_cells'] 4050 = $query['repeat_cells']; 4051 } 4052 4053 /** 4054 * Prepare a table of results returned by a SQL query. 4055 * 4056 * @param int $dt_result the link id associated to the query 4057 * which results have to be displayed 4058 * @param array $displayParts the parts to display 4059 * @param array $analyzed_sql_results analyzed sql results 4060 * @param bool $is_limited_display With limited operations or not 4061 * 4062 * @return string Generated HTML content for resulted table 4063 * 4064 * @access public 4065 */ 4066 public function getTable( 4067 &$dt_result, 4068 array &$displayParts, 4069 array $analyzed_sql_results, 4070 $is_limited_display = false 4071 ) { 4072 // The statement this table is built for. 4073 if (isset($analyzed_sql_results['statement'])) { 4074 /** @var SelectStatement $statement */ 4075 $statement = $analyzed_sql_results['statement']; 4076 } else { 4077 $statement = null; 4078 } 4079 4080 // Following variable are needed for use in isset/empty or 4081 // use with array indexes/safe use in foreach 4082 $fields_meta = $this->properties['fields_meta']; 4083 $showtable = $this->properties['showtable']; 4084 $printview = $this->properties['printview']; 4085 4086 /** 4087 * @todo move this to a central place 4088 * @todo for other future table types 4089 */ 4090 $is_innodb = (isset($showtable['Type']) 4091 && $showtable['Type'] === self::TABLE_TYPE_INNO_DB); 4092 4093 if ($is_innodb && Sql::isJustBrowsing($analyzed_sql_results, true)) { 4094 $pre_count = '~'; 4095 $after_count = Generator::showHint( 4096 Sanitize::sanitizeMessage( 4097 __('May be approximate. See [doc@faq3-11]FAQ 3.11[/doc].') 4098 ) 4099 ); 4100 } else { 4101 $pre_count = ''; 4102 $after_count = ''; 4103 } 4104 4105 // 1. ----- Prepares the work ----- 4106 4107 // 1.1 Gets the information about which functionalities should be 4108 // displayed 4109 4110 [ 4111 $displayParts, 4112 $total, 4113 ] = $this->setDisplayPartsAndTotal($displayParts); 4114 4115 // 1.2 Defines offsets for the next and previous pages 4116 $pos_next = 0; 4117 $pos_prev = 0; 4118 if ($displayParts['nav_bar'] == '1') { 4119 [$pos_next, $pos_prev] = $this->getOffsets(); 4120 } 4121 4122 // 1.3 Extract sorting expressions. 4123 // we need $sort_expression and $sort_expression_nodirection 4124 // even if there are many table references 4125 $sort_expression = []; 4126 $sort_expression_nodirection = []; 4127 $sort_direction = []; 4128 4129 if ($statement !== null && ! empty($statement->order)) { 4130 foreach ($statement->order as $o) { 4131 $sort_expression[] = $o->expr->expr . ' ' . $o->type; 4132 $sort_expression_nodirection[] = $o->expr->expr; 4133 $sort_direction[] = $o->type; 4134 } 4135 } else { 4136 $sort_expression[] = ''; 4137 $sort_expression_nodirection[] = ''; 4138 $sort_direction[] = ''; 4139 } 4140 4141 $number_of_columns = count($sort_expression_nodirection); 4142 4143 // 1.4 Prepares display of first and last value of the sorted column 4144 $sorted_column_message = ''; 4145 for ($i = 0; $i < $number_of_columns; $i++) { 4146 $sorted_column_message .= $this->getSortedColumnMessage( 4147 $dt_result, 4148 $sort_expression_nodirection[$i] 4149 ); 4150 } 4151 4152 // 2. ----- Prepare to display the top of the page ----- 4153 4154 // 2.1 Prepares a messages with position information 4155 $sqlQueryMessage = ''; 4156 if (($displayParts['nav_bar'] == '1') && $pos_next !== null) { 4157 $message = $this->setMessageInformation( 4158 $sorted_column_message, 4159 $analyzed_sql_results, 4160 $total, 4161 $pos_next, 4162 $pre_count, 4163 $after_count 4164 ); 4165 4166 $sqlQueryMessage = Generator::getMessage( 4167 $message, 4168 $this->properties['sql_query'], 4169 'success' 4170 ); 4171 } elseif ((! isset($printview) || ($printview != '1')) && ! $is_limited_display) { 4172 $sqlQueryMessage = Generator::getMessage( 4173 __('Your SQL query has been executed successfully.'), 4174 $this->properties['sql_query'], 4175 'success' 4176 ); 4177 } 4178 4179 // 2.3 Prepare the navigation bars 4180 if (strlen($this->properties['table']) === 0) { 4181 if ($analyzed_sql_results['querytype'] === 'SELECT') { 4182 // table does not always contain a real table name, 4183 // for example in MySQL 5.0.x, the query SHOW STATUS 4184 // returns STATUS as a table name 4185 $this->properties['table'] = $fields_meta[0]->table; 4186 } else { 4187 $this->properties['table'] = ''; 4188 } 4189 } 4190 4191 // can the result be sorted? 4192 if ($displayParts['sort_lnk'] == '1' && $analyzed_sql_results['statement'] !== null) { 4193 // At this point, $sort_expression is an array 4194 [$unsorted_sql_query, $sort_by_key_html] 4195 = $this->getUnsortedSqlAndSortByKeyDropDown( 4196 $analyzed_sql_results, 4197 $sort_expression 4198 ); 4199 } else { 4200 $sort_by_key_html = $unsorted_sql_query = ''; 4201 } 4202 4203 $navigation = []; 4204 if ($displayParts['nav_bar'] == '1' && $statement !== null && empty($statement->limit)) { 4205 $navigation = $this->getTableNavigation( 4206 $pos_next, 4207 $pos_prev, 4208 $is_innodb, 4209 $sort_by_key_html 4210 ); 4211 } 4212 4213 // 2b ----- Get field references from Database ----- 4214 // (see the 'relation' configuration variable) 4215 4216 // initialize map 4217 $map = []; 4218 4219 if (strlen($this->properties['table']) > 0) { 4220 // This method set the values for $map array 4221 $this->setParamForLinkForeignKeyRelatedTables($map); 4222 4223 // Coming from 'Distinct values' action of structure page 4224 // We manipulate relations mechanism to show a link to related rows. 4225 if ($this->properties['is_browse_distinct']) { 4226 $map[$fields_meta[1]->name] = [ 4227 $this->properties['table'], 4228 $fields_meta[1]->name, 4229 '', 4230 $this->properties['db'], 4231 ]; 4232 } 4233 } 4234 // end 2b 4235 4236 // 3. ----- Prepare the results table ----- 4237 $headers = $this->getTableHeaders( 4238 $displayParts, 4239 $analyzed_sql_results, 4240 $unsorted_sql_query, 4241 $sort_expression, 4242 $sort_expression_nodirection, 4243 $sort_direction, 4244 $is_limited_display 4245 ); 4246 4247 $body = $this->getTableBody( 4248 $dt_result, 4249 $displayParts, 4250 $map, 4251 $analyzed_sql_results, 4252 $is_limited_display 4253 ); 4254 4255 $this->properties['display_params'] = null; 4256 4257 // 4. ----- Prepares the link for multi-fields edit and delete 4258 $bulkLinks = $this->getBulkLinks( 4259 $dt_result, 4260 $analyzed_sql_results, 4261 $displayParts['del_lnk'] 4262 ); 4263 4264 // 5. ----- Prepare "Query results operations" 4265 $operations = []; 4266 if ((! isset($printview) || ($printview != '1')) && ! $is_limited_display) { 4267 $operations = $this->getResultsOperations( 4268 $displayParts, 4269 $analyzed_sql_results 4270 ); 4271 } 4272 4273 return $this->template->render('display/results/table', [ 4274 'sql_query_message' => $sqlQueryMessage, 4275 'navigation' => $navigation, 4276 'headers' => $headers, 4277 'body' => $body, 4278 'bulk_links' => $bulkLinks, 4279 'operations' => $operations, 4280 'db' => $this->properties['db'], 4281 'table' => $this->properties['table'], 4282 'unique_id' => $this->properties['unique_id'], 4283 'sql_query' => $this->properties['sql_query'], 4284 'goto' => $this->properties['goto'], 4285 'unlim_num_rows' => $this->properties['unlim_num_rows'], 4286 'displaywork' => $GLOBALS['cfgRelation']['displaywork'], 4287 'relwork' => $GLOBALS['cfgRelation']['relwork'], 4288 'save_cells_at_once' => $GLOBALS['cfg']['SaveCellsAtOnce'], 4289 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'], 4290 'select_all_arrow' => $this->properties['theme_image_path'] . 'arrow_' 4291 . $this->properties['text_dir'] . '.png', 4292 ]); 4293 } 4294 4295 /** 4296 * Get offsets for next page and previous page 4297 * 4298 * @see getTable() 4299 * 4300 * @return int[] array with two elements - $pos_next, $pos_prev 4301 * 4302 * @access private 4303 */ 4304 private function getOffsets() 4305 { 4306 if ($_SESSION['tmpval']['max_rows'] === self::ALL_ROWS) { 4307 $pos_next = 0; 4308 $pos_prev = 0; 4309 } else { 4310 $pos_next = $_SESSION['tmpval']['pos'] 4311 + $_SESSION['tmpval']['max_rows']; 4312 4313 $pos_prev = $_SESSION['tmpval']['pos'] 4314 - $_SESSION['tmpval']['max_rows']; 4315 4316 if ($pos_prev < 0) { 4317 $pos_prev = 0; 4318 } 4319 } 4320 4321 return [ 4322 $pos_next, 4323 $pos_prev, 4324 ]; 4325 } 4326 4327 /** 4328 * Prepare sorted column message 4329 * 4330 * @see getTable() 4331 * 4332 * @param int $dt_result the link id associated to the 4333 * query which results have to 4334 * be displayed 4335 * @param string $sort_expression_nodirection sort expression without direction 4336 * 4337 * @return string|null html content, null if not found sorted column 4338 * 4339 * @access private 4340 */ 4341 private function getSortedColumnMessage( 4342 &$dt_result, 4343 $sort_expression_nodirection 4344 ) { 4345 global $dbi; 4346 4347 $fields_meta = $this->properties['fields_meta']; // To use array indexes 4348 4349 if (empty($sort_expression_nodirection)) { 4350 return null; 4351 } 4352 4353 if (mb_strpos($sort_expression_nodirection, '.') === false) { 4354 $sort_table = $this->properties['table']; 4355 $sort_column = $sort_expression_nodirection; 4356 } else { 4357 [$sort_table, $sort_column] 4358 = explode('.', $sort_expression_nodirection); 4359 } 4360 4361 $sort_table = Util::unQuote($sort_table); 4362 $sort_column = Util::unQuote($sort_column); 4363 4364 // find the sorted column index in row result 4365 // (this might be a multi-table query) 4366 $sorted_column_index = false; 4367 4368 foreach ($fields_meta as $key => $meta) { 4369 if (($meta->table == $sort_table) && ($meta->name == $sort_column)) { 4370 $sorted_column_index = $key; 4371 break; 4372 } 4373 } 4374 4375 if ($sorted_column_index === false) { 4376 return null; 4377 } 4378 4379 // fetch first row of the result set 4380 $row = $dbi->fetchRow($dt_result); 4381 4382 // initializing default arguments 4383 $default_function = [ 4384 Core::class, 4385 'mimeDefaultFunction', 4386 ]; 4387 $transformation_plugin = $default_function; 4388 $transform_options = []; 4389 4390 // check for non printable sorted row data 4391 $meta = $fields_meta[$sorted_column_index]; 4392 4393 if (stripos($meta->type, self::BLOB_FIELD) !== false 4394 || ($meta->type === self::GEOMETRY_FIELD) 4395 || ($meta->type === 'string' && $meta->charsetnr === 63)// Is a binary string 4396 ) { 4397 $column_for_first_row = $this->handleNonPrintableContents( 4398 $meta->type, 4399 $row[$sorted_column_index], 4400 $transformation_plugin, 4401 $transform_options, 4402 $default_function, 4403 $meta 4404 ); 4405 } else { 4406 $column_for_first_row = $row !== null ? $row[$sorted_column_index] : ''; 4407 } 4408 4409 $column_for_first_row = mb_strtoupper( 4410 mb_substr( 4411 (string) $column_for_first_row, 4412 0, 4413 (int) $GLOBALS['cfg']['LimitChars'] 4414 ) . '...' 4415 ); 4416 4417 // fetch last row of the result set 4418 $dbi->dataSeek( 4419 $dt_result, 4420 $this->properties['num_rows'] > 0 ? $this->properties['num_rows'] - 1 : 0 4421 ); 4422 $row = $dbi->fetchRow($dt_result); 4423 4424 // check for non printable sorted row data 4425 $meta = $fields_meta[$sorted_column_index]; 4426 if (stripos($meta->type, self::BLOB_FIELD) !== false 4427 || ($meta->type === self::GEOMETRY_FIELD) 4428 || ($meta->type === 'string' && $meta->charsetnr === 63)// Is a binary string 4429 ) { 4430 $column_for_last_row = $this->handleNonPrintableContents( 4431 $meta->type, 4432 $row[$sorted_column_index], 4433 $transformation_plugin, 4434 $transform_options, 4435 $default_function, 4436 $meta 4437 ); 4438 } else { 4439 $column_for_last_row = $row !== null ? $row[$sorted_column_index] : ''; 4440 } 4441 4442 $column_for_last_row = mb_strtoupper( 4443 mb_substr( 4444 (string) $column_for_last_row, 4445 0, 4446 (int) $GLOBALS['cfg']['LimitChars'] 4447 ) . '...' 4448 ); 4449 4450 // reset to first row for the loop in getTableBody() 4451 $dbi->dataSeek($dt_result, 0); 4452 4453 // we could also use here $sort_expression_nodirection 4454 return ' [' . htmlspecialchars($sort_column) 4455 . ': <strong>' . htmlspecialchars($column_for_first_row) . ' - ' 4456 . htmlspecialchars($column_for_last_row) . '</strong>]'; 4457 } 4458 4459 /** 4460 * Set the content that needs to be shown in message 4461 * 4462 * @see getTable() 4463 * 4464 * @param string $sorted_column_message the message for sorted column 4465 * @param array $analyzed_sql_results the analyzed query 4466 * @param int $total the total number of rows returned by 4467 * the SQL query without any 4468 * programmatically appended LIMIT clause 4469 * @param int $pos_next the offset for next page 4470 * @param string $pre_count the string renders before row count 4471 * @param string $after_count the string renders after row count 4472 * 4473 * @return Message an object of Message 4474 * 4475 * @access private 4476 */ 4477 private function setMessageInformation( 4478 $sorted_column_message, 4479 array $analyzed_sql_results, 4480 $total, 4481 $pos_next, 4482 $pre_count, 4483 $after_count 4484 ) { 4485 $unlim_num_rows = $this->properties['unlim_num_rows']; // To use in isset() 4486 4487 if (! empty($analyzed_sql_results['statement']->limit)) { 4488 $first_shown_rec = $analyzed_sql_results['statement']->limit->offset; 4489 $row_count = $analyzed_sql_results['statement']->limit->rowCount; 4490 4491 if ($row_count < $total) { 4492 $last_shown_rec = $first_shown_rec + $row_count - 1; 4493 } else { 4494 $last_shown_rec = $first_shown_rec + $total - 1; 4495 } 4496 } elseif (($_SESSION['tmpval']['max_rows'] === self::ALL_ROWS) 4497 || ($pos_next > $total) 4498 ) { 4499 $first_shown_rec = $_SESSION['tmpval']['pos']; 4500 $last_shown_rec = $total - 1; 4501 } else { 4502 $first_shown_rec = $_SESSION['tmpval']['pos']; 4503 $last_shown_rec = $pos_next - 1; 4504 } 4505 4506 $table = new Table($this->properties['table'], $this->properties['db']); 4507 if ($table->isView() 4508 && ($total == $GLOBALS['cfg']['MaxExactCountViews']) 4509 ) { 4510 $message = Message::notice( 4511 __( 4512 'This view has at least this number of rows. ' 4513 . 'Please refer to %sdocumentation%s.' 4514 ) 4515 ); 4516 4517 $message->addParam('[doc@cfg_MaxExactCount]'); 4518 $message->addParam('[/doc]'); 4519 $message_view_warning = Generator::showHint($message); 4520 } else { 4521 $message_view_warning = false; 4522 } 4523 4524 $message = Message::success(__('Showing rows %1s - %2s')); 4525 $message->addParam($first_shown_rec); 4526 4527 if ($message_view_warning !== false) { 4528 $message->addParamHtml('... ' . $message_view_warning); 4529 } else { 4530 $message->addParam($last_shown_rec); 4531 } 4532 4533 $message->addText('('); 4534 4535 if ($message_view_warning === false) { 4536 if (isset($unlim_num_rows) && ($unlim_num_rows != $total)) { 4537 $message_total = Message::notice( 4538 $pre_count . __('%1$d total, %2$d in query') 4539 ); 4540 $message_total->addParam($total); 4541 $message_total->addParam($unlim_num_rows); 4542 } else { 4543 $message_total = Message::notice($pre_count . __('%d total')); 4544 $message_total->addParam($total); 4545 } 4546 4547 if (! empty($after_count)) { 4548 $message_total->addHtml($after_count); 4549 } 4550 $message->addMessage($message_total, ''); 4551 4552 $message->addText(', ', ''); 4553 } 4554 4555 $message_qt = Message::notice(__('Query took %01.4f seconds.') . ')'); 4556 $message_qt->addParam($this->properties['querytime']); 4557 4558 $message->addMessage($message_qt, ''); 4559 if ($sorted_column_message !== null) { 4560 $message->addHtml($sorted_column_message, ''); 4561 } 4562 4563 return $message; 4564 } 4565 4566 /** 4567 * Set the value of $map array for linking foreign key related tables 4568 * 4569 * @see getTable() 4570 * 4571 * @param array $map the list of relations 4572 * 4573 * @return void 4574 * 4575 * @access private 4576 */ 4577 private function setParamForLinkForeignKeyRelatedTables(array &$map) 4578 { 4579 // To be able to later display a link to the related table, 4580 // we verify both types of relations: either those that are 4581 // native foreign keys or those defined in the phpMyAdmin 4582 // configuration storage. If no PMA storage, we won't be able 4583 // to use the "column to display" notion (for example show 4584 // the name related to a numeric id). 4585 $exist_rel = $this->relation->getForeigners( 4586 $this->properties['db'], 4587 $this->properties['table'], 4588 '', 4589 self::POSITION_BOTH 4590 ); 4591 4592 if (empty($exist_rel)) { 4593 return; 4594 } 4595 4596 foreach ($exist_rel as $master_field => $rel) { 4597 if ($master_field !== 'foreign_keys_data') { 4598 $display_field = $this->relation->getDisplayField( 4599 $rel['foreign_db'], 4600 $rel['foreign_table'] 4601 ); 4602 $map[$master_field] = [ 4603 $rel['foreign_table'], 4604 $rel['foreign_field'], 4605 $display_field, 4606 $rel['foreign_db'], 4607 ]; 4608 } else { 4609 foreach ($rel as $key => $one_key) { 4610 foreach ($one_key['index_list'] as $index => $one_field) { 4611 $display_field = $this->relation->getDisplayField( 4612 $one_key['ref_db_name'] ?? $GLOBALS['db'], 4613 $one_key['ref_table_name'] 4614 ); 4615 4616 $map[$one_field] = [ 4617 $one_key['ref_table_name'], 4618 $one_key['ref_index_list'][$index], 4619 $display_field, 4620 $one_key['ref_db_name'] ?? $GLOBALS['db'], 4621 ]; 4622 } 4623 } 4624 } 4625 } 4626 } 4627 4628 /** 4629 * Prepare multi field edit/delete links 4630 * 4631 * @see getTable() 4632 * 4633 * @param int $dt_result the link id associated to the query which 4634 * results have to be displayed 4635 * @param array $analyzed_sql_results analyzed sql results 4636 * @param string $del_link the display element - 'del_link' 4637 * 4638 * @return array 4639 */ 4640 private function getBulkLinks( 4641 &$dt_result, 4642 array $analyzed_sql_results, 4643 $del_link 4644 ): array { 4645 global $dbi; 4646 4647 if ($del_link !== self::DELETE_ROW) { 4648 return []; 4649 } 4650 4651 // fetch last row of the result set 4652 $dbi->dataSeek( 4653 $dt_result, 4654 $this->properties['num_rows'] > 0 ? $this->properties['num_rows'] - 1 : 0 4655 ); 4656 $row = $dbi->fetchRow($dt_result); 4657 4658 // @see DbiMysqi::fetchRow & DatabaseInterface::fetchRow 4659 if (! is_array($row)) { 4660 $row = []; 4661 } 4662 4663 $expressions = []; 4664 4665 if (isset($analyzed_sql_results['statement']) 4666 && $analyzed_sql_results['statement'] instanceof SelectStatement 4667 ) { 4668 $expressions = $analyzed_sql_results['statement']->expr; 4669 } 4670 4671 /** 4672 * $clause_is_unique is needed by getTable() to generate the proper param 4673 * in the multi-edit and multi-delete form 4674 */ 4675 [, $clause_is_unique] = Util::getUniqueCondition( 4676 $dt_result, 4677 $this->properties['fields_cnt'], 4678 $this->properties['fields_meta'], 4679 $row, 4680 false, 4681 false, 4682 $expressions 4683 ); 4684 4685 // reset to first row for the loop in getTableBody() 4686 $dbi->dataSeek($dt_result, 0); 4687 4688 return [ 4689 'has_export_button' => $analyzed_sql_results['querytype'] === 'SELECT', 4690 'clause_is_unique' => $clause_is_unique, 4691 ]; 4692 } 4693 4694 /** 4695 * Get operations that are available on results. 4696 * 4697 * @see getTable() 4698 * 4699 * @param array $displayParts the parts to display 4700 * @param array $analyzed_sql_results analyzed sql results 4701 * 4702 * @return array<string, bool|array<string, string>> 4703 */ 4704 private function getResultsOperations( 4705 array $displayParts, 4706 array $analyzed_sql_results 4707 ): array { 4708 global $printview, $dbi; 4709 4710 $_url_params = [ 4711 'db' => $this->properties['db'], 4712 'table' => $this->properties['table'], 4713 'printview' => '1', 4714 'sql_query' => $this->properties['sql_query'], 4715 ]; 4716 4717 $geometry_found = false; 4718 4719 // Export link 4720 // (the single_table parameter is used in \PhpMyAdmin\Export->getDisplay() 4721 // to hide the SQL and the structure export dialogs) 4722 // If the parser found a PROCEDURE clause 4723 // (most probably PROCEDURE ANALYSE()) it makes no sense to 4724 // display the Export link). 4725 if (($analyzed_sql_results['querytype'] === self::QUERY_TYPE_SELECT) 4726 && ! isset($printview) 4727 && empty($analyzed_sql_results['procedure']) 4728 ) { 4729 if (count($analyzed_sql_results['select_tables']) === 1) { 4730 $_url_params['single_table'] = 'true'; 4731 } 4732 4733 // In case this query doesn't involve any tables, 4734 // implies only raw query is to be exported 4735 if (! $analyzed_sql_results['select_tables']) { 4736 $_url_params['raw_query'] = 'true'; 4737 } 4738 4739 $_url_params['unlim_num_rows'] = $this->properties['unlim_num_rows']; 4740 4741 /** 4742 * At this point we don't know the table name; this can happen 4743 * for example with a query like 4744 * SELECT bike_code FROM (SELECT bike_code FROM bikes) tmp 4745 * As a workaround we set in the table parameter the name of the 4746 * first table of this database, so that /table/export and 4747 * the script it calls do not fail 4748 */ 4749 if (empty($_url_params['table']) && ! empty($_url_params['db'])) { 4750 $_url_params['table'] = $dbi->fetchValue('SHOW TABLES'); 4751 /* No result (probably no database selected) */ 4752 if ($_url_params['table'] === false) { 4753 unset($_url_params['table']); 4754 } 4755 } 4756 4757 $fields_meta = $this->properties['fields_meta']; 4758 foreach ($fields_meta as $meta) { 4759 if ($meta->type === self::GEOMETRY_FIELD) { 4760 $geometry_found = true; 4761 break; 4762 } 4763 } 4764 } 4765 4766 return [ 4767 'has_procedure' => ! empty($analyzed_sql_results['procedure']), 4768 'has_geometry' => $geometry_found, 4769 'has_print_link' => $displayParts['pview_lnk'] == '1', 4770 'has_export_link' => $analyzed_sql_results['querytype'] === self::QUERY_TYPE_SELECT && ! isset($printview), 4771 'url_params' => $_url_params, 4772 ]; 4773 } 4774 4775 /** 4776 * Verifies what to do with non-printable contents (binary or BLOB) 4777 * in Browse mode. 4778 * 4779 * @see getDataCellForGeometryColumns(), getDataCellForNonNumericColumns(), getSortedColumnMessage() 4780 * 4781 * @param string $category BLOB|BINARY|GEOMETRY 4782 * @param string|null $content the binary content 4783 * @param mixed $transformation_plugin transformation plugin. 4784 * Can also be the 4785 * default function: 4786 * Core::mimeDefaultFunction 4787 * @param array $transform_options transformation parameters 4788 * @param string $default_function default transformation function 4789 * @param stdClass $meta the meta-information about the field 4790 * @param array $url_params parameters that should go to the 4791 * download link 4792 * @param bool $is_truncated the result is truncated or not 4793 * 4794 * @return mixed string or float 4795 * 4796 * @access private 4797 */ 4798 private function handleNonPrintableContents( 4799 $category, 4800 ?string $content, 4801 $transformation_plugin, 4802 $transform_options, 4803 $default_function, 4804 $meta, 4805 array $url_params = [], 4806 &$is_truncated = null 4807 ) { 4808 $is_truncated = false; 4809 $result = '[' . $category; 4810 4811 if ($content !== null) { 4812 $size = strlen($content); 4813 $display_size = Util::formatByteDown($size, 3, 1); 4814 $result .= ' - ' . $display_size[0] . ' ' . $display_size[1]; 4815 } else { 4816 $result .= ' - NULL'; 4817 $size = 0; 4818 $content = ''; 4819 } 4820 4821 $result .= ']'; 4822 4823 // if we want to use a text transformation on a BLOB column 4824 if (is_object($transformation_plugin)) { 4825 $posMimeOctetstream = strpos( 4826 $transformation_plugin->getMIMESubtype(), 4827 'Octetstream' 4828 ); 4829 $posMimeText = strpos($transformation_plugin->getMIMEtype(), 'Text'); 4830 if ($posMimeOctetstream 4831 || $posMimeText !== false 4832 ) { 4833 // Applying Transformations on hex string of binary data 4834 // seems more appropriate 4835 $result = pack('H*', bin2hex($content)); 4836 } 4837 } 4838 4839 if ($size <= 0) { 4840 return $result; 4841 } 4842 4843 if ($default_function != $transformation_plugin) { 4844 $result = $transformation_plugin->applyTransformation( 4845 $result, 4846 $transform_options, 4847 $meta 4848 ); 4849 4850 return $result; 4851 } 4852 4853 $result = $default_function($result, [], $meta); 4854 if (($_SESSION['tmpval']['display_binary'] 4855 && $meta->type === self::STRING_FIELD) 4856 || ($_SESSION['tmpval']['display_blob'] 4857 && stripos($meta->type, self::BLOB_FIELD) !== false) 4858 ) { 4859 // in this case, restart from the original $content 4860 if (mb_check_encoding($content, 'utf-8') 4861 && ! preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', $content) 4862 ) { 4863 // show as text if it's valid utf-8 4864 $result = htmlspecialchars($content); 4865 } else { 4866 $result = '0x' . bin2hex($content); 4867 } 4868 [ 4869 $is_truncated, 4870 $result, 4871 // skip 3rd param 4872 ] = $this->getPartialText($result); 4873 } 4874 4875 /* Create link to download */ 4876 4877 // in PHP < 5.5, empty() only checks variables 4878 $tmpdb = $this->properties['db']; 4879 if (count($url_params) > 0 4880 && (! empty($tmpdb) && ! empty($meta->orgtable)) 4881 ) { 4882 $url_params['where_clause_sign'] = Core::signSqlQuery($url_params['where_clause']); 4883 $result = '<a href="' 4884 . Url::getFromRoute('/table/get-field', $url_params) 4885 . '" class="disableAjax">' 4886 . $result . '</a>'; 4887 } 4888 4889 return $result; 4890 } 4891 4892 /** 4893 * Retrieves the associated foreign key info for a data cell 4894 * 4895 * @param array $map the list of relations 4896 * @param stdClass $meta the meta-information about the field 4897 * @param string $where_comparison data for the where clause 4898 * 4899 * @return string|null formatted data 4900 * 4901 * @access private 4902 */ 4903 private function getFromForeign(array $map, $meta, $where_comparison) 4904 { 4905 global $dbi; 4906 4907 $dispsql = 'SELECT ' 4908 . Util::backquote($map[$meta->name][2]) 4909 . ' FROM ' 4910 . Util::backquote($map[$meta->name][3]) 4911 . '.' 4912 . Util::backquote($map[$meta->name][0]) 4913 . ' WHERE ' 4914 . Util::backquote($map[$meta->name][1]) 4915 . $where_comparison; 4916 4917 $dispresult = $dbi->tryQuery( 4918 $dispsql, 4919 DatabaseInterface::CONNECT_USER, 4920 DatabaseInterface::QUERY_STORE 4921 ); 4922 4923 if ($dispresult && $dbi->numRows($dispresult) > 0) { 4924 [$dispval] = $dbi->fetchRow($dispresult); 4925 } else { 4926 $dispval = __('Link not found!'); 4927 } 4928 4929 $dbi->freeResult($dispresult); 4930 4931 return $dispval; 4932 } 4933 4934 /** 4935 * Prepares the displayable content of a data cell in Browse mode, 4936 * taking into account foreign key description field and transformations 4937 * 4938 * @see getDataCellForNumericColumns(), getDataCellForGeometryColumns(), 4939 * getDataCellForNonNumericColumns(), 4940 * 4941 * @param string $class css classes for the td element 4942 * @param bool $condition_field whether the column is a part of 4943 * the where clause 4944 * @param array $analyzed_sql_results the analyzed query 4945 * @param stdClass $meta the meta-information about the 4946 * field 4947 * @param array $map the list of relations 4948 * @param string $data data 4949 * @param string $displayedData data that will be displayed (maybe be chunked) 4950 * @param TransformationsPlugin $transformation_plugin transformation plugin. 4951 * Can also be the default function: 4952 * Core::mimeDefaultFunction 4953 * @param string $default_function default function 4954 * @param string $nowrap 'nowrap' if the content should 4955 * not be wrapped 4956 * @param string $where_comparison data for the where clause 4957 * @param array $transform_options options for transformation 4958 * @param bool $is_field_truncated whether the field is truncated 4959 * @param string $original_length of a truncated column, or '' 4960 * 4961 * @return string formatted data 4962 * 4963 * @access private 4964 */ 4965 private function getRowData( 4966 $class, 4967 $condition_field, 4968 array $analyzed_sql_results, 4969 $meta, 4970 array $map, 4971 $data, 4972 $displayedData, 4973 $transformation_plugin, 4974 $default_function, 4975 $nowrap, 4976 $where_comparison, 4977 array $transform_options, 4978 $is_field_truncated, 4979 $original_length = '' 4980 ) { 4981 $relational_display = $_SESSION['tmpval']['relational_display']; 4982 $printview = $this->properties['printview']; 4983 $decimals = $meta->decimals ?? '-1'; 4984 $result = '<td data-decimals="' . $decimals . '"' 4985 . ' data-type="' . $meta->type . '"'; 4986 4987 if (! empty($original_length)) { 4988 // cannot use data-original-length 4989 $result .= ' data-originallength="' . $original_length . '"'; 4990 } 4991 4992 $result .= ' class="' 4993 . $this->addClass( 4994 $class, 4995 $condition_field, 4996 $meta, 4997 $nowrap, 4998 $is_field_truncated, 4999 $transformation_plugin, 5000 $default_function 5001 ) 5002 . '">'; 5003 5004 if (! empty($analyzed_sql_results['statement']->expr)) { 5005 foreach ($analyzed_sql_results['statement']->expr as $expr) { 5006 if (empty($expr->alias) || empty($expr->column)) { 5007 continue; 5008 } 5009 if (strcasecmp($meta->name, $expr->alias) != 0) { 5010 continue; 5011 } 5012 5013 $meta->name = $expr->column; 5014 } 5015 } 5016 5017 if (isset($map[$meta->name])) { 5018 // Field to display from the foreign table? 5019 if (isset($map[$meta->name][2]) 5020 && strlen((string) $map[$meta->name][2]) > 0 5021 ) { 5022 $dispval = $this->getFromForeign( 5023 $map, 5024 $meta, 5025 $where_comparison 5026 ); 5027 } else { 5028 $dispval = ''; 5029 } 5030 5031 if (isset($printview) && ($printview == '1')) { 5032 $result .= ($transformation_plugin != $default_function 5033 ? $transformation_plugin->applyTransformation( 5034 $data, 5035 $transform_options, 5036 $meta 5037 ) 5038 : $default_function($data) 5039 ) 5040 . ' <code>[->' . $dispval . ']</code>'; 5041 } else { 5042 if ($relational_display === self::RELATIONAL_KEY) { 5043 // user chose "relational key" in the display options, so 5044 // the title contains the display field 5045 $title = ! empty($dispval) 5046 ? htmlspecialchars($dispval) 5047 : ''; 5048 } else { 5049 $title = htmlspecialchars($data); 5050 } 5051 5052 $sqlQuery = 'SELECT * FROM ' 5053 . Util::backquote($map[$meta->name][3]) . '.' 5054 . Util::backquote($map[$meta->name][0]) 5055 . ' WHERE ' 5056 . Util::backquote($map[$meta->name][1]) 5057 . $where_comparison; 5058 5059 $_url_params = [ 5060 'db' => $map[$meta->name][3], 5061 'table' => $map[$meta->name][0], 5062 'pos' => '0', 5063 'sql_signature' => Core::signSqlQuery($sqlQuery), 5064 'sql_query' => $sqlQuery, 5065 ]; 5066 5067 if ($transformation_plugin != $default_function) { 5068 // always apply a transformation on the real data, 5069 // not on the display field 5070 $displayedData = $transformation_plugin->applyTransformation( 5071 $data, 5072 $transform_options, 5073 $meta 5074 ); 5075 } else { 5076 if ($relational_display === self::RELATIONAL_DISPLAY_COLUMN 5077 && ! empty($map[$meta->name][2]) 5078 ) { 5079 // user chose "relational display field" in the 5080 // display options, so show display field in the cell 5081 $displayedData = $dispval === null ? '<em>NULL</em>' : $default_function($dispval); 5082 } else { 5083 // otherwise display data in the cell 5084 $displayedData = $default_function($displayedData); 5085 } 5086 } 5087 5088 $tag_params = ['title' => $title]; 5089 if (strpos($class, 'grid_edit') !== false) { 5090 $tag_params['class'] = 'ajax'; 5091 } 5092 $result .= Generator::linkOrButton( 5093 Url::getFromRoute('/sql'), 5094 $_url_params, 5095 $displayedData, 5096 $tag_params 5097 ); 5098 } 5099 } else { 5100 $result .= ($transformation_plugin != $default_function 5101 ? $transformation_plugin->applyTransformation( 5102 $data, 5103 $transform_options, 5104 $meta 5105 ) 5106 : $default_function($data) 5107 ); 5108 } 5109 5110 $result .= '</td>' . "\n"; 5111 5112 return $result; 5113 } 5114 5115 /** 5116 * Truncates given string based on LimitChars configuration 5117 * and Session pftext variable 5118 * (string is truncated only if necessary) 5119 * 5120 * @see handleNonPrintableContents(), getDataCellForGeometryColumns(), getDataCellForNonNumericColumns 5121 * 5122 * @param string $str string to be truncated 5123 * 5124 * @return array 5125 * 5126 * @access private 5127 */ 5128 private function getPartialText($str): array 5129 { 5130 $original_length = mb_strlen($str); 5131 if ($original_length > $GLOBALS['cfg']['LimitChars'] 5132 && $_SESSION['tmpval']['pftext'] === self::DISPLAY_PARTIAL_TEXT 5133 ) { 5134 $str = mb_substr( 5135 $str, 5136 0, 5137 (int) $GLOBALS['cfg']['LimitChars'] 5138 ) . '...'; 5139 $truncated = true; 5140 } else { 5141 $truncated = false; 5142 } 5143 5144 return [ 5145 $truncated, 5146 $str, 5147 $original_length, 5148 ]; 5149 } 5150} 5151