DatabaseInterface::getTablesFull()   F
last analyzed

Complexity

Conditions 53
Paths > 20000

Size

Total Lines 245
Code Lines 120

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 54
CRAP Score 567.7504

Importance

Changes 0
Metric Value
eloc 120
c 0
b 0
f 0
dl 0
loc 245
rs 0
ccs 54
cts 125
cp 0.432
cc 53
nc 80484
nop 9
crap 567.7504

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\Dbal;
6
7
use PhpMyAdmin\Column;
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\Column 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...
8
use PhpMyAdmin\Config;
9
use PhpMyAdmin\Config\Settings\Server;
10
use PhpMyAdmin\Current;
11
use PhpMyAdmin\Error\ErrorHandler;
12
use PhpMyAdmin\FieldMetadata;
13
use PhpMyAdmin\Html\Generator;
14
use PhpMyAdmin\I18n\LanguageManager;
15
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...
16
use PhpMyAdmin\Indexes\Index;
17
use PhpMyAdmin\ListDatabase;
18
use PhpMyAdmin\Query\Cache;
19
use PhpMyAdmin\Query\Compatibility;
20
use PhpMyAdmin\Query\Generator as QueryGenerator;
21
use PhpMyAdmin\Query\Utilities;
22
use PhpMyAdmin\Routing\Routing;
23
use PhpMyAdmin\SqlParser\Context;
24
use PhpMyAdmin\StorageEngine;
25
use PhpMyAdmin\Table\Table;
26
use PhpMyAdmin\Tracking\Tracker;
27
use PhpMyAdmin\Types;
28
use PhpMyAdmin\UserPrivilegesFactory;
29
use PhpMyAdmin\Util;
30
use PhpMyAdmin\Utils\SessionCache;
31
use stdClass;
32
33
use function __;
34
use function array_column;
35
use function array_combine;
36
use function array_diff;
37
use function array_keys;
38
use function array_map;
39
use function array_multisort;
40
use function array_reverse;
41
use function array_shift;
42
use function array_slice;
43
use function basename;
44
use function closelog;
45
use function explode;
46
use function implode;
47
use function is_array;
48
use function is_int;
49
use function mb_strtolower;
50
use function microtime;
51
use function openlog;
52
use function sprintf;
53
use function str_contains;
54
use function str_replace;
55
use function str_starts_with;
56
use function stripos;
57
use function strnatcasecmp;
58
use function strtolower;
59
use function strtoupper;
60
use function strtr;
61
use function substr;
62
use function syslog;
63
use function uasort;
64
use function uksort;
65
use function usort;
66
67
use const LOG_INFO;
68
use const LOG_NDELAY;
69
use const LOG_PID;
70
use const LOG_USER;
71
use const SORT_ASC;
72
use const SORT_DESC;
73
74
/**
75
 * Main interface for database interactions
76
 */
77
class DatabaseInterface
78
{
79
    public static self|null $instance = null;
80
81
    /**
82
     * Do not read all rows immediately.
83
     */
84
    public const QUERY_UNBUFFERED = true;
85
86
    /**
87
     * Get session variable.
88
     */
89
    public const GETVAR_SESSION = 1;
90
91
    /**
92
     * Get global variable.
93
     */
94
    public const GETVAR_GLOBAL = 2;
95
96
    public const FETCH_NUM = 'NUM';
97
    public const FETCH_ASSOC = 'ASSOC';
98
99
    /**
100
     * Opened database connections.
101
     *
102
     * @var array<int, Connection>
103
     * @psalm-var array<value-of<ConnectionType>, Connection>
104
     */
105
    private array $connections = [];
106
107
    /** @var array<int, string>|null */
108
    private array|null $currentUserAndHost = null;
109
110
    /** @var array<int, array<int, string>>|null Current role and host cache */
111
    private array|null $currentRoleAndHost = null;
112
113
    /**
114
     * @var int|null lower_case_table_names value cache
115
     * @psalm-var 0|1|2|null
116
     */
117
    private int|null $lowerCaseTableNames = null;
118
119
    /** @var bool Whether connection is MariaDB */
120
    private bool $isMariaDb = false;
121
    /** @var bool Whether connection is Percona */
122
    private bool $isPercona = false;
123
    /** @var int Server version as number */
124
    private int $versionInt = 55000;
125
    /** @var string Server version */
126
    private string $versionString = '5.50.0';
127
    /** @var string Server version comment */
128
    private string $versionComment = '';
129
130
    /** @var Types MySQL types data */
131
    public Types $types;
132
133
    private Cache $cache;
134
135
    public float $lastQueryExecutionTime = 0;
136
137
    private ListDatabase|null $databaseList = null;
138
139
    /** @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...
140
    private static int|string $cachedAffectedRows = -1;
141
142
    public static int|null $errorNumber = null;
143
144
    /** @param DbiExtension $extension Object to be used for database queries */
145 452
    private function __construct(private DbiExtension $extension, private readonly Config $config)
146
    {
147 452
        $this->cache = new Cache();
148 452
        $this->types = new Types($this);
149
    }
150
151
    /** @deprecated Use dependency injection instead. */
152 16
    public static function getInstance(Config|null $config = null): self
153
    {
154 16
        if (self::$instance === null) {
155 4
            self::$instance = new self(new DbiMysqli(), $config ?? Config::getInstance());
0 ignored issues
show
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

155
            self::$instance = new self(new DbiMysqli(), $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...
156
        }
157
158 16
        return self::$instance;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::instance could return the type null which is incompatible with the type-hinted return PhpMyAdmin\Dbal\DatabaseInterface. Consider adding an additional type-check to rule them out.
Loading history...
159
    }
160
161 452
    public static function getInstanceForTest(DbiExtension $extension, Config|null $config = null): self
162
    {
163 452
        $instance = new self($extension, $config ?? Config::getInstance());
0 ignored issues
show
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

163
        $instance = new self($extension, $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...
164 452
        $instance->connections[ConnectionType::User->value] = new Connection(new stdClass());
165 452
        $instance->connections[ConnectionType::ControlUser->value] = new Connection(new stdClass());
166
167 452
        return $instance;
168
    }
169
170 8
    public function query(
171
        string $query,
172
        ConnectionType $connectionType = ConnectionType::User,
173
        bool $unbuffered = false,
174
        bool $cacheAffectedRows = true,
175
    ): ResultInterface {
176 8
        $result = $this->tryQuery($query, $connectionType, $unbuffered, $cacheAffectedRows);
177
178 8
        if (! $result) {
179
            Generator::mysqlDie($this->getError($connectionType), $query);
180
        }
181
182 8
        return $result;
183
    }
184
185
    public function getCache(): Cache
186
    {
187
        return $this->cache;
188
    }
189
190 300
    public function tryQuery(
191
        string $query,
192
        ConnectionType $connectionType = ConnectionType::User,
193
        bool $unbuffered = false,
194
        bool $cacheAffectedRows = true,
195
    ): ResultInterface|false {
196 300
        if (! isset($this->connections[$connectionType->value])) {
197 24
            return false;
198
        }
199
200 276
        $time = microtime(true);
201
202 276
        $result = $this->extension->realQuery($query, $this->connections[$connectionType->value], $unbuffered);
203
204 276
        if ($connectionType === ConnectionType::User) {
205 272
            $this->lastQueryExecutionTime = microtime(true) - $time;
206
        }
207
208 276
        if ($cacheAffectedRows) {
209 8
            self::$cachedAffectedRows = $this->affectedRows($connectionType, false);
210
        }
211
212 276
        if ($this->config->config->debug->sql) {
213 4
            $errorMessage = $this->getError($connectionType);
214 4
            Utilities::debugLogQueryIntoSession(
215 4
                $query,
216 4
                $errorMessage !== '' ? $errorMessage : null,
217 4
                $result,
218 4
                $this->lastQueryExecutionTime,
219 4
            );
220 4
            if ($this->config->config->debug->sqllog) {
221
                openlog('phpMyAdmin', LOG_NDELAY | LOG_PID, LOG_USER);
222
223
                syslog(
224
                    LOG_INFO,
225
                    sprintf(
226
                        'SQL[%s?route=%s]: %0.3f(W:%d,C:%s,L:0x%02X) > %s',
227
                        basename($_SERVER['SCRIPT_NAME']),
228
                        Routing::$route,
229
                        $this->lastQueryExecutionTime,
230
                        $this->getWarningCount($connectionType),
231
                        $cacheAffectedRows ? 'y' : 'n',
232
                        $connectionType->value,
233
                        $query,
234
                    ),
235
                );
236
                closelog();
237
            }
238
        }
239
240 276
        if ($result !== false && Tracker::isActive()) {
241
            Tracker::handleQuery($query);
242
        }
243
244 276
        return $result;
245
    }
246
247
    /**
248
     * Send multiple SQL queries to the database server and execute the first one
249
     *
250
     * @param string $multiQuery multi query statement to execute
251
     */
252
    public function tryMultiQuery(
253
        string $multiQuery = '',
254
        ConnectionType $connectionType = ConnectionType::User,
255
    ): bool {
256
        if (! isset($this->connections[$connectionType->value])) {
257
            return false;
258
        }
259
260
        return $this->extension->realMultiQuery($this->connections[$connectionType->value], $multiQuery);
261
    }
262
263
    /**
264
     * Executes a query as controluser.
265
     * The result is always buffered and never cached
266
     *
267
     * @param string $sql the query to execute
268
     *
269
     * @return ResultInterface the result set
270
     */
271 4
    public function queryAsControlUser(string $sql): ResultInterface
272
    {
273
        // Avoid caching of the number of rows affected; for example, this function
274
        // is called for tracking purposes but we want to display the correct number
275
        // of rows affected by the original query, not by the query generated for
276
        // tracking.
277 4
        return $this->query($sql, ConnectionType::ControlUser, cacheAffectedRows: false);
278
    }
279
280
    /**
281
     * Executes a query as controluser.
282
     * The result is always buffered and never cached
283
     *
284
     * @param string $sql the query to execute
285
     *
286
     * @return ResultInterface|false the result set, or false if the query failed
287
     */
288 4
    public function tryQueryAsControlUser(string $sql): ResultInterface|false
289
    {
290
        // Avoid caching of the number of rows affected; for example, this function
291
        // is called for tracking purposes but we want to display the correct number
292
        // of rows affected by the original query, not by the query generated for
293
        // tracking.
294 4
        return $this->tryQuery($sql, ConnectionType::ControlUser, cacheAffectedRows: false);
295
    }
296
297
    /**
298
     * returns array with table names for given db
299
     *
300
     * @param string $database name of database
301
     *
302
     * @return array<int, string>   tables names
303
     */
304 20
    public function getTables(string $database, ConnectionType $connectionType = ConnectionType::User): array
305
    {
306 20
        if ($database === '') {
307 4
            return [];
308
        }
309
310 16
        $result = $this->tryQuery(
311 16
            'SHOW TABLES FROM ' . Util::backquote($database) . ';',
312 16
            $connectionType,
313 16
            cacheAffectedRows: false,
314 16
        );
315
316 16
        if ($result === false) {
317 4
            return [];
318
        }
319
320
        /** @var list<string> $tables */
321 12
        $tables = $result->fetchAllColumn();
322
323 12
        if ($this->config->settings['NaturalOrder']) {
324 8
            usort($tables, 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...
Bug introduced by
$tables of type PhpMyAdmin\Dbal\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

324
            usort(/** @scrutinizer ignore-type */ $tables, strnatcasecmp(...));
Loading history...
325
        }
326
327 12
        return $tables;
328
    }
329
330
    /**
331
     * returns array of all tables in given db or dbs
332
     * this function expects unquoted names:
333
     * RIGHT: my_database
334
     * WRONG: `my_database`
335
     * WRONG: my\_database
336
     * if $tbl_is_group is true, $table is used as filter for table names
337
     *
338
     * <code>
339
     * $dbi->getTablesFull('my_database');
340
     * $dbi->getTablesFull('my_database', 'my_table'));
341
     * $dbi->getTablesFull('my_database', 'my_tables_', true));
342
     * </code>
343
     *
344
     * @param string         $database     database
345
     * @param string|mixed[] $table        table name(s)
346
     * @param bool           $tableIsGroup $table is a table group
347
     * @param int            $limitOffset  zero-based offset for the count
348
     * @param bool|int       $limitCount   number of tables to return
349
     * @param string         $sortBy       table attribute to sort by
350
     * @param string         $sortOrder    direction to sort (ASC or DESC)
351
     * @param string|null    $tableType    whether table or view
352
     *
353
     * @return (string|int|null)[][]           list of tables in given db(s)
354
     *
355
     * @todo    move into Table
356
     */
357 12
    public function getTablesFull(
358
        string $database,
359
        string|array $table = '',
360
        bool $tableIsGroup = false,
361
        int $limitOffset = 0,
362
        bool|int $limitCount = false,
363
        string $sortBy = 'Name',
364
        string $sortOrder = 'ASC',
365
        string|null $tableType = null,
366
        ConnectionType $connectionType = ConnectionType::User,
367
    ): array {
368 12
        if ($limitCount === true) {
369
            $limitCount = $this->config->settings['MaxTableList'];
370
        }
371
372 12
        $tables = [];
373 12
        $pagingApplied = false;
374
375 12
        if ($limitCount && is_array($table) && $sortBy === 'Name') {
376
            if ($sortOrder === 'DESC') {
377
                $table = array_reverse($table);
378
            }
379
380
            $table = array_slice($table, $limitOffset, $limitCount);
381
            $pagingApplied = true;
382
        }
383
384 12
        if (! $this->config->selectedServer['DisableIS']) {
385 4
            $sqlWhereTable = '';
386 4
            if ($table !== [] && $table !== '') {
387
                if (is_array($table)) {
388
                    $sqlWhereTable = QueryGenerator::getTableNameConditionForMultiple(
389
                        array_map($this->quoteString(...), $table),
390
                    );
391
                } else {
392
                    $sqlWhereTable = QueryGenerator::getTableNameCondition(
393
                        $this->quoteString($tableIsGroup ? $this->escapeMysqlWildcards($table) : $table),
394
                        $tableIsGroup,
395
                    );
396
                }
397
            }
398
399 4
            $sqlWhereTable .= QueryGenerator::getTableTypeCondition($tableType);
400
401
            // for PMA bc:
402
            // `SCHEMA_FIELD_NAME` AS `SHOW_TABLE_STATUS_FIELD_NAME`
403
            //
404
            // on non-Windows servers,
405
            // added BINARY in the WHERE clause to force a case sensitive
406
            // comparison (if we are looking for the db Aa we don't want
407
            // to find the db aa)
408
409 4
            $sql = QueryGenerator::getSqlForTablesFull($this->quoteString($database), $sqlWhereTable);
410
411
            // Sort the tables
412 4
            $sql .= ' ORDER BY ' . $sortBy . ' ' . $sortOrder;
413
414 4
            if ($limitCount && ! $pagingApplied) {
415
                $sql .= ' LIMIT ' . $limitCount . ' OFFSET ' . $limitOffset;
416
            }
417
418
            /** @var (string|int|null)[][][] $tables */
419 4
            $tables = $this->fetchResultMultidimensional(
420 4
                $sql,
421 4
                ['TABLE_SCHEMA', 'TABLE_NAME'],
422 4
                null,
423 4
                $connectionType,
424 4
            );
425
426
            // here, we check for Mroonga engine and compute the good data_length and index_length
427
            // in the StructureController only we need to sum the two values as the other engines
428 4
            foreach ($tables as $oneDatabaseName => $oneDatabaseTables) {
429 4
                foreach ($oneDatabaseTables as $oneTableName => $oneTableData) {
430 4
                    if ($oneTableData['Engine'] !== 'Mroonga') {
431 4
                        continue;
432
                    }
433
434
                    if (! StorageEngine::hasMroongaEngine()) {
435
                        continue;
436
                    }
437
438
                    [
439
                        $tables[$oneDatabaseName][$oneTableName]['Data_length'],
440
                        $tables[$oneDatabaseName][$oneTableName]['Index_length'],
441
                    ] = StorageEngine::getMroongaLengths((string) $oneDatabaseName, (string) $oneTableName);
442
                }
443
            }
444
445 4
            if ($sortBy === 'Name' && $this->config->settings['NaturalOrder']) {
446
                // here, the array's first key is by schema name
447 4
                foreach ($tables as $oneDatabaseName => $oneDatabaseTables) {
448 4
                    uksort($oneDatabaseTables, strnatcasecmp(...));
449
450 4
                    if ($sortOrder === 'DESC') {
451
                        $oneDatabaseTables = array_reverse($oneDatabaseTables);
452
                    }
453
454 4
                    $tables[$oneDatabaseName] = $oneDatabaseTables;
455
                }
456
            } elseif ($sortBy === 'Data_length') {
457
                // Size = Data_length + Index_length
458
                foreach ($tables as $oneDatabaseName => $oneDatabaseTables) {
459
                    uasort(
460
                        $oneDatabaseTables,
461
                        static function (array $a, array $b): int {
462
                            $aLength = $a['Data_length'] + $a['Index_length'];
463
                            $bLength = $b['Data_length'] + $b['Index_length'];
464
465
                            return $aLength <=> $bLength;
466
                        },
467
                    );
468
469
                    if ($sortOrder === 'DESC') {
470
                        $oneDatabaseTables = array_reverse($oneDatabaseTables);
471
                    }
472
473
                    $tables[$oneDatabaseName] = $oneDatabaseTables;
474
                }
475
            }
476
477
            // on windows with lower_case_table_names = 1
478
            // MySQL returns
479
            // with SHOW DATABASES or information_schema.SCHEMATA: `Test`
480
            // but information_schema.TABLES gives `test`
481
            // see https://github.com/phpmyadmin/phpmyadmin/issues/8402
482 4
            $tables = $tables[$database]
483 4
                ?? $tables[mb_strtolower($database)]
484
                ?? [];
485
        }
486
487
        // If permissions are wrong on even one database directory,
488
        // information_schema does not return any table info for any database
489
        // this is why we fall back to SHOW TABLE STATUS even for MySQL >= 50002
490 12
        if ($tables === []) {
491 8
            $sql = 'SHOW TABLE STATUS FROM ' . Util::backquote($database);
492 8
            if (($table !== '' && $table !== []) || $tableIsGroup || ($tableType !== null && $tableType !== '')) {
493
                $sql .= ' WHERE';
494
                $needAnd = false;
495
                if (($table !== '' && $table !== []) || $tableIsGroup) {
496
                    if (is_array($table)) {
497
                        $sql .= ' `Name` IN ('
498
                            . implode(
499
                                ', ',
500
                                array_map(
501
                                    fn (string $string): string => $this->quoteString($string, $connectionType),
502
                                    $table,
503
                                ),
504
                            ) . ')';
505
                    } else {
506
                        $sql .= ' `Name` LIKE '
507
                            . $this->quoteString($this->escapeMysqlWildcards($table) . '%', $connectionType);
508
                    }
509
510
                    $needAnd = true;
511
                }
512
513
                if ($tableType !== null && $tableType !== '') {
514
                    if ($needAnd) {
515
                        $sql .= ' AND';
516
                    }
517
518
                    if ($tableType === 'view') {
519
                        $sql .= " `Comment` = 'VIEW'";
520
                    } elseif ($tableType === 'table') {
521
                        $sql .= " `Comment` != 'VIEW'";
522
                    }
523
                }
524
            }
525
526
            /** @var (string|int|null)[][] $eachTables */
527 8
            $eachTables = $this->fetchResult($sql, 'Name', null, $connectionType);
528
529
            // here, we check for Mroonga engine and compute the good data_length and index_length
530
            // in the StructureController only we need to sum the two values as the other engines
531 8
            foreach ($eachTables as $tableName => $tableData) {
532 8
                if ($tableData['Engine'] !== 'Mroonga') {
533 8
                    continue;
534
                }
535
536
                if (! StorageEngine::hasMroongaEngine()) {
537
                    continue;
538
                }
539
540
                [
541
                    $eachTables[$tableName]['Data_length'],
542
                    $eachTables[$tableName]['Index_length'],
543
                ] = StorageEngine::getMroongaLengths($database, (string) $tableName);
544
            }
545
546
            // Sort naturally if the config allows it and we're sorting
547
            // the Name column.
548 8
            if ($sortBy === 'Name' && $this->config->settings['NaturalOrder']) {
549 4
                uksort($eachTables, strnatcasecmp(...));
550
551 4
                if ($sortOrder === 'DESC') {
552
                    $eachTables = array_reverse($eachTables);
553
                }
554
            } else {
555
                // Prepare to sort by creating array of the selected sort
556
                // value to pass to array_multisort
557
558
                // Size = Data_length + Index_length
559 4
                $sortValues = [];
560 4
                if ($sortBy === 'Data_length') {
561
                    foreach ($eachTables as $tableName => $tableData) {
562
                        $sortValues[$tableName] = strtolower(
563
                            (string) ($tableData['Data_length']
564
                            + $tableData['Index_length']),
565
                        );
566
                    }
567
                } else {
568 4
                    foreach ($eachTables as $tableName => $tableData) {
569 4
                        $sortValues[$tableName] = strtolower($tableData[$sortBy] ?? '');
570
                    }
571
                }
572
573 4
                if ($sortValues !== []) {
574
                    // See https://stackoverflow.com/a/32461188 for the explanation of below hack
575 4
                    $keys = array_keys($eachTables);
576 4
                    if ($sortOrder === 'DESC') {
577
                        array_multisort($sortValues, SORT_DESC, $eachTables, $keys);
0 ignored issues
show
Bug introduced by
SORT_DESC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

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

577
                        array_multisort($sortValues, /** @scrutinizer ignore-type */ SORT_DESC, $eachTables, $keys);
Loading history...
578
                    } else {
579 4
                        array_multisort($sortValues, SORT_ASC, $eachTables, $keys);
0 ignored issues
show
Bug introduced by
SORT_ASC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

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

579
                        array_multisort($sortValues, /** @scrutinizer ignore-type */ SORT_ASC, $eachTables, $keys);
Loading history...
580
                    }
581
582 4
                    $eachTables = array_combine($keys, $eachTables);
583
                }
584
585
                // cleanup the temporary sort array
586 4
                unset($sortValues);
587
            }
588
589 8
            if ($limitCount && ! $pagingApplied) {
590
                $eachTables = array_slice($eachTables, $limitOffset, $limitCount, true);
591
            }
592
593 8
            $tables = Compatibility::getISCompatForGetTablesFull($eachTables, $database);
594
        }
595
596 12
        if ($tables !== []) {
597
            // cache table data, so Table does not require to issue SHOW TABLE STATUS again
598 12
            $this->cache->cacheTableData($database, $tables);
599
        }
600
601 12
        return $tables;
602
    }
603
604
    /**
605
     * returns array with databases containing extended infos about them
606
     *
607
     * @param string|null $database    database
608
     * @param bool        $forceStats  retrieve stats also for MySQL < 5
609
     * @param string      $sortBy      column to order by
610
     * @param string      $sortOrder   ASC or DESC
611
     * @param int         $limitOffset starting offset for LIMIT
612
     * @param bool|int    $limitCount  row count for LIMIT or true for $cfg['MaxDbList']
613
     *
614
     * @return mixed[]
615
     *
616
     * @todo    move into ListDatabase?
617
     */
618 4
    public function getDatabasesFull(
619
        string|null $database = null,
620
        bool $forceStats = false,
621
        ConnectionType $connectionType = ConnectionType::User,
622
        string $sortBy = 'SCHEMA_NAME',
623
        string $sortOrder = 'ASC',
624
        int $limitOffset = 0,
625
        bool|int $limitCount = false,
626
    ): array {
627 4
        $sortOrder = strtoupper($sortOrder);
628
629 4
        if ($limitCount === true) {
630
            $limitCount = $this->config->settings['MaxDbList'];
631
        }
632
633 4
        $applyLimitAndOrderManual = true;
634
635 4
        if (! $this->config->selectedServer['DisableIS']) {
636
            /**
637
             * if NaturalOrder config is enabled, we cannot use LIMIT
638
             * cause MySQL does not support natural ordering,
639
             * we have to do it afterward
640
             */
641
            $limit = '';
642
            if (! $this->config->settings['NaturalOrder']) {
643
                if ($limitCount) {
644
                    $limit = ' LIMIT ' . $limitCount . ' OFFSET ' . $limitOffset;
645
                }
646
647
                $applyLimitAndOrderManual = false;
648
            }
649
650
            // get table information from information_schema
651
            $sqlWhereSchema = '';
652
            if ($database !== null) {
653
                $sqlWhereSchema = 'WHERE `SCHEMA_NAME` LIKE ' . $this->quoteString($database, $connectionType);
654
            }
655
656
            $sql = QueryGenerator::getInformationSchemaDatabasesFullRequest(
657
                $forceStats,
658
                $sqlWhereSchema,
659
                $sortBy,
660
                $sortOrder,
661
                $limit,
662
            );
663
664
            $databases = $this->fetchResult($sql, 'SCHEMA_NAME', null, $connectionType);
665
666
            $mysqlError = $this->getError($connectionType);
667
            if ($databases === [] && self::$errorNumber !== null) {
668
                Generator::mysqlDie($mysqlError, $sql);
669
            }
670
671
            // display only databases also in official database list
672
            // f.e. to apply hide_db and only_db
673
            $drops = array_diff(
674
                array_keys($databases),
675
                (array) $this->getDatabaseList(),
676
            );
677
            foreach ($drops as $drop) {
678
                unset($databases[$drop]);
679
            }
680
        } else {
681 4
            $databases = [];
682 4
            foreach ($this->getDatabaseList() as $databaseName) {
683
                // Compatibility with INFORMATION_SCHEMA output
684 4
                $databases[$databaseName]['SCHEMA_NAME'] = $databaseName;
685
686 4
                $databases[$databaseName]['DEFAULT_COLLATION_NAME'] = $this->getDbCollation($databaseName);
687
688 4
                if (! $forceStats) {
689
                    continue;
690
                }
691
692
                // get additional info about tables
693 4
                $databases[$databaseName]['SCHEMA_TABLES'] = 0;
694 4
                $databases[$databaseName]['SCHEMA_TABLE_ROWS'] = 0;
695 4
                $databases[$databaseName]['SCHEMA_DATA_LENGTH'] = 0;
696 4
                $databases[$databaseName]['SCHEMA_MAX_DATA_LENGTH'] = 0;
697 4
                $databases[$databaseName]['SCHEMA_INDEX_LENGTH'] = 0;
698 4
                $databases[$databaseName]['SCHEMA_LENGTH'] = 0;
699 4
                $databases[$databaseName]['SCHEMA_DATA_FREE'] = 0;
700
701 4
                $res = $this->query(
702 4
                    'SHOW TABLE STATUS FROM '
703 4
                    . Util::backquote($databaseName) . ';',
704 4
                );
705
706 4
                while ($row = $res->fetchAssoc()) {
707 4
                    $databases[$databaseName]['SCHEMA_TABLES']++;
708 4
                    $databases[$databaseName]['SCHEMA_TABLE_ROWS'] += $row['Rows'];
709 4
                    $databases[$databaseName]['SCHEMA_DATA_LENGTH'] += $row['Data_length'];
710 4
                    $databases[$databaseName]['SCHEMA_MAX_DATA_LENGTH'] += $row['Max_data_length'];
711 4
                    $databases[$databaseName]['SCHEMA_INDEX_LENGTH'] += $row['Index_length'];
712
713
                    // for InnoDB, this does not contain the number of
714
                    // overhead bytes but the total free space
715 4
                    if ($row['Engine'] !== 'InnoDB') {
716
                        $databases[$databaseName]['SCHEMA_DATA_FREE'] += $row['Data_free'];
717
                    }
718
719 4
                    $databases[$databaseName]['SCHEMA_LENGTH'] += $row['Data_length'] + $row['Index_length'];
720
                }
721
722 4
                unset($res);
723
            }
724
        }
725
726
        /**
727
         * apply limit and order manually now
728
         * (caused by older MySQL < 5 or NaturalOrder config)
729
         */
730 4
        if ($applyLimitAndOrderManual) {
731 4
            usort(
732 4
                $databases,
733 4
                static fn ($a, $b): int => Utilities::usortComparisonCallback($a, $b, $sortBy, $sortOrder),
734 4
            );
735
736
            /**
737
             * now apply limit
738
             */
739 4
            if ($limitCount) {
740 4
                $databases = array_slice($databases, $limitOffset, $limitCount);
741
            }
742
        }
743
744 4
        return $databases;
745
    }
746
747
    /**
748
     * Returns description of a $column in given table
749
     *
750
     * @param string $database name of database
751
     * @param string $table    name of table to retrieve columns from
752
     * @param string $column   name of column
753
     */
754 8
    public function getColumn(
755
        string $database,
756
        string $table,
757
        string $column,
758
        ConnectionType $connectionType = ConnectionType::User,
759
    ): Column|null {
760 8
        $sql = QueryGenerator::getColumns(
761 8
            $this->quoteString($database, $connectionType),
762 8
            $this->quoteString($table, $connectionType),
763 8
            $this->quoteString($column, $connectionType),
764 8
        );
765 8
        $fields = $this->fetchResult($sql, 'Field', null, $connectionType);
766
767
        /**
768
         * @var array{
769
         *  Field: string,
770
         *  Type: string,
771
         *  Collation: string|null,
772
         *  Null:'YES'|'NO',
773
         *  Key: string,
774
         *  Default: string|null,
775
         *  Extra: string,
776
         *  Privileges: string,
777
         *  Comment: string
778
         * }[] $columns
779
         */
780 8
        $columns = $this->attachIndexInfoToColumns($database, $table, $fields);
781
782 8
        $columns = $this->convertToColumns($columns);
783
784 8
        return array_shift($columns);
785
    }
786
787
    /**
788
     * Returns descriptions of columns in given table
789
     *
790
     * @param string $database name of database
791
     * @param string $table    name of table to retrieve columns from
792
     *
793
     * @return Column[]
794
     */
795
    public function getColumns(
796
        string $database,
797
        string $table,
798
        ConnectionType $connectionType = ConnectionType::User,
799
    ): array {
800
        $sql = QueryGenerator::getColumns(
801
            $this->quoteString($database, $connectionType),
802
            $this->quoteString($table, $connectionType),
803
        );
804
        $fields = $this->fetchResult($sql, 'Field', null, $connectionType);
805
806
        /**
807
         * @var array{
808
         *  Field: string,
809
         *  Type: string,
810
         *  Collation: string|null,
811
         *  Null:'YES'|'NO',
812
         *  Key: string,
813
         *  Default: string|null,
814
         *  Extra: string,
815
         *  Privileges: string,
816
         *  Comment: string
817
         * }[] $columns
818
         */
819
        $columns = $this->attachIndexInfoToColumns($database, $table, $fields);
820
821
        return $this->convertToColumns($columns);
822
    }
823
824
    /**
825
     * Attach index information to the column definition
826
     *
827
     * @param string            $database name of database
828
     * @param string            $table    name of table to retrieve columns from
829
     * @param (string|null)[][] $fields   column array indexed by their names
830
     *
831
     * @return (string|null)[][] Column defintions with index information
832
     */
833 8
    private function attachIndexInfoToColumns(
834
        string $database,
835
        string $table,
836
        array $fields,
837
    ): array {
838 8
        if ($fields === []) {
839
            return [];
840
        }
841
842
        // Check if column is a part of multiple-column index and set its 'Key'.
843 8
        $indexes = Index::getFromTable($this, $table, $database);
844 8
        foreach ($fields as $field => $fieldData) {
845 8
            if (! empty($fieldData['Key'])) {
846
                continue;
847
            }
848
849 8
            foreach ($indexes as $index) {
850
                if (! $index->hasColumn((string) $field)) {
851
                    continue;
852
                }
853
854
                $indexColumns = $index->getColumns();
855
                if ($indexColumns[$field]->getSeqInIndex() <= 1) {
856
                    continue;
857
                }
858
859
                $fields[$field]['Key'] = $index->isUnique() ? 'UNI' : 'MUL';
860
            }
861
        }
862
863 8
        return $fields;
864
    }
865
866
    /**
867
     * @psalm-param array{
868
     *  Field: string,
869
     *  Type: string,
870
     *  Collation: string|null,
871
     *  Null:'YES'|'NO',
872
     *  Key: string,
873
     *  Default: string|null,
874
     *  Extra: string,
875
     *  Privileges: string,
876
     *  Comment: string
877
     * }[] $fields   column array indexed by their names
878
     *
879
     * @return Column[]
880
     */
881 8
    private function convertToColumns(array $fields): array
882
    {
883 8
        $columns = [];
884 8
        foreach ($fields as $field => $column) {
885 8
            $columns[$field] = new Column(
886 8
                $column['Field'],
887 8
                $column['Type'],
888 8
                $column['Collation'],
889 8
                $column['Null'] === 'YES',
890 8
                $column['Key'],
891
                // null means lack of default value, 'NULL' means default value is NULL
892 8
                $column['Default'] === null || $column['Default'] === 'NULL'
893 8
                    ? null
894 8
                    : Util::unquoteDefaultValue($column['Default']),
895 8
                $column['Extra'],
896 8
                $column['Privileges'],
897 8
                $column['Comment'],
898 8
            );
899
        }
900
901 8
        return $columns;
902
    }
903
904
    /**
905
     * Returns all column names in given table
906
     *
907
     * @param string $database name of database
908
     * @param string $table    name of table to retrieve columns from
909
     *
910
     * @return list<string>
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\Dbal\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...
911
     */
912
    public function getColumnNames(
913
        string $database,
914
        string $table,
915
        ConnectionType $connectionType = ConnectionType::User,
916
    ): array {
917
        $sql = QueryGenerator::getColumnNamesAndTypes(
918
            $this->quoteString($database, $connectionType),
919
            $this->quoteString($table, $connectionType),
920
        );
921
922
        $result = $this->tryQuery($sql, $connectionType, cacheAffectedRows: false);
923
924
        if ($result === false) {
925
            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\Dbal\list.
Loading history...
926
        }
927
928
        // We only need the 'Field' column which contains the table's column names
929
        return array_column($result->fetchAllAssoc(), 'COLUMN_NAME');
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_column($res...Assoc(), 'COLUMN_NAME') returns the type array which is incompatible with the documented return type PhpMyAdmin\Dbal\list.
Loading history...
930
    }
931
932
    /**
933
     * Returns indexes of a table
934
     *
935
     * @param string $database name of database
936
     * @param string $table    name of the table whose indexes are to be retrieved
937
     *
938
     * @return array<int, array<string, string|null>>
939
     * @psalm-return array<int, array{
940
     *   Table: string,
941
     *   Non_unique: '0'|'1',
942
     *   Key_name: string,
943
     *   Seq_in_index: string,
944
     *   Column_name: string|null,
945
     *   Collation: 'A'|'D'|null,
946
     *   Cardinality: string,
947
     *   Sub_part: string|null,
948
     *   Packed: string|null,
949
     *   Null: string|null,
950
     *   Index_type: 'BTREE'|'FULLTEXT'|'HASH'|'RTREE',
951
     *   Comment: string,
952
     *   Index_comment: string,
953
     *   Ignored?: string,
954
     *   Visible?: string,
955
     *   Expression?: string|null
956
     * }>
957
     */
958 8
    public function getTableIndexes(
959
        string $database,
960
        string $table,
961
        ConnectionType $connectionType = ConnectionType::User,
962
    ): array {
963 8
        $sql = QueryGenerator::getTableIndexesSql($database, $table);
964
965 8
        return $this->fetchResultSimple($sql, $connectionType);
966
    }
967
968
    /**
969
     * returns value of given mysql server variable
970
     *
971
     * @param string $var  mysql server variable name
972
     * @param int    $type DatabaseInterface::GETVAR_SESSION | DatabaseInterface::GETVAR_GLOBAL
973
     *
974
     * @return false|string|null value for mysql server variable
975
     */
976
    public function getVariable(
977
        string $var,
978
        int $type = self::GETVAR_SESSION,
979
        ConnectionType $connectionType = ConnectionType::User,
980
    ): false|string|null {
981
        $modifier = match ($type) {
982
            self::GETVAR_SESSION => ' SESSION',
983
            self::GETVAR_GLOBAL => ' GLOBAL',
984
            default => '',
985
        };
986
987
        return $this->fetchValue('SHOW' . $modifier . ' VARIABLES LIKE \'' . $var . '\';', 1, $connectionType);
988
    }
989
990
    /**
991
     * Sets new value for a variable if it is different from the current value
992
     *
993
     * @param string $var   variable name
994
     * @param string $value value to set
995
     */
996
    public function setVariable(
997
        string $var,
998
        string $value,
999
        ConnectionType $connectionType = ConnectionType::User,
1000
    ): void {
1001
        $currentValue = (string) $this->getVariable($var, self::GETVAR_SESSION, $connectionType);
1002
        if ($currentValue === $value) {
1003
            return;
1004
        }
1005
1006
        $this->query('SET ' . $var . ' = ' . $value . ';', $connectionType);
1007
    }
1008
1009 28
    public function getDefaultCharset(): string
1010
    {
1011 28
        return $this->versionInt > 50503 ? 'utf8mb4' : 'utf8';
1012
    }
1013
1014 24
    public function getDefaultCollation(): string
1015
    {
1016 24
        return $this->versionInt > 50503 ? 'utf8mb4_general_ci' : 'utf8_general_ci';
1017
    }
1018
1019
    /**
1020
     * Function called just after a connection to the MySQL database server has
1021
     * been established. It sets the connection collation, and determines the
1022
     * version of MySQL which is running.
1023
     */
1024 24
    public function postConnect(Server $currentServer): void
1025
    {
1026 24
        $version = $this->fetchSingleRow('SELECT @@version, @@version_comment');
1027
1028 24
        if ($version !== []) {
1029 20
            $this->setVersion($version);
1030
        }
1031
1032 24
        $this->query(
1033 24
            sprintf('SET NAMES \'%s\' COLLATE \'%s\';', $this->getDefaultCharset(), $this->getDefaultCollation()),
1034 24
        );
1035
1036
        /* Locale for messages */
1037 24
        $locale = LanguageManager::getInstance()->getCurrentLanguage()->getMySQLLocale();
1038 24
        if ($locale !== '') {
1039 24
            $this->tryQuery("SET lc_messages = '" . $locale . "';");
1040
        }
1041
1042
        // Set timezone for the session, if required.
1043 24
        if ($currentServer->sessionTimeZone !== '') {
1044
            $sqlQueryTz = 'SET ' . Util::backquote('time_zone') . ' = '
1045
                . $this->quoteString($currentServer->sessionTimeZone);
1046
1047
            if (! $this->tryQuery($sqlQueryTz)) {
1048
                $errorHandler = ErrorHandler::getInstance();
1049
                $errorHandler->addUserError(
1050
                    sprintf(
1051
                        __(
1052
                            'Unable to use timezone "%1$s" for server %2$d. '
1053
                            . 'Please check your configuration setting for '
1054
                            . '[em]$cfg[\'Servers\'][%3$d][\'SessionTimeZone\'][/em]. '
1055
                            . 'phpMyAdmin is currently using the default time zone '
1056
                            . 'of the database server.',
1057
                        ),
1058
                        $currentServer->sessionTimeZone,
1059
                        Current::$server,
1060
                        Current::$server,
1061
                    ),
1062
                );
1063
            }
1064
        }
1065
1066
        /* Loads closest context to this version. */
1067 24
        Context::loadClosest(($this->isMariaDb ? 'MariaDb' : 'MySql') . $this->versionInt);
1068
1069 24
        $this->databaseList = null;
1070
    }
1071
1072
    /**
1073
     * Sets collation connection for user link
1074
     *
1075
     * @param string $collation collation to set
1076
     */
1077 4
    public function setCollation(string $collation): void
1078
    {
1079 4
        $charset = $this->getDefaultCharset();
1080
        /* Automatically adjust collation if not supported by server */
1081 4
        if ($charset === 'utf8' && str_starts_with($collation, 'utf8mb4_')) {
1082 4
            $collation = 'utf8_' . substr($collation, 8);
1083
        }
1084
1085 4
        $result = $this->tryQuery(
1086 4
            'SET collation_connection = '
1087 4
            . $this->quoteString($collation)
1088 4
            . ';',
1089 4
        );
1090
1091 4
        if ($result === false) {
1092
            $errorHandler = ErrorHandler::getInstance();
1093
            $errorHandler->addUserError(__('Failed to set configured collation connection!'));
1094
1095
            return;
1096
        }
1097
    }
1098
1099
    /**
1100
     * returns a single value from the given result or query,
1101
     * if the query or the result has more than one row or field
1102
     * the first field of the first row is returned
1103
     *
1104
     * <code>
1105
     * $sql = 'SELECT `name` FROM `user` WHERE `id` = 123';
1106
     * $user_name = $dbi->fetchValue($sql);
1107
     * // produces
1108
     * // $user_name = 'John Doe'
1109
     * </code>
1110
     *
1111
     * @param string     $query The query to execute
1112
     * @param int|string $field field to fetch the value from, starting at 0, with 0 being default
1113
     *
1114
     * @return string|false|null value of first field in first row from result or false if not found
1115
     */
1116 208
    public function fetchValue(
1117
        string $query,
1118
        int|string $field = 0,
1119
        ConnectionType $connectionType = ConnectionType::User,
1120
    ): string|false|null {
1121 208
        $result = $this->tryQuery($query, $connectionType, cacheAffectedRows: false);
1122 208
        if ($result === false) {
1123 32
            return false;
1124
        }
1125
1126 200
        return $result->fetchValue($field);
1127
    }
1128
1129
    /**
1130
     * Returns only the first row from the result or null if result is empty.
1131
     *
1132
     * <code>
1133
     * $sql = 'SELECT * FROM `user` WHERE `id` = 123';
1134
     * $user = $dbi->fetchSingleRow($sql);
1135
     * // produces
1136
     * // $user = array('id' => 123, 'name' => 'John Doe')
1137
     * </code>
1138
     *
1139
     * @param string $query The query to execute
1140
     * @param string $type  NUM|ASSOC|BOTH returned array should either numeric associative or both
1141
     * @psalm-param DatabaseInterface::FETCH_NUM|DatabaseInterface::FETCH_ASSOC $type
1142
     *
1143
     * @return array<string|null>
1144
     */
1145
    public function fetchSingleRow(
1146
        string $query,
1147
        string $type = self::FETCH_ASSOC,
1148
        ConnectionType $connectionType = ConnectionType::User,
1149
    ): array {
1150
        $result = $this->tryQuery($query, $connectionType, cacheAffectedRows: false);
1151
        if ($result === false) {
1152
            return [];
1153
        }
1154
1155
        return $this->fetchByMode($result, $type);
1156
    }
1157
1158
    /**
1159
     * returns array of rows with numeric or associative keys
1160
     *
1161
     * @param ResultInterface $result result set identifier
1162
     * @param string          $mode   either self::FETCH_NUM, self::FETCH_ASSOC or self::FETCH_BOTH
1163
     * @psalm-param self::FETCH_NUM|self::FETCH_ASSOC $mode
1164
     *
1165
     * @return array<string|null>
1166
     */
1167 16
    private function fetchByMode(ResultInterface $result, string $mode): array
1168
    {
1169 16
        return $mode === self::FETCH_NUM ? $result->fetchRow() : $result->fetchAssoc();
1170
    }
1171
1172
    /**
1173
     * returns all rows in the resultset in one array
1174
     *
1175
     * <code>
1176
     * $sql = 'SELECT `id`, `name` FROM `user`';
1177
     * $users = $dbi->fetchResult($sql, 'id');
1178
     * // produces
1179
     * // $users['123'] = array('id' => 123, 'name' => 'John Doe')
1180
     *
1181
     * $sql = 'SELECT `id`, `name` FROM `user`';
1182
     * $users = $dbi->fetchResult($sql, 0);
1183
     * // produces
1184
     * // $users['123'] = array(0 => 123, 1 => 'John Doe')
1185
     *
1186
     * $sql = 'SELECT `id`, `name` FROM `user`';
1187
     * $users = $dbi->fetchResult($sql, 'id', 'name');
1188
     * // or
1189
     * $users = $dbi->fetchResult($sql, 0, 1);
1190
     * // produces
1191
     * // $users['123'] = 'John Doe'
1192
     * </code>
1193
     *
1194
     * @psalm-return ($column is null ? array<array<string|null>> : array<string|null>)
1195
     */
1196 16
    public function fetchResult(
1197
        string $query,
1198
        string|int $key,
1199
        string|int|null $column = null,
1200
        ConnectionType $connectionType = ConnectionType::User,
1201
    ): array {
1202 16
        $resultRows = [];
1203
1204 16
        $result = $this->tryQuery($query, $connectionType, cacheAffectedRows: false);
1205
1206
        // return empty array if result is empty or false
1207 16
        if ($result === false) {
1208
            return [];
1209
        }
1210
1211 16
        if ($key === 0 && $column === 1) {
1212
            return $result->fetchAllKeyPair();
1213
        }
1214
1215
        // if $key is an integer use non associative mysql fetch function
1216 16
        $fetchFunction = is_int($key) ? self::FETCH_NUM : self::FETCH_ASSOC;
1217
1218 16
        while ($row = $this->fetchByMode($result, $fetchFunction)) {
1219 16
            $resultRows[$row[$key]] = $column === null ? $row : $row[$column];
1220
        }
1221
1222 16
        return $resultRows;
1223
    }
1224
1225
    /**
1226
     * returns all rows in the resultset in one array
1227
     *
1228
     * <code>
1229
     * $sql = 'SELECT `group`, `name` FROM `user`'
1230
     * $users = $dbi->fetchResult($sql, array('group', null), 'name');
1231
     * // produces
1232
     * // $users['admin'][] = 'John Doe'
1233
     *
1234
     * $sql = 'SELECT `group`, `name` FROM `user`'
1235
     * $users = $dbi->fetchResult($sql, array('group', 'name'), 'id');
1236
     * // produces
1237
     * // $users['admin']['John Doe'] = '123'
1238
     * </code>
1239
     *
1240
     * @param array<array-key|null> $keys
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key|null> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key|null>.
Loading history...
1241
     *
1242
     * @return array<mixed>
1243
     */
1244 4
    public function fetchResultMultidimensional(
1245
        string $query,
1246
        array $keys,
1247
        string|int|null $column = null,
1248
        ConnectionType $connectionType = ConnectionType::User,
1249
    ): array {
1250 4
        $resultRows = [];
1251
1252 4
        $result = $this->tryQuery($query, $connectionType, cacheAffectedRows: false);
1253
1254
        // return empty array if result is empty or false
1255 4
        if ($result === false) {
1256
            return [];
1257
        }
1258
1259 4
        while ($row = $result->fetchAssoc()) {
1260 4
            $resultTarget =& $resultRows;
1261 4
            foreach ($keys as $keyIndex) {
1262 4
                if ($keyIndex === null) {
1263
                    $resultTarget =& $resultTarget[];
1264
                    continue;
1265
                }
1266
1267 4
                if (! isset($resultTarget[$row[$keyIndex]])) {
1268 4
                    $resultTarget[$row[$keyIndex]] = [];
1269
                }
1270
1271 4
                $resultTarget =& $resultTarget[$row[$keyIndex]];
1272
            }
1273
1274 4
            $resultTarget = $column === null ? $row : $row[$column];
1275
        }
1276
1277 4
        return $resultRows;
1278
    }
1279
1280
    /** @return list<array<string|null>> */
1281 8
    public function fetchResultSimple(
1282
        string $query,
1283
        ConnectionType $connectionType = ConnectionType::User,
1284
    ): array {
1285 8
        $result = $this->tryQuery($query, $connectionType, cacheAffectedRows: false);
1286
1287 8
        if ($result === false) {
1288
            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\Dbal\list.
Loading history...
1289
        }
1290
1291 8
        return $result->fetchAllAssoc();
1292
    }
1293
1294
    /** @return list<string|null> */
1295 28
    public function fetchSingleColumn(
1296
        string $query,
1297
        ConnectionType $connectionType = ConnectionType::User,
1298
    ): array {
1299 28
        $result = $this->tryQuery($query, $connectionType, cacheAffectedRows: false);
1300
1301 28
        if ($result === false) {
1302
            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\Dbal\list.
Loading history...
1303
        }
1304
1305 28
        return $result->fetchAllColumn();
1306
    }
1307
1308
    /**
1309
     * Get supported SQL compatibility modes
1310
     *
1311
     * @return string[] supported SQL compatibility modes
1312
     */
1313 4
    public function getCompatibilities(): array
1314
    {
1315 4
        return [
1316 4
            'NONE',
1317 4
            'ANSI',
1318 4
            'DB2',
1319 4
            'MAXDB',
1320 4
            'MYSQL323',
1321 4
            'MYSQL40',
1322 4
            'MSSQL',
1323 4
            'ORACLE',
1324
            // removed; in MySQL 5.0.33, this produces exports that
1325
            // can't be read by POSTGRESQL (see our bug #1596328)
1326
            // 'POSTGRESQL',
1327 4
            'TRADITIONAL',
1328 4
        ];
1329
    }
1330
1331
    /**
1332
     * returns warnings for last query
1333
     *
1334
     * @return Warning[] warnings
1335
     */
1336 12
    public function getWarnings(ConnectionType $connectionType = ConnectionType::User): array
1337
    {
1338 12
        $result = $this->tryQuery('SHOW WARNINGS', $connectionType, cacheAffectedRows: false);
1339 12
        if ($result === false) {
1340 4
            return [];
1341
        }
1342
1343 8
        $warnings = [];
1344 8
        while ($row = $result->fetchAssoc()) {
1345 4
            $warnings[] = Warning::fromArray($row);
1346
        }
1347
1348 8
        return $warnings;
1349
    }
1350
1351
    /**
1352
     * gets the current user with host
1353
     *
1354
     * @return string the current user i.e. user@host
1355
     */
1356 96
    public function getCurrentUser(): string
1357
    {
1358 96
        if (SessionCache::has('mysql_cur_user')) {
1359 8
            return SessionCache::get('mysql_cur_user');
1360
        }
1361
1362 96
        $user = $this->fetchValue('SELECT CURRENT_USER();');
1363 96
        if ($user !== false) {
1364 88
            SessionCache::set('mysql_cur_user', $user);
1365
1366 88
            return $user;
1367
        }
1368
1369 8
        return '@';
1370
    }
1371
1372
    /**
1373
     * gets the current role with host. Role maybe multiple separated by comma
1374
     * Support start from MySQL 8.x / MariaDB 10.0.5
1375
     *
1376
     * @see https://dev.mysql.com/doc/refman/8.0/en/roles.html
1377
     * @see https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_current-role
1378
     * @see https://mariadb.com/kb/en/mariadb-1005-release-notes/#newly-implemented-features
1379
     * @see https://mariadb.com/kb/en/roles_overview/
1380
     *
1381
     * @return list<string> the current roles i.e. array of role@host
1382
     */
1383 108
    public function getCurrentRoles(): array
1384
    {
1385 108
        if (($this->isMariaDB() && $this->getVersion() < 100500) || $this->getVersion() < 80000) {
1386 24
            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\Dbal\list.
Loading history...
1387
        }
1388
1389 84
        if (SessionCache::has('mysql_cur_role')) {
1390 28
            return SessionCache::get('mysql_cur_role');
1391
        }
1392
1393 84
        $role = $this->fetchValue('SELECT CURRENT_ROLE();');
1394 84
        if ($role === false || $role === null || $role === 'NONE') {
1395 32
            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\Dbal\list.
Loading history...
1396
        }
1397
1398 52
        $role = array_map('trim', explode(',', str_replace('`', '', $role)));
1399 52
        SessionCache::set('mysql_cur_role', $role);
1400
1401 52
        return $role;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $role returns the type array which is incompatible with the documented return type PhpMyAdmin\Dbal\list.
Loading history...
1402
    }
1403
1404 32
    public function isSuperUser(): bool
1405
    {
1406 32
        if (SessionCache::has('is_superuser')) {
1407 16
            return (bool) SessionCache::get('is_superuser');
1408
        }
1409
1410 16
        if (! $this->isConnected()) {
1411 4
            return false;
1412
        }
1413
1414 12
        $isSuperUser = (bool) $this->fetchValue('SELECT 1 FROM mysql.user LIMIT 1');
1415
1416 12
        SessionCache::set('is_superuser', $isSuperUser);
1417
1418 12
        return $isSuperUser;
1419
    }
1420
1421 60
    public function isGrantUser(): bool
1422
    {
1423 60
        if (SessionCache::has('is_grantuser')) {
1424 8
            return (bool) SessionCache::get('is_grantuser');
1425
        }
1426
1427 52
        if (! $this->isConnected()) {
1428 4
            return false;
1429
        }
1430
1431 48
        $hasGrantPrivilege = false;
1432
1433 48
        if ($this->config->selectedServer['DisableIS']) {
1434 8
            $grants = $this->getCurrentUserGrants();
1435
1436 8
            foreach ($grants as $grant) {
1437 8
                if (str_contains($grant, 'WITH GRANT OPTION')) {
1438 4
                    $hasGrantPrivilege = true;
1439 4
                    break;
1440
                }
1441
            }
1442
1443 8
            SessionCache::set('is_grantuser', $hasGrantPrivilege);
1444
1445 8
            return $hasGrantPrivilege;
1446
        }
1447
1448 40
        $collation = $this->getServerCollation();
1449
1450 40
        [$user, $host] = $this->getCurrentUserAndHost();
1451 40
        $query = QueryGenerator::getInformationSchemaDataForGranteeRequest($user, $host, $collation);
1452 40
        $hasGrantPrivilege = (bool) $this->fetchValue($query);
1453
1454 40
        if (! $hasGrantPrivilege) {
1455 36
            foreach ($this->getCurrentRolesAndHost() as [$role, $roleHost]) {
1456 12
                $query = QueryGenerator::getInformationSchemaDataForGranteeRequest($role, $roleHost, $collation);
1457 12
                $hasGrantPrivilege = (bool) $this->fetchValue($query);
1458
1459 12
                if ($hasGrantPrivilege) {
1460 4
                    break;
1461
                }
1462
            }
1463
        }
1464
1465 40
        SessionCache::set('is_grantuser', $hasGrantPrivilege);
1466
1467 40
        return $hasGrantPrivilege;
1468
    }
1469
1470 68
    public function isCreateUser(): bool
1471
    {
1472 68
        if (SessionCache::has('is_createuser')) {
1473 8
            return (bool) SessionCache::get('is_createuser');
1474
        }
1475
1476 60
        if (! $this->isConnected()) {
1477 4
            return false;
1478
        }
1479
1480 56
        $hasCreatePrivilege = false;
1481
1482 56
        if ($this->config->selectedServer['DisableIS']) {
1483 16
            $grants = $this->getCurrentUserGrants();
1484
1485 16
            foreach ($grants as $grant) {
1486 16
                if (str_contains($grant, 'ALL PRIVILEGES ON *.*') || str_contains($grant, 'CREATE USER')) {
1487 8
                    $hasCreatePrivilege = true;
1488 8
                    break;
1489
                }
1490
            }
1491
1492 16
            SessionCache::set('is_createuser', $hasCreatePrivilege);
1493
1494 16
            return $hasCreatePrivilege;
1495
        }
1496
1497 40
        $collation = $this->getServerCollation();
1498
1499 40
        [$user, $host] = $this->getCurrentUserAndHost();
1500 40
        $query = QueryGenerator::getInformationSchemaDataForCreateRequest($user, $host, $collation);
1501 40
        $hasCreatePrivilege = (bool) $this->fetchValue($query);
1502
1503 40
        if (! $hasCreatePrivilege) {
1504 36
            foreach ($this->getCurrentRolesAndHost() as [$role, $roleHost]) {
1505 12
                $query = QueryGenerator::getInformationSchemaDataForCreateRequest($role, $roleHost, $collation);
1506 12
                $hasCreatePrivilege = (bool) $this->fetchValue($query);
1507
1508 12
                if ($hasCreatePrivilege) {
1509 4
                    break;
1510
                }
1511
            }
1512
        }
1513
1514 40
        SessionCache::set('is_createuser', $hasCreatePrivilege);
1515
1516 40
        return $hasCreatePrivilege;
1517
    }
1518
1519 128
    public function isConnected(): bool
1520
    {
1521 128
        return isset($this->connections[ConnectionType::User->value]);
1522
    }
1523
1524
    /** @return string[] */
1525 24
    private function getCurrentUserGrants(): array
1526
    {
1527
        /** @var string[] $grants */
1528 24
        $grants = $this->fetchSingleColumn('SHOW GRANTS FOR CURRENT_USER();');
1529
1530 24
        return $grants;
1531
    }
1532
1533
    /**
1534
     * Get the current user and host
1535
     *
1536
     * @return array<int, string> array of username and hostname
1537
     */
1538 96
    public function getCurrentUserAndHost(): array
1539
    {
1540 96
        if ($this->currentUserAndHost === null) {
1541 96
            $user = $this->getCurrentUser();
1542 96
            $this->currentUserAndHost = explode('@', $user);
1543
        }
1544
1545 96
        return $this->currentUserAndHost;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->currentUserAndHost could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
1546
    }
1547
1548
    /**
1549
     * Get the current role and host.
1550
     *
1551
     * @return array<int, array<int, string>> array of role and hostname
1552
     */
1553 108
    public function getCurrentRolesAndHost(): array
1554
    {
1555 108
        if ($this->currentRoleAndHost === null) {
1556 108
            $roles = $this->getCurrentRoles();
1557
1558 108
            $this->currentRoleAndHost = array_map(static function (string $role) {
1559 52
                return str_contains($role, '@') ? explode('@', $role) : [$role, ''];
1560 108
            }, $roles);
1561
        }
1562
1563 108
        return $this->currentRoleAndHost;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->currentRoleAndHost could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
1564
    }
1565
1566
    /**
1567
     * Returns value for lower_case_table_names variable
1568
     *
1569
     * @see https://mariadb.com/kb/en/server-system-variables/#lower_case_table_names
1570
     * @see https://dev.mysql.com/doc/refman/en/server-system-variables.html#sysvar_lower_case_table_names
1571
     *
1572
     * @psalm-return 0|1|2
1573
     */
1574 44
    public function getLowerCaseNames(): int
1575
    {
1576 44
        if ($this->lowerCaseTableNames === null) {
1577 44
            $value = (int) $this->fetchValue('SELECT @@lower_case_table_names');
1578 44
            $this->lowerCaseTableNames = $value >= 0 && $value <= 2 ? $value : 0;
1579
        }
1580
1581 44
        return $this->lowerCaseTableNames;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->lowerCaseTableNames could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
1582
    }
1583
1584
    /**
1585
     * Connects to the database server.
1586
     *
1587
     * @param ConnectionType|null $target How to store connection link, defaults to $connectionType
1588
     */
1589
    public function connect(
1590
        Server $currentServer,
1591
        ConnectionType $connectionType,
1592
        ConnectionType|null $target = null,
1593
    ): Connection|null {
1594
        $server = Config::getConnectionParams($currentServer, $connectionType);
1595
1596
        $target ??= $connectionType;
1597
1598
        // Do not show location and backtrace for connection errors
1599
        $errorHandler = ErrorHandler::getInstance();
1600
        $errorHandler->setHideLocation(true);
1601
        try {
1602
            $result = $this->extension->connect($server);
1603
        } catch (ConnectionException $exception) {
1604
            $errorHandler->addUserError($exception->getMessage());
1605
1606
            return null;
1607
        }
1608
1609
        $errorHandler->setHideLocation(false);
1610
1611
        $this->connections[$target->value] = $result;
1612
        /* Run post connect for user connections */
1613
        if ($target === ConnectionType::User) {
1614
            $this->postConnect($currentServer);
1615
        }
1616
1617
        return $result;
1618
    }
1619
1620
    /**
1621
     * selects given database
1622
     *
1623
     * @param string|DatabaseName $dbname database name to select
1624
     */
1625 8
    public function selectDb(string|DatabaseName $dbname, ConnectionType $connectionType = ConnectionType::User): bool
1626
    {
1627 8
        if (! isset($this->connections[$connectionType->value])) {
1628
            return false;
1629
        }
1630
1631 8
        return $this->extension->selectDb($dbname, $this->connections[$connectionType->value]);
1632
    }
1633
1634
    /**
1635
     * Prepare next result from multi_query
1636
     */
1637
    public function nextResult(ConnectionType $connectionType = ConnectionType::User): ResultInterface|false
1638
    {
1639
        if (! isset($this->connections[$connectionType->value])) {
1640
            return false;
1641
        }
1642
1643
        // TODO: Figure out if we really need to check the return value of this function.
1644
        if (! $this->extension->nextResult($this->connections[$connectionType->value])) {
1645
            return false;
1646
        }
1647
1648
        return $this->extension->storeResult($this->connections[$connectionType->value]);
1649
    }
1650
1651
    /**
1652
     * Returns a string representing the type of connection used
1653
     *
1654
     * @return string|bool type of connection used
1655
     */
1656
    public function getHostInfo(ConnectionType $connectionType = ConnectionType::User): string|bool
1657
    {
1658
        if (! isset($this->connections[$connectionType->value])) {
1659
            return false;
1660
        }
1661
1662
        return $this->extension->getHostInfo($this->connections[$connectionType->value]);
1663
    }
1664
1665
    /**
1666
     * returns a string that represents the client library version
1667
     *
1668
     * @return string MySQL client library version
1669
     */
1670
    public function getClientInfo(): string
1671
    {
1672
        return $this->extension->getClientInfo();
1673
    }
1674
1675
    /**
1676
     * Returns last error message or an empty string if no errors occurred.
1677
     */
1678 4
    public function getError(ConnectionType $connectionType = ConnectionType::User): string
1679
    {
1680 4
        if (! isset($this->connections[$connectionType->value])) {
1681
            return '';
1682
        }
1683
1684 4
        return $this->extension->getError($this->connections[$connectionType->value]);
1685
    }
1686
1687
    public function getConnectionErrorNumber(): int
1688
    {
1689
        return $this->extension->getConnectionErrorNumber();
1690
    }
1691
1692
    /**
1693
     * returns last inserted auto_increment id for given $link
1694
     */
1695 4
    public function insertId(ConnectionType $connectionType = ConnectionType::User): int
1696
    {
1697
        // If the primary key is BIGINT we get an incorrect result
1698
        // (sometimes negative, sometimes positive)
1699
        // and in the present function we don't know if the PK is BIGINT
1700
        // so better play safe and use LAST_INSERT_ID()
1701
        //
1702
        // When no controluser is defined, using mysqli_insert_id($link)
1703
        // does not always return the last insert id due to a mixup with
1704
        // the tracking mechanism, but this works:
1705 4
        return (int) $this->fetchValue('SELECT LAST_INSERT_ID();', 0, $connectionType);
1706
    }
1707
1708
    /**
1709
     * returns the number of rows affected by last query
1710
     *
1711
     * @param bool $getFromCache whether to retrieve from cache
1712
     *
1713
     * @psalm-return int|numeric-string
1714
     */
1715 8
    public function affectedRows(
1716
        ConnectionType $connectionType = ConnectionType::User,
1717
        bool $getFromCache = true,
1718
    ): int|string {
1719 8
        if (! isset($this->connections[$connectionType->value])) {
1720
            return -1;
1721
        }
1722
1723 8
        if ($getFromCache) {
1724
            return self::$cachedAffectedRows;
1725
        }
1726
1727 8
        return $this->extension->affectedRows($this->connections[$connectionType->value]);
1728
    }
1729
1730
    /**
1731
     * returns metainfo for fields in $result
1732
     *
1733
     * @param ResultInterface $result result set identifier
1734
     *
1735
     * @return list<FieldMetadata> meta info for fields in $result
1736
     */
1737
    public function getFieldsMeta(ResultInterface $result): array
1738
    {
1739
        $fields = $result->getFieldsMeta();
1740
1741
        if ($this->getLowerCaseNames() === 2) {
1742
            /**
1743
             * Fixup orgtable for lower_case_table_names = 2
1744
             *
1745
             * In this setup MySQL server reports table name lower case
1746
             * but we still need to operate on original case to properly
1747
             * match existing strings
1748
             */
1749
            foreach ($fields as $value) {
1750
                if (
1751
                    $value->orgtable === '' ||
1752
                        mb_strtolower($value->orgtable) !== mb_strtolower($value->table)
1753
                ) {
1754
                    continue;
1755
                }
1756
1757
                $value->orgtable = $value->table;
1758
            }
1759
        }
1760
1761
        return $fields;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $fields returns the type array which is incompatible with the documented return type PhpMyAdmin\Dbal\list.
Loading history...
1762
    }
1763
1764
    /**
1765
     * Returns properly quoted string for use in MySQL queries.
1766
     *
1767
     * @param string $str string to be quoted
1768
     *
1769
     * @psalm-return non-empty-string
1770
     *
1771
     * @psalm-taint-escape sql
1772
     */
1773 20
    public function quoteString(string $str, ConnectionType $connectionType = ConnectionType::User): string
1774
    {
1775 20
        return "'" . $this->extension->escapeString($this->connections[$connectionType->value], $str) . "'";
1776
    }
1777
1778
    /**
1779
     * Returns properly escaped string for use in MySQL LIKE clauses.
1780
     * This method escapes only _, %, and /. It does not escape quotes or any other characters.
1781
     *
1782
     * @param string $str string to be escaped
1783
     *
1784
     * @return string a MySQL escaped LIKE string
1785
     */
1786 16
    public function escapeMysqlWildcards(string $str): string
1787
    {
1788 16
        return strtr($str, ['\\' => '\\\\', '_' => '\\_', '%' => '\\%']);
1789
    }
1790
1791
    /**
1792
     * Checks if this database server is running on Amazon RDS.
1793
     */
1794 32
    public function isAmazonRds(): bool
1795
    {
1796 32
        if (SessionCache::has('is_amazon_rds')) {
1797 16
            return (bool) SessionCache::get('is_amazon_rds');
1798
        }
1799
1800 16
        $sql = 'SELECT @@basedir';
1801 16
        $result = (string) $this->fetchValue($sql);
1802 16
        $rds = str_starts_with($result, '/rdsdbbin/');
1803 16
        SessionCache::set('is_amazon_rds', $rds);
1804
1805 16
        return $rds;
1806
    }
1807
1808
    /**
1809
     * Gets SQL for killing a process.
1810
     *
1811
     * @param int $process Process ID
1812
     */
1813 16
    public function getKillQuery(int $process): string
1814
    {
1815 16
        if ($this->isAmazonRds() && $this->isSuperUser()) {
1816 4
            return 'CALL mysql.rds_kill(' . $process . ');';
1817
        }
1818
1819 12
        return 'KILL ' . $process . ';';
1820
    }
1821
1822
    /**
1823
     * Get a table with database name and table name
1824
     *
1825
     * @param string $dbName    DB name
1826
     * @param string $tableName Table name
1827
     */
1828 4
    public function getTable(string $dbName, string $tableName): Table
1829
    {
1830 4
        return new Table($tableName, $dbName, $this);
1831
    }
1832
1833
    /**
1834
     * returns collation of given db
1835
     *
1836
     * @param string $db name of db
1837
     *
1838
     * @return string  collation of $db
1839
     */
1840 8
    public function getDbCollation(string $db): string
1841
    {
1842 8
        if (! $this->config->selectedServer['DisableIS']) {
1843
            // this is slow with thousands of databases
1844 4
            $sql = 'SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA'
1845 4
                . ' WHERE SCHEMA_NAME = ' . $this->quoteString($db)
1846 4
                . ' LIMIT 1';
1847
1848 4
            return (string) $this->fetchValue($sql);
1849
        }
1850
1851 8
        $this->selectDb($db);
1852 8
        $return = (string) $this->fetchValue('SELECT @@collation_database');
1853 8
        if ($db !== Current::$database) {
1854 4
            $this->selectDb(Current::$database);
1855
        }
1856
1857 8
        return $return;
1858
    }
1859
1860
    /**
1861
     * returns default server collation from show variables
1862
     */
1863 84
    public function getServerCollation(): string
1864
    {
1865 84
        return (string) $this->fetchValue('SELECT @@collation_server');
1866
    }
1867
1868
    /**
1869
     * Server version as number
1870
     *
1871
     * @example 80011
1872
     */
1873 140
    public function getVersion(): int
1874
    {
1875 140
        return $this->versionInt;
1876
    }
1877
1878
    /**
1879
     * Server version
1880
     */
1881 32
    public function getVersionString(): string
1882
    {
1883 32
        return $this->versionString;
1884
    }
1885
1886
    /**
1887
     * Server version comment
1888
     */
1889 32
    public function getVersionComment(): string
1890
    {
1891 32
        return $this->versionComment;
1892
    }
1893
1894
    /** Whether connection is MySQL */
1895 32
    public function isMySql(): bool
1896
    {
1897 32
        return ! $this->isMariaDb;
1898
    }
1899
1900
    /**
1901
     * Whether connection is MariaDB
1902
     */
1903 140
    public function isMariaDB(): bool
1904
    {
1905 140
        return $this->isMariaDb;
1906
    }
1907
1908
    /**
1909
     * Whether connection is PerconaDB
1910
     */
1911 32
    public function isPercona(): bool
1912
    {
1913 32
        return $this->isPercona;
1914
    }
1915
1916
    /**
1917
     * Set version
1918
     *
1919
     * @param array $version Database version information
1920
     * @phpstan-param array<array-key, mixed> $version
1921
     */
1922 124
    public function setVersion(array $version): void
1923
    {
1924 124
        $this->versionString = $version['@@version'] ?? '';
1925 124
        $this->versionInt = Utilities::versionToInt($this->versionString);
1926 124
        $this->versionComment = $version['@@version_comment'] ?? '';
1927
1928 124
        $this->isMariaDb = stripos($this->versionString, 'mariadb') !== false;
1929 124
        $this->isPercona = stripos($this->versionComment, 'percona') !== false;
1930
    }
1931
1932
    /** @param list<string> $params */
1933 4
    public function executeQuery(
1934
        string $query,
1935
        array $params,
1936
        ConnectionType $connectionType = ConnectionType::User,
1937
    ): ResultInterface|null {
1938 4
        return $this->extension->executeQuery($this->connections[$connectionType->value], $query, $params);
1939
    }
1940
1941 4
    public function getDatabaseList(): ListDatabase
1942
    {
1943 4
        if ($this->databaseList === null) {
1944 4
            $this->databaseList = new ListDatabase($this, $this->config, new UserPrivilegesFactory($this));
1945
        }
1946
1947 4
        return $this->databaseList;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->databaseList could return the type null which is incompatible with the type-hinted return PhpMyAdmin\ListDatabase. Consider adding an additional type-check to rule them out.
Loading history...
1948
    }
1949
1950
    /**
1951
     * Returns the number of warnings from the last query.
1952
     */
1953
    private function getWarningCount(ConnectionType $connectionType): int
1954
    {
1955
        if (! isset($this->connections[$connectionType->value])) {
1956
            return 0;
1957
        }
1958
1959
        return $this->extension->getWarningCount($this->connections[$connectionType->value]);
1960
    }
1961
}
1962