Relation::foreignDropdown()   B
last analyzed

Complexity

Conditions 11
Paths 108

Size

Total Lines 56
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 0
Metric Value
eloc 26
dl 0
loc 56
ccs 0
cts 27
cp 0
rs 7.25
c 0
b 0
f 0
cc 11
nc 108
nop 5
crap 132

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\ConfigStorage;
6
7
use PhpMyAdmin\Config;
8
use PhpMyAdmin\ConfigStorage\Features\PdfFeature;
9
use PhpMyAdmin\Current;
10
use PhpMyAdmin\Dbal\ConnectionType;
11
use PhpMyAdmin\Dbal\DatabaseInterface;
12
use PhpMyAdmin\Identifiers\DatabaseName;
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\Identifiers\DatabaseName 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...
13
use PhpMyAdmin\Identifiers\TableName;
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\Identifiers\TableName 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...
14
use PhpMyAdmin\InternalRelations;
15
use PhpMyAdmin\Message;
16
use PhpMyAdmin\SqlParser\Parser;
17
use PhpMyAdmin\SqlParser\Statements\CreateStatement;
18
use PhpMyAdmin\SqlParser\Utils\ForeignKey;
19
use PhpMyAdmin\Table\Table;
20
use PhpMyAdmin\TypeClass;
21
use PhpMyAdmin\Util;
22
use PhpMyAdmin\Version;
23
24
use function __;
25
use function array_fill_keys;
26
use function array_keys;
27
use function array_search;
28
use function asort;
29
use function bin2hex;
30
use function count;
31
use function explode;
32
use function file_get_contents;
33
use function htmlspecialchars;
34
use function implode;
35
use function in_array;
36
use function is_string;
37
use function ksort;
38
use function mb_check_encoding;
39
use function mb_strlen;
40
use function mb_substr;
41
use function natcasesort;
42
use function preg_match;
43
use function preg_replace;
44
use function sprintf;
45
use function str_contains;
46
use function str_replace;
47
use function strnatcasecmp;
48
use function strtolower;
49
use function trim;
50
use function uksort;
51
use function usort;
52
53
use const SQL_DIR;
54
55
/**
56
 * Set of functions used with the relation and PDF feature
57
 */
58
class Relation
59
{
60
    private static RelationParameters|null $cache = null;
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\ConfigStorage\RelationParameters 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...
61
    private readonly Config $config;
62
63 120
    public function __construct(public DatabaseInterface $dbi, Config|null $config = null)
64
    {
65 120
        $this->config = $config ?? Config::getInstance();
0 ignored issues
show
Bug introduced by
The property config is declared read-only in PhpMyAdmin\ConfigStorage\Relation.
Loading history...
Deprecated Code introduced by
The function PhpMyAdmin\Config::getInstance() has been deprecated: Use dependency injection instead. ( Ignorable by Annotation )

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

65
        $this->config = $config ?? /** @scrutinizer ignore-deprecated */ Config::getInstance();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
66
    }
67
68 76
    public function getRelationParameters(): RelationParameters
69
    {
70 76
        if (self::$cache === null) {
71 48
            self::$cache = RelationParameters::fromArray($this->checkRelationsParam());
72
        }
73
74 76
        return self::$cache;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::cache could return the type null which is incompatible with the type-hinted return PhpMyAdmin\ConfigStorage\RelationParameters. Consider adding an additional type-check to rule them out.
Loading history...
75
    }
76
77
    /**
78
     * @param array<string, bool|string|null> $relationParams
79
     *
80
     * @return array<string, bool|string|null>
81
     */
82 32
    private function checkTableAccess(array $relationParams): array
83
    {
84 32
        if (isset($relationParams['relation'], $relationParams['table_info'])) {
85
            if ($this->canAccessStorageTable((string) $relationParams['table_info'])) {
86
                $relationParams['displaywork'] = true;
87
            }
88
        }
89
90 32
        if (isset($relationParams['table_coords'], $relationParams['pdf_pages'])) {
91
            if ($this->canAccessStorageTable((string) $relationParams['table_coords'])) {
92
                if ($this->canAccessStorageTable((string) $relationParams['pdf_pages'])) {
93
                    $relationParams['pdfwork'] = true;
94
                }
95
            }
96
        }
97
98 32
        if (isset($relationParams['column_info'])) {
99
            if ($this->canAccessStorageTable((string) $relationParams['column_info'])) {
100
                $relationParams['commwork'] = true;
101
                // phpMyAdmin 4.3+
102
                // Check for input transformations upgrade.
103
                $relationParams['mimework'] = $this->tryUpgradeTransformations();
104
            }
105
        }
106
107 32
        if (isset($relationParams['users'], $relationParams['usergroups'])) {
108
            if ($this->canAccessStorageTable((string) $relationParams['users'])) {
109
                if ($this->canAccessStorageTable((string) $relationParams['usergroups'])) {
110
                    $relationParams['menuswork'] = true;
111
                }
112
            }
113
        }
114
115 32
        $settings = [
116 32
            'export_templates' => 'exporttemplateswork',
117 32
            'designer_settings' => 'designersettingswork',
118 32
            'central_columns' => 'centralcolumnswork',
119 32
            'savedsearches' => 'savedsearcheswork',
120 32
            'navigationhiding' => 'navwork',
121 32
            'bookmark' => 'bookmarkwork',
122 32
            'userconfig' => 'userconfigwork',
123 32
            'tracking' => 'trackingwork',
124 32
            'table_uiprefs' => 'uiprefswork',
125 32
            'favorite' => 'favoritework',
126 32
            'recent' => 'recentwork',
127 32
            'history' => 'historywork',
128 32
            'relation' => 'relwork',
129 32
        ];
130
131 32
        foreach ($settings as $setingName => $worksKey) {
132 32
            if (! isset($relationParams[$setingName])) {
133 32
                continue;
134
            }
135
136 28
            if (! $this->canAccessStorageTable((string) $relationParams[$setingName])) {
137 4
                continue;
138
            }
139
140 24
            $relationParams[$worksKey] = true;
141
        }
142
143 32
        return $relationParams;
144
    }
145
146
    /**
147
     * @param array<string, bool|string|null> $relationParams
148
     *
149
     * @return array<string, bool|string|null>|null
150
     */
151 40
    private function fillRelationParamsWithTableNames(array $relationParams): array|null
152
    {
153 40
        if ($this->arePmadbTablesAllDisabled()) {
154
            return null;
155
        }
156
157 40
        $tables = $this->dbi->getTables($this->config->selectedServer['pmadb'], ConnectionType::ControlUser);
158 40
        if ($tables === []) {
159 8
            return null;
160
        }
161
162 32
        foreach ($tables as $table) {
163 32
            if ($table == $this->config->selectedServer['bookmarktable']) {
164
                $relationParams['bookmark'] = $table;
165 32
            } elseif ($table == $this->config->selectedServer['relation']) {
166
                $relationParams['relation'] = $table;
167 32
            } elseif ($table == $this->config->selectedServer['table_info']) {
168
                $relationParams['table_info'] = $table;
169 32
            } elseif ($table == $this->config->selectedServer['table_coords']) {
170
                $relationParams['table_coords'] = $table;
171 32
            } elseif ($table == $this->config->selectedServer['column_info']) {
172
                $relationParams['column_info'] = $table;
173 32
            } elseif ($table == $this->config->selectedServer['pdf_pages']) {
174
                $relationParams['pdf_pages'] = $table;
175 32
            } elseif ($table == $this->config->selectedServer['history']) {
176
                $relationParams['history'] = $table;
177 32
            } elseif ($table == $this->config->selectedServer['recent']) {
178
                $relationParams['recent'] = $table;
179 32
            } elseif ($table == $this->config->selectedServer['favorite']) {
180 4
                $relationParams['favorite'] = $table;
181 28
            } elseif ($table == $this->config->selectedServer['table_uiprefs']) {
182
                $relationParams['table_uiprefs'] = $table;
183 28
            } elseif ($table == $this->config->selectedServer['tracking']) {
184
                $relationParams['tracking'] = $table;
185 28
            } elseif ($table == $this->config->selectedServer['userconfig']) {
186 24
                $relationParams['userconfig'] = $table;
187 8
            } elseif ($table == $this->config->selectedServer['users']) {
188
                $relationParams['users'] = $table;
189 8
            } elseif ($table == $this->config->selectedServer['usergroups']) {
190
                $relationParams['usergroups'] = $table;
191 8
            } elseif ($table == $this->config->selectedServer['navigationhiding']) {
192
                $relationParams['navigationhiding'] = $table;
193 8
            } elseif ($table == $this->config->selectedServer['savedsearches']) {
194
                $relationParams['savedsearches'] = $table;
195 8
            } elseif ($table == $this->config->selectedServer['central_columns']) {
196
                $relationParams['central_columns'] = $table;
197 8
            } elseif ($table == $this->config->selectedServer['designer_settings']) {
198
                $relationParams['designer_settings'] = $table;
199 8
            } elseif ($table == $this->config->selectedServer['export_templates']) {
200
                $relationParams['export_templates'] = $table;
201
            }
202
        }
203
204 32
        return $relationParams;
205
    }
206
207
    /**
208
     * Defines the relation parameters for the current user
209
     * just a copy of the functions used for relations ;-)
210
     * but added some stuff to check what will work
211
     *
212
     * @return array<string, bool|string|null> the relation parameters for the current user
213
     */
214 48
    private function checkRelationsParam(): array
215
    {
216 48
        $workToTable = [
217 48
            'relwork' => 'relation',
218 48
            'displaywork' => ['relation', 'table_info'],
219 48
            'bookmarkwork' => 'bookmarktable',
220 48
            'pdfwork' => ['table_coords', 'pdf_pages'],
221 48
            'commwork' => 'column_info',
222 48
            'mimework' => 'column_info',
223 48
            'historywork' => 'history',
224 48
            'recentwork' => 'recent',
225 48
            'favoritework' => 'favorite',
226 48
            'uiprefswork' => 'table_uiprefs',
227 48
            'trackingwork' => 'tracking',
228 48
            'userconfigwork' => 'userconfig',
229 48
            'menuswork' => ['users', 'usergroups'],
230 48
            'navwork' => 'navigationhiding',
231 48
            'savedsearcheswork' => 'savedsearches',
232 48
            'centralcolumnswork' => 'central_columns',
233 48
            'designersettingswork' => 'designer_settings',
234 48
            'exporttemplateswork' => 'export_templates',
235 48
        ];
236
237 48
        $relationParams = array_fill_keys(array_keys($workToTable), false);
238
239 48
        $relationParams['version'] = Version::VERSION;
240 48
        $relationParams['allworks'] = false;
241 48
        $relationParams['user'] = null;
242 48
        $relationParams['db'] = null;
243
244
        if (
245 48
            Current::$server === 0
246 44
            || $this->config->selectedServer['pmadb'] === ''
247 48
            || ! $this->dbi->selectDb($this->config->selectedServer['pmadb'], ConnectionType::ControlUser)
248
        ) {
249 8
            $this->config->selectedServer['pmadb'] = '';
250
251 8
            return $relationParams;
252
        }
253
254 40
        $relationParams['user'] = $this->config->selectedServer['user'];
255 40
        $relationParams['db'] = $this->config->selectedServer['pmadb'];
256
257 40
        $relationParamsFilled = $this->fillRelationParamsWithTableNames($relationParams);
258
259 40
        if ($relationParamsFilled === null) {
260 8
            return $relationParams;
261
        }
262
263 32
        $relationParams = $this->checkTableAccess($relationParamsFilled);
264
265 32
        $allWorks = true;
266 32
        foreach ($workToTable as $work => $table) {
267 32
            if ($relationParams[$work]) {
268
                continue;
269
            }
270
271 32
            if (is_string($table)) {
272 32
                if (isset($this->config->selectedServer[$table]) && $this->config->selectedServer[$table] !== false) {
273 32
                    $allWorks = false;
274 32
                    break;
275
                }
276
            } else {
277
                $oneNull = false;
278
                foreach ($table as $t) {
279
                    if (isset($this->config->selectedServer[$t]) && $this->config->selectedServer[$t] === false) {
280
                        $oneNull = true;
281
                        break;
282
                    }
283
                }
284
285
                if (! $oneNull) {
286
                    $allWorks = false;
287
                    break;
288
                }
289
            }
290
        }
291
292 32
        $relationParams['allworks'] = $allWorks;
293
294 32
        return $relationParams;
295
    }
296
297
    /**
298
     * Check if the table is accessible
299
     *
300
     * @param string $tableDbName The table or table.db
301
     */
302 28
    public function canAccessStorageTable(string $tableDbName): bool
303
    {
304 28
        $result = $this->dbi->tryQueryAsControlUser('SELECT NULL FROM ' . Util::backquote($tableDbName) . ' LIMIT 0');
305
306 28
        return $result !== false;
307
    }
308
309
    /**
310
     * Check whether column_info table input transformation
311
     * upgrade is required and try to upgrade silently
312
     */
313 4
    public function tryUpgradeTransformations(): bool
314
    {
315
        // From 4.3, new input oriented transformation feature was introduced.
316
        // Check whether column_info table has input transformation columns
317 4
        $newCols = ['input_transformation', 'input_transformation_options'];
318 4
        $query = 'SHOW COLUMNS FROM '
319 4
            . Util::backquote($this->config->selectedServer['pmadb'])
320 4
            . '.' . Util::backquote($this->config->selectedServer['column_info'])
321 4
            . ' WHERE Field IN (\'' . implode('\', \'', $newCols) . '\')';
322 4
        $result = $this->dbi->tryQueryAsControlUser($query);
323 4
        if ($result) {
324 4
            $rows = $result->numRows();
325 4
            unset($result);
326
            // input transformations are present
327
            // no need to upgrade
328 4
            if ($rows === 2) {
329
                return true;
330
331
                // try silent upgrade without disturbing the user
332
            }
333
334
            // read upgrade query file
335 4
            $query = @file_get_contents(SQL_DIR . 'upgrade_column_info_4_3_0+.sql');
336
            // replace database name from query to with set in config.inc.php
337
            // replace pma__column_info table name from query
338
            // to with set in config.inc.php
339 4
            $query = str_replace(
340 4
                ['`phpmyadmin`', '`pma__column_info`'],
341 4
                [
342 4
                    Util::backquote($this->config->selectedServer['pmadb']),
343 4
                    Util::backquote($this->config->selectedServer['column_info']),
344 4
                ],
345 4
                (string) $query,
346 4
            );
347 4
            $this->dbi->tryMultiQuery($query, ConnectionType::ControlUser);
348
            // skips result sets of query as we are not interested in it
349
            /** @infection-ignore-all */
350
            do {
351 4
                $hasResult = $this->dbi->nextResult(ConnectionType::ControlUser);
352 4
            } while ($hasResult !== false);
353
354 4
            $error = $this->dbi->getError(ConnectionType::ControlUser);
355
356
            // return true if no error exists otherwise false
357 4
            return $error === '';
358
        }
359
360
        // some failure, either in upgrading or something else
361
        // make some noise, time to wake up user.
362
        return false;
363
    }
364
365
    /**
366
     * Gets all Relations to foreign tables for a given table or
367
     * optionally a given column in a table
368
     *
369
     * @param string $db     the name of the db to check for
370
     * @param string $table  the name of the table to check for
371
     * @param string $column the name of the column to check for
372
     *
373
     * @return array<array<mixed>>
374
     */
375
    public function getForeigners(string $db, string $table, string $column = ''): array
376
    {
377
        $foreign = $this->getForeignersInternal($db, $table, $column);
378
379
        if ($table !== '') {
380
            $foreign['foreign_keys_data'] = $this->getForeignKeysData($db, $table);
381
        }
382
383
        return $foreign;
384
    }
385
386
    /**
387
     * Gets all Relations to foreign tables for a given table or
388
     * optionally a given column in a table
389
     *
390
     * @return array<array<mixed>>
391
     */
392
    public function getForeignersInternal(string $db, string $table, string $column = ''): array
393
    {
394
        $relationFeature = $this->getRelationParameters()->relationFeature;
395
        $foreign = [];
396
397
        if ($relationFeature !== null) {
398
            $relQuery = 'SELECT `master_field`, `foreign_db`, '
399
                . '`foreign_table`, `foreign_field`'
400
                . ' FROM ' . Util::backquote($relationFeature->database)
401
                . '.' . Util::backquote($relationFeature->relation)
402
                . ' WHERE `master_db` = ' . $this->dbi->quoteString($db)
403
                . ' AND `master_table` = ' . $this->dbi->quoteString($table);
404
            if ($column !== '') {
405
                $relQuery .= ' AND `master_field` = ' . $this->dbi->quoteString($column);
406
            }
407
408
            /** @var array<array<string|null>> $foreign */
409
            $foreign = $this->dbi->fetchResult($relQuery, 'master_field', null, ConnectionType::ControlUser);
410
        }
411
412
        /**
413
         * Emulating relations for some information_schema tables
414
         */
415
        if (in_array(strtolower($db), ['information_schema', 'mysql'], true)) {
416
            $internalRelations = strtolower($db) === 'information_schema'
417
                ? InternalRelations::INFORMATION_SCHEMA
418
                : InternalRelations::MYSQL;
419
420
            if (isset($internalRelations[$table])) {
421
                foreach ($internalRelations[$table] as $field => $relations) {
422
                    if (
423
                        ($column !== '' && $column !== $field)
424
                        || (isset($foreign[$field]) && $foreign[$field] != '')
425
                    ) {
426
                        continue;
427
                    }
428
429
                    $foreign[$field] = $relations;
430
                }
431
            }
432
        }
433
434
        return $foreign;
435
    }
436
437
    /** @return list<ForeignKey> */
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\ConfigStorage\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...
438
    public function getForeignKeysData(string $db, string $table): array
439
    {
440
        if ($table === '') {
441
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type PhpMyAdmin\ConfigStorage\list.
Loading history...
442
        }
443
444
        $tableObj = new Table($table, $db, $this->dbi);
445
        $showCreateTable = $tableObj->showCreate();
446
        if ($showCreateTable !== '') {
447
            $parser = new Parser($showCreateTable);
448
            $stmt = $parser->statements[0];
449
            if ($stmt instanceof CreateStatement) {
450
                return $stmt->getForeignKeys();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $stmt->getForeignKeys() returns the type PhpMyAdmin\SqlParser\Utils\ForeignKey[]|array which is incompatible with the documented return type PhpMyAdmin\ConfigStorage\list.
Loading history...
451
            }
452
        }
453
454
        return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type PhpMyAdmin\ConfigStorage\list.
Loading history...
455
    }
456
457
    /**
458
     * Gets the display field of a table
459
     *
460
     * @param string $db    the name of the db to check for
461
     * @param string $table the name of the table to check for
462
     *
463
     * @return string field name
464
     */
465 4
    public function getDisplayField(string $db, string $table): string
466
    {
467 4
        $displayFeature = $this->getRelationParameters()->displayFeature;
468
469
        /**
470
         * Try to fetch the display field from DB.
471
         */
472 4
        if ($displayFeature !== null) {
473 4
            $dispQuery = 'SELECT `display_field`'
474 4
                    . ' FROM ' . Util::backquote($displayFeature->database)
475 4
                    . '.' . Util::backquote($displayFeature->tableInfo)
476 4
                    . ' WHERE `db_name` = ' . $this->dbi->quoteString($db)
477 4
                    . ' AND `table_name` = ' . $this->dbi->quoteString($table);
478
479 4
            $displayField = $this->dbi->fetchValue($dispQuery, 0, ConnectionType::ControlUser);
480 4
            if (is_string($displayField)) {
481 4
                return $displayField;
482
            }
483
        }
484
485
        /**
486
         * Emulating the display field for some information_schema tables.
487
         */
488 4
        if ($db === 'information_schema') {
489
            switch ($table) {
490 4
                case 'CHARACTER_SETS':
491 4
                    return 'DESCRIPTION';
492
493 4
                case 'TABLES':
494 4
                    return 'TABLE_COMMENT';
495
            }
496
        }
497
498
        /**
499
         * Pick first char field
500
         */
501 4
        $columns = $this->dbi->getColumns($db, $table);
502 4
        foreach ($columns as $column) {
503
            $columnType = preg_replace('@(\(.*)|(\s/.*)@s', '', $column->type);
504
            if ($this->dbi->types->getTypeClass($columnType) === TypeClass::Char) {
505
                return $column->field;
506
            }
507
        }
508
509 4
        return '';
510
    }
511
512
    /**
513
     * Gets the comments for all columns of a table or the db itself
514
     *
515
     * @param string $db    the name of the db to check for
516
     * @param string $table the name of the table to check for
517
     *
518
     * @return string[]    [column_name] = comment
519
     */
520 4
    public function getComments(string $db, string $table = ''): array
521
    {
522 4
        if ($table === '') {
523 4
            return [$this->getDbComment($db)];
524
        }
525
526 4
        $comments = [];
527
528
        // MySQL native column comments
529 4
        $columns = $this->dbi->getColumns($db, $table);
530 4
        foreach ($columns as $column) {
531 4
            if ($column->comment === '') {
532
                continue;
533
            }
534
535 4
            $comments[$column->field] = $column->comment;
536
        }
537
538 4
        return $comments;
539
    }
540
541
    /**
542
     * Gets the comment for a db
543
     *
544
     * @param string $db the name of the db to check for
545
     */
546 4
    public function getDbComment(string $db): string
547
    {
548 4
        $columnCommentsFeature = $this->getRelationParameters()->columnCommentsFeature;
549 4
        if ($columnCommentsFeature !== null) {
550
            // pmadb internal db comment
551
            $comQry = 'SELECT `comment`'
552
                    . ' FROM ' . Util::backquote($columnCommentsFeature->database)
553
                    . '.' . Util::backquote($columnCommentsFeature->columnInfo)
554
                    . ' WHERE db_name = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser)
555
                    . ' AND table_name  = \'\''
556
                    . ' AND column_name = \'(db_comment)\'';
557
            $comRs = $this->dbi->tryQueryAsControlUser($comQry);
558
559
            if ($comRs && $comRs->numRows() > 0) {
560
                $row = $comRs->fetchAssoc();
561
562
                return (string) $row['comment'];
563
            }
564
        }
565
566 4
        return '';
567
    }
568
569
    /**
570
     * Set a database comment to a certain value.
571
     *
572
     * @param string $db      the name of the db
573
     * @param string $comment the value of the column
574
     */
575
    public function setDbComment(string $db, string $comment = ''): bool
576
    {
577
        $columnCommentsFeature = $this->getRelationParameters()->columnCommentsFeature;
578
        if ($columnCommentsFeature === null) {
579
            return false;
580
        }
581
582
        if ($comment !== '') {
583
            $updQuery = 'INSERT INTO '
584
                . Util::backquote($columnCommentsFeature->database) . '.'
585
                . Util::backquote($columnCommentsFeature->columnInfo)
586
                . ' (`db_name`, `table_name`, `column_name`, `comment`)'
587
                . ' VALUES ('
588
                . $this->dbi->quoteString($db, ConnectionType::ControlUser)
589
                . ", '', '(db_comment)', "
590
                . $this->dbi->quoteString($comment, ConnectionType::ControlUser)
591
                . ') '
592
                . ' ON DUPLICATE KEY UPDATE '
593
                . '`comment` = ' . $this->dbi->quoteString($comment, ConnectionType::ControlUser);
594
        } else {
595
            $updQuery = 'DELETE FROM '
596
                . Util::backquote($columnCommentsFeature->database) . '.'
597
                . Util::backquote($columnCommentsFeature->columnInfo)
598
                . ' WHERE `db_name`     = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser)
599
                . '
600
                    AND `table_name`  = \'\'
601
                    AND `column_name` = \'(db_comment)\'';
602
        }
603
604
        return (bool) $this->dbi->queryAsControlUser($updQuery);
605
    }
606
607
    /**
608
     * Prepares the dropdown for one mode
609
     *
610
     * @param mixed[] $foreign the keys and values for foreigns
611
     * @param string  $data    the current data of the dropdown
612
     * @param string  $mode    the needed mode
613
     *
614
     * @return string[] the <option value=""><option>s
615
     */
616
    public function buildForeignDropdown(array $foreign, string $data, string $mode): array
617
    {
618
        $reloptions = [];
619
620
        // id-only is a special mode used when no foreign display column
621
        // is available
622
        if ($mode === 'id-content' || $mode === 'id-only') {
623
            // sort for id-content
624
            if ($this->config->settings['NaturalOrder']) {
625
                uksort($foreign, strnatcasecmp(...));
0 ignored issues
show
Bug introduced by
The type strnatcasecmp 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...
626
            } else {
627
                ksort($foreign);
628
            }
629
        } elseif ($mode === 'content-id') {
630
            // sort for content-id
631
            if ($this->config->settings['NaturalOrder']) {
632
                natcasesort($foreign);
633
            } else {
634
                asort($foreign);
635
            }
636
        }
637
638
        foreach ($foreign as $key => $value) {
639
            $key = (string) $key;
640
            $value = (string) $value;
641
642
            if (
643
                mb_check_encoding($key, 'utf-8')
644
                && preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', $key) !== 1
645
            ) {
646
                $selected = $key === $data;
647
                // show as text if it's valid utf-8
648
                $key = htmlspecialchars($key);
649
            } else {
650
                $key = '0x' . bin2hex($key);
651
                if (str_contains($data, '0x')) {
652
                    $selected = $key === trim($data);
653
                } else {
654
                    $selected = $key === '0x' . $data;
655
                }
656
            }
657
658
            if (
659
                mb_check_encoding($value, 'utf-8')
660
                && preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', $value) !== 1
661
            ) {
662
                if (mb_strlen($value) <= $this->config->settings['LimitChars']) {
663
                    // show as text if it's valid utf-8
664
                    $value = htmlspecialchars($value);
665
                } else {
666
                    // show as truncated text if it's valid utf-8
667
                    $value = htmlspecialchars(
668
                        mb_substr(
669
                            $value,
670
                            0,
671
                            $this->config->settings['LimitChars'],
672
                        ) . '...',
673
                    );
674
                }
675
            } else {
676
                $value = '0x' . bin2hex($value);
677
            }
678
679
            $reloption = '<option value="' . $key . '"';
680
681
            if ($selected) {
682
                $reloption .= ' selected';
683
            }
684
685
            if ($mode === 'content-id') {
686
                $reloptions[] = $reloption . '>'
687
                    . $value . '&nbsp;-&nbsp;' . $key . '</option>';
688
            } elseif ($mode === 'id-content') {
689
                $reloptions[] = $reloption . '>'
690
                    . $key . '&nbsp;-&nbsp;' . $value . '</option>';
691
            } elseif ($mode === 'id-only') {
692
                $reloptions[] = $reloption . '>'
693
                    . $key . '</option>';
694
            }
695
        }
696
697
        return $reloptions;
698
    }
699
700
    /**
701
     * Outputs dropdown with values of foreign fields
702
     *
703
     * @param mixed[][] $dispRow        array of the displayed row
704
     * @param string    $foreignField   the foreign field
705
     * @param string    $foreignDisplay the foreign field to display
706
     * @param string    $data           the current data of the dropdown (field in row)
707
     * @param int|null  $max            maximum number of items in the dropdown
708
     *
709
     * @return string   the <option value=""><option>s
710
     */
711
    public function foreignDropdown(
712
        array $dispRow,
713
        string $foreignField,
714
        string $foreignDisplay,
715
        string $data,
716
        int|null $max = null,
717
    ): string {
718
        if ($max === null) {
719
            $max = $this->config->settings['ForeignKeyMaxLimit'];
720
        }
721
722
        $foreign = [];
723
724
        // collect the data
725
        foreach ($dispRow as $relrow) {
726
            $key = $relrow[$foreignField];
727
728
            // if the display field has been defined for this foreign table
729
            $value = $foreignDisplay !== '' ? $relrow[$foreignDisplay] : '';
730
731
            $foreign[$key] = $value;
732
        }
733
734
        // put the dropdown sections in correct order
735
        $bottom = [];
736
        if ($foreignDisplay !== '') {
737
            $top = $this->buildForeignDropdown($foreign, $data, $this->config->settings['ForeignKeyDropdownOrder'][0]);
738
739
            if (isset($this->config->settings['ForeignKeyDropdownOrder'][1])) {
740
                $bottom = $this->buildForeignDropdown(
741
                    $foreign,
742
                    $data,
743
                    $this->config->settings['ForeignKeyDropdownOrder'][1],
744
                );
745
            }
746
        } else {
747
            $top = $this->buildForeignDropdown($foreign, $data, 'id-only');
748
        }
749
750
        // beginning of dropdown
751
        $ret = '<option value="">&nbsp;</option>';
752
        $topCount = count($top);
753
        if ($max == -1 || $topCount < $max) {
754
            $ret .= implode('', $top);
755
            if ($foreignDisplay && $topCount > 0) {
756
                // this empty option is to visually mark the beginning of the
757
                // second series of values (bottom)
758
                $ret .= '<option value="">&nbsp;</option>';
759
            }
760
        }
761
762
        if ($foreignDisplay !== '') {
763
            $ret .= implode('', $bottom);
764
        }
765
766
        return $ret;
767
    }
768
769
    /**
770
     * Gets foreign keys in preparation for a drop-down selector
771
     *
772
     * @param mixed[] $foreigners    array of the foreign keys
773
     * @param string  $field         the foreign field name
774
     * @param bool    $overrideTotal whether to override the total
775
     * @param string  $foreignFilter a possible filter
776
     * @param string  $foreignLimit  a possible LIMIT clause
777
     * @param bool    $getTotal      optional, whether to get total num of rows
778
     *                               in $foreignData['the_total;]
779
     *                               (has an effect of performance)
780
     */
781
    public function getForeignData(
782
        array $foreigners,
783
        string $field,
784
        bool $overrideTotal,
785
        string $foreignFilter,
786
        string $foreignLimit,
787
        bool $getTotal = false,
788
    ): ForeignData {
789
        // we always show the foreign field in the drop-down; if a display
790
        // field is defined, we show it besides the foreign field
791
        $foreignLink = false;
792
        $dispRow = $foreignDisplay = $theTotal = $foreignField = null;
793
        do {
794
            if ($foreigners === []) {
795
                break;
796
            }
797
798
            $foreigner = $this->searchColumnInForeigners($foreigners, $field);
799
            if ($foreigner == false) {
800
                break;
801
            }
802
803
            $foreignDb = $foreigner['foreign_db'];
804
            $foreignTable = $foreigner['foreign_table'];
805
            $foreignField = $foreigner['foreign_field'];
806
807
            // Count number of rows in the foreign table. Currently we do
808
            // not use a drop-down if more than ForeignKeyMaxLimit rows in the
809
            // foreign table,
810
            // for speed reasons and because we need a better interface for this.
811
            //
812
            // We could also do the SELECT anyway, with a LIMIT, and ensure that
813
            // the current value of the field is one of the choices.
814
815
            // Check if table has more rows than specified by ForeignKeyMaxLimit
816
            $moreThanLimit = $this->dbi->getTable($foreignDb, $foreignTable)
817
                ->checkIfMinRecordsExist($this->config->settings['ForeignKeyMaxLimit']);
818
819
            if ($overrideTotal || ! $moreThanLimit) {
820
                // foreign_display can be false if no display field defined:
821
                $foreignDisplay = $this->getDisplayField($foreignDb, $foreignTable);
822
823
                $fQueryMain = 'SELECT ' . Util::backquote($foreignField)
824
                    . (
825
                        $foreignDisplay === ''
826
                            ? ''
827
                            : ', ' . Util::backquote($foreignDisplay)
828
                    );
829
                $fQueryFrom = ' FROM ' . Util::backquote($foreignDb)
830
                    . '.' . Util::backquote($foreignTable);
831
                $fQueryFilter = $foreignFilter === '' ? '' : ' WHERE '
832
                    . Util::backquote($foreignField)
833
                    . ' LIKE ' . $this->dbi->quoteString(
834
                        '%' . $this->dbi->escapeMysqlWildcards($foreignFilter) . '%',
835
                    )
836
                    . (
837
                        $foreignDisplay === ''
838
                        ? ''
839
                        : ' OR ' . Util::backquote($foreignDisplay)
840
                        . ' LIKE ' . $this->dbi->quoteString(
841
                            '%' . $this->dbi->escapeMysqlWildcards($foreignFilter) . '%',
842
                        )
843
                    );
844
                $fQueryOrder = $foreignDisplay === '' ? '' : ' ORDER BY '
845
                    . Util::backquote($foreignTable) . '.'
846
                    . Util::backquote($foreignDisplay);
847
848
                $fQueryLimit = $foreignLimit;
849
850
                if ($foreignFilter !== '') {
851
                    $theTotal = $this->dbi->fetchValue('SELECT COUNT(*)' . $fQueryFrom . $fQueryFilter);
852
                }
853
854
                $disp = $this->dbi->tryQuery($fQueryMain . $fQueryFrom . $fQueryFilter . $fQueryOrder . $fQueryLimit);
855
                if ($disp && $disp->numRows() > 0) {
856
                    $dispRow = $disp->fetchAllAssoc();
857
                } else {
858
                    // Either no data in the foreign table or
859
                    // user does not have select permission to foreign table/field
860
                    // Show an input field with a 'Browse foreign values' link
861
                    $dispRow = null;
862
                    $foreignLink = true;
863
                }
864
            } else {
865
                $dispRow = null;
866
                $foreignLink = true;
867
            }
868
        } while (false);
869
870
        if ($getTotal && isset($foreignDb, $foreignTable)) {
871
            $theTotal = $this->dbi->getTable($foreignDb, $foreignTable)
872
                ->countRecords(true);
873
        }
874
875
        return new ForeignData(
876
            $foreignLink,
877
            (int) $theTotal,
878
            is_string($foreignDisplay) ? $foreignDisplay : '',
879
            $dispRow,
880
            $foreignField,
881
        );
882
    }
883
884
    /**
885
     * Rename a field in relation tables
886
     *
887
     * usually called after a column in a table was renamed
888
     *
889
     * @param string $db      database name
890
     * @param string $table   table name
891
     * @param string $field   old field name
892
     * @param string $newName new field name
893
     */
894
    public function renameField(string $db, string $table, string $field, string $newName): void
895
    {
896
        $relationParameters = $this->getRelationParameters();
897
898
        if ($relationParameters->displayFeature !== null) {
899
            $tableQuery = 'UPDATE '
900
                . Util::backquote($relationParameters->displayFeature->database) . '.'
901
                . Util::backquote($relationParameters->displayFeature->tableInfo)
902
                . '   SET display_field = ' . $this->dbi->quoteString($newName, ConnectionType::ControlUser)
903
                . ' WHERE db_name       = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser)
904
                . '   AND table_name    = ' . $this->dbi->quoteString($table, ConnectionType::ControlUser)
905
                . '   AND display_field = ' . $this->dbi->quoteString($field, ConnectionType::ControlUser);
906
            $this->dbi->queryAsControlUser($tableQuery);
907
        }
908
909
        if ($relationParameters->relationFeature === null) {
910
            return;
911
        }
912
913
        $tableQuery = 'UPDATE '
914
            . Util::backquote($relationParameters->relationFeature->database) . '.'
915
            . Util::backquote($relationParameters->relationFeature->relation)
916
            . '   SET master_field = ' . $this->dbi->quoteString($newName, ConnectionType::ControlUser)
917
            . ' WHERE master_db    = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser)
918
            . '   AND master_table = ' . $this->dbi->quoteString($table, ConnectionType::ControlUser)
919
            . '   AND master_field = ' . $this->dbi->quoteString($field, ConnectionType::ControlUser);
920
        $this->dbi->queryAsControlUser($tableQuery);
921
922
        $tableQuery = 'UPDATE '
923
            . Util::backquote($relationParameters->relationFeature->database) . '.'
924
            . Util::backquote($relationParameters->relationFeature->relation)
925
            . '   SET foreign_field = ' . $this->dbi->quoteString($newName, ConnectionType::ControlUser)
926
            . ' WHERE foreign_db    = ' . $this->dbi->quoteString($db, ConnectionType::ControlUser)
927
            . '   AND foreign_table = ' . $this->dbi->quoteString($table, ConnectionType::ControlUser)
928
            . '   AND foreign_field = ' . $this->dbi->quoteString($field, ConnectionType::ControlUser);
929
        $this->dbi->queryAsControlUser($tableQuery);
930
    }
931
932
    /**
933
     * Performs SQL query used for renaming table.
934
     *
935
     * @param string $sourceDb    Source database name
936
     * @param string $targetDb    Target database name
937
     * @param string $sourceTable Source table name
938
     * @param string $targetTable Target table name
939
     * @param string $dbField     Name of database field
940
     * @param string $tableField  Name of table field
941
     */
942 24
    public function renameSingleTable(
943
        DatabaseName $configStorageDatabase,
944
        TableName $configStorageTable,
945
        string $sourceDb,
946
        string $targetDb,
947
        string $sourceTable,
948
        string $targetTable,
949
        string $dbField,
950
        string $tableField,
951
    ): void {
952 24
        $query = 'UPDATE '
953 24
            . Util::backquote($configStorageDatabase) . '.'
954 24
            . Util::backquote($configStorageTable)
955 24
            . ' SET '
956 24
            . $dbField . ' = ' . $this->dbi->quoteString($targetDb, ConnectionType::ControlUser)
957 24
            . ', '
958 24
            . $tableField . ' = ' . $this->dbi->quoteString($targetTable, ConnectionType::ControlUser)
959 24
            . ' WHERE '
960 24
            . $dbField . '  = ' . $this->dbi->quoteString($sourceDb, ConnectionType::ControlUser)
961 24
            . ' AND '
962 24
            . $tableField . ' = ' . $this->dbi->quoteString($sourceTable, ConnectionType::ControlUser);
963 24
        $this->dbi->queryAsControlUser($query);
964
    }
965
966
    /**
967
     * Rename a table in relation tables
968
     *
969
     * usually called after table has been moved
970
     *
971
     * @param string $sourceDb    Source database name
972
     * @param string $targetDb    Target database name
973
     * @param string $sourceTable Source table name
974
     * @param string $targetTable Target table name
975
     */
976 28
    public function renameTable(string $sourceDb, string $targetDb, string $sourceTable, string $targetTable): void
977
    {
978 28
        $relationParameters = $this->getRelationParameters();
979
980
        // Move old entries from PMA-DBs to new table
981 28
        if ($relationParameters->columnCommentsFeature !== null) {
982 4
            $this->renameSingleTable(
983 4
                $relationParameters->columnCommentsFeature->database,
984 4
                $relationParameters->columnCommentsFeature->columnInfo,
985 4
                $sourceDb,
986 4
                $targetDb,
987 4
                $sourceTable,
988 4
                $targetTable,
989 4
                'db_name',
990 4
                'table_name',
991 4
            );
992
        }
993
994
        // updating bookmarks is not possible since only a single table is
995
        // moved, and not the whole DB.
996
997 28
        if ($relationParameters->displayFeature !== null) {
998 4
            $this->renameSingleTable(
999 4
                $relationParameters->displayFeature->database,
1000 4
                $relationParameters->displayFeature->tableInfo,
1001 4
                $sourceDb,
1002 4
                $targetDb,
1003 4
                $sourceTable,
1004 4
                $targetTable,
1005 4
                'db_name',
1006 4
                'table_name',
1007 4
            );
1008
        }
1009
1010 28
        if ($relationParameters->relationFeature !== null) {
1011 4
            $this->renameSingleTable(
1012 4
                $relationParameters->relationFeature->database,
1013 4
                $relationParameters->relationFeature->relation,
1014 4
                $sourceDb,
1015 4
                $targetDb,
1016 4
                $sourceTable,
1017 4
                $targetTable,
1018 4
                'foreign_db',
1019 4
                'foreign_table',
1020 4
            );
1021
1022 4
            $this->renameSingleTable(
1023 4
                $relationParameters->relationFeature->database,
1024 4
                $relationParameters->relationFeature->relation,
1025 4
                $sourceDb,
1026 4
                $targetDb,
1027 4
                $sourceTable,
1028 4
                $targetTable,
1029 4
                'master_db',
1030 4
                'master_table',
1031 4
            );
1032
        }
1033
1034 28
        if ($relationParameters->pdfFeature !== null) {
1035 8
            if ($sourceDb === $targetDb) {
1036
                // rename within the database can be handled
1037 4
                $this->renameSingleTable(
1038 4
                    $relationParameters->pdfFeature->database,
1039 4
                    $relationParameters->pdfFeature->tableCoords,
1040 4
                    $sourceDb,
1041 4
                    $targetDb,
1042 4
                    $sourceTable,
1043 4
                    $targetTable,
1044 4
                    'db_name',
1045 4
                    'table_name',
1046 4
                );
1047
            } else {
1048
                // if the table is moved out of the database we can no longer keep the
1049
                // record for table coordinate
1050 4
                $removeQuery = 'DELETE FROM '
1051 4
                    . Util::backquote($relationParameters->pdfFeature->database) . '.'
1052 4
                    . Util::backquote($relationParameters->pdfFeature->tableCoords)
1053 4
                    . ' WHERE db_name  = ' . $this->dbi->quoteString($sourceDb, ConnectionType::ControlUser)
1054 4
                    . ' AND table_name = ' . $this->dbi->quoteString($sourceTable, ConnectionType::ControlUser);
1055 4
                $this->dbi->queryAsControlUser($removeQuery);
1056
            }
1057
        }
1058
1059 28
        if ($relationParameters->uiPreferencesFeature !== null) {
1060 4
            $this->renameSingleTable(
1061 4
                $relationParameters->uiPreferencesFeature->database,
1062 4
                $relationParameters->uiPreferencesFeature->tableUiPrefs,
1063 4
                $sourceDb,
1064 4
                $targetDb,
1065 4
                $sourceTable,
1066 4
                $targetTable,
1067 4
                'db_name',
1068 4
                'table_name',
1069 4
            );
1070
        }
1071
1072 28
        if ($relationParameters->navigationItemsHidingFeature === null) {
1073 24
            return;
1074
        }
1075
1076
        // update hidden items inside table
1077 4
        $this->renameSingleTable(
1078 4
            $relationParameters->navigationItemsHidingFeature->database,
1079 4
            $relationParameters->navigationItemsHidingFeature->navigationHiding,
1080 4
            $sourceDb,
1081 4
            $targetDb,
1082 4
            $sourceTable,
1083 4
            $targetTable,
1084 4
            'db_name',
1085 4
            'table_name',
1086 4
        );
1087
1088
        // update data for hidden table
1089 4
        $query = 'UPDATE '
1090 4
            . Util::backquote($relationParameters->navigationItemsHidingFeature->database) . '.'
1091 4
            . Util::backquote($relationParameters->navigationItemsHidingFeature->navigationHiding)
1092 4
            . ' SET db_name = ' . $this->dbi->quoteString($targetDb, ConnectionType::ControlUser)
1093 4
            . ','
1094 4
            . ' item_name = ' . $this->dbi->quoteString($targetTable, ConnectionType::ControlUser)
1095 4
            . ' WHERE db_name  = ' . $this->dbi->quoteString($sourceDb, ConnectionType::ControlUser)
1096 4
            . ' AND item_name = ' . $this->dbi->quoteString($sourceTable, ConnectionType::ControlUser)
1097 4
            . " AND item_type = 'table'";
1098 4
        $this->dbi->queryAsControlUser($query);
1099
    }
1100
1101
    /**
1102
     * Create a PDF page
1103
     *
1104
     * @param string $newpage name of the new PDF page
1105
     * @param string $db      database name
1106
     */
1107
    public function createPage(string $newpage, PdfFeature $pdfFeature, string $db): int
1108
    {
1109
        $insQuery = 'INSERT INTO '
1110
            . Util::backquote($pdfFeature->database) . '.'
1111
            . Util::backquote($pdfFeature->pdfPages)
1112
            . ' (db_name, page_descr)'
1113
            . ' VALUES ('
1114
            . $this->dbi->quoteString($db, ConnectionType::ControlUser) . ', '
1115
            . $this->dbi->quoteString(
1116
                $newpage !== '' ? $newpage : __('no description'),
1117
                ConnectionType::ControlUser,
1118
            ) . ')';
1119
        $this->dbi->tryQueryAsControlUser($insQuery);
1120
1121
        return $this->dbi->insertId(ConnectionType::ControlUser);
1122
    }
1123
1124
    /**
1125
     * Get child table references for a table column.
1126
     * This works only if 'DisableIS' is false. An empty array is returned otherwise.
1127
     *
1128
     * @param string $db     name of master table db.
1129
     * @param string $table  name of master table.
1130
     * @param string $column name of master table column.
1131
     *
1132
     * @return array<list<mixed[]>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<list<mixed[]>> at position 2 could not be parsed: Expected '>' at position 2, but found 'list'.
Loading history...
1133
     */
1134
    public function getChildReferences(string $db, string $table, string $column = ''): array
1135
    {
1136
        if (! $this->config->selectedServer['DisableIS']) {
1137
            $relQuery = 'SELECT `column_name`, `table_name`,'
1138
                . ' `table_schema`, `referenced_column_name`'
1139
                . ' FROM `information_schema`.`key_column_usage`'
1140
                . ' WHERE `referenced_table_name` = '
1141
                . $this->dbi->quoteString($table)
1142
                . ' AND `referenced_table_schema` = '
1143
                . $this->dbi->quoteString($db);
1144
            if ($column !== '') {
1145
                $relQuery .= ' AND `referenced_column_name` = '
1146
                    . $this->dbi->quoteString($column);
1147
            }
1148
1149
            return $this->dbi->fetchResult(
1150
                $relQuery,
1151
                ['referenced_column_name', null],
1152
            );
1153
        }
1154
1155
        return [];
1156
    }
1157
1158
    /**
1159
     * Check child table references and foreign key for a table column.
1160
     *
1161
     * @param string                    $db                  name of master table db.
1162
     * @param string                    $table               name of master table.
1163
     * @param string                    $column              name of master table column.
1164
     * @param list<ForeignKey>|null     $foreignersFull      foreigners array for the whole table.
1165
     * @param array<list<mixed[]>>|null $childReferencesFull child references for the whole table.
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<list<mixed[]>>|null at position 2 could not be parsed: Expected '>' at position 2, but found 'list'.
Loading history...
1166
     *
1167
     * @return array<string, mixed> telling about references if foreign key.
1168
     * @psalm-return array{isEditable: bool, isForeignKey: bool, isReferenced: bool, references: string[]}
1169
     */
1170
    public function checkChildForeignReferences(
1171
        string $db,
1172
        string $table,
1173
        string $column,
1174
        array|null $foreignersFull = null,
1175
        array|null $childReferencesFull = null,
1176
    ): array {
1177
        $columnStatus = ['isEditable' => true, 'isReferenced' => false, 'isForeignKey' => false, 'references' => []];
1178
1179
        $foreigners = $foreignersFull ?? $this->getForeignKeysData($db, $table);
1180
1181
        $foreigner = $this->getColumnFromForeignKeysData($foreigners, $column);
1182
1183
        $childReferences = [];
1184
        if ($childReferencesFull !== null) {
1185
            if (isset($childReferencesFull[$column])) {
1186
                $childReferences = $childReferencesFull[$column];
1187
            }
1188
        } else {
1189
            $childReferences = $this->getChildReferences($db, $table, $column);
1190
        }
1191
1192
        if ($childReferences !== [] || $foreigner !== false) {
1193
            $columnStatus['isEditable'] = false;
1194
            if ($childReferences !== []) {
1195
                $columnStatus['isReferenced'] = true;
1196
                foreach ($childReferences as $columns) {
1197
                    $columnStatus['references'][] = Util::backquote($columns['table_schema'])
1198
                        . '.' . Util::backquote($columns['table_name']);
1199
                }
1200
            }
1201
1202
            if ($foreigner !== false) {
1203
                $columnStatus['isForeignKey'] = true;
1204
            }
1205
        }
1206
1207
        return $columnStatus;
1208
    }
1209
1210
    /**
1211
     * Search a table column in foreign data.
1212
     *
1213
     * @param mixed[] $foreigners Table Foreign data
1214
     * @param string  $column     Column name
1215
     */
1216 8
    public function searchColumnInForeigners(array $foreigners, string $column): array|false
1217
    {
1218 8
        if (isset($foreigners[$column])) {
1219
            return $foreigners[$column];
1220
        }
1221
1222 8
        if (! isset($foreigners['foreign_keys_data'])) {
1223 4
            return false;
1224
        }
1225
1226
        /** @var list<ForeignKey> $foreignKeysData */
1227 4
        $foreignKeysData = $foreigners['foreign_keys_data'];
1228
1229 4
        return $this->getColumnFromForeignKeysData($foreignKeysData, $column);
0 ignored issues
show
Bug introduced by
$foreignKeysData of type PhpMyAdmin\ConfigStorage\list is incompatible with the type array expected by parameter $foreignKeysData of PhpMyAdmin\ConfigStorage...mnFromForeignKeysData(). ( Ignorable by Annotation )

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

1229
        return $this->getColumnFromForeignKeysData(/** @scrutinizer ignore-type */ $foreignKeysData, $column);
Loading history...
1230
    }
1231
1232
    /**
1233
     * @param list<ForeignKey> $foreignKeysData
1234
     *
1235
     * @return false|array{
0 ignored issues
show
Documentation Bug introduced by
The doc comment false|array{ at position 4 could not be parsed: the token is null at position 4.
Loading history...
1236
     *  foreign_field: string,
1237
     *  foreign_db: string,
1238
     *  foreign_table: string|null,
1239
     *  constraint: string|null,
1240
     *  on_update: string,
1241
     *  on_delete: string
1242
     * }
1243
     */
1244 4
    public function getColumnFromForeignKeysData(array $foreignKeysData, string $column): array|false
1245
    {
1246 4
        $foreigner = [];
1247 4
        foreach ($foreignKeysData as $oneKey) {
1248 4
            $columnIndex = array_search($column, $oneKey->indexList);
1249 4
            if ($columnIndex !== false) {
1250 4
                $foreigner['foreign_field'] = $oneKey->refIndexList[$columnIndex];
1251 4
                $foreigner['foreign_db'] = $oneKey->refDbName ?? Current::$database;
1252 4
                $foreigner['foreign_table'] = $oneKey->refTableName;
1253 4
                $foreigner['constraint'] = $oneKey->constraint;
1254 4
                $foreigner['on_update'] = $oneKey->onUpdate ?? 'RESTRICT';
1255 4
                $foreigner['on_delete'] = $oneKey->onDelete ?? 'RESTRICT';
1256
1257 4
                return $foreigner;
1258
            }
1259
        }
1260
1261
        return false;
1262
    }
1263
1264
    /**
1265
     * Returns default PMA table names and their create queries.
1266
     *
1267
     * @param array<string, string> $tableNameReplacements
1268
     *
1269
     * @return array<string, string> table name, create query
1270
     */
1271 16
    public function getCreateTableSqlQueries(array $tableNameReplacements): array
1272
    {
1273 16
        $pmaTables = [];
1274 16
        $createTablesFile = (string) file_get_contents(SQL_DIR . 'create_tables.sql');
1275
1276 16
        $queries = explode(';', $createTablesFile);
1277
1278 16
        foreach ($queries as $query) {
1279 16
            if (preg_match('/CREATE TABLE IF NOT EXISTS `(.*)` \(/', $query, $table) !== 1) {
1280 16
                continue;
1281
            }
1282
1283 16
            $tableName = $table[1];
1284
1285
            // Replace the table name with another one
1286 16
            if (isset($tableNameReplacements[$tableName])) {
1287 8
                $query = str_replace($tableName, $tableNameReplacements[$tableName], $query);
1288
            }
1289
1290 16
            $pmaTables[$tableName] = $query . ';';
1291
        }
1292
1293 16
        return $pmaTables;
1294
    }
1295
1296
    /**
1297
     * Create a database to be used as configuration storage
1298
     */
1299 12
    public function createPmaDatabase(string $configurationStorageDbName): bool
1300
    {
1301 12
        $this->dbi->tryQuery(
1302 12
            'CREATE DATABASE IF NOT EXISTS ' . Util::backquote($configurationStorageDbName),
1303 12
            ConnectionType::ControlUser,
1304 12
        );
1305
1306 12
        $error = $this->dbi->getError(ConnectionType::ControlUser);
1307 12
        if ($error === '') {
1308
            // Re-build the cache to show the list of tables created or not
1309
            // This is the case when the DB could be created but no tables just after
1310
            // So just purge the cache and show the new configuration storage state
1311 4
            self::$cache = null;
1312 4
            $this->getRelationParameters();
1313
1314 4
            return true;
1315
        }
1316
1317 8
        Current::$message = Message::error($error);
1318
1319 8
        if (DatabaseInterface::$errorNumber === 1044) {
1320 4
            Current::$message = Message::error(sprintf(
1321 4
                __(
1322 4
                    'You do not have necessary privileges to create a database named'
1323 4
                    . ' \'%s\'. You may go to \'Operations\' tab of any'
1324 4
                    . ' database to set up the phpMyAdmin configuration storage there.',
1325 4
                ),
1326 4
                $configurationStorageDbName,
1327 4
            ));
1328
        }
1329
1330 8
        return false;
1331
    }
1332
1333
    /**
1334
     * Creates PMA tables in the given db, updates if already exists.
1335
     *
1336
     * @param string $db     database
1337
     * @param bool   $create whether to create tables if they don't exist.
1338
     */
1339 48
    public function fixPmaTables(string $db, bool $create = true): void
1340
    {
1341 48
        if ($this->arePmadbTablesAllDisabled()) {
1342
            return;
1343
        }
1344
1345 48
        $tablesToFeatures = [
1346 48
            'pma__bookmark' => 'bookmarktable',
1347 48
            'pma__relation' => 'relation',
1348 48
            'pma__table_info' => 'table_info',
1349 48
            'pma__table_coords' => 'table_coords',
1350 48
            'pma__pdf_pages' => 'pdf_pages',
1351 48
            'pma__column_info' => 'column_info',
1352 48
            'pma__history' => 'history',
1353 48
            'pma__recent' => 'recent',
1354 48
            'pma__favorite' => 'favorite',
1355 48
            'pma__table_uiprefs' => 'table_uiprefs',
1356 48
            'pma__tracking' => 'tracking',
1357 48
            'pma__userconfig' => 'userconfig',
1358 48
            'pma__users' => 'users',
1359 48
            'pma__usergroups' => 'usergroups',
1360 48
            'pma__navigationhiding' => 'navigationhiding',
1361 48
            'pma__savedsearches' => 'savedsearches',
1362 48
            'pma__central_columns' => 'central_columns',
1363 48
            'pma__designer_settings' => 'designer_settings',
1364 48
            'pma__export_templates' => 'export_templates',
1365 48
        ];
1366
1367 48
        $existingTables = $this->dbi->getTables($db, ConnectionType::ControlUser);
1368
1369 48
        $tableNameReplacements = $this->getTableReplacementNames($tablesToFeatures);
1370
1371 48
        $createQueries = [];
1372 48
        if ($create) {
1373 12
            $createQueries = $this->getCreateTableSqlQueries($tableNameReplacements);
1374 12
            if (! $this->dbi->selectDb($db, ConnectionType::ControlUser)) {
1375
                Current::$message = Message::error($this->dbi->getError(ConnectionType::ControlUser));
1376
1377
                return;
1378
            }
1379
        }
1380
1381 48
        $foundOne = false;
1382 48
        foreach ($tablesToFeatures as $table => $feature) {
1383 48
            if (($this->config->selectedServer[$feature] ?? null) === false) {
1384
                // The feature is disabled by the user in config
1385 8
                continue;
1386
            }
1387
1388
            // Check if the table already exists
1389
            // use the possible replaced name first and fallback on the table name
1390
            // if no replacement exists
1391 48
            if (! in_array($tableNameReplacements[$table] ?? $table, $existingTables, true)) {
1392 48
                if (! $create) {
1393 36
                    continue;
1394
                }
1395
1396 12
                $this->dbi->tryQuery($createQueries[$table], ConnectionType::ControlUser);
1397
1398 12
                $error = $this->dbi->getError(ConnectionType::ControlUser);
1399 12
                if ($error !== '') {
1400 4
                    Current::$message = Message::error($error);
1401
1402 4
                    return;
1403
                }
1404
            }
1405
1406 32
            $foundOne = true;
1407
1408
            // Do not override a user defined value, only fill if empty
1409 32
            if (isset($this->config->selectedServer[$feature]) && $this->config->selectedServer[$feature] !== '') {
1410 12
                continue;
1411
            }
1412
1413
            // Fill it with the default table name
1414 24
            $this->config->selectedServer[$feature] = $table;
1415
        }
1416
1417 44
        if (! $foundOne) {
1418 16
            return;
1419
        }
1420
1421 32
        $this->config->selectedServer['pmadb'] = $db;
1422
1423
        // Unset the cache as new tables might have been added
1424 32
        self::$cache = null;
1425
        // Fill back the cache
1426 32
        $this->getRelationParameters();
1427
    }
1428
1429
    /**
1430
     * Verifies that all pmadb features are disabled
1431
     */
1432 60
    public function arePmadbTablesAllDisabled(): bool
1433
    {
1434 60
        return ($this->config->selectedServer['bookmarktable'] ?? null) === false
1435 60
            && ($this->config->selectedServer['relation'] ?? null) === false
1436 60
            && ($this->config->selectedServer['table_info'] ?? null) === false
1437 60
            && ($this->config->selectedServer['table_coords'] ?? null) === false
1438 60
            && ($this->config->selectedServer['column_info'] ?? null) === false
1439 60
            && ($this->config->selectedServer['pdf_pages'] ?? null) === false
1440 60
            && ($this->config->selectedServer['history'] ?? null) === false
1441 60
            && ($this->config->selectedServer['recent'] ?? null) === false
1442 60
            && ($this->config->selectedServer['favorite'] ?? null) === false
1443 60
            && ($this->config->selectedServer['table_uiprefs'] ?? null) === false
1444 60
            && ($this->config->selectedServer['tracking'] ?? null) === false
1445 60
            && ($this->config->selectedServer['userconfig'] ?? null) === false
1446 60
            && ($this->config->selectedServer['users'] ?? null) === false
1447 60
            && ($this->config->selectedServer['usergroups'] ?? null) === false
1448 60
            && ($this->config->selectedServer['navigationhiding'] ?? null) === false
1449 60
            && ($this->config->selectedServer['savedsearches'] ?? null) === false
1450 60
            && ($this->config->selectedServer['central_columns'] ?? null) === false
1451 60
            && ($this->config->selectedServer['designer_settings'] ?? null) === false
1452 60
            && ($this->config->selectedServer['export_templates'] ?? null) === false;
1453
    }
1454
1455
    /**
1456
     * Verifies if all the pmadb tables are defined
1457
     */
1458 4
    public function arePmadbTablesDefined(): bool
1459
    {
1460 4
        return ! (empty($this->config->selectedServer['bookmarktable'])
1461 4
            || empty($this->config->selectedServer['relation'])
1462 4
            || empty($this->config->selectedServer['table_info'])
1463 4
            || empty($this->config->selectedServer['table_coords'])
1464 4
            || empty($this->config->selectedServer['column_info'])
1465 4
            || empty($this->config->selectedServer['pdf_pages'])
1466 4
            || empty($this->config->selectedServer['history'])
1467 4
            || empty($this->config->selectedServer['recent'])
1468 4
            || empty($this->config->selectedServer['favorite'])
1469 4
            || empty($this->config->selectedServer['table_uiprefs'])
1470 4
            || empty($this->config->selectedServer['tracking'])
1471 4
            || empty($this->config->selectedServer['userconfig'])
1472 4
            || empty($this->config->selectedServer['users'])
1473 4
            || empty($this->config->selectedServer['usergroups'])
1474 4
            || empty($this->config->selectedServer['navigationhiding'])
1475 4
            || empty($this->config->selectedServer['savedsearches'])
1476 4
            || empty($this->config->selectedServer['central_columns'])
1477 4
            || empty($this->config->selectedServer['designer_settings'])
1478 4
            || empty($this->config->selectedServer['export_templates']));
1479
    }
1480
1481
    /**
1482
     * Get tables for foreign key constraint
1483
     *
1484
     * @param string $foreignDb     Database name
1485
     * @param string $storageEngine Table storage engine
1486
     *
1487
     * @return string[] Table names
1488
     */
1489 4
    public function getTables(string $foreignDb, string $storageEngine): array
1490
    {
1491 4
        $tablesRows = $this->dbi->query(
1492 4
            'SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '
1493 4
            . $this->dbi->quoteString($foreignDb)
1494 4
            . ' AND ENGINE = ' . $this->dbi->quoteString($storageEngine),
1495 4
        );
1496
        /** @var list<string> $tables */
1497 4
        $tables = $tablesRows->fetchAllColumn();
1498
1499 4
        if ($this->config->settings['NaturalOrder']) {
1500 4
            usort($tables, strnatcasecmp(...));
0 ignored issues
show
Bug introduced by
$tables of type PhpMyAdmin\ConfigStorage\list is incompatible with the type array expected by parameter $array of usort(). ( Ignorable by Annotation )

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

1500
            usort(/** @scrutinizer ignore-type */ $tables, strnatcasecmp(...));
Loading history...
1501
        }
1502
1503 4
        return $tables;
1504
    }
1505
1506
    public function getConfigurationStorageDbName(): string
1507
    {
1508
        $cfgStorageDbName = $this->config->selectedServer['pmadb'] ?? '';
1509
1510
        // Use "phpmyadmin" as a default database name to check to keep the behavior consistent
1511
        return empty($cfgStorageDbName) ? 'phpmyadmin' : $cfgStorageDbName;
1512
    }
1513
1514
    /**
1515
     * This function checks and initializes the phpMyAdmin configuration
1516
     * storage state before it is used into session cache.
1517
     */
1518 28
    public function initRelationParamsCache(): void
1519
    {
1520 28
        $storageDbName = $this->config->selectedServer['pmadb'] ?? '';
1521
        // Use "phpmyadmin" as a default database name to check to keep the behavior consistent
1522 28
        $storageDbName = $storageDbName !== '' ? $storageDbName : 'phpmyadmin';
1523
1524
        // This will make users not having explicitly listed databases
1525
        // have config values filled by the default phpMyAdmin storage table name values
1526 28
        $this->fixPmaTables($storageDbName, false);
1527
1528
        // This global will be changed if fixPmaTables did find one valid table
1529
        // Empty means that until now no pmadb was found eligible
1530 28
        if ($this->config->selectedServer['pmadb'] !== '') {
1531 20
            return;
1532
        }
1533
1534 8
        $this->fixPmaTables(Current::$database, false);
1535
    }
1536
1537
    /**
1538
     * @param non-empty-array<string, string> $tablesToFeatures
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-array<string, string> at position 0 could not be parsed: Unknown type name 'non-empty-array' at position 0 in non-empty-array<string, string>.
Loading history...
1539
     *
1540
     * @return array<string, string>
1541
     */
1542 48
    private function getTableReplacementNames(array $tablesToFeatures): array
1543
    {
1544 48
        $tableNameReplacements = [];
1545
1546 48
        foreach ($tablesToFeatures as $table => $feature) {
1547 48
            if (empty($this->config->selectedServer[$feature]) || $this->config->selectedServer[$feature] === $table) {
1548 48
                continue;
1549
            }
1550
1551
            // Set the replacement to transform the default table name into a custom name
1552 12
            $tableNameReplacements[$table] = $this->config->selectedServer[$feature];
1553
        }
1554
1555 48
        return $tableNameReplacements;
1556
    }
1557
}
1558