Extract results nav buttons into table template
[phpmyadmin.git] / libraries / classes / Display / Results.php
bloba6ea8702b4516572baed7286208da3ecdd096f17
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin\Display;
7 use PhpMyAdmin\Config\SpecialSchemaLinks;
8 use PhpMyAdmin\ConfigStorage\Relation;
9 use PhpMyAdmin\Core;
10 use PhpMyAdmin\DatabaseInterface;
11 use PhpMyAdmin\Dbal\ResultInterface;
12 use PhpMyAdmin\FieldMetadata;
13 use PhpMyAdmin\Html\Generator;
14 use PhpMyAdmin\Index;
15 use PhpMyAdmin\Message;
16 use PhpMyAdmin\Plugins\Transformations\Output\Text_Octetstream_Sql;
17 use PhpMyAdmin\Plugins\Transformations\Output\Text_Plain_Json;
18 use PhpMyAdmin\Plugins\Transformations\Output\Text_Plain_Sql;
19 use PhpMyAdmin\Plugins\Transformations\Text_Plain_Link;
20 use PhpMyAdmin\Plugins\TransformationsPlugin;
21 use PhpMyAdmin\ResponseRenderer;
22 use PhpMyAdmin\Sanitize;
23 use PhpMyAdmin\Sql;
24 use PhpMyAdmin\SqlParser\Parser;
25 use PhpMyAdmin\SqlParser\Statements\SelectStatement;
26 use PhpMyAdmin\SqlParser\Utils\Query;
27 use PhpMyAdmin\Table;
28 use PhpMyAdmin\Template;
29 use PhpMyAdmin\Theme;
30 use PhpMyAdmin\Transformations;
31 use PhpMyAdmin\Url;
32 use PhpMyAdmin\Util;
33 use PhpMyAdmin\Utils\Gis;
35 use function __;
36 use function array_filter;
37 use function array_keys;
38 use function array_merge;
39 use function array_shift;
40 use function bin2hex;
41 use function ceil;
42 use function class_exists;
43 use function count;
44 use function explode;
45 use function file_exists;
46 use function floor;
47 use function htmlspecialchars;
48 use function implode;
49 use function in_array;
50 use function intval;
51 use function is_array;
52 use function is_numeric;
53 use function json_encode;
54 use function max;
55 use function mb_check_encoding;
56 use function mb_strlen;
57 use function mb_strpos;
58 use function mb_strtolower;
59 use function mb_strtoupper;
60 use function mb_substr;
61 use function md5;
62 use function mt_getrandmax;
63 use function pack;
64 use function preg_match;
65 use function preg_replace;
66 use function random_int;
67 use function str_contains;
68 use function str_ends_with;
69 use function str_replace;
70 use function strcasecmp;
71 use function strip_tags;
72 use function stripos;
73 use function strlen;
74 use function strpos;
75 use function strtoupper;
76 use function substr;
77 use function trim;
79 /**
80 * Handle all the functionalities related to displaying results
81 * of sql queries, stored procedure, browsing sql processes or
82 * displaying binary log.
84 class Results
86 public const POSITION_LEFT = 'left';
87 public const POSITION_RIGHT = 'right';
88 public const POSITION_BOTH = 'both';
89 public const POSITION_NONE = 'none';
91 public const DISPLAY_FULL_TEXT = 'F';
92 public const DISPLAY_PARTIAL_TEXT = 'P';
94 public const HEADER_FLIP_TYPE_AUTO = 'auto';
95 public const HEADER_FLIP_TYPE_CSS = 'css';
96 public const HEADER_FLIP_TYPE_FAKE = 'fake';
98 public const RELATIONAL_KEY = 'K';
99 public const RELATIONAL_DISPLAY_COLUMN = 'D';
101 public const GEOMETRY_DISP_GEOM = 'GEOM';
102 public const GEOMETRY_DISP_WKT = 'WKT';
103 public const GEOMETRY_DISP_WKB = 'WKB';
105 public const SMART_SORT_ORDER = 'SMART';
106 public const ASCENDING_SORT_DIR = 'ASC';
107 public const DESCENDING_SORT_DIR = 'DESC';
109 public const TABLE_TYPE_INNO_DB = 'InnoDB';
110 public const ALL_ROWS = 'all';
111 public const QUERY_TYPE_SELECT = 'SELECT';
113 public const ROUTINE_PROCEDURE = 'procedure';
114 public const ROUTINE_FUNCTION = 'function';
116 public const ACTION_LINK_CONTENT_ICONS = 'icons';
117 public const ACTION_LINK_CONTENT_TEXT = 'text';
120 * @psalm-var array{
121 * server: int,
122 * db: string,
123 * table: string,
124 * goto: string,
125 * sql_query: string,
126 * unlim_num_rows: int|numeric-string|false,
127 * fields_meta: FieldMetadata[],
128 * is_count: bool|null,
129 * is_export: bool|null,
130 * is_func: bool|null,
131 * is_analyse: bool|null,
132 * num_rows: int|numeric-string,
133 * fields_cnt: int,
134 * querytime: float|null,
135 * text_dir: string|null,
136 * is_maint: bool|null,
137 * is_explain: bool|null,
138 * is_show: bool|null,
139 * is_browse_distinct: bool|null,
140 * showtable: array<string, mixed>|null,
141 * printview: string|null,
142 * highlight_columns: array|null,
143 * display_params: array|null,
144 * mime_map: array|null,
145 * editable: bool|null,
146 * unique_id: int,
147 * whereClauseMap: array,
150 public $properties = [
151 /* server id */
152 'server' => 0,
154 /* Database name */
155 'db' => '',
157 /* Table name */
158 'table' => '',
160 /* the URL to go back in case of errors */
161 'goto' => '',
163 /* the SQL query */
164 'sql_query' => '',
166 /* the total number of rows returned by the SQL query without any appended "LIMIT" clause programmatically */
167 'unlim_num_rows' => 0,
169 /* meta information about fields */
170 'fields_meta' => [],
172 'is_count' => null,
174 'is_export' => null,
176 'is_func' => null,
178 'is_analyse' => null,
180 /* the total number of rows returned by the SQL query */
181 'num_rows' => 0,
183 /* the total number of fields returned by the SQL query */
184 'fields_cnt' => 0,
186 /* time taken for execute the SQL query */
187 'querytime' => null,
189 'text_dir' => null,
191 'is_maint' => null,
193 'is_explain' => null,
195 'is_show' => null,
197 'is_browse_distinct' => null,
199 /* table definitions */
200 'showtable' => null,
202 'printview' => null,
204 /* column names to highlight */
205 'highlight_columns' => null,
207 /* display information */
208 'display_params' => null,
210 /* mime types information of fields */
211 'mime_map' => null,
213 'editable' => null,
215 /* random unique ID to distinguish result set */
216 'unique_id' => 0,
218 /* where clauses for each row, each table in the row */
219 'whereClauseMap' => [],
223 * This variable contains the column transformation information
224 * for some of the system databases.
225 * One element of this array represent all relevant columns in all tables in
226 * one specific database
228 * @var array<string, array<string, array<string, string[]>>>
229 * @psalm-var array<string, array<string, array<string, array{string, class-string, string}>>> $transformationInfo
231 public $transformationInfo;
233 /** @var DatabaseInterface */
234 private $dbi;
236 /** @var Relation */
237 private $relation;
239 /** @var Transformations */
240 private $transformations;
242 /** @var Template */
243 public $template;
246 * @param string $db the database name
247 * @param string $table the table name
248 * @param int $server the server id
249 * @param string $goto the URL to go back in case of errors
250 * @param string $sqlQuery the SQL query
252 public function __construct(DatabaseInterface $dbi, $db, $table, $server, $goto, $sqlQuery)
254 $this->dbi = $dbi;
256 $this->relation = new Relation($this->dbi);
257 $this->transformations = new Transformations();
258 $this->template = new Template();
260 $this->setDefaultTransformations();
262 $this->properties['db'] = $db;
263 $this->properties['table'] = $table;
264 $this->properties['server'] = $server;
265 $this->properties['goto'] = $goto;
266 $this->properties['sql_query'] = $sqlQuery;
267 $this->properties['unique_id'] = random_int(0, mt_getrandmax());
271 * Sets default transformations for some columns
273 private function setDefaultTransformations(): void
275 $jsonHighlightingData = [
276 'libraries/classes/Plugins/Transformations/Output/Text_Plain_Json.php',
277 Text_Plain_Json::class,
278 'Text_Plain',
280 $sqlHighlightingData = [
281 'libraries/classes/Plugins/Transformations/Output/Text_Plain_Sql.php',
282 Text_Plain_Sql::class,
283 'Text_Plain',
285 $blobSqlHighlightingData = [
286 'libraries/classes/Plugins/Transformations/Output/Text_Octetstream_Sql.php',
287 Text_Octetstream_Sql::class,
288 'Text_Octetstream',
290 $linkData = [
291 'libraries/classes/Plugins/Transformations/Text_Plain_Link.php',
292 Text_Plain_Link::class,
293 'Text_Plain',
295 $this->transformationInfo = [
296 'information_schema' => [
297 'events' => ['event_definition' => $sqlHighlightingData],
298 'processlist' => ['info' => $sqlHighlightingData],
299 'routines' => ['routine_definition' => $sqlHighlightingData],
300 'triggers' => ['action_statement' => $sqlHighlightingData],
301 'views' => ['view_definition' => $sqlHighlightingData],
303 'mysql' => [
304 'event' => [
305 'body' => $blobSqlHighlightingData,
306 'body_utf8' => $blobSqlHighlightingData,
308 'general_log' => ['argument' => $sqlHighlightingData],
309 'help_category' => ['url' => $linkData],
310 'help_topic' => [
311 'example' => $sqlHighlightingData,
312 'url' => $linkData,
314 'proc' => [
315 'param_list' => $blobSqlHighlightingData,
316 'returns' => $blobSqlHighlightingData,
317 'body' => $blobSqlHighlightingData,
318 'body_utf8' => $blobSqlHighlightingData,
320 'slow_log' => ['sql_text' => $sqlHighlightingData],
324 $relationParameters = $this->relation->getRelationParameters();
325 if ($relationParameters->db === null) {
326 return;
329 $relDb = [];
330 if ($relationParameters->sqlHistoryFeature !== null) {
331 $relDb[$relationParameters->sqlHistoryFeature->history->getName()] = ['sqlquery' => $sqlHighlightingData];
334 if ($relationParameters->bookmarkFeature !== null) {
335 $relDb[$relationParameters->bookmarkFeature->bookmark->getName()] = ['query' => $sqlHighlightingData];
338 if ($relationParameters->trackingFeature !== null) {
339 $relDb[$relationParameters->trackingFeature->tracking->getName()] = [
340 'schema_sql' => $sqlHighlightingData,
341 'data_sql' => $sqlHighlightingData,
345 if ($relationParameters->favoriteTablesFeature !== null) {
346 $table = $relationParameters->favoriteTablesFeature->favorite->getName();
347 $relDb[$table] = ['tables' => $jsonHighlightingData];
350 if ($relationParameters->recentlyUsedTablesFeature !== null) {
351 $table = $relationParameters->recentlyUsedTablesFeature->recent->getName();
352 $relDb[$table] = ['tables' => $jsonHighlightingData];
355 if ($relationParameters->savedQueryByExampleSearchesFeature !== null) {
356 $table = $relationParameters->savedQueryByExampleSearchesFeature->savedSearches->getName();
357 $relDb[$table] = ['search_data' => $jsonHighlightingData];
360 if ($relationParameters->databaseDesignerSettingsFeature !== null) {
361 $table = $relationParameters->databaseDesignerSettingsFeature->designerSettings->getName();
362 $relDb[$table] = ['settings_data' => $jsonHighlightingData];
365 if ($relationParameters->uiPreferencesFeature !== null) {
366 $table = $relationParameters->uiPreferencesFeature->tableUiPrefs->getName();
367 $relDb[$table] = ['prefs' => $jsonHighlightingData];
370 if ($relationParameters->userPreferencesFeature !== null) {
371 $table = $relationParameters->userPreferencesFeature->userConfig->getName();
372 $relDb[$table] = ['config_data' => $jsonHighlightingData];
375 if ($relationParameters->exportTemplatesFeature !== null) {
376 $table = $relationParameters->exportTemplatesFeature->exportTemplates->getName();
377 $relDb[$table] = ['template_data' => $jsonHighlightingData];
380 $this->transformationInfo[$relationParameters->db->getName()] = $relDb;
384 * Set properties which were not initialized at the constructor
386 * @param int|string $unlimNumRows the total number of rows returned by the SQL query without
387 * any appended "LIMIT" clause programmatically
388 * @param FieldMetadata[] $fieldsMeta meta information about fields
389 * @param bool $isCount statement is SELECT COUNT
390 * @param bool $isExport statement contains INTO OUTFILE
391 * @param bool $isFunction statement contains a function like SUM()
392 * @param bool $isAnalyse statement contains PROCEDURE ANALYSE
393 * @param int|string $numRows total no. of rows returned by SQL query
394 * @param int $fieldsCount total no.of fields returned by SQL query
395 * @param double $queryTime time taken for execute the SQL query
396 * @param string $textDirection text direction
397 * @param bool $isMaintenance statement contains a maintenance command
398 * @param bool $isExplain statement contains EXPLAIN
399 * @param bool $isShow statement contains SHOW
400 * @param array<string, mixed>|null $showTable table definitions
401 * @param string|null $printView print view was requested
402 * @param bool $editable whether the results set is editable
403 * @param bool $isBrowseDistinct whether browsing distinct values
404 * @psalm-param int|numeric-string $unlimNumRows
405 * @psalm-param int|numeric-string $numRows
407 public function setProperties(
408 $unlimNumRows,
409 array $fieldsMeta,
410 $isCount,
411 $isExport,
412 $isFunction,
413 $isAnalyse,
414 $numRows,
415 $fieldsCount,
416 $queryTime,
417 $textDirection,
418 $isMaintenance,
419 $isExplain,
420 $isShow,
421 ?array $showTable,
422 $printView,
423 $editable,
424 $isBrowseDistinct
425 ): void {
426 $this->properties['unlim_num_rows'] = $unlimNumRows;
427 $this->properties['fields_meta'] = $fieldsMeta;
428 $this->properties['is_count'] = $isCount;
429 $this->properties['is_export'] = $isExport;
430 $this->properties['is_func'] = $isFunction;
431 $this->properties['is_analyse'] = $isAnalyse;
432 $this->properties['num_rows'] = $numRows;
433 $this->properties['fields_cnt'] = $fieldsCount;
434 $this->properties['querytime'] = $queryTime;
435 $this->properties['text_dir'] = $textDirection;
436 $this->properties['is_maint'] = $isMaintenance;
437 $this->properties['is_explain'] = $isExplain;
438 $this->properties['is_show'] = $isShow;
439 $this->properties['showtable'] = $showTable;
440 $this->properties['printview'] = $printView;
441 $this->properties['editable'] = $editable;
442 $this->properties['is_browse_distinct'] = $isBrowseDistinct;
446 * Defines the parts to display for a print view
448 private function setDisplayPartsForPrintView(DisplayParts $displayParts): DisplayParts
450 return $displayParts->with([
451 'hasEditLink' => false,
452 'deleteLink' => DisplayParts::NO_DELETE,
453 'hasSortLink' => false,
454 'hasNavigationBar' => false,
455 'hasBookmarkForm' => false,
456 'hasTextButton' => false,
457 'hasPrintLink' => false,
462 * Defines the parts to display for a SHOW statement
464 private function setDisplayPartsForShow(DisplayParts $displayParts): DisplayParts
466 preg_match(
467 '@^SHOW[[:space:]]+(VARIABLES|(FULL[[:space:]]+)?'
468 . 'PROCESSLIST|STATUS|TABLE|GRANTS|CREATE|LOGS|DATABASES|FIELDS'
469 . ')@i',
470 $this->properties['sql_query'],
471 $which
474 $bIsProcessList = isset($which[1]);
475 if ($bIsProcessList) {
476 $str = ' ' . strtoupper($which[1]);
477 $bIsProcessList = strpos($str, 'PROCESSLIST') > 0;
480 return $displayParts->with([
481 'hasEditLink' => false,
482 'deleteLink' => $bIsProcessList ? DisplayParts::KILL_PROCESS : DisplayParts::NO_DELETE,
483 'hasSortLink' => false,
484 'hasNavigationBar' => false,
485 'hasBookmarkForm' => true,
486 'hasTextButton' => true,
487 'hasPrintLink' => true,
492 * Defines the parts to display for statements not related to data
494 private function setDisplayPartsForNonData(DisplayParts $displayParts): DisplayParts
496 // Statement is a "SELECT COUNT", a
497 // "CHECK/ANALYZE/REPAIR/OPTIMIZE/CHECKSUM", an "EXPLAIN" one or
498 // contains a "PROC ANALYSE" part
499 return $displayParts->with([
500 'hasEditLink' => false,
501 'deleteLink' => DisplayParts::NO_DELETE,
502 'hasSortLink' => false,
503 'hasNavigationBar' => false,
504 'hasBookmarkForm' => true,
505 'hasTextButton' => (bool) $this->properties['is_maint'],
506 'hasPrintLink' => true,
511 * Defines the parts to display for other statements (probably SELECT).
513 private function setDisplayPartsForSelect(DisplayParts $displayParts): DisplayParts
515 $fieldsMeta = $this->properties['fields_meta'];
516 $previousTable = '';
517 $numberOfColumns = $this->properties['fields_cnt'];
518 $hasTextButton = true;
519 $hasEditLink = $displayParts->hasEditLink;
520 $deleteLink = $displayParts->deleteLink;
521 $hasPrintLink = $displayParts->hasPrintLink;
523 for ($i = 0; $i < $numberOfColumns; $i++) {
524 $isLink = $hasEditLink || $deleteLink !== DisplayParts::NO_DELETE || $displayParts->hasSortLink;
526 // Displays edit/delete/sort/insert links?
527 if (
528 $isLink
529 && $previousTable != ''
530 && $fieldsMeta[$i]->table != ''
531 && $fieldsMeta[$i]->table != $previousTable
533 // don't display links
534 $hasEditLink = false;
535 $deleteLink = DisplayParts::NO_DELETE;
536 break;
539 // Always display print view link
540 $hasPrintLink = true;
541 if ($fieldsMeta[$i]->table == '') {
542 continue;
545 $previousTable = $fieldsMeta[$i]->table;
548 if ($previousTable == '') { // no table for any of the columns
549 // don't display links
550 $hasEditLink = false;
551 $deleteLink = DisplayParts::NO_DELETE;
554 return $displayParts->with([
555 'hasEditLink' => $hasEditLink,
556 'deleteLink' => $deleteLink,
557 'hasTextButton' => $hasTextButton,
558 'hasPrintLink' => $hasPrintLink,
563 * Defines the parts to display for the results of a SQL query
564 * and the total number of rows
566 * @see getTable()
568 * @return array<int, DisplayParts|int|mixed> the first element is a {@see DisplayParts} object
569 * the second element is the total number of rows returned
570 * by the SQL query without any programmatically appended
571 * LIMIT clause (just a copy of $unlim_num_rows if it exists,
572 * else computed inside this function)
573 * @psalm-return array{DisplayParts, int|mixed}
575 private function setDisplayPartsAndTotal(DisplayParts $displayParts): array
577 $theTotal = 0;
579 // 1. Following variables are needed for use in isset/empty or
580 // use with array indexes or safe use in foreach
581 $db = $this->properties['db'];
582 $table = $this->properties['table'];
583 $unlimNumRows = $this->properties['unlim_num_rows'];
584 $numRows = $this->properties['num_rows'];
585 $printView = $this->properties['printview'];
587 // 2. Updates the display parts
588 if ($printView == '1') {
589 $displayParts = $this->setDisplayPartsForPrintView($displayParts);
590 } elseif (
591 $this->properties['is_count'] || $this->properties['is_analyse']
592 || $this->properties['is_maint'] || $this->properties['is_explain']
594 $displayParts = $this->setDisplayPartsForNonData($displayParts);
595 } elseif ($this->properties['is_show']) {
596 $displayParts = $this->setDisplayPartsForShow($displayParts);
597 } else {
598 $displayParts = $this->setDisplayPartsForSelect($displayParts);
601 // 3. Gets the total number of rows if it is unknown
602 if ($unlimNumRows > 0) {
603 $theTotal = $unlimNumRows;
604 } elseif (
605 $displayParts->hasNavigationBar
606 || $displayParts->hasSortLink
607 && $db !== '' && $table !== ''
609 $theTotal = $this->dbi->getTable($db, $table)->countRecords();
612 // if for COUNT query, number of rows returned more than 1
613 // (may be being used GROUP BY)
614 if ($this->properties['is_count'] && $numRows > 1) {
615 $displayParts = $displayParts->with([
616 'hasNavigationBar' => true,
617 'hasSortLink' => true,
621 // 4. If navigation bar or sorting fields names URLs should be
622 // displayed but there is only one row, change these settings to
623 // false
624 if ($displayParts->hasNavigationBar || $displayParts->hasSortLink) {
625 // - Do not display sort links if less than 2 rows.
626 // - For a VIEW we (probably) did not count the number of rows
627 // so don't test this number here, it would remove the possibility
628 // of sorting VIEW results.
629 $tableObject = new Table($table, $db);
630 if ($unlimNumRows < 2 && ! $tableObject->isView()) {
631 $displayParts = $displayParts->with(['hasSortLink' => false]);
635 return [
636 $displayParts,
637 $theTotal,
642 * Return true if we are executing a query in the form of
643 * "SELECT * FROM <a table> ..."
645 * @see getTableHeaders(), getColumnParams()
647 * @param array $analyzedSqlResults analyzed sql results
649 private function isSelect(array $analyzedSqlResults): bool
651 return ! ($this->properties['is_count']
652 || $this->properties['is_export']
653 || $this->properties['is_func']
654 || $this->properties['is_analyse'])
655 && ! empty($analyzedSqlResults['select_from'])
656 && ! empty($analyzedSqlResults['statement']->from)
657 && (count($analyzedSqlResults['statement']->from) === 1)
658 && ! empty($analyzedSqlResults['statement']->from[0]->table);
662 * Possibly return a page selector for table navigation
664 * @return array{string, int} ($output, $nbTotalPage)
666 private function getHtmlPageSelector(): array
668 $pageNow = (int) floor($_SESSION['tmpval']['pos'] / $_SESSION['tmpval']['max_rows']) + 1;
670 $nbTotalPage = (int) ceil((int) $this->properties['unlim_num_rows'] / $_SESSION['tmpval']['max_rows']);
672 $output = '';
673 if ($nbTotalPage > 1) {
674 $urlParams = [
675 'db' => $this->properties['db'],
676 'table' => $this->properties['table'],
677 'sql_query' => $this->properties['sql_query'],
678 'goto' => $this->properties['goto'],
679 'is_browse_distinct' => $this->properties['is_browse_distinct'],
682 $output = $this->template->render('display/results/page_selector', [
683 'url_params' => $urlParams,
684 'page_selector' => Util::pageselector(
685 'pos',
686 $_SESSION['tmpval']['max_rows'],
687 $pageNow,
688 $nbTotalPage
693 return [
694 $output,
695 $nbTotalPage,
700 * Get a navigation bar to browse among the results of a SQL query
702 * @see getTable()
704 * @param int $posNext the offset for the "next" page
705 * @param int $posPrevious the offset for the "previous" page
706 * @param bool $isInnodb whether its InnoDB or not
707 * @param array $sortByKeyData the sort by key dialog
709 * @return array
711 private function getTableNavigation(
712 int $posNext,
713 int $posPrevious,
714 bool $isInnodb,
715 array $sortByKeyData
716 ): array {
717 $isShowingAll = $_SESSION['tmpval']['max_rows'] === self::ALL_ROWS;
719 $pageSelector = '';
720 $numberTotalPage = 1;
721 if (! $isShowingAll) {
723 $pageSelector,
724 $numberTotalPage,
725 ] = $this->getHtmlPageSelector();
728 $isLastPage = $this->properties['unlim_num_rows'] !== -1 && $this->properties['unlim_num_rows'] !== false
729 && ($isShowingAll
730 || intval($_SESSION['tmpval']['pos']) + intval($_SESSION['tmpval']['max_rows'])
731 >= $this->properties['unlim_num_rows']
732 || $this->properties['num_rows'] < $_SESSION['tmpval']['max_rows']);
734 $onsubmit = ' onsubmit="return '
735 . (intval($_SESSION['tmpval']['pos'])
736 + intval($_SESSION['tmpval']['max_rows'])
737 < $this->properties['unlim_num_rows']
738 && $this->properties['num_rows'] >= intval($_SESSION['tmpval']['max_rows'])
739 ? 'true'
740 : 'false') . ';"';
742 $hasRealEndInput = $isInnodb && $this->properties['unlim_num_rows'] > $GLOBALS['cfg']['MaxExactCount'];
743 $posLast = @((int) ceil(
744 (int) $this->properties['unlim_num_rows'] / $_SESSION['tmpval']['max_rows']
745 ) - 1) * intval($_SESSION['tmpval']['max_rows']);
747 $hiddenFields = [
748 'db' => $this->properties['db'],
749 'table' => $this->properties['table'],
750 'server' => $this->properties['server'],
751 'sql_query' => $this->properties['sql_query'],
752 'is_browse_distinct' => $this->properties['is_browse_distinct'],
753 'goto' => $this->properties['goto'],
756 return [
757 'page_selector' => $pageSelector,
758 'number_total_page' => $numberTotalPage,
759 'has_show_all' => $GLOBALS['cfg']['ShowAll'] || ($this->properties['unlim_num_rows'] <= 500),
760 'hidden_fields' => $hiddenFields,
761 'session_max_rows' => $isShowingAll ? $GLOBALS['cfg']['MaxRows'] : 'all',
762 'is_showing_all' => $isShowingAll,
763 'max_rows' => $_SESSION['tmpval']['max_rows'],
764 'pos' => $_SESSION['tmpval']['pos'],
765 'sort_by_key' => $sortByKeyData,
766 'pos_previous' => $posPrevious,
767 'pos_next' => $posNext,
768 'pos_last' => $posLast,
769 'is_last_page' => $isLastPage,
770 'has_real_end_input' => $hasRealEndInput,
771 'onsubmit' => $onsubmit,
776 * Get the headers of the results table, for all of the columns
778 * @see getTableHeaders()
780 * @param array $analyzedSqlResults analyzed sql results
781 * @param array $sortExpression sort expression
782 * @param array<int, string> $sortExpressionNoDirection sort expression
783 * without direction
784 * @param array $sortDirection sort direction
785 * @param bool $isLimitedDisplay with limited operations
786 * or not
787 * @param string $unsortedSqlQuery query without the sort part
789 * @return string html content
791 private function getTableHeadersForColumns(
792 bool $hasSortLink,
793 array $analyzedSqlResults,
794 array $sortExpression,
795 array $sortExpressionNoDirection,
796 array $sortDirection,
797 $isLimitedDisplay,
798 $unsortedSqlQuery
800 // required to generate sort links that will remember whether the
801 // "Show all" button has been clicked
802 $sqlMd5 = md5($this->properties['server'] . $this->properties['db'] . $this->properties['sql_query']);
803 $sessionMaxRows = $isLimitedDisplay
805 : $_SESSION['tmpval']['query'][$sqlMd5]['max_rows'];
807 // Following variable are needed for use in isset/empty or
808 // use with array indexes/safe use in the for loop
809 $highlightColumns = $this->properties['highlight_columns'];
810 $fieldsMeta = $this->properties['fields_meta'];
812 // Prepare Display column comments if enabled
813 // ($GLOBALS['cfg']['ShowBrowseComments']).
814 $commentsMap = $this->getTableCommentsArray($analyzedSqlResults);
816 [$colOrder, $colVisib] = $this->getColumnParams($analyzedSqlResults);
818 // optimize: avoid calling a method on each iteration
819 $numberOfColumns = $this->properties['fields_cnt'];
821 $columns = [];
823 for ($j = 0; $j < $numberOfColumns; $j++) {
824 // PHP 7.4 fix for accessing array offset on bool
825 $colVisibCurrent = $colVisib[$j] ?? null;
827 // assign $i with the appropriate column order
828 $i = $colOrder ? $colOrder[$j] : $j;
830 // See if this column should get highlight because it's used in the
831 // where-query.
832 $name = $fieldsMeta[$i]->name;
833 $conditionField = isset($highlightColumns[$name])
834 || isset($highlightColumns[Util::backquote($name)]);
836 // Prepare comment-HTML-wrappers for each row, if defined/enabled.
837 $comments = $this->getCommentForRow($commentsMap, $fieldsMeta[$i]);
838 $displayParams = $this->properties['display_params'] ?? [];
840 if ($hasSortLink && ! $isLimitedDisplay) {
841 $sortedHeaderData = $this->getOrderLinkAndSortedHeaderHtml(
842 $fieldsMeta[$i],
843 $sortExpression,
844 $sortExpressionNoDirection,
845 $unsortedSqlQuery,
846 $sessionMaxRows,
847 $comments,
848 $sortDirection,
849 $colVisib,
850 $colVisibCurrent
853 $orderLink = $sortedHeaderData['order_link'];
854 $columns[] = $sortedHeaderData;
856 $displayParams['desc'][] = ' <th '
857 . 'class="draggable'
858 . ($conditionField ? ' condition' : '')
859 . '" data-column="' . htmlspecialchars($fieldsMeta[$i]->name)
860 . '">' . "\n" . $orderLink . $comments . ' </th>' . "\n";
861 } else {
862 // Results can't be sorted
863 // Prepare columns to draggable effect for non sortable columns
864 $columns[] = [
865 'column_name' => $fieldsMeta[$i]->name,
866 'comments' => $comments,
867 'is_column_hidden' => $colVisib && ! $colVisibCurrent,
868 'is_column_numeric' => $this->isColumnNumeric($fieldsMeta[$i]),
869 'has_condition' => $conditionField,
872 $displayParams['desc'][] = ' <th '
873 . 'class="draggable'
874 . ($conditionField ? ' condition"' : '')
875 . '" data-column="' . htmlspecialchars((string) $fieldsMeta[$i]->name)
876 . '"> '
877 . htmlspecialchars((string) $fieldsMeta[$i]->name)
878 . $comments . ' </th>';
881 $this->properties['display_params'] = $displayParams;
884 return $this->template->render('display/results/table_headers_for_columns', [
885 'is_sortable' => $hasSortLink && ! $isLimitedDisplay,
886 'columns' => $columns,
891 * Get the headers of the results table
893 * @see getTable()
895 * @param array $analyzedSqlResults analyzed sql results
896 * @param string $unsortedSqlQuery the unsorted sql query
897 * @param array $sortExpression sort expression
898 * @param array<int, string> $sortExpressionNoDirection sort expression without direction
899 * @param array $sortDirection sort direction
900 * @param bool $isLimitedDisplay with limited operations or not
902 * @psalm-return array{
903 * column_order: array,
904 * options: array,
905 * has_bulk_actions_form: bool,
906 * button: string,
907 * table_headers_for_columns: string,
908 * column_at_right_side: string,
911 private function getTableHeaders(
912 DisplayParts $displayParts,
913 array $analyzedSqlResults,
914 $unsortedSqlQuery,
915 array $sortExpression = [],
916 array $sortExpressionNoDirection = [],
917 array $sortDirection = [],
918 $isLimitedDisplay = false
919 ): array {
920 // Needed for use in isset/empty or
921 // use with array indexes/safe use in foreach
922 $printView = $this->properties['printview'];
923 $displayParams = $this->properties['display_params'];
925 // Output data needed for column reordering and show/hide column
926 $columnOrder = $this->getDataForResettingColumnOrder($analyzedSqlResults);
928 $displayParams['emptypre'] = 0;
929 $displayParams['emptyafter'] = 0;
930 $displayParams['textbtn'] = '';
931 $fullOrPartialTextLink = '';
933 $this->properties['display_params'] = $displayParams;
935 // Display options (if we are not in print view)
936 $optionsBlock = [];
937 if (! (isset($printView) && ($printView == '1')) && ! $isLimitedDisplay) {
938 $optionsBlock = $this->getOptionsBlock();
940 // prepare full/partial text button or link
941 $fullOrPartialTextLink = $this->getFullOrPartialTextButtonOrLink();
944 // 1. Set $colspan and generate html with full/partial
945 // text button or link
946 $colspan = $displayParts->hasEditLink
947 && $displayParts->deleteLink !== DisplayParts::NO_DELETE ? ' colspan="4"' : '';
948 $buttonHtml = $this->getFieldVisibilityParams($displayParts, $fullOrPartialTextLink, $colspan);
950 // 2. Displays the fields' name
951 // 2.0 If sorting links should be used, checks if the query is a "JOIN"
952 // statement (see 2.1.3)
954 // See if we have to highlight any header fields of a WHERE query.
955 // Uses SQL-Parser results.
956 $this->setHighlightedColumnGlobalField($analyzedSqlResults);
958 // Get the headers for all of the columns
959 $tableHeadersForColumns = $this->getTableHeadersForColumns(
960 $displayParts->hasSortLink,
961 $analyzedSqlResults,
962 $sortExpression,
963 $sortExpressionNoDirection,
964 $sortDirection,
965 $isLimitedDisplay,
966 $unsortedSqlQuery
969 // Display column at rightside - checkboxes or empty column
970 $columnAtRightSide = '';
971 if (! $printView) {
972 $columnAtRightSide = $this->getColumnAtRightSide($displayParts, $fullOrPartialTextLink, $colspan);
975 return [
976 'column_order' => $columnOrder,
977 'options' => $optionsBlock,
978 'has_bulk_actions_form' => $displayParts->deleteLink === DisplayParts::DELETE_ROW
979 || $displayParts->deleteLink === DisplayParts::KILL_PROCESS,
980 'button' => $buttonHtml,
981 'table_headers_for_columns' => $tableHeadersForColumns,
982 'column_at_right_side' => $columnAtRightSide,
987 * Prepare sort by key dropdown - html code segment
989 * @see getTableHeaders()
991 * @param array|null $sortExpression the sort expression
992 * @param string $unsortedSqlQuery the unsorted sql query
994 * @return array[]
995 * @psalm-return array{hidden_fields?:array, options?:array}
997 private function getSortByKeyDropDown(
998 ?array $sortExpression,
999 string $unsortedSqlQuery
1000 ): array {
1001 // grab indexes data:
1002 $indexes = Index::getFromTable($this->properties['table'], $this->properties['db']);
1004 // do we have any index?
1005 if ($indexes === []) {
1006 return [];
1009 $hiddenFields = [
1010 'db' => $this->properties['db'],
1011 'table' => $this->properties['table'],
1012 'server' => $this->properties['server'],
1013 'sort_by_key' => '1',
1016 // Keep the number of rows (25, 50, 100, ...) when changing sort key value
1017 if (isset($_SESSION['tmpval']) && isset($_SESSION['tmpval']['max_rows'])) {
1018 $hiddenFields['session_max_rows'] = $_SESSION['tmpval']['max_rows'];
1021 $isIndexUsed = false;
1022 $localOrder = is_array($sortExpression) ? implode(', ', $sortExpression) : '';
1024 $options = [];
1025 foreach ($indexes as $index) {
1026 $ascSort = '`'
1027 . implode('` ASC, `', array_keys($index->getColumns()))
1028 . '` ASC';
1030 $descSort = '`'
1031 . implode('` DESC, `', array_keys($index->getColumns()))
1032 . '` DESC';
1034 $isIndexUsed = $isIndexUsed
1035 || $localOrder === $ascSort
1036 || $localOrder === $descSort;
1038 $unsortedSqlQueryFirstPart = $unsortedSqlQuery;
1039 $unsortedSqlQuerySecondPart = '';
1040 if (
1041 preg_match(
1042 '@(.*)([[:space:]](LIMIT (.*)|PROCEDURE (.*)|FOR UPDATE|LOCK IN SHARE MODE))@is',
1043 $unsortedSqlQuery,
1044 $myReg
1047 $unsortedSqlQueryFirstPart = $myReg[1];
1048 $unsortedSqlQuerySecondPart = $myReg[2];
1051 $options[] = [
1052 'value' => $unsortedSqlQueryFirstPart . ' ORDER BY '
1053 . $ascSort . $unsortedSqlQuerySecondPart,
1054 'content' => $index->getName() . ' (ASC)',
1055 'is_selected' => $localOrder === $ascSort,
1057 $options[] = [
1058 'value' => $unsortedSqlQueryFirstPart . ' ORDER BY '
1059 . $descSort . $unsortedSqlQuerySecondPart,
1060 'content' => $index->getName() . ' (DESC)',
1061 'is_selected' => $localOrder === $descSort,
1065 $options[] = [
1066 'value' => $unsortedSqlQuery,
1067 'content' => __('None'),
1068 'is_selected' => ! $isIndexUsed,
1071 return ['hidden_fields' => $hiddenFields, 'options' => $options];
1075 * Set column span, row span and prepare html with full/partial
1076 * text button or link
1078 * @see getTableHeaders()
1080 * @param string $fullOrPartialTextLink full/partial link or text button
1081 * @param string $colspan column span of table header
1083 * @return string html with full/partial text button or link
1085 private function getFieldVisibilityParams(
1086 DisplayParts $displayParts,
1087 string $fullOrPartialTextLink,
1088 string $colspan
1090 $displayParams = $this->properties['display_params'];
1092 // 1. Displays the full/partial text button (part 1)...
1093 $buttonHtml = '<thead class="table-light"><tr>' . "\n";
1095 $emptyPreCondition = $displayParts->hasEditLink && $displayParts->deleteLink !== DisplayParts::NO_DELETE;
1097 $leftOrBoth = $GLOBALS['cfg']['RowActionLinks'] === self::POSITION_LEFT
1098 || $GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH;
1100 // ... before the result table
1101 if (
1102 ! $displayParts->hasEditLink
1103 && $displayParts->deleteLink === DisplayParts::NO_DELETE
1104 && $displayParts->hasTextButton
1106 $displayParams['emptypre'] = 0;
1107 } elseif ($leftOrBoth && $displayParts->hasTextButton) {
1108 // ... at the left column of the result table header if possible
1109 // and required
1111 $displayParams['emptypre'] = $emptyPreCondition ? 4 : 0;
1113 $buttonHtml .= '<th class="column_action sticky d-print-none"' . $colspan
1114 . '>' . $fullOrPartialTextLink . '</th>';
1115 } elseif (
1116 $leftOrBoth
1117 && ($displayParts->hasEditLink || $displayParts->deleteLink !== DisplayParts::NO_DELETE)
1119 // ... elseif no button, displays empty(ies) col(s) if required
1121 $displayParams['emptypre'] = $emptyPreCondition ? 4 : 0;
1123 $buttonHtml .= '<td' . $colspan . '></td>';
1124 } elseif ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_NONE) {
1125 // ... elseif display an empty column if the actions links are
1126 // disabled to match the rest of the table
1127 $buttonHtml .= '<th class="column_action sticky"></th>';
1130 $this->properties['display_params'] = $displayParams;
1132 return $buttonHtml;
1136 * Get table comments as array
1138 * @see getTableHeaders()
1140 * @param array $analyzedSqlResults analyzed sql results
1142 * @return array table comments
1144 private function getTableCommentsArray(array $analyzedSqlResults)
1146 if (! $GLOBALS['cfg']['ShowBrowseComments'] || empty($analyzedSqlResults['statement']->from)) {
1147 return [];
1150 $ret = [];
1151 foreach ($analyzedSqlResults['statement']->from as $field) {
1152 if (empty($field->table)) {
1153 continue;
1156 $ret[$field->table] = $this->relation->getComments(
1157 empty($field->database) ? $this->properties['db'] : $field->database,
1158 $field->table
1162 return $ret;
1166 * Set global array for store highlighted header fields
1168 * @see getTableHeaders()
1170 * @param array $analyzedSqlResults analyzed sql results
1172 private function setHighlightedColumnGlobalField(array $analyzedSqlResults): void
1174 $highlightColumns = [];
1176 if (! empty($analyzedSqlResults['statement']->where)) {
1177 foreach ($analyzedSqlResults['statement']->where as $expr) {
1178 foreach ($expr->identifiers as $identifier) {
1179 $highlightColumns[$identifier] = 'true';
1184 $this->properties['highlight_columns'] = $highlightColumns;
1188 * Prepare data for column restoring and show/hide
1190 * @see getTableHeaders()
1192 * @param array $analyzedSqlResults analyzed sql results
1194 * @return array
1196 private function getDataForResettingColumnOrder(array $analyzedSqlResults): array
1198 if (! $this->isSelect($analyzedSqlResults)) {
1199 return [];
1202 [$columnOrder, $columnVisibility] = $this->getColumnParams($analyzedSqlResults);
1204 $tableCreateTime = '';
1205 $table = new Table($this->properties['table'], $this->properties['db']);
1206 if (! $table->isView()) {
1207 $tableCreateTime = $this->dbi->getTable(
1208 $this->properties['db'],
1209 $this->properties['table']
1210 )->getStatusInfo('Create_time');
1213 return [
1214 'order' => $columnOrder,
1215 'visibility' => $columnVisibility,
1216 'is_view' => $table->isView(),
1217 'table_create_time' => $tableCreateTime,
1222 * Prepare option fields block
1224 * @see getTableHeaders()
1226 * @return array
1228 private function getOptionsBlock(): array
1230 if (
1231 isset($_SESSION['tmpval']['possible_as_geometry'])
1232 && $_SESSION['tmpval']['possible_as_geometry'] == false
1233 && $_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_GEOM
1235 $_SESSION['tmpval']['geoOption'] = self::GEOMETRY_DISP_WKT;
1238 return [
1239 'geo_option' => $_SESSION['tmpval']['geoOption'],
1240 'hide_transformation' => $_SESSION['tmpval']['hide_transformation'],
1241 'display_blob' => $_SESSION['tmpval']['display_blob'],
1242 'display_binary' => $_SESSION['tmpval']['display_binary'],
1243 'relational_display' => $_SESSION['tmpval']['relational_display'],
1244 'possible_as_geometry' => $_SESSION['tmpval']['possible_as_geometry'],
1245 'pftext' => $_SESSION['tmpval']['pftext'],
1250 * Get full/partial text button or link
1252 * @see getTableHeaders()
1254 * @return string html content
1256 private function getFullOrPartialTextButtonOrLink(): string
1258 $GLOBALS['theme'] = $GLOBALS['theme'] ?? null;
1260 $urlParamsFullText = [
1261 'db' => $this->properties['db'],
1262 'table' => $this->properties['table'],
1263 'sql_query' => $this->properties['sql_query'],
1264 'goto' => $this->properties['goto'],
1265 'full_text_button' => 1,
1268 if ($_SESSION['tmpval']['pftext'] === self::DISPLAY_FULL_TEXT) {
1269 // currently in fulltext mode so show the opposite link
1270 $tmpImageFile = 's_partialtext.png';
1271 $tmpTxt = __('Partial texts');
1272 $urlParamsFullText['pftext'] = self::DISPLAY_PARTIAL_TEXT;
1273 } else {
1274 $tmpImageFile = 's_fulltext.png';
1275 $tmpTxt = __('Full texts');
1276 $urlParamsFullText['pftext'] = self::DISPLAY_FULL_TEXT;
1279 $tmpImage = '<img class="fulltext" src="'
1280 . ($GLOBALS['theme'] instanceof Theme ? $GLOBALS['theme']->getImgPath($tmpImageFile) : '')
1281 . '" alt="' . $tmpTxt . '" title="' . $tmpTxt . '">';
1283 return Generator::linkOrButton(Url::getFromRoute('/sql'), $urlParamsFullText, $tmpImage);
1287 * Get comment for row
1289 * @see getTableHeaders()
1291 * @param array $commentsMap comments array
1292 * @param FieldMetadata $fieldsMeta set of field properties
1294 * @return string html content
1296 private function getCommentForRow(array $commentsMap, FieldMetadata $fieldsMeta): string
1298 return $this->template->render('display/results/comment_for_row', [
1299 'comments_map' => $commentsMap,
1300 'column_name' => $fieldsMeta->name,
1301 'table_name' => $fieldsMeta->table,
1302 'limit_chars' => $GLOBALS['cfg']['LimitChars'],
1307 * Prepare parameters and html for sorted table header fields
1309 * @see getTableHeaders()
1311 * @param FieldMetadata $fieldsMeta set of field properties
1312 * @param array $sortExpression sort expression
1313 * @param array<int, string> $sortExpressionNoDirection sort expression without direction
1314 * @param string $unsortedSqlQuery the unsorted sql query
1315 * @param int $sessionMaxRows maximum rows resulted by sql
1316 * @param string $comments comment for row
1317 * @param array $sortDirection sort direction
1318 * @param bool $colVisib column is visible(false)
1319 * or column isn't visible(string array)
1320 * @param string $colVisibElement element of $col_visib array
1322 * @return array 2 element array - $orderLink, $sortedHeaderHtml
1323 * @psalm-return array{
1324 * column_name: string,
1325 * order_link: string,
1326 * comments: string,
1327 * is_browse_pointer_enabled: bool,
1328 * is_browse_marker_enabled: bool,
1329 * is_column_hidden: bool,
1330 * is_column_numeric: bool,
1333 private function getOrderLinkAndSortedHeaderHtml(
1334 FieldMetadata $fieldsMeta,
1335 array $sortExpression,
1336 array $sortExpressionNoDirection,
1337 $unsortedSqlQuery,
1338 $sessionMaxRows,
1339 string $comments,
1340 array $sortDirection,
1341 $colVisib,
1342 $colVisibElement
1343 ): array {
1344 // Checks if the table name is required; it's the case
1345 // for a query with a "JOIN" statement and if the column
1346 // isn't aliased, or in queries like
1347 // SELECT `1`.`master_field` , `2`.`master_field`
1348 // FROM `PMA_relation` AS `1` , `PMA_relation` AS `2`
1350 $sortTable = $fieldsMeta->table !== ''
1351 && $fieldsMeta->orgname === $fieldsMeta->name
1352 ? Util::backquote($fieldsMeta->table) . '.'
1353 : '';
1355 // Generates the orderby clause part of the query which is part
1356 // of URL
1357 [$singleSortOrder, $multiSortOrder, $orderImg] = $this->getSingleAndMultiSortUrls(
1358 $sortExpression,
1359 $sortExpressionNoDirection,
1360 $sortTable,
1361 $fieldsMeta->name,
1362 $sortDirection,
1363 $fieldsMeta
1366 if (
1367 preg_match(
1368 '@(.*)([[:space:]](LIMIT (.*)|PROCEDURE (.*)|FOR UPDATE|LOCK IN SHARE MODE))@is',
1369 $unsortedSqlQuery,
1370 $regs3
1373 $singleSortedSqlQuery = $regs3[1] . $singleSortOrder . $regs3[2];
1374 $multiSortedSqlQuery = $regs3[1] . $multiSortOrder . $regs3[2];
1375 } else {
1376 $singleSortedSqlQuery = $unsortedSqlQuery . $singleSortOrder;
1377 $multiSortedSqlQuery = $unsortedSqlQuery . $multiSortOrder;
1380 $singleUrlParams = [
1381 'db' => $this->properties['db'],
1382 'table' => $this->properties['table'],
1383 'sql_query' => $singleSortedSqlQuery,
1384 'sql_signature' => Core::signSqlQuery($singleSortedSqlQuery),
1385 'session_max_rows' => $sessionMaxRows,
1386 'is_browse_distinct' => $this->properties['is_browse_distinct'],
1389 $multiUrlParams = [
1390 'db' => $this->properties['db'],
1391 'table' => $this->properties['table'],
1392 'sql_query' => $multiSortedSqlQuery,
1393 'sql_signature' => Core::signSqlQuery($multiSortedSqlQuery),
1394 'session_max_rows' => $sessionMaxRows,
1395 'is_browse_distinct' => $this->properties['is_browse_distinct'],
1398 // Displays the sorting URL
1399 // enable sort order swapping for image
1400 $orderLink = $this->getSortOrderLink($orderImg, $fieldsMeta, $singleUrlParams, $multiUrlParams);
1402 $orderLink .= $this->getSortOrderHiddenInputs($multiUrlParams, $fieldsMeta->name);
1404 return [
1405 'column_name' => $fieldsMeta->name,
1406 'order_link' => $orderLink,
1407 'comments' => $comments,
1408 'is_browse_pointer_enabled' => $GLOBALS['cfg']['BrowsePointerEnable'] === true,
1409 'is_browse_marker_enabled' => $GLOBALS['cfg']['BrowseMarkerEnable'] === true,
1410 'is_column_hidden' => $colVisib && ! $colVisibElement,
1411 'is_column_numeric' => $this->isColumnNumeric($fieldsMeta),
1416 * Prepare parameters and html for sorted table header fields
1418 * @param array $sortExpression sort expression
1419 * @param array<int, string> $sortExpressionNoDirection sort expression without direction
1420 * @param string $sortTable The name of the table to which
1421 * the current column belongs to
1422 * @param string $nameToUseInSort The current column under
1423 * consideration
1424 * @param string[] $sortDirection sort direction
1425 * @param FieldMetadata $fieldsMeta set of field properties
1427 * @return string[] 3 element array - $single_sort_order, $sort_order, $order_img
1429 private function getSingleAndMultiSortUrls(
1430 array $sortExpression,
1431 array $sortExpressionNoDirection,
1432 string $sortTable,
1433 string $nameToUseInSort,
1434 array $sortDirection,
1435 FieldMetadata $fieldsMeta
1436 ): array {
1437 // Check if the current column is in the order by clause
1438 $isInSort = $this->isInSorted($sortExpression, $sortExpressionNoDirection, $sortTable, $nameToUseInSort);
1439 $currentName = $nameToUseInSort;
1440 if ($sortExpressionNoDirection[0] == '' || ! $isInSort) {
1441 $specialIndex = $sortExpressionNoDirection[0] == ''
1443 : count($sortExpressionNoDirection);
1444 $sortExpressionNoDirection[$specialIndex] = Util::backquote($currentName);
1445 $isTimeOrDate = $fieldsMeta->isType(FieldMetadata::TYPE_TIME)
1446 || $fieldsMeta->isType(FieldMetadata::TYPE_DATE)
1447 || $fieldsMeta->isType(FieldMetadata::TYPE_DATETIME)
1448 || $fieldsMeta->isType(FieldMetadata::TYPE_TIMESTAMP);
1449 $sortDirection[$specialIndex] = $isTimeOrDate ? self::DESCENDING_SORT_DIR : self::ASCENDING_SORT_DIR;
1452 $sortExpressionNoDirection = array_filter($sortExpressionNoDirection);
1453 $singleSortOrder = '';
1454 $sortOrderColumns = [];
1455 foreach ($sortExpressionNoDirection as $index => $expression) {
1456 $sortOrder = '';
1457 // check if this is the first clause,
1458 // if it is then we have to add "order by"
1459 $isFirstClause = ($index === 0);
1460 $nameToUseInSort = $expression;
1461 $sortTableNew = $sortTable;
1462 // Test to detect if the column name is a standard name
1463 // Standard name has the table name prefixed to the column name
1464 if (str_contains($nameToUseInSort, '.') && ! str_contains($nameToUseInSort, '(')) {
1465 $matches = explode('.', $nameToUseInSort);
1466 // Matches[0] has the table name
1467 // Matches[1] has the column name
1468 $nameToUseInSort = $matches[1];
1469 $sortTableNew = $matches[0];
1472 // $name_to_use_in_sort might contain a space due to
1473 // formatting of function expressions like "COUNT(name )"
1474 // so we remove the space in this situation
1475 $nameToUseInSort = str_replace([' )', '``'], [')', '`'], $nameToUseInSort);
1476 $nameToUseInSort = trim($nameToUseInSort, '`');
1478 // If this the first column name in the order by clause add
1479 // order by clause to the column name
1480 $sortOrder .= $isFirstClause ? "\nORDER BY " : '';
1482 // Again a check to see if the given column is a aggregate column
1483 if (str_contains($nameToUseInSort, '(')) {
1484 $sortOrder .= $nameToUseInSort;
1485 } else {
1486 if ($sortTableNew !== '' && ! str_ends_with($sortTableNew, '.')) {
1487 $sortTableNew .= '.';
1490 $sortOrder .= $sortTableNew . Util::backquote($nameToUseInSort);
1493 // Incase this is the current column save $single_sort_order
1494 if ($currentName === $nameToUseInSort) {
1495 $singleSortOrder = "\n" . 'ORDER BY ';
1497 if (! str_contains($currentName, '(')) {
1498 $singleSortOrder .= $sortTable;
1501 $singleSortOrder .= Util::backquote($currentName) . ' ';
1503 if ($isInSort) {
1504 [$singleSortOrder, $orderImg] = $this->getSortingUrlParams(
1505 $sortDirection[$index],
1506 $singleSortOrder
1508 } else {
1509 $singleSortOrder .= strtoupper($sortDirection[$index]);
1513 $sortOrder .= ' ';
1514 if ($currentName === $nameToUseInSort && $isInSort) {
1515 // We need to generate the arrow button and related html
1516 [$sortOrder, $orderImg] = $this->getSortingUrlParams($sortDirection[$index], $sortOrder);
1517 $orderImg .= ' <small>' . ($index + 1) . '</small>';
1518 } else {
1519 $sortOrder .= strtoupper($sortDirection[$index]);
1522 // Separate columns by a comma
1523 $sortOrderColumns[] = $sortOrder;
1526 return [
1527 $singleSortOrder,
1528 implode(', ', $sortOrderColumns),
1529 $orderImg ?? '',
1534 * Check whether the column is sorted
1536 * @see getTableHeaders()
1538 * @param array $sortExpression sort expression
1539 * @param array $sortExpressionNoDirection sort expression without direction
1540 * @param string $sortTable the table name
1541 * @param string $nameToUseInSort the sorting column name
1543 private function isInSorted(
1544 array $sortExpression,
1545 array $sortExpressionNoDirection,
1546 string $sortTable,
1547 string $nameToUseInSort
1548 ): bool {
1549 $indexInExpression = 0;
1551 foreach ($sortExpressionNoDirection as $index => $clause) {
1552 if (str_contains($clause, '.')) {
1553 $fragments = explode('.', $clause);
1554 $clause2 = $fragments[0] . '.' . str_replace('`', '', $fragments[1]);
1555 } else {
1556 $clause2 = $sortTable . str_replace('`', '', $clause);
1559 if ($clause2 === $sortTable . $nameToUseInSort) {
1560 $indexInExpression = $index;
1561 break;
1565 if (empty($sortExpression[$indexInExpression])) {
1566 return false;
1569 // Field name may be preceded by a space, or any number
1570 // of characters followed by a dot (tablename.fieldname)
1571 // so do a direct comparison for the sort expression;
1572 // this avoids problems with queries like
1573 // "SELECT id, count(id)..." and clicking to sort
1574 // on id or on count(id).
1575 // Another query to test this:
1576 // SELECT p.*, FROM_UNIXTIME(p.temps) FROM mytable AS p
1577 // (and try clicking on each column's header twice)
1578 $noSortTable = $sortTable === '' || mb_strpos(
1579 $sortExpressionNoDirection[$indexInExpression],
1580 $sortTable
1581 ) === false;
1582 $noOpenParenthesis = mb_strpos($sortExpressionNoDirection[$indexInExpression], '(') === false;
1583 if ($sortTable !== '' && $noSortTable && $noOpenParenthesis) {
1584 $newSortExpressionNoDirection = $sortTable
1585 . $sortExpressionNoDirection[$indexInExpression];
1586 } else {
1587 $newSortExpressionNoDirection = $sortExpressionNoDirection[$indexInExpression];
1590 //Back quotes are removed in next comparison, so remove them from value
1591 //to compare.
1592 $nameToUseInSort = str_replace('`', '', $nameToUseInSort);
1594 $sortName = str_replace('`', '', $sortTable) . $nameToUseInSort;
1596 return $sortName == str_replace('`', '', $newSortExpressionNoDirection)
1597 || $sortName == str_replace('`', '', $sortExpressionNoDirection[$indexInExpression]);
1601 * Get sort url parameters - sort order and order image
1603 * @see getSingleAndMultiSortUrls()
1605 * @param string $sortDirection the sort direction
1606 * @param string $sortOrder the sorting order
1608 * @return string[] 2 element array - $sort_order, $order_img
1610 private function getSortingUrlParams(string $sortDirection, $sortOrder): array
1612 if (strtoupper(trim($sortDirection)) === self::DESCENDING_SORT_DIR) {
1613 $sortOrder .= self::ASCENDING_SORT_DIR;
1614 $orderImg = ' ' . Generator::getImage(
1615 's_desc',
1616 __('Descending'),
1618 'class' => 'soimg',
1619 'title' => '',
1622 $orderImg .= ' ' . Generator::getImage(
1623 's_asc',
1624 __('Ascending'),
1626 'class' => 'soimg hide',
1627 'title' => '',
1630 } else {
1631 $sortOrder .= self::DESCENDING_SORT_DIR;
1632 $orderImg = ' ' . Generator::getImage(
1633 's_asc',
1634 __('Ascending'),
1636 'class' => 'soimg',
1637 'title' => '',
1640 $orderImg .= ' ' . Generator::getImage(
1641 's_desc',
1642 __('Descending'),
1644 'class' => 'soimg hide',
1645 'title' => '',
1650 return [
1651 $sortOrder,
1652 $orderImg,
1657 * Get sort order link
1659 * @see getTableHeaders()
1661 * @param string $orderImg the sort order image
1662 * @param FieldMetadata $fieldsMeta set of field properties
1663 * @param array<int|string, mixed> $orderUrlParams the url params for sort
1664 * @param array<int|string, mixed> $multiOrderUrlParams the url params for sort
1666 * @return string the sort order link
1668 private function getSortOrderLink(
1669 string $orderImg,
1670 FieldMetadata $fieldsMeta,
1671 array $orderUrlParams,
1672 array $multiOrderUrlParams
1673 ): string {
1674 $urlPath = Url::getFromRoute('/sql');
1675 $innerLinkContent = htmlspecialchars($fieldsMeta->name) . $orderImg
1676 . '<input type="hidden" value="'
1677 . $urlPath
1678 . Url::getCommon($multiOrderUrlParams, str_contains($urlPath, '?') ? '&' : '?', false)
1679 . '">';
1681 return Generator::linkOrButton(
1682 Url::getFromRoute('/sql'),
1683 $orderUrlParams,
1684 $innerLinkContent,
1685 ['class' => 'sortlink']
1689 private function getSortOrderHiddenInputs(
1690 array $multipleUrlParams,
1691 string $nameToUseInSort
1692 ): string {
1693 $sqlQuery = $multipleUrlParams['sql_query'];
1694 $sqlQueryAdd = $sqlQuery;
1695 $sqlQueryRemove = null;
1696 $parser = new Parser($sqlQuery);
1698 $firstStatement = $parser->statements[0] ?? null;
1699 $numberOfClausesFound = null;
1700 if ($firstStatement instanceof SelectStatement) {
1701 $orderClauses = $firstStatement->order ?? [];
1702 foreach ($orderClauses as $key => $order) {
1703 // If this is the column name, then remove it from the order clause
1704 if ($order->expr->column !== $nameToUseInSort) {
1705 continue;
1708 // remove the order clause for this column and from the counted array
1709 unset($firstStatement->order[$key], $orderClauses[$key]);
1712 $numberOfClausesFound = count($orderClauses);
1713 $sqlQueryRemove = $firstStatement->build();
1716 $multipleUrlParams['sql_query'] = $sqlQueryRemove ?? $sqlQuery;
1717 $multipleUrlParams['sql_signature'] = Core::signSqlQuery($multipleUrlParams['sql_query']);
1719 $urlRemoveOrder = Url::getFromRoute('/sql', $multipleUrlParams);
1720 if ($numberOfClausesFound === 0) {
1721 $urlRemoveOrder .= '&discard_remembered_sort=1';
1724 $multipleUrlParams['sql_query'] = $sqlQueryAdd;
1725 $multipleUrlParams['sql_signature'] = Core::signSqlQuery($multipleUrlParams['sql_query']);
1727 $urlAddOrder = Url::getFromRoute('/sql', $multipleUrlParams);
1729 return '<input type="hidden" name="url-remove-order" value="' . $urlRemoveOrder . '">' . "\n"
1730 . '<input type="hidden" name="url-add-order" value="' . $urlAddOrder . '">';
1734 * Check if the column contains numeric data
1736 * @param FieldMetadata $fieldsMeta set of field properties
1738 private function isColumnNumeric(FieldMetadata $fieldsMeta): bool
1740 // This was defined in commit b661cd7c9b31f8bc564d2f9a1b8527e0eb966de8
1741 // For issue https://github.com/phpmyadmin/phpmyadmin/issues/4746
1742 return $fieldsMeta->isType(FieldMetadata::TYPE_REAL)
1743 || $fieldsMeta->isMappedTypeBit
1744 || $fieldsMeta->isType(FieldMetadata::TYPE_INT);
1748 * Prepare column to show at right side - check boxes or empty column
1750 * @see getTableHeaders()
1752 * @param string $fullOrPartialTextLink full/partial link or text button
1753 * @param string $colspan column span of table header
1755 * @return string html content
1757 private function getColumnAtRightSide(
1758 DisplayParts $displayParts,
1759 string $fullOrPartialTextLink,
1760 string $colspan
1762 $rightColumnHtml = '';
1763 $displayParams = $this->properties['display_params'];
1765 // Displays the needed checkboxes at the right
1766 // column of the result table header if possible and required...
1767 if (
1768 ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_RIGHT)
1769 || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH)
1770 && ($displayParts->hasEditLink || $displayParts->deleteLink !== DisplayParts::NO_DELETE)
1771 && $displayParts->hasTextButton
1773 $displayParams['emptyafter'] = $displayParts->hasEditLink
1774 && $displayParts->deleteLink !== DisplayParts::NO_DELETE ? 4 : 1;
1776 $rightColumnHtml .= "\n"
1777 . '<th class="column_action d-print-none"' . $colspan . '>'
1778 . $fullOrPartialTextLink
1779 . '</th>';
1780 } elseif (
1781 ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_LEFT)
1782 || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH)
1783 && (! $displayParts->hasEditLink
1784 && $displayParts->deleteLink === DisplayParts::NO_DELETE)
1785 && (! isset($GLOBALS['is_header_sent']) || ! $GLOBALS['is_header_sent'])
1787 // ... elseif no button, displays empty columns if required
1788 // (unless coming from Browse mode print view)
1790 $displayParams['emptyafter'] = $displayParts->hasEditLink
1791 && $displayParts->deleteLink !== DisplayParts::NO_DELETE ? 4 : 1;
1793 $rightColumnHtml .= "\n" . '<td class="d-print-none"' . $colspan
1794 . '></td>';
1797 $this->properties['display_params'] = $displayParams;
1799 return $rightColumnHtml;
1803 * Prepares the display for a value
1805 * @see getDataCellForGeometryColumns(),
1806 * getDataCellForNonNumericColumns()
1808 * @param string $class class of table cell
1809 * @param bool $conditionField whether to add CSS class condition
1810 * @param string $value value to display
1812 * @return string the td
1814 private function buildValueDisplay($class, $conditionField, $value)
1816 return $this->template->render('display/results/value_display', [
1817 'class' => $class,
1818 'condition_field' => $conditionField,
1819 'value' => $value,
1824 * Prepares the display for a null value
1826 * @see getDataCellForNumericColumns(),
1827 * getDataCellForGeometryColumns(),
1828 * getDataCellForNonNumericColumns()
1830 * @param string $class class of table cell
1831 * @param bool $conditionField whether to add CSS class condition
1832 * @param FieldMetadata $meta the meta-information about this field
1834 * @return string the td
1836 private function buildNullDisplay(string $class, bool $conditionField, FieldMetadata $meta): string
1838 $classes = $this->addClass($class, $conditionField, $meta, '');
1840 return $this->template->render('display/results/null_display', [
1841 'data_decimals' => $meta->decimals,
1842 'data_type' => $meta->getMappedType(),
1843 'classes' => $classes,
1848 * Prepares the display for an empty value
1850 * @see getDataCellForNumericColumns(),
1851 * getDataCellForGeometryColumns(),
1852 * getDataCellForNonNumericColumns()
1854 * @param string $class class of table cell
1855 * @param bool $conditionField whether to add CSS class condition
1856 * @param FieldMetadata $meta the meta-information about this field
1858 * @return string the td
1860 private function buildEmptyDisplay(string $class, bool $conditionField, FieldMetadata $meta): string
1862 $classes = $this->addClass($class, $conditionField, $meta, 'text-nowrap');
1864 return $this->template->render('display/results/empty_display', ['classes' => $classes]);
1868 * Adds the relevant classes.
1870 * @see buildNullDisplay(), getRowData()
1872 * @param string $class class of table cell
1873 * @param bool $conditionField whether to add CSS class condition
1874 * @param FieldMetadata $meta the meta-information about the field
1875 * @param string $nowrap avoid wrapping
1876 * @param bool $isFieldTruncated is field truncated (display ...)
1878 * @return string the list of classes
1880 private function addClass(
1881 string $class,
1882 bool $conditionField,
1883 FieldMetadata $meta,
1884 string $nowrap,
1885 bool $isFieldTruncated = false,
1886 bool $hasTransformationPlugin = false
1887 ): string {
1888 $classes = array_filter([
1889 $class,
1890 $nowrap,
1893 if (isset($meta->internalMediaType)) {
1894 $classes[] = preg_replace('/\//', '_', $meta->internalMediaType);
1897 if ($conditionField) {
1898 $classes[] = 'condition';
1901 if ($isFieldTruncated) {
1902 $classes[] = 'truncated';
1905 $mediaTypeMap = $this->properties['mime_map'];
1906 $orgFullColName = $this->properties['db'] . '.' . $meta->orgtable
1907 . '.' . $meta->orgname;
1908 if ($hasTransformationPlugin || ! empty($mediaTypeMap[$orgFullColName]['input_transformation'])) {
1909 $classes[] = 'transformed';
1912 // Define classes to be added to this data field based on the type of data
1914 if ($meta->isEnum()) {
1915 $classes[] = 'enum';
1918 if ($meta->isSet()) {
1919 $classes[] = 'set';
1922 if ($meta->isMappedTypeBit) {
1923 $classes[] = 'bit';
1926 if ($meta->isBinary()) {
1927 $classes[] = 'hex';
1930 return implode(' ', $classes);
1934 * Prepare the body of the results table
1936 * @see getTable()
1938 * @param ResultInterface $dtResult the link id associated to the query
1939 * which results have to be displayed
1940 * @param array<string, string[]> $map the list of relations
1941 * @param array $analyzedSqlResults analyzed sql results
1942 * @param bool $isLimitedDisplay with limited operations or not
1944 * @return string html content
1946 * @global array $row current row data
1948 private function getTableBody(
1949 ResultInterface $dtResult,
1950 DisplayParts $displayParts,
1951 array $map,
1952 array $analyzedSqlResults,
1953 $isLimitedDisplay = false
1955 // Mostly because of browser transformations, to make the row-data accessible in a plugin.
1957 $GLOBALS['row'] = $GLOBALS['row'] ?? null;
1959 $tableBodyHtml = '';
1961 // query without conditions to shorten URLs when needed, 200 is just
1962 // guess, it should depend on remaining URL length
1963 $urlSqlQuery = $this->getUrlSqlQuery($analyzedSqlResults);
1965 $displayParams = $this->properties['display_params'];
1967 $rowNumber = 0;
1968 $displayParams['edit'] = [];
1969 $displayParams['copy'] = [];
1970 $displayParams['delete'] = [];
1971 $displayParams['data'] = [];
1972 $displayParams['row_delete'] = [];
1973 $this->properties['display_params'] = $displayParams;
1975 $gridEditConfig = 'double-click';
1976 // If we don't have all the columns of a unique key in the result set, do not permit grid editing.
1977 if ($isLimitedDisplay || ! $this->properties['editable'] || $GLOBALS['cfg']['GridEditing'] === 'disabled') {
1978 $gridEditConfig = 'disabled';
1979 } elseif ($GLOBALS['cfg']['GridEditing'] === 'click') {
1980 $gridEditConfig = 'click';
1983 // prepare to get the column order, if available
1984 [$colOrder, $colVisib] = $this->getColumnParams($analyzedSqlResults);
1986 // Correction University of Virginia 19991216 in the while below
1987 // Previous code assumed that all tables have keys, specifically that
1988 // the phpMyAdmin GUI should support row delete/edit only for such
1989 // tables.
1990 // Although always using keys is arguably the prescribed way of
1991 // defining a relational table, it is not required. This will in
1992 // particular be violated by the novice.
1993 // We want to encourage phpMyAdmin usage by such novices. So the code
1994 // below has been changed to conditionally work as before when the
1995 // table being displayed has one or more keys; but to display
1996 // delete/edit options correctly for tables without keys.
1998 $whereClauseMap = $this->properties['whereClauseMap'];
1999 while ($GLOBALS['row'] = $dtResult->fetchRow()) {
2000 // add repeating headers
2001 if (
2002 ($rowNumber !== 0) && ($_SESSION['tmpval']['repeat_cells'] > 0)
2003 && ($rowNumber % $_SESSION['tmpval']['repeat_cells']) === 0
2005 $tableBodyHtml .= $this->getRepeatingHeaders(
2006 $displayParams['emptypre'],
2007 $displayParams['desc'],
2008 $displayParams['emptyafter']
2012 $trClass = [];
2013 if ($GLOBALS['cfg']['BrowsePointerEnable'] != true) {
2014 $trClass[] = 'nopointer';
2017 if ($GLOBALS['cfg']['BrowseMarkerEnable'] != true) {
2018 $trClass[] = 'nomarker';
2021 // pointer code part
2022 $tableBodyHtml .= '<tr' . ($trClass === [] ? '' : ' class="' . implode(' ', $trClass) . '"') . '>';
2024 // 1. Prepares the row
2026 // In print view these variable needs to be initialized
2027 $deleteUrl = null;
2028 $deleteString = null;
2029 $editString = null;
2030 $jsConf = null;
2031 $copyUrl = null;
2032 $copyString = null;
2033 $editUrl = null;
2034 $editCopyUrlParams = [];
2035 $delUrlParams = null;
2037 // 1.2 Defines the URLs for the modify/delete link(s)
2039 if (
2040 $displayParts->hasEditLink
2041 || ($displayParts->deleteLink !== DisplayParts::NO_DELETE)
2043 $expressions = [];
2045 if (
2046 isset($analyzedSqlResults['statement'])
2047 && $analyzedSqlResults['statement'] instanceof SelectStatement
2049 $expressions = $analyzedSqlResults['statement']->expr;
2052 // Results from a "SELECT" statement -> builds the
2053 // WHERE clause to use in links (a unique key if possible)
2055 * @todo $where_clause could be empty, for example a table
2056 * with only one field and it's a BLOB; in this case,
2057 * avoid to display the delete and edit links
2059 [$whereClause, $clauseIsUnique, $conditionArray] = Util::getUniqueCondition(
2060 $this->properties['fields_cnt'],
2061 $this->properties['fields_meta'],
2062 $GLOBALS['row'],
2063 false,
2064 $this->properties['table'],
2065 $expressions
2067 $whereClauseMap[$rowNumber][$this->properties['table']] = $whereClause;
2068 $this->properties['whereClauseMap'] = $whereClauseMap;
2070 // 1.2.1 Modify link(s) - update row case
2071 if ($displayParts->hasEditLink) {
2073 $editUrl,
2074 $copyUrl,
2075 $editString,
2076 $copyString,
2077 $editCopyUrlParams,
2078 ] = $this->getModifiedLinks($whereClause, $clauseIsUnique, $urlSqlQuery);
2081 // 1.2.2 Delete/Kill link(s)
2082 [$deleteUrl, $deleteString, $jsConf, $delUrlParams] = $this->getDeleteAndKillLinks(
2083 $whereClause,
2084 $clauseIsUnique,
2085 $urlSqlQuery,
2086 $displayParts->deleteLink,
2087 (int) $GLOBALS['row'][0]
2090 // 1.3 Displays the links at left if required
2091 if (
2092 ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_LEFT)
2093 || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH)
2095 $tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
2096 'position' => self::POSITION_LEFT,
2097 'has_checkbox' => $deleteUrl && $displayParts->deleteLink !== DisplayParts::KILL_PROCESS,
2098 'edit' => [
2099 'url' => $editUrl,
2100 'params' => $editCopyUrlParams + ['default_action' => 'update'],
2101 'string' => $editString,
2102 'clause_is_unique' => $clauseIsUnique,
2104 'copy' => [
2105 'url' => $copyUrl,
2106 'params' => $editCopyUrlParams + ['default_action' => 'insert'],
2107 'string' => $copyString,
2109 'delete' => ['url' => $deleteUrl, 'params' => $delUrlParams, 'string' => $deleteString],
2110 'row_number' => $rowNumber,
2111 'where_clause' => $whereClause,
2112 'condition' => json_encode($conditionArray),
2113 'is_ajax' => ResponseRenderer::getInstance()->isAjax(),
2114 'js_conf' => $jsConf ?? '',
2115 'grid_edit_config' => $gridEditConfig,
2117 } elseif ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_NONE) {
2118 $tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
2119 'position' => self::POSITION_NONE,
2120 'has_checkbox' => $deleteUrl && $displayParts->deleteLink !== DisplayParts::KILL_PROCESS,
2121 'edit' => [
2122 'url' => $editUrl,
2123 'params' => $editCopyUrlParams + ['default_action' => 'update'],
2124 'string' => $editString,
2125 'clause_is_unique' => $clauseIsUnique,
2127 'copy' => [
2128 'url' => $copyUrl,
2129 'params' => $editCopyUrlParams + ['default_action' => 'insert'],
2130 'string' => $copyString,
2132 'delete' => ['url' => $deleteUrl, 'params' => $delUrlParams, 'string' => $deleteString],
2133 'row_number' => $rowNumber,
2134 'where_clause' => $whereClause,
2135 'condition' => json_encode($conditionArray),
2136 'is_ajax' => ResponseRenderer::getInstance()->isAjax(),
2137 'js_conf' => $jsConf ?? '',
2138 'grid_edit_config' => $gridEditConfig,
2143 // 2. Displays the rows' values
2144 if ($this->properties['mime_map'] === null) {
2145 $this->setMimeMap();
2148 $tableBodyHtml .= $this->getRowValues(
2149 $GLOBALS['row'],
2150 $rowNumber,
2151 $colOrder,
2152 $map,
2153 $gridEditConfig,
2154 $colVisib,
2155 $urlSqlQuery,
2156 $analyzedSqlResults
2159 // 3. Displays the modify/delete links on the right if required
2160 if (
2161 ($displayParts->hasEditLink
2162 || $displayParts->deleteLink !== DisplayParts::NO_DELETE)
2163 && ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_RIGHT
2164 || $GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH)
2166 $tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
2167 'position' => self::POSITION_RIGHT,
2168 'has_checkbox' => $deleteUrl && $displayParts->deleteLink !== DisplayParts::KILL_PROCESS,
2169 'edit' => [
2170 'url' => $editUrl,
2171 'params' => $editCopyUrlParams + ['default_action' => 'update'],
2172 'string' => $editString,
2173 'clause_is_unique' => $clauseIsUnique ?? true,
2175 'copy' => [
2176 'url' => $copyUrl,
2177 'params' => $editCopyUrlParams + ['default_action' => 'insert'],
2178 'string' => $copyString,
2180 'delete' => ['url' => $deleteUrl, 'params' => $delUrlParams, 'string' => $deleteString],
2181 'row_number' => $rowNumber,
2182 'where_clause' => $whereClause ?? '',
2183 'condition' => json_encode($conditionArray ?? []),
2184 'is_ajax' => ResponseRenderer::getInstance()->isAjax(),
2185 'js_conf' => $jsConf ?? '',
2186 'grid_edit_config' => $gridEditConfig,
2190 $tableBodyHtml .= '</tr>';
2191 $tableBodyHtml .= "\n";
2192 $rowNumber++;
2195 return $tableBodyHtml;
2199 * Sets the MIME details of the columns in the results set
2201 private function setMimeMap(): void
2203 $fieldsMeta = $this->properties['fields_meta'];
2204 $mediaTypeMap = [];
2205 $added = [];
2206 $relationParameters = $this->relation->getRelationParameters();
2208 for ($currentColumn = 0; $currentColumn < $this->properties['fields_cnt']; ++$currentColumn) {
2209 $meta = $fieldsMeta[$currentColumn];
2210 $orgFullTableName = $this->properties['db'] . '.' . $meta->orgtable;
2212 if (
2213 $relationParameters->columnCommentsFeature === null
2214 || $relationParameters->browserTransformationFeature === null
2215 || ! $GLOBALS['cfg']['BrowseMIME']
2216 || $_SESSION['tmpval']['hide_transformation']
2217 || ! empty($added[$orgFullTableName])
2219 continue;
2222 $mediaTypeMap = array_merge(
2223 $mediaTypeMap,
2224 $this->transformations->getMime($this->properties['db'], $meta->orgtable, false, true) ?? []
2226 $added[$orgFullTableName] = true;
2229 // special browser transformation for some SHOW statements
2230 if ($this->properties['is_show'] && ! $_SESSION['tmpval']['hide_transformation']) {
2231 preg_match(
2232 '@^SHOW[[:space:]]+(VARIABLES|(FULL[[:space:]]+)?'
2233 . 'PROCESSLIST|STATUS|TABLE|GRANTS|CREATE|LOGS|DATABASES|FIELDS'
2234 . ')@i',
2235 $this->properties['sql_query'],
2236 $which
2239 if (isset($which[1])) {
2240 $str = ' ' . strtoupper($which[1]);
2241 $isShowProcessList = strpos($str, 'PROCESSLIST') > 0;
2242 if ($isShowProcessList) {
2243 $mediaTypeMap['..Info'] = [
2244 'mimetype' => 'Text_Plain',
2245 'transformation' => 'output/Text_Plain_Sql.php',
2249 $isShowCreateTable = preg_match('@CREATE[[:space:]]+TABLE@i', $this->properties['sql_query']);
2250 if ($isShowCreateTable) {
2251 $mediaTypeMap['..Create Table'] = [
2252 'mimetype' => 'Text_Plain',
2253 'transformation' => 'output/Text_Plain_Sql.php',
2259 $this->properties['mime_map'] = $mediaTypeMap;
2263 * Get the values for one data row
2265 * @see getTableBody()
2267 * @param array $row current row data
2268 * @param int $rowNumber the index of current row
2269 * @param array|false $colOrder the column order false when
2270 * a property not found false
2271 * when a property not found
2272 * @param array<string, string[]> $map the list of relations
2273 * @param bool|array|string $colVisib column is visible(false);
2274 * column isn't visible(string
2275 * array)
2276 * @param string $urlSqlQuery the analyzed sql query
2277 * @param array $analyzedSqlResults analyzed sql results
2278 * @psalm-param 'double-click'|'click'|'disabled' $gridEditConfig
2280 * @return string html content
2282 private function getRowValues(
2283 array $row,
2284 $rowNumber,
2285 $colOrder,
2286 array $map,
2287 string $gridEditConfig,
2288 $colVisib,
2289 $urlSqlQuery,
2290 array $analyzedSqlResults
2292 $rowValuesHtml = '';
2294 // Following variable are needed for use in isset/empty or
2295 // use with array indexes/safe use in foreach
2296 $sqlQuery = $this->properties['sql_query'];
2297 $fieldsMeta = $this->properties['fields_meta'];
2298 $highlightColumns = $this->properties['highlight_columns'];
2299 $mediaTypeMap = $this->properties['mime_map'];
2301 $rowInfo = $this->getRowInfoForSpecialLinks($row, $colOrder);
2303 $whereClauseMap = $this->properties['whereClauseMap'];
2305 $columnCount = $this->properties['fields_cnt'];
2307 // Load SpecialSchemaLinks for all rows
2308 $specialSchemaLinks = SpecialSchemaLinks::get();
2309 $relationParameters = $this->relation->getRelationParameters();
2311 for ($currentColumn = 0; $currentColumn < $columnCount; ++$currentColumn) {
2312 // assign $i with appropriate column order
2313 $i = is_array($colOrder) ? $colOrder[$currentColumn] : $currentColumn;
2315 $meta = $fieldsMeta[$i];
2316 $orgFullColName = $this->properties['db'] . '.' . $meta->orgtable . '.' . $meta->orgname;
2318 $notNullClass = $meta->isNotNull() ? 'not_null' : '';
2319 $relationClass = isset($map[$meta->name]) ? 'relation' : '';
2320 $hideClass = is_array($colVisib) && isset($colVisib[$currentColumn]) && ! $colVisib[$currentColumn]
2321 ? 'hide'
2322 : '';
2324 $gridEdit = '';
2325 if ($meta->orgtable != '' && $gridEditConfig !== 'disabled') {
2326 $gridEdit = $gridEditConfig === 'click' ? 'grid_edit click1' : 'grid_edit click2';
2329 // handle datetime-related class, for grid editing
2330 $fieldTypeClass = $this->getClassForDateTimeRelatedFields($meta);
2332 // combine all the classes applicable to this column's value
2333 $class = implode(' ', array_filter([
2334 'data',
2335 $gridEdit,
2336 $notNullClass,
2337 $relationClass,
2338 $hideClass,
2339 $fieldTypeClass,
2340 ]));
2342 // See if this column should get highlight because it's used in the
2343 // where-query.
2344 $conditionField = isset($highlightColumns)
2345 && (isset($highlightColumns[$meta->name])
2346 || isset($highlightColumns[Util::backquote($meta->name)]));
2348 // Wrap MIME-transformations. [MIME]
2349 $transformationPlugin = null;
2350 $transformOptions = [];
2352 if ($relationParameters->browserTransformationFeature !== null && $GLOBALS['cfg']['BrowseMIME']) {
2353 if (
2354 isset($mediaTypeMap[$orgFullColName]['mimetype'])
2355 && ! empty($mediaTypeMap[$orgFullColName]['transformation'])
2357 $file = $mediaTypeMap[$orgFullColName]['transformation'];
2358 $includeFile = 'libraries/classes/Plugins/Transformations/' . $file;
2360 if (@file_exists(ROOT_PATH . $includeFile)) {
2361 $className = $this->transformations->getClassName($includeFile);
2362 if (class_exists($className)) {
2363 $plugin = new $className();
2364 if ($plugin instanceof TransformationsPlugin) {
2365 $transformationPlugin = $plugin;
2366 $transformOptions = $this->transformations->getOptions(
2367 $mediaTypeMap[$orgFullColName]['transformation_options'] ?? ''
2370 $meta->internalMediaType = str_replace(
2371 '_',
2372 '/',
2373 $mediaTypeMap[$orgFullColName]['mimetype']
2381 // Check whether the field needs to display with syntax highlighting
2383 $dbLower = mb_strtolower($this->properties['db']);
2384 $tblLower = mb_strtolower($meta->orgtable);
2385 $nameLower = mb_strtolower($meta->orgname);
2386 if (
2387 ! empty($this->transformationInfo[$dbLower][$tblLower][$nameLower])
2388 && isset($row[$i])
2389 && (trim($row[$i]) != '')
2390 && ! $_SESSION['tmpval']['hide_transformation']
2392 /** @psalm-suppress UnresolvableInclude */
2393 include_once ROOT_PATH . $this->transformationInfo[$dbLower][$tblLower][$nameLower][0];
2394 $plugin = new $this->transformationInfo[$dbLower][$tblLower][$nameLower][1]();
2395 if ($plugin instanceof TransformationsPlugin) {
2396 $transformationPlugin = $plugin;
2397 $transformOptions = $this->transformations->getOptions(
2398 $mediaTypeMap[$orgFullColName]['transformation_options'] ?? ''
2401 $orgTable = mb_strtolower($meta->orgtable);
2402 $orgName = mb_strtolower($meta->orgname);
2404 $meta->internalMediaType = str_replace(
2405 '_',
2406 '/',
2407 $this->transformationInfo[$dbLower][$orgTable][$orgName][2]
2412 // Check for the predefined fields need to show as link in schemas
2413 if (! empty($specialSchemaLinks[$dbLower][$tblLower][$nameLower])) {
2414 $linkingUrl = $this->getSpecialLinkUrl(
2415 $specialSchemaLinks[$dbLower][$tblLower][$nameLower],
2416 $row[$i],
2417 $rowInfo
2419 $transformationPlugin = new Text_Plain_Link();
2421 $transformOptions = [
2422 0 => $linkingUrl,
2423 2 => true,
2426 $meta->internalMediaType = str_replace('_', '/', 'Text/Plain');
2429 $expressions = [];
2431 if (
2432 isset($analyzedSqlResults['statement'])
2433 && $analyzedSqlResults['statement'] instanceof SelectStatement
2435 $expressions = $analyzedSqlResults['statement']->expr;
2439 * The result set can have columns from more than one table,
2440 * this is why we have to check for the unique conditions
2441 * related to this table; however getUniqueCondition() is
2442 * costly and does not need to be called if we already know
2443 * the conditions for the current table.
2445 if (! isset($whereClauseMap[$rowNumber][$meta->orgtable])) {
2446 $uniqueConditions = Util::getUniqueCondition(
2447 $this->properties['fields_cnt'],
2448 $this->properties['fields_meta'],
2449 $row,
2450 false,
2451 $meta->orgtable,
2452 $expressions
2454 $whereClauseMap[$rowNumber][$meta->orgtable] = $uniqueConditions[0];
2457 $urlParams = [
2458 'db' => $this->properties['db'],
2459 'table' => $meta->orgtable,
2460 'where_clause_sign' => Core::signSqlQuery($whereClauseMap[$rowNumber][$meta->orgtable]),
2461 'where_clause' => $whereClauseMap[$rowNumber][$meta->orgtable],
2462 'transform_key' => $meta->orgname,
2465 if ($sqlQuery !== '') {
2466 $urlParams['sql_query'] = $urlSqlQuery;
2469 $transformOptions['wrapper_link'] = Url::getCommon($urlParams);
2470 $transformOptions['wrapper_params'] = $urlParams;
2472 $displayParams = $this->properties['display_params'] ?? [];
2474 // in some situations (issue 11406), numeric returns 1
2475 // even for a string type
2476 // for decimal numeric is returning 1
2477 // have to improve logic
2478 // Nullable text fields and text fields have the blob flag (issue 16896)
2479 $isNumericAndNotBlob = $meta->isNumeric && ! $meta->isBlob;
2480 if (
2481 ($isNumericAndNotBlob && $meta->isNotType(FieldMetadata::TYPE_STRING))
2482 || $meta->isType(FieldMetadata::TYPE_REAL)
2484 // n u m e r i c
2486 $displayParams['data'][$rowNumber][$i] = $this->getDataCellForNumericColumns(
2487 $row[$i] === null ? null : (string) $row[$i],
2488 'text-end ' . $class,
2489 $conditionField,
2490 $meta,
2491 $map,
2492 $analyzedSqlResults,
2493 $transformationPlugin,
2494 $transformOptions
2496 } elseif ($meta->isMappedTypeGeometry) {
2497 // g e o m e t r y
2499 // Remove 'grid_edit' from $class as we do not allow to
2500 // inline-edit geometry data.
2501 $class = str_replace('grid_edit', '', $class);
2503 $displayParams['data'][$rowNumber][$i] = $this->getDataCellForGeometryColumns(
2504 $row[$i] === null ? null : (string) $row[$i],
2505 $class,
2506 $meta,
2507 $map,
2508 $urlParams,
2509 $conditionField,
2510 $transformationPlugin,
2511 $transformOptions,
2512 $analyzedSqlResults
2514 } else {
2515 // n o t n u m e r i c
2517 $displayParams['data'][$rowNumber][$i] = $this->getDataCellForNonNumericColumns(
2518 $row[$i] === null ? null : (string) $row[$i],
2519 $class,
2520 $meta,
2521 $map,
2522 $urlParams,
2523 $conditionField,
2524 $transformationPlugin,
2525 $transformOptions,
2526 $analyzedSqlResults
2530 // output stored cell
2531 $rowValuesHtml .= $displayParams['data'][$rowNumber][$i];
2533 if (isset($displayParams['rowdata'][$i][$rowNumber])) {
2534 $displayParams['rowdata'][$i][$rowNumber] .= $displayParams['data'][$rowNumber][$i];
2535 } else {
2536 $displayParams['rowdata'][$i][$rowNumber] = $displayParams['data'][$rowNumber][$i];
2539 $this->properties['display_params'] = $displayParams;
2542 return $rowValuesHtml;
2546 * Get link for display special schema links
2548 * @param array<string,array<int,array<string,string>>|string> $linkRelations
2549 * @param string $columnValue column value
2550 * @param array $rowInfo information about row
2551 * @phpstan-param array{
2552 * 'link_param': string,
2553 * 'link_dependancy_params'?: array<
2554 * int,
2555 * array{'param_info': string, 'column_name': string}
2556 * >,
2557 * 'default_page': string
2558 * } $linkRelations
2560 * @return string generated link
2562 private function getSpecialLinkUrl(
2563 array $linkRelations,
2564 $columnValue,
2565 array $rowInfo
2567 $linkingUrlParams = [];
2569 $linkingUrlParams[$linkRelations['link_param']] = $columnValue;
2571 $divider = strpos($linkRelations['default_page'], '?') ? '&' : '?';
2572 if (empty($linkRelations['link_dependancy_params'])) {
2573 return $linkRelations['default_page']
2574 . Url::getCommonRaw($linkingUrlParams, $divider);
2577 foreach ($linkRelations['link_dependancy_params'] as $new_param) {
2578 $columnName = mb_strtolower($new_param['column_name']);
2580 // If there is a value for this column name in the rowInfo provided
2581 if (isset($rowInfo[$columnName])) {
2582 $linkingUrlParams[$new_param['param_info']] = $rowInfo[$columnName];
2585 // Special case 1 - when executing routines, according
2586 // to the type of the routine, url param changes
2587 if (empty($rowInfo['routine_type'])) {
2588 continue;
2592 return $linkRelations['default_page']
2593 . Url::getCommonRaw($linkingUrlParams, $divider);
2597 * Prepare row information for display special links
2599 * @param array $row current row data
2600 * @param array|bool $colOrder the column order
2602 * @return array<string, mixed> associative array with column nama -> value
2604 private function getRowInfoForSpecialLinks(array $row, $colOrder): array
2606 $rowInfo = [];
2607 $fieldsMeta = $this->properties['fields_meta'];
2609 for ($n = 0; $n < $this->properties['fields_cnt']; ++$n) {
2610 $m = is_array($colOrder) ? $colOrder[$n] : $n;
2611 $rowInfo[mb_strtolower($fieldsMeta[$m]->orgname)] = $row[$m];
2614 return $rowInfo;
2618 * Get url sql query without conditions to shorten URLs
2620 * @see getTableBody()
2622 * @param array $analyzedSqlResults analyzed sql results
2624 * @return string analyzed sql query
2626 private function getUrlSqlQuery(array $analyzedSqlResults)
2628 if (($analyzedSqlResults['querytype'] !== 'SELECT') || (mb_strlen($this->properties['sql_query']) < 200)) {
2629 return $this->properties['sql_query'];
2632 $query = 'SELECT ' . Query::getClause(
2633 $analyzedSqlResults['statement'],
2634 $analyzedSqlResults['parser']->list,
2635 'SELECT'
2638 $fromClause = Query::getClause($analyzedSqlResults['statement'], $analyzedSqlResults['parser']->list, 'FROM');
2640 if ($fromClause !== '') {
2641 $query .= ' FROM ' . $fromClause;
2644 return $query;
2648 * Get column order and column visibility
2650 * @see getTableBody()
2652 * @param array $analyzedSqlResults analyzed sql results
2654 * @return mixed[] 2 element array - $col_order, $col_visib
2656 private function getColumnParams(array $analyzedSqlResults): array
2658 if ($this->isSelect($analyzedSqlResults)) {
2659 $pmatable = new Table($this->properties['table'], $this->properties['db']);
2660 $colOrder = $pmatable->getUiProp(Table::PROP_COLUMN_ORDER);
2661 /* Validate the value */
2662 if ($colOrder !== false) {
2663 $fieldsCount = $this->properties['fields_cnt'];
2664 foreach ($colOrder as $value) {
2665 if ($value < $fieldsCount) {
2666 continue;
2669 $pmatable->removeUiProp(Table::PROP_COLUMN_ORDER);
2670 $fieldsCount = false;
2674 $colVisib = $pmatable->getUiProp(Table::PROP_COLUMN_VISIB);
2675 } else {
2676 $colOrder = false;
2677 $colVisib = false;
2680 return [
2681 $colOrder,
2682 $colVisib,
2687 * Get HTML for repeating headers
2689 * @see getTableBody()
2691 * @param int $numEmptyColumnsBefore The number of blank columns before this one
2692 * @param string[] $descriptions A list of descriptions
2693 * @param int $numEmptyColumnsAfter The number of blank columns after this one
2695 * @return string html content
2697 private function getRepeatingHeaders(
2698 int $numEmptyColumnsBefore,
2699 array $descriptions,
2700 int $numEmptyColumnsAfter
2701 ): string {
2702 $headerHtml = '<tr>' . "\n";
2704 if ($numEmptyColumnsBefore > 0) {
2705 $headerHtml .= ' <th colspan="'
2706 . $numEmptyColumnsBefore . '">'
2707 . "\n" . ' &nbsp;</th>' . "\n";
2708 } elseif ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_NONE) {
2709 $headerHtml .= ' <th></th>' . "\n";
2712 $headerHtml .= implode($descriptions);
2714 if ($numEmptyColumnsAfter > 0) {
2715 $headerHtml .= ' <th colspan="' . $numEmptyColumnsAfter
2716 . '">'
2717 . "\n" . ' &nbsp;</th>' . "\n";
2720 $headerHtml .= '</tr>' . "\n";
2722 return $headerHtml;
2726 * Get modified links
2728 * @see getTableBody()
2730 * @param string $whereClause the where clause of the sql
2731 * @param bool $clauseIsUnique the unique condition of clause
2732 * @param string $urlSqlQuery the analyzed sql query
2734 * @return array<int,string|array<string, bool|string>>
2736 private function getModifiedLinks(
2737 $whereClause,
2738 $clauseIsUnique,
2739 $urlSqlQuery
2741 $urlParams = [
2742 'db' => $this->properties['db'],
2743 'table' => $this->properties['table'],
2744 'where_clause' => $whereClause,
2745 'clause_is_unique' => $clauseIsUnique,
2746 'sql_query' => $urlSqlQuery,
2747 'goto' => Url::getFromRoute('/sql'),
2750 $editUrl = Url::getFromRoute('/table/change');
2752 $copyUrl = Url::getFromRoute('/table/change');
2754 $editStr = $this->getActionLinkContent(
2755 'b_edit',
2756 __('Edit')
2758 $copyStr = $this->getActionLinkContent(
2759 'b_insrow',
2760 __('Copy')
2763 return [
2764 $editUrl,
2765 $copyUrl,
2766 $editStr,
2767 $copyStr,
2768 $urlParams,
2773 * Get delete and kill links
2775 * @see getTableBody()
2777 * @param string $whereClause the where clause of the sql
2778 * @param bool $clauseIsUnique the unique condition of clause
2779 * @param string $urlSqlQuery the analyzed sql query
2780 * @param int $processId Process ID
2781 * @psalm-param DisplayParts::NO_DELETE|DisplayParts::DELETE_ROW|DisplayParts::KILL_PROCESS $deleteLink
2783 * @return array $del_url, $del_str, $js_conf
2784 * @psalm-return array{?string, ?string, ?string}
2786 private function getDeleteAndKillLinks(
2787 $whereClause,
2788 $clauseIsUnique,
2789 $urlSqlQuery,
2790 int $deleteLink,
2791 int $processId
2793 $goto = $this->properties['goto'];
2795 if ($deleteLink === DisplayParts::DELETE_ROW) { // delete row case
2796 $urlParams = [
2797 'db' => $this->properties['db'],
2798 'table' => $this->properties['table'],
2799 'sql_query' => $urlSqlQuery,
2800 'message_to_show' => __('The row has been deleted.'),
2801 'goto' => $goto ?: Url::getFromRoute('/table/sql'),
2804 $linkGoto = Url::getFromRoute('/sql', $urlParams);
2806 $deleteQuery = 'DELETE FROM '
2807 . Util::backquote($this->properties['table'])
2808 . ' WHERE ' . $whereClause .
2809 ($clauseIsUnique ? '' : ' LIMIT 1');
2811 $urlParams = [
2812 'db' => $this->properties['db'],
2813 'table' => $this->properties['table'],
2814 'sql_query' => $deleteQuery,
2815 'message_to_show' => __('The row has been deleted.'),
2816 'goto' => $linkGoto,
2818 $deleteUrl = Url::getFromRoute('/sql');
2820 $jsConf = 'DELETE FROM ' . $this->properties['table']
2821 . ' WHERE ' . $whereClause
2822 . ($clauseIsUnique ? '' : ' LIMIT 1');
2824 $deleteString = $this->getActionLinkContent('b_drop', __('Delete'));
2825 } elseif ($deleteLink === DisplayParts::KILL_PROCESS) { // kill process case
2826 $urlParams = [
2827 'db' => $this->properties['db'],
2828 'table' => $this->properties['table'],
2829 'sql_query' => $urlSqlQuery,
2830 'goto' => Url::getFromRoute('/'),
2833 $linkGoto = Url::getFromRoute('/sql', $urlParams);
2835 $kill = $this->dbi->getKillQuery($processId);
2837 $urlParams = [
2838 'db' => 'mysql',
2839 'sql_query' => $kill,
2840 'goto' => $linkGoto,
2843 $deleteUrl = Url::getFromRoute('/sql');
2844 $jsConf = $kill;
2845 $deleteString = Generator::getIcon(
2846 'b_drop',
2847 __('Kill')
2849 } else {
2850 $deleteUrl = $deleteString = $jsConf = $urlParams = null;
2853 return [
2854 $deleteUrl,
2855 $deleteString,
2856 $jsConf,
2857 $urlParams,
2862 * Get content inside the table row action links (Edit/Copy/Delete)
2864 * @see getModifiedLinks(), getDeleteAndKillLinks()
2866 * @param string $icon The name of the file to get
2867 * @param string $displayText The text displaying after the image icon
2869 * @return string
2871 private function getActionLinkContent($icon, $displayText)
2873 if (
2874 isset($GLOBALS['cfg']['RowActionType'])
2875 && $GLOBALS['cfg']['RowActionType'] === self::ACTION_LINK_CONTENT_ICONS
2877 return '<span class="text-nowrap">'
2878 . Generator::getImage($icon, $displayText)
2879 . '</span>';
2882 if (
2883 isset($GLOBALS['cfg']['RowActionType'])
2884 && $GLOBALS['cfg']['RowActionType'] === self::ACTION_LINK_CONTENT_TEXT
2886 return '<span class="text-nowrap">' . $displayText . '</span>';
2889 return Generator::getIcon($icon, $displayText);
2893 * Get class for datetime related fields
2895 * @see getTableBody()
2897 * @param FieldMetadata $meta the type of the column field
2899 * @return string the class for the column
2901 private function getClassForDateTimeRelatedFields(FieldMetadata $meta): string
2903 $fieldTypeClass = '';
2905 if ($meta->isMappedTypeTimestamp || $meta->isType(FieldMetadata::TYPE_DATETIME)) {
2906 $fieldTypeClass = 'datetimefield';
2907 } elseif ($meta->isType(FieldMetadata::TYPE_DATE)) {
2908 $fieldTypeClass = 'datefield';
2909 } elseif ($meta->isType(FieldMetadata::TYPE_TIME)) {
2910 $fieldTypeClass = 'timefield';
2911 } elseif ($meta->isType(FieldMetadata::TYPE_STRING)) {
2912 $fieldTypeClass = 'text';
2915 return $fieldTypeClass;
2919 * Prepare data cell for numeric type fields
2921 * @see getTableBody()
2923 * @param string|null $column the column's value
2924 * @param string $class the html class for column
2925 * @param bool $conditionField the column should highlighted or not
2926 * @param FieldMetadata $meta the meta-information about this field
2927 * @param array<string, string[]> $map the list of relations
2928 * @param array $analyzedSqlResults the analyzed query
2929 * @param array $transformOptions the transformation parameters
2931 * @return string the prepared cell, html content
2933 private function getDataCellForNumericColumns(
2934 ?string $column,
2935 string $class,
2936 bool $conditionField,
2937 FieldMetadata $meta,
2938 array $map,
2939 array $analyzedSqlResults,
2940 ?TransformationsPlugin $transformationPlugin,
2941 array $transformOptions
2943 if ($column === null) {
2944 return $this->buildNullDisplay($class, $conditionField, $meta);
2947 if ($column === '') {
2948 return $this->buildEmptyDisplay($class, $conditionField, $meta);
2951 $whereComparison = ' = ' . $column;
2953 return $this->getRowData(
2954 $class,
2955 $conditionField,
2956 $analyzedSqlResults,
2957 $meta,
2958 $map,
2959 $column,
2960 $column,
2961 $transformationPlugin,
2962 'text-nowrap',
2963 $whereComparison,
2964 $transformOptions
2969 * Get data cell for geometry type fields
2971 * @see getTableBody()
2973 * @param string|null $column the relevant column in data row
2974 * @param string $class the html class for column
2975 * @param FieldMetadata $meta the meta-information about this field
2976 * @param array<string, string[]> $map the list of relations
2977 * @param array $urlParams the parameters for generate url
2978 * @param bool $conditionField the column should highlighted or not
2979 * @param array $transformOptions the transformation parameters
2980 * @param array $analyzedSqlResults the analyzed query
2982 * @return string the prepared data cell, html content
2984 private function getDataCellForGeometryColumns(
2985 ?string $column,
2986 string $class,
2987 FieldMetadata $meta,
2988 array $map,
2989 array $urlParams,
2990 bool $conditionField,
2991 ?TransformationsPlugin $transformationPlugin,
2992 $transformOptions,
2993 array $analyzedSqlResults
2995 if ($column === null) {
2996 return $this->buildNullDisplay($class, $conditionField, $meta);
2999 if ($column === '') {
3000 return $this->buildEmptyDisplay($class, $conditionField, $meta);
3003 // Display as [GEOMETRY - (size)]
3004 if ($_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_GEOM) {
3005 $geometryText = $this->handleNonPrintableContents(
3006 'GEOMETRY',
3007 $column,
3008 $transformationPlugin,
3009 $transformOptions,
3010 $meta,
3011 $urlParams
3014 return $this->buildValueDisplay($class, $conditionField, $geometryText);
3017 if ($_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_WKT) {
3018 // Prepare in Well Known Text(WKT) format.
3019 $whereComparison = ' = ' . $column;
3021 // Convert to WKT format
3022 $wktval = Gis::convertToWellKnownText($column);
3024 $isFieldTruncated,
3025 $displayedColumn,
3026 // skip 3rd param
3027 ] = $this->getPartialText($wktval);
3029 return $this->getRowData(
3030 $class,
3031 $conditionField,
3032 $analyzedSqlResults,
3033 $meta,
3034 $map,
3035 $wktval,
3036 $displayedColumn,
3037 $transformationPlugin,
3039 $whereComparison,
3040 $transformOptions,
3041 $isFieldTruncated
3045 // Prepare in Well Known Binary (WKB) format.
3047 if ($_SESSION['tmpval']['display_binary']) {
3048 $whereComparison = ' = ' . $column;
3050 $wkbval = substr(bin2hex($column), 8);
3052 $isFieldTruncated,
3053 $displayedColumn,
3054 // skip 3rd param
3055 ] = $this->getPartialText($wkbval);
3057 return $this->getRowData(
3058 $class,
3059 $conditionField,
3060 $analyzedSqlResults,
3061 $meta,
3062 $map,
3063 $wkbval,
3064 $displayedColumn,
3065 $transformationPlugin,
3067 $whereComparison,
3068 $transformOptions,
3069 $isFieldTruncated
3073 $wkbval = $this->handleNonPrintableContents(
3074 'BINARY',
3075 $column,
3076 $transformationPlugin,
3077 $transformOptions,
3078 $meta,
3079 $urlParams
3082 return $this->buildValueDisplay($class, $conditionField, $wkbval);
3086 * Get data cell for non numeric type fields
3088 * @see getTableBody()
3090 * @param string|null $column the relevant column in data row
3091 * @param string $class the html class for column
3092 * @param FieldMetadata $meta the meta-information about the field
3093 * @param array<string, string[]> $map the list of relations
3094 * @param array $urlParams the parameters for generate url
3095 * @param bool $conditionField the column should highlighted or not
3096 * @param array $transformOptions the transformation parameters
3097 * @param array $analyzedSqlResults the analyzed query
3099 * @return string the prepared data cell, html content
3101 private function getDataCellForNonNumericColumns(
3102 ?string $column,
3103 string $class,
3104 FieldMetadata $meta,
3105 array $map,
3106 array $urlParams,
3107 bool $conditionField,
3108 ?TransformationsPlugin $transformationPlugin,
3109 $transformOptions,
3110 array $analyzedSqlResults
3112 $originalLength = 0;
3114 $isAnalyse = $this->properties['is_analyse'];
3116 $bIsText = $transformationPlugin !== null && ! str_contains($transformationPlugin->getMIMEType(), 'Text');
3118 // disable inline grid editing
3119 // if binary fields are protected
3120 // or transformation plugin is of non text type
3121 // such as image
3122 $isTypeBlob = $meta->isType(FieldMetadata::TYPE_BLOB);
3123 $cfgProtectBinary = $GLOBALS['cfg']['ProtectBinary'];
3124 if (
3125 ($meta->isBinary()
3126 && (
3127 $cfgProtectBinary === 'all'
3128 || ($cfgProtectBinary === 'noblob' && ! $isTypeBlob)
3129 || ($cfgProtectBinary === 'blob' && $isTypeBlob)
3131 ) || $bIsText
3133 $class = str_replace('grid_edit', '', $class);
3136 if ($column === null) {
3137 return $this->buildNullDisplay($class, $conditionField, $meta);
3140 if ($column === '') {
3141 return $this->buildEmptyDisplay($class, $conditionField, $meta);
3144 // Cut all fields to $GLOBALS['cfg']['LimitChars']
3145 // (unless it's a link-type transformation or binary)
3146 $originalDataForWhereClause = $column;
3147 $displayedColumn = $column;
3148 $isFieldTruncated = false;
3149 if (
3150 ! ($transformationPlugin !== null
3151 && str_contains($transformationPlugin->getName(), 'Link'))
3152 && ! $meta->isBinary()
3155 $isFieldTruncated,
3156 $column,
3157 $originalLength,
3158 ] = $this->getPartialText($column);
3161 if ($meta->isMappedTypeBit) {
3162 $displayedColumn = Util::printableBitValue((int) $displayedColumn, (int) $meta->length);
3164 // some results of PROCEDURE ANALYSE() are reported as
3165 // being BINARY but they are quite readable,
3166 // so don't treat them as BINARY
3167 } elseif ($meta->isBinary() && $isAnalyse !== true) {
3168 // we show the BINARY or BLOB message and field's size
3169 // (or maybe use a transformation)
3170 $binaryOrBlob = 'BLOB';
3171 if ($meta->isType(FieldMetadata::TYPE_STRING)) {
3172 $binaryOrBlob = 'BINARY';
3175 $displayedColumn = $this->handleNonPrintableContents(
3176 $binaryOrBlob,
3177 $displayedColumn,
3178 $transformationPlugin,
3179 $transformOptions,
3180 $meta,
3181 $urlParams,
3182 $isFieldTruncated
3184 $class = $this->addClass(
3185 $class,
3186 $conditionField,
3187 $meta,
3189 $isFieldTruncated,
3190 $transformationPlugin !== null
3192 $result = strip_tags($column);
3193 // disable inline grid editing
3194 // if binary or blob data is not shown
3195 if (stripos($result, $binaryOrBlob) !== false) {
3196 $class = str_replace('grid_edit', '', $class);
3199 return $this->buildValueDisplay($class, $conditionField, $displayedColumn);
3202 // transform functions may enable no-wrapping:
3203 $boolNoWrap = $transformationPlugin !== null
3204 && $transformationPlugin->applyTransformationNoWrap($transformOptions);
3206 // do not wrap if date field type or if no-wrapping enabled by transform functions
3207 // otherwise, preserve whitespaces and wrap
3208 $nowrap = $meta->isDateTimeType() || $boolNoWrap ? 'text-nowrap' : 'pre_wrap';
3210 $whereComparison = ' = \''
3211 . $this->dbi->escapeString($originalDataForWhereClause)
3212 . '\'';
3214 return $this->getRowData(
3215 $class,
3216 $conditionField,
3217 $analyzedSqlResults,
3218 $meta,
3219 $map,
3220 $column,
3221 $displayedColumn,
3222 $transformationPlugin,
3223 $nowrap,
3224 $whereComparison,
3225 $transformOptions,
3226 $isFieldTruncated,
3227 (string) $originalLength
3232 * Checks the posted options for viewing query results
3233 * and sets appropriate values in the session.
3235 * @todo make maximum remembered queries configurable
3236 * @todo move/split into SQL class!?
3237 * @todo currently this is called twice unnecessary
3238 * @todo ignore LIMIT and ORDER in query!?
3240 public function setConfigParamsForDisplayTable(): void
3242 $sqlMd5 = md5($this->properties['server'] . $this->properties['db'] . $this->properties['sql_query']);
3243 $query = [];
3244 if (isset($_SESSION['tmpval']['query'][$sqlMd5])) {
3245 $query = $_SESSION['tmpval']['query'][$sqlMd5];
3248 $query['sql'] = $this->properties['sql_query'];
3250 if (empty($query['repeat_cells'])) {
3251 $query['repeat_cells'] = $GLOBALS['cfg']['RepeatCells'];
3254 // The value can also be from _GET as described on issue #16146 when sorting results
3255 $sessionMaxRows = $_GET['session_max_rows'] ?? $_POST['session_max_rows'] ?? '';
3257 if (isset($sessionMaxRows) && is_numeric($sessionMaxRows)) {
3258 $query['max_rows'] = (int) $sessionMaxRows;
3259 unset($_GET['session_max_rows'], $_POST['session_max_rows']);
3260 } elseif ($sessionMaxRows === self::ALL_ROWS) {
3261 $query['max_rows'] = self::ALL_ROWS;
3262 unset($_GET['session_max_rows'], $_POST['session_max_rows']);
3263 } elseif (empty($query['max_rows'])) {
3264 $query['max_rows'] = intval($GLOBALS['cfg']['MaxRows']);
3267 if (isset($_REQUEST['pos']) && is_numeric($_REQUEST['pos'])) {
3268 $query['pos'] = (int) $_REQUEST['pos'];
3269 unset($_REQUEST['pos']);
3270 } elseif (empty($query['pos'])) {
3271 $query['pos'] = 0;
3274 if (
3275 isset($_REQUEST['pftext']) && in_array(
3276 $_REQUEST['pftext'],
3277 [self::DISPLAY_PARTIAL_TEXT, self::DISPLAY_FULL_TEXT]
3280 $query['pftext'] = $_REQUEST['pftext'];
3281 unset($_REQUEST['pftext']);
3282 } elseif (empty($query['pftext'])) {
3283 $query['pftext'] = self::DISPLAY_PARTIAL_TEXT;
3286 if (
3287 isset($_REQUEST['relational_display']) && in_array(
3288 $_REQUEST['relational_display'],
3289 [self::RELATIONAL_KEY, self::RELATIONAL_DISPLAY_COLUMN]
3292 $query['relational_display'] = $_REQUEST['relational_display'];
3293 unset($_REQUEST['relational_display']);
3294 } elseif (empty($query['relational_display'])) {
3295 // The current session value has priority over a
3296 // change via Settings; this change will be apparent
3297 // starting from the next session
3298 $query['relational_display'] = $GLOBALS['cfg']['RelationalDisplay'];
3301 if (
3302 isset($_REQUEST['geoOption']) && in_array(
3303 $_REQUEST['geoOption'],
3304 [self::GEOMETRY_DISP_WKT, self::GEOMETRY_DISP_WKB, self::GEOMETRY_DISP_GEOM]
3307 $query['geoOption'] = $_REQUEST['geoOption'];
3308 unset($_REQUEST['geoOption']);
3309 } elseif (empty($query['geoOption'])) {
3310 $query['geoOption'] = self::GEOMETRY_DISP_GEOM;
3313 if (isset($_REQUEST['display_binary'])) {
3314 $query['display_binary'] = true;
3315 unset($_REQUEST['display_binary']);
3316 } elseif (isset($_REQUEST['display_options_form'])) {
3317 // we know that the checkbox was unchecked
3318 unset($query['display_binary']);
3319 } elseif (! isset($_REQUEST['full_text_button'])) {
3320 // selected by default because some operations like OPTIMIZE TABLE
3321 // and all queries involving functions return "binary" contents,
3322 // according to low-level field flags
3323 $query['display_binary'] = true;
3326 if (isset($_REQUEST['display_blob'])) {
3327 $query['display_blob'] = true;
3328 unset($_REQUEST['display_blob']);
3329 } elseif (isset($_REQUEST['display_options_form'])) {
3330 // we know that the checkbox was unchecked
3331 unset($query['display_blob']);
3334 if (isset($_REQUEST['hide_transformation'])) {
3335 $query['hide_transformation'] = true;
3336 unset($_REQUEST['hide_transformation']);
3337 } elseif (isset($_REQUEST['display_options_form'])) {
3338 // we know that the checkbox was unchecked
3339 unset($query['hide_transformation']);
3342 // move current query to the last position, to be removed last
3343 // so only least executed query will be removed if maximum remembered
3344 // queries limit is reached
3345 unset($_SESSION['tmpval']['query'][$sqlMd5]);
3346 $_SESSION['tmpval']['query'][$sqlMd5] = $query;
3348 // do not exceed a maximum number of queries to remember
3349 if (count($_SESSION['tmpval']['query']) > 10) {
3350 array_shift($_SESSION['tmpval']['query']);
3351 //echo 'deleting one element ...';
3354 // populate query configuration
3355 $_SESSION['tmpval']['pftext'] = $query['pftext'];
3356 $_SESSION['tmpval']['relational_display'] = $query['relational_display'];
3357 $_SESSION['tmpval']['geoOption'] = $query['geoOption'];
3358 $_SESSION['tmpval']['display_binary'] = isset($query['display_binary']);
3359 $_SESSION['tmpval']['display_blob'] = isset($query['display_blob']);
3360 $_SESSION['tmpval']['hide_transformation'] = isset($query['hide_transformation']);
3361 $_SESSION['tmpval']['pos'] = $query['pos'];
3362 $_SESSION['tmpval']['max_rows'] = $query['max_rows'];
3363 $_SESSION['tmpval']['repeat_cells'] = $query['repeat_cells'];
3367 * Prepare a table of results returned by a SQL query.
3369 * @param ResultInterface $dtResult the link id associated to the query
3370 * which results have to be displayed
3371 * @param array $analyzedSqlResults analyzed sql results
3372 * @param bool $isLimitedDisplay With limited operations or not
3374 * @return string Generated HTML content for resulted table
3376 public function getTable(
3377 ResultInterface $dtResult,
3378 DisplayParts $displayParts,
3379 array $analyzedSqlResults,
3380 $isLimitedDisplay = false
3382 // The statement this table is built for.
3383 if (isset($analyzedSqlResults['statement'])) {
3384 /** @var SelectStatement $statement */
3385 $statement = $analyzedSqlResults['statement'];
3386 } else {
3387 $statement = null;
3390 // Following variable are needed for use in isset/empty or
3391 // use with array indexes/safe use in foreach
3392 $fieldsMeta = $this->properties['fields_meta'];
3393 $showTable = $this->properties['showtable'];
3394 $printView = $this->properties['printview'];
3397 * @todo move this to a central place
3398 * @todo for other future table types
3400 $isInnodb = (isset($showTable['Type'])
3401 && $showTable['Type'] === self::TABLE_TYPE_INNO_DB);
3403 if ($isInnodb && Sql::isJustBrowsing($analyzedSqlResults, true)) {
3404 $preCount = '~';
3405 $afterCount = Generator::showHint(
3406 Sanitize::sanitizeMessage(
3407 __('May be approximate. See [doc@faq3-11]FAQ 3.11[/doc].')
3410 } else {
3411 $preCount = '';
3412 $afterCount = '';
3415 // 1. ----- Prepares the work -----
3417 // 1.1 Gets the information about which functionalities should be
3418 // displayed
3421 $displayParts,
3422 $total,
3423 ] = $this->setDisplayPartsAndTotal($displayParts);
3425 // 1.2 Defines offsets for the next and previous pages
3426 $posNext = 0;
3427 $posPrev = 0;
3428 if ($displayParts->hasNavigationBar) {
3429 [$posNext, $posPrev] = $this->getOffsets();
3432 // 1.3 Extract sorting expressions.
3433 // we need $sort_expression and $sort_expression_nodirection
3434 // even if there are many table references
3435 $sortExpression = [];
3436 $sortExpressionNoDirection = [];
3437 $sortDirection = [];
3439 if ($statement !== null && ! empty($statement->order)) {
3440 foreach ($statement->order as $o) {
3441 $sortExpression[] = $o->expr->expr . ' ' . $o->type;
3442 $sortExpressionNoDirection[] = $o->expr->expr;
3443 $sortDirection[] = $o->type;
3445 } else {
3446 $sortExpression[] = '';
3447 $sortExpressionNoDirection[] = '';
3448 $sortDirection[] = '';
3451 $numberOfColumns = count($sortExpressionNoDirection);
3453 // 1.4 Prepares display of first and last value of the sorted column
3454 $sortedColumnMessage = '';
3455 for ($i = 0; $i < $numberOfColumns; $i++) {
3456 $sortedColumnMessage .= $this->getSortedColumnMessage($dtResult, $sortExpressionNoDirection[$i]);
3459 // 2. ----- Prepare to display the top of the page -----
3461 // 2.1 Prepares a messages with position information
3462 $sqlQueryMessage = '';
3463 if ($displayParts->hasNavigationBar) {
3464 $message = $this->setMessageInformation(
3465 $sortedColumnMessage,
3466 $analyzedSqlResults,
3467 $total,
3468 $posNext,
3469 $preCount,
3470 $afterCount
3473 $sqlQueryMessage = Generator::getMessage($message, $this->properties['sql_query'], 'success');
3474 } elseif (($printView === null || $printView != '1') && ! $isLimitedDisplay) {
3475 $sqlQueryMessage = Generator::getMessage(
3476 __('Your SQL query has been executed successfully.'),
3477 $this->properties['sql_query'],
3478 'success'
3482 // 2.3 Prepare the navigation bars
3483 if ($this->properties['table'] === '' && $analyzedSqlResults['querytype'] === 'SELECT') {
3484 // table does not always contain a real table name,
3485 // for example in MySQL 5.0.x, the query SHOW STATUS
3486 // returns STATUS as a table name
3487 $this->properties['table'] = $fieldsMeta[0]->table;
3490 $unsortedSqlQuery = '';
3491 $sortByKeyData = [];
3492 // can the result be sorted?
3493 if ($displayParts->hasSortLink && isset($analyzedSqlResults['statement'])) {
3494 $unsortedSqlQuery = Query::replaceClause(
3495 $analyzedSqlResults['statement'],
3496 $analyzedSqlResults['parser']->list,
3497 'ORDER BY',
3501 // Data is sorted by indexes only if there is only one table.
3502 if ($this->isSelect($analyzedSqlResults)) {
3503 $sortByKeyData = $this->getSortByKeyDropDown(
3504 $sortExpression,
3505 $unsortedSqlQuery
3510 $navigation = [];
3511 if ($displayParts->hasNavigationBar && $statement !== null && empty($statement->limit)) {
3512 $navigation = $this->getTableNavigation($posNext, $posPrev, $isInnodb, $sortByKeyData);
3515 // 2b ----- Get field references from Database -----
3516 // (see the 'relation' configuration variable)
3518 // initialize map
3519 $map = [];
3521 if ($this->properties['table'] !== '') {
3522 // This method set the values for $map array
3523 $map = $this->setParamForLinkForeignKeyRelatedTables($map);
3525 // Coming from 'Distinct values' action of structure page
3526 // We manipulate relations mechanism to show a link to related rows.
3527 if ($this->properties['is_browse_distinct']) {
3528 $map[$fieldsMeta[1]->name] = [
3529 $this->properties['table'],
3530 $fieldsMeta[1]->name,
3532 $this->properties['db'],
3537 // end 2b
3539 // 3. ----- Prepare the results table -----
3540 $headers = $this->getTableHeaders(
3541 $displayParts,
3542 $analyzedSqlResults,
3543 $unsortedSqlQuery,
3544 $sortExpression,
3545 $sortExpressionNoDirection,
3546 $sortDirection,
3547 $isLimitedDisplay
3550 $body = $this->getTableBody($dtResult, $displayParts, $map, $analyzedSqlResults, $isLimitedDisplay);
3552 $this->properties['display_params'] = null;
3554 // 4. ----- Prepares the link for multi-fields edit and delete
3555 $bulkLinks = $this->getBulkLinks($dtResult, $analyzedSqlResults, $displayParts->deleteLink);
3557 // 5. ----- Prepare "Query results operations"
3558 $operations = [];
3559 if (($printView === null || $printView != '1') && ! $isLimitedDisplay) {
3560 $operations = $this->getResultsOperations($displayParts->hasPrintLink, $analyzedSqlResults);
3563 $relationParameters = $this->relation->getRelationParameters();
3565 return $this->template->render('display/results/table', [
3566 'sql_query_message' => $sqlQueryMessage,
3567 'navigation' => $navigation,
3568 'headers' => $headers,
3569 'body' => $body,
3570 'bulk_links' => $bulkLinks,
3571 'operations' => $operations,
3572 'db' => $this->properties['db'],
3573 'table' => $this->properties['table'],
3574 'unique_id' => $this->properties['unique_id'],
3575 'sql_query' => $this->properties['sql_query'],
3576 'goto' => $this->properties['goto'],
3577 'unlim_num_rows' => $this->properties['unlim_num_rows'],
3578 'displaywork' => $relationParameters->displayFeature !== null,
3579 'relwork' => $relationParameters->relationFeature !== null,
3580 'save_cells_at_once' => $GLOBALS['cfg']['SaveCellsAtOnce'],
3581 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'],
3582 'text_dir' => $this->properties['text_dir'],
3583 'is_browse_distinct' => $this->properties['is_browse_distinct'],
3588 * Get offsets for next page and previous page
3590 * @see getTable()
3592 * @return int[] array with two elements - $pos_next, $pos_prev
3594 private function getOffsets()
3596 if ($_SESSION['tmpval']['max_rows'] === self::ALL_ROWS) {
3597 return [0, 0];
3600 return [
3601 $_SESSION['tmpval']['pos'] + $_SESSION['tmpval']['max_rows'],
3602 max(0, $_SESSION['tmpval']['pos'] - $_SESSION['tmpval']['max_rows']),
3607 * Prepare sorted column message
3609 * @see getTable()
3611 * @param ResultInterface $dtResult the link id associated to the
3612 * query which results have to
3613 * be displayed
3614 * @param string $sortExpressionNoDirection sort expression without direction
3616 * @return string|null html content, null if not found sorted column
3618 private function getSortedColumnMessage(
3619 ResultInterface $dtResult,
3620 $sortExpressionNoDirection
3622 $fieldsMeta = $this->properties['fields_meta']; // To use array indexes
3624 if (empty($sortExpressionNoDirection)) {
3625 return null;
3628 if (! str_contains($sortExpressionNoDirection, '.')) {
3629 $sortTable = $this->properties['table'];
3630 $sortColumn = $sortExpressionNoDirection;
3631 } else {
3632 [$sortTable, $sortColumn] = explode('.', $sortExpressionNoDirection);
3635 $sortTable = Util::unQuote($sortTable);
3636 $sortColumn = Util::unQuote($sortColumn);
3638 // find the sorted column index in row result
3639 // (this might be a multi-table query)
3640 $sortedColumnIndex = false;
3642 foreach ($fieldsMeta as $key => $meta) {
3643 if (($meta->table == $sortTable) && ($meta->name == $sortColumn)) {
3644 $sortedColumnIndex = $key;
3645 break;
3649 if ($sortedColumnIndex === false) {
3650 return null;
3653 // fetch first row of the result set
3654 $row = $dtResult->fetchRow();
3656 // check for non printable sorted row data
3657 $meta = $fieldsMeta[$sortedColumnIndex];
3659 $isBlobOrGeometryOrBinary = $meta->isType(FieldMetadata::TYPE_BLOB)
3660 || $meta->isMappedTypeGeometry || $meta->isBinary;
3662 if ($isBlobOrGeometryOrBinary) {
3663 $columnForFirstRow = $this->handleNonPrintableContents(
3664 $meta->getMappedType(),
3665 $row ? $row[$sortedColumnIndex] : '',
3666 null,
3668 $meta
3670 } else {
3671 $columnForFirstRow = $row !== [] ? $row[$sortedColumnIndex] : '';
3674 $columnForFirstRow = mb_strtoupper(
3675 mb_substr(
3676 (string) $columnForFirstRow,
3678 (int) $GLOBALS['cfg']['LimitChars']
3679 ) . '...'
3682 // fetch last row of the result set
3683 $dtResult->seek($this->properties['num_rows'] > 0 ? $this->properties['num_rows'] - 1 : 0);
3684 $row = $dtResult->fetchRow();
3686 // check for non printable sorted row data
3687 $meta = $fieldsMeta[$sortedColumnIndex];
3688 if ($isBlobOrGeometryOrBinary) {
3689 $columnForLastRow = $this->handleNonPrintableContents(
3690 $meta->getMappedType(),
3691 $row ? $row[$sortedColumnIndex] : '',
3692 null,
3694 $meta
3696 } else {
3697 $columnForLastRow = $row !== [] ? $row[$sortedColumnIndex] : '';
3700 $columnForLastRow = mb_strtoupper(
3701 mb_substr(
3702 (string) $columnForLastRow,
3704 (int) $GLOBALS['cfg']['LimitChars']
3705 ) . '...'
3708 // reset to first row for the loop in getTableBody()
3709 $dtResult->seek(0);
3711 // we could also use here $sort_expression_nodirection
3712 return ' [' . htmlspecialchars($sortColumn)
3713 . ': <strong>' . htmlspecialchars($columnForFirstRow) . ' - '
3714 . htmlspecialchars($columnForLastRow) . '</strong>]';
3718 * Set the content that needs to be shown in message
3720 * @see getTable()
3722 * @param string $sortedColumnMessage the message for sorted column
3723 * @param array $analyzedSqlResults the analyzed query
3724 * @param int $total the total number of rows returned by
3725 * the SQL query without any
3726 * programmatically appended LIMIT clause
3727 * @param int $posNext the offset for next page
3728 * @param string $preCount the string renders before row count
3729 * @param string $afterCount the string renders after row count
3731 * @return Message an object of Message
3733 private function setMessageInformation(
3734 string $sortedColumnMessage,
3735 array $analyzedSqlResults,
3736 $total,
3737 $posNext,
3738 string $preCount,
3739 string $afterCount
3741 $unlimNumRows = $this->properties['unlim_num_rows']; // To use in isset()
3743 if (! empty($analyzedSqlResults['statement']->limit)) {
3744 $firstShownRec = $analyzedSqlResults['statement']->limit->offset;
3745 $rowCount = $analyzedSqlResults['statement']->limit->rowCount;
3747 if ($rowCount < $total) {
3748 $lastShownRec = $firstShownRec + $rowCount - 1;
3749 } else {
3750 $lastShownRec = $firstShownRec + $total - 1;
3752 } elseif (($_SESSION['tmpval']['max_rows'] === self::ALL_ROWS) || ($posNext > $total)) {
3753 $firstShownRec = $_SESSION['tmpval']['pos'];
3754 $lastShownRec = $total - 1;
3755 } else {
3756 $firstShownRec = $_SESSION['tmpval']['pos'];
3757 $lastShownRec = $posNext - 1;
3760 $messageViewWarning = false;
3761 $table = new Table($this->properties['table'], $this->properties['db']);
3762 if ($table->isView() && ($total == $GLOBALS['cfg']['MaxExactCountViews'])) {
3763 $message = Message::notice(
3765 'This view has at least this number of rows. Please refer to %sdocumentation%s.'
3769 $message->addParam('[doc@cfg_MaxExactCount]');
3770 $message->addParam('[/doc]');
3771 $messageViewWarning = Generator::showHint($message->getMessage());
3774 $message = Message::success(__('Showing rows %1s - %2s'));
3775 $message->addParam($firstShownRec);
3777 if ($messageViewWarning !== false) {
3778 $message->addParamHtml('... ' . $messageViewWarning);
3779 } else {
3780 $message->addParam($lastShownRec);
3783 $message->addText('(');
3785 if ($messageViewWarning === false) {
3786 if ($unlimNumRows != $total) {
3787 $messageTotal = Message::notice(
3788 $preCount . __('%1$d total, %2$d in query')
3790 $messageTotal->addParam($total);
3791 $messageTotal->addParam($unlimNumRows);
3792 } else {
3793 $messageTotal = Message::notice($preCount . __('%d total'));
3794 $messageTotal->addParam($total);
3797 if ($afterCount !== '') {
3798 $messageTotal->addHtml($afterCount);
3801 $message->addMessage($messageTotal, '');
3803 $message->addText(', ', '');
3806 $messageQueryTime = Message::notice(__('Query took %01.4f seconds.') . ')');
3807 $messageQueryTime->addParam($this->properties['querytime']);
3809 $message->addMessage($messageQueryTime, '');
3810 $message->addHtml($sortedColumnMessage, '');
3812 return $message;
3816 * Set the value of $map array for linking foreign key related tables
3818 * @see getTable()
3820 * @param array<string, string[]> $map the list of relations
3822 * @return array<string, string[]>
3824 private function setParamForLinkForeignKeyRelatedTables(array $map): array
3826 // To be able to later display a link to the related table,
3827 // we verify both types of relations: either those that are
3828 // native foreign keys or those defined in the phpMyAdmin
3829 // configuration storage. If no PMA storage, we won't be able
3830 // to use the "column to display" notion (for example show
3831 // the name related to a numeric id).
3832 $existRel = $this->relation->getForeigners(
3833 $this->properties['db'],
3834 $this->properties['table'],
3836 self::POSITION_BOTH
3839 if ($existRel === []) {
3840 return $map;
3843 foreach ($existRel as $masterField => $rel) {
3844 if ($masterField !== 'foreign_keys_data') {
3845 $displayField = $this->relation->getDisplayField($rel['foreign_db'], $rel['foreign_table']);
3846 $map[$masterField] = [
3847 $rel['foreign_table'],
3848 $rel['foreign_field'],
3849 $displayField,
3850 $rel['foreign_db'],
3852 } else {
3853 foreach ($rel as $oneKey) {
3854 foreach ($oneKey['index_list'] as $index => $oneField) {
3855 $displayField = $this->relation->getDisplayField(
3856 $oneKey['ref_db_name'] ?? $GLOBALS['db'],
3857 $oneKey['ref_table_name']
3860 $map[$oneField] = [
3861 $oneKey['ref_table_name'],
3862 $oneKey['ref_index_list'][$index],
3863 $displayField,
3864 $oneKey['ref_db_name'] ?? $GLOBALS['db'],
3871 return $map;
3875 * Prepare multi field edit/delete links
3877 * @see getTable()
3879 * @param ResultInterface $dtResult the link id associated to the query which
3880 * results have to be displayed
3881 * @param array $analyzedSqlResults analyzed sql results
3882 * @psalm-param DisplayParts::NO_DELETE|DisplayParts::DELETE_ROW|DisplayParts::KILL_PROCESS $deleteLink
3884 * @psalm-return array{has_export_button:bool, clause_is_unique:mixed}|array<empty, empty>
3886 private function getBulkLinks(
3887 ResultInterface $dtResult,
3888 array $analyzedSqlResults,
3889 int $deleteLink
3890 ): array {
3891 if ($deleteLink !== DisplayParts::DELETE_ROW) {
3892 return [];
3895 // fetch last row of the result set
3896 $dtResult->seek($this->properties['num_rows'] > 0 ? $this->properties['num_rows'] - 1 : 0);
3897 $row = $dtResult->fetchRow();
3899 $expressions = [];
3901 if (isset($analyzedSqlResults['statement']) && $analyzedSqlResults['statement'] instanceof SelectStatement) {
3902 $expressions = $analyzedSqlResults['statement']->expr;
3906 * $clause_is_unique is needed by getTable() to generate the proper param
3907 * in the multi-edit and multi-delete form
3909 [, $clauseIsUnique] = Util::getUniqueCondition(
3910 $this->properties['fields_cnt'],
3911 $this->properties['fields_meta'],
3912 $row,
3913 false,
3914 false,
3915 $expressions
3918 // reset to first row for the loop in getTableBody()
3919 $dtResult->seek(0);
3921 return [
3922 'has_export_button' => $analyzedSqlResults['querytype'] === 'SELECT',
3923 'clause_is_unique' => $clauseIsUnique,
3928 * Get operations that are available on results.
3930 * @see getTable()
3932 * @param array $analyzedSqlResults analyzed sql results
3934 * @psalm-return array{
3935 * has_export_link: bool,
3936 * has_geometry: bool,
3937 * has_print_link: bool,
3938 * has_procedure: bool,
3939 * url_params: array{
3940 * db: string,
3941 * table: string,
3942 * printview: "1",
3943 * sql_query: string,
3944 * single_table?: "true",
3945 * raw_query?: "true",
3946 * unlim_num_rows?: int|numeric-string|false
3950 private function getResultsOperations(
3951 bool $hasPrintLink,
3952 array $analyzedSqlResults
3953 ): array {
3954 $urlParams = [
3955 'db' => $this->properties['db'],
3956 'table' => $this->properties['table'],
3957 'printview' => '1',
3958 'sql_query' => $this->properties['sql_query'],
3961 $geometryFound = false;
3963 // Export link
3964 // (the single_table parameter is used in \PhpMyAdmin\Export->getDisplay()
3965 // to hide the SQL and the structure export dialogs)
3966 // If the parser found a PROCEDURE clause
3967 // (most probably PROCEDURE ANALYSE()) it makes no sense to
3968 // display the Export link).
3969 if (
3970 ($analyzedSqlResults['querytype'] === self::QUERY_TYPE_SELECT)
3971 && empty($analyzedSqlResults['procedure'])
3973 if (count($analyzedSqlResults['select_tables']) === 1) {
3974 $urlParams['single_table'] = 'true';
3977 // In case this query doesn't involve any tables,
3978 // implies only raw query is to be exported
3979 if (! $analyzedSqlResults['select_tables']) {
3980 $urlParams['raw_query'] = 'true';
3983 $urlParams['unlim_num_rows'] = $this->properties['unlim_num_rows'];
3986 * At this point we don't know the table name; this can happen
3987 * for example with a query like
3988 * SELECT bike_code FROM (SELECT bike_code FROM bikes) tmp
3989 * As a workaround we set in the table parameter the name of the
3990 * first table of this database, so that /table/export and
3991 * the script it calls do not fail
3993 if ($urlParams['table'] === '' && $urlParams['db'] !== '') {
3994 $urlParams['table'] = (string) $this->dbi->fetchValue('SHOW TABLES');
3997 $fieldsMeta = $this->properties['fields_meta'];
3998 foreach ($fieldsMeta as $meta) {
3999 if ($meta->isMappedTypeGeometry) {
4000 $geometryFound = true;
4001 break;
4006 return [
4007 'has_procedure' => ! empty($analyzedSqlResults['procedure']),
4008 'has_geometry' => $geometryFound,
4009 'has_print_link' => $hasPrintLink,
4010 'has_export_link' => $analyzedSqlResults['querytype'] === self::QUERY_TYPE_SELECT,
4011 'url_params' => $urlParams,
4016 * Verifies what to do with non-printable contents (binary or BLOB)
4017 * in Browse mode.
4019 * @see getDataCellForGeometryColumns(), getDataCellForNonNumericColumns(), getSortedColumnMessage()
4021 * @param string $category BLOB|BINARY|GEOMETRY
4022 * @param string|null $content the binary content
4023 * @param array $transformOptions transformation parameters
4024 * @param FieldMetadata $meta the meta-information about the field
4025 * @param array $urlParams parameters that should go to the download link
4026 * @param bool $isTruncated the result is truncated or not
4028 private function handleNonPrintableContents(
4029 $category,
4030 ?string $content,
4031 ?TransformationsPlugin $transformationPlugin,
4032 array $transformOptions,
4033 FieldMetadata $meta,
4034 array $urlParams = [],
4035 &$isTruncated = false
4036 ): string {
4037 $isTruncated = false;
4038 $result = '[' . $category;
4040 if ($content !== null) {
4041 $size = strlen($content);
4042 $displaySize = Util::formatByteDown($size, 3, 1);
4043 if ($displaySize !== null) {
4044 $result .= ' - ' . $displaySize[0] . ' ' . $displaySize[1];
4046 } else {
4047 $result .= ' - NULL';
4048 $size = 0;
4049 $content = '';
4052 $result .= ']';
4054 // if we want to use a text transformation on a BLOB column
4055 if ($transformationPlugin !== null) {
4056 $posMimeOctetstream = strpos(
4057 $transformationPlugin->getMIMESubtype(),
4058 'Octetstream'
4060 $posMimeText = strpos($transformationPlugin->getMIMEtype(), 'Text');
4061 if ($posMimeOctetstream || $posMimeText !== false) {
4062 // Applying Transformations on hex string of binary data
4063 // seems more appropriate
4064 $result = pack('H*', bin2hex($content));
4068 if ($size <= 0) {
4069 return $result;
4072 if ($transformationPlugin !== null) {
4073 return $transformationPlugin->applyTransformation($result, $transformOptions, $meta);
4076 $result = Core::mimeDefaultFunction($result);
4077 if (
4078 ($_SESSION['tmpval']['display_binary']
4079 && $meta->isType(FieldMetadata::TYPE_STRING))
4080 || ($_SESSION['tmpval']['display_blob']
4081 && $meta->isType(FieldMetadata::TYPE_BLOB))
4083 // in this case, restart from the original $content
4084 if (
4085 mb_check_encoding($content, 'utf-8')
4086 && ! preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', $content)
4088 // show as text if it's valid utf-8
4089 $result = htmlspecialchars($content);
4090 } else {
4091 $result = '0x' . bin2hex($content);
4095 $isTruncated,
4096 $result,
4097 // skip 3rd param
4098 ] = $this->getPartialText($result);
4101 /* Create link to download */
4103 if ($urlParams !== [] && $this->properties['db'] !== '' && $meta->orgtable !== '') {
4104 $urlParams['where_clause_sign'] = Core::signSqlQuery($urlParams['where_clause']);
4105 $result = '<a href="'
4106 . Url::getFromRoute('/table/get-field', $urlParams)
4107 . '" class="disableAjax">'
4108 . $result . '</a>';
4111 return $result;
4115 * Retrieves the associated foreign key info for a data cell
4117 * @param string[] $fieldInfo the relation
4118 * @param string $whereComparison data for the where clause
4120 * @return string formatted data
4122 private function getFromForeign(array $fieldInfo, string $whereComparison): ?string
4124 $dispsql = 'SELECT '
4125 . Util::backquote($fieldInfo[2])
4126 . ' FROM '
4127 . Util::backquote($fieldInfo[3])
4128 . '.'
4129 . Util::backquote($fieldInfo[0])
4130 . ' WHERE '
4131 . Util::backquote($fieldInfo[1])
4132 . $whereComparison;
4134 $dispval = $this->dbi->fetchValue($dispsql);
4135 if ($dispval === false) {
4136 return __('Link not found!');
4139 return $dispval;
4143 * Prepares the displayable content of a data cell in Browse mode,
4144 * taking into account foreign key description field and transformations
4146 * @see getDataCellForNumericColumns(), getDataCellForGeometryColumns(),
4147 * getDataCellForNonNumericColumns(),
4149 * @param string $class css classes for the td element
4150 * @param bool $conditionField whether the column is a part of the where clause
4151 * @param array $analyzedSqlResults the analyzed query
4152 * @param FieldMetadata $meta the meta-information about the field
4153 * @param array<string, string[]> $map the list of relations
4154 * @param string $data data
4155 * @param string $displayedData data that will be displayed (maybe be chunked)
4156 * @param string $nowrap 'nowrap' if the content should not be wrapped
4157 * @param string $whereComparison data for the where clause
4158 * @param array $transformOptions options for transformation
4159 * @param bool $isFieldTruncated whether the field is truncated
4160 * @param string $originalLength of a truncated column, or ''
4162 * @return string formatted data
4164 private function getRowData(
4165 string $class,
4166 bool $conditionField,
4167 array $analyzedSqlResults,
4168 FieldMetadata $meta,
4169 array $map,
4170 $data,
4171 $displayedData,
4172 ?TransformationsPlugin $transformationPlugin,
4173 string $nowrap,
4174 string $whereComparison,
4175 array $transformOptions,
4176 bool $isFieldTruncated = false,
4177 string $originalLength = ''
4179 $relationalDisplay = $_SESSION['tmpval']['relational_display'];
4180 $printView = $this->properties['printview'];
4181 $value = '';
4182 $tableDataCellClass = $this->addClass(
4183 $class,
4184 $conditionField,
4185 $meta,
4186 $nowrap,
4187 $isFieldTruncated,
4188 $transformationPlugin !== null
4191 if (! empty($analyzedSqlResults['statement']->expr)) {
4192 foreach ($analyzedSqlResults['statement']->expr as $expr) {
4193 if (empty($expr->alias) || empty($expr->column)) {
4194 continue;
4197 if (strcasecmp($meta->name, $expr->alias) !== 0) {
4198 continue;
4201 $meta->name = $expr->column;
4205 if (isset($map[$meta->name])) {
4206 /** @var array<int, string> $relation */
4207 $relation = $map[$meta->name];
4208 // Field to display from the foreign table?
4209 $dispval = '';
4210 if ($relation[2] !== '') {
4211 $dispval = $this->getFromForeign($relation, $whereComparison);
4214 if ($printView == '1') {
4215 if ($transformationPlugin !== null) {
4216 $value .= $transformationPlugin->applyTransformation($data, $transformOptions, $meta);
4217 } else {
4218 $value .= Core::mimeDefaultFunction($data);
4221 $value .= ' <code>[-&gt;' . $dispval . ']</code>';
4222 } else {
4223 $sqlQuery = 'SELECT * FROM '
4224 . Util::backquote($relation[3]) . '.'
4225 . Util::backquote($relation[0])
4226 . ' WHERE '
4227 . Util::backquote($relation[1])
4228 . $whereComparison;
4230 $urlParams = [
4231 'db' => $relation[3],
4232 'table' => $relation[0],
4233 'pos' => '0',
4234 'sql_signature' => Core::signSqlQuery($sqlQuery),
4235 'sql_query' => $sqlQuery,
4238 if ($transformationPlugin !== null) {
4239 // always apply a transformation on the real data,
4240 // not on the display field
4241 $displayedData = $transformationPlugin->applyTransformation($data, $transformOptions, $meta);
4242 } elseif ($relationalDisplay === self::RELATIONAL_DISPLAY_COLUMN && $relation[2]) {
4243 // user chose "relational display field" in the
4244 // display options, so show display field in the cell
4245 $displayedData = $dispval === null ? '<em>NULL</em>' : Core::mimeDefaultFunction($dispval);
4246 } else {
4247 // otherwise display data in the cell
4248 $displayedData = Core::mimeDefaultFunction($displayedData);
4251 if ($relationalDisplay === self::RELATIONAL_KEY) {
4252 // user chose "relational key" in the display options, so
4253 // the title contains the display field
4254 $title = htmlspecialchars($dispval ?? '');
4255 } else {
4256 $title = htmlspecialchars($data);
4259 $tagParams = ['title' => $title];
4260 if (str_contains($class, 'grid_edit')) {
4261 $tagParams['class'] = 'ajax';
4264 $value .= Generator::linkOrButton(
4265 Url::getFromRoute('/sql'),
4266 $urlParams,
4267 $displayedData,
4268 $tagParams
4271 } elseif ($transformationPlugin !== null) {
4272 $value .= $transformationPlugin->applyTransformation($data, $transformOptions, $meta);
4273 } else {
4274 $value .= Core::mimeDefaultFunction($data);
4277 return $this->template->render('display/results/row_data', [
4278 'value' => $value,
4279 'td_class' => $tableDataCellClass,
4280 'decimals' => $meta->decimals,
4281 'type' => $meta->getMappedType(),
4282 'original_length' => $originalLength,
4287 * Truncates given string based on LimitChars configuration
4288 * and Session pftext variable
4289 * (string is truncated only if necessary)
4291 * @see handleNonPrintableContents(), getDataCellForGeometryColumns(), getDataCellForNonNumericColumns
4293 * @param string $str string to be truncated
4295 * @return array
4296 * @psalm-return array{bool, string, int}
4298 private function getPartialText($str): array
4300 $originalLength = mb_strlen($str);
4301 if (
4302 $originalLength > $GLOBALS['cfg']['LimitChars']
4303 && $_SESSION['tmpval']['pftext'] === self::DISPLAY_PARTIAL_TEXT
4305 $str = mb_substr($str, 0, (int) $GLOBALS['cfg']['LimitChars']) . '...';
4306 $truncated = true;
4307 } else {
4308 $truncated = false;
4311 return [
4312 $truncated,
4313 $str,
4314 $originalLength,