Results::getPartialText()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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

1203
            /** @scrutinizer ignore-type */ $statementInfo->statement,
Loading history...
1204 8
            $statementInfo->parser->list,
0 ignored issues
show
Bug introduced by
It seems like $statementInfo->parser->list can also be of type null; however, parameter $list of PhpMyAdmin\SqlParser\Utils\Query::replaceClause() does only seem to accept PhpMyAdmin\SqlParser\TokensList, 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

1204
            /** @scrutinizer ignore-type */ $statementInfo->parser->list,
Loading history...
1205 8
            $singleSortOrder,
1206 8
        );
1207 8
        $multiSortedSqlQuery = Query::replaceClause(
1208 8
            $statementInfo->statement,
1209 8
            $statementInfo->parser->list,
1210 8
            $multiSortOrder,
1211 8
        );
1212
1213 8
        $singleUrlParams = [
1214 8
            'db' => $this->db,
1215 8
            'table' => $this->table,
1216 8
            'sql_query' => $singleSortedSqlQuery,
1217 8
            'sql_signature' => Core::signSqlQuery($singleSortedSqlQuery),
1218 8
            'session_max_rows' => $sessionMaxRows,
1219 8
            'is_browse_distinct' => $this->isBrowseDistinct,
1220 8
        ];
1221
1222 8
        $multiUrlParams = [
1223 8
            'db' => $this->db,
1224 8
            'table' => $this->table,
1225 8
            'sql_query' => $multiSortedSqlQuery,
1226 8
            'sql_signature' => Core::signSqlQuery($multiSortedSqlQuery),
1227 8
            'session_max_rows' => $sessionMaxRows,
1228 8
            'is_browse_distinct' => $this->isBrowseDistinct,
1229 8
        ];
1230
1231
        // Displays the sorting URL
1232
        // enable sort order swapping for image
1233 8
        $orderLink = $this->getSortOrderLink($orderImg, $fieldsMeta, $singleUrlParams, $multiUrlParams);
1234
1235 8
        $orderLink .= $this->getSortOrderHiddenInputs($multiUrlParams, $fieldsMeta->name);
1236
1237 8
        return [
1238 8
            'column_name' => $fieldsMeta->name,
1239 8
            'order_link' => $orderLink,
1240 8
            'comments' => $comments,
1241 8
            'is_browse_pointer_enabled' => $this->config->settings['BrowsePointerEnable'] === true,
1242 8
            'is_browse_marker_enabled' => $this->config->settings['BrowseMarkerEnable'] === true,
1243 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...
1244 8
            'is_column_numeric' => $this->isColumnNumeric($fieldsMeta),
1245 8
        ];
1246
    }
1247
1248
    /**
1249
     * Prepare parameters and html for sorted table header fields
1250
     *
1251
     * @param list<SortExpression> $sortExpressions
1252
     * @param string               $currentTable    The name of the table to which
1253
     *                                           the current column belongs to
1254
     * @param string               $currentColumn   The current column under consideration
1255
     * @param FieldMetadata        $fieldsMeta      set of field properties
1256
     *
1257
     * @return string[]   3 element array - $single_sort_order, $sort_order, $order_img
1258
     */
1259 68
    private function getSingleAndMultiSortUrls(
1260
        array $sortExpressions,
1261
        string $currentTable,
1262
        string $currentColumn,
1263
        FieldMetadata $fieldsMeta,
1264
    ): array {
1265
        // Check if the current column is in the order by clause
1266 68
        $isInSort = $this->isInSorted($sortExpressions, $currentTable, $currentColumn);
1267 68
        if ($sortExpressions === [] || ! $isInSort) {
1268 68
            $tableName = $currentTable === '' ? '' : Util::backquote($currentTable) . '.';
1269 68
            $expression = $tableName . Util::backquote($currentColumn);
1270
            // Set the direction to the config value or perform SMART mode
1271 68
            if ($this->config->settings['Order'] === self::SMART_SORT_ORDER) {
1272 28
                $direction = $fieldsMeta->isDateTimeType()
1273 20
                    ? self::DESCENDING_SORT_DIR
1274 12
                    : self::ASCENDING_SORT_DIR;
1275
            } else {
1276 40
                $direction = $this->config->settings['Order'];
1277
            }
1278
1279 68
            $sortExpressions[] = new SortExpression(
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\Display\SortExpression 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...
1280 68
                $currentTable === '' ? null : $currentTable,
1281 68
                $currentColumn,
1282 68
                $direction,
1283 68
                $expression,
1284 68
            );
1285
        }
1286
1287 68
        $singleSortOrder = '';
1288 68
        $sortOrderColumns = [];
1289 68
        foreach ($sortExpressions as $index => $expression) {
1290
            // If this the first column name in the order by clause add
1291
            // order by clause to the  column name
1292 68
            $sortOrder = $index === 0 ? 'ORDER BY ' : '';
1293
1294 68
            $tableOfColumn = $currentTable;
1295 68
            $column = '';
1296
1297
            // Test to detect if the column name is a standard name
1298
            // Standard name has the table name prefixed to the column name
1299 68
            if ($expression->columnName === null) {
1300
                $sortOrder .= $expression->expression;
1301 68
            } elseif ($expression->tableName !== null) {
1302 60
                $column = $expression->columnName;
1303 60
                $sortOrder .= Util::backquote($expression->tableName) . '.' . Util::backquote($column);
1304 60
                $tableOfColumn = $expression->tableName;
1305
            } else {
1306 8
                $column = $expression->columnName;
1307 8
                $sortOrder .= Util::backquote($column);
1308
            }
1309
1310
            // Incase this is the current column save $single_sort_order
1311 68
            if ($currentColumn === $column && $currentTable === $tableOfColumn) {
1312 68
                $singleSortOrder = 'ORDER BY ';
1313 68
                if ($currentTable !== '') {
1314 60
                    $singleSortOrder .= Util::backquote($currentTable) . '.';
1315
                }
1316
1317 68
                $singleSortOrder .= Util::backquote($currentColumn) . ' ';
1318
1319 68
                if ($isInSort) {
1320 4
                    $singleSortOrder .= match ($expression->direction) {
1321 4
                        self::ASCENDING_SORT_DIR => self::DESCENDING_SORT_DIR,
1322
                        self::DESCENDING_SORT_DIR => self::ASCENDING_SORT_DIR,
1323 4
                    };
1324
                } else {
1325 68
                    $singleSortOrder .= $expression->direction;
1326
                }
1327
            }
1328
1329 68
            $sortOrder .= ' ';
1330 68
            if ($currentColumn === $column && $currentTable === $tableOfColumn && $isInSort) {
1331
                // We need to generate the arrow button and related html
1332 4
                $orderImg = $this->getSortingUrlParams($expression->direction);
1333 4
                $orderImg .= ' <small>' . ($index + 1) . '</small>';
1334 4
                $sortOrder .= match ($expression->direction) {
1335 4
                    self::ASCENDING_SORT_DIR => self::DESCENDING_SORT_DIR,
1336
                    self::DESCENDING_SORT_DIR => self::ASCENDING_SORT_DIR,
1337 4
                };
1338
            } else {
1339 68
                $sortOrder .= $expression->direction;
1340
            }
1341
1342 68
            $sortOrderColumns[] = $sortOrder;
1343
        }
1344
1345 68
        return [$singleSortOrder, implode(', ', $sortOrderColumns), $orderImg ?? ''];
1346
    }
1347
1348
    /**
1349
     * Check whether the column is sorted
1350
     *
1351
     * @see getTableHeaders()
1352
     *
1353
     * @param list<SortExpression> $sortExpressions
1354
     */
1355 68
    private function isInSorted(
1356
        array $sortExpressions,
1357
        string $currentTable,
1358
        string $currentColumn,
1359
    ): bool {
1360 68
        foreach ($sortExpressions as $sortExpression) {
1361 64
            if ($sortExpression->columnName === null) {
1362
                continue;
1363
            }
1364
1365 64
            if ($sortExpression->tableName !== null) {
1366 60
                if ($currentTable === $sortExpression->tableName && $currentColumn === $sortExpression->columnName) {
1367
                    return true;
1368
                }
1369 4
            } elseif ($currentColumn === $sortExpression->columnName) {
1370 4
                return true;
1371
            }
1372
        }
1373
1374 68
        return false;
1375
    }
1376
1377
    /**
1378
     * Get sort url parameters - sort order and order image
1379
     *
1380
     * @see     getSingleAndMultiSortUrls()
1381
     */
1382 4
    private function getSortingUrlParams(string $sortDirection): string
1383
    {
1384 4
        if ($sortDirection === self::DESCENDING_SORT_DIR) {
1385
            $orderImg = ' ' . Generator::getImage(
1386
                's_desc',
1387
                __('Descending'),
1388
                ['class' => 'soimg', 'title' => ''],
1389
            );
1390
            $orderImg .= ' ' . Generator::getImage(
1391
                's_asc',
1392
                __('Ascending'),
1393
                ['class' => 'soimg hide', 'title' => ''],
1394
            );
1395
        } else {
1396 4
            $orderImg = ' ' . Generator::getImage(
1397 4
                's_asc',
1398 4
                __('Ascending'),
1399 4
                ['class' => 'soimg', 'title' => ''],
1400 4
            );
1401 4
            $orderImg .= ' ' . Generator::getImage(
1402 4
                's_desc',
1403 4
                __('Descending'),
1404 4
                ['class' => 'soimg hide', 'title' => ''],
1405 4
            );
1406
        }
1407
1408 4
        return $orderImg;
1409
    }
1410
1411
    /**
1412
     * Get sort order link
1413
     *
1414
     * @see getTableHeaders()
1415
     *
1416
     * @param string                         $orderImg            the sort order image
1417
     * @param FieldMetadata                  $fieldsMeta          set of field properties
1418
     * @param mixed[]                        $orderUrlParams      the url params for sort
1419
     * @param array<string, int|string|bool> $multiOrderUrlParams the url params for sort
1420
     *
1421
     * @return string the sort order link
1422
     */
1423 8
    private function getSortOrderLink(
1424
        string $orderImg,
1425
        FieldMetadata $fieldsMeta,
1426
        array $orderUrlParams,
1427
        array $multiOrderUrlParams,
1428
    ): string {
1429 8
        $urlPath = Url::getFromRoute('/sql', $multiOrderUrlParams, true);
1430 8
        $innerLinkContent = htmlspecialchars($fieldsMeta->name) . $orderImg
1431 8
            . '<input type="hidden" value="'
1432 8
            . $urlPath
1433 8
            . '">';
1434
1435 8
        return Generator::linkOrButton(
1436 8
            Url::getFromRoute('/sql', $orderUrlParams, true),
1437 8
            null,
1438 8
            $innerLinkContent,
1439 8
            ['class' => 'sortlink'],
1440 8
        );
1441
    }
1442
1443
    /** @param array<string, int|string|bool> $multipleUrlParams */
1444 36
    private function getSortOrderHiddenInputs(
1445
        array $multipleUrlParams,
1446
        string $nameToUseInSort,
1447
    ): string {
1448
        /** @var string $sqlQuery */
1449 36
        $sqlQuery = $multipleUrlParams['sql_query'];
1450 36
        $sqlQueryAdd = $sqlQuery;
1451 36
        $parser = new Parser($sqlQuery);
1452
1453 36
        $firstStatement = $parser->statements[0] ?? null;
1454 36
        $numberOfClausesFound = null;
1455 36
        if ($firstStatement instanceof SelectStatement) {
1456 32
            $orderClauses = $firstStatement->order ?? [];
1457 32
            foreach ($orderClauses as $key => $order) {
1458
                // If this is the column name, then remove it from the order clause
1459 28
                if ($order->expr->column !== $nameToUseInSort) {
1460 20
                    continue;
1461
                }
1462
1463
                // remove the order clause for this column and from the counted array
1464 24
                unset($firstStatement->order[$key], $orderClauses[$key]);
1465
            }
1466
1467 32
            $numberOfClausesFound = count($orderClauses);
1468 32
            $sqlQuery = $firstStatement->build();
1469
        }
1470
1471 36
        $multipleUrlParams['sql_query'] = $sqlQuery;
1472 36
        $multipleUrlParams['sql_signature'] = Core::signSqlQuery($sqlQuery);
1473
1474 36
        $urlRemoveOrder = Url::getFromRoute('/sql', $multipleUrlParams);
1475 36
        if ($numberOfClausesFound === 0) {
1476 16
            $urlRemoveOrder .= '&discard_remembered_sort=1';
1477
        }
1478
1479 36
        $multipleUrlParams['sql_query'] = $sqlQueryAdd;
1480 36
        $multipleUrlParams['sql_signature'] = Core::signSqlQuery($sqlQueryAdd);
1481
1482 36
        $urlAddOrder = Url::getFromRoute('/sql', $multipleUrlParams);
1483
1484 36
        return '<input type="hidden" name="url-remove-order" value="' . $urlRemoveOrder . '">' . "\n"
1485 36
             . '<input type="hidden" name="url-add-order" value="' . $urlAddOrder . '">';
1486
    }
1487
1488
    /**
1489
     * Check if the column contains numeric data
1490
     *
1491
     * @param FieldMetadata $fieldsMeta set of field properties
1492
     */
1493 8
    private function isColumnNumeric(FieldMetadata $fieldsMeta): bool
1494
    {
1495
        // This was defined in commit b661cd7c9b31f8bc564d2f9a1b8527e0eb966de8
1496
        // For issue https://github.com/phpmyadmin/phpmyadmin/issues/4746
1497 8
        return $fieldsMeta->isType(FieldMetadata::TYPE_REAL)
1498 8
            || $fieldsMeta->isMappedTypeBit
1499 8
            || $fieldsMeta->isType(FieldMetadata::TYPE_INT);
1500
    }
1501
1502
    /**
1503
     * Prepare column to show at right side - check boxes or empty column
1504
     *
1505
     * @see getTableHeaders()
1506
     *
1507
     * @param string $fullOrPartialTextLink full/partial link or text button
1508
     * @param string $colspan               column span of table header
1509
     *
1510
     * @return string  html content
1511
     */
1512 8
    private function getColumnAtRightSide(
1513
        DisplayParts $displayParts,
1514
        string $fullOrPartialTextLink,
1515
        string $colspan,
1516
    ): string {
1517 8
        $rightColumnHtml = '';
1518
1519
        // Displays the needed checkboxes at the right
1520
        // column of the result table header if possible and required...
1521
        if (
1522 8
            ($this->config->settings['RowActionLinks'] === self::POSITION_RIGHT)
1523 8
            || ($this->config->settings['RowActionLinks'] === self::POSITION_BOTH)
1524 8
            && ($displayParts->hasEditLink || $displayParts->deleteLink !== DeleteLinkEnum::NO_DELETE)
1525 8
            && $displayParts->hasTextButton
1526
        ) {
1527
            $this->numEmptyColumnsAfter = $displayParts->hasEditLink
1528
                && $displayParts->deleteLink !== DeleteLinkEnum::NO_DELETE ? 4 : 1;
1529
1530
            $rightColumnHtml .= "\n"
1531
                . '<th class="column_action position-sticky bg-body d-print-none"' . $colspan . '>'
1532
                . $fullOrPartialTextLink
1533
                . '</th>';
1534
        } elseif (
1535 8
            ($this->config->settings['RowActionLinks'] === self::POSITION_LEFT)
1536 8
            || ($this->config->settings['RowActionLinks'] === self::POSITION_BOTH)
1537 8
            && (! $displayParts->hasEditLink
1538 8
            && $displayParts->deleteLink === DeleteLinkEnum::NO_DELETE)
1539
        ) {
1540
            //     ... elseif no button, displays empty columns if required
1541
            // (unless coming from Browse mode print view)
1542
1543 8
            $this->numEmptyColumnsAfter = $displayParts->hasEditLink
1544 8
                && $displayParts->deleteLink !== DeleteLinkEnum::NO_DELETE ? 4 : 1;
1545
1546 8
            $rightColumnHtml .= "\n" . '<td class="position-sticky bg-body d-print-none"' . $colspan
1547 8
                . '></td>';
1548
        }
1549
1550 8
        return $rightColumnHtml;
1551
    }
1552
1553
    /**
1554
     * Prepares the display for a value
1555
     *
1556
     * @see     getDataCellForGeometryColumns(),
1557
     *          getDataCellForNonNumericColumns()
1558
     *
1559
     * @param string $class          class of table cell
1560
     * @param bool   $conditionField whether to add CSS class condition
1561
     * @param string $value          value to display
1562
     *
1563
     * @return string  the td
1564
     */
1565 12
    private function buildValueDisplay(string $class, bool $conditionField, string $value): string
1566
    {
1567 12
        return $this->template->render('display/results/value_display', [
1568 12
            'class' => $class,
1569 12
            'condition_field' => $conditionField,
1570 12
            'value' => $value,
1571 12
        ]);
1572
    }
1573
1574
    /**
1575
     * Prepares the display for a null value
1576
     *
1577
     * @see     getDataCellForNumericColumns(),
1578
     *          getDataCellForGeometryColumns(),
1579
     *          getDataCellForNonNumericColumns()
1580
     *
1581
     * @param string        $class          class of table cell
1582
     * @param bool          $conditionField whether to add CSS class condition
1583
     * @param FieldMetadata $meta           the meta-information about this field
1584
     *
1585
     * @return string  the td
1586
     */
1587 4
    private function buildNullDisplay(string $class, bool $conditionField, FieldMetadata $meta): string
1588
    {
1589 4
        $classes = $this->addClass($class, $conditionField, $meta, '');
1590
1591 4
        return $this->template->render('display/results/null_display', [
1592 4
            'data_decimals' => $meta->decimals,
1593 4
            'data_type' => $meta->getMappedType(),
1594 4
            'classes' => $classes,
1595 4
        ]);
1596
    }
1597
1598
    /**
1599
     * Prepares the display for an empty value
1600
     *
1601
     * @see     getDataCellForNumericColumns(),
1602
     *          getDataCellForGeometryColumns(),
1603
     *          getDataCellForNonNumericColumns()
1604
     *
1605
     * @param string        $class          class of table cell
1606
     * @param bool          $conditionField whether to add CSS class condition
1607
     * @param FieldMetadata $meta           the meta-information about this field
1608
     *
1609
     * @return string  the td
1610
     */
1611
    private function buildEmptyDisplay(string $class, bool $conditionField, FieldMetadata $meta): string
1612
    {
1613
        $classes = $this->addClass($class, $conditionField, $meta, 'text-nowrap');
1614
1615
        return $this->template->render('display/results/empty_display', ['classes' => $classes]);
1616
    }
1617
1618
    /**
1619
     * Adds the relevant classes.
1620
     *
1621
     * @see buildNullDisplay(), getRowData()
1622
     *
1623
     * @param string        $class            class of table cell
1624
     * @param bool          $conditionField   whether to add CSS class condition
1625
     * @param FieldMetadata $meta             the meta-information about the field
1626
     * @param string        $nowrap           avoid wrapping
1627
     * @param bool          $isFieldTruncated is field truncated (display ...)
1628
     *
1629
     * @return string the list of classes
1630
     */
1631 36
    private function addClass(
1632
        string $class,
1633
        bool $conditionField,
1634
        FieldMetadata $meta,
1635
        string $nowrap,
1636
        bool $isFieldTruncated = false,
1637
        bool $hasTransformationPlugin = false,
1638
    ): string {
1639 36
        $classes = array_filter([$class, $nowrap]);
1640
1641 36
        if ($meta->internalMediaType !== null) {
1642
            // TODO: Is str_replace() necessary here?
1643 4
            $classes[] = str_replace('/', '_', $meta->internalMediaType);
1644
        }
1645
1646 36
        if ($conditionField) {
1647
            $classes[] = 'condition';
1648
        }
1649
1650 36
        if ($isFieldTruncated) {
1651
            $classes[] = 'truncated';
1652
        }
1653
1654 36
        $orgFullColName = $this->db . '.' . $meta->orgtable
1655 36
            . '.' . $meta->orgname;
1656 36
        if ($hasTransformationPlugin || ! empty($this->mediaTypeMap[$orgFullColName]['input_transformation'])) {
1657 12
            $classes[] = 'transformed';
1658
        }
1659
1660
        // Define classes to be added to this data field based on the type of data
1661
1662 36
        if ($meta->isEnum()) {
1663
            $classes[] = 'enum';
1664
        }
1665
1666 36
        if ($meta->isSet()) {
1667
            $classes[] = 'set';
1668
        }
1669
1670 36
        if ($meta->isMappedTypeBit) {
1671
            $classes[] = 'bit';
1672
        }
1673
1674 36
        if ($meta->isBinary()) {
1675 8
            $classes[] = 'hex';
1676
        }
1677
1678 36
        return implode(' ', $classes);
1679
    }
1680
1681
    /**
1682
     * Prepare the body of the results table
1683
     *
1684
     * @see     getTable()
1685
     *
1686
     * @param ResultInterface          $dtResult         the link id associated to the query
1687
     *                                                                     which results have to be displayed
1688
     * @param ForeignKeyRelatedTable[] $map              the list of relations
1689
     * @param bool                     $isLimitedDisplay with limited operations or not
1690
     *
1691
     * @return string  html content
1692
     *
1693
     * @global array  $row                  current row data
1694
     */
1695 8
    private function getTableBody(
1696
        ResultInterface $dtResult,
1697
        DisplayParts $displayParts,
1698
        array $map,
1699
        StatementInfo $statementInfo,
1700
        bool $isLimitedDisplay = false,
1701
    ): string {
1702
        // Mostly because of browser transformations, to make the row-data accessible in a plugin.
1703
1704 8
        $tableBodyHtml = '';
1705
1706
        // query without conditions to shorten URLs when needed, 200 is just
1707
        // guess, it should depend on remaining URL length
1708 8
        $urlSqlQuery = $this->getUrlSqlQuery($statementInfo);
1709
1710 8
        $rowNumber = 0;
1711
1712 8
        $gridEditConfig = 'double-click';
1713
        // If we don't have all the columns of a unique key in the result set, do not permit grid editing.
1714 8
        if ($isLimitedDisplay || ! $this->editable || $this->config->settings['GridEditing'] === 'disabled') {
1715
            $gridEditConfig = 'disabled';
1716 8
        } elseif ($this->config->settings['GridEditing'] === 'click') {
1717
            $gridEditConfig = 'click';
1718
        }
1719
1720
        // prepare to get the column order, if available
1721 8
        [$colOrder, $colVisib] = $this->getColumnParams($statementInfo);
1722
1723
        // Correction University of Virginia 19991216 in the while below
1724
        // Previous code assumed that all tables have keys, specifically that
1725
        // the phpMyAdmin GUI should support row delete/edit only for such
1726
        // tables.
1727
        // Although always using keys is arguably the prescribed way of
1728
        // defining a relational table, it is not required. This will in
1729
        // particular be violated by the novice.
1730
        // We want to encourage phpMyAdmin usage by such novices. So the code
1731
        // below has been changed to conditionally work as before when the
1732
        // table being displayed has one or more keys; but to display
1733
        // delete/edit options correctly for tables without keys.
1734
1735 8
        while (self::$row = $dtResult->fetchRow()) {
0 ignored issues
show
Documentation Bug introduced by
It seems like $dtResult->fetchRow() of type array<integer,null|string> is incompatible with the declared type PhpMyAdmin\Display\list of property $row.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1736
            // add repeating headers
1737
            if (
1738 8
                $rowNumber !== 0 && $_SESSION['tmpval']['repeat_cells'] > 0
1739 8
                && ($rowNumber % $_SESSION['tmpval']['repeat_cells']) === 0
1740
            ) {
1741
                $tableBodyHtml .= $this->getRepeatingHeaders();
1742
            }
1743
1744 8
            $trClass = [];
1745 8
            if ($this->config->settings['BrowsePointerEnable'] != true) {
1746
                $trClass[] = 'nopointer';
1747
            }
1748
1749 8
            if ($this->config->settings['BrowseMarkerEnable'] != true) {
1750
                $trClass[] = 'nomarker';
1751
            }
1752
1753
            // pointer code part
1754 8
            $tableBodyHtml .= '<tr' . ($trClass === [] ? '' : ' class="' . implode(' ', $trClass) . '"') . '>';
1755
1756
            // 1. Prepares the row
1757
1758
            // In print view these variable needs to be initialized
1759 8
            $deleteUrl = null;
1760 8
            $deleteString = null;
1761 8
            $editString = null;
1762 8
            $jsConf = null;
1763 8
            $copyUrl = null;
1764 8
            $copyString = null;
1765 8
            $editUrl = null;
1766 8
            $editCopyUrlParams = [];
1767 8
            $delUrlParams = null;
1768
1769
            // 1.2 Defines the URLs for the modify/delete link(s)
1770
1771
            if (
1772 8
                $displayParts->hasEditLink
1773 8
                || $displayParts->deleteLink !== DeleteLinkEnum::NO_DELETE
1774
            ) {
1775
                $expressions = [];
1776
1777
                if ($statementInfo->statement instanceof SelectStatement) {
1778
                    $expressions = $statementInfo->statement->expr;
1779
                }
1780
1781
                // Results from a "SELECT" statement -> builds the
1782
                // WHERE clause to use in links (a unique key if possible)
1783
                /**
1784
                 * @todo $where_clause could be empty, for example a table
1785
                 *       with only one field and it's a BLOB; in this case,
1786
                 *       avoid to display the delete and edit links
1787
                 */
1788
                $uniqueCondition = new UniqueCondition(
1789
                    $this->fieldsMeta,
1790
                    self::$row,
1791
                    false,
1792
                    $this->table,
1793
                    $expressions,
1794
                );
1795
                $whereClause = $uniqueCondition->getWhereClause();
1796
                $clauseIsUnique = $uniqueCondition->isClauseUnique();
1797
                $conditionArray = $uniqueCondition->getConditionArray();
1798
                $this->whereClauseMap[$rowNumber][$this->table] = $whereClause;
1799
1800
                // 1.2.1 Modify link(s) - update row case
1801
                if ($displayParts->hasEditLink) {
1802
                    $editCopyUrlParams = $this->getUrlParams($whereClause, $clauseIsUnique, $urlSqlQuery);
1803
                    $editUrl = Url::getFromRoute('/table/change');
1804
                    $copyUrl = Url::getFromRoute('/table/change');
1805
                    $editString = $this->getActionLinkContent('b_edit', __('Edit'));
1806
                    $copyString = $this->getActionLinkContent('b_insrow', __('Copy'));
1807
                }
1808
1809
                // 1.2.2 Delete/Kill link(s)
1810
                [$deleteUrl, $deleteString, $jsConf, $delUrlParams] = $this->getDeleteAndKillLinks(
1811
                    $whereClause,
1812
                    $clauseIsUnique,
1813
                    $urlSqlQuery,
1814
                    $displayParts->deleteLink,
1815
                    (int) self::$row[0],
1816
                );
1817
1818
                // 1.3 Displays the links at left if required
1819
                if (
1820
                    $this->config->settings['RowActionLinks'] === self::POSITION_LEFT
1821
                    || $this->config->settings['RowActionLinks'] === self::POSITION_BOTH
1822
                ) {
1823
                    $tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
1824
                        'position' => self::POSITION_LEFT,
1825
                        'has_checkbox' => $deleteUrl && $displayParts->deleteLink !== DeleteLinkEnum::KILL_PROCESS,
1826
                        'edit' => [
1827
                            'url' => $editUrl,
1828
                            'params' => $editCopyUrlParams + ['default_action' => 'update'],
1829
                            'string' => $editString,
1830
                            'clause_is_unique' => $clauseIsUnique,
1831
                        ],
1832
                        'copy' => [
1833
                            'url' => $copyUrl,
1834
                            'params' => $editCopyUrlParams + ['default_action' => 'insert'],
1835
                            'string' => $copyString,
1836
                        ],
1837
                        'delete' => ['url' => $deleteUrl, 'params' => $delUrlParams, 'string' => $deleteString],
1838
                        'row_number' => $rowNumber,
1839
                        'where_clause' => $whereClause,
1840
                        'condition' => json_encode($conditionArray),
1841
                        'is_ajax' => ResponseRenderer::getInstance()->isAjax(),
1842
                        'js_conf' => $jsConf ?? '',
1843
                        'grid_edit_config' => $gridEditConfig,
1844
                    ]);
1845
                } elseif ($this->config->settings['RowActionLinks'] === self::POSITION_NONE) {
1846
                    $tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
1847
                        'position' => self::POSITION_NONE,
1848
                        'has_checkbox' => $deleteUrl && $displayParts->deleteLink !== DeleteLinkEnum::KILL_PROCESS,
1849
                        'edit' => [
1850
                            'url' => $editUrl,
1851
                            'params' => $editCopyUrlParams + ['default_action' => 'update'],
1852
                            'string' => $editString,
1853
                            'clause_is_unique' => $clauseIsUnique,
1854
                        ],
1855
                        'copy' => [
1856
                            'url' => $copyUrl,
1857
                            'params' => $editCopyUrlParams + ['default_action' => 'insert'],
1858
                            'string' => $copyString,
1859
                        ],
1860
                        'delete' => ['url' => $deleteUrl, 'params' => $delUrlParams, 'string' => $deleteString],
1861
                        'row_number' => $rowNumber,
1862
                        'where_clause' => $whereClause,
1863
                        'condition' => json_encode($conditionArray),
1864
                        'is_ajax' => ResponseRenderer::getInstance()->isAjax(),
1865
                        'js_conf' => $jsConf ?? '',
1866
                        'grid_edit_config' => $gridEditConfig,
1867
                    ]);
1868
                }
1869
            }
1870
1871
            // 2. Displays the rows' values
1872 8
            if ($this->mediaTypeMap === []) {
1873 8
                $this->setMimeMap();
1874
            }
1875
1876 8
            $tableBodyHtml .= $this->getRowValues(
1877 8
                self::$row,
1878 8
                $rowNumber,
1879 8
                $colOrder,
1880 8
                $map,
1881 8
                $gridEditConfig,
1882 8
                $colVisib,
1883 8
                $urlSqlQuery,
1884 8
                $statementInfo,
1885 8
            );
1886
1887
            // 3. Displays the modify/delete links on the right if required
1888
            if (
1889 8
                ($displayParts->hasEditLink
1890 8
                    || $displayParts->deleteLink !== DeleteLinkEnum::NO_DELETE)
1891 8
                && ($this->config->settings['RowActionLinks'] === self::POSITION_RIGHT
1892 8
                    || $this->config->settings['RowActionLinks'] === self::POSITION_BOTH)
1893
            ) {
1894
                $tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
1895
                    'position' => self::POSITION_RIGHT,
1896
                    'has_checkbox' => $deleteUrl && $displayParts->deleteLink !== DeleteLinkEnum::KILL_PROCESS,
1897
                    'edit' => [
1898
                        'url' => $editUrl,
1899
                        'params' => $editCopyUrlParams + ['default_action' => 'update'],
1900
                        'string' => $editString,
1901
                        'clause_is_unique' => $clauseIsUnique ?? true,
1902
                    ],
1903
                    'copy' => [
1904
                        'url' => $copyUrl,
1905
                        'params' => $editCopyUrlParams + ['default_action' => 'insert'],
1906
                        'string' => $copyString,
1907
                    ],
1908
                    'delete' => ['url' => $deleteUrl, 'params' => $delUrlParams, 'string' => $deleteString],
1909
                    'row_number' => $rowNumber,
1910
                    'where_clause' => $whereClause ?? '',
1911
                    'condition' => json_encode($conditionArray ?? []),
1912
                    'is_ajax' => ResponseRenderer::getInstance()->isAjax(),
1913
                    'js_conf' => $jsConf ?? '',
1914
                    'grid_edit_config' => $gridEditConfig,
1915
                ]);
1916
            }
1917
1918 8
            $tableBodyHtml .= '</tr>';
1919 8
            $tableBodyHtml .= "\n";
1920 8
            $rowNumber++;
1921
        }
1922
1923 8
        return $tableBodyHtml;
1924
    }
1925
1926
    /**
1927
     * Sets the MIME details of the columns in the results set
1928
     */
1929 8
    private function setMimeMap(): void
1930
    {
1931 8
        $mediaTypeMap = [];
1932 8
        $added = [];
1933 8
        $relationParameters = $this->relation->getRelationParameters();
1934
1935 8
        foreach ($this->fieldsMeta as $field) {
1936 8
            $orgFullTableName = $this->db . '.' . $field->orgtable;
1937
1938
            if (
1939 8
                $relationParameters->columnCommentsFeature === null
1940
                || $relationParameters->browserTransformationFeature === null
1941
                || ! $this->config->settings['BrowseMIME']
1942
                || $_SESSION['tmpval']['hide_transformation']
1943 8
                || ! empty($added[$orgFullTableName])
1944
            ) {
1945 8
                continue;
1946
            }
1947
1948
            $mediaTypeMap = array_merge(
1949
                $mediaTypeMap,
1950
                $this->transformations->getMime($this->db, $field->orgtable, false, true) ?? [],
0 ignored issues
show
Bug introduced by
It seems like $this->transformations->...false, true) ?? array() can also be of type null; however, parameter $arrays of array_merge() does only seem to accept array, 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

1950
                /** @scrutinizer ignore-type */ $this->transformations->getMime($this->db, $field->orgtable, false, true) ?? [],
Loading history...
1951
            );
1952
            $added[$orgFullTableName] = true;
1953
        }
1954
1955
        // special browser transformation for some SHOW statements
1956 8
        if ($this->isShow && ! $_SESSION['tmpval']['hide_transformation']) {
1957
            preg_match(
1958
                '@^SHOW[[:space:]]+(VARIABLES|(FULL[[:space:]]+)?'
1959
                . 'PROCESSLIST|STATUS|TABLE|GRANTS|CREATE|LOGS|DATABASES|FIELDS'
1960
                . ')@i',
1961
                $this->sqlQuery,
1962
                $which,
1963
            );
1964
1965
            if (isset($which[1])) {
1966
                if (str_contains(strtoupper($which[1]), 'PROCESSLIST')) {
1967
                    $mediaTypeMap['..Info'] = [
1968
                        'mimetype' => 'Text_Plain',
1969
                        'transformation' => 'output/Text_Plain_Sql.php',
1970
                    ];
1971
                }
1972
1973
                $isShowCreateTable = preg_match('@CREATE[[:space:]]+TABLE@i', $this->sqlQuery) === 1;
1974
                if ($isShowCreateTable) {
1975
                    $mediaTypeMap['..Create Table'] = [
1976
                        'mimetype' => 'Text_Plain',
1977
                        'transformation' => 'output/Text_Plain_Sql.php',
1978
                    ];
1979
                }
1980
            }
1981
        }
1982
1983 8
        $this->mediaTypeMap = $mediaTypeMap;
1984
    }
1985
1986
    /**
1987
     * Get the values for one data row
1988
     *
1989
     * @see     getTableBody()
1990
     *
1991
     * @param list<string|null>        $row         current row data
1992
     * @param int                      $rowNumber   the index of current row
1993
     * @param mixed[]|false            $colOrder    the column order false when
1994
     *                                             a property not found false
1995
     *                                             when a property not found
1996
     * @param ForeignKeyRelatedTable[] $map         the list of relations
1997
     * @param bool|mixed[]|string      $colVisib    column is visible(false);
1998
     *                                             column isn't visible(string
1999
     *                                             array)
2000
     * @param string                   $urlSqlQuery the analyzed sql query
2001
     * @psalm-param 'double-click'|'click'|'disabled' $gridEditConfig
2002
     *
2003
     * @return string  html content
2004
     */
2005 12
    private function getRowValues(
2006
        array $row,
2007
        int $rowNumber,
2008
        array|false $colOrder,
2009
        array $map,
2010
        string $gridEditConfig,
2011
        bool|array|string $colVisib,
2012
        string $urlSqlQuery,
2013
        StatementInfo $statementInfo,
2014
    ): string {
2015 12
        $rowValuesHtml = '';
2016
2017 12
        $rowInfo = $this->getRowInfoForSpecialLinks($row);
2018
2019 12
        $relationParameters = $this->relation->getRelationParameters();
2020
2021 12
        $columnCount = count($this->fieldsMeta);
2022 12
        for ($currentColumn = 0; $currentColumn < $columnCount; ++$currentColumn) {
2023
            // assign $i with appropriate column order
2024 12
            $i = is_array($colOrder) ? $colOrder[$currentColumn] : $currentColumn;
2025
2026 12
            $meta = $this->fieldsMeta[$i];
2027 12
            $orgFullColName = $this->db . '.' . $meta->orgtable . '.' . $meta->orgname;
2028
2029 12
            $notNullClass = $meta->isNotNull() ? 'not_null' : '';
2030 12
            $relationClass = isset($map[$meta->name]) ? 'relation' : '';
2031 12
            $hideClass = is_array($colVisib) && isset($colVisib[$currentColumn]) && ! $colVisib[$currentColumn]
2032
                ? 'hide'
2033 12
                : '';
2034
2035 12
            $gridEdit = '';
2036 12
            if ($meta->orgtable != '' && $gridEditConfig !== 'disabled') {
2037
                $gridEdit = $gridEditConfig === 'click' ? 'grid_edit click1' : 'grid_edit click2';
2038
            }
2039
2040
            // handle datetime-related class, for grid editing
2041 12
            $fieldTypeClass = $this->getClassForDateTimeRelatedFields($meta);
2042
2043
            // combine all the classes applicable to this column's value
2044 12
            $class = implode(' ', array_filter([
2045 12
                'data',
2046 12
                $gridEdit,
2047 12
                $notNullClass,
2048 12
                $relationClass,
2049 12
                $hideClass,
2050 12
                $fieldTypeClass,
2051 12
            ]));
2052
2053
            //  See if this column should get highlight because it's used in the
2054
            //  where-query.
2055 12
            $conditionField = isset($this->highlightColumns[$meta->name])
2056 12
                || isset($this->highlightColumns[Util::backquote($meta->name)]);
2057
2058 12
            $transformationPlugin = null;
2059 12
            $transformOptions = [];
2060
2061
            if (
2062 12
                $relationParameters->browserTransformationFeature !== null
2063 12
                && $this->config->settings['BrowseMIME']
2064 12
                && isset($this->mediaTypeMap[$orgFullColName]['mimetype'])
2065 12
                && ! empty($this->mediaTypeMap[$orgFullColName]['transformation'])
2066
            ) {
2067 4
                $file = $this->mediaTypeMap[$orgFullColName]['transformation'];
2068 4
                $plugin = $this->transformations->getPluginInstance($file);
2069 4
                if ($plugin instanceof TransformationsInterface) {
2070 4
                    $transformationPlugin = $plugin;
2071 4
                    $transformOptions = $this->transformations->getOptions(
2072 4
                        $this->mediaTypeMap[$orgFullColName]['transformation_options'] ?? '',
2073 4
                    );
2074
2075 4
                    $meta->internalMediaType = $this->mediaTypeMap[$orgFullColName]['mimetype'];
2076
                }
2077
            }
2078
2079
            // Check whether the field needs to display with syntax highlighting
2080
2081 12
            $dbLower = mb_strtolower($this->db);
2082 12
            $tblLower = mb_strtolower($meta->orgtable);
2083 12
            $nameLower = mb_strtolower($meta->orgname);
2084
            if (
2085 12
                isset($this->transformationInfo[$dbLower][$tblLower][$nameLower])
2086 12
                && isset($row[$i])
2087 12
                && trim($row[$i]) !== ''
2088 12
                && ! $_SESSION['tmpval']['hide_transformation']
2089
            ) {
2090
                $transformationPlugin = new $this->transformationInfo[$dbLower][$tblLower][$nameLower]();
2091
                $transformOptions = $this->transformations->getOptions(
2092
                    $this->mediaTypeMap[$orgFullColName]['transformation_options'] ?? '',
2093
                );
2094
2095
                $orgTable = mb_strtolower($meta->orgtable);
2096
                $orgName = mb_strtolower($meta->orgname);
2097
2098
                $meta->internalMediaType = $this->transformationInfo[$dbLower][$orgTable][$orgName]::getMIMEType()
2099
                    . '_' . $this->transformationInfo[$dbLower][$orgTable][$orgName]::getMIMESubtype();
2100
            }
2101
2102 12
            if ($dbLower === 'mysql' || $dbLower === 'information_schema') {
2103
                // Check for the predefined fields need to show as link in schemas
2104
                $specialSchemaLink = SpecialSchemaLinks::get($dbLower, $tblLower, $nameLower);
2105
                if ($specialSchemaLink !== null) {
2106
                    $linkingUrl = $this->getSpecialLinkUrl($specialSchemaLink, $row[$i], $rowInfo);
2107
                    $transformationPlugin = new Text_Plain_Link();
2108
2109
                    $transformOptions = [0 => $linkingUrl, 2 => true];
2110
2111
                    $meta->internalMediaType = 'Text_Plain';
2112
                }
2113
            }
2114
2115 12
            $expressions = [];
2116
2117 12
            if ($statementInfo->statement instanceof SelectStatement) {
2118 12
                $expressions = $statementInfo->statement->expr;
2119
            }
2120
2121
            /**
2122
             * The result set can have columns from more than one table,
2123
             * this is why we have to check for the unique conditions
2124
             * related to this table; however getting UniqueCondition is
2125
             * costly and does not need to be called if we already know
2126
             * the conditions for the current table.
2127
             */
2128 12
            if (! isset($this->whereClauseMap[$rowNumber][$meta->orgtable])) {
2129 12
                $this->whereClauseMap[$rowNumber][$meta->orgtable] = (new UniqueCondition(
2130 12
                    $this->fieldsMeta,
2131 12
                    $row,
2132 12
                    false,
2133 12
                    $meta->orgtable,
2134 12
                    $expressions,
2135 12
                ))->getWhereClause();
2136
            }
2137
2138 12
            $urlParams = [
2139 12
                'db' => $this->db,
2140 12
                'table' => $meta->orgtable,
2141 12
                'where_clause_sign' => Core::signSqlQuery($this->whereClauseMap[$rowNumber][$meta->orgtable]),
2142 12
                'where_clause' => $this->whereClauseMap[$rowNumber][$meta->orgtable],
2143 12
                'transform_key' => $meta->orgname,
2144 12
            ];
2145
2146 12
            if ($this->sqlQuery !== '') {
2147 8
                $urlParams['sql_query'] = $urlSqlQuery;
2148
            }
2149
2150 12
            $transformOptions['wrapper_link'] = Url::getCommon($urlParams);
2151 12
            $transformOptions['wrapper_params'] = $urlParams;
2152
2153 12
            if ($meta->isNumeric) {
2154 12
                $rowValuesHtml .= $this->getDataCellForNumericColumns(
2155 12
                    $row[$i],
2156 12
                    'text-end ' . $class,
2157 12
                    $conditionField,
2158 12
                    $meta,
2159 12
                    $map,
2160 12
                    $statementInfo,
2161 12
                    $transformationPlugin,
2162 12
                    $transformOptions,
2163 12
                );
2164 8
            } elseif ($meta->isMappedTypeGeometry) {
2165
                // Remove 'grid_edit' from $class as we do not allow to
2166
                // inline-edit geometry data.
2167
                $class = str_replace('grid_edit', '', $class);
2168
2169
                $rowValuesHtml .= $this->getDataCellForGeometryColumns(
2170
                    $row[$i],
2171
                    $class,
2172
                    $meta,
2173
                    $map,
2174
                    $urlParams,
2175
                    $conditionField,
2176
                    $transformationPlugin,
2177
                    $transformOptions,
2178
                    $statementInfo,
2179
                );
2180
            } else {
2181 8
                $rowValuesHtml .= $this->getDataCellForNonNumericColumns(
2182 8
                    $row[$i],
2183 8
                    $class,
2184 8
                    $meta,
2185 8
                    $map,
2186 8
                    $urlParams,
2187 8
                    $conditionField,
2188 8
                    $transformationPlugin,
2189 8
                    $transformOptions,
2190 8
                    $statementInfo,
2191 8
                );
2192
            }
2193
        }
2194
2195 12
        return $rowValuesHtml;
2196
    }
2197
2198
    /**
2199
     * Get link for display special schema links
2200
     *
2201
     * @param array<string,array<int,array<string,string>>|string> $linkRelations
2202
     * @param array<string|null>                                   $rowInfo       information about row
2203
     * @phpstan-param array{
2204
     *                         'link_param': string,
2205
     *                         'link_dependancy_params'?: array<
2206
     *                                                      int,
2207
     *                                                      array{'param_info': string, 'column_name': string}
2208
     *                                                     >,
2209
     *                         'default_page': string
2210
     *                     } $linkRelations
2211
     */
2212 8
    private function getSpecialLinkUrl(
2213
        array $linkRelations,
2214
        string|null $columnValue,
2215
        array $rowInfo,
2216
    ): string {
2217 8
        $linkingUrlParams = [];
2218
2219 8
        $linkingUrlParams[$linkRelations['link_param']] = $columnValue;
2220
2221 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

2221
        $divider = strpos(/** @scrutinizer ignore-type */ $linkRelations['default_page'], '?') ? '&' : '?';
Loading history...
2222 8
        if (empty($linkRelations['link_dependancy_params'])) {
2223
            return $linkRelations['default_page']
2224
                . Url::getCommonRaw($linkingUrlParams, $divider);
2225
        }
2226
2227 8
        foreach ($linkRelations['link_dependancy_params'] as $newParam) {
2228 8
            $columnName = mb_strtolower($newParam['column_name']);
2229
2230
            // If there is a value for this column name in the rowInfo provided
2231 8
            if (isset($rowInfo[$columnName])) {
2232 8
                $linkingUrlParams[$newParam['param_info']] = $rowInfo[$columnName];
2233
            }
2234
2235
            // Special case 1 - when executing routines, according
2236
            // to the type of the routine, url param changes
2237 8
            if (empty($rowInfo['routine_type'])) {
2238
                continue;
2239
            }
2240
        }
2241
2242 8
        return $linkRelations['default_page']
2243 8
            . Url::getCommonRaw($linkingUrlParams, $divider);
2244
    }
2245
2246
    /**
2247
     * Prepare row information for display special links
2248
     *
2249
     * @param list<string|null> $row current row data
2250
     *
2251
     * @return array<string|null> associative array with column nama -> value
2252
     */
2253 16
    private function getRowInfoForSpecialLinks(array $row): array
2254
    {
2255 16
        $rowInfo = [];
2256
2257 16
        foreach ($this->fieldsMeta as $m => $field) {
2258 16
            $rowInfo[mb_strtolower($field->orgname)] = $row[$m];
2259
        }
2260
2261 16
        return $rowInfo;
2262
    }
2263
2264
    /**
2265
     * Get url sql query without conditions to shorten URLs
2266
     *
2267
     * @see     getTableBody()
2268
     *
2269
     * @return string analyzed sql query
2270
     */
2271 8
    private function getUrlSqlQuery(StatementInfo $statementInfo): string
2272
    {
2273
        if (
2274 8
            $statementInfo->flags->queryType !== StatementType::Select
2275 8
            || mb_strlen($this->sqlQuery) < 200
2276 8
            || $statementInfo->statement === null
2277
        ) {
2278 8
            return $this->sqlQuery;
2279
        }
2280
2281
        $query = 'SELECT ' . Query::getClause($statementInfo->statement, $statementInfo->parser->list, 'SELECT');
0 ignored issues
show
Bug introduced by
It seems like $statementInfo->parser->list can also be of type null; however, parameter $list of PhpMyAdmin\SqlParser\Utils\Query::getClause() does only seem to accept PhpMyAdmin\SqlParser\TokensList, 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

2281
        $query = 'SELECT ' . Query::getClause($statementInfo->statement, /** @scrutinizer ignore-type */ $statementInfo->parser->list, 'SELECT');
Loading history...
2282
2283
        $fromClause = Query::getClause($statementInfo->statement, $statementInfo->parser->list, 'FROM');
2284
2285
        if ($fromClause !== '') {
2286
            $query .= ' FROM ' . $fromClause;
2287
        }
2288
2289
        return $query;
2290
    }
2291
2292
    /**
2293
     * Get column order and column visibility
2294
     *
2295
     * @see    getTableBody()
2296
     *
2297
     * @return mixed[] 2 element array - $col_order, $col_visib
2298
     */
2299 8
    private function getColumnParams(StatementInfo $statementInfo): array
2300
    {
2301 8
        $colOrder = false;
2302 8
        $colVisib = false;
2303
2304 8
        if ($this->isSelect($statementInfo)) {
2305 4
            $pmatable = new Table($this->table, $this->db, $this->dbi);
2306 4
            $colOrder = $pmatable->getUiProp(UiProperty::ColumnOrder);
2307 4
            $fieldsCount = count($this->fieldsMeta);
2308
            /* Validate the value */
2309 4
            if (is_array($colOrder)) {
2310
                foreach ($colOrder as $value) {
2311
                    if ($value >= $fieldsCount) {
2312
                        $pmatable->removeUiProp(UiProperty::ColumnOrder);
2313
                        break;
2314
                    }
2315
                }
2316
2317
                if ($fieldsCount !== count($colOrder)) {
2318
                    $pmatable->removeUiProp(UiProperty::ColumnOrder);
2319
                    $colOrder = false;
2320
                }
2321
            }
2322
2323 4
            $colVisib = $pmatable->getUiProp(UiProperty::ColumnVisibility);
2324 4
            if (is_array($colVisib) && $fieldsCount !== count($colVisib)) {
2325
                $pmatable->removeUiProp(UiProperty::ColumnVisibility);
2326
                $colVisib = false;
2327
            }
2328
        }
2329
2330 8
        return [$colOrder, $colVisib];
2331
    }
2332
2333
    /**
2334
     * Get HTML for repeating headers
2335
     *
2336
     * @see    getTableBody()
2337
     *
2338
     * @return string html content
2339
     */
2340
    private function getRepeatingHeaders(): string
2341
    {
2342
        $headerHtml = '<tr class="repeating_header_row">' . "\n";
2343
2344
        if ($this->numEmptyColumnsBefore > 0) {
2345
            $headerHtml .= '    <th colspan="'
2346
                . $this->numEmptyColumnsBefore . '">'
2347
                . "\n" . '        &nbsp;</th>' . "\n";
2348
        } elseif ($this->config->settings['RowActionLinks'] === self::POSITION_NONE) {
2349
            $headerHtml .= '    <th></th>' . "\n";
2350
        }
2351
2352
        $headerHtml .= implode($this->descriptions);
2353
2354
        if ($this->numEmptyColumnsAfter > 0) {
2355
            $headerHtml .= '    <th colspan="' . $this->numEmptyColumnsAfter
2356
                . '">'
2357
                . "\n" . '        &nbsp;</th>' . "\n";
2358
        }
2359
2360
        $headerHtml .= '</tr>' . "\n";
2361
2362
        return $headerHtml;
2363
    }
2364
2365
    /** @return (string|bool)[] */
2366
    private function getUrlParams(string $whereClause, bool $clauseIsUnique, string $urlSqlQuery): array
2367
    {
2368
        return [
2369
            'db' => $this->db,
2370
            'table' => $this->table,
2371
            'where_clause' => $whereClause,
2372
            'where_clause_signature' => Core::signSqlQuery($whereClause),
2373
            'clause_is_unique' => $clauseIsUnique,
2374
            'sql_query' => $urlSqlQuery,
2375
            'sql_signature' => Core::signSqlQuery($urlSqlQuery),
2376
            'goto' => Url::getFromRoute('/sql'),
2377
        ];
2378
    }
2379
2380
    /**
2381
     * Get delete and kill links
2382
     *
2383
     * @see     getTableBody()
2384
     *
2385
     * @param string $whereClause    the where clause of the sql
2386
     * @param bool   $clauseIsUnique the unique condition of clause
2387
     * @param string $urlSqlQuery    the analyzed sql query
2388
     * @param int    $processId      Process ID
2389
     *
2390
     * @return array{?string, ?string, ?string, string[]|null}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{?string, ?string, ?string, string[]|null} at position 2 could not be parsed: Expected ':' at position 2, but found '?string'.
Loading history...
2391
     */
2392
    private function getDeleteAndKillLinks(
2393
        string $whereClause,
2394
        bool $clauseIsUnique,
2395
        string $urlSqlQuery,
2396
        DeleteLinkEnum $deleteLink,
2397
        int $processId,
2398
    ): array {
2399
        if ($deleteLink === DeleteLinkEnum::DELETE_ROW) { // delete row case
2400
            $urlParams = [
2401
                'db' => $this->db,
2402
                'table' => $this->table,
2403
                'sql_query' => $urlSqlQuery,
2404
                'message_to_show' => __('The row has been deleted.'),
2405
                'goto' => $this->goto ?: Url::getFromRoute('/table/sql'),
2406
            ];
2407
2408
            $linkGoto = Url::getFromRoute('/sql', $urlParams);
2409
2410
            $deleteQuery = 'DELETE FROM '
2411
                . Util::backquote($this->table)
2412
                . ' WHERE ' . $whereClause
2413
                . ($clauseIsUnique ? '' : ' LIMIT 1');
2414
2415
            $urlParams = [
2416
                'db' => $this->db,
2417
                'table' => $this->table,
2418
                'sql_query' => $deleteQuery,
2419
                'message_to_show' => __('The row has been deleted.'),
2420
                'goto' => $linkGoto,
2421
            ];
2422
            $deleteUrl = Url::getFromRoute('/sql');
2423
2424
            $jsConf = 'DELETE FROM ' . $this->table
2425
                . ' WHERE ' . $whereClause
2426
                . ($clauseIsUnique ? '' : ' LIMIT 1');
2427
2428
            $deleteString = $this->getActionLinkContent('b_drop', __('Delete'));
2429
        } elseif ($deleteLink === DeleteLinkEnum::KILL_PROCESS) { // kill process case
2430
            $urlParams = [
2431
                'db' => $this->db,
2432
                'table' => $this->table,
2433
                'sql_query' => $urlSqlQuery,
2434
                'goto' => Url::getFromRoute('/'),
2435
            ];
2436
2437
            $linkGoto = Url::getFromRoute('/sql', $urlParams);
2438
2439
            $kill = $this->dbi->getKillQuery($processId);
2440
2441
            $urlParams = ['db' => 'mysql', 'sql_query' => $kill, 'goto' => $linkGoto];
2442
2443
            $deleteUrl = Url::getFromRoute('/sql');
2444
            $jsConf = $kill;
2445
            $deleteString = Generator::getIcon(
2446
                'b_drop',
2447
                __('Kill'),
2448
            );
2449
        } else {
2450
            $deleteUrl = $deleteString = $jsConf = $urlParams = null;
2451
        }
2452
2453
        return [$deleteUrl, $deleteString, $jsConf, $urlParams];
2454
    }
2455
2456
    /**
2457
     * Get content inside the table row action links (Edit/Copy/Delete)
2458
     *
2459
     * @see     getDeleteAndKillLinks()
2460
     *
2461
     * @param string $icon        The name of the file to get
2462
     * @param string $displayText The text displaying after the image icon
2463
     */
2464
    private function getActionLinkContent(string $icon, string $displayText): string
2465
    {
2466
        if (
2467
            isset($this->config->settings['RowActionType'])
2468
            && $this->config->settings['RowActionType'] === self::ACTION_LINK_CONTENT_ICONS
2469
        ) {
2470
            return '<span class="text-nowrap">'
2471
                . Generator::getImage($icon, $displayText)
2472
                . '</span>';
2473
        }
2474
2475
        if (
2476
            isset($this->config->settings['RowActionType'])
2477
            && $this->config->settings['RowActionType'] === self::ACTION_LINK_CONTENT_TEXT
2478
        ) {
2479
            return '<span class="text-nowrap">' . $displayText . '</span>';
2480
        }
2481
2482
        return Generator::getIcon($icon, $displayText);
2483
    }
2484
2485
    /**
2486
     * Get class for datetime related fields
2487
     *
2488
     * @see    getTableBody()
2489
     *
2490
     * @param FieldMetadata $meta the type of the column field
2491
     *
2492
     * @return string   the class for the column
2493
     */
2494 24
    private function getClassForDateTimeRelatedFields(FieldMetadata $meta): string
2495
    {
2496 24
        $fieldTypeClass = '';
2497
2498 24
        if ($meta->isMappedTypeTimestamp || $meta->isType(FieldMetadata::TYPE_DATETIME)) {
2499 8
            $fieldTypeClass = 'datetimefield';
2500 20
        } elseif ($meta->isType(FieldMetadata::TYPE_DATE)) {
2501 4
            $fieldTypeClass = 'datefield';
2502 16
        } elseif ($meta->isType(FieldMetadata::TYPE_TIME)) {
2503
            $fieldTypeClass = 'timefield';
2504 16
        } elseif ($meta->isType(FieldMetadata::TYPE_STRING)) {
2505 12
            $fieldTypeClass = 'text';
2506
        }
2507
2508 24
        return $fieldTypeClass;
2509
    }
2510
2511
    /**
2512
     * Prepare data cell for numeric type fields
2513
     *
2514
     * @see    getTableBody()
2515
     *
2516
     * @param string|null              $column           the column's value
2517
     * @param string                   $class            the html class for column
2518
     * @param bool                     $conditionField   the column should highlighted or not
2519
     * @param FieldMetadata            $meta             the meta-information about this field
2520
     * @param ForeignKeyRelatedTable[] $map              the list of relations
2521
     * @param mixed[]                  $transformOptions the transformation parameters
2522
     *
2523
     * @return string the prepared cell, html content
2524
     */
2525 12
    private function getDataCellForNumericColumns(
2526
        string|null $column,
2527
        string $class,
2528
        bool $conditionField,
2529
        FieldMetadata $meta,
2530
        array $map,
2531
        StatementInfo $statementInfo,
2532
        TransformationsInterface|null $transformationPlugin,
2533
        array $transformOptions,
2534
    ): string {
2535 12
        if ($column === null) {
2536
            return $this->buildNullDisplay($class, $conditionField, $meta);
2537
        }
2538
2539 12
        if ($column === '') {
2540
            return $this->buildEmptyDisplay($class, $conditionField, $meta);
2541
        }
2542
2543 12
        $whereComparison = ' = ' . $column;
2544
2545 12
        return $this->getRowData(
2546 12
            $class,
2547 12
            $conditionField,
2548 12
            $statementInfo,
2549 12
            $meta,
2550 12
            $map,
2551 12
            $column,
2552 12
            $column,
2553 12
            $transformationPlugin,
2554 12
            'text-nowrap',
2555 12
            $whereComparison,
2556 12
            $transformOptions,
2557 12
        );
2558
    }
2559
2560
    /**
2561
     * Get data cell for geometry type fields
2562
     *
2563
     * @see     getTableBody()
2564
     *
2565
     * @param string|null              $column           the relevant column in data row
2566
     * @param string                   $class            the html class for column
2567
     * @param FieldMetadata            $meta             the meta-information about this field
2568
     * @param ForeignKeyRelatedTable[] $map              the list of relations
2569
     * @param mixed[]                  $urlParams        the parameters for generate url
2570
     * @param bool                     $conditionField   the column should highlighted or not
2571
     * @param mixed[]                  $transformOptions the transformation parameters
2572
     *
2573
     * @return string the prepared data cell, html content
2574
     */
2575
    private function getDataCellForGeometryColumns(
2576
        string|null $column,
2577
        string $class,
2578
        FieldMetadata $meta,
2579
        array $map,
2580
        array $urlParams,
2581
        bool $conditionField,
2582
        TransformationsInterface|null $transformationPlugin,
2583
        array $transformOptions,
2584
        StatementInfo $statementInfo,
2585
    ): string {
2586
        if ($column === null) {
2587
            return $this->buildNullDisplay($class, $conditionField, $meta);
2588
        }
2589
2590
        if ($column === '') {
2591
            return $this->buildEmptyDisplay($class, $conditionField, $meta);
2592
        }
2593
2594
        // Display as [GEOMETRY - (size)]
2595
        if ($_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_GEOM) {
2596
            $geometryText = $this->handleNonPrintableContents(
2597
                'GEOMETRY',
2598
                $column,
2599
                $transformationPlugin,
2600
                $transformOptions,
2601
                $meta,
2602
                $urlParams,
2603
            );
2604
2605
            return $this->buildValueDisplay($class, $conditionField, $geometryText);
2606
        }
2607
2608
        if ($_SESSION['tmpval']['geoOption'] === self::GEOMETRY_DISP_WKT) {
2609
            // Prepare in Well Known Text(WKT) format.
2610
            $whereComparison = ' = ' . $column;
2611
2612
            // Convert to WKT format
2613
            $wktval = Gis::convertToWellKnownText($column);
2614
            $displayedColumn = $this->getPartialText($wktval);
2615
2616
            return $this->getRowData(
2617
                $class,
2618
                $conditionField,
2619
                $statementInfo,
2620
                $meta,
2621
                $map,
2622
                $wktval,
2623
                $displayedColumn,
2624
                $transformationPlugin,
2625
                '',
2626
                $whereComparison,
2627
                $transformOptions,
2628
                $displayedColumn !== $wktval,
2629
            );
2630
        }
2631
2632
        // Prepare in  Well Known Binary (WKB) format.
2633
2634
        if ($_SESSION['tmpval']['display_binary']) {
2635
            $whereComparison = ' = ' . $column;
2636
2637
            $wkbval = substr(bin2hex($column), 8);
2638
            $displayedColumn = $this->getPartialText($wkbval);
2639
2640
            return $this->getRowData(
2641
                $class,
2642
                $conditionField,
2643
                $statementInfo,
2644
                $meta,
2645
                $map,
2646
                $wkbval,
2647
                $displayedColumn,
2648
                $transformationPlugin,
2649
                '',
2650
                $whereComparison,
2651
                $transformOptions,
2652
                $displayedColumn !== $wkbval,
2653
            );
2654
        }
2655
2656
        $wkbval = $this->handleNonPrintableContents(
2657
            'BINARY',
2658
            $column,
2659
            $transformationPlugin,
2660
            $transformOptions,
2661
            $meta,
2662
            $urlParams,
2663
        );
2664
2665
        return $this->buildValueDisplay($class, $conditionField, $wkbval);
2666
    }
2667
2668
    /**
2669
     * Get data cell for non numeric type fields
2670
     *
2671
     * @see    getTableBody()
2672
     *
2673
     * @param string|null              $column           the relevant column in data row
2674
     * @param string                   $class            the html class for column
2675
     * @param FieldMetadata            $meta             the meta-information about the field
2676
     * @param ForeignKeyRelatedTable[] $map              the list of relations
2677
     * @param mixed[]                  $urlParams        the parameters for generate url
2678
     * @param bool                     $conditionField   the column should highlighted or not
2679
     * @param mixed[]                  $transformOptions the transformation parameters
2680
     *
2681
     * @return string the prepared data cell, html content
2682
     */
2683 32
    private function getDataCellForNonNumericColumns(
2684
        string|null $column,
2685
        string $class,
2686
        FieldMetadata $meta,
2687
        array $map,
2688
        array $urlParams,
2689
        bool $conditionField,
2690
        TransformationsInterface|null $transformationPlugin,
2691
        array $transformOptions,
2692
        StatementInfo $statementInfo,
2693
    ): string {
2694 32
        $bIsText = $transformationPlugin !== null && ! str_contains($transformationPlugin::getMIMEType(), 'Text');
2695
2696
        // disable inline grid editing
2697
        // if binary fields are protected
2698
        // or transformation plugin is of non text type
2699
        // such as image
2700 32
        $isTypeBlob = $meta->isType(FieldMetadata::TYPE_BLOB);
2701 32
        $cfgProtectBinary = $this->config->settings['ProtectBinary'];
2702
        if (
2703 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...
2704
            && (
2705 8
                $cfgProtectBinary === 'all'
2706 8
                || ($cfgProtectBinary === 'noblob' && ! $isTypeBlob)
2707 8
                || ($cfgProtectBinary === 'blob' && $isTypeBlob)
2708
                )
2709
            ) || $bIsText
2710
        ) {
2711 4
            $class = str_replace('grid_edit', '', $class);
2712
        }
2713
2714 32
        if ($column === null) {
2715 4
            return $this->buildNullDisplay($class, $conditionField, $meta);
2716
        }
2717
2718 28
        if ($column === '') {
2719
            return $this->buildEmptyDisplay($class, $conditionField, $meta);
2720
        }
2721
2722
        // Cut all fields to \PhpMyAdmin\Config::getInstance()->settings['LimitChars']
2723
        // (unless it's a link-type transformation or binary)
2724 28
        $originalLength = 0;
2725 28
        $originalDataForWhereClause = $column;
2726 28
        $displayedColumn = $column;
2727 28
        $isFieldTruncated = false;
2728
        if (
2729 28
            ! ($transformationPlugin !== null && str_contains($transformationPlugin::getName(), 'Link'))
2730 28
            && ! $meta->isBinary()
2731
        ) {
2732 20
            $originalLength = mb_strlen($displayedColumn);
2733 20
            $column = $this->getPartialText($displayedColumn);
2734 20
            $isFieldTruncated = $column !== $displayedColumn;
2735
        }
2736
2737 28
        if ($meta->isMappedTypeBit) {
2738
            $displayedColumn = Util::printableBitValue((int) $displayedColumn, $meta->length);
2739
2740
            // some results of PROCEDURE ANALYSE() are reported as
2741
            // being BINARY but they are quite readable,
2742
            // so don't treat them as BINARY
2743 28
        } elseif ($meta->isBinary() && ! $this->isAnalyse) {
2744
            // we show the BINARY or BLOB message and field's size
2745
            // (or maybe use a transformation)
2746 8
            $binaryOrBlob = 'BLOB';
2747 8
            if ($meta->isType(FieldMetadata::TYPE_STRING)) {
2748
                $binaryOrBlob = 'BINARY';
2749
            }
2750
2751 8
            $displayedColumn = $this->handleNonPrintableContents(
2752 8
                $binaryOrBlob,
2753 8
                $displayedColumn,
2754 8
                $transformationPlugin,
2755 8
                $transformOptions,
2756 8
                $meta,
2757 8
                $urlParams,
2758 8
                $isFieldTruncated,
2759 8
            );
2760 8
            $class = $this->addClass(
2761 8
                $class,
2762 8
                $conditionField,
2763 8
                $meta,
2764 8
                '',
2765 8
                $isFieldTruncated,
2766 8
                $transformationPlugin !== null,
2767 8
            );
2768 8
            $result = strip_tags($column);
2769
            // disable inline grid editing
2770
            // if binary or blob data is not shown
2771 8
            if (stripos($result, $binaryOrBlob) !== false) {
2772
                $class = str_replace('grid_edit', '', $class);
2773
            }
2774
2775 8
            return $this->buildValueDisplay($class, $conditionField, $displayedColumn);
2776
        }
2777
2778
        // transform functions may enable no-wrapping:
2779 20
        $boolNoWrap = $transformationPlugin !== null
2780 20
            && $transformationPlugin->applyTransformationNoWrap($transformOptions);
2781
2782
        // do not wrap if date field type or if no-wrapping enabled by transform functions
2783
        // otherwise, preserve whitespaces and wrap
2784 20
        $nowrap = $meta->isDateTimeType() || $boolNoWrap ? 'text-nowrap' : 'pre_wrap';
2785
2786 20
        $whereComparison = ' = ' . $this->dbi->quoteString($originalDataForWhereClause);
2787
2788 20
        return $this->getRowData(
2789 20
            $class,
2790 20
            $conditionField,
2791 20
            $statementInfo,
2792 20
            $meta,
2793 20
            $map,
2794 20
            $column,
2795 20
            $displayedColumn,
2796 20
            $transformationPlugin,
2797 20
            $nowrap,
2798 20
            $whereComparison,
2799 20
            $transformOptions,
2800 20
            $isFieldTruncated,
2801 20
            (string) $originalLength,
2802 20
        );
2803
    }
2804
2805
    /**
2806
     * Checks the posted options for viewing query results
2807
     * and sets appropriate values in the session.
2808
     *
2809
     * @todo    make maximum remembered queries configurable
2810
     * @todo    move/split into SQL class!?
2811
     * @todo    currently this is called twice unnecessary
2812
     * @todo    ignore LIMIT and ORDER in query!?
2813
     */
2814 20
    public function setConfigParamsForDisplayTable(StatementInfo $statementInfo): void
2815
    {
2816 20
        $sqlMd5 = md5($this->server . $this->db . $this->sqlQuery);
2817 20
        $query = $_SESSION['tmpval']['query'][$sqlMd5] ?? [];
2818
2819 20
        $query['sql'] = $this->sqlQuery;
2820
2821 20
        if (empty($query['repeat_cells'])) {
2822 12
            $query['repeat_cells'] = $this->config->settings['RepeatCells'];
2823
        }
2824
2825
        // The value can also be from _GET as described on issue #16146 when sorting results
2826 20
        $sessionMaxRows = $_GET['session_max_rows'] ?? $_POST['session_max_rows'] ?? '';
2827
2828 20
        if (is_numeric($sessionMaxRows)) {
2829 4
            $query['max_rows'] = (int) $sessionMaxRows;
2830 4
            unset($_GET['session_max_rows'], $_POST['session_max_rows']);
2831 16
        } elseif ($sessionMaxRows === self::ALL_ROWS) {
2832 4
            $query['max_rows'] = self::ALL_ROWS;
2833 4
            unset($_GET['session_max_rows'], $_POST['session_max_rows']);
2834 12
        } elseif (empty($query['max_rows'])) {
2835 8
            $query['max_rows'] = (int) $this->config->settings['MaxRows'];
2836
        }
2837
2838 20
        if (isset($_REQUEST['pos']) && is_numeric($_REQUEST['pos'])) {
2839 4
            $query['pos'] = (int) $_REQUEST['pos'];
2840 4
            unset($_REQUEST['pos']);
2841 16
        } elseif (empty($query['pos'])) {
2842 12
            $query['pos'] = 0;
2843
        }
2844
2845
        // Full text is needed in case of explain statements, if not specified.
2846 20
        $fullText = $statementInfo->flags->queryType === StatementType::Explain;
2847
2848
        if (
2849 20
            isset($_REQUEST['pftext']) && in_array(
2850 20
                $_REQUEST['pftext'],
2851 20
                [self::DISPLAY_PARTIAL_TEXT, self::DISPLAY_FULL_TEXT],
2852 20
                true,
2853 20
            )
2854
        ) {
2855 8
            $query['pftext'] = $_REQUEST['pftext'];
2856 8
            unset($_REQUEST['pftext']);
2857 12
        } elseif ($fullText) {
2858 4
            $query['pftext'] = self::DISPLAY_FULL_TEXT;
2859 12
        } elseif (empty($query['pftext'])) {
2860 8
            $query['pftext'] = self::DISPLAY_PARTIAL_TEXT;
2861
        }
2862
2863
        if (
2864 20
            isset($_REQUEST['relational_display']) && in_array(
2865 20
                $_REQUEST['relational_display'],
2866 20
                [self::RELATIONAL_KEY, self::RELATIONAL_DISPLAY_COLUMN],
2867 20
                true,
2868 20
            )
2869
        ) {
2870 8
            $query['relational_display'] = $_REQUEST['relational_display'];
2871 8
            unset($_REQUEST['relational_display']);
2872 12
        } elseif (empty($query['relational_display'])) {
2873
            // The current session value has priority over a
2874
            // change via Settings; this change will be apparent
2875
            // starting from the next session
2876 8
            $query['relational_display'] = $this->config->settings['RelationalDisplay'];
2877
        }
2878
2879
        if (
2880 20
            isset($_REQUEST['geoOption']) && in_array(
2881 20
                $_REQUEST['geoOption'],
2882 20
                [self::GEOMETRY_DISP_WKT, self::GEOMETRY_DISP_WKB, self::GEOMETRY_DISP_GEOM],
2883 20
                true,
2884 20
            )
2885
        ) {
2886 8
            $query['geoOption'] = $_REQUEST['geoOption'];
2887 8
            unset($_REQUEST['geoOption']);
2888 12
        } elseif (empty($query['geoOption'])) {
2889 8
            $query['geoOption'] = self::GEOMETRY_DISP_GEOM;
2890
        }
2891
2892 20
        if (isset($_REQUEST['display_binary'])) {
2893 4
            $query['display_binary'] = true;
2894 4
            unset($_REQUEST['display_binary']);
2895 16
        } elseif (isset($_REQUEST['display_options_form'])) {
2896
            // we know that the checkbox was unchecked
2897 4
            unset($query['display_binary']);
2898 12
        } elseif (! isset($_REQUEST['full_text_button'])) {
2899
            // selected by default because some operations like OPTIMIZE TABLE
2900
            // and all queries involving functions return "binary" contents,
2901
            // according to low-level field flags
2902 12
            $query['display_binary'] = true;
2903
        }
2904
2905 20
        if (isset($_REQUEST['display_blob'])) {
2906 4
            $query['display_blob'] = true;
2907 4
            unset($_REQUEST['display_blob']);
2908 16
        } elseif (isset($_REQUEST['display_options_form'])) {
2909
            // we know that the checkbox was unchecked
2910 4
            unset($query['display_blob']);
2911
        }
2912
2913 20
        if (isset($_REQUEST['hide_transformation'])) {
2914 4
            $query['hide_transformation'] = true;
2915 4
            unset($_REQUEST['hide_transformation']);
2916 16
        } elseif (isset($_REQUEST['display_options_form'])) {
2917
            // we know that the checkbox was unchecked
2918 4
            unset($query['hide_transformation']);
2919
        }
2920
2921
        // move current query to the last position, to be removed last
2922
        // so only least executed query will be removed if maximum remembered
2923
        // queries limit is reached
2924 20
        unset($_SESSION['tmpval']['query'][$sqlMd5]);
2925 20
        $_SESSION['tmpval']['query'][$sqlMd5] = $query;
2926
2927
        // do not exceed a maximum number of queries to remember
2928 20
        if (count($_SESSION['tmpval']['query']) > 10) {
2929 4
            array_shift($_SESSION['tmpval']['query']);
2930
            //echo 'deleting one element ...';
2931
        }
2932
2933
        // populate query configuration
2934 20
        $_SESSION['tmpval']['pftext'] = $query['pftext'];
2935 20
        $_SESSION['tmpval']['relational_display'] = $query['relational_display'];
2936 20
        $_SESSION['tmpval']['geoOption'] = $query['geoOption'];
2937 20
        $_SESSION['tmpval']['display_binary'] = isset($query['display_binary']);
2938 20
        $_SESSION['tmpval']['display_blob'] = isset($query['display_blob']);
2939 20
        $_SESSION['tmpval']['hide_transformation'] = isset($query['hide_transformation']);
2940 20
        $_SESSION['tmpval']['pos'] = $query['pos'];
2941 20
        $_SESSION['tmpval']['max_rows'] = $query['max_rows'];
2942 20
        $_SESSION['tmpval']['repeat_cells'] = $query['repeat_cells'];
2943
    }
2944
2945
    /**
2946
     * Prepare a table of results returned by a SQL query.
2947
     *
2948
     * @param ResultInterface $dtResult         the link id associated to the query
2949
     *                                 which results have to be displayed
2950
     * @param bool            $isLimitedDisplay With limited operations or not
2951
     *
2952
     * @return string   Generated HTML content for resulted table
2953
     */
2954 8
    public function getTable(
2955
        ResultInterface $dtResult,
2956
        DisplayParts $displayParts,
2957
        StatementInfo $statementInfo,
2958
        bool $isLimitedDisplay = false,
2959
    ): string {
2960
        // The statement this table is built for.
2961
        /** @var SelectStatement|null $statement */
2962 8
        $statement = $statementInfo->statement;
2963
2964 8
        $preCount = '';
2965 8
        $afterCount = '';
2966
2967
        // 1. ----- Prepares the work -----
2968
2969
        // 1.1 Gets the information about which functionalities should be
2970
        //     displayed
2971
2972 8
        [$displayParts, $total] = $this->setDisplayPartsAndTotal($displayParts);
2973
2974
        // 1.2 Defines offsets for the next and previous pages
2975 8
        $posNext = 0;
2976 8
        $posPrev = 0;
2977 8
        if ($displayParts->hasNavigationBar) {
2978 8
            [$posNext, $posPrev] = $this->getOffsets();
2979
        }
2980
2981
        // 1.3 Extract sorting expressions.
2982
        //     we need $sort_expression and $sort_expression_nodirection
2983
        //     even if there are many table references
2984 8
        $sortExpressions = [];
2985
2986 8
        if ($statement !== null && ! empty($statement->order)) {
2987 4
            $sortExpressions = $this->extractSortingExpressions($statement->order);
2988
        }
2989
2990
        // 1.4 Prepares display of first and last value of the sorted column
2991 8
        $sortedColumnMessage = '';
2992 8
        foreach ($sortExpressions as $expression) {
2993 4
            $sortedColumnMessage .= $this->getSortedColumnMessage($dtResult, $expression->expression);
2994
        }
2995
2996
        // 2. ----- Prepare to display the top of the page -----
2997
2998
        // 2.1 Prepares a messages with position information
2999 8
        $sqlQueryMessage = '';
3000 8
        if ($displayParts->hasNavigationBar) {
3001 8
            $message = $this->setMessageInformation(
3002 8
                $sortedColumnMessage,
3003 8
                $statementInfo,
3004 8
                $total,
3005 8
                $posNext,
3006 8
                $preCount,
3007 8
                $afterCount,
3008 8
            );
3009
3010 8
            $sqlQueryMessage = Generator::getMessage($message, $this->sqlQuery, MessageType::Success);
3011
        } elseif (! $this->printView && ! $isLimitedDisplay) {
3012
            $sqlQueryMessage = Generator::getMessage(
3013
                __('Your SQL query has been executed successfully.'),
3014
                $this->sqlQuery,
3015
                MessageType::Success,
3016
            );
3017
        }
3018
3019
        // 2.3 Prepare the navigation bars
3020 8
        if ($this->table === '' && $statementInfo->flags->queryType === StatementType::Select) {
3021
            // table does not always contain a real table name,
3022
            // for example in MySQL 5.0.x, the query SHOW STATUS
3023
            // returns STATUS as a table name
3024
            $this->table = $this->fieldsMeta[0]->table;
3025
        }
3026
3027 8
        $sortByKeyData = [];
3028
        // can the result be sorted?
3029 8
        if ($displayParts->hasSortLink && $statementInfo->statement !== null) {
3030 8
            $unsortedSqlQuery = Query::replaceClause(
3031 8
                $statementInfo->statement,
3032 8
                $statementInfo->parser->list,
0 ignored issues
show
Bug introduced by
It seems like $statementInfo->parser->list can also be of type null; however, parameter $list of PhpMyAdmin\SqlParser\Utils\Query::replaceClause() does only seem to accept PhpMyAdmin\SqlParser\TokensList, 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

3032
                /** @scrutinizer ignore-type */ $statementInfo->parser->list,
Loading history...
3033 8
                'ORDER BY',
3034 8
                '',
3035 8
            );
3036
3037
            // Data is sorted by indexes only if there is only one table.
3038 8
            if ($this->isSelect($statementInfo)) {
3039 4
                $sortExpression = [];
3040 4
                foreach ($sortExpressions as $expression) {
3041
                    $sortExpression[] = $expression->expression . ' ' . $expression->direction;
3042
                }
3043
3044 4
                $sortByKeyData = $this->getSortByKeyDropDown($sortExpression, $unsortedSqlQuery);
3045
            }
3046
        }
3047
3048 8
        $navigation = [];
3049 8
        if ($displayParts->hasNavigationBar && $statement !== null && empty($statement->limit)) {
3050 8
            $navigation = $this->getTableNavigation($posNext, $posPrev, $sortByKeyData);
3051
        }
3052
3053
        // 2b ----- Get field references from Database -----
3054
        // (see the 'relation' configuration variable)
3055
3056
        // initialize map
3057 8
        $map = [];
3058
3059 8
        if ($this->table !== '') {
3060
            // This method set the values for $map array
3061 8
            $map = $this->getForeignKeyRelatedTables();
3062
3063
            // Coming from 'Distinct values' action of structure page
3064
            // We manipulate relations mechanism to show a link to related rows.
3065 8
            if ($this->isBrowseDistinct) {
3066 4
                $map[$this->fieldsMeta[1]->name] = new ForeignKeyRelatedTable(
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\Display\ForeignKeyRelatedTable 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...
3067 4
                    $this->table,
3068 4
                    $this->fieldsMeta[1]->name,
3069 4
                    '',
3070 4
                    $this->db,
3071 4
                );
3072
            }
3073
        }
3074
3075
        // end 2b
3076
3077
        // 3. ----- Prepare the results table -----
3078 8
        $headers = $this->getTableHeaders($displayParts, $statementInfo, $sortExpressions, $isLimitedDisplay);
3079
3080 8
        $body = $this->getTableBody($dtResult, $displayParts, $map, $statementInfo, $isLimitedDisplay);
3081
3082
        // 4. ----- Prepares the link for multi-fields edit and delete
3083 8
        $isClauseUnique = $this->isClauseUnique($dtResult, $statementInfo, $displayParts->deleteLink);
3084
3085
        // 5. ----- Prepare "Query results operations"
3086 8
        $operations = [];
3087 8
        if (! $this->printView && ! $isLimitedDisplay) {
3088 8
            $operations = $this->getResultsOperations($displayParts->hasPrintLink, $statementInfo);
3089
        }
3090
3091 8
        $relationParameters = $this->relation->getRelationParameters();
3092
3093 8
        return $this->template->render('display/results/table', [
3094 8
            'sql_query_message' => $sqlQueryMessage,
3095 8
            'navigation' => $navigation,
3096 8
            'headers' => $headers,
3097 8
            'body' => $body,
3098 8
            'has_bulk_links' => $displayParts->deleteLink === DeleteLinkEnum::DELETE_ROW,
3099 8
            'has_export_button' => $this->hasExportButton($statementInfo, $displayParts->deleteLink),
3100 8
            'clause_is_unique' => $isClauseUnique,
3101 8
            'operations' => $operations,
3102 8
            'db' => $this->db,
3103 8
            'table' => $this->table,
3104 8
            'unique_id' => $this->uniqueId,
3105 8
            'sql_query' => $this->sqlQuery,
3106 8
            'goto' => $this->goto,
3107 8
            'unlim_num_rows' => $this->unlimNumRows,
3108 8
            'displaywork' => $relationParameters->displayFeature !== null,
3109 8
            'relwork' => $relationParameters->relationFeature !== null,
3110 8
            'save_cells_at_once' => $this->config->settings['SaveCellsAtOnce'],
3111 8
            'default_sliders_state' => $this->config->settings['InitialSlidersState'],
3112 8
            'is_browse_distinct' => $this->isBrowseDistinct,
3113 8
        ]);
3114
    }
3115
3116
    /**
3117
     * Gets offsets for next page and previous page.
3118
     *
3119
     * @return array<int, int>
3120
     * @psalm-return array{int, int}
3121
     */
3122 16
    private function getOffsets(): array
3123
    {
3124 16
        $tempVal = isset($_SESSION['tmpval']) && is_array($_SESSION['tmpval']) ? $_SESSION['tmpval'] : [];
3125 16
        if (isset($tempVal['max_rows']) && $tempVal['max_rows'] === self::ALL_ROWS) {
3126 4
            return [0, 0];
3127
        }
3128
3129 12
        $pos = isset($tempVal['pos']) && is_int($tempVal['pos']) ? $tempVal['pos'] : 0;
3130 12
        $maxRows = isset($tempVal['max_rows']) && is_int($tempVal['max_rows']) ? $tempVal['max_rows'] : 25;
3131
3132 12
        return [$pos + $maxRows, max(0, $pos - $maxRows)];
3133
    }
3134
3135
    /**
3136
     * Prepare sorted column message
3137
     *
3138
     * @see     getTable()
3139
     *
3140
     * @param ResultInterface $dtResult                  the link id associated to the query
3141
     *                                                   which results have to be displayed
3142
     * @param string|null     $sortExpressionNoDirection sort expression without direction
3143
     */
3144 4
    private function getSortedColumnMessage(
3145
        ResultInterface $dtResult,
3146
        string|null $sortExpressionNoDirection,
3147
    ): string {
3148 4
        if ($sortExpressionNoDirection === null || $sortExpressionNoDirection === '') {
3149
            return '';
3150
        }
3151
3152 4
        if (! str_contains($sortExpressionNoDirection, '.')) {
3153 4
            $sortTable = $this->table;
3154 4
            $sortColumn = $sortExpressionNoDirection;
3155
        } else {
3156
            [$sortTable, $sortColumn] = explode('.', $sortExpressionNoDirection);
3157
        }
3158
3159 4
        $sortTable = Util::unQuote($sortTable);
3160 4
        $sortColumn = Util::unQuote($sortColumn);
3161
3162
        // find the sorted column index in row result
3163
        // (this might be a multi-table query)
3164 4
        $sortedColumnIndex = false;
3165
3166 4
        foreach ($this->fieldsMeta as $key => $meta) {
3167 4
            if ($meta->table === $sortTable && $meta->name === $sortColumn) {
3168
                $sortedColumnIndex = $key;
3169
                break;
3170
            }
3171
        }
3172
3173 4
        if ($sortedColumnIndex === false) {
3174 4
            return '';
3175
        }
3176
3177
        // fetch first row of the result set
3178
        $row = $dtResult->fetchRow();
3179
3180
        // check for non printable sorted row data
3181
        $meta = $this->fieldsMeta[$sortedColumnIndex];
3182
3183
        $isBlobOrGeometryOrBinary = $meta->isType(FieldMetadata::TYPE_BLOB)
3184
                                    || $meta->isMappedTypeGeometry || $meta->isBinary;
3185
3186
        if ($isBlobOrGeometryOrBinary) {
3187
            $columnForFirstRow = $this->handleNonPrintableContents(
3188
                $meta->getMappedType(),
3189
                $row !== [] ? $row[$sortedColumnIndex] : '',
3190
                null,
3191
                [],
3192
                $meta,
3193
            );
3194
        } else {
3195
            $columnForFirstRow = $row !== [] ? $row[$sortedColumnIndex] : '';
3196
        }
3197
3198
        $columnForFirstRow = mb_strtoupper(
3199
            mb_substr(
3200
                (string) $columnForFirstRow,
3201
                0,
3202
                $this->config->settings['LimitChars'],
3203
            ) . '...',
3204
        );
3205
3206
        // fetch last row of the result set
3207
        $dtResult->seek($this->numRows > 0 ? $this->numRows - 1 : 0);
3208
        $row = $dtResult->fetchRow();
3209
3210
        // check for non printable sorted row data
3211
        $meta = $this->fieldsMeta[$sortedColumnIndex];
3212
        if ($isBlobOrGeometryOrBinary) {
3213
            $columnForLastRow = $this->handleNonPrintableContents(
3214
                $meta->getMappedType(),
3215
                $row !== [] ? $row[$sortedColumnIndex] : '',
3216
                null,
3217
                [],
3218
                $meta,
3219
            );
3220
        } else {
3221
            $columnForLastRow = $row !== [] ? $row[$sortedColumnIndex] : '';
3222
        }
3223
3224
        $columnForLastRow = mb_strtoupper(
3225
            mb_substr(
3226
                (string) $columnForLastRow,
3227
                0,
3228
                $this->config->settings['LimitChars'],
3229
            ) . '...',
3230
        );
3231
3232
        // reset to first row for the loop in getTableBody()
3233
        $dtResult->seek(0);
3234
3235
        // we could also use here $sort_expression_nodirection
3236
        return ' [' . htmlspecialchars($sortColumn)
3237
            . ': <strong>' . htmlspecialchars($columnForFirstRow) . ' - '
3238
            . htmlspecialchars($columnForLastRow) . '</strong>]';
3239
    }
3240
3241
    /**
3242
     * Set the content that needs to be shown in message
3243
     *
3244
     * @see     getTable()
3245
     *
3246
     * @param string $sortedColumnMessage the message for sorted column
3247
     * @param int    $total               the total number of rows returned by
3248
     *                                    the SQL query without any
3249
     *                                    programmatically appended LIMIT clause
3250
     * @param int    $posNext             the offset for next page
3251
     * @param string $preCount            the string renders before row count
3252
     * @param string $afterCount          the string renders after row count
3253
     *
3254
     * @return Message an object of Message
3255
     */
3256 8
    private function setMessageInformation(
3257
        string $sortedColumnMessage,
3258
        StatementInfo $statementInfo,
3259
        int $total,
3260
        int $posNext,
3261
        string $preCount,
3262
        string $afterCount,
3263
    ): Message {
3264 8
        if (! empty($statementInfo->statement->limit)) {
3265
            $firstShownRec = $statementInfo->statement->limit->offset;
3266
            $rowCount = $statementInfo->statement->limit->rowCount;
3267
3268
            if ($rowCount < $total) {
3269
                $lastShownRec = $firstShownRec + $rowCount - 1;
3270
            } else {
3271
                $lastShownRec = $firstShownRec + $total - 1;
3272
            }
3273 8
        } elseif ($_SESSION['tmpval']['max_rows'] === self::ALL_ROWS || $posNext > $total) {
3274 8
            $firstShownRec = $_SESSION['tmpval']['pos'];
3275 8
            $lastShownRec = $firstShownRec + $this->numRows - 1;
3276
        } else {
3277
            $firstShownRec = $_SESSION['tmpval']['pos'];
3278
            $lastShownRec = $posNext - 1;
3279
        }
3280
3281 8
        $messageViewWarning = false;
3282 8
        $table = new Table($this->table, $this->db, $this->dbi);
3283 8
        if ($table->isView() && $total === -1) {
3284
            $message = Message::notice(
3285
                __(
3286
                    'This view has at least this number of rows. Please refer to %sdocumentation%s.',
3287
                ),
3288
            );
3289
3290
            $message->addParam('[doc@cfg_MaxExactCount]');
3291
            $message->addParam('[/doc]');
3292
            $messageViewWarning = Generator::showHint($message->getMessage());
3293
        }
3294
3295 8
        $message = Message::success(__('Showing rows %1s - %2s'));
3296 8
        $message->addParam($firstShownRec);
3297
3298 8
        if ($messageViewWarning !== false) {
3299
            $message->addParamHtml('... ' . $messageViewWarning);
3300
        } else {
3301 8
            $message->addParam($lastShownRec);
3302
        }
3303
3304 8
        $message->addText('(');
3305
3306 8
        if ($messageViewWarning === false) {
3307 8
            if ($this->unlimNumRows != $total) {
3308
                $messageTotal = Message::notice(
3309
                    $preCount . __('%1$s total, %2$s in query'),
3310
                );
3311
                $messageTotal->addParam(Util::formatNumber($total, 0));
3312
                $messageTotal->addParam(Util::formatNumber($this->unlimNumRows, 0));
3313
            } else {
3314 8
                $messageTotal = Message::notice($preCount . __('%s total'));
3315 8
                $messageTotal->addParam(Util::formatNumber($total, 0));
3316
            }
3317
3318 8
            if ($afterCount !== '') {
3319
                $messageTotal->addHtml($afterCount);
3320
            }
3321
3322 8
            $message->addMessage($messageTotal, '');
3323
3324 8
            $message->addText(', ', '');
3325
        }
3326
3327 8
        $messageQueryTime = Message::notice(__('Query took %01.4f seconds.') . ')');
3328 8
        $messageQueryTime->addParam($this->queryTime);
3329
3330 8
        $message->addMessage($messageQueryTime, '');
3331 8
        $message->addHtml($sortedColumnMessage, '');
3332
3333 8
        return $message;
3334
    }
3335
3336
    /**
3337
     * Set the value of $map array for linking foreign key related tables
3338
     *
3339
     * @return ForeignKeyRelatedTable[]
3340
     */
3341 8
    private function getForeignKeyRelatedTables(): array
3342
    {
3343
        // To be able to later display a link to the related table,
3344
        // we verify both types of relations: either those that are
3345
        // native foreign keys or those defined in the phpMyAdmin
3346
        // configuration storage. If no PMA storage, we won't be able
3347
        // to use the "column to display" notion (for example show
3348
        // the name related to a numeric id).
3349
3350 8
        $map = [];
3351 8
        foreach ($this->relation->getForeignersInternal($this->db, $this->table) as $masterField => $rel) {
3352
            $map[$masterField] = new ForeignKeyRelatedTable(
3353
                $rel['foreign_table'],
3354
                $rel['foreign_field'],
3355
                $this->relation->getDisplayField($rel['foreign_db'], $rel['foreign_table']),
3356
                $rel['foreign_db'],
3357
            );
3358
        }
3359
3360 8
        foreach ($this->relation->getForeignKeysData($this->db, $this->table) as $oneKey) {
3361
            foreach ($oneKey->indexList as $index => $oneField) {
3362
                $displayField = $this->relation->getDisplayField(
3363
                    $oneKey->refDbName ?? Current::$database,
3364
                    $oneKey->refTableName,
3365
                );
3366
3367
                $map[$oneField] = new ForeignKeyRelatedTable(
3368
                    $oneKey->refTableName,
3369
                    $oneKey->refIndexList[$index],
3370
                    $displayField,
3371
                    $oneKey->refDbName ?? Current::$database,
3372
                );
3373
            }
3374
        }
3375
3376 8
        return $map;
3377
    }
3378
3379
    /**
3380
     * Prepare multi field edit/delete links
3381
     *
3382
     * @see     getTable()
3383
     *
3384
     * @param ResultInterface $dtResult the link id associated to the query which results have to be displayed
3385
     */
3386 8
    private function isClauseUnique(
3387
        ResultInterface $dtResult,
3388
        StatementInfo $statementInfo,
3389
        DeleteLinkEnum $deleteLink,
3390
    ): bool {
3391 8
        if ($deleteLink !== DeleteLinkEnum::DELETE_ROW) {
3392 8
            return false;
3393
        }
3394
3395
        // fetch last row of the result set
3396
        $dtResult->seek($this->numRows > 0 ? $this->numRows - 1 : 0);
3397
        $row = $dtResult->fetchRow();
3398
3399
        $expressions = [];
3400
3401
        if ($statementInfo->statement instanceof SelectStatement) {
3402
            $expressions = $statementInfo->statement->expr;
3403
        }
3404
3405
        /**
3406
         * $clauseIsUnique is needed by getTable() to generate the proper param
3407
         * in the multi-edit and multi-delete form
3408
         */
3409
        $clauseIsUnique = (new UniqueCondition($this->fieldsMeta, $row, expressions: $expressions))->isClauseUnique();
3410
3411
        // reset to first row for the loop in getTableBody()
3412
        $dtResult->seek(0);
3413
3414
        return $clauseIsUnique;
3415
    }
3416
3417 8
    private function hasExportButton(StatementInfo $statementInfo, DeleteLinkEnum $deleteLink): bool
3418
    {
3419 8
        return $deleteLink === DeleteLinkEnum::DELETE_ROW && $statementInfo->flags->queryType === StatementType::Select;
3420
    }
3421
3422
    /**
3423
     * Get operations that are available on results.
3424
     *
3425
     * @see     getTable()
3426
     *
3427
     * @psalm-return array{
3428
     *   has_export_link: bool,
3429
     *   has_geometry: bool,
3430
     *   has_print_link: bool,
3431
     *   has_procedure: bool,
3432
     *   url_params: array{
3433
     *     db: string,
3434
     *     table: string,
3435
     *     printview: "1",
3436
     *     sql_query: string,
3437
     *     single_table?: "true",
3438
     *     raw_query?: "true",
3439
     *     unlim_num_rows?: int|numeric-string|false
3440
     *   }
3441
     * }
3442
     */
3443 8
    private function getResultsOperations(
3444
        bool $hasPrintLink,
3445
        StatementInfo $statementInfo,
3446
    ): array {
3447 8
        $urlParams = [
3448 8
            'db' => $this->db,
3449 8
            'table' => $this->table,
3450 8
            'printview' => '1',
3451 8
            'sql_query' => $this->sqlQuery,
3452 8
        ];
3453
3454 8
        $geometryFound = false;
3455
3456
        // Export link
3457
        // (the single_table parameter is used in \PhpMyAdmin\Export\Export->getDisplay()
3458
        //  to hide the SQL and the structure export dialogs)
3459
        // If the parser found a PROCEDURE clause
3460
        // (most probably PROCEDURE ANALYSE()) it makes no sense to
3461
        // display the Export link).
3462
        if (
3463 8
            $statementInfo->flags->queryType === StatementType::Select
3464 8
            && ! $statementInfo->flags->isProcedure
3465
        ) {
3466 8
            if (count($statementInfo->selectTables) === 1) {
3467 8
                $urlParams['single_table'] = 'true';
3468
            }
3469
3470
            // In case this query doesn't involve any tables,
3471
            // implies only raw query is to be exported
3472 8
            if ($statementInfo->selectTables === []) {
3473
                $urlParams['raw_query'] = 'true';
3474
            }
3475
3476 8
            $urlParams['unlim_num_rows'] = $this->unlimNumRows;
3477
3478
            /**
3479
             * At this point we don't know the table name; this can happen
3480
             * for example with a query like
3481
             * SELECT bike_code FROM (SELECT bike_code FROM bikes) tmp
3482
             * As a workaround we set in the table parameter the name of the
3483
             * first table of this database, so that /table/export and
3484
             * the script it calls do not fail
3485
             */
3486 8
            if ($urlParams['table'] === '' && $urlParams['db'] !== '') {
3487
                $urlParams['table'] = (string) $this->dbi->fetchValue('SHOW TABLES');
3488
            }
3489
3490 8
            foreach ($this->fieldsMeta as $meta) {
3491 8
                if ($meta->isMappedTypeGeometry) {
3492
                    $geometryFound = true;
3493
                    break;
3494
                }
3495
            }
3496
        }
3497
3498 8
        return [
3499 8
            'has_procedure' => $statementInfo->flags->isProcedure,
3500 8
            'has_geometry' => $geometryFound,
3501 8
            'has_print_link' => $hasPrintLink,
3502 8
            'has_export_link' => $statementInfo->flags->queryType === StatementType::Select,
3503 8
            'url_params' => $urlParams,
3504 8
        ];
3505
    }
3506
3507
    /**
3508
     * Verifies what to do with non-printable contents (binary or BLOB)
3509
     * in Browse mode.
3510
     *
3511
     * @see getDataCellForGeometryColumns(), getDataCellForNonNumericColumns(), getSortedColumnMessage()
3512
     *
3513
     * @param string        $category         BLOB|BINARY|GEOMETRY
3514
     * @param string|null   $content          the binary content
3515
     * @param mixed[]       $transformOptions transformation parameters
3516
     * @param FieldMetadata $meta             the meta-information about the field
3517
     * @param mixed[]       $urlParams        parameters that should go to the download link
3518
     * @param bool          $isTruncated      the result is truncated or not
3519
     */
3520 28
    private function handleNonPrintableContents(
3521
        string $category,
3522
        string|null $content,
3523
        TransformationsInterface|null $transformationPlugin,
3524
        array $transformOptions,
3525
        FieldMetadata $meta,
3526
        array $urlParams = [],
3527
        bool &$isTruncated = false,
3528
    ): string {
3529 28
        $isTruncated = false;
3530 28
        $result = '[' . $category;
3531
3532 28
        if ($content !== null) {
3533 24
            $size = strlen($content);
3534 24
            $displaySize = Util::formatByteDown($size, 3, 1);
3535 24
            $result .= ' - ' . $displaySize[0] . ' ' . $displaySize[1];
3536
        } else {
3537 4
            $result .= ' - NULL';
3538 4
            $size = 0;
3539 4
            $content = '';
3540
        }
3541
3542 28
        $result .= ']';
3543
3544
        // if we want to use a text transformation on a BLOB column
3545 28
        if ($transformationPlugin !== null) {
3546 8
            $posMimeOctetstream = strpos(
3547 8
                $transformationPlugin::getMIMESubtype(),
3548 8
                'Octetstream',
3549 8
            );
3550 8
            if ($posMimeOctetstream || str_contains($transformationPlugin::getMIMEType(), 'Text')) {
3551
                // Applying Transformations on hex string of binary data
3552
                // seems more appropriate
3553 8
                $result = pack('H*', bin2hex($content));
3554
            }
3555
        }
3556
3557 28
        if ($size <= 0) {
3558 4
            return $result;
3559
        }
3560
3561 24
        if ($transformationPlugin !== null) {
3562 8
            return $transformationPlugin->applyTransformation($result, $transformOptions, $meta);
3563
        }
3564
3565 16
        $result = Core::mimeDefaultFunction($result);
3566
        if (
3567 16
            ($_SESSION['tmpval']['display_binary']
3568 16
            && $meta->isType(FieldMetadata::TYPE_STRING))
3569 16
            || ($_SESSION['tmpval']['display_blob']
3570 16
            && $meta->isType(FieldMetadata::TYPE_BLOB))
3571
        ) {
3572
            // in this case, restart from the original $content
3573
            if (
3574 8
                mb_check_encoding($content, 'utf-8')
3575 8
                && preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', $content) !== 1
3576
            ) {
3577
                // show as text if it's valid utf-8
3578 4
                $result = htmlspecialchars($content);
3579
            } else {
3580 4
                $result = '0x' . bin2hex($content);
3581
            }
3582
3583 8
            $preTruncation = $result;
3584 8
            $result = $this->getPartialText($result);
3585 8
            $isTruncated = $result !== $preTruncation;
3586
        }
3587
3588
        /* Create link to download */
3589
3590 16
        if ($urlParams !== [] && $this->db !== '' && $meta->orgtable !== '') {
3591 16
            $urlParams['where_clause_sign'] = Core::signSqlQuery($urlParams['where_clause']);
3592 16
            $result = '<a href="'
3593 16
                . Url::getFromRoute('/table/get-field', $urlParams)
3594 16
                . '" class="disableAjax">'
3595 16
                . $result . '</a>';
3596
        }
3597
3598 16
        return $result;
3599
    }
3600
3601
    /**
3602
     * Retrieves the associated foreign key info for a data cell
3603
     *
3604
     * @param ForeignKeyRelatedTable $fieldInfo       the relation
3605
     * @param string                 $whereComparison data for the where clause
3606
     *
3607
     * @return string|null  formatted data
3608
     */
3609
    private function getFromForeign(ForeignKeyRelatedTable $fieldInfo, string $whereComparison): string|null
3610
    {
3611
        $dispsql = 'SELECT '
3612
            . Util::backquote($fieldInfo->displayField)
3613
            . ' FROM '
3614
            . Util::backquote($fieldInfo->database)
3615
            . '.'
3616
            . Util::backquote($fieldInfo->table)
3617
            . ' WHERE '
3618
            . Util::backquote($fieldInfo->field)
3619
            . $whereComparison;
3620
3621
        $dispval = $this->dbi->fetchValue($dispsql);
3622
        if ($dispval === false) {
3623
            return __('Link not found!');
3624
        }
3625
3626
        if ($dispval === null) {
0 ignored issues
show
introduced by
The condition $dispval === null is always false.
Loading history...
3627
            return null;
3628
        }
3629
3630
        // Truncate values that are too long, see: #17902
3631
        return $this->getPartialText($dispval);
3632
    }
3633
3634
    /**
3635
     * Prepares the displayable content of a data cell in Browse mode,
3636
     * taking into account foreign key description field and transformations
3637
     *
3638
     * @see     getDataCellForNumericColumns(), getDataCellForGeometryColumns(),
3639
     *          getDataCellForNonNumericColumns(),
3640
     *
3641
     * @param string                   $class            css classes for the td element
3642
     * @param bool                     $conditionField   whether the column is a part of the where clause
3643
     * @param FieldMetadata            $meta             the meta-information about the field
3644
     * @param ForeignKeyRelatedTable[] $map              the list of relations
3645
     * @param string                   $data             data
3646
     * @param string                   $displayedData    data that will be displayed (maybe be chunked)
3647
     * @param string                   $nowrap           'nowrap' if the content should not be wrapped
3648
     * @param string                   $whereComparison  data for the where clause
3649
     * @param mixed[]                  $transformOptions options for transformation
3650
     * @param bool                     $isFieldTruncated whether the field is truncated
3651
     * @param string                   $originalLength   of a truncated column, or ''
3652
     *
3653
     * @return string  formatted data
3654
     */
3655 24
    private function getRowData(
3656
        string $class,
3657
        bool $conditionField,
3658
        StatementInfo $statementInfo,
3659
        FieldMetadata $meta,
3660
        array $map,
3661
        string $data,
3662
        string $displayedData,
3663
        TransformationsInterface|null $transformationPlugin,
3664
        string $nowrap,
3665
        string $whereComparison,
3666
        array $transformOptions,
3667
        bool $isFieldTruncated = false,
3668
        string $originalLength = '',
3669
    ): string {
3670 24
        $relationalDisplay = $_SESSION['tmpval']['relational_display'];
3671 24
        $value = '';
3672 24
        $tableDataCellClass = $this->addClass(
3673 24
            $class,
3674 24
            $conditionField,
3675 24
            $meta,
3676 24
            $nowrap,
3677 24
            $isFieldTruncated,
3678 24
            $transformationPlugin !== null,
3679 24
        );
3680
3681 24
        if (! empty($statementInfo->statement->expr)) {
3682 12
            foreach ($statementInfo->statement->expr as $expr) {
3683 12
                if (empty($expr->alias) || empty($expr->column)) {
3684 12
                    continue;
3685
                }
3686
3687
                if (strcasecmp($meta->name, $expr->alias) !== 0) {
3688
                    continue;
3689
                }
3690
3691
                $meta->name = $expr->column;
3692
            }
3693
        }
3694
3695 24
        if (isset($map[$meta->name])) {
3696 4
            $relation = $map[$meta->name];
3697
            // Field to display from the foreign table?
3698 4
            $dispval = '';
3699
3700
            // Check that we have a valid column name
3701 4
            if ($relation->displayField !== '') {
3702
                $dispval = $this->getFromForeign($relation, $whereComparison);
3703
            }
3704
3705 4
            if ($this->printView) {
3706
                if ($transformationPlugin !== null) {
3707
                    $value .= $transformationPlugin->applyTransformation($data, $transformOptions, $meta);
3708
                } else {
3709
                    $value .= Core::mimeDefaultFunction($data);
3710
                }
3711
3712
                $value .= ' <code>[-&gt;' . $dispval . ']</code>';
3713
            } else {
3714 4
                $sqlQuery = 'SELECT * FROM '
3715 4
                    . Util::backquote($relation->database) . '.'
3716 4
                    . Util::backquote($relation->table)
3717 4
                    . ' WHERE '
3718 4
                    . Util::backquote($relation->field)
3719 4
                    . $whereComparison;
3720
3721 4
                $urlParams = [
3722 4
                    'db' => $relation->database,
3723 4
                    'table' => $relation->table,
3724 4
                    'pos' => '0',
3725 4
                    'sql_signature' => Core::signSqlQuery($sqlQuery),
3726 4
                    'sql_query' => $sqlQuery,
3727 4
                ];
3728
3729 4
                if ($transformationPlugin !== null) {
3730
                    // always apply a transformation on the real data,
3731
                    // not on the display field
3732
                    $displayedData = $transformationPlugin->applyTransformation($data, $transformOptions, $meta);
3733 4
                } elseif ($relationalDisplay === self::RELATIONAL_DISPLAY_COLUMN && $relation->displayField !== '') {
3734
                    // user chose "relational display field" in the
3735
                    // display options, so show display field in the cell
3736
                    $displayedData = $dispval === null ? '<em>NULL</em>' : Core::mimeDefaultFunction($dispval);
0 ignored issues
show
introduced by
The condition $dispval === null is always false.
Loading history...
3737
                } else {
3738
                    // otherwise display data in the cell
3739 4
                    $displayedData = Core::mimeDefaultFunction($displayedData);
3740
                }
3741
3742 4
                if ($relationalDisplay === self::RELATIONAL_KEY) {
3743
                    // user chose "relational key" in the display options, so
3744
                    // the title contains the display field
3745
                    $title = $dispval ?? '';
3746
                } else {
3747 4
                    $title = $data;
3748
                }
3749
3750 4
                $tagParams = ['title' => $title];
3751 4
                if (str_contains($class, 'grid_edit')) {
3752
                    $tagParams['class'] = 'ajax';
3753
                }
3754
3755 4
                $value .= Generator::linkOrButton(
3756 4
                    Url::getFromRoute('/sql', $urlParams, false),
3757 4
                    null,
3758 4
                    $displayedData,
3759 4
                    $tagParams,
3760 4
                );
3761
            }
3762 24
        } elseif ($transformationPlugin !== null) {
3763 8
            $value .= $transformationPlugin->applyTransformation($data, $transformOptions, $meta);
3764
        } else {
3765 16
            $value .= Core::mimeDefaultFunction($data);
3766
        }
3767
3768 24
        return $this->template->render('display/results/row_data', [
3769 24
            'value' => $value,
3770 24
            'td_class' => $tableDataCellClass,
3771 24
            'decimals' => $meta->decimals,
3772 24
            'type' => $meta->getMappedType(),
3773 24
            'original_length' => $originalLength,
3774 24
        ]);
3775
    }
3776
3777
    /**
3778
     * Truncates given string based on LimitChars configuration
3779
     * and Session pftext variable
3780
     * (string is truncated only if necessary)
3781
     *
3782
     * @see handleNonPrintableContents(), getDataCellForGeometryColumns(), getDataCellForNonNumericColumns
3783
     *
3784
     * @param string $str string to be truncated
3785
     */
3786 44
    private function getPartialText(string $str): string
3787
    {
3788
        if (
3789 44
            mb_strlen($str) > $this->config->settings['LimitChars']
3790 44
            && $_SESSION['tmpval']['pftext'] === self::DISPLAY_PARTIAL_TEXT
3791
        ) {
3792 4
            return mb_substr($str, 0, $this->config->settings['LimitChars']) . '...';
3793
        }
3794
3795 40
        return $str;
3796
    }
3797
3798
    /** @return array{table:string|null, column:string|null} */
3799 4
    private function parseStringIntoTableAndColumn(string $nameToUseInSort): array
3800
    {
3801 4
        $lexer = new Lexer($nameToUseInSort);
3802
3803
        if (
3804 4
            $lexer->list->count === 6
3805 4
            && ($lexer->list[2]->type === TokenType::Symbol || $lexer->list[2]->type === TokenType::None)
3806 4
            && is_string($lexer->list[2]->value)
3807 4
            && ($lexer->list[4]->type === TokenType::Symbol || $lexer->list[4]->type === TokenType::None)
3808 4
            && is_string($lexer->list[4]->value)
3809
        ) {
3810
            // If a database name, table name, and column name were provided
3811
            return ['table' => $lexer->list[2]->value, 'column' => $lexer->list[4]->value];
3812
        }
3813
3814
        if (
3815 4
            $lexer->list->count === 4
3816 4
            && ($lexer->list[0]->type === TokenType::Symbol || $lexer->list[0]->type === TokenType::None)
3817 4
            && is_string($lexer->list[0]->value)
3818 4
            && ($lexer->list[2]->type === TokenType::Symbol || $lexer->list[2]->type === TokenType::None)
3819 4
            && is_string($lexer->list[2]->value)
3820
        ) {
3821
            // If a table name and column name were provided
3822
            return ['table' => $lexer->list[0]->value, 'column' => $lexer->list[2]->value];
3823
        }
3824
3825
        if (
3826 4
            $lexer->list->count === 2
3827 4
            && ($lexer->list[0]->type === TokenType::Symbol || $lexer->list[0]->type === TokenType::None)
3828 4
            && is_string($lexer->list[0]->value)
3829
        ) {
3830
            // If only a column name was provided
3831 4
            return ['table' => null, 'column' => $lexer->list[0]->value];
3832
        }
3833
3834
        // If some other expression was provided
3835
        return ['table' => null, 'column' => null];
3836
    }
3837
3838
    /**
3839
     * @param OrderKeyword[] $orderKeywords
3840
     *
3841
     * @return list<SortExpression>
3842
     */
3843 4
    private function extractSortingExpressions(array $orderKeywords): array
3844
    {
3845 4
        $expressions = [];
3846 4
        foreach ($orderKeywords as $o) {
3847 4
            if ((string) (int) $o->expr->expr === $o->expr->expr) {
3848
                // If a numerical column index is used, we need to convert it to a column name
3849
                $field = $this->fieldsMeta[(int) $o->expr->expr - 1];
3850
                $normalizedExpression = '';
3851
                $tableName = null;
3852
                if ($field->table !== '') {
3853
                    $tableName = $field->table;
3854
                    $normalizedExpression = Util::backquote($field->table) . '.';
3855
                }
3856
3857
                $columnName = $field->name;
3858
                $normalizedExpression .= Util::backquote($field->name);
3859
            } else {
3860 4
                $normalizedExpression = $o->expr->expr ?? '';
3861 4
                [
3862 4
                    'table' => $tableName,
3863 4
                    'column' => $columnName,
3864 4
                ] = $this->parseStringIntoTableAndColumn($normalizedExpression);
3865
            }
3866
3867 4
            $expressions[] = new SortExpression($tableName, $columnName, $o->type->value, $normalizedExpression);
3868
        }
3869
3870 4
        return $expressions;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $expressions returns the type PhpMyAdmin\Display\SortExpression[]|array which is incompatible with the documented return type PhpMyAdmin\Display\list.
Loading history...
3871
    }
3872
}
3873