Passed
Push — master ( 37473d...30ff7d )
by Maurício
11:30 queued 14s
created

Results::getUrlSqlQuery()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 9.3798

Importance

Changes 0
Metric Value
cc 6
eloc 11
nc 3
nop 1
dl 0
loc 20
ccs 6
cts 11
cp 0.5455
crap 9.3798
rs 9.2222
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\Display;
6
7
use PhpMyAdmin\Config\SpecialSchemaLinks;
8
use PhpMyAdmin\ConfigStorage\Relation;
9
use PhpMyAdmin\Core;
10
use PhpMyAdmin\DatabaseInterface;
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\DatabaseInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
11
use PhpMyAdmin\Dbal\ResultInterface;
12
use PhpMyAdmin\FieldMetadata;
13
use PhpMyAdmin\Html\Generator;
14
use PhpMyAdmin\Index;
15
use PhpMyAdmin\Message;
16
use PhpMyAdmin\Plugins\Transformations\Output\Text_Octetstream_Sql;
17
use PhpMyAdmin\Plugins\Transformations\Output\Text_Plain_Json;
18
use PhpMyAdmin\Plugins\Transformations\Output\Text_Plain_Sql;
19
use PhpMyAdmin\Plugins\Transformations\Text_Plain_Link;
20
use PhpMyAdmin\Plugins\TransformationsPlugin;
21
use PhpMyAdmin\ResponseRenderer;
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\ResponseRenderer was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
use PhpMyAdmin\Sanitize;
23
use PhpMyAdmin\Sql;
24
use PhpMyAdmin\SqlParser\Parser;
25
use PhpMyAdmin\SqlParser\Statements\SelectStatement;
26
use PhpMyAdmin\SqlParser\Utils\Query;
27
use PhpMyAdmin\StatementInfo;
28
use PhpMyAdmin\Table;
29
use PhpMyAdmin\Template;
30
use PhpMyAdmin\Theme\Theme;
31
use PhpMyAdmin\Transformations;
32
use PhpMyAdmin\Url;
33
use PhpMyAdmin\Util;
34
use PhpMyAdmin\Utils\Gis;
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\Utils\Gis was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
35
36
use function __;
37
use function array_filter;
38
use function array_keys;
39
use function array_merge;
40
use function array_shift;
41
use function bin2hex;
42
use function ceil;
43
use function class_exists;
44
use function count;
45
use function explode;
46
use function file_exists;
47
use function floor;
48
use function htmlspecialchars;
49
use function implode;
50
use function in_array;
51
use function intval;
52
use function is_array;
53
use function is_int;
54
use function is_numeric;
55
use function json_encode;
56
use function max;
57
use function mb_check_encoding;
58
use function mb_strlen;
59
use function mb_strpos;
60
use function mb_strtolower;
61
use function mb_strtoupper;
62
use function mb_substr;
63
use function md5;
64
use function mt_getrandmax;
65
use function pack;
66
use function preg_match;
67
use function preg_replace;
68
use function random_int;
69
use function str_contains;
70
use function str_ends_with;
71
use function str_replace;
72
use function strcasecmp;
73
use function strip_tags;
74
use function stripos;
75
use function strlen;
76
use function strpos;
77
use function strtoupper;
78
use function substr;
79
use function trim;
80
81
/**
82
 * Handle all the functionalities related to displaying results
83
 * of sql queries, stored procedure, browsing sql processes or
84
 * displaying binary log.
85
 */
86
class Results
87
{
88
    public const POSITION_LEFT = 'left';
89
    public const POSITION_RIGHT = 'right';
90
    public const POSITION_BOTH = 'both';
91
    public const POSITION_NONE = 'none';
92
93
    public const DISPLAY_FULL_TEXT = 'F';
94
    public const DISPLAY_PARTIAL_TEXT = 'P';
95
96
    public const HEADER_FLIP_TYPE_AUTO = 'auto';
97
    public const HEADER_FLIP_TYPE_CSS = 'css';
98
    public const HEADER_FLIP_TYPE_FAKE = 'fake';
99
100
    public const RELATIONAL_KEY = 'K';
101
    public const RELATIONAL_DISPLAY_COLUMN = 'D';
102
103
    public const GEOMETRY_DISP_GEOM = 'GEOM';
104
    public const GEOMETRY_DISP_WKT = 'WKT';
105
    public const GEOMETRY_DISP_WKB = 'WKB';
106
107
    public const SMART_SORT_ORDER = 'SMART';
108
    public const ASCENDING_SORT_DIR = 'ASC';
109
    public const DESCENDING_SORT_DIR = 'DESC';
110
111
    public const TABLE_TYPE_INNO_DB = 'InnoDB';
112
    public const ALL_ROWS = 'all';
113
    public const QUERY_TYPE_SELECT = 'SELECT';
114
115
    public const ROUTINE_PROCEDURE = 'procedure';
116
    public const ROUTINE_FUNCTION = 'function';
117
118
    public const ACTION_LINK_CONTENT_ICONS = 'icons';
119
    public const ACTION_LINK_CONTENT_TEXT = 'text';
120
121
    /**
122
     * @psalm-var array{
123
     *   server: int,
124
     *   db: string,
125
     *   table: string,
126
     *   goto: string,
127
     *   sql_query: string,
128
     *   unlim_num_rows: int|numeric-string|false,
129
     *   fields_meta: FieldMetadata[],
130
     *   is_count: bool|null,
131
     *   is_export: bool|null,
132
     *   is_func: bool|null,
133
     *   is_analyse: bool|null,
134
     *   num_rows: int|numeric-string,
135
     *   fields_cnt: int,
136
     *   querytime: float|null,
137
     *   text_dir: string|null,
138
     *   is_maint: bool|null,
139
     *   is_explain: bool|null,
140
     *   is_show: bool|null,
141
     *   is_browse_distinct: bool|null,
142
     *   showtable: array<string, mixed>|null,
143
     *   printview: string|null,
144
     *   highlight_columns: array|null,
145
     *   display_params: array|null,
146
     *   mime_map: array|null,
147
     *   editable: bool|null,
148
     *   unique_id: int,
149
     *   whereClauseMap: array,
150
     * }
151
     */
152
    public $properties = [
153
        /* server id */
154
        'server' => 0,
155
156
        /* Database name */
157
        'db' => '',
158
159
        /* Table name */
160
        'table' => '',
161
162
        /* the URL to go back in case of errors */
163
        'goto' => '',
164
165
        /* the SQL query */
166
        'sql_query' => '',
167
168
        /* the total number of rows returned by the SQL query without any appended "LIMIT" clause programmatically */
169
        'unlim_num_rows' => 0,
170
171
        /* meta information about fields */
172
        'fields_meta' => [],
173
174
        'is_count' => null,
175
176
        'is_export' => null,
177
178
        'is_func' => null,
179
180
        'is_analyse' => null,
181
182
        /* the total number of rows returned by the SQL query */
183
        'num_rows' => 0,
184
185
        /* the total number of fields returned by the SQL query */
186
        'fields_cnt' => 0,
187
188
        /* time taken for execute the SQL query */
189
        'querytime' => null,
190
191
        'text_dir' => null,
192
193
        'is_maint' => null,
194
195
        'is_explain' => null,
196
197
        'is_show' => null,
198
199
        'is_browse_distinct' => null,
200
201
        /* table definitions */
202
        'showtable' => null,
203
204
        'printview' => null,
205
206
        /* column names to highlight */
207
        'highlight_columns' => null,
208
209
        /* display information */
210
        'display_params' => null,
211
212
        /* mime types information of fields */
213
        'mime_map' => null,
214
215
        'editable' => null,
216
217
        /* random unique ID to distinguish result set */
218
        'unique_id' => 0,
219
220
        /* where clauses for each row, each table in the row */
221
        'whereClauseMap' => [],
222
    ];
223
224
    /**
225
     * This variable contains the column transformation information
226
     * for some of the system databases.
227
     * One element of this array represent all relevant columns in all tables in
228
     * one specific database
229
     *
230
     * @var array<string, array<string, array<string, string[]>>>
231
     * @psalm-var array<string, array<string, array<string, array{string, class-string, string}>>> $transformationInfo
232
     */
233
    public array $transformationInfo = [];
234
235
    private Relation $relation;
236
237
    private Transformations $transformations;
238
239
    public Template $template;
240
241
    /**
242
     * @param string $db       the database name
243
     * @param string $table    the table name
244
     * @param int    $server   the server id
245
     * @param string $goto     the URL to go back in case of errors
246
     * @param string $sqlQuery the SQL query
247
     */
248 224
    public function __construct(
249
        private DatabaseInterface $dbi,
250
        string $db,
251
        string $table,
252
        int $server,
253
        string $goto,
254
        string $sqlQuery,
255
    ) {
256 224
        $this->relation = new Relation($this->dbi);
257 224
        $this->transformations = new Transformations();
258 224
        $this->template = new Template();
259
260 224
        $this->setDefaultTransformations();
261
262 224
        $this->properties['db'] = $db;
263 224
        $this->properties['table'] = $table;
264 224
        $this->properties['server'] = $server;
265 224
        $this->properties['goto'] = $goto;
266 224
        $this->properties['sql_query'] = $sqlQuery;
267 224
        $this->properties['unique_id'] = random_int(0, mt_getrandmax());
268
    }
269
270
    /**
271
     * Sets default transformations for some columns
272
     */
273 224
    private function setDefaultTransformations(): void
274
    {
275 224
        $jsonHighlightingData = [
276 224
            'libraries/classes/Plugins/Transformations/Output/Text_Plain_Json.php',
277 224
            Text_Plain_Json::class,
278 224
            'Text_Plain',
279 224
        ];
280 224
        $sqlHighlightingData = [
281 224
            'libraries/classes/Plugins/Transformations/Output/Text_Plain_Sql.php',
282 224
            Text_Plain_Sql::class,
283 224
            'Text_Plain',
284 224
        ];
285 224
        $blobSqlHighlightingData = [
286 224
            'libraries/classes/Plugins/Transformations/Output/Text_Octetstream_Sql.php',
287 224
            Text_Octetstream_Sql::class,
288 224
            'Text_Octetstream',
289 224
        ];
290 224
        $linkData = [
291 224
            'libraries/classes/Plugins/Transformations/Text_Plain_Link.php',
292 224
            Text_Plain_Link::class,
293 224
            'Text_Plain',
294 224
        ];
295 224
        $this->transformationInfo = [
296 224
            'information_schema' => [
297 224
                'events' => ['event_definition' => $sqlHighlightingData],
298 224
                'processlist' => ['info' => $sqlHighlightingData],
299 224
                'routines' => ['routine_definition' => $sqlHighlightingData],
300 224
                'triggers' => ['action_statement' => $sqlHighlightingData],
301 224
                'views' => ['view_definition' => $sqlHighlightingData],
302 224
            ],
303 224
            'mysql' => [
304 224
                'event' => ['body' => $blobSqlHighlightingData, 'body_utf8' => $blobSqlHighlightingData],
305 224
                'general_log' => ['argument' => $sqlHighlightingData],
306 224
                'help_category' => ['url' => $linkData],
307 224
                'help_topic' => ['example' => $sqlHighlightingData, 'url' => $linkData],
308 224
                'proc' => [
309 224
                    'param_list' => $blobSqlHighlightingData,
310 224
                    'returns' => $blobSqlHighlightingData,
311 224
                    'body' => $blobSqlHighlightingData,
312 224
                    'body_utf8' => $blobSqlHighlightingData,
313 224
                ],
314 224
                'slow_log' => ['sql_text' => $sqlHighlightingData],
315 224
            ],
316 224
        ];
317
318 224
        $relationParameters = $this->relation->getRelationParameters();
319 224
        if ($relationParameters->db === null) {
320 224
            return;
321
        }
322
323
        $relDb = [];
324
        if ($relationParameters->sqlHistoryFeature !== null) {
325
            $relDb[$relationParameters->sqlHistoryFeature->history->getName()] = ['sqlquery' => $sqlHighlightingData];
326
        }
327
328
        if ($relationParameters->bookmarkFeature !== null) {
329
            $relDb[$relationParameters->bookmarkFeature->bookmark->getName()] = ['query' => $sqlHighlightingData];
330
        }
331
332
        if ($relationParameters->trackingFeature !== null) {
333
            $relDb[$relationParameters->trackingFeature->tracking->getName()] = [
334
                'schema_sql' => $sqlHighlightingData,
335
                'data_sql' => $sqlHighlightingData,
336
            ];
337
        }
338
339
        if ($relationParameters->favoriteTablesFeature !== null) {
340
            $table = $relationParameters->favoriteTablesFeature->favorite->getName();
341
            $relDb[$table] = ['tables' => $jsonHighlightingData];
342
        }
343
344
        if ($relationParameters->recentlyUsedTablesFeature !== null) {
345
            $table = $relationParameters->recentlyUsedTablesFeature->recent->getName();
346
            $relDb[$table] = ['tables' => $jsonHighlightingData];
347
        }
348
349
        if ($relationParameters->savedQueryByExampleSearchesFeature !== null) {
350
            $table = $relationParameters->savedQueryByExampleSearchesFeature->savedSearches->getName();
351
            $relDb[$table] = ['search_data' => $jsonHighlightingData];
352
        }
353
354
        if ($relationParameters->databaseDesignerSettingsFeature !== null) {
355
            $table = $relationParameters->databaseDesignerSettingsFeature->designerSettings->getName();
356
            $relDb[$table] = ['settings_data' => $jsonHighlightingData];
357
        }
358
359
        if ($relationParameters->uiPreferencesFeature !== null) {
360
            $table = $relationParameters->uiPreferencesFeature->tableUiPrefs->getName();
361
            $relDb[$table] = ['prefs' => $jsonHighlightingData];
362
        }
363
364
        if ($relationParameters->userPreferencesFeature !== null) {
365
            $table = $relationParameters->userPreferencesFeature->userConfig->getName();
366
            $relDb[$table] = ['config_data' => $jsonHighlightingData];
367
        }
368
369
        if ($relationParameters->exportTemplatesFeature !== null) {
370
            $table = $relationParameters->exportTemplatesFeature->exportTemplates->getName();
371
            $relDb[$table] = ['template_data' => $jsonHighlightingData];
372
        }
373
374
        $this->transformationInfo[$relationParameters->db->getName()] = $relDb;
375
    }
376
377
    /**
378
     * Set properties which were not initialized at the constructor
379
     *
380
     * @param int|string                $unlimNumRows     the total number of rows returned by the SQL query without
381
     *                                                    any appended "LIMIT" clause programmatically
382
     * @param FieldMetadata[]           $fieldsMeta       meta information about fields
383
     * @param bool                      $isCount          statement is SELECT COUNT
384
     * @param bool                      $isExport         statement contains INTO OUTFILE
385
     * @param bool                      $isFunction       statement contains a function like SUM()
386
     * @param bool                      $isAnalyse        statement contains PROCEDURE ANALYSE
387
     * @param int|string                $numRows          total no. of rows returned by SQL query
388
     * @param int                       $fieldsCount      total no.of fields returned by SQL query
389
     * @param float                     $queryTime        time taken for execute the SQL query
390
     * @param string                    $textDirection    text direction
391
     * @param bool                      $isMaintenance    statement contains a maintenance command
392
     * @param bool                      $isExplain        statement contains EXPLAIN
393
     * @param bool                      $isShow           statement contains SHOW
394
     * @param array<string, mixed>|null $showTable        table definitions
395
     * @param string|null               $printView        print view was requested
396
     * @param bool                      $editable         whether the results set is editable
397
     * @param bool                      $isBrowseDistinct whether browsing distinct values
398
     * @psalm-param int|numeric-string $unlimNumRows
399
     * @psalm-param int|numeric-string $numRows
400
     */
401 8
    public function setProperties(
402
        int|string $unlimNumRows,
403
        array $fieldsMeta,
404
        bool $isCount,
405
        bool $isExport,
406
        bool $isFunction,
407
        bool $isAnalyse,
408
        int|string $numRows,
409
        int $fieldsCount,
410
        float $queryTime,
411
        string $textDirection,
412
        bool $isMaintenance,
413
        bool $isExplain,
414
        bool $isShow,
415
        array|null $showTable,
416
        string|null $printView,
417
        bool $editable,
418
        bool $isBrowseDistinct,
419
    ): void {
420 8
        $this->properties['unlim_num_rows'] = $unlimNumRows;
421 8
        $this->properties['fields_meta'] = $fieldsMeta;
422 8
        $this->properties['is_count'] = $isCount;
423 8
        $this->properties['is_export'] = $isExport;
424 8
        $this->properties['is_func'] = $isFunction;
425 8
        $this->properties['is_analyse'] = $isAnalyse;
426 8
        $this->properties['num_rows'] = $numRows;
427 8
        $this->properties['fields_cnt'] = $fieldsCount;
428 8
        $this->properties['querytime'] = $queryTime;
429 8
        $this->properties['text_dir'] = $textDirection;
430 8
        $this->properties['is_maint'] = $isMaintenance;
431 8
        $this->properties['is_explain'] = $isExplain;
432 8
        $this->properties['is_show'] = $isShow;
433 8
        $this->properties['showtable'] = $showTable;
434 8
        $this->properties['printview'] = $printView;
435 8
        $this->properties['editable'] = $editable;
436 8
        $this->properties['is_browse_distinct'] = $isBrowseDistinct;
437
    }
438
439
    /**
440
     * Defines the parts to display for a print view
441
     */
442
    private function setDisplayPartsForPrintView(DisplayParts $displayParts): DisplayParts
443
    {
444
        return $displayParts->with([
445
            'hasEditLink' => false,
446
            'deleteLink' => DeleteLinkEnum::NO_DELETE,
447
            'hasSortLink' => false,
448
            'hasNavigationBar' => false,
449
            'hasBookmarkForm' => false,
450
            'hasTextButton' => false,
451
            'hasPrintLink' => false,
452
        ]);
453
    }
454
455
    /**
456
     * Defines the parts to display for a SHOW statement
457
     */
458
    private function setDisplayPartsForShow(DisplayParts $displayParts): DisplayParts
459
    {
460
        preg_match(
461
            '@^SHOW[[:space:]]+(VARIABLES|(FULL[[:space:]]+)?'
462
            . 'PROCESSLIST|STATUS|TABLE|GRANTS|CREATE|LOGS|DATABASES|FIELDS'
463
            . ')@i',
464
            $this->properties['sql_query'],
465
            $which,
466
        );
467
468
        $bIsProcessList = isset($which[1]);
469
        if ($bIsProcessList) {
470
            $str = ' ' . strtoupper($which[1]);
471
            $bIsProcessList = strpos($str, 'PROCESSLIST') > 0;
472
        }
473
474
        return $displayParts->with([
475
            'hasEditLink' => false,
476
            'deleteLink' => $bIsProcessList ? DeleteLinkEnum::KILL_PROCESS : DeleteLinkEnum::NO_DELETE,
477
            'hasSortLink' => false,
478
            'hasNavigationBar' => false,
479
            'hasBookmarkForm' => true,
480
            'hasTextButton' => true,
481
            'hasPrintLink' => true,
482
        ]);
483
    }
484
485
    /**
486
     * Defines the parts to display for statements not related to data
487
     */
488 4
    private function setDisplayPartsForNonData(DisplayParts $displayParts): DisplayParts
489
    {
490
        // Statement is a "SELECT COUNT", a
491
        // "CHECK/ANALYZE/REPAIR/OPTIMIZE/CHECKSUM", an "EXPLAIN" one or
492
        // contains a "PROC ANALYSE" part
493 4
        return $displayParts->with([
494 4
            'hasEditLink' => false,
495 4
            'deleteLink' => DeleteLinkEnum::NO_DELETE,
496 4
            'hasSortLink' => false,
497 4
            'hasNavigationBar' => false,
498 4
            'hasBookmarkForm' => true,
499 4
            'hasTextButton' => (bool) $this->properties['is_maint'],
500 4
            'hasPrintLink' => true,
501 4
        ]);
502
    }
503
504
    /**
505
     * Defines the parts to display for other statements (probably SELECT).
506
     */
507 4
    private function setDisplayPartsForSelect(DisplayParts $displayParts): DisplayParts
508
    {
509 4
        $fieldsMeta = $this->properties['fields_meta'];
510 4
        $previousTable = '';
511 4
        $numberOfColumns = $this->properties['fields_cnt'];
512 4
        $hasEditLink = $displayParts->hasEditLink;
513 4
        $deleteLink = $displayParts->deleteLink;
514 4
        $hasPrintLink = $displayParts->hasPrintLink;
515
516 4
        for ($i = 0; $i < $numberOfColumns; $i++) {
517 4
            $isLink = $hasEditLink || $deleteLink !== DeleteLinkEnum::NO_DELETE || $displayParts->hasSortLink;
518
519
            // Displays edit/delete/sort/insert links?
520
            if (
521 4
                $isLink
522 4
                && $previousTable != ''
523 4
                && $fieldsMeta[$i]->table != ''
524 4
                && $fieldsMeta[$i]->table !== $previousTable
525
            ) {
526
                // don't display links
527
                $hasEditLink = false;
528
                $deleteLink = DeleteLinkEnum::NO_DELETE;
529
                break;
530
            }
531
532
            // Always display print view link
533 4
            $hasPrintLink = true;
534 4
            if ($fieldsMeta[$i]->table == '') {
535 4
                continue;
536
            }
537
538
            $previousTable = $fieldsMeta[$i]->table;
539
        }
540
541 4
        if ($previousTable == '') { // no table for any of the columns
0 ignored issues
show
introduced by
The condition $previousTable == '' is always true.
Loading history...
542
            // don't display links
543 4
            $hasEditLink = false;
544 4
            $deleteLink = DeleteLinkEnum::NO_DELETE;
545
        }
546
547 4
        return $displayParts->with([
548 4
            'hasEditLink' => $hasEditLink,
549 4
            'deleteLink' => $deleteLink,
550 4
            'hasTextButton' => true,
551 4
            'hasPrintLink' => $hasPrintLink,
552 4
        ]);
553
    }
554
555
    /**
556
     * Defines the parts to display for the results of a SQL query
557
     * and the total number of rows
558
     *
559
     * @see     getTable()
560
     *
561
     * @return array<int, DisplayParts|int|mixed> the first element is a {@see DisplayParts} object
562
     *               the second element is the total number of rows returned
563
     *               by the SQL query without any programmatically appended
564
     *               LIMIT clause (just a copy of $unlim_num_rows if it exists,
565
     *               else computed inside this function)
566
     * @psalm-return array{DisplayParts, int}
567
     */
568 8
    private function setDisplayPartsAndTotal(DisplayParts $displayParts): array
569
    {
570 8
        $theTotal = 0;
571
572
        // 1. Following variables are needed for use in isset/empty or
573
        //    use with array indexes or safe use in foreach
574 8
        $db = $this->properties['db'];
575 8
        $table = $this->properties['table'];
576 8
        $unlimNumRows = $this->properties['unlim_num_rows'];
577 8
        $numRows = $this->properties['num_rows'];
578 8
        $printView = $this->properties['printview'];
579
580
        // 2. Updates the display parts
581 8
        if ($printView == '1') {
582
            $displayParts = $this->setDisplayPartsForPrintView($displayParts);
583
        } elseif (
584 8
            $this->properties['is_count'] || $this->properties['is_analyse']
585 8
            || $this->properties['is_maint'] || $this->properties['is_explain']
586
        ) {
587 4
            $displayParts = $this->setDisplayPartsForNonData($displayParts);
588 4
        } elseif ($this->properties['is_show']) {
589
            $displayParts = $this->setDisplayPartsForShow($displayParts);
590
        } else {
591 4
            $displayParts = $this->setDisplayPartsForSelect($displayParts);
592
        }
593
594
        // 3. Gets the total number of rows if it is unknown
595 8
        if ($unlimNumRows > 0) {
596 8
            $theTotal = $unlimNumRows;
597
        } elseif (
598
            $displayParts->hasNavigationBar
599
            || $displayParts->hasSortLink
600
            && $db !== '' && $table !== ''
601
        ) {
602
            $theTotal = $this->dbi->getTable($db, $table)->countRecords();
603
        }
604
605
        // if for COUNT query, number of rows returned more than 1
606
        // (may be being used GROUP BY)
607 8
        if ($this->properties['is_count'] && $numRows > 1) {
608 4
            $displayParts = $displayParts->with(['hasNavigationBar' => true, 'hasSortLink' => true]);
609
        }
610
611
        // 4. If navigation bar or sorting fields names URLs should be
612
        //    displayed but there is only one row, change these settings to
613
        //    false
614 8
        if ($displayParts->hasNavigationBar || $displayParts->hasSortLink) {
615
            // - Do not display sort links if less than 2 rows.
616
            // - For a VIEW we (probably) did not count the number of rows
617
            //   so don't test this number here, it would remove the possibility
618
            //   of sorting VIEW results.
619 8
            $tableObject = new Table($table, $db, $this->dbi);
620 8
            if ($unlimNumRows < 2 && ! $tableObject->isView()) {
621
                $displayParts = $displayParts->with(['hasSortLink' => false]);
622
            }
623
        }
624
625 8
        return [$displayParts, (int) $theTotal];
626
    }
627
628
    /**
629
     * Return true if we are executing a query in the form of
630
     * "SELECT * FROM <a table> ..."
631
     *
632
     * @see getTableHeaders(), getColumnParams()
633
     */
634 12
    private function isSelect(StatementInfo $statementInfo): bool
635
    {
636 12
        return ! ($this->properties['is_count']
637 12
                || $this->properties['is_export']
638 12
                || $this->properties['is_func']
639 12
                || $this->properties['is_analyse'])
640 12
            && $statementInfo->selectFrom
641 12
            && ! empty($statementInfo->statement->from)
642 12
            && (count($statementInfo->statement->from) === 1)
643 12
            && ! empty($statementInfo->statement->from[0]->table);
644
    }
645
646
    /**
647
     * Possibly return a page selector for table navigation
648
     *
649
     * @return array{string, int} ($output, $nbTotalPage)
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{string, int} at position 2 could not be parsed: Expected ':' at position 2, but found 'string'.
Loading history...
650
     */
651 8
    private function getHtmlPageSelector(): array
652
    {
653 8
        $pageNow = (int) floor($_SESSION['tmpval']['pos'] / $_SESSION['tmpval']['max_rows']) + 1;
654
655 8
        $nbTotalPage = (int) ceil((int) $this->properties['unlim_num_rows'] / $_SESSION['tmpval']['max_rows']);
656
657 8
        $output = '';
658 8
        if ($nbTotalPage > 1) {
659
            $urlParams = [
660
                'db' => $this->properties['db'],
661
                'table' => $this->properties['table'],
662
                'sql_query' => $this->properties['sql_query'],
663
                'goto' => $this->properties['goto'],
664
                'is_browse_distinct' => $this->properties['is_browse_distinct'],
665
            ];
666
667
            $output = $this->template->render('display/results/page_selector', [
668
                'url_params' => $urlParams,
669
                'page_selector' => Util::pageselector(
670
                    'pos',
671
                    $_SESSION['tmpval']['max_rows'],
672
                    $pageNow,
673
                    $nbTotalPage,
674
                ),
675
            ]);
676
        }
677
678 8
        return [$output, $nbTotalPage];
679
    }
680
681
    /**
682
     * Get a navigation bar to browse among the results of a SQL query
683
     *
684
     * @see getTable()
685
     *
686
     * @param int     $posNext       the offset for the "next" page
687
     * @param int     $posPrevious   the offset for the "previous" page
688
     * @param bool    $isInnodb      whether its InnoDB or not
689
     * @param mixed[] $sortByKeyData the sort by key dialog
690
     *
691
     * @return mixed[]
692
     */
693 8
    private function getTableNavigation(
694
        int $posNext,
695
        int $posPrevious,
696
        bool $isInnodb,
697
        array $sortByKeyData,
698
    ): array {
699 8
        $isShowingAll = $_SESSION['tmpval']['max_rows'] === self::ALL_ROWS;
700
701 8
        $pageSelector = '';
702 8
        $numberTotalPage = 1;
703 8
        if (! $isShowingAll) {
704 8
            [$pageSelector, $numberTotalPage] = $this->getHtmlPageSelector();
705
        }
706
707 8
        $isLastPage = $this->properties['unlim_num_rows'] !== -1 && $this->properties['unlim_num_rows'] !== false
708 8
            && ($isShowingAll
709 8
                || intval($_SESSION['tmpval']['pos']) + intval($_SESSION['tmpval']['max_rows'])
710 8
                >= $this->properties['unlim_num_rows']
711 8
                || $this->properties['num_rows'] < $_SESSION['tmpval']['max_rows']);
712
713 8
        $onsubmit = ' onsubmit="return '
714 8
            . (intval($_SESSION['tmpval']['pos'])
715 8
            + intval($_SESSION['tmpval']['max_rows'])
716 8
            < $this->properties['unlim_num_rows']
717 8
            && $this->properties['num_rows'] >= intval($_SESSION['tmpval']['max_rows'])
718
                ? 'true'
719 8
                : 'false') . ';"';
720
721 8
        $hasRealEndInput = $isInnodb && $this->properties['unlim_num_rows'] > $GLOBALS['cfg']['MaxExactCount'];
722 8
        $posLast = 0;
723 8
        if (is_numeric($_SESSION['tmpval']['max_rows'])) {
724 8
            $posLast = @((int) ceil(
725 8
                (int) $this->properties['unlim_num_rows'] / $_SESSION['tmpval']['max_rows'],
726 8
            ) - 1) * intval($_SESSION['tmpval']['max_rows']);
727
        }
728
729 8
        $hiddenFields = [
730 8
            'db' => $this->properties['db'],
731 8
            'table' => $this->properties['table'],
732 8
            'server' => $this->properties['server'],
733 8
            'sql_query' => $this->properties['sql_query'],
734 8
            'is_browse_distinct' => $this->properties['is_browse_distinct'],
735 8
            'goto' => $this->properties['goto'],
736 8
        ];
737
738 8
        return [
739 8
            'page_selector' => $pageSelector,
740 8
            'number_total_page' => $numberTotalPage,
741 8
            'has_show_all' => $GLOBALS['cfg']['ShowAll'] || ($this->properties['unlim_num_rows'] <= 500),
742 8
            'hidden_fields' => $hiddenFields,
743 8
            'session_max_rows' => $isShowingAll ? $GLOBALS['cfg']['MaxRows'] : 'all',
744 8
            'is_showing_all' => $isShowingAll,
745 8
            'max_rows' => $_SESSION['tmpval']['max_rows'],
746 8
            'pos' => $_SESSION['tmpval']['pos'],
747 8
            'sort_by_key' => $sortByKeyData,
748 8
            'pos_previous' => $posPrevious,
749 8
            'pos_next' => $posNext,
750 8
            'pos_last' => $posLast,
751 8
            'is_last_page' => $isLastPage,
752 8
            'is_last_page_known' => $this->properties['unlim_num_rows'] !== false,
753 8
            'has_real_end_input' => $hasRealEndInput,
754 8
            'onsubmit' => $onsubmit,
755 8
        ];
756
    }
757
758
    /**
759
     * Get the headers of the results table, for all of the columns
760
     *
761
     * @see getTableHeaders()
762
     *
763
     * @param mixed[]            $sortExpression            sort expression
764
     * @param array<int, string> $sortExpressionNoDirection sort expression
765
     *                                                        without direction
766
     * @param mixed[]            $sortDirection             sort direction
767
     * @param bool               $isLimitedDisplay          with limited operations
768
     *                                                        or not
769
     * @param string             $unsortedSqlQuery          query without the sort part
770
     *
771
     * @return string html content
772
     */
773 8
    private function getTableHeadersForColumns(
774
        bool $hasSortLink,
775
        StatementInfo $statementInfo,
776
        array $sortExpression,
777
        array $sortExpressionNoDirection,
778
        array $sortDirection,
779
        bool $isLimitedDisplay,
780
        string $unsortedSqlQuery,
781
    ): string {
782
        // required to generate sort links that will remember whether the
783
        // "Show all" button has been clicked
784 8
        $sqlMd5 = md5($this->properties['server'] . $this->properties['db'] . $this->properties['sql_query']);
785 8
        $sessionMaxRows = $isLimitedDisplay
786
            ? 0
787 8
            : (int) $_SESSION['tmpval']['query'][$sqlMd5]['max_rows'];
788
789
        // Following variable are needed for use in isset/empty or
790
        // use with array indexes/safe use in the for loop
791 8
        $highlightColumns = $this->properties['highlight_columns'];
792 8
        $fieldsMeta = $this->properties['fields_meta'];
793
794
        // Prepare Display column comments if enabled
795
        // ($GLOBALS['cfg']['ShowBrowseComments']).
796 8
        $commentsMap = $this->getTableCommentsArray($statementInfo);
797
798 8
        [$colOrder, $colVisib] = $this->getColumnParams($statementInfo);
799
800
        // optimize: avoid calling a method on each iteration
801 8
        $numberOfColumns = $this->properties['fields_cnt'];
802
803 8
        $columns = [];
804
805 8
        for ($j = 0; $j < $numberOfColumns; $j++) {
806
            // PHP 7.4 fix for accessing array offset on bool
807 8
            $colVisibCurrent = $colVisib[$j] ?? null;
808
809
            // assign $i with the appropriate column order
810 8
            $i = $colOrder ? $colOrder[$j] : $j;
811
812
            //  See if this column should get highlight because it's used in the
813
            //  where-query.
814 8
            $name = $fieldsMeta[$i]->name;
815 8
            $conditionField = isset($highlightColumns[$name])
816 8
                || isset($highlightColumns[Util::backquote($name)]);
817
818
            // Prepare comment-HTML-wrappers for each row, if defined/enabled.
819 8
            $comments = $this->getCommentForRow($commentsMap, $fieldsMeta[$i]);
820 8
            $displayParams = $this->properties['display_params'] ?? [];
821
822 8
            if ($hasSortLink && ! $isLimitedDisplay) {
823 8
                $sortedHeaderData = $this->getOrderLinkAndSortedHeaderHtml(
824 8
                    $fieldsMeta[$i],
825 8
                    $sortExpression,
826 8
                    $sortExpressionNoDirection,
827 8
                    $unsortedSqlQuery,
828 8
                    $sessionMaxRows,
829 8
                    $comments,
830 8
                    $sortDirection,
831 8
                    $colVisib,
832 8
                    $colVisibCurrent,
833 8
                );
834
835 8
                $orderLink = $sortedHeaderData['order_link'];
836 8
                $columns[] = $sortedHeaderData;
837
838 8
                $displayParams['desc'][] = '    <th '
839 8
                    . 'class="draggable'
840 8
                    . ($conditionField ? ' condition' : '')
841 8
                    . '" data-column="' . htmlspecialchars($fieldsMeta[$i]->name)
842 8
                    . '">' . "\n" . $orderLink . $comments . '    </th>' . "\n";
843
            } else {
844
                // Results can't be sorted
845
                // Prepare columns to draggable effect for non sortable columns
846
                $columns[] = [
847
                    'column_name' => $fieldsMeta[$i]->name,
848
                    'comments' => $comments,
849
                    'is_column_hidden' => $colVisib && ! $colVisibCurrent,
850
                    'is_column_numeric' => $this->isColumnNumeric($fieldsMeta[$i]),
851
                    'has_condition' => $conditionField,
852
                ];
853
854
                $displayParams['desc'][] = '    <th '
855
                    . 'class="draggable'
856
                    . ($conditionField ? ' condition"' : '')
857
                    . '" data-column="' . htmlspecialchars($fieldsMeta[$i]->name)
858
                    . '">        '
859
                    . htmlspecialchars($fieldsMeta[$i]->name)
860
                    . $comments . '    </th>';
861
            }
862
863 8
            $this->properties['display_params'] = $displayParams;
864
        }
865
866 8
        return $this->template->render('display/results/table_headers_for_columns', [
867 8
            'is_sortable' => $hasSortLink && ! $isLimitedDisplay,
868 8
            'columns' => $columns,
869 8
        ]);
870
    }
871
872
    /**
873
     * Get the headers of the results table
874
     *
875
     * @see getTable()
876
     *
877
     * @param string             $unsortedSqlQuery          the unsorted sql query
878
     * @param mixed[]            $sortExpression            sort expression
879
     * @param array<int, string> $sortExpressionNoDirection sort expression without direction
880
     * @param mixed[]            $sortDirection             sort direction
881
     * @param bool               $isLimitedDisplay          with limited operations or not
882
     *
883
     * @psalm-return array{
884
     *   column_order: array,
885
     *   options: array,
886
     *   has_bulk_actions_form: bool,
887
     *   button: string,
888
     *   table_headers_for_columns: string,
889
     *   column_at_right_side: string,
890
     * }
891
     */
892 8
    private function getTableHeaders(
893
        DisplayParts $displayParts,
894
        StatementInfo $statementInfo,
895
        string $unsortedSqlQuery,
896
        array $sortExpression = [],
897
        array $sortExpressionNoDirection = [],
898
        array $sortDirection = [],
899
        bool $isLimitedDisplay = false,
900
    ): array {
901
        // Needed for use in isset/empty or
902
        // use with array indexes/safe use in foreach
903 8
        $printView = $this->properties['printview'];
904 8
        $displayParams = $this->properties['display_params'];
905
906
        // Output data needed for column reordering and show/hide column
907 8
        $columnOrder = $this->getDataForResettingColumnOrder($statementInfo);
908
909 8
        $displayParams['emptypre'] = 0;
910 8
        $displayParams['emptyafter'] = 0;
911 8
        $displayParams['textbtn'] = '';
912 8
        $fullOrPartialTextLink = '';
913
914 8
        $this->properties['display_params'] = $displayParams;
915
916
        // Display options (if we are not in print view)
917 8
        $optionsBlock = [];
918 8
        if (! (isset($printView) && ($printView == '1')) && ! $isLimitedDisplay) {
919 8
            $optionsBlock = $this->getOptionsBlock();
920
921
            // prepare full/partial text button or link
922 8
            $fullOrPartialTextLink = $this->getFullOrPartialTextButtonOrLink();
923
        }
924
925
        // 1. Set $colspan and generate html with full/partial
926
        // text button or link
927 8
        $colspan = $displayParts->hasEditLink
928 8
            && $displayParts->deleteLink !== DeleteLinkEnum::NO_DELETE ? ' colspan="4"' : '';
929 8
        $buttonHtml = $this->getFieldVisibilityParams($displayParts, $fullOrPartialTextLink, $colspan);
930
931
        // 2. Displays the fields' name
932
        // 2.0 If sorting links should be used, checks if the query is a "JOIN"
933
        //     statement (see 2.1.3)
934
935
        // See if we have to highlight any header fields of a WHERE query.
936
        // Uses SQL-Parser results.
937 8
        $this->setHighlightedColumnGlobalField($statementInfo);
938
939
        // Get the headers for all of the columns
940 8
        $tableHeadersForColumns = $this->getTableHeadersForColumns(
941 8
            $displayParts->hasSortLink,
942 8
            $statementInfo,
943 8
            $sortExpression,
944 8
            $sortExpressionNoDirection,
945 8
            $sortDirection,
946 8
            $isLimitedDisplay,
947 8
            $unsortedSqlQuery,
948 8
        );
949
950
        // Display column at rightside - checkboxes or empty column
951 8
        $columnAtRightSide = '';
952 8
        if (! $printView) {
953 8
            $columnAtRightSide = $this->getColumnAtRightSide($displayParts, $fullOrPartialTextLink, $colspan);
954
        }
955
956 8
        return [
957 8
            'column_order' => $columnOrder,
958 8
            'options' => $optionsBlock,
959 8
            'has_bulk_actions_form' => $displayParts->deleteLink === DeleteLinkEnum::DELETE_ROW
960 8
                || $displayParts->deleteLink === DeleteLinkEnum::KILL_PROCESS,
961 8
            'button' => $buttonHtml,
962 8
            'table_headers_for_columns' => $tableHeadersForColumns,
963 8
            'column_at_right_side' => $columnAtRightSide,
964 8
        ];
965
    }
966
967
    /**
968
     * Prepare sort by key dropdown - html code segment
969
     *
970
     * @see getTableHeaders()
971
     *
972
     * @param mixed[]|null $sortExpression   the sort expression
973
     * @param string       $unsortedSqlQuery the unsorted sql query
974
     *
975
     * @return mixed[][]
976
     * @psalm-return array{hidden_fields?:array, options?:array}
977
     */
978 4
    private function getSortByKeyDropDown(
979
        array|null $sortExpression,
980
        string $unsortedSqlQuery,
981
    ): array {
982
        // grab indexes data:
983 4
        $indexes = Index::getFromTable($this->dbi, $this->properties['table'], $this->properties['db']);
984
985
        // do we have any index?
986 4
        if ($indexes === []) {
987
            return [];
988
        }
989
990 4
        $hiddenFields = [
991 4
            'db' => $this->properties['db'],
992 4
            'table' => $this->properties['table'],
993 4
            'server' => $this->properties['server'],
994 4
            'sort_by_key' => '1',
995 4
        ];
996
997
        // Keep the number of rows (25, 50, 100, ...) when changing sort key value
998 4
        if (isset($_SESSION['tmpval']) && isset($_SESSION['tmpval']['max_rows'])) {
999 4
            $hiddenFields['session_max_rows'] = $_SESSION['tmpval']['max_rows'];
1000
        }
1001
1002 4
        $isIndexUsed = false;
1003 4
        $localOrder = is_array($sortExpression) ? implode(', ', $sortExpression) : '';
0 ignored issues
show
introduced by
The condition is_array($sortExpression) is always true.
Loading history...
1004
1005 4
        $options = [];
1006 4
        foreach ($indexes as $index) {
1007 4
            $ascSort = '`'
1008 4
                . implode('` ASC, `', array_keys($index->getColumns()))
1009 4
                . '` ASC';
1010
1011 4
            $descSort = '`'
1012 4
                . implode('` DESC, `', array_keys($index->getColumns()))
1013 4
                . '` DESC';
1014
1015 4
            $isIndexUsed = $isIndexUsed
1016 4
                || $localOrder === $ascSort
1017 4
                || $localOrder === $descSort;
1018
1019 4
            $unsortedSqlQueryFirstPart = $unsortedSqlQuery;
1020 4
            $unsortedSqlQuerySecondPart = '';
1021
            if (
1022 4
                preg_match(
1023 4
                    '@(.*)([[:space:]](LIMIT (.*)|PROCEDURE (.*)|FOR UPDATE|LOCK IN SHARE MODE))@is',
1024 4
                    $unsortedSqlQuery,
1025 4
                    $myReg,
1026 4
                )
1027
            ) {
1028
                $unsortedSqlQueryFirstPart = $myReg[1];
1029
                $unsortedSqlQuerySecondPart = $myReg[2];
1030
            }
1031
1032 4
            $options[] = [
1033 4
                'value' => $unsortedSqlQueryFirstPart . ' ORDER BY '
1034 4
                    . $ascSort . $unsortedSqlQuerySecondPart,
1035 4
                'content' => $index->getName() . ' (ASC)',
1036 4
                'is_selected' => $localOrder === $ascSort,
1037 4
            ];
1038 4
            $options[] = [
1039 4
                'value' => $unsortedSqlQueryFirstPart . ' ORDER BY '
1040 4
                    . $descSort . $unsortedSqlQuerySecondPart,
1041 4
                'content' => $index->getName() . ' (DESC)',
1042 4
                'is_selected' => $localOrder === $descSort,
1043 4
            ];
1044
        }
1045
1046 4
        $options[] = ['value' => $unsortedSqlQuery, 'content' => __('None'), 'is_selected' => ! $isIndexUsed];
1047
1048 4
        return ['hidden_fields' => $hiddenFields, 'options' => $options];
1049
    }
1050
1051
    /**
1052
     * Set column span, row span and prepare html with full/partial
1053
     * text button or link
1054
     *
1055
     * @see getTableHeaders()
1056
     *
1057
     * @param string $fullOrPartialTextLink full/partial link or text button
1058
     * @param string $colspan               column span of table header
1059
     *
1060
     * @return string html with full/partial text button or link
1061
     */
1062 8
    private function getFieldVisibilityParams(
1063
        DisplayParts $displayParts,
1064
        string $fullOrPartialTextLink,
1065
        string $colspan,
1066
    ): string {
1067 8
        $displayParams = $this->properties['display_params'];
1068
1069
        // 1. Displays the full/partial text button (part 1)...
1070 8
        $buttonHtml = '<thead><tr>' . "\n";
1071
1072 8
        $emptyPreCondition = $displayParts->hasEditLink && $displayParts->deleteLink !== DeleteLinkEnum::NO_DELETE;
1073
1074 8
        $leftOrBoth = $GLOBALS['cfg']['RowActionLinks'] === self::POSITION_LEFT
1075 8
                   || $GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH;
1076
1077
        //     ... before the result table
1078
        if (
1079 8
            ! $displayParts->hasEditLink
1080 8
            && $displayParts->deleteLink === DeleteLinkEnum::NO_DELETE
1081 8
            && $displayParts->hasTextButton
1082
        ) {
1083 4
            $displayParams['emptypre'] = 0;
1084 4
        } elseif ($leftOrBoth && $displayParts->hasTextButton) {
1085
            //     ... at the left column of the result table header if possible
1086
            //     and required
1087
1088
            $displayParams['emptypre'] = $emptyPreCondition ? 4 : 0;
1089
1090
            $buttonHtml .= '<th class="column_action position-sticky d-print-none"' . $colspan
1091
                . '>' . $fullOrPartialTextLink . '</th>';
1092
        } elseif (
1093 4
            $leftOrBoth
1094 4
            && ($displayParts->hasEditLink || $displayParts->deleteLink !== DeleteLinkEnum::NO_DELETE)
1095
        ) {
1096
            //     ... elseif no button, displays empty(ies) col(s) if required
1097
1098
            $displayParams['emptypre'] = $emptyPreCondition ? 4 : 0;
1099
1100
            $buttonHtml .= '<td' . $colspan . '></td>';
1101 4
        } elseif ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_NONE) {
1102
            // ... elseif display an empty column if the actions links are
1103
            //  disabled to match the rest of the table
1104
            $buttonHtml .= '<th class="column_action position-sticky"></th>';
1105
        }
1106
1107 8
        $this->properties['display_params'] = $displayParams;
1108
1109 8
        return $buttonHtml;
1110
    }
1111
1112
    /**
1113
     * Get table comments as array
1114
     *
1115
     * @see getTableHeaders()
1116
     *
1117
     * @return mixed[] table comments
1118
     */
1119 8
    private function getTableCommentsArray(StatementInfo $statementInfo): array
1120
    {
1121 8
        if (! $GLOBALS['cfg']['ShowBrowseComments'] || empty($statementInfo->statement->from)) {
1122
            return [];
1123
        }
1124
1125 8
        $ret = [];
1126 8
        foreach ($statementInfo->statement->from as $field) {
1127 8
            if (empty($field->table)) {
1128
                continue;
1129
            }
1130
1131 8
            $ret[$field->table] = $this->relation->getComments(
1132 8
                empty($field->database) ? $this->properties['db'] : $field->database,
1133 8
                $field->table,
1134 8
            );
1135
        }
1136
1137 8
        return $ret;
1138
    }
1139
1140
    /**
1141
     * Set global array for store highlighted header fields
1142
     *
1143
     * @see getTableHeaders()
1144
     */
1145 12
    private function setHighlightedColumnGlobalField(StatementInfo $statementInfo): void
1146
    {
1147 12
        $highlightColumns = [];
1148
1149 12
        if (! empty($statementInfo->statement->where)) {
1150 4
            foreach ($statementInfo->statement->where as $expr) {
1151 4
                foreach ($expr->identifiers as $identifier) {
1152 4
                    $highlightColumns[$identifier] = 'true';
1153
                }
1154
            }
1155
        }
1156
1157 12
        $this->properties['highlight_columns'] = $highlightColumns;
1158
    }
1159
1160
    /**
1161
     * Prepare data for column restoring and show/hide
1162
     *
1163
     * @see getTableHeaders()
1164
     *
1165
     * @return mixed[]
1166
     */
1167 8
    private function getDataForResettingColumnOrder(StatementInfo $statementInfo): array
1168
    {
1169 8
        if (! $this->isSelect($statementInfo)) {
1170 4
            return [];
1171
        }
1172
1173 4
        [$columnOrder, $columnVisibility] = $this->getColumnParams($statementInfo);
1174
1175 4
        $tableCreateTime = '';
1176 4
        $table = new Table($this->properties['table'], $this->properties['db'], $this->dbi);
1177 4
        if (! $table->isView()) {
1178 4
            $tableCreateTime = $this->dbi->getTable(
1179 4
                $this->properties['db'],
1180 4
                $this->properties['table'],
1181 4
            )->getStatusInfo('Create_time');
1182
        }
1183
1184 4
        return [
1185 4
            'order' => $columnOrder,
1186 4
            'visibility' => $columnVisibility,
1187 4
            'is_view' => $table->isView(),
1188 4
            'table_create_time' => $tableCreateTime,
1189 4
        ];
1190
    }
1191
1192
    /**
1193
     * Prepare option fields block
1194
     *
1195
     * @see getTableHeaders()
1196
     *
1197
     * @return mixed[]
1198
     */
1199 8
    private function getOptionsBlock(): array
1200
    {
1201
        if (
1202 8
            isset($_SESSION['tmpval']['possible_as_geometry'])
1203 8
            && $_SESSION['tmpval']['possible_as_geometry'] == false
1204 8
            && $_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_GEOM
1205
        ) {
1206
            $_SESSION['tmpval']['geoOption'] = self::GEOMETRY_DISP_WKT;
1207
        }
1208
1209 8
        return [
1210 8
            'geo_option' => $_SESSION['tmpval']['geoOption'],
1211 8
            'hide_transformation' => $_SESSION['tmpval']['hide_transformation'],
1212 8
            'display_blob' => $_SESSION['tmpval']['display_blob'],
1213 8
            'display_binary' => $_SESSION['tmpval']['display_binary'],
1214 8
            'relational_display' => $_SESSION['tmpval']['relational_display'],
1215 8
            'possible_as_geometry' => $_SESSION['tmpval']['possible_as_geometry'],
1216 8
            'pftext' => $_SESSION['tmpval']['pftext'],
1217 8
        ];
1218
    }
1219
1220
    /**
1221
     * Get full/partial text button or link
1222
     *
1223
     * @see getTableHeaders()
1224
     *
1225
     * @return string html content
1226
     */
1227 8
    private function getFullOrPartialTextButtonOrLink(): string
1228
    {
1229 8
        $GLOBALS['theme'] ??= null;
1230
1231 8
        $urlParamsFullText = [
1232 8
            'db' => $this->properties['db'],
1233 8
            'table' => $this->properties['table'],
1234 8
            'sql_query' => $this->properties['sql_query'],
1235 8
            'goto' => $this->properties['goto'],
1236 8
            'full_text_button' => 1,
1237 8
        ];
1238
1239 8
        if ($_SESSION['tmpval']['pftext'] === self::DISPLAY_FULL_TEXT) {
1240
            // currently in fulltext mode so show the opposite link
1241
            $tmpImageFile = 's_partialtext.png';
1242
            $tmpTxt = __('Partial texts');
1243
            $urlParamsFullText['pftext'] = self::DISPLAY_PARTIAL_TEXT;
1244
        } else {
1245 8
            $tmpImageFile = 's_fulltext.png';
1246 8
            $tmpTxt = __('Full texts');
1247 8
            $urlParamsFullText['pftext'] = self::DISPLAY_FULL_TEXT;
1248
        }
1249
1250 8
        $tmpImage = '<img class="fulltext" src="'
1251 8
            . ($GLOBALS['theme'] instanceof Theme ? $GLOBALS['theme']->getImgPath($tmpImageFile) : '')
1252 8
            . '" alt="' . $tmpTxt . '" title="' . $tmpTxt . '">';
1253
1254 8
        return Generator::linkOrButton(Url::getFromRoute('/sql'), $urlParamsFullText, $tmpImage);
1255
    }
1256
1257
    /**
1258
     * Get comment for row
1259
     *
1260
     * @see getTableHeaders()
1261
     *
1262
     * @param mixed[]       $commentsMap comments array
1263
     * @param FieldMetadata $fieldsMeta  set of field properties
1264
     *
1265
     * @return string html content
1266
     */
1267 8
    private function getCommentForRow(array $commentsMap, FieldMetadata $fieldsMeta): string
1268
    {
1269 8
        return $this->template->render('display/results/comment_for_row', [
1270 8
            'comments_map' => $commentsMap,
1271 8
            'column_name' => $fieldsMeta->name,
1272 8
            'table_name' => $fieldsMeta->table,
1273 8
            'limit_chars' => $GLOBALS['cfg']['LimitChars'],
1274 8
        ]);
1275
    }
1276
1277
    /**
1278
     * Prepare parameters and html for sorted table header fields
1279
     *
1280
     * @see getTableHeaders()
1281
     *
1282
     * @param FieldMetadata      $fieldsMeta                set of field properties
1283
     * @param mixed[]            $sortExpression            sort expression
1284
     * @param array<int, string> $sortExpressionNoDirection sort expression without direction
1285
     * @param string             $unsortedSqlQuery          the unsorted sql query
1286
     * @param int                $sessionMaxRows            maximum rows resulted by sql
1287
     * @param string             $comments                  comment for row
1288
     * @param mixed[]            $sortDirection             sort direction
1289
     * @param bool|mixed[]       $colVisib                  column is visible(false)
1290
     *                                                      or column isn't visible(string array)
1291
     * @param int|string|null    $colVisibElement           element of $col_visib array
1292
     *
1293
     * @return mixed[]   2 element array - $orderLink, $sortedHeaderHtml
1294
     * @psalm-return array{
1295
     *   column_name: string,
1296
     *   order_link: string,
1297
     *   comments: string,
1298
     *   is_browse_pointer_enabled: bool,
1299
     *   is_browse_marker_enabled: bool,
1300
     *   is_column_hidden: bool,
1301
     *   is_column_numeric: bool,
1302
     * }
1303
     */
1304 8
    private function getOrderLinkAndSortedHeaderHtml(
1305
        FieldMetadata $fieldsMeta,
1306
        array $sortExpression,
1307
        array $sortExpressionNoDirection,
1308
        string $unsortedSqlQuery,
1309
        int $sessionMaxRows,
1310
        string $comments,
1311
        array $sortDirection,
1312
        bool|array $colVisib,
1313
        int|string|null $colVisibElement,
1314
    ): array {
1315
        // Checks if the table name is required; it's the case
1316
        // for a query with a "JOIN" statement and if the column
1317
        // isn't aliased, or in queries like
1318
        // SELECT `1`.`master_field` , `2`.`master_field`
1319
        // FROM `PMA_relation` AS `1` , `PMA_relation` AS `2`
1320
1321 8
        $sortTable = $fieldsMeta->table !== ''
1322 8
            && $fieldsMeta->orgname === $fieldsMeta->name
1323
            ? Util::backquote($fieldsMeta->table) . '.'
1324 8
            : '';
1325
1326
        // Generates the orderby clause part of the query which is part
1327
        // of URL
1328 8
        [$singleSortOrder, $multiSortOrder, $orderImg] = $this->getSingleAndMultiSortUrls(
1329 8
            $sortExpression,
1330 8
            $sortExpressionNoDirection,
1331 8
            $sortTable,
1332 8
            $fieldsMeta->name,
1333 8
            $sortDirection,
1334 8
            $fieldsMeta,
1335 8
        );
1336
1337
        if (
1338 8
            preg_match(
1339 8
                '@(.*)([[:space:]](LIMIT (.*)|PROCEDURE (.*)|FOR UPDATE|LOCK IN SHARE MODE))@is',
1340 8
                $unsortedSqlQuery,
1341 8
                $regs3,
1342 8
            )
1343
        ) {
1344
            $singleSortedSqlQuery = $regs3[1] . $singleSortOrder . $regs3[2];
1345
            $multiSortedSqlQuery = $regs3[1] . $multiSortOrder . $regs3[2];
1346
        } else {
1347 8
            $singleSortedSqlQuery = $unsortedSqlQuery . $singleSortOrder;
1348 8
            $multiSortedSqlQuery = $unsortedSqlQuery . $multiSortOrder;
1349
        }
1350
1351 8
        $singleUrlParams = [
1352 8
            'db' => $this->properties['db'],
1353 8
            'table' => $this->properties['table'],
1354 8
            'sql_query' => $singleSortedSqlQuery,
1355 8
            'sql_signature' => Core::signSqlQuery($singleSortedSqlQuery),
1356 8
            'session_max_rows' => $sessionMaxRows,
1357 8
            'is_browse_distinct' => $this->properties['is_browse_distinct'],
1358 8
        ];
1359
1360 8
        $multiUrlParams = [
1361 8
            'db' => $this->properties['db'],
1362 8
            'table' => $this->properties['table'],
1363 8
            'sql_query' => $multiSortedSqlQuery,
1364 8
            'sql_signature' => Core::signSqlQuery($multiSortedSqlQuery),
1365 8
            'session_max_rows' => $sessionMaxRows,
1366 8
            'is_browse_distinct' => $this->properties['is_browse_distinct'],
1367 8
        ];
1368
1369
        // Displays the sorting URL
1370
        // enable sort order swapping for image
1371 8
        $orderLink = $this->getSortOrderLink($orderImg, $fieldsMeta, $singleUrlParams, $multiUrlParams);
1372
1373 8
        $orderLink .= $this->getSortOrderHiddenInputs($multiUrlParams, $fieldsMeta->name);
1374
1375 8
        return [
1376 8
            'column_name' => $fieldsMeta->name,
1377 8
            'order_link' => $orderLink,
1378 8
            'comments' => $comments,
1379 8
            'is_browse_pointer_enabled' => $GLOBALS['cfg']['BrowsePointerEnable'] === true,
1380 8
            'is_browse_marker_enabled' => $GLOBALS['cfg']['BrowseMarkerEnable'] === true,
1381 8
            'is_column_hidden' => $colVisib && ! $colVisibElement,
0 ignored issues
show
Bug Best Practice introduced by
The expression $colVisib of type array<mixed,mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1382 8
            'is_column_numeric' => $this->isColumnNumeric($fieldsMeta),
1383 8
        ];
1384
    }
1385
1386
    /**
1387
     * Prepare parameters and html for sorted table header fields
1388
     *
1389
     * @param mixed[]            $sortExpression            sort expression
1390
     * @param array<int, string> $sortExpressionNoDirection sort expression without direction
1391
     * @param string             $sortTable                 The name of the table to which
1392
     *                                                      the current column belongs to
1393
     * @param string             $nameToUseInSort           The current column under
1394
     *                                                      consideration
1395
     * @param string[]           $sortDirection             sort direction
1396
     * @param FieldMetadata      $fieldsMeta                set of field properties
1397
     *
1398
     * @return string[]   3 element array - $single_sort_order, $sort_order, $order_img
1399
     */
1400 68
    private function getSingleAndMultiSortUrls(
1401
        array $sortExpression,
1402
        array $sortExpressionNoDirection,
1403
        string $sortTable,
1404
        string $nameToUseInSort,
1405
        array $sortDirection,
1406
        FieldMetadata $fieldsMeta,
1407
    ): array {
1408
        // Check if the current column is in the order by clause
1409 68
        $isInSort = $this->isInSorted($sortExpression, $sortExpressionNoDirection, $sortTable, $nameToUseInSort);
1410 68
        $currentName = $nameToUseInSort;
1411 68
        if ($sortExpressionNoDirection[0] == '' || ! $isInSort) {
1412 68
            $specialIndex = $sortExpressionNoDirection[0] == ''
1413 4
                ? 0
1414 64
                : count($sortExpressionNoDirection);
1415 68
            $sortExpressionNoDirection[$specialIndex] = Util::backquote($currentName);
1416
            // Set the direction to the config value
1417 68
            $sortDirection[$specialIndex] = $GLOBALS['cfg']['Order'];
1418
            // Or perform SMART mode
1419 68
            if ($GLOBALS['cfg']['Order'] === self::SMART_SORT_ORDER) {
1420 28
                $isTimeOrDate = $fieldsMeta->isType(FieldMetadata::TYPE_TIME)
1421 28
                    || $fieldsMeta->isType(FieldMetadata::TYPE_DATE)
1422 28
                    || $fieldsMeta->isType(FieldMetadata::TYPE_DATETIME)
1423 28
                    || $fieldsMeta->isType(FieldMetadata::TYPE_TIMESTAMP);
1424 28
                $sortDirection[$specialIndex] = $isTimeOrDate ? self::DESCENDING_SORT_DIR : self::ASCENDING_SORT_DIR;
1425
            }
1426
        }
1427
1428 68
        $sortExpressionNoDirection = array_filter($sortExpressionNoDirection);
1429 68
        $singleSortOrder = '';
1430 68
        $sortOrderColumns = [];
1431 68
        foreach ($sortExpressionNoDirection as $index => $expression) {
1432 68
            $sortOrder = '';
1433
            // check if this is the first clause,
1434
            // if it is then we have to add "order by"
1435 68
            $isFirstClause = ($index === 0);
1436 68
            $nameToUseInSort = $expression;
1437 68
            $sortTableNew = $sortTable;
1438
            // Test to detect if the column name is a standard name
1439
            // Standard name has the table name prefixed to the column name
1440 68
            if (str_contains($nameToUseInSort, '.') && ! str_contains($nameToUseInSort, '(')) {
1441 60
                $matches = explode('.', $nameToUseInSort);
1442
                // Matches[0] has the table name
1443
                // Matches[1] has the column name
1444 60
                $nameToUseInSort = $matches[1];
1445 60
                $sortTableNew = $matches[0];
1446
            }
1447
1448
            // $name_to_use_in_sort might contain a space due to
1449
            // formatting of function expressions like "COUNT(name )"
1450
            // so we remove the space in this situation
1451 68
            $nameToUseInSort = str_replace([' )', '``'], [')', '`'], $nameToUseInSort);
1452 68
            $nameToUseInSort = trim($nameToUseInSort, '`');
1453
1454
            // If this the first column name in the order by clause add
1455
            // order by clause to the  column name
1456 68
            $sortOrder .= $isFirstClause ? "\nORDER BY " : '';
1457
1458
            // Again a check to see if the given column is a aggregate column
1459 68
            if (str_contains($nameToUseInSort, '(')) {
1460
                $sortOrder .= $nameToUseInSort;
1461
            } else {
1462 68
                if ($sortTableNew !== '' && ! str_ends_with($sortTableNew, '.')) {
1463 60
                    $sortTableNew .= '.';
1464
                }
1465
1466 68
                $sortOrder .= $sortTableNew . Util::backquote($nameToUseInSort);
1467
            }
1468
1469
            // Incase this is the current column save $single_sort_order
1470 68
            if ($currentName === $nameToUseInSort) {
1471 68
                $singleSortOrder = "\n" . 'ORDER BY ';
1472
1473 68
                if (! str_contains($currentName, '(')) {
1474 68
                    $singleSortOrder .= $sortTable;
1475
                }
1476
1477 68
                $singleSortOrder .= Util::backquote($currentName) . ' ';
1478
1479 68
                if ($isInSort) {
1480 4
                    [$singleSortOrder, $orderImg] = $this->getSortingUrlParams(
1481 4
                        $sortDirection[$index],
1482 4
                        $singleSortOrder,
1483 4
                    );
1484
                } else {
1485 68
                    $singleSortOrder .= strtoupper($sortDirection[$index]);
1486
                }
1487
            }
1488
1489 68
            $sortOrder .= ' ';
1490 68
            if ($currentName === $nameToUseInSort && $isInSort) {
1491
                // We need to generate the arrow button and related html
1492 4
                [$sortOrder, $orderImg] = $this->getSortingUrlParams($sortDirection[$index], $sortOrder);
1493 4
                $orderImg .= ' <small>' . ($index + 1) . '</small>';
1494
            } else {
1495 68
                $sortOrder .= strtoupper($sortDirection[$index]);
1496
            }
1497
1498
            // Separate columns by a comma
1499 68
            $sortOrderColumns[] = $sortOrder;
1500
        }
1501
1502 68
        return [$singleSortOrder, implode(', ', $sortOrderColumns), $orderImg ?? ''];
1503
    }
1504
1505
    /**
1506
     * Check whether the column is sorted
1507
     *
1508
     * @see getTableHeaders()
1509
     *
1510
     * @param mixed[] $sortExpression            sort expression
1511
     * @param mixed[] $sortExpressionNoDirection sort expression without direction
1512
     * @param string  $sortTable                 the table name
1513
     * @param string  $nameToUseInSort           the sorting column name
1514
     */
1515 68
    private function isInSorted(
1516
        array $sortExpression,
1517
        array $sortExpressionNoDirection,
1518
        string $sortTable,
1519
        string $nameToUseInSort,
1520
    ): bool {
1521 68
        $indexInExpression = 0;
1522
1523 68
        foreach ($sortExpressionNoDirection as $index => $clause) {
1524 68
            if (str_contains($clause, '.')) {
1525 60
                $fragments = explode('.', $clause);
1526 60
                $clause2 = $fragments[0] . '.' . str_replace('`', '', $fragments[1]);
1527
            } else {
1528 8
                $clause2 = $sortTable . str_replace('`', '', $clause);
1529
            }
1530
1531 68
            if ($clause2 === $sortTable . $nameToUseInSort) {
1532 4
                $indexInExpression = $index;
1533 4
                break;
1534
            }
1535
        }
1536
1537 68
        if (empty($sortExpression[$indexInExpression])) {
1538 4
            return false;
1539
        }
1540
1541
        // Field name may be preceded by a space, or any number
1542
        // of characters followed by a dot (tablename.fieldname)
1543
        // so do a direct comparison for the sort expression;
1544
        // this avoids problems with queries like
1545
        // "SELECT id, count(id)..." and clicking to sort
1546
        // on id or on count(id).
1547
        // Another query to test this:
1548
        // SELECT p.*, FROM_UNIXTIME(p.temps) FROM mytable AS p
1549
        // (and try clicking on each column's header twice)
1550 64
        $noSortTable = $sortTable === '' || mb_strpos(
1551 64
            $sortExpressionNoDirection[$indexInExpression],
1552 64
            $sortTable,
1553 64
        ) === false;
1554 64
        $noOpenParenthesis = mb_strpos($sortExpressionNoDirection[$indexInExpression], '(') === false;
1555 64
        if ($sortTable !== '' && $noSortTable && $noOpenParenthesis) {
1556
            $newSortExpressionNoDirection = $sortTable
1557
                . $sortExpressionNoDirection[$indexInExpression];
1558
        } else {
1559 64
            $newSortExpressionNoDirection = $sortExpressionNoDirection[$indexInExpression];
1560
        }
1561
1562
        //Back quotes are removed in next comparison, so remove them from value
1563
        //to compare.
1564 64
        $nameToUseInSort = str_replace('`', '', $nameToUseInSort);
1565
1566 64
        $sortName = str_replace('`', '', $sortTable) . $nameToUseInSort;
1567
1568 64
        return $sortName == str_replace('`', '', $newSortExpressionNoDirection)
1569 64
            || $sortName == str_replace('`', '', $sortExpressionNoDirection[$indexInExpression]);
1570
    }
1571
1572
    /**
1573
     * Get sort url parameters - sort order and order image
1574
     *
1575
     * @see     getSingleAndMultiSortUrls()
1576
     *
1577
     * @param string $sortDirection the sort direction
1578
     * @param string $sortOrder     the sorting order
1579
     *
1580
     * @return string[]             2 element array - $sort_order, $order_img
1581
     */
1582 4
    private function getSortingUrlParams(string $sortDirection, string $sortOrder): array
1583
    {
1584 4
        if (strtoupper(trim($sortDirection)) === self::DESCENDING_SORT_DIR) {
1585
            $sortOrder .= self::ASCENDING_SORT_DIR;
1586
            $orderImg = ' ' . Generator::getImage(
1587
                's_desc',
1588
                __('Descending'),
1589
                ['class' => 'soimg', 'title' => ''],
1590
            );
1591
            $orderImg .= ' ' . Generator::getImage(
1592
                's_asc',
1593
                __('Ascending'),
1594
                ['class' => 'soimg hide', 'title' => ''],
1595
            );
1596
        } else {
1597 4
            $sortOrder .= self::DESCENDING_SORT_DIR;
1598 4
            $orderImg = ' ' . Generator::getImage(
1599 4
                's_asc',
1600 4
                __('Ascending'),
1601 4
                ['class' => 'soimg', 'title' => ''],
1602 4
            );
1603 4
            $orderImg .= ' ' . Generator::getImage(
1604 4
                's_desc',
1605 4
                __('Descending'),
1606 4
                ['class' => 'soimg hide', 'title' => ''],
1607 4
            );
1608
        }
1609
1610 4
        return [$sortOrder, $orderImg];
1611
    }
1612
1613
    /**
1614
     * Get sort order link
1615
     *
1616
     * @see getTableHeaders()
1617
     *
1618
     * @param string                   $orderImg            the sort order image
1619
     * @param FieldMetadata            $fieldsMeta          set of field properties
1620
     * @param array<int|string, mixed> $orderUrlParams      the url params for sort
1621
     * @param array<int|string, mixed> $multiOrderUrlParams the url params for sort
1622
     *
1623
     * @return string the sort order link
1624
     */
1625 8
    private function getSortOrderLink(
1626
        string $orderImg,
1627
        FieldMetadata $fieldsMeta,
1628
        array $orderUrlParams,
1629
        array $multiOrderUrlParams,
1630
    ): string {
1631 8
        $urlPath = Url::getFromRoute('/sql');
1632 8
        $innerLinkContent = htmlspecialchars($fieldsMeta->name) . $orderImg
1633 8
            . '<input type="hidden" value="'
1634 8
            . $urlPath
1635 8
            . Url::getCommon($multiOrderUrlParams, str_contains($urlPath, '?') ? '&' : '?', false)
1636 8
            . '">';
1637
1638 8
        return Generator::linkOrButton(
1639 8
            Url::getFromRoute('/sql'),
1640 8
            $orderUrlParams,
1641 8
            $innerLinkContent,
1642 8
            ['class' => 'sortlink'],
1643 8
        );
1644
    }
1645
1646
    /** @param mixed[] $multipleUrlParams */
1647 36
    private function getSortOrderHiddenInputs(
1648
        array $multipleUrlParams,
1649
        string $nameToUseInSort,
1650
    ): string {
1651 36
        $sqlQuery = $multipleUrlParams['sql_query'];
1652 36
        $sqlQueryAdd = $sqlQuery;
1653 36
        $sqlQueryRemove = null;
1654 36
        $parser = new Parser($sqlQuery);
1655
1656 36
        $firstStatement = $parser->statements[0] ?? null;
1657 36
        $numberOfClausesFound = null;
1658 36
        if ($firstStatement instanceof SelectStatement) {
1659 32
            $orderClauses = $firstStatement->order ?? [];
1660 32
            foreach ($orderClauses as $key => $order) {
1661
                // If this is the column name, then remove it from the order clause
1662 28
                if ($order->expr->column !== $nameToUseInSort) {
1663 20
                    continue;
1664
                }
1665
1666
                // remove the order clause for this column and from the counted array
1667 24
                unset($firstStatement->order[$key], $orderClauses[$key]);
1668
            }
1669
1670 32
            $numberOfClausesFound = count($orderClauses);
1671 32
            $sqlQueryRemove = $firstStatement->build();
1672
        }
1673
1674 36
        $multipleUrlParams['sql_query'] = $sqlQueryRemove ?? $sqlQuery;
1675 36
        $multipleUrlParams['sql_signature'] = Core::signSqlQuery($multipleUrlParams['sql_query']);
1676
1677 36
        $urlRemoveOrder = Url::getFromRoute('/sql', $multipleUrlParams);
1678 36
        if ($numberOfClausesFound === 0) {
1679 16
            $urlRemoveOrder .= '&discard_remembered_sort=1';
1680
        }
1681
1682 36
        $multipleUrlParams['sql_query'] = $sqlQueryAdd;
1683 36
        $multipleUrlParams['sql_signature'] = Core::signSqlQuery($multipleUrlParams['sql_query']);
1684
1685 36
        $urlAddOrder = Url::getFromRoute('/sql', $multipleUrlParams);
1686
1687 36
        return '<input type="hidden" name="url-remove-order" value="' . $urlRemoveOrder . '">' . "\n"
1688 36
             . '<input type="hidden" name="url-add-order" value="' . $urlAddOrder . '">';
1689
    }
1690
1691
    /**
1692
     * Check if the column contains numeric data
1693
     *
1694
     * @param FieldMetadata $fieldsMeta set of field properties
1695
     */
1696 8
    private function isColumnNumeric(FieldMetadata $fieldsMeta): bool
1697
    {
1698
        // This was defined in commit b661cd7c9b31f8bc564d2f9a1b8527e0eb966de8
1699
        // For issue https://github.com/phpmyadmin/phpmyadmin/issues/4746
1700 8
        return $fieldsMeta->isType(FieldMetadata::TYPE_REAL)
1701 8
            || $fieldsMeta->isMappedTypeBit
1702 8
            || $fieldsMeta->isType(FieldMetadata::TYPE_INT);
1703
    }
1704
1705
    /**
1706
     * Prepare column to show at right side - check boxes or empty column
1707
     *
1708
     * @see getTableHeaders()
1709
     *
1710
     * @param string $fullOrPartialTextLink full/partial link or text button
1711
     * @param string $colspan               column span of table header
1712
     *
1713
     * @return string  html content
1714
     */
1715 8
    private function getColumnAtRightSide(
1716
        DisplayParts $displayParts,
1717
        string $fullOrPartialTextLink,
1718
        string $colspan,
1719
    ): string {
1720 8
        $rightColumnHtml = '';
1721 8
        $displayParams = $this->properties['display_params'];
1722
1723
        // Displays the needed checkboxes at the right
1724
        // column of the result table header if possible and required...
1725
        if (
1726 8
            ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_RIGHT)
1727 8
            || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH)
1728 8
            && ($displayParts->hasEditLink || $displayParts->deleteLink !== DeleteLinkEnum::NO_DELETE)
1729 8
            && $displayParts->hasTextButton
1730
        ) {
1731
            $displayParams['emptyafter'] = $displayParts->hasEditLink
1732
                && $displayParts->deleteLink !== DeleteLinkEnum::NO_DELETE ? 4 : 1;
1733
1734
            $rightColumnHtml .= "\n"
1735
                . '<th class="column_action d-print-none"' . $colspan . '>'
1736
                . $fullOrPartialTextLink
1737
                . '</th>';
1738
        } elseif (
1739 8
            ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_LEFT)
1740 8
            || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH)
1741 8
            && (! $displayParts->hasEditLink
1742 8
            && $displayParts->deleteLink === DeleteLinkEnum::NO_DELETE)
1743 8
            && (! isset($GLOBALS['is_header_sent']) || ! $GLOBALS['is_header_sent'])
1744
        ) {
1745
            //     ... elseif no button, displays empty columns if required
1746
            // (unless coming from Browse mode print view)
1747
1748 8
            $displayParams['emptyafter'] = $displayParts->hasEditLink
1749 8
                && $displayParts->deleteLink !== DeleteLinkEnum::NO_DELETE ? 4 : 1;
1750
1751 8
            $rightColumnHtml .= "\n" . '<td class="d-print-none"' . $colspan
1752 8
                . '></td>';
1753
        }
1754
1755 8
        $this->properties['display_params'] = $displayParams;
1756
1757 8
        return $rightColumnHtml;
1758
    }
1759
1760
    /**
1761
     * Prepares the display for a value
1762
     *
1763
     * @see     getDataCellForGeometryColumns(),
1764
     *          getDataCellForNonNumericColumns()
1765
     *
1766
     * @param string $class          class of table cell
1767
     * @param bool   $conditionField whether to add CSS class condition
1768
     * @param string $value          value to display
1769
     *
1770
     * @return string  the td
1771
     */
1772 12
    private function buildValueDisplay(string $class, bool $conditionField, string $value): string
1773
    {
1774 12
        return $this->template->render('display/results/value_display', [
1775 12
            'class' => $class,
1776 12
            'condition_field' => $conditionField,
1777 12
            'value' => $value,
1778 12
        ]);
1779
    }
1780
1781
    /**
1782
     * Prepares the display for a null value
1783
     *
1784
     * @see     getDataCellForNumericColumns(),
1785
     *          getDataCellForGeometryColumns(),
1786
     *          getDataCellForNonNumericColumns()
1787
     *
1788
     * @param string        $class          class of table cell
1789
     * @param bool          $conditionField whether to add CSS class condition
1790
     * @param FieldMetadata $meta           the meta-information about this field
1791
     *
1792
     * @return string  the td
1793
     */
1794 4
    private function buildNullDisplay(string $class, bool $conditionField, FieldMetadata $meta): string
1795
    {
1796 4
        $classes = $this->addClass($class, $conditionField, $meta, '');
1797
1798 4
        return $this->template->render('display/results/null_display', [
1799 4
            'data_decimals' => $meta->decimals,
1800 4
            'data_type' => $meta->getMappedType(),
1801 4
            'classes' => $classes,
1802 4
        ]);
1803
    }
1804
1805
    /**
1806
     * Prepares the display for an empty value
1807
     *
1808
     * @see     getDataCellForNumericColumns(),
1809
     *          getDataCellForGeometryColumns(),
1810
     *          getDataCellForNonNumericColumns()
1811
     *
1812
     * @param string        $class          class of table cell
1813
     * @param bool          $conditionField whether to add CSS class condition
1814
     * @param FieldMetadata $meta           the meta-information about this field
1815
     *
1816
     * @return string  the td
1817
     */
1818
    private function buildEmptyDisplay(string $class, bool $conditionField, FieldMetadata $meta): string
1819
    {
1820
        $classes = $this->addClass($class, $conditionField, $meta, 'text-nowrap');
1821
1822
        return $this->template->render('display/results/empty_display', ['classes' => $classes]);
1823
    }
1824
1825
    /**
1826
     * Adds the relevant classes.
1827
     *
1828
     * @see buildNullDisplay(), getRowData()
1829
     *
1830
     * @param string        $class            class of table cell
1831
     * @param bool          $conditionField   whether to add CSS class condition
1832
     * @param FieldMetadata $meta             the meta-information about the field
1833
     * @param string        $nowrap           avoid wrapping
1834
     * @param bool          $isFieldTruncated is field truncated (display ...)
1835
     *
1836
     * @return string the list of classes
1837
     */
1838 36
    private function addClass(
1839
        string $class,
1840
        bool $conditionField,
1841
        FieldMetadata $meta,
1842
        string $nowrap,
1843
        bool $isFieldTruncated = false,
1844
        bool $hasTransformationPlugin = false,
1845
    ): string {
1846 36
        $classes = array_filter([$class, $nowrap]);
1847
1848 36
        if ($meta->internalMediaType !== null) {
1849 4
            $classes[] = preg_replace('/\//', '_', $meta->internalMediaType);
1850
        }
1851
1852 36
        if ($conditionField) {
1853
            $classes[] = 'condition';
1854
        }
1855
1856 36
        if ($isFieldTruncated) {
1857
            $classes[] = 'truncated';
1858
        }
1859
1860 36
        $mediaTypeMap = $this->properties['mime_map'];
1861 36
        $orgFullColName = $this->properties['db'] . '.' . $meta->orgtable
1862 36
            . '.' . $meta->orgname;
1863 36
        if ($hasTransformationPlugin || ! empty($mediaTypeMap[$orgFullColName]['input_transformation'])) {
1864 12
            $classes[] = 'transformed';
1865
        }
1866
1867
        // Define classes to be added to this data field based on the type of data
1868
1869 36
        if ($meta->isEnum()) {
1870
            $classes[] = 'enum';
1871
        }
1872
1873 36
        if ($meta->isSet()) {
1874
            $classes[] = 'set';
1875
        }
1876
1877 36
        if ($meta->isMappedTypeBit) {
1878
            $classes[] = 'bit';
1879
        }
1880
1881 36
        if ($meta->isBinary()) {
1882 8
            $classes[] = 'hex';
1883
        }
1884
1885 36
        return implode(' ', $classes);
1886
    }
1887
1888
    /**
1889
     * Prepare the body of the results table
1890
     *
1891
     * @see     getTable()
1892
     *
1893
     * @param ResultInterface          $dtResult         the link id associated to the query
1894
     *                                                                     which results have to be displayed
1895
     * @param ForeignKeyRelatedTable[] $map              the list of relations
1896
     * @param bool                     $isLimitedDisplay with limited operations or not
1897
     *
1898
     * @return string  html content
1899
     *
1900
     * @global array  $row                  current row data
1901
     */
1902 8
    private function getTableBody(
1903
        ResultInterface $dtResult,
1904
        DisplayParts $displayParts,
1905
        array $map,
1906
        StatementInfo $statementInfo,
1907
        bool $isLimitedDisplay = false,
1908
    ): string {
1909
        // Mostly because of browser transformations, to make the row-data accessible in a plugin.
1910
1911 8
        $GLOBALS['row'] ??= null;
1912
1913 8
        $tableBodyHtml = '';
1914
1915
        // query without conditions to shorten URLs when needed, 200 is just
1916
        // guess, it should depend on remaining URL length
1917 8
        $urlSqlQuery = $this->getUrlSqlQuery($statementInfo);
1918
1919 8
        $displayParams = $this->properties['display_params'];
1920
1921 8
        $rowNumber = 0;
1922 8
        $displayParams['edit'] = [];
1923 8
        $displayParams['copy'] = [];
1924 8
        $displayParams['delete'] = [];
1925 8
        $displayParams['data'] = [];
1926 8
        $displayParams['row_delete'] = [];
1927 8
        $this->properties['display_params'] = $displayParams;
1928
1929 8
        $gridEditConfig = 'double-click';
1930
        // If we don't have all the columns of a unique key in the result set, do not permit grid editing.
1931 8
        if ($isLimitedDisplay || ! $this->properties['editable'] || $GLOBALS['cfg']['GridEditing'] === 'disabled') {
1932
            $gridEditConfig = 'disabled';
1933 8
        } elseif ($GLOBALS['cfg']['GridEditing'] === 'click') {
1934
            $gridEditConfig = 'click';
1935
        }
1936
1937
        // prepare to get the column order, if available
1938 8
        [$colOrder, $colVisib] = $this->getColumnParams($statementInfo);
1939
1940
        // Correction University of Virginia 19991216 in the while below
1941
        // Previous code assumed that all tables have keys, specifically that
1942
        // the phpMyAdmin GUI should support row delete/edit only for such
1943
        // tables.
1944
        // Although always using keys is arguably the prescribed way of
1945
        // defining a relational table, it is not required. This will in
1946
        // particular be violated by the novice.
1947
        // We want to encourage phpMyAdmin usage by such novices. So the code
1948
        // below has been changed to conditionally work as before when the
1949
        // table being displayed has one or more keys; but to display
1950
        // delete/edit options correctly for tables without keys.
1951
1952 8
        $whereClauseMap = $this->properties['whereClauseMap'];
1953 8
        while ($GLOBALS['row'] = $dtResult->fetchRow()) {
1954
            // add repeating headers
1955
            if (
1956 8
                ($rowNumber !== 0) && ($_SESSION['tmpval']['repeat_cells'] > 0)
1957 8
                && ($rowNumber % $_SESSION['tmpval']['repeat_cells']) === 0
1958
            ) {
1959
                $tableBodyHtml .= $this->getRepeatingHeaders(
1960
                    $displayParams['emptypre'],
1961
                    $displayParams['desc'],
1962
                    $displayParams['emptyafter'],
1963
                );
1964
            }
1965
1966 8
            $trClass = [];
1967 8
            if ($GLOBALS['cfg']['BrowsePointerEnable'] != true) {
1968
                $trClass[] = 'nopointer';
1969
            }
1970
1971 8
            if ($GLOBALS['cfg']['BrowseMarkerEnable'] != true) {
1972
                $trClass[] = 'nomarker';
1973
            }
1974
1975
            // pointer code part
1976 8
            $tableBodyHtml .= '<tr' . ($trClass === [] ? '' : ' class="' . implode(' ', $trClass) . '"') . '>';
1977
1978
            // 1. Prepares the row
1979
1980
            // In print view these variable needs to be initialized
1981 8
            $deleteUrl = null;
1982 8
            $deleteString = null;
1983 8
            $editString = null;
1984 8
            $jsConf = null;
1985 8
            $copyUrl = null;
1986 8
            $copyString = null;
1987 8
            $editUrl = null;
1988 8
            $editCopyUrlParams = [];
1989 8
            $delUrlParams = null;
1990
1991
            // 1.2 Defines the URLs for the modify/delete link(s)
1992
1993
            if (
1994 8
                $displayParts->hasEditLink
1995 8
                || ($displayParts->deleteLink !== DeleteLinkEnum::NO_DELETE)
1996
            ) {
1997
                $expressions = [];
1998
1999
                if (
2000
                    isset($statementInfo->statement)
2001
                    && $statementInfo->statement instanceof SelectStatement
2002
                ) {
2003
                    $expressions = $statementInfo->statement->expr;
2004
                }
2005
2006
                // Results from a "SELECT" statement -> builds the
2007
                // WHERE clause to use in links (a unique key if possible)
2008
                /**
2009
                 * @todo $where_clause could be empty, for example a table
2010
                 *       with only one field and it's a BLOB; in this case,
2011
                 *       avoid to display the delete and edit links
2012
                 */
2013
                [$whereClause, $clauseIsUnique, $conditionArray] = Util::getUniqueCondition(
2014
                    $this->properties['fields_cnt'],
2015
                    $this->properties['fields_meta'],
2016
                    $GLOBALS['row'],
2017
                    false,
2018
                    $this->properties['table'],
2019
                    $expressions,
2020
                );
2021
                $whereClauseMap[$rowNumber][$this->properties['table']] = $whereClause;
2022
                $this->properties['whereClauseMap'] = $whereClauseMap;
2023
2024
                // 1.2.1 Modify link(s) - update row case
2025
                if ($displayParts->hasEditLink) {
2026
                    [
2027
                        $editUrl,
2028
                        $copyUrl,
2029
                        $editString,
2030
                        $copyString,
2031
                        $editCopyUrlParams,
2032
                    ] = $this->getModifiedLinks($whereClause, $clauseIsUnique, $urlSqlQuery);
2033
                }
2034
2035
                // 1.2.2 Delete/Kill link(s)
2036
                [$deleteUrl, $deleteString, $jsConf, $delUrlParams] = $this->getDeleteAndKillLinks(
2037
                    $whereClause,
2038
                    $clauseIsUnique,
2039
                    $urlSqlQuery,
2040
                    $displayParts->deleteLink,
2041
                    (int) $GLOBALS['row'][0],
2042
                );
2043
2044
                // 1.3 Displays the links at left if required
2045
                if (
2046
                    ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_LEFT)
2047
                    || ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH)
2048
                ) {
2049
                    $tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
2050
                        'position' => self::POSITION_LEFT,
2051
                        'has_checkbox' => $deleteUrl && $displayParts->deleteLink !== DeleteLinkEnum::KILL_PROCESS,
2052
                        'edit' => [
2053
                            'url' => $editUrl,
2054
                            'params' => $editCopyUrlParams + ['default_action' => 'update'],
2055
                            'string' => $editString,
2056
                            'clause_is_unique' => $clauseIsUnique,
2057
                        ],
2058
                        'copy' => [
2059
                            'url' => $copyUrl,
2060
                            'params' => $editCopyUrlParams + ['default_action' => 'insert'],
2061
                            'string' => $copyString,
2062
                        ],
2063
                        'delete' => ['url' => $deleteUrl, 'params' => $delUrlParams, 'string' => $deleteString],
2064
                        'row_number' => $rowNumber,
2065
                        'where_clause' => $whereClause,
2066
                        'condition' => json_encode($conditionArray),
2067
                        'is_ajax' => ResponseRenderer::getInstance()->isAjax(),
2068
                        'js_conf' => $jsConf ?? '',
2069
                        'grid_edit_config' => $gridEditConfig,
2070
                    ]);
2071
                } elseif ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_NONE) {
2072
                    $tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
2073
                        'position' => self::POSITION_NONE,
2074
                        'has_checkbox' => $deleteUrl && $displayParts->deleteLink !== DeleteLinkEnum::KILL_PROCESS,
2075
                        'edit' => [
2076
                            'url' => $editUrl,
2077
                            'params' => $editCopyUrlParams + ['default_action' => 'update'],
2078
                            'string' => $editString,
2079
                            'clause_is_unique' => $clauseIsUnique,
2080
                        ],
2081
                        'copy' => [
2082
                            'url' => $copyUrl,
2083
                            'params' => $editCopyUrlParams + ['default_action' => 'insert'],
2084
                            'string' => $copyString,
2085
                        ],
2086
                        'delete' => ['url' => $deleteUrl, 'params' => $delUrlParams, 'string' => $deleteString],
2087
                        'row_number' => $rowNumber,
2088
                        'where_clause' => $whereClause,
2089
                        'condition' => json_encode($conditionArray),
2090
                        'is_ajax' => ResponseRenderer::getInstance()->isAjax(),
2091
                        'js_conf' => $jsConf ?? '',
2092
                        'grid_edit_config' => $gridEditConfig,
2093
                    ]);
2094
                }
2095
            }
2096
2097
            // 2. Displays the rows' values
2098 8
            if ($this->properties['mime_map'] === null) {
2099 8
                $this->setMimeMap();
2100
            }
2101
2102 8
            $tableBodyHtml .= $this->getRowValues(
2103 8
                $GLOBALS['row'],
2104 8
                $rowNumber,
2105 8
                $colOrder,
2106 8
                $map,
2107 8
                $gridEditConfig,
2108 8
                $colVisib,
2109 8
                $urlSqlQuery,
2110 8
                $statementInfo,
2111 8
            );
2112
2113
            // 3. Displays the modify/delete links on the right if required
2114
            if (
2115 8
                ($displayParts->hasEditLink
2116 8
                    || $displayParts->deleteLink !== DeleteLinkEnum::NO_DELETE)
2117 8
                && ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_RIGHT
2118 8
                    || $GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH)
2119
            ) {
2120
                $tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
2121
                    'position' => self::POSITION_RIGHT,
2122
                    'has_checkbox' => $deleteUrl && $displayParts->deleteLink !== DeleteLinkEnum::KILL_PROCESS,
2123
                    'edit' => [
2124
                        'url' => $editUrl,
2125
                        'params' => $editCopyUrlParams + ['default_action' => 'update'],
2126
                        'string' => $editString,
2127
                        'clause_is_unique' => $clauseIsUnique ?? true,
2128
                    ],
2129
                    'copy' => [
2130
                        'url' => $copyUrl,
2131
                        'params' => $editCopyUrlParams + ['default_action' => 'insert'],
2132
                        'string' => $copyString,
2133
                    ],
2134
                    'delete' => ['url' => $deleteUrl, 'params' => $delUrlParams, 'string' => $deleteString],
2135
                    'row_number' => $rowNumber,
2136
                    'where_clause' => $whereClause ?? '',
2137
                    'condition' => json_encode($conditionArray ?? []),
2138
                    'is_ajax' => ResponseRenderer::getInstance()->isAjax(),
2139
                    'js_conf' => $jsConf ?? '',
2140
                    'grid_edit_config' => $gridEditConfig,
2141
                ]);
2142
            }
2143
2144 8
            $tableBodyHtml .= '</tr>';
2145 8
            $tableBodyHtml .= "\n";
2146 8
            $rowNumber++;
2147
        }
2148
2149 8
        return $tableBodyHtml;
2150
    }
2151
2152
    /**
2153
     * Sets the MIME details of the columns in the results set
2154
     */
2155 8
    private function setMimeMap(): void
2156
    {
2157 8
        $fieldsMeta = $this->properties['fields_meta'];
2158 8
        $mediaTypeMap = [];
2159 8
        $added = [];
2160 8
        $relationParameters = $this->relation->getRelationParameters();
2161
2162 8
        for ($currentColumn = 0; $currentColumn < $this->properties['fields_cnt']; ++$currentColumn) {
2163 8
            $meta = $fieldsMeta[$currentColumn];
2164 8
            $orgFullTableName = $this->properties['db'] . '.' . $meta->orgtable;
2165
2166
            if (
2167 8
                $relationParameters->columnCommentsFeature === null
2168 6
                || $relationParameters->browserTransformationFeature === null
2169 6
                || ! $GLOBALS['cfg']['BrowseMIME']
2170 6
                || $_SESSION['tmpval']['hide_transformation']
2171 8
                || ! empty($added[$orgFullTableName])
2172
            ) {
2173 8
                continue;
2174
            }
2175
2176
            $mediaTypeMap = array_merge(
2177
                $mediaTypeMap,
2178
                $this->transformations->getMime($this->properties['db'], $meta->orgtable, false, true) ?? [],
2179
            );
2180
            $added[$orgFullTableName] = true;
2181
        }
2182
2183
        // special browser transformation for some SHOW statements
2184 8
        if ($this->properties['is_show'] && ! $_SESSION['tmpval']['hide_transformation']) {
2185
            preg_match(
2186
                '@^SHOW[[:space:]]+(VARIABLES|(FULL[[:space:]]+)?'
2187
                . 'PROCESSLIST|STATUS|TABLE|GRANTS|CREATE|LOGS|DATABASES|FIELDS'
2188
                . ')@i',
2189
                $this->properties['sql_query'],
2190
                $which,
2191
            );
2192
2193
            if (isset($which[1])) {
2194
                $str = ' ' . strtoupper($which[1]);
2195
                $isShowProcessList = strpos($str, 'PROCESSLIST') > 0;
2196
                if ($isShowProcessList) {
2197
                    $mediaTypeMap['..Info'] = [
2198
                        'mimetype' => 'Text_Plain',
2199
                        'transformation' => 'output/Text_Plain_Sql.php',
2200
                    ];
2201
                }
2202
2203
                $isShowCreateTable = preg_match('@CREATE[[:space:]]+TABLE@i', $this->properties['sql_query']);
2204
                if ($isShowCreateTable) {
2205
                    $mediaTypeMap['..Create Table'] = [
2206
                        'mimetype' => 'Text_Plain',
2207
                        'transformation' => 'output/Text_Plain_Sql.php',
2208
                    ];
2209
                }
2210
            }
2211
        }
2212
2213 8
        $this->properties['mime_map'] = $mediaTypeMap;
2214
    }
2215
2216
    /**
2217
     * Get the values for one data row
2218
     *
2219
     * @see     getTableBody()
2220
     *
2221
     * @param mixed[]                  $row         current row data
2222
     * @param int                      $rowNumber   the index of current row
2223
     * @param mixed[]|false            $colOrder    the column order false when
2224
     *                                             a property not found false
2225
     *                                             when a property not found
2226
     * @param ForeignKeyRelatedTable[] $map         the list of relations
2227
     * @param bool|mixed[]|string      $colVisib    column is visible(false);
2228
     *                                             column isn't visible(string
2229
     *                                             array)
2230
     * @param string                   $urlSqlQuery the analyzed sql query
2231
     * @psalm-param 'double-click'|'click'|'disabled' $gridEditConfig
2232
     *
2233
     * @return string  html content
2234
     */
2235 12
    private function getRowValues(
2236
        array $row,
2237
        int $rowNumber,
2238
        array|false $colOrder,
2239
        array $map,
2240
        string $gridEditConfig,
2241
        bool|array|string $colVisib,
2242
        string $urlSqlQuery,
2243
        StatementInfo $statementInfo,
2244
    ): string {
2245 12
        $rowValuesHtml = '';
2246
2247
        // Following variable are needed for use in isset/empty or
2248
        // use with array indexes/safe use in foreach
2249 12
        $sqlQuery = $this->properties['sql_query'];
2250 12
        $fieldsMeta = $this->properties['fields_meta'];
2251 12
        $highlightColumns = $this->properties['highlight_columns'];
2252 12
        $mediaTypeMap = $this->properties['mime_map'];
2253
2254 12
        $rowInfo = $this->getRowInfoForSpecialLinks($row, $colOrder);
2255
2256 12
        $whereClauseMap = $this->properties['whereClauseMap'];
2257
2258 12
        $columnCount = $this->properties['fields_cnt'];
2259
2260
        // Load SpecialSchemaLinks for all rows
2261 12
        $specialSchemaLinks = SpecialSchemaLinks::get();
2262 12
        $relationParameters = $this->relation->getRelationParameters();
2263
2264 12
        for ($currentColumn = 0; $currentColumn < $columnCount; ++$currentColumn) {
2265
            // assign $i with appropriate column order
2266 12
            $i = is_array($colOrder) ? $colOrder[$currentColumn] : $currentColumn;
2267
2268 12
            $meta = $fieldsMeta[$i];
2269 12
            $orgFullColName = $this->properties['db'] . '.' . $meta->orgtable . '.' . $meta->orgname;
2270
2271 12
            $notNullClass = $meta->isNotNull() ? 'not_null' : '';
2272 12
            $relationClass = isset($map[$meta->name]) ? 'relation' : '';
2273 12
            $hideClass = is_array($colVisib) && isset($colVisib[$currentColumn]) && ! $colVisib[$currentColumn]
2274
                ? 'hide'
2275 12
                : '';
2276
2277 12
            $gridEdit = '';
2278 12
            if ($meta->orgtable != '' && $gridEditConfig !== 'disabled') {
2279
                $gridEdit = $gridEditConfig === 'click' ? 'grid_edit click1' : 'grid_edit click2';
2280
            }
2281
2282
            // handle datetime-related class, for grid editing
2283 12
            $fieldTypeClass = $this->getClassForDateTimeRelatedFields($meta);
2284
2285
            // combine all the classes applicable to this column's value
2286 12
            $class = implode(' ', array_filter([
2287 12
                'data',
2288 12
                $gridEdit,
2289 12
                $notNullClass,
2290 12
                $relationClass,
2291 12
                $hideClass,
2292 12
                $fieldTypeClass,
2293 12
            ]));
2294
2295
            //  See if this column should get highlight because it's used in the
2296
            //  where-query.
2297 12
            $conditionField = isset($highlightColumns)
2298 12
                && (isset($highlightColumns[$meta->name])
2299 12
                || isset($highlightColumns[Util::backquote($meta->name)]));
2300
2301
            // Wrap MIME-transformations. [MIME]
2302 12
            $transformationPlugin = null;
2303 12
            $transformOptions = [];
2304
2305
            if (
2306 12
                $relationParameters->browserTransformationFeature !== null && $GLOBALS['cfg']['BrowseMIME']
2307 12
                && isset($mediaTypeMap[$orgFullColName]['mimetype'])
2308 12
                && ! empty($mediaTypeMap[$orgFullColName]['transformation'])
2309
            ) {
2310 4
                $file = $mediaTypeMap[$orgFullColName]['transformation'];
2311 4
                $includeFile = 'libraries/classes/Plugins/Transformations/' . $file;
2312
2313 4
                if (@file_exists(ROOT_PATH . $includeFile)) {
2314 4
                    $className = $this->transformations->getClassName($includeFile);
2315 4
                    if (class_exists($className)) {
2316 4
                        $plugin = new $className();
2317 4
                        if ($plugin instanceof TransformationsPlugin) {
2318 4
                            $transformationPlugin = $plugin;
2319 4
                            $transformOptions = $this->transformations->getOptions(
2320 4
                                $mediaTypeMap[$orgFullColName]['transformation_options'] ?? '',
2321 4
                            );
2322
2323 4
                            $meta->internalMediaType = str_replace(
2324 4
                                '_',
2325 4
                                '/',
2326 4
                                $mediaTypeMap[$orgFullColName]['mimetype'],
2327 4
                            );
2328
                        }
2329
                    }
2330
                }
2331
            }
2332
2333
            // Check whether the field needs to display with syntax highlighting
2334
2335 12
            $dbLower = mb_strtolower($this->properties['db']);
2336 12
            $tblLower = mb_strtolower($meta->orgtable);
2337 12
            $nameLower = mb_strtolower($meta->orgname);
2338
            if (
2339 12
                ! empty($this->transformationInfo[$dbLower][$tblLower][$nameLower])
2340 12
                && isset($row[$i])
2341 12
                && (trim($row[$i]) != '')
2342 12
                && ! $_SESSION['tmpval']['hide_transformation']
2343
            ) {
2344
                /** @psalm-suppress UnresolvableInclude */
2345
                include_once ROOT_PATH . $this->transformationInfo[$dbLower][$tblLower][$nameLower][0];
2346
                $plugin = new $this->transformationInfo[$dbLower][$tblLower][$nameLower][1]();
2347
                if ($plugin instanceof TransformationsPlugin) {
2348
                    $transformationPlugin = $plugin;
2349
                    $transformOptions = $this->transformations->getOptions(
2350
                        $mediaTypeMap[$orgFullColName]['transformation_options'] ?? '',
2351
                    );
2352
2353
                    $orgTable = mb_strtolower($meta->orgtable);
2354
                    $orgName = mb_strtolower($meta->orgname);
2355
2356
                    $meta->internalMediaType = str_replace(
2357
                        '_',
2358
                        '/',
2359
                        $this->transformationInfo[$dbLower][$orgTable][$orgName][2],
2360
                    );
2361
                }
2362
            }
2363
2364
            // Check for the predefined fields need to show as link in schemas
2365 12
            if (! empty($specialSchemaLinks[$dbLower][$tblLower][$nameLower])) {
2366
                $linkingUrl = $this->getSpecialLinkUrl(
2367
                    $specialSchemaLinks[$dbLower][$tblLower][$nameLower],
2368
                    $row[$i],
2369
                    $rowInfo,
2370
                );
2371
                $transformationPlugin = new Text_Plain_Link();
2372
2373
                $transformOptions = [0 => $linkingUrl, 2 => true];
2374
2375
                $meta->internalMediaType = str_replace('_', '/', 'Text/Plain');
2376
            }
2377
2378 12
            $expressions = [];
2379
2380
            if (
2381 12
                isset($statementInfo->statement)
2382 12
                && $statementInfo->statement instanceof SelectStatement
2383
            ) {
2384 12
                $expressions = $statementInfo->statement->expr;
2385
            }
2386
2387
            /**
2388
             * The result set can have columns from more than one table,
2389
             * this is why we have to check for the unique conditions
2390
             * related to this table; however getUniqueCondition() is
2391
             * costly and does not need to be called if we already know
2392
             * the conditions for the current table.
2393
             */
2394 12
            if (! isset($whereClauseMap[$rowNumber][$meta->orgtable])) {
2395 12
                [$uniqueConditions] = Util::getUniqueCondition(
2396 12
                    $this->properties['fields_cnt'],
2397 12
                    $this->properties['fields_meta'],
2398 12
                    $row,
2399 12
                    false,
2400 12
                    $meta->orgtable,
2401 12
                    $expressions,
2402 12
                );
2403 12
                $whereClauseMap[$rowNumber][$meta->orgtable] = $uniqueConditions;
2404
            }
2405
2406 12
            $urlParams = [
2407 12
                'db' => $this->properties['db'],
2408 12
                'table' => $meta->orgtable,
2409 12
                'where_clause_sign' => Core::signSqlQuery($whereClauseMap[$rowNumber][$meta->orgtable]),
2410 12
                'where_clause' => $whereClauseMap[$rowNumber][$meta->orgtable],
2411 12
                'transform_key' => $meta->orgname,
2412 12
            ];
2413
2414 12
            if ($sqlQuery !== '') {
2415 8
                $urlParams['sql_query'] = $urlSqlQuery;
2416
            }
2417
2418 12
            $transformOptions['wrapper_link'] = Url::getCommon($urlParams);
2419 12
            $transformOptions['wrapper_params'] = $urlParams;
2420
2421 12
            $displayParams = $this->properties['display_params'] ?? [];
2422
2423 12
            if ($meta->isNumeric) {
2424
                // n u m e r i c
2425
2426 12
                $displayParams['data'][$rowNumber][$i] = $this->getDataCellForNumericColumns(
2427 12
                    $row[$i] === null ? null : (string) $row[$i],
2428 12
                    'text-end ' . $class,
2429 12
                    $conditionField,
2430 12
                    $meta,
2431 12
                    $map,
2432 12
                    $statementInfo,
2433 12
                    $transformationPlugin,
2434 12
                    $transformOptions,
2435 12
                );
2436 8
            } elseif ($meta->isMappedTypeGeometry) {
2437
                // g e o m e t r y
2438
2439
                // Remove 'grid_edit' from $class as we do not allow to
2440
                // inline-edit geometry data.
2441
                $class = str_replace('grid_edit', '', $class);
2442
2443
                $displayParams['data'][$rowNumber][$i] = $this->getDataCellForGeometryColumns(
2444
                    $row[$i] === null ? null : (string) $row[$i],
2445
                    $class,
2446
                    $meta,
2447
                    $map,
2448
                    $urlParams,
2449
                    $conditionField,
2450
                    $transformationPlugin,
2451
                    $transformOptions,
2452
                    $statementInfo,
2453
                );
2454
            } else {
2455
                // n o t   n u m e r i c
2456
2457 8
                $displayParams['data'][$rowNumber][$i] = $this->getDataCellForNonNumericColumns(
2458 8
                    $row[$i] === null ? null : (string) $row[$i],
2459 8
                    $class,
2460 8
                    $meta,
2461 8
                    $map,
2462 8
                    $urlParams,
2463 8
                    $conditionField,
2464 8
                    $transformationPlugin,
2465 8
                    $transformOptions,
2466 8
                    $statementInfo,
2467 8
                );
2468
            }
2469
2470
            // output stored cell
2471 12
            $rowValuesHtml .= $displayParams['data'][$rowNumber][$i];
2472
2473 12
            if (isset($displayParams['rowdata'][$i][$rowNumber])) {
2474
                $displayParams['rowdata'][$i][$rowNumber] .= $displayParams['data'][$rowNumber][$i];
2475
            } else {
2476 12
                $displayParams['rowdata'][$i][$rowNumber] = $displayParams['data'][$rowNumber][$i];
2477
            }
2478
2479 12
            $this->properties['display_params'] = $displayParams;
2480
        }
2481
2482 12
        return $rowValuesHtml;
2483
    }
2484
2485
    /**
2486
     * Get link for display special schema links
2487
     *
2488
     * @param array<string,array<int,array<string,string>>|string> $linkRelations
2489
     * @param string                                               $columnValue   column value
2490
     * @param mixed[]                                              $rowInfo       information about row
2491
     * @phpstan-param array{
2492
     *                         'link_param': string,
2493
     *                         'link_dependancy_params'?: array<
2494
     *                                                      int,
2495
     *                                                      array{'param_info': string, 'column_name': string}
2496
     *                                                     >,
2497
     *                         'default_page': string
2498
     *                     } $linkRelations
2499
     *
2500
     * @return string generated link
2501
     */
2502 8
    private function getSpecialLinkUrl(
2503
        array $linkRelations,
2504
        string $columnValue,
2505
        array $rowInfo,
2506
    ): string {
2507 8
        $linkingUrlParams = [];
2508
2509 8
        $linkingUrlParams[$linkRelations['link_param']] = $columnValue;
2510
2511 8
        $divider = strpos($linkRelations['default_page'], '?') ? '&' : '?';
0 ignored issues
show
Bug introduced by
It seems like $linkRelations['default_page'] can also be of type array<integer,array<string,string>>; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2511
        $divider = strpos(/** @scrutinizer ignore-type */ $linkRelations['default_page'], '?') ? '&' : '?';
Loading history...
2512 8
        if (empty($linkRelations['link_dependancy_params'])) {
2513
            return $linkRelations['default_page']
2514
                . Url::getCommonRaw($linkingUrlParams, $divider);
2515
        }
2516
2517 8
        foreach ($linkRelations['link_dependancy_params'] as $newParam) {
2518 8
            $columnName = mb_strtolower($newParam['column_name']);
2519
2520
            // If there is a value for this column name in the rowInfo provided
2521 8
            if (isset($rowInfo[$columnName])) {
2522 8
                $linkingUrlParams[$newParam['param_info']] = $rowInfo[$columnName];
2523
            }
2524
2525
            // Special case 1 - when executing routines, according
2526
            // to the type of the routine, url param changes
2527 8
            if (empty($rowInfo['routine_type'])) {
2528
                continue;
2529
            }
2530
        }
2531
2532 8
        return $linkRelations['default_page']
2533 8
            . Url::getCommonRaw($linkingUrlParams, $divider);
2534
    }
2535
2536
    /**
2537
     * Prepare row information for display special links
2538
     *
2539
     * @param mixed[]      $row      current row data
2540
     * @param mixed[]|bool $colOrder the column order
2541
     *
2542
     * @return array<string, mixed> associative array with column nama -> value
2543
     */
2544 16
    private function getRowInfoForSpecialLinks(array $row, array|bool $colOrder): array
2545
    {
2546 16
        $rowInfo = [];
2547 16
        $fieldsMeta = $this->properties['fields_meta'];
2548
2549 16
        for ($n = 0; $n < $this->properties['fields_cnt']; ++$n) {
2550 16
            $m = is_array($colOrder) ? $colOrder[$n] : $n;
2551 16
            $rowInfo[mb_strtolower($fieldsMeta[$m]->orgname)] = $row[$m];
2552
        }
2553
2554 16
        return $rowInfo;
2555
    }
2556
2557
    /**
2558
     * Get url sql query without conditions to shorten URLs
2559
     *
2560
     * @see     getTableBody()
2561
     *
2562
     * @return string analyzed sql query
2563
     */
2564 8
    private function getUrlSqlQuery(StatementInfo $statementInfo): string
2565
    {
2566
        if (
2567 8
            $statementInfo->queryType !== 'SELECT'
2568 8
            || mb_strlen($this->properties['sql_query']) < 200
2569 6
            || $statementInfo->statement === null
2570 8
            || $statementInfo->parser === null
2571
        ) {
2572 8
            return $this->properties['sql_query'];
2573
        }
2574
2575
        $query = 'SELECT ' . Query::getClause($statementInfo->statement, $statementInfo->parser->list, 'SELECT');
2576
2577
        $fromClause = Query::getClause($statementInfo->statement, $statementInfo->parser->list, 'FROM');
2578
2579
        if ($fromClause !== '') {
2580
            $query .= ' FROM ' . $fromClause;
2581
        }
2582
2583
        return $query;
2584
    }
2585
2586
    /**
2587
     * Get column order and column visibility
2588
     *
2589
     * @see    getTableBody()
2590
     *
2591
     * @return mixed[] 2 element array - $col_order, $col_visib
2592
     */
2593 8
    private function getColumnParams(StatementInfo $statementInfo): array
2594
    {
2595 8
        if ($this->isSelect($statementInfo)) {
2596 4
            $pmatable = new Table($this->properties['table'], $this->properties['db'], $this->dbi);
2597 4
            $colOrder = $pmatable->getUiProp(Table::PROP_COLUMN_ORDER);
2598 4
            $fieldsCount = $this->properties['fields_cnt'];
2599
            /* Validate the value */
2600 4
            if (is_array($colOrder)) {
2601
                foreach ($colOrder as $value) {
2602
                    if ($value < $fieldsCount) {
2603
                        continue;
2604
                    }
2605
2606
                    $pmatable->removeUiProp(Table::PROP_COLUMN_ORDER);
2607
                    break;
2608
                }
2609
2610
                if ($fieldsCount !== count($colOrder)) {
2611
                    $pmatable->removeUiProp(Table::PROP_COLUMN_ORDER);
2612
                    $colOrder = false;
2613
                }
2614
            }
2615
2616 4
            $colVisib = $pmatable->getUiProp(Table::PROP_COLUMN_VISIB);
2617 4
            if (is_array($colVisib) && $fieldsCount !== count($colVisib)) {
2618
                $pmatable->removeUiProp(Table::PROP_COLUMN_VISIB);
2619 4
                $colVisib = false;
2620
            }
2621
        } else {
2622 4
            $colOrder = false;
2623 4
            $colVisib = false;
2624
        }
2625
2626 8
        return [$colOrder, $colVisib];
2627
    }
2628
2629
    /**
2630
     * Get HTML for repeating headers
2631
     *
2632
     * @see    getTableBody()
2633
     *
2634
     * @param int      $numEmptyColumnsBefore The number of blank columns before this one
2635
     * @param string[] $descriptions          A list of descriptions
2636
     * @param int      $numEmptyColumnsAfter  The number of blank columns after this one
2637
     *
2638
     * @return string html content
2639
     */
2640
    private function getRepeatingHeaders(
2641
        int $numEmptyColumnsBefore,
2642
        array $descriptions,
2643
        int $numEmptyColumnsAfter,
2644
    ): string {
2645
        $headerHtml = '<tr>' . "\n";
2646
2647
        if ($numEmptyColumnsBefore > 0) {
2648
            $headerHtml .= '    <th colspan="'
2649
                . $numEmptyColumnsBefore . '">'
2650
                . "\n" . '        &nbsp;</th>' . "\n";
2651
        } elseif ($GLOBALS['cfg']['RowActionLinks'] === self::POSITION_NONE) {
2652
            $headerHtml .= '    <th></th>' . "\n";
2653
        }
2654
2655
        $headerHtml .= implode($descriptions);
2656
2657
        if ($numEmptyColumnsAfter > 0) {
2658
            $headerHtml .= '    <th colspan="' . $numEmptyColumnsAfter
2659
                . '">'
2660
                . "\n" . '        &nbsp;</th>' . "\n";
2661
        }
2662
2663
        $headerHtml .= '</tr>' . "\n";
2664
2665
        return $headerHtml;
2666
    }
2667
2668
    /**
2669
     * Get modified links
2670
     *
2671
     * @see     getTableBody()
2672
     *
2673
     * @param string $whereClause    the where clause of the sql
2674
     * @param bool   $clauseIsUnique the unique condition of clause
2675
     * @param string $urlSqlQuery    the analyzed sql query
2676
     *
2677
     * @return array<int,string|array<string, bool|string>>
2678
     */
2679
    private function getModifiedLinks(
2680
        string $whereClause,
2681
        bool $clauseIsUnique,
2682
        string $urlSqlQuery,
2683
    ): array {
2684
        $urlParams = [
2685
            'db' => $this->properties['db'],
2686
            'table' => $this->properties['table'],
2687
            'where_clause' => $whereClause,
2688
            'where_clause_signature' => Core::signSqlQuery($whereClause),
2689
            'clause_is_unique' => $clauseIsUnique,
2690
            'sql_query' => $urlSqlQuery,
2691
            'sql_signature' => Core::signSqlQuery($urlSqlQuery),
2692
            'goto' => Url::getFromRoute('/sql'),
2693
        ];
2694
2695
        $editUrl = Url::getFromRoute('/table/change');
2696
2697
        $copyUrl = Url::getFromRoute('/table/change');
2698
2699
        $editStr = $this->getActionLinkContent(
2700
            'b_edit',
2701
            __('Edit'),
2702
        );
2703
        $copyStr = $this->getActionLinkContent(
2704
            'b_insrow',
2705
            __('Copy'),
2706
        );
2707
2708
        return [$editUrl, $copyUrl, $editStr, $copyStr, $urlParams];
2709
    }
2710
2711
    /**
2712
     * Get delete and kill links
2713
     *
2714
     * @see     getTableBody()
2715
     *
2716
     * @param string $whereClause    the where clause of the sql
2717
     * @param bool   $clauseIsUnique the unique condition of clause
2718
     * @param string $urlSqlQuery    the analyzed sql query
2719
     * @param int    $processId      Process ID
2720
     *
2721
     * @return mixed[]  $del_url, $del_str, $js_conf
2722
     * @psalm-return array{?string, ?string, ?string}
2723
     */
2724
    private function getDeleteAndKillLinks(
2725
        string $whereClause,
2726
        bool $clauseIsUnique,
2727
        string $urlSqlQuery,
2728
        DeleteLinkEnum $deleteLink,
2729
        int $processId,
2730
    ): array {
2731
        $goto = $this->properties['goto'];
2732
2733
        if ($deleteLink === DeleteLinkEnum::DELETE_ROW) { // delete row case
2734
            $urlParams = [
2735
                'db' => $this->properties['db'],
2736
                'table' => $this->properties['table'],
2737
                'sql_query' => $urlSqlQuery,
2738
                'message_to_show' => __('The row has been deleted.'),
2739
                'goto' => $goto ?: Url::getFromRoute('/table/sql'),
2740
            ];
2741
2742
            $linkGoto = Url::getFromRoute('/sql', $urlParams);
2743
2744
            $deleteQuery = 'DELETE FROM '
2745
                . Util::backquote($this->properties['table'])
2746
                . ' WHERE ' . $whereClause
2747
                . ($clauseIsUnique ? '' : ' LIMIT 1');
2748
2749
            $urlParams = [
2750
                'db' => $this->properties['db'],
2751
                'table' => $this->properties['table'],
2752
                'sql_query' => $deleteQuery,
2753
                'message_to_show' => __('The row has been deleted.'),
2754
                'goto' => $linkGoto,
2755
            ];
2756
            $deleteUrl = Url::getFromRoute('/sql');
2757
2758
            $jsConf = 'DELETE FROM ' . $this->properties['table']
2759
                . ' WHERE ' . $whereClause
2760
                . ($clauseIsUnique ? '' : ' LIMIT 1');
2761
2762
            $deleteString = $this->getActionLinkContent('b_drop', __('Delete'));
2763
        } elseif ($deleteLink === DeleteLinkEnum::KILL_PROCESS) { // kill process case
2764
            $urlParams = [
2765
                'db' => $this->properties['db'],
2766
                'table' => $this->properties['table'],
2767
                'sql_query' => $urlSqlQuery,
2768
                'goto' => Url::getFromRoute('/'),
2769
            ];
2770
2771
            $linkGoto = Url::getFromRoute('/sql', $urlParams);
2772
2773
            $kill = $this->dbi->getKillQuery($processId);
2774
2775
            $urlParams = ['db' => 'mysql', 'sql_query' => $kill, 'goto' => $linkGoto];
2776
2777
            $deleteUrl = Url::getFromRoute('/sql');
2778
            $jsConf = $kill;
2779
            $deleteString = Generator::getIcon(
2780
                'b_drop',
2781
                __('Kill'),
2782
            );
2783
        } else {
2784
            $deleteUrl = $deleteString = $jsConf = $urlParams = null;
2785
        }
2786
2787
        return [$deleteUrl, $deleteString, $jsConf, $urlParams];
2788
    }
2789
2790
    /**
2791
     * Get content inside the table row action links (Edit/Copy/Delete)
2792
     *
2793
     * @see     getModifiedLinks(), getDeleteAndKillLinks()
2794
     *
2795
     * @param string $icon        The name of the file to get
2796
     * @param string $displayText The text displaying after the image icon
2797
     */
2798
    private function getActionLinkContent(string $icon, string $displayText): string
2799
    {
2800
        if (
2801
            isset($GLOBALS['cfg']['RowActionType'])
2802
            && $GLOBALS['cfg']['RowActionType'] === self::ACTION_LINK_CONTENT_ICONS
2803
        ) {
2804
            return '<span class="text-nowrap">'
2805
                . Generator::getImage($icon, $displayText)
2806
                . '</span>';
2807
        }
2808
2809
        if (
2810
            isset($GLOBALS['cfg']['RowActionType'])
2811
            && $GLOBALS['cfg']['RowActionType'] === self::ACTION_LINK_CONTENT_TEXT
2812
        ) {
2813
            return '<span class="text-nowrap">' . $displayText . '</span>';
2814
        }
2815
2816
        return Generator::getIcon($icon, $displayText);
2817
    }
2818
2819
    /**
2820
     * Get class for datetime related fields
2821
     *
2822
     * @see    getTableBody()
2823
     *
2824
     * @param FieldMetadata $meta the type of the column field
2825
     *
2826
     * @return string   the class for the column
2827
     */
2828 24
    private function getClassForDateTimeRelatedFields(FieldMetadata $meta): string
2829
    {
2830 24
        $fieldTypeClass = '';
2831
2832 24
        if ($meta->isMappedTypeTimestamp || $meta->isType(FieldMetadata::TYPE_DATETIME)) {
2833 8
            $fieldTypeClass = 'datetimefield';
2834 20
        } elseif ($meta->isType(FieldMetadata::TYPE_DATE)) {
2835 4
            $fieldTypeClass = 'datefield';
2836 16
        } elseif ($meta->isType(FieldMetadata::TYPE_TIME)) {
2837
            $fieldTypeClass = 'timefield';
2838 16
        } elseif ($meta->isType(FieldMetadata::TYPE_STRING)) {
2839 12
            $fieldTypeClass = 'text';
2840
        }
2841
2842 24
        return $fieldTypeClass;
2843
    }
2844
2845
    /**
2846
     * Prepare data cell for numeric type fields
2847
     *
2848
     * @see    getTableBody()
2849
     *
2850
     * @param string|null              $column           the column's value
2851
     * @param string                   $class            the html class for column
2852
     * @param bool                     $conditionField   the column should highlighted or not
2853
     * @param FieldMetadata            $meta             the meta-information about this field
2854
     * @param ForeignKeyRelatedTable[] $map              the list of relations
2855
     * @param mixed[]                  $transformOptions the transformation parameters
2856
     *
2857
     * @return string the prepared cell, html content
2858
     */
2859 12
    private function getDataCellForNumericColumns(
2860
        string|null $column,
2861
        string $class,
2862
        bool $conditionField,
2863
        FieldMetadata $meta,
2864
        array $map,
2865
        StatementInfo $statementInfo,
2866
        TransformationsPlugin|null $transformationPlugin,
2867
        array $transformOptions,
2868
    ): string {
2869 12
        if ($column === null) {
2870
            return $this->buildNullDisplay($class, $conditionField, $meta);
2871
        }
2872
2873 12
        if ($column === '') {
2874
            return $this->buildEmptyDisplay($class, $conditionField, $meta);
2875
        }
2876
2877 12
        $whereComparison = ' = ' . $column;
2878
2879 12
        return $this->getRowData(
2880 12
            $class,
2881 12
            $conditionField,
2882 12
            $statementInfo,
2883 12
            $meta,
2884 12
            $map,
2885 12
            $column,
2886 12
            $column,
2887 12
            $transformationPlugin,
2888 12
            'text-nowrap',
2889 12
            $whereComparison,
2890 12
            $transformOptions,
2891 12
        );
2892
    }
2893
2894
    /**
2895
     * Get data cell for geometry type fields
2896
     *
2897
     * @see     getTableBody()
2898
     *
2899
     * @param string|null              $column           the relevant column in data row
2900
     * @param string                   $class            the html class for column
2901
     * @param FieldMetadata            $meta             the meta-information about this field
2902
     * @param ForeignKeyRelatedTable[] $map              the list of relations
2903
     * @param mixed[]                  $urlParams        the parameters for generate url
2904
     * @param bool                     $conditionField   the column should highlighted or not
2905
     * @param mixed[]                  $transformOptions the transformation parameters
2906
     *
2907
     * @return string the prepared data cell, html content
2908
     */
2909
    private function getDataCellForGeometryColumns(
2910
        string|null $column,
2911
        string $class,
2912
        FieldMetadata $meta,
2913
        array $map,
2914
        array $urlParams,
2915
        bool $conditionField,
2916
        TransformationsPlugin|null $transformationPlugin,
2917
        array $transformOptions,
2918
        StatementInfo $statementInfo,
2919
    ): string {
2920
        if ($column === null) {
2921
            return $this->buildNullDisplay($class, $conditionField, $meta);
2922
        }
2923
2924
        if ($column === '') {
2925
            return $this->buildEmptyDisplay($class, $conditionField, $meta);
2926
        }
2927
2928
        // Display as [GEOMETRY - (size)]
2929
        if ($_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_GEOM) {
2930
            $geometryText = $this->handleNonPrintableContents(
2931
                'GEOMETRY',
2932
                $column,
2933
                $transformationPlugin,
2934
                $transformOptions,
2935
                $meta,
2936
                $urlParams,
2937
            );
2938
2939
            return $this->buildValueDisplay($class, $conditionField, $geometryText);
2940
        }
2941
2942
        if ($_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_WKT) {
2943
            // Prepare in Well Known Text(WKT) format.
2944
            $whereComparison = ' = ' . $column;
2945
2946
            // Convert to WKT format
2947
            $wktval = Gis::convertToWellKnownText($column);
2948
            [
2949
                $isFieldTruncated,
2950
                $displayedColumn,
2951
                // skip 3rd param
2952
            ] = $this->getPartialText($wktval);
2953
2954
            return $this->getRowData(
2955
                $class,
2956
                $conditionField,
2957
                $statementInfo,
2958
                $meta,
2959
                $map,
2960
                $wktval,
2961
                $displayedColumn,
2962
                $transformationPlugin,
2963
                '',
2964
                $whereComparison,
2965
                $transformOptions,
2966
                $isFieldTruncated,
2967
            );
2968
        }
2969
2970
        // Prepare in  Well Known Binary (WKB) format.
2971
2972
        if ($_SESSION['tmpval']['display_binary']) {
2973
            $whereComparison = ' = ' . $column;
2974
2975
            $wkbval = substr(bin2hex($column), 8);
2976
            [
2977
                $isFieldTruncated,
2978
                $displayedColumn,
2979
                // skip 3rd param
2980
            ] = $this->getPartialText($wkbval);
2981
2982
            return $this->getRowData(
2983
                $class,
2984
                $conditionField,
2985
                $statementInfo,
2986
                $meta,
2987
                $map,
2988
                $wkbval,
2989
                $displayedColumn,
2990
                $transformationPlugin,
2991
                '',
2992
                $whereComparison,
2993
                $transformOptions,
2994
                $isFieldTruncated,
2995
            );
2996
        }
2997
2998
        $wkbval = $this->handleNonPrintableContents(
2999
            'BINARY',
3000
            $column,
3001
            $transformationPlugin,
3002
            $transformOptions,
3003
            $meta,
3004
            $urlParams,
3005
        );
3006
3007
        return $this->buildValueDisplay($class, $conditionField, $wkbval);
3008
    }
3009
3010
    /**
3011
     * Get data cell for non numeric type fields
3012
     *
3013
     * @see    getTableBody()
3014
     *
3015
     * @param string|null              $column           the relevant column in data row
3016
     * @param string                   $class            the html class for column
3017
     * @param FieldMetadata            $meta             the meta-information about the field
3018
     * @param ForeignKeyRelatedTable[] $map              the list of relations
3019
     * @param mixed[]                  $urlParams        the parameters for generate url
3020
     * @param bool                     $conditionField   the column should highlighted or not
3021
     * @param mixed[]                  $transformOptions the transformation parameters
3022
     *
3023
     * @return string the prepared data cell, html content
3024
     */
3025 32
    private function getDataCellForNonNumericColumns(
3026
        string|null $column,
3027
        string $class,
3028
        FieldMetadata $meta,
3029
        array $map,
3030
        array $urlParams,
3031
        bool $conditionField,
3032
        TransformationsPlugin|null $transformationPlugin,
3033
        array $transformOptions,
3034
        StatementInfo $statementInfo,
3035
    ): string {
3036 32
        $originalLength = 0;
3037
3038 32
        $isAnalyse = $this->properties['is_analyse'];
3039
3040 32
        $bIsText = $transformationPlugin !== null && ! str_contains($transformationPlugin->getMIMEType(), 'Text');
3041
3042
        // disable inline grid editing
3043
        // if binary fields are protected
3044
        // or transformation plugin is of non text type
3045
        // such as image
3046 32
        $isTypeBlob = $meta->isType(FieldMetadata::TYPE_BLOB);
3047 32
        $cfgProtectBinary = $GLOBALS['cfg']['ProtectBinary'];
3048
        if (
3049 32
            ($meta->isBinary()
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($meta->isBinary() && $c...isTypeBlob) || $bIsText, Probably Intended Meaning: $meta->isBinary() && ($c...isTypeBlob || $bIsText)
Loading history...
3050
            && (
3051 26
                $cfgProtectBinary === 'all'
3052 26
                || ($cfgProtectBinary === 'noblob' && ! $isTypeBlob)
3053 26
                || ($cfgProtectBinary === 'blob' && $isTypeBlob)
3054
                )
3055
            ) || $bIsText
3056
        ) {
3057 4
            $class = str_replace('grid_edit', '', $class);
3058
        }
3059
3060 32
        if ($column === null) {
3061 4
            return $this->buildNullDisplay($class, $conditionField, $meta);
3062
        }
3063
3064 28
        if ($column === '') {
3065
            return $this->buildEmptyDisplay($class, $conditionField, $meta);
3066
        }
3067
3068
        // Cut all fields to $GLOBALS['cfg']['LimitChars']
3069
        // (unless it's a link-type transformation or binary)
3070 28
        $originalDataForWhereClause = $column;
3071 28
        $displayedColumn = $column;
3072 28
        $isFieldTruncated = false;
3073
        if (
3074 28
            ! ($transformationPlugin !== null
3075 28
            && str_contains($transformationPlugin->getName(), 'Link'))
3076 28
            && ! $meta->isBinary()
3077
        ) {
3078 20
            [$isFieldTruncated, $column, $originalLength] = $this->getPartialText($column);
3079
        }
3080
3081 28
        if ($meta->isMappedTypeBit) {
3082
            $displayedColumn = Util::printableBitValue((int) $displayedColumn, $meta->length);
3083
3084
            // some results of PROCEDURE ANALYSE() are reported as
3085
            // being BINARY but they are quite readable,
3086
            // so don't treat them as BINARY
3087 28
        } elseif ($meta->isBinary() && $isAnalyse !== true) {
3088
            // we show the BINARY or BLOB message and field's size
3089
            // (or maybe use a transformation)
3090 8
            $binaryOrBlob = 'BLOB';
3091 8
            if ($meta->isType(FieldMetadata::TYPE_STRING)) {
3092
                $binaryOrBlob = 'BINARY';
3093
            }
3094
3095 8
            $displayedColumn = $this->handleNonPrintableContents(
3096 8
                $binaryOrBlob,
3097 8
                $displayedColumn,
3098 8
                $transformationPlugin,
3099 8
                $transformOptions,
3100 8
                $meta,
3101 8
                $urlParams,
3102 8
                $isFieldTruncated,
3103 8
            );
3104 8
            $class = $this->addClass(
3105 8
                $class,
3106 8
                $conditionField,
3107 8
                $meta,
3108 8
                '',
3109 8
                $isFieldTruncated,
3110 8
                $transformationPlugin !== null,
3111 8
            );
3112 8
            $result = strip_tags($column);
3113
            // disable inline grid editing
3114
            // if binary or blob data is not shown
3115 8
            if (stripos($result, $binaryOrBlob) !== false) {
3116
                $class = str_replace('grid_edit', '', $class);
3117
            }
3118
3119 8
            return $this->buildValueDisplay($class, $conditionField, $displayedColumn);
3120
        }
3121
3122
        // transform functions may enable no-wrapping:
3123 20
        $boolNoWrap = $transformationPlugin !== null
3124 20
            && $transformationPlugin->applyTransformationNoWrap($transformOptions);
3125
3126
        // do not wrap if date field type or if no-wrapping enabled by transform functions
3127
        // otherwise, preserve whitespaces and wrap
3128 20
        $nowrap = $meta->isDateTimeType() || $boolNoWrap ? 'text-nowrap' : 'pre_wrap';
3129
3130 20
        $whereComparison = ' = \''
3131 20
            . $this->dbi->escapeString($originalDataForWhereClause)
3132 20
            . '\'';
3133
3134 20
        return $this->getRowData(
3135 20
            $class,
3136 20
            $conditionField,
3137 20
            $statementInfo,
3138 20
            $meta,
3139 20
            $map,
3140 20
            $column,
3141 20
            $displayedColumn,
3142 20
            $transformationPlugin,
3143 20
            $nowrap,
3144 20
            $whereComparison,
3145 20
            $transformOptions,
3146 20
            $isFieldTruncated,
3147 20
            (string) $originalLength,
3148 20
        );
3149
    }
3150
3151
    /**
3152
     * Checks the posted options for viewing query results
3153
     * and sets appropriate values in the session.
3154
     *
3155
     * @param StatementInfo $analyzedSqlResults the analyzed query results
3156
     *
3157
     * @todo    make maximum remembered queries configurable
3158
     * @todo    move/split into SQL class!?
3159
     * @todo    currently this is called twice unnecessary
3160
     * @todo    ignore LIMIT and ORDER in query!?
3161
     */
3162 20
    public function setConfigParamsForDisplayTable(StatementInfo $analyzedSqlResults): void
3163
    {
3164 20
        $sqlMd5 = md5($this->properties['server'] . $this->properties['db'] . $this->properties['sql_query']);
3165 20
        $query = [];
3166 20
        if (isset($_SESSION['tmpval']['query'][$sqlMd5])) {
3167 8
            $query = $_SESSION['tmpval']['query'][$sqlMd5];
3168
        }
3169
3170 20
        $query['sql'] = $this->properties['sql_query'];
3171
3172 20
        if (empty($query['repeat_cells'])) {
3173 12
            $query['repeat_cells'] = $GLOBALS['cfg']['RepeatCells'];
3174
        }
3175
3176
        // The value can also be from _GET as described on issue #16146 when sorting results
3177 20
        $sessionMaxRows = $_GET['session_max_rows'] ?? $_POST['session_max_rows'] ?? '';
3178
3179 20
        if (is_numeric($sessionMaxRows)) {
3180 4
            $query['max_rows'] = (int) $sessionMaxRows;
3181 4
            unset($_GET['session_max_rows'], $_POST['session_max_rows']);
3182 16
        } elseif ($sessionMaxRows === self::ALL_ROWS) {
3183 4
            $query['max_rows'] = self::ALL_ROWS;
3184 4
            unset($_GET['session_max_rows'], $_POST['session_max_rows']);
3185 12
        } elseif (empty($query['max_rows'])) {
3186 8
            $query['max_rows'] = intval($GLOBALS['cfg']['MaxRows']);
3187
        }
3188
3189 20
        if (isset($_REQUEST['pos']) && is_numeric($_REQUEST['pos'])) {
3190 4
            $query['pos'] = (int) $_REQUEST['pos'];
3191 4
            unset($_REQUEST['pos']);
3192 16
        } elseif (empty($query['pos'])) {
3193 12
            $query['pos'] = 0;
3194
        }
3195
3196
        // Full text is needed in case of explain statements, if not specified.
3197 20
        $fullText = $analyzedSqlResults->isExplain;
3198
3199
        if (
3200 20
            isset($_REQUEST['pftext']) && in_array(
3201 20
                $_REQUEST['pftext'],
3202 20
                [self::DISPLAY_PARTIAL_TEXT, self::DISPLAY_FULL_TEXT],
3203 20
            )
3204
        ) {
3205 8
            $query['pftext'] = $_REQUEST['pftext'];
3206 8
            unset($_REQUEST['pftext']);
3207 12
        } elseif ($fullText) {
3208 4
            $query['pftext'] = self::DISPLAY_FULL_TEXT;
3209 12
        } elseif (empty($query['pftext'])) {
3210 8
            $query['pftext'] = self::DISPLAY_PARTIAL_TEXT;
3211
        }
3212
3213
        if (
3214 20
            isset($_REQUEST['relational_display']) && in_array(
3215 20
                $_REQUEST['relational_display'],
3216 20
                [self::RELATIONAL_KEY, self::RELATIONAL_DISPLAY_COLUMN],
3217 20
            )
3218
        ) {
3219 8
            $query['relational_display'] = $_REQUEST['relational_display'];
3220 8
            unset($_REQUEST['relational_display']);
3221 12
        } elseif (empty($query['relational_display'])) {
3222
            // The current session value has priority over a
3223
            // change via Settings; this change will be apparent
3224
            // starting from the next session
3225 8
            $query['relational_display'] = $GLOBALS['cfg']['RelationalDisplay'];
3226
        }
3227
3228
        if (
3229 20
            isset($_REQUEST['geoOption']) && in_array(
3230 20
                $_REQUEST['geoOption'],
3231 20
                [self::GEOMETRY_DISP_WKT, self::GEOMETRY_DISP_WKB, self::GEOMETRY_DISP_GEOM],
3232 20
            )
3233
        ) {
3234 8
            $query['geoOption'] = $_REQUEST['geoOption'];
3235 8
            unset($_REQUEST['geoOption']);
3236 12
        } elseif (empty($query['geoOption'])) {
3237 8
            $query['geoOption'] = self::GEOMETRY_DISP_GEOM;
3238
        }
3239
3240 20
        if (isset($_REQUEST['display_binary'])) {
3241 4
            $query['display_binary'] = true;
3242 4
            unset($_REQUEST['display_binary']);
3243 16
        } elseif (isset($_REQUEST['display_options_form'])) {
3244
            // we know that the checkbox was unchecked
3245 4
            unset($query['display_binary']);
3246 12
        } elseif (! isset($_REQUEST['full_text_button'])) {
3247
            // selected by default because some operations like OPTIMIZE TABLE
3248
            // and all queries involving functions return "binary" contents,
3249
            // according to low-level field flags
3250 12
            $query['display_binary'] = true;
3251
        }
3252
3253 20
        if (isset($_REQUEST['display_blob'])) {
3254 4
            $query['display_blob'] = true;
3255 4
            unset($_REQUEST['display_blob']);
3256 16
        } elseif (isset($_REQUEST['display_options_form'])) {
3257
            // we know that the checkbox was unchecked
3258 4
            unset($query['display_blob']);
3259
        }
3260
3261 20
        if (isset($_REQUEST['hide_transformation'])) {
3262 4
            $query['hide_transformation'] = true;
3263 4
            unset($_REQUEST['hide_transformation']);
3264 16
        } elseif (isset($_REQUEST['display_options_form'])) {
3265
            // we know that the checkbox was unchecked
3266 4
            unset($query['hide_transformation']);
3267
        }
3268
3269
        // move current query to the last position, to be removed last
3270
        // so only least executed query will be removed if maximum remembered
3271
        // queries limit is reached
3272 20
        unset($_SESSION['tmpval']['query'][$sqlMd5]);
3273 20
        $_SESSION['tmpval']['query'][$sqlMd5] = $query;
3274
3275
        // do not exceed a maximum number of queries to remember
3276 20
        if (count($_SESSION['tmpval']['query']) > 10) {
3277 4
            array_shift($_SESSION['tmpval']['query']);
3278
            //echo 'deleting one element ...';
3279
        }
3280
3281
        // populate query configuration
3282 20
        $_SESSION['tmpval']['pftext'] = $query['pftext'];
3283 20
        $_SESSION['tmpval']['relational_display'] = $query['relational_display'];
3284 20
        $_SESSION['tmpval']['geoOption'] = $query['geoOption'];
3285 20
        $_SESSION['tmpval']['display_binary'] = isset($query['display_binary']);
3286 20
        $_SESSION['tmpval']['display_blob'] = isset($query['display_blob']);
3287 20
        $_SESSION['tmpval']['hide_transformation'] = isset($query['hide_transformation']);
3288 20
        $_SESSION['tmpval']['pos'] = $query['pos'];
3289 20
        $_SESSION['tmpval']['max_rows'] = $query['max_rows'];
3290 20
        $_SESSION['tmpval']['repeat_cells'] = $query['repeat_cells'];
3291
    }
3292
3293
    /**
3294
     * Prepare a table of results returned by a SQL query.
3295
     *
3296
     * @param ResultInterface $dtResult         the link id associated to the query
3297
     *                                 which results have to be displayed
3298
     * @param bool            $isLimitedDisplay With limited operations or not
3299
     *
3300
     * @return string   Generated HTML content for resulted table
3301
     */
3302 8
    public function getTable(
3303
        ResultInterface $dtResult,
3304
        DisplayParts $displayParts,
3305
        StatementInfo $statementInfo,
3306
        bool $isLimitedDisplay = false,
3307
    ): string {
3308
        // The statement this table is built for.
3309 8
        if (isset($statementInfo->statement)) {
3310
            /** @var SelectStatement $statement */
3311 8
            $statement = $statementInfo->statement;
3312
        } else {
3313
            $statement = null;
3314
        }
3315
3316
        // Following variable are needed for use in isset/empty or
3317
        // use with array indexes/safe use in foreach
3318 8
        $fieldsMeta = $this->properties['fields_meta'];
3319 8
        $showTable = $this->properties['showtable'];
3320 8
        $printView = $this->properties['printview'];
3321
3322
        /**
3323
         * @todo move this to a central place
3324
         * @todo for other future table types
3325
         */
3326 8
        $isInnodb = (isset($showTable['Type'])
3327 8
            && $showTable['Type'] === self::TABLE_TYPE_INNO_DB);
3328
3329 8
        if ($isInnodb && Sql::isJustBrowsing($statementInfo, true)) {
3330
            $preCount = '~';
3331
            $afterCount = Generator::showHint(
3332
                Sanitize::sanitizeMessage(
3333
                    __('May be approximate. See [doc@faq3-11]FAQ 3.11[/doc].'),
3334
                ),
3335
            );
3336
        } else {
3337 8
            $preCount = '';
3338 8
            $afterCount = '';
3339
        }
3340
3341
        // 1. ----- Prepares the work -----
3342
3343
        // 1.1 Gets the information about which functionalities should be
3344
        //     displayed
3345
3346 8
        [$displayParts, $total] = $this->setDisplayPartsAndTotal($displayParts);
3347
3348
        // 1.2 Defines offsets for the next and previous pages
3349 8
        $posNext = 0;
3350 8
        $posPrev = 0;
3351 8
        if ($displayParts->hasNavigationBar) {
3352 8
            [$posNext, $posPrev] = $this->getOffsets();
3353
        }
3354
3355
        // 1.3 Extract sorting expressions.
3356
        //     we need $sort_expression and $sort_expression_nodirection
3357
        //     even if there are many table references
3358 8
        $sortExpression = [];
3359 8
        $sortExpressionNoDirection = [];
3360 8
        $sortDirection = [];
3361
3362 8
        if ($statement !== null && ! empty($statement->order)) {
3363 4
            foreach ($statement->order as $o) {
3364 4
                $sortExpression[] = $o->expr->expr . ' ' . $o->type;
3365 4
                $sortExpressionNoDirection[] = $o->expr->expr;
3366 4
                $sortDirection[] = $o->type;
3367
            }
3368
        } else {
3369 4
            $sortExpression[] = '';
3370 4
            $sortExpressionNoDirection[] = '';
3371 4
            $sortDirection[] = '';
3372
        }
3373
3374
        // 1.4 Prepares display of first and last value of the sorted column
3375 8
        $sortedColumnMessage = '';
3376 8
        foreach ($sortExpressionNoDirection as $expression) {
3377 8
            $sortedColumnMessage .= $this->getSortedColumnMessage($dtResult, $expression);
3378
        }
3379
3380
        // 2. ----- Prepare to display the top of the page -----
3381
3382
        // 2.1 Prepares a messages with position information
3383 8
        $sqlQueryMessage = '';
3384 8
        if ($displayParts->hasNavigationBar) {
3385 8
            $message = $this->setMessageInformation(
3386 8
                $sortedColumnMessage,
3387 8
                $statementInfo,
3388 8
                $total,
3389 8
                $posNext,
3390 8
                $preCount,
3391 8
                $afterCount,
3392 8
            );
3393
3394 8
            $sqlQueryMessage = Generator::getMessage($message, $this->properties['sql_query'], 'success');
3395
        } elseif (($printView === null || $printView != '1') && ! $isLimitedDisplay) {
3396
            $sqlQueryMessage = Generator::getMessage(
3397
                __('Your SQL query has been executed successfully.'),
3398
                $this->properties['sql_query'],
3399
                'success',
3400
            );
3401
        }
3402
3403
        // 2.3 Prepare the navigation bars
3404 8
        if ($this->properties['table'] === '' && $statementInfo->queryType === 'SELECT') {
3405
            // table does not always contain a real table name,
3406
            // for example in MySQL 5.0.x, the query SHOW STATUS
3407
            // returns STATUS as a table name
3408
            $this->properties['table'] = $fieldsMeta[0]->table;
3409
        }
3410
3411 8
        $unsortedSqlQuery = '';
3412 8
        $sortByKeyData = [];
3413
        // can the result be sorted?
3414
        if (
3415 8
            $displayParts->hasSortLink
3416 8
            && $statementInfo->statement !== null
3417 8
            && $statementInfo->parser !== null
3418
        ) {
3419 8
            $unsortedSqlQuery = Query::replaceClause(
3420 8
                $statementInfo->statement,
3421 8
                $statementInfo->parser->list,
3422 8
                'ORDER BY',
3423 8
                '',
3424 8
            );
3425
3426
            // Data is sorted by indexes only if there is only one table.
3427 8
            if ($this->isSelect($statementInfo)) {
3428 4
                $sortByKeyData = $this->getSortByKeyDropDown($sortExpression, $unsortedSqlQuery);
3429
            }
3430
        }
3431
3432 8
        $navigation = [];
3433 8
        if ($displayParts->hasNavigationBar && $statement !== null && empty($statement->limit)) {
3434 8
            $navigation = $this->getTableNavigation($posNext, $posPrev, $isInnodb, $sortByKeyData);
3435
        }
3436
3437
        // 2b ----- Get field references from Database -----
3438
        // (see the 'relation' configuration variable)
3439
3440
        // initialize map
3441 8
        $map = [];
3442
3443 8
        if ($this->properties['table'] !== '') {
3444
            // This method set the values for $map array
3445 8
            $map = $this->getForeignKeyRelatedTables();
3446
3447
            // Coming from 'Distinct values' action of structure page
3448
            // We manipulate relations mechanism to show a link to related rows.
3449 8
            if ($this->properties['is_browse_distinct']) {
3450 4
                $map[$fieldsMeta[1]->name] = new ForeignKeyRelatedTable(
3451 4
                    $this->properties['table'],
3452 4
                    $fieldsMeta[1]->name,
3453 4
                    '',
3454 4
                    $this->properties['db'],
3455 4
                );
3456
            }
3457
        }
3458
3459
        // end 2b
3460
3461
        // 3. ----- Prepare the results table -----
3462 8
        $headers = $this->getTableHeaders(
3463 8
            $displayParts,
3464 8
            $statementInfo,
3465 8
            $unsortedSqlQuery,
3466 8
            $sortExpression,
3467 8
            $sortExpressionNoDirection,
3468 8
            $sortDirection,
3469 8
            $isLimitedDisplay,
3470 8
        );
3471
3472 8
        $body = $this->getTableBody($dtResult, $displayParts, $map, $statementInfo, $isLimitedDisplay);
3473
3474 8
        $this->properties['display_params'] = null;
3475
3476
        // 4. ----- Prepares the link for multi-fields edit and delete
3477 8
        $isClauseUnique = $this->isClauseUnique($dtResult, $statementInfo, $displayParts->deleteLink);
3478
3479
        // 5. ----- Prepare "Query results operations"
3480 8
        $operations = [];
3481 8
        if (($printView === null || $printView != '1') && ! $isLimitedDisplay) {
3482 8
            $operations = $this->getResultsOperations($displayParts->hasPrintLink, $statementInfo);
3483
        }
3484
3485 8
        $relationParameters = $this->relation->getRelationParameters();
3486
3487 8
        return $this->template->render('display/results/table', [
3488 8
            'sql_query_message' => $sqlQueryMessage,
3489 8
            'navigation' => $navigation,
3490 8
            'headers' => $headers,
3491 8
            'body' => $body,
3492 8
            'has_bulk_links' => $displayParts->deleteLink === DeleteLinkEnum::DELETE_ROW,
3493 8
            'has_export_button' => $this->hasExportButton($statementInfo, $displayParts->deleteLink),
3494 8
            'clause_is_unique' => $isClauseUnique,
3495 8
            'operations' => $operations,
3496 8
            'db' => $this->properties['db'],
3497 8
            'table' => $this->properties['table'],
3498 8
            'unique_id' => $this->properties['unique_id'],
3499 8
            'sql_query' => $this->properties['sql_query'],
3500 8
            'goto' => $this->properties['goto'],
3501 8
            'unlim_num_rows' => $this->properties['unlim_num_rows'],
3502 8
            'displaywork' => $relationParameters->displayFeature !== null,
3503 8
            'relwork' => $relationParameters->relationFeature !== null,
3504 8
            'save_cells_at_once' => $GLOBALS['cfg']['SaveCellsAtOnce'],
3505 8
            'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'],
3506 8
            'text_dir' => $this->properties['text_dir'],
3507 8
            'is_browse_distinct' => $this->properties['is_browse_distinct'],
3508 8
        ]);
3509
    }
3510
3511
    /**
3512
     * Gets offsets for next page and previous page.
3513
     *
3514
     * @return array<int, int>
3515
     * @psalm-return array{int, int}
3516
     */
3517 16
    private function getOffsets(): array
3518
    {
3519 16
        $tempVal = isset($_SESSION['tmpval']) && is_array($_SESSION['tmpval']) ? $_SESSION['tmpval'] : [];
3520 16
        if (isset($tempVal['max_rows']) && $tempVal['max_rows'] === self::ALL_ROWS) {
3521 4
            return [0, 0];
3522
        }
3523
3524 12
        $pos = isset($tempVal['pos']) && is_int($tempVal['pos']) ? $tempVal['pos'] : 0;
3525 12
        $maxRows = isset($tempVal['max_rows']) && is_int($tempVal['max_rows']) ? $tempVal['max_rows'] : 25;
3526
3527 12
        return [$pos + $maxRows, max(0, $pos - $maxRows)];
3528
    }
3529
3530
    /**
3531
     * Prepare sorted column message
3532
     *
3533
     * @see     getTable()
3534
     *
3535
     * @param ResultInterface $dtResult                  the link id associated to the query
3536
     *                                                   which results have to be displayed
3537
     * @param string|null     $sortExpressionNoDirection sort expression without direction
3538
     */
3539 8
    private function getSortedColumnMessage(
3540
        ResultInterface $dtResult,
3541
        string|null $sortExpressionNoDirection,
3542
    ): string {
3543 8
        $fieldsMeta = $this->properties['fields_meta']; // To use array indexes
3544
3545 8
        if (empty($sortExpressionNoDirection)) {
3546 4
            return '';
3547
        }
3548
3549 4
        if (! str_contains($sortExpressionNoDirection, '.')) {
3550 4
            $sortTable = $this->properties['table'];
3551 4
            $sortColumn = $sortExpressionNoDirection;
3552
        } else {
3553
            [$sortTable, $sortColumn] = explode('.', $sortExpressionNoDirection);
3554
        }
3555
3556 4
        $sortTable = Util::unQuote($sortTable);
3557 4
        $sortColumn = Util::unQuote($sortColumn);
3558
3559
        // find the sorted column index in row result
3560
        // (this might be a multi-table query)
3561 4
        $sortedColumnIndex = false;
3562
3563 4
        foreach ($fieldsMeta as $key => $meta) {
3564 4
            if (($meta->table === $sortTable) && ($meta->name === $sortColumn)) {
3565
                $sortedColumnIndex = $key;
3566
                break;
3567
            }
3568
        }
3569
3570 4
        if ($sortedColumnIndex === false) {
3571 4
            return '';
3572
        }
3573
3574
        // fetch first row of the result set
3575
        $row = $dtResult->fetchRow();
3576
3577
        // check for non printable sorted row data
3578
        $meta = $fieldsMeta[$sortedColumnIndex];
3579
3580
        $isBlobOrGeometryOrBinary = $meta->isType(FieldMetadata::TYPE_BLOB)
3581
                                    || $meta->isMappedTypeGeometry || $meta->isBinary;
3582
3583
        if ($isBlobOrGeometryOrBinary) {
3584
            $columnForFirstRow = $this->handleNonPrintableContents(
3585
                $meta->getMappedType(),
3586
                $row ? $row[$sortedColumnIndex] : '',
3587
                null,
3588
                [],
3589
                $meta,
3590
            );
3591
        } else {
3592
            $columnForFirstRow = $row !== [] ? $row[$sortedColumnIndex] : '';
3593
        }
3594
3595
        $columnForFirstRow = mb_strtoupper(
3596
            mb_substr(
3597
                (string) $columnForFirstRow,
3598
                0,
3599
                (int) $GLOBALS['cfg']['LimitChars'],
3600
            ) . '...',
3601
        );
3602
3603
        // fetch last row of the result set
3604
        $dtResult->seek($this->properties['num_rows'] > 0 ? $this->properties['num_rows'] - 1 : 0);
3605
        $row = $dtResult->fetchRow();
3606
3607
        // check for non printable sorted row data
3608
        $meta = $fieldsMeta[$sortedColumnIndex];
3609
        if ($isBlobOrGeometryOrBinary) {
3610
            $columnForLastRow = $this->handleNonPrintableContents(
3611
                $meta->getMappedType(),
3612
                $row ? $row[$sortedColumnIndex] : '',
3613
                null,
3614
                [],
3615
                $meta,
3616
            );
3617
        } else {
3618
            $columnForLastRow = $row !== [] ? $row[$sortedColumnIndex] : '';
3619
        }
3620
3621
        $columnForLastRow = mb_strtoupper(
3622
            mb_substr(
3623
                (string) $columnForLastRow,
3624
                0,
3625
                (int) $GLOBALS['cfg']['LimitChars'],
3626
            ) . '...',
3627
        );
3628
3629
        // reset to first row for the loop in getTableBody()
3630
        $dtResult->seek(0);
3631
3632
        // we could also use here $sort_expression_nodirection
3633
        return ' [' . htmlspecialchars($sortColumn)
3634
            . ': <strong>' . htmlspecialchars($columnForFirstRow) . ' - '
3635
            . htmlspecialchars($columnForLastRow) . '</strong>]';
3636
    }
3637
3638
    /**
3639
     * Set the content that needs to be shown in message
3640
     *
3641
     * @see     getTable()
3642
     *
3643
     * @param string $sortedColumnMessage the message for sorted column
3644
     * @param int    $total               the total number of rows returned by
3645
     *                                    the SQL query without any
3646
     *                                    programmatically appended LIMIT clause
3647
     * @param int    $posNext             the offset for next page
3648
     * @param string $preCount            the string renders before row count
3649
     * @param string $afterCount          the string renders after row count
3650
     *
3651
     * @return Message an object of Message
3652
     */
3653 8
    private function setMessageInformation(
3654
        string $sortedColumnMessage,
3655
        StatementInfo $statementInfo,
3656
        int $total,
3657
        int $posNext,
3658
        string $preCount,
3659
        string $afterCount,
3660
    ): Message {
3661 8
        $unlimNumRows = $this->properties['unlim_num_rows']; // To use in isset()
3662
3663 8
        if (! empty($statementInfo->statement->limit)) {
3664
            $firstShownRec = $statementInfo->statement->limit->offset;
3665
            $rowCount = $statementInfo->statement->limit->rowCount;
3666
3667
            if ($rowCount < $total) {
3668
                $lastShownRec = $firstShownRec + $rowCount - 1;
3669
            } else {
3670
                $lastShownRec = $firstShownRec + $total - 1;
3671
            }
3672 8
        } elseif (($_SESSION['tmpval']['max_rows'] === self::ALL_ROWS) || ($posNext > $total)) {
3673 8
            $firstShownRec = $_SESSION['tmpval']['pos'];
3674 8
            $lastShownRec = $total - 1;
3675
        } else {
3676
            $firstShownRec = $_SESSION['tmpval']['pos'];
3677
            $lastShownRec = $posNext - 1;
3678
        }
3679
3680 8
        $messageViewWarning = false;
3681 8
        $table = new Table($this->properties['table'], $this->properties['db'], $this->dbi);
3682 8
        if ($table->isView() && ($total == $GLOBALS['cfg']['MaxExactCountViews'])) {
3683
            $message = Message::notice(
3684
                __(
3685
                    'This view has at least this number of rows. Please refer to %sdocumentation%s.',
3686
                ),
3687
            );
3688
3689
            $message->addParam('[doc@cfg_MaxExactCount]');
3690
            $message->addParam('[/doc]');
3691
            $messageViewWarning = Generator::showHint($message->getMessage());
3692
        }
3693
3694 8
        $message = Message::success(__('Showing rows %1s - %2s'));
3695 8
        $message->addParam($firstShownRec);
3696
3697 8
        if ($messageViewWarning !== false) {
3698
            $message->addParamHtml('... ' . $messageViewWarning);
3699
        } else {
3700 8
            $message->addParam($lastShownRec);
3701
        }
3702
3703 8
        $message->addText('(');
3704
3705 8
        if ($messageViewWarning === false) {
3706 8
            if ($unlimNumRows != $total) {
3707
                $messageTotal = Message::notice(
3708
                    $preCount . __('%1$d total, %2$d in query'),
3709
                );
3710
                $messageTotal->addParam($total);
3711
                $messageTotal->addParam($unlimNumRows);
3712
            } else {
3713 8
                $messageTotal = Message::notice($preCount . __('%d total'));
3714 8
                $messageTotal->addParam($total);
3715
            }
3716
3717 8
            if ($afterCount !== '') {
3718
                $messageTotal->addHtml($afterCount);
3719
            }
3720
3721 8
            $message->addMessage($messageTotal, '');
3722
3723 8
            $message->addText(', ', '');
3724
        }
3725
3726 8
        $messageQueryTime = Message::notice(__('Query took %01.4f seconds.') . ')');
3727 8
        $messageQueryTime->addParam($this->properties['querytime']);
3728
3729 8
        $message->addMessage($messageQueryTime, '');
3730 8
        $message->addHtml($sortedColumnMessage, '');
3731
3732 8
        return $message;
3733
    }
3734
3735
    /**
3736
     * Set the value of $map array for linking foreign key related tables
3737
     *
3738
     * @return ForeignKeyRelatedTable[]
3739
     */
3740 8
    private function getForeignKeyRelatedTables(): array
3741
    {
3742
        // To be able to later display a link to the related table,
3743
        // we verify both types of relations: either those that are
3744
        // native foreign keys or those defined in the phpMyAdmin
3745
        // configuration storage. If no PMA storage, we won't be able
3746
        // to use the "column to display" notion (for example show
3747
        // the name related to a numeric id).
3748 8
        $existRel = $this->relation->getForeigners(
3749 8
            $this->properties['db'],
3750 8
            $this->properties['table'],
3751 8
            '',
3752 8
            self::POSITION_BOTH,
3753 8
        );
3754
3755 8
        if ($existRel === []) {
3756
            return [];
3757
        }
3758
3759 8
        $map = [];
3760 8
        foreach ($existRel as $masterField => $rel) {
3761 8
            if ($masterField !== 'foreign_keys_data') {
3762
                $displayField = $this->relation->getDisplayField($rel['foreign_db'], $rel['foreign_table']);
3763
                $map[$masterField] = new ForeignKeyRelatedTable(
3764
                    $rel['foreign_table'],
3765
                    $rel['foreign_field'],
3766
                    $displayField,
3767
                    $rel['foreign_db'],
3768
                );
3769
            } else {
3770 8
                foreach ($rel as $oneKey) {
3771
                    foreach ($oneKey['index_list'] as $index => $oneField) {
3772
                        $displayField = $this->relation->getDisplayField(
3773
                            $oneKey['ref_db_name'] ?? $GLOBALS['db'],
3774
                            $oneKey['ref_table_name'],
3775
                        );
3776
3777
                        $map[$oneField] = new ForeignKeyRelatedTable(
3778
                            $oneKey['ref_table_name'],
3779
                            $oneKey['ref_index_list'][$index],
3780
                            $displayField,
3781
                            $oneKey['ref_db_name'] ?? $GLOBALS['db'],
3782
                        );
3783
                    }
3784
                }
3785
            }
3786
        }
3787
3788 8
        return $map;
3789
    }
3790
3791
    /**
3792
     * Prepare multi field edit/delete links
3793
     *
3794
     * @see     getTable()
3795
     *
3796
     * @param ResultInterface $dtResult the link id associated to the query which results have to be displayed
3797
     */
3798 8
    private function isClauseUnique(
3799
        ResultInterface $dtResult,
3800
        StatementInfo $statementInfo,
3801
        DeleteLinkEnum $deleteLink,
3802
    ): bool {
3803 8
        if ($deleteLink !== DeleteLinkEnum::DELETE_ROW) {
3804 8
            return false;
3805
        }
3806
3807
        // fetch last row of the result set
3808
        $dtResult->seek($this->properties['num_rows'] > 0 ? $this->properties['num_rows'] - 1 : 0);
3809
        $row = $dtResult->fetchRow();
3810
3811
        $expressions = [];
3812
3813
        if ($statementInfo->statement instanceof SelectStatement) {
3814
            $expressions = $statementInfo->statement->expr;
3815
        }
3816
3817
        /**
3818
         * $clause_is_unique is needed by getTable() to generate the proper param
3819
         * in the multi-edit and multi-delete form
3820
         */
3821
        [, $clauseIsUnique] = Util::getUniqueCondition(
3822
            $this->properties['fields_cnt'],
3823
            $this->properties['fields_meta'],
3824
            $row,
3825
            false,
3826
            false,
3827
            $expressions,
3828
        );
3829
3830
        // reset to first row for the loop in getTableBody()
3831
        $dtResult->seek(0);
3832
3833
        return $clauseIsUnique;
3834
    }
3835
3836 8
    private function hasExportButton(StatementInfo $statementInfo, DeleteLinkEnum $deleteLink): bool
3837
    {
3838 8
        return $deleteLink === DeleteLinkEnum::DELETE_ROW && $statementInfo->queryType === 'SELECT';
3839
    }
3840
3841
    /**
3842
     * Get operations that are available on results.
3843
     *
3844
     * @see     getTable()
3845
     *
3846
     * @psalm-return array{
3847
     *   has_export_link: bool,
3848
     *   has_geometry: bool,
3849
     *   has_print_link: bool,
3850
     *   has_procedure: bool,
3851
     *   url_params: array{
3852
     *     db: string,
3853
     *     table: string,
3854
     *     printview: "1",
3855
     *     sql_query: string,
3856
     *     single_table?: "true",
3857
     *     raw_query?: "true",
3858
     *     unlim_num_rows?: int|numeric-string|false
3859
     *   }
3860
     * }
3861
     */
3862 8
    private function getResultsOperations(
3863
        bool $hasPrintLink,
3864
        StatementInfo $statementInfo,
3865
    ): array {
3866 8
        $urlParams = [
3867 8
            'db' => $this->properties['db'],
3868 8
            'table' => $this->properties['table'],
3869 8
            'printview' => '1',
3870 8
            'sql_query' => $this->properties['sql_query'],
3871 8
        ];
3872
3873 8
        $geometryFound = false;
3874
3875
        // Export link
3876
        // (the single_table parameter is used in \PhpMyAdmin\Export->getDisplay()
3877
        //  to hide the SQL and the structure export dialogs)
3878
        // If the parser found a PROCEDURE clause
3879
        // (most probably PROCEDURE ANALYSE()) it makes no sense to
3880
        // display the Export link).
3881
        if (
3882 8
            ($statementInfo->queryType === self::QUERY_TYPE_SELECT)
3883 8
            && ! $statementInfo->isProcedure
3884
        ) {
3885 8
            if (count($statementInfo->selectTables) === 1) {
3886 8
                $urlParams['single_table'] = 'true';
3887
            }
3888
3889
            // In case this query doesn't involve any tables,
3890
            // implies only raw query is to be exported
3891 8
            if (! $statementInfo->selectTables) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $statementInfo->selectTables of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3892
                $urlParams['raw_query'] = 'true';
3893
            }
3894
3895 8
            $urlParams['unlim_num_rows'] = $this->properties['unlim_num_rows'];
3896
3897
            /**
3898
             * At this point we don't know the table name; this can happen
3899
             * for example with a query like
3900
             * SELECT bike_code FROM (SELECT bike_code FROM bikes) tmp
3901
             * As a workaround we set in the table parameter the name of the
3902
             * first table of this database, so that /table/export and
3903
             * the script it calls do not fail
3904
             */
3905 8
            if ($urlParams['table'] === '' && $urlParams['db'] !== '') {
3906
                $urlParams['table'] = (string) $this->dbi->fetchValue('SHOW TABLES');
3907
            }
3908
3909 8
            $fieldsMeta = $this->properties['fields_meta'];
3910 8
            foreach ($fieldsMeta as $meta) {
3911 8
                if ($meta->isMappedTypeGeometry) {
3912
                    $geometryFound = true;
3913
                    break;
3914
                }
3915
            }
3916
        }
3917
3918 8
        return [
3919 8
            'has_procedure' => $statementInfo->isProcedure,
3920 8
            'has_geometry' => $geometryFound,
3921 8
            'has_print_link' => $hasPrintLink,
3922 8
            'has_export_link' => $statementInfo->queryType === self::QUERY_TYPE_SELECT,
3923 8
            'url_params' => $urlParams,
3924 8
        ];
3925
    }
3926
3927
    /**
3928
     * Verifies what to do with non-printable contents (binary or BLOB)
3929
     * in Browse mode.
3930
     *
3931
     * @see getDataCellForGeometryColumns(), getDataCellForNonNumericColumns(), getSortedColumnMessage()
3932
     *
3933
     * @param string        $category         BLOB|BINARY|GEOMETRY
3934
     * @param string|null   $content          the binary content
3935
     * @param mixed[]       $transformOptions transformation parameters
3936
     * @param FieldMetadata $meta             the meta-information about the field
3937
     * @param mixed[]       $urlParams        parameters that should go to the download link
3938
     * @param bool          $isTruncated      the result is truncated or not
3939
     */
3940 28
    private function handleNonPrintableContents(
3941
        string $category,
3942
        string|null $content,
3943
        TransformationsPlugin|null $transformationPlugin,
3944
        array $transformOptions,
3945
        FieldMetadata $meta,
3946
        array $urlParams = [],
3947
        bool &$isTruncated = false,
3948
    ): string {
3949 28
        $isTruncated = false;
3950 28
        $result = '[' . $category;
3951
3952 28
        if ($content !== null) {
3953 24
            $size = strlen($content);
3954 24
            $displaySize = Util::formatByteDown($size, 3, 1);
3955 24
            if ($displaySize !== null) {
3956 24
                $result .= ' - ' . $displaySize[0] . ' ' . $displaySize[1];
3957
            }
3958
        } else {
3959 4
            $result .= ' - NULL';
3960 4
            $size = 0;
3961 4
            $content = '';
3962
        }
3963
3964 28
        $result .= ']';
3965
3966
        // if we want to use a text transformation on a BLOB column
3967 28
        if ($transformationPlugin !== null) {
3968 8
            $posMimeOctetstream = strpos(
3969 8
                $transformationPlugin->getMIMESubtype(),
3970 8
                'Octetstream',
3971 8
            );
3972 8
            $posMimeText = strpos($transformationPlugin->getMIMEType(), 'Text');
3973 8
            if ($posMimeOctetstream || $posMimeText !== false) {
3974
                // Applying Transformations on hex string of binary data
3975
                // seems more appropriate
3976 8
                $result = pack('H*', bin2hex($content));
3977
            }
3978
        }
3979
3980 28
        if ($size <= 0) {
3981 4
            return $result;
3982
        }
3983
3984 24
        if ($transformationPlugin !== null) {
3985 8
            return $transformationPlugin->applyTransformation($result, $transformOptions, $meta);
3986
        }
3987
3988 16
        $result = Core::mimeDefaultFunction($result);
3989
        if (
3990 16
            ($_SESSION['tmpval']['display_binary']
3991 16
            && $meta->isType(FieldMetadata::TYPE_STRING))
3992 16
            || ($_SESSION['tmpval']['display_blob']
3993 16
            && $meta->isType(FieldMetadata::TYPE_BLOB))
3994
        ) {
3995
            // in this case, restart from the original $content
3996
            if (
3997 8
                mb_check_encoding($content, 'utf-8')
3998 8
                && ! preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', $content)
3999
            ) {
4000
                // show as text if it's valid utf-8
4001 4
                $result = htmlspecialchars($content);
4002
            } else {
4003 4
                $result = '0x' . bin2hex($content);
4004
            }
4005
4006 8
            [
4007 8
                $isTruncated,
4008 8
                $result,
4009 8
                // skip 3rd param
4010 8
            ] = $this->getPartialText($result);
4011
        }
4012
4013
        /* Create link to download */
4014
4015 16
        if ($urlParams !== [] && $this->properties['db'] !== '' && $meta->orgtable !== '') {
4016 16
            $urlParams['where_clause_sign'] = Core::signSqlQuery($urlParams['where_clause']);
4017 16
            $result = '<a href="'
4018 16
                . Url::getFromRoute('/table/get-field', $urlParams)
4019 16
                . '" class="disableAjax">'
4020 16
                . $result . '</a>';
4021
        }
4022
4023 16
        return $result;
4024
    }
4025
4026
    /**
4027
     * Retrieves the associated foreign key info for a data cell
4028
     *
4029
     * @param ForeignKeyRelatedTable $fieldInfo       the relation
4030
     * @param string                 $whereComparison data for the where clause
4031
     *
4032
     * @return string|null  formatted data
4033
     */
4034
    private function getFromForeign(ForeignKeyRelatedTable $fieldInfo, string $whereComparison): string|null
4035
    {
4036
        $dispsql = 'SELECT '
4037
            . Util::backquote($fieldInfo->displayField)
4038
            . ' FROM '
4039
            . Util::backquote($fieldInfo->database)
4040
            . '.'
4041
            . Util::backquote($fieldInfo->table)
4042
            . ' WHERE '
4043
            . Util::backquote($fieldInfo->field)
4044
            . $whereComparison;
4045
4046
        $dispval = $this->dbi->fetchValue($dispsql);
4047
        if ($dispval === false) {
4048
            return __('Link not found!');
4049
        }
4050
4051
        if ($dispval === null) {
4052
            return null;
4053
        }
4054
4055
        // Truncate values that are too long, see: #17902
4056
        [, $dispval] = $this->getPartialText($dispval);
4057
4058
        return $dispval;
4059
    }
4060
4061
    /**
4062
     * Prepares the displayable content of a data cell in Browse mode,
4063
     * taking into account foreign key description field and transformations
4064
     *
4065
     * @see     getDataCellForNumericColumns(), getDataCellForGeometryColumns(),
4066
     *          getDataCellForNonNumericColumns(),
4067
     *
4068
     * @param string                   $class            css classes for the td element
4069
     * @param bool                     $conditionField   whether the column is a part of the where clause
4070
     * @param FieldMetadata            $meta             the meta-information about the field
4071
     * @param ForeignKeyRelatedTable[] $map              the list of relations
4072
     * @param string                   $data             data
4073
     * @param string                   $displayedData    data that will be displayed (maybe be chunked)
4074
     * @param string                   $nowrap           'nowrap' if the content should not be wrapped
4075
     * @param string                   $whereComparison  data for the where clause
4076
     * @param mixed[]                  $transformOptions options for transformation
4077
     * @param bool                     $isFieldTruncated whether the field is truncated
4078
     * @param string                   $originalLength   of a truncated column, or ''
4079
     *
4080
     * @return string  formatted data
4081
     */
4082 24
    private function getRowData(
4083
        string $class,
4084
        bool $conditionField,
4085
        StatementInfo $statementInfo,
4086
        FieldMetadata $meta,
4087
        array $map,
4088
        string $data,
4089
        string $displayedData,
4090
        TransformationsPlugin|null $transformationPlugin,
4091
        string $nowrap,
4092
        string $whereComparison,
4093
        array $transformOptions,
4094
        bool $isFieldTruncated = false,
4095
        string $originalLength = '',
4096
    ): string {
4097 24
        $relationalDisplay = $_SESSION['tmpval']['relational_display'];
4098 24
        $printView = $this->properties['printview'];
4099 24
        $value = '';
4100 24
        $tableDataCellClass = $this->addClass(
4101 24
            $class,
4102 24
            $conditionField,
4103 24
            $meta,
4104 24
            $nowrap,
4105 24
            $isFieldTruncated,
4106 24
            $transformationPlugin !== null,
4107 24
        );
4108
4109 24
        if (! empty($statementInfo->statement->expr)) {
4110 12
            foreach ($statementInfo->statement->expr as $expr) {
4111 12
                if (empty($expr->alias) || empty($expr->column)) {
4112 12
                    continue;
4113
                }
4114
4115
                if (strcasecmp($meta->name, $expr->alias) !== 0) {
4116
                    continue;
4117
                }
4118
4119
                $meta->name = $expr->column;
4120
            }
4121
        }
4122
4123 24
        if (isset($map[$meta->name])) {
4124 4
            $relation = $map[$meta->name];
4125
            // Field to display from the foreign table?
4126 4
            $dispval = '';
4127
4128
            // Check that we have a valid column name
4129 4
            if ($relation->displayField !== '') {
4130
                $dispval = $this->getFromForeign($relation, $whereComparison);
4131
            }
4132
4133 4
            if ($printView == '1') {
4134
                if ($transformationPlugin !== null) {
4135
                    $value .= $transformationPlugin->applyTransformation($data, $transformOptions, $meta);
4136
                } else {
4137
                    $value .= Core::mimeDefaultFunction($data);
4138
                }
4139
4140
                $value .= ' <code>[-&gt;' . $dispval . ']</code>';
4141
            } else {
4142 4
                $sqlQuery = 'SELECT * FROM '
4143 4
                    . Util::backquote($relation->database) . '.'
4144 4
                    . Util::backquote($relation->table)
4145 4
                    . ' WHERE '
4146 4
                    . Util::backquote($relation->field)
4147 4
                    . $whereComparison;
4148
4149 4
                $urlParams = [
4150 4
                    'db' => $relation->database,
4151 4
                    'table' => $relation->table,
4152 4
                    'pos' => '0',
4153 4
                    'sql_signature' => Core::signSqlQuery($sqlQuery),
4154 4
                    'sql_query' => $sqlQuery,
4155 4
                ];
4156
4157 4
                if ($transformationPlugin !== null) {
4158
                    // always apply a transformation on the real data,
4159
                    // not on the display field
4160
                    $displayedData = $transformationPlugin->applyTransformation($data, $transformOptions, $meta);
4161 4
                } elseif ($relationalDisplay === self::RELATIONAL_DISPLAY_COLUMN && $relation->displayField !== '') {
4162
                    // user chose "relational display field" in the
4163
                    // display options, so show display field in the cell
4164
                    $displayedData = $dispval === null ? '<em>NULL</em>' : Core::mimeDefaultFunction($dispval);
4165
                } else {
4166
                    // otherwise display data in the cell
4167 4
                    $displayedData = Core::mimeDefaultFunction($displayedData);
4168
                }
4169
4170 4
                if ($relationalDisplay === self::RELATIONAL_KEY) {
4171
                    // user chose "relational key" in the display options, so
4172
                    // the title contains the display field
4173
                    $title = htmlspecialchars($dispval ?? '');
4174
                } else {
4175 4
                    $title = htmlspecialchars($data);
4176
                }
4177
4178 4
                $tagParams = ['title' => $title];
4179 4
                if (str_contains($class, 'grid_edit')) {
4180
                    $tagParams['class'] = 'ajax';
4181
                }
4182
4183 4
                $value .= Generator::linkOrButton(
4184 4
                    Url::getFromRoute('/sql'),
4185 4
                    $urlParams,
4186 4
                    $displayedData,
4187 4
                    $tagParams,
4188 4
                );
4189
            }
4190 24
        } elseif ($transformationPlugin !== null) {
4191 8
            $value .= $transformationPlugin->applyTransformation($data, $transformOptions, $meta);
4192
        } else {
4193 16
            $value .= Core::mimeDefaultFunction($data);
4194
        }
4195
4196 24
        return $this->template->render('display/results/row_data', [
4197 24
            'value' => $value,
4198 24
            'td_class' => $tableDataCellClass,
4199 24
            'decimals' => $meta->decimals,
4200 24
            'type' => $meta->getMappedType(),
4201 24
            'original_length' => $originalLength,
4202 24
        ]);
4203
    }
4204
4205
    /**
4206
     * Truncates given string based on LimitChars configuration
4207
     * and Session pftext variable
4208
     * (string is truncated only if necessary)
4209
     *
4210
     * @see handleNonPrintableContents(), getDataCellForGeometryColumns(), getDataCellForNonNumericColumns
4211
     *
4212
     * @param string $str string to be truncated
4213
     *
4214
     * @return mixed[]
4215
     * @psalm-return array{bool, string, int}
4216
     */
4217 44
    private function getPartialText(string $str): array
4218
    {
4219 44
        $originalLength = mb_strlen($str);
4220
        if (
4221 44
            $originalLength > $GLOBALS['cfg']['LimitChars']
4222 44
            && $_SESSION['tmpval']['pftext'] === self::DISPLAY_PARTIAL_TEXT
4223
        ) {
4224 4
            $str = mb_substr($str, 0, (int) $GLOBALS['cfg']['LimitChars']) . '...';
4225 4
            $truncated = true;
4226
        } else {
4227 40
            $truncated = false;
4228
        }
4229
4230 44
        return [$truncated, $str, $originalLength];
4231
    }
4232
}
4233