DatabaseInterface   F
last analyzed

Complexity

Total Complexity 256

Size/Duplication

Total Lines 1978
Duplicated Lines 0 %

Test Coverage

Coverage 46.07%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 256
eloc 628
c 1
b 0
f 0
dl 0
loc 1978
ccs 322
cts 699
cp 0.4607
rs 1.972

68 Methods

Rating   Name   Duplication   Size   Complexity  
A quoteString() 0 3 1
A getTables() 0 18 3
A queryAsControlUser() 0 7 1
A tryQueryAsControlUser() 0 7 1
A tryMultiQuery() 0 9 2
A getFieldsMeta() 0 25 5
A affectedRows() 0 13 3
A __construct() 0 10 2
A getCache() 0 3 1
A query() 0 16 2
A getInstance() 0 7 2
A fetchValue() 0 11 2
A getCompatibilities() 0 15 1
A fetchValueOrValueByIndex() 0 3 2
A getVariable() 0 12 1
A isAmazonRds() 0 12 2
A getWarnings() 0 13 3
A getColumns() 0 26 1
A escapeMysqlWildcards() 0 3 1
A isConnected() 0 3 1
A getCurrentUserAndHost() 0 8 2
B isCreateUser() 0 45 10
A convertToColumns() 0 25 3
B getColumnsFull() 0 50 11
C getDatabasesFull() 0 127 15
A connect() 0 42 5
A getDefaultCharset() 0 3 2
A setVariable() 0 11 2
A getTableIndexes() 0 8 1
A insertId() 0 11 1
A getClientInfo() 0 3 1
A getCurrentUserGrants() 0 6 1
A getHostInfo() 0 7 2
A fetchSingleRow() 0 11 3
A getError() 0 7 2
A isSuperUser() 0 15 3
A getColumnNames() 0 9 1
A getColumn() 0 34 1
A setCollation() 0 21 4
F getTablesFull() 0 245 53
C fetchResult() 0 58 15
A nextResult() 0 12 3
A getDefaultCollation() 0 3 2
A getProtoInfo() 0 7 2
A getCurrentRolesAndHost() 0 11 2
A fetchByMode() 0 3 2
A queryAndGetNumRows() 0 9 2
B isGrantUser() 0 45 9
A getLowerCaseNames() 0 8 4
B getCurrentRoles() 0 19 8
B tryQuery() 0 55 10
A getCurrentUser() 0 14 3
B postConnect() 0 45 6
A selectDb() 0 7 2
B attachIndexInfoToColumns() 0 31 8
A getKillQuery() 0 7 2
A isPercona() 0 3 1
A getVersionString() 0 3 1
A getDatabaseList() 0 7 2
A getTable() 0 3 1
A getWarningCount() 0 7 2
A getDbCollation() 0 18 3
A prepare() 0 3 1
A getServerCollation() 0 3 1
A isMariaDB() 0 3 1
A getVersion() 0 3 1
A getVersionComment() 0 3 1
A setVersion() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like DatabaseInterface often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DatabaseInterface, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin;
6
7
use PhpMyAdmin\Config\Settings\Server;
8
use PhpMyAdmin\Dbal\Connection;
9
use PhpMyAdmin\Dbal\ConnectionException;
10
use PhpMyAdmin\Dbal\ConnectionType;
11
use PhpMyAdmin\Dbal\DbalInterface;
12
use PhpMyAdmin\Dbal\DbiExtension;
13
use PhpMyAdmin\Dbal\DbiMysqli;
14
use PhpMyAdmin\Dbal\ResultInterface;
15
use PhpMyAdmin\Dbal\Statement;
16
use PhpMyAdmin\Dbal\Warning;
17
use PhpMyAdmin\Error\ErrorHandler;
18
use PhpMyAdmin\Html\Generator;
19
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...
20
use PhpMyAdmin\Query\Cache;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, PhpMyAdmin\Cache. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
21
use PhpMyAdmin\Query\Compatibility;
22
use PhpMyAdmin\Query\Generator as QueryGenerator;
23
use PhpMyAdmin\Query\Utilities;
24
use PhpMyAdmin\Routing\Routing;
25
use PhpMyAdmin\SqlParser\Context;
26
use PhpMyAdmin\Table\Table;
27
use PhpMyAdmin\Tracking\Tracker;
28
use PhpMyAdmin\Utils\SessionCache;
29
use stdClass;
30
31
use function __;
32
use function array_column;
33
use function array_combine;
34
use function array_diff;
35
use function array_keys;
36
use function array_map;
37
use function array_multisort;
38
use function array_reverse;
39
use function array_shift;
40
use function array_slice;
41
use function basename;
42
use function closelog;
43
use function defined;
44
use function explode;
45
use function implode;
46
use function is_array;
47
use function is_int;
48
use function mb_strtolower;
49
use function microtime;
50
use function openlog;
51
use function reset;
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 trigger_error;
64
use function uasort;
65
use function uksort;
66
use function usort;
67
68
use const E_USER_WARNING;
69
use const LOG_INFO;
70
use const LOG_NDELAY;
71
use const LOG_PID;
72
use const LOG_USER;
73
use const SORT_ASC;
74
use const SORT_DESC;
75
76
/**
77
 * Main interface for database interactions
78
 */
79
class DatabaseInterface implements DbalInterface
80
{
81
    public static self|null $instance = null;
82
83
    /**
84
     * Force STORE_RESULT method, ignored by classic MySQL.
85
     */
86
    public const QUERY_BUFFERED = 0;
87
88
    /**
89
     * Do not read all rows immediately.
90
     */
91
    public const QUERY_UNBUFFERED = 2;
92
93
    /**
94
     * Get session variable.
95
     */
96
    public const GETVAR_SESSION = 1;
97
98
    /**
99
     * Get global variable.
100
     */
101
    public const GETVAR_GLOBAL = 2;
102
103
    /**
104
     * Opened database connections.
105
     *
106
     * @var array<int, Connection>
107
     * @psalm-var array<value-of<ConnectionType>, Connection>
108
     */
109
    private array $connections = [];
110
111
    /** @var array<int, string>|null */
112
    private array|null $currentUserAndHost = null;
113
114
    /** @var array<int, array<int, string>>|null Current role and host cache */
115
    private array|null $currentRoleAndHost = null;
116
117
    /**
118
     * @var int|null lower_case_table_names value cache
119
     * @psalm-var 0|1|2|null
120
     */
121
    private int|null $lowerCaseTableNames = null;
122
123
    /** @var bool Whether connection is MariaDB */
124
    private bool $isMariaDb = false;
125
    /** @var bool Whether connection is Percona */
126
    private bool $isPercona = false;
127
    /** @var int Server version as number */
128
    private int $versionInt = 55000;
129
    /** @var string Server version */
130
    private string $versionString = '5.50.0';
131
    /** @var string Server version comment */
132
    private string $versionComment = '';
133
134
    /** @var Types MySQL types data */
135
    public Types $types;
136
137
    private Cache $cache;
138
139
    public float $lastQueryExecutionTime = 0;
140
141
    private ListDatabase|null $databaseList = null;
142
    private readonly Config $config;
143
144
    /** @param DbiExtension $extension Object to be used for database queries */
145 220
    public function __construct(private DbiExtension $extension)
146
    {
147 220
        if (defined('TESTSUITE')) {
148 220
            $this->connections[ConnectionType::User->value] = new Connection(new stdClass());
149 220
            $this->connections[ConnectionType::ControlUser->value] = new Connection(new stdClass());
150
        }
151
152 220
        $this->cache = new Cache();
153 220
        $this->types = new Types($this);
154 220
        $this->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

154
        $this->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...
Bug introduced by
The property config is declared read-only in PhpMyAdmin\DatabaseInterface.
Loading history...
155
    }
156
157
    /** @deprecated Use dependency injection instead. */
158 8
    public static function getInstance(): self
159
    {
160 8
        if (self::$instance === null) {
161 4
            self::$instance = new self(new DbiMysqli());
162
        }
163
164 8
        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\DatabaseInterface. Consider adding an additional type-check to rule them out.
Loading history...
165
    }
166
167
    /**
168
     * runs a query
169
     *
170
     * @param string $query             SQL query to execute
171
     * @param int    $options           optional query options
172
     * @param bool   $cacheAffectedRows whether to cache affected rows
173
     */
174 8
    public function query(
175
        string $query,
176
        ConnectionType $connectionType = ConnectionType::User,
177
        int $options = self::QUERY_BUFFERED,
178
        bool $cacheAffectedRows = true,
179
    ): ResultInterface {
180 8
        $result = $this->tryQuery($query, $connectionType, $options, $cacheAffectedRows);
181
182 8
        if (! $result) {
183
            // The following statement will exit
184
            Generator::mysqlDie($this->getError($connectionType), $query);
185
186
            ResponseRenderer::getInstance()->callExit();
187
        }
188
189 8
        return $result;
190
    }
191
192
    public function getCache(): Cache
193
    {
194
        return $this->cache;
195
    }
196
197
    /**
198
     * runs a query and returns the result
199
     *
200
     * @param string $query             query to run
201
     * @param int    $options           if DatabaseInterface::QUERY_UNBUFFERED
202
     *                                  is provided, it will instruct the extension
203
     *                                  to use unbuffered mode
204
     * @param bool   $cacheAffectedRows whether to cache affected row
205
     */
206 124
    public function tryQuery(
207
        string $query,
208
        ConnectionType $connectionType = ConnectionType::User,
209
        int $options = self::QUERY_BUFFERED,
210
        bool $cacheAffectedRows = true,
211
    ): ResultInterface|false {
212 124
        if (! isset($this->connections[$connectionType->value])) {
213
            return false;
214
        }
215
216 124
        $time = microtime(true);
217
218 124
        $result = $this->extension->realQuery($query, $this->connections[$connectionType->value], $options);
219
220 124
        if ($connectionType === ConnectionType::User) {
221 120
            $this->lastQueryExecutionTime = microtime(true) - $time;
222
        }
223
224 124
        if ($cacheAffectedRows) {
225 8
            $GLOBALS['cached_affected_rows'] = $this->affectedRows($connectionType, false);
226
        }
227
228 124
        if ($this->config->config->debug->sql) {
229 4
            $errorMessage = $this->getError($connectionType);
230 4
            Utilities::debugLogQueryIntoSession(
231 4
                $query,
232 4
                $errorMessage !== '' ? $errorMessage : null,
233 4
                $result,
234 4
                $this->lastQueryExecutionTime,
235 4
            );
236 4
            if ($this->config->config->debug->sqllog) {
237
                openlog('phpMyAdmin', LOG_NDELAY | LOG_PID, LOG_USER);
238
239
                syslog(
240
                    LOG_INFO,
241
                    sprintf(
242
                        'SQL[%s?route=%s]: %0.3f(W:%d,C:%s,L:0x%02X) > %s',
243
                        basename($_SERVER['SCRIPT_NAME']),
244
                        Routing::$route,
245
                        $this->lastQueryExecutionTime,
246
                        $this->getWarningCount($connectionType),
247
                        $cacheAffectedRows ? 'y' : 'n',
248
                        $connectionType->value,
249
                        $query,
250
                    ),
251
                );
252
                closelog();
253
            }
254
        }
255
256 124
        if ($result !== false && Tracker::isActive()) {
257
            Tracker::handleQuery($query);
258
        }
259
260 124
        return $result;
261
    }
262
263
    /**
264
     * Send multiple SQL queries to the database server and execute the first one
265
     *
266
     * @param string $multiQuery multi query statement to execute
267
     */
268
    public function tryMultiQuery(
269
        string $multiQuery = '',
270
        ConnectionType $connectionType = ConnectionType::User,
271
    ): bool {
272
        if (! isset($this->connections[$connectionType->value])) {
273
            return false;
274
        }
275
276
        return $this->extension->realMultiQuery($this->connections[$connectionType->value], $multiQuery);
277
    }
278
279
    /**
280
     * Executes a query as controluser.
281
     * The result is always buffered and never cached
282
     *
283
     * @param string $sql the query to execute
284
     *
285
     * @return ResultInterface the result set
286
     */
287 4
    public function queryAsControlUser(string $sql): ResultInterface
288
    {
289
        // Avoid caching of the number of rows affected; for example, this function
290
        // is called for tracking purposes but we want to display the correct number
291
        // of rows affected by the original query, not by the query generated for
292
        // tracking.
293 4
        return $this->query($sql, ConnectionType::ControlUser, self::QUERY_BUFFERED, false);
294
    }
295
296
    /**
297
     * Executes a query as controluser.
298
     * The result is always buffered and never cached
299
     *
300
     * @param string $sql the query to execute
301
     *
302
     * @return ResultInterface|false the result set, or false if the query failed
303
     */
304 4
    public function tryQueryAsControlUser(string $sql): ResultInterface|false
305
    {
306
        // Avoid caching of the number of rows affected; for example, this function
307
        // is called for tracking purposes but we want to display the correct number
308
        // of rows affected by the original query, not by the query generated for
309
        // tracking.
310 4
        return $this->tryQuery($sql, ConnectionType::ControlUser, self::QUERY_BUFFERED, false);
311
    }
312
313
    /**
314
     * returns array with table names for given db
315
     *
316
     * @param string $database name of database
317
     *
318
     * @return array<int, string>   tables names
319
     */
320
    public function getTables(string $database, ConnectionType $connectionType = ConnectionType::User): array
321
    {
322
        if ($database === '') {
323
            return [];
324
        }
325
326
        /** @var array<int, string> $tables */
327
        $tables = $this->fetchResult(
328
            'SHOW TABLES FROM ' . Util::backquote($database) . ';',
329
            null,
330
            0,
331
            $connectionType,
332
        );
333
        if ($this->config->settings['NaturalOrder']) {
334
            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...
335
        }
336
337
        return $tables;
338
    }
339
340
    /**
341
     * returns array of all tables in given db or dbs
342
     * this function expects unquoted names:
343
     * RIGHT: my_database
344
     * WRONG: `my_database`
345
     * WRONG: my\_database
346
     * if $tbl_is_group is true, $table is used as filter for table names
347
     *
348
     * <code>
349
     * $dbi->getTablesFull('my_database');
350
     * $dbi->getTablesFull('my_database', 'my_table'));
351
     * $dbi->getTablesFull('my_database', 'my_tables_', true));
352
     * </code>
353
     *
354
     * @param string         $database     database
355
     * @param string|mixed[] $table        table name(s)
356
     * @param bool           $tableIsGroup $table is a table group
357
     * @param int            $limitOffset  zero-based offset for the count
358
     * @param bool|int       $limitCount   number of tables to return
359
     * @param string         $sortBy       table attribute to sort by
360
     * @param string         $sortOrder    direction to sort (ASC or DESC)
361
     * @param string|null    $tableType    whether table or view
362
     *
363
     * @return (string|int|null)[][]           list of tables in given db(s)
364
     *
365
     * @todo    move into Table
366
     */
367 12
    public function getTablesFull(
368
        string $database,
369
        string|array $table = '',
370
        bool $tableIsGroup = false,
371
        int $limitOffset = 0,
372
        bool|int $limitCount = false,
373
        string $sortBy = 'Name',
374
        string $sortOrder = 'ASC',
375
        string|null $tableType = null,
376
        ConnectionType $connectionType = ConnectionType::User,
377
    ): array {
378 12
        if ($limitCount === true) {
379
            $limitCount = $this->config->settings['MaxTableList'];
380
        }
381
382 12
        $tables = [];
383 12
        $pagingApplied = false;
384
385 12
        if ($limitCount && is_array($table) && $sortBy === 'Name') {
386
            if ($sortOrder === 'DESC') {
387
                $table = array_reverse($table);
388
            }
389
390
            $table = array_slice($table, $limitOffset, $limitCount);
391
            $pagingApplied = true;
392
        }
393
394 12
        if (! $this->config->selectedServer['DisableIS']) {
395 4
            $sqlWhereTable = '';
396 4
            if ($table !== [] && $table !== '') {
397
                if (is_array($table)) {
398
                    $sqlWhereTable = QueryGenerator::getTableNameConditionForMultiple(
399
                        array_map($this->quoteString(...), $table),
400
                    );
401
                } else {
402
                    $sqlWhereTable = QueryGenerator::getTableNameCondition(
403
                        $this->quoteString($tableIsGroup ? $this->escapeMysqlWildcards($table) : $table),
404
                        $tableIsGroup,
405
                    );
406
                }
407
            }
408
409 4
            $sqlWhereTable .= QueryGenerator::getTableTypeCondition($tableType);
410
411
            // for PMA bc:
412
            // `SCHEMA_FIELD_NAME` AS `SHOW_TABLE_STATUS_FIELD_NAME`
413
            //
414
            // on non-Windows servers,
415
            // added BINARY in the WHERE clause to force a case sensitive
416
            // comparison (if we are looking for the db Aa we don't want
417
            // to find the db aa)
418
419 4
            $sql = QueryGenerator::getSqlForTablesFull($this->quoteString($database), $sqlWhereTable);
420
421
            // Sort the tables
422 4
            $sql .= ' ORDER BY ' . $sortBy . ' ' . $sortOrder;
423
424 4
            if ($limitCount && ! $pagingApplied) {
425
                $sql .= ' LIMIT ' . $limitCount . ' OFFSET ' . $limitOffset;
426
            }
427
428
            /** @var (string|int|null)[][][] $tables */
429 4
            $tables = $this->fetchResult(
430 4
                $sql,
431 4
                ['TABLE_SCHEMA', 'TABLE_NAME'],
432 4
                null,
433 4
                $connectionType,
434 4
            );
435
436
            // here, we check for Mroonga engine and compute the good data_length and index_length
437
            // in the StructureController only we need to sum the two values as the other engines
438 4
            foreach ($tables as $oneDatabaseName => $oneDatabaseTables) {
439 4
                foreach ($oneDatabaseTables as $oneTableName => $oneTableData) {
440 4
                    if ($oneTableData['Engine'] !== 'Mroonga') {
441 4
                        continue;
442
                    }
443
444
                    if (! StorageEngine::hasMroongaEngine()) {
445
                        continue;
446
                    }
447
448
                    [
449
                        $tables[$oneDatabaseName][$oneTableName]['Data_length'],
450
                        $tables[$oneDatabaseName][$oneTableName]['Index_length'],
451
                    ] = StorageEngine::getMroongaLengths((string) $oneDatabaseName, (string) $oneTableName);
452
                }
453
            }
454
455 4
            if ($sortBy === 'Name' && $this->config->settings['NaturalOrder']) {
456
                // here, the array's first key is by schema name
457 4
                foreach ($tables as $oneDatabaseName => $oneDatabaseTables) {
458 4
                    uksort($oneDatabaseTables, strnatcasecmp(...));
459
460 4
                    if ($sortOrder === 'DESC') {
461
                        $oneDatabaseTables = array_reverse($oneDatabaseTables);
462
                    }
463
464 4
                    $tables[$oneDatabaseName] = $oneDatabaseTables;
465
                }
466
            } elseif ($sortBy === 'Data_length') {
467
                // Size = Data_length + Index_length
468
                foreach ($tables as $oneDatabaseName => $oneDatabaseTables) {
469
                    uasort(
470
                        $oneDatabaseTables,
471
                        static function (array $a, array $b): int {
472
                            $aLength = $a['Data_length'] + $a['Index_length'];
473
                            $bLength = $b['Data_length'] + $b['Index_length'];
474
475
                            return $aLength <=> $bLength;
476
                        },
477
                    );
478
479
                    if ($sortOrder === 'DESC') {
480
                        $oneDatabaseTables = array_reverse($oneDatabaseTables);
481
                    }
482
483
                    $tables[$oneDatabaseName] = $oneDatabaseTables;
484
                }
485
            }
486
487
            // on windows with lower_case_table_names = 1
488
            // MySQL returns
489
            // with SHOW DATABASES or information_schema.SCHEMATA: `Test`
490
            // but information_schema.TABLES gives `test`
491
            // see https://github.com/phpmyadmin/phpmyadmin/issues/8402
492 4
            $tables = $tables[$database]
493 4
                ?? $tables[mb_strtolower($database)]
494
                ?? [];
495
        }
496
497
        // If permissions are wrong on even one database directory,
498
        // information_schema does not return any table info for any database
499
        // this is why we fall back to SHOW TABLE STATUS even for MySQL >= 50002
500 12
        if ($tables === []) {
501 8
            $sql = 'SHOW TABLE STATUS FROM ' . Util::backquote($database);
502 8
            if (($table !== '' && $table !== []) || $tableIsGroup || ($tableType !== null && $tableType !== '')) {
503
                $sql .= ' WHERE';
504
                $needAnd = false;
505
                if (($table !== '' && $table !== []) || $tableIsGroup) {
506
                    if (is_array($table)) {
507
                        $sql .= ' `Name` IN ('
508
                            . implode(
509
                                ', ',
510
                                array_map(
511
                                    fn (string $string): string => $this->quoteString($string, $connectionType),
512
                                    $table,
513
                                ),
514
                            ) . ')';
515
                    } else {
516
                        $sql .= ' `Name` LIKE '
517
                            . $this->quoteString($this->escapeMysqlWildcards($table) . '%', $connectionType);
518
                    }
519
520
                    $needAnd = true;
521
                }
522
523
                if ($tableType !== null && $tableType !== '') {
524
                    if ($needAnd) {
525
                        $sql .= ' AND';
526
                    }
527
528
                    if ($tableType === 'view') {
529
                        $sql .= " `Comment` = 'VIEW'";
530
                    } elseif ($tableType === 'table') {
531
                        $sql .= " `Comment` != 'VIEW'";
532
                    }
533
                }
534
            }
535
536
            /** @var (string|int|null)[][] $eachTables */
537 8
            $eachTables = $this->fetchResult($sql, 'Name', null, $connectionType);
538
539
            // here, we check for Mroonga engine and compute the good data_length and index_length
540
            // in the StructureController only we need to sum the two values as the other engines
541 8
            foreach ($eachTables as $tableName => $tableData) {
542 8
                if ($tableData['Engine'] !== 'Mroonga') {
543 8
                    continue;
544
                }
545
546
                if (! StorageEngine::hasMroongaEngine()) {
547
                    continue;
548
                }
549
550
                [
551
                    $eachTables[$tableName]['Data_length'],
552
                    $eachTables[$tableName]['Index_length'],
553
                ] = StorageEngine::getMroongaLengths($database, (string) $tableName);
554
            }
555
556
            // Sort naturally if the config allows it and we're sorting
557
            // the Name column.
558 8
            if ($sortBy === 'Name' && $this->config->settings['NaturalOrder']) {
559 4
                uksort($eachTables, strnatcasecmp(...));
560
561 4
                if ($sortOrder === 'DESC') {
562 2
                    $eachTables = array_reverse($eachTables);
563
                }
564
            } else {
565
                // Prepare to sort by creating array of the selected sort
566
                // value to pass to array_multisort
567
568
                // Size = Data_length + Index_length
569 4
                $sortValues = [];
570 4
                if ($sortBy === 'Data_length') {
571
                    foreach ($eachTables as $tableName => $tableData) {
572
                        $sortValues[$tableName] = strtolower(
573
                            (string) ($tableData['Data_length']
574
                            + $tableData['Index_length']),
575
                        );
576
                    }
577
                } else {
578 4
                    foreach ($eachTables as $tableName => $tableData) {
579 4
                        $sortValues[$tableName] = strtolower($tableData[$sortBy] ?? '');
580
                    }
581
                }
582
583 4
                if ($sortValues !== []) {
584
                    // See https://stackoverflow.com/a/32461188 for the explanation of below hack
585 4
                    $keys = array_keys($eachTables);
586 4
                    if ($sortOrder === 'DESC') {
587
                        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

587
                        array_multisort($sortValues, /** @scrutinizer ignore-type */ SORT_DESC, $eachTables, $keys);
Loading history...
588
                    } else {
589 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

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