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