Passed
Push — master ( 416bf9...23ce7c )
by Maurício
07:36
created

DatabaseInterface::getForeignKeyConstrains()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 25
rs 9.6
c 0
b 0
f 0
cc 2
nc 2
nop 3
1
<?php
2
/* vim: set expandtab sw=4 ts=4 sts=4: */
3
/**
4
 * Main interface for database interactions
5
 *
6
 * @package PhpMyAdmin-DBI
7
 */
8
declare(strict_types=1);
9
10
namespace PhpMyAdmin;
11
12
use PhpMyAdmin\Database\DatabaseList;
13
use PhpMyAdmin\Dbi\DbiDummy;
14
use PhpMyAdmin\Dbi\DbiExtension;
15
use PhpMyAdmin\Dbi\DbiMysqli;
16
use PhpMyAdmin\Di\Container;
17
18
/**
19
 * Main interface for database interactions
20
 *
21
 * @package PhpMyAdmin-DBI
22
 */
23
class DatabaseInterface
24
{
25
    /**
26
     * Force STORE_RESULT method, ignored by classic MySQL.
27
     */
28
    public const QUERY_STORE = 1;
29
    /**
30
     * Do not read whole query.
31
     */
32
    public const QUERY_UNBUFFERED = 2;
33
    /**
34
     * Get session variable.
35
     */
36
    public const GETVAR_SESSION = 1;
37
    /**
38
     * Get global variable.
39
     */
40
    public const GETVAR_GLOBAL = 2;
41
42
    /**
43
     * User connection.
44
     */
45
    public const CONNECT_USER = 0x100;
46
    /**
47
     * Control user connection.
48
     */
49
    public const CONNECT_CONTROL = 0x101;
50
    /**
51
     * Auxiliary connection.
52
     *
53
     * Used for example for replication setup.
54
     */
55
    public const CONNECT_AUXILIARY = 0x102;
56
57
    /**
58
     * @var DbiExtension
59
     */
60
    private $_extension;
61
62
    /**
63
     * Opened database links
64
     *
65
     * @var array
66
     */
67
    private $_links;
68
69
    /**
70
     * @var array Table data cache
71
     */
72
    private $_table_cache;
73
74
    /**
75
     * @var array Current user and host cache
76
     */
77
    private $_current_user;
78
79
    /**
80
     * @var null|string lower_case_table_names value cache
81
     */
82
    private $_lower_case_table_names = null;
83
84
    /**
85
     * @var boolean Whether connection is MariaDB
86
     */
87
    private $_is_mariadb = false;
88
    /**
89
     * @var boolean Whether connection is Percona
90
     */
91
    private $_is_percona = false;
92
    /**
93
     * @var integer Server version as number
94
     */
95
    private $_version_int = 55000;
96
    /**
97
     * @var string Server version
98
     */
99
    private $_version_str = '5.50.0';
100
    /**
101
     * @var string Server version comment
102
     */
103
    private $_version_comment = '';
104
105
    /**
106
     * @var Types MySQL types data
107
     */
108
    public $types;
109
110
    /**
111
     * @var Relation
112
     */
113
    private $relation;
114
115
    /**
116
     * Constructor
117
     *
118
     * @param DbiExtension $ext Object to be used for database queries
119
     */
120
    public function __construct(DbiExtension $ext)
121
    {
122
        $this->_extension = $ext;
123
        $this->_links = [];
124
        if (defined('TESTSUITE')) {
125
            $this->_links[DatabaseInterface::CONNECT_USER] = 1;
126
            $this->_links[DatabaseInterface::CONNECT_CONTROL] = 2;
127
        }
128
        $this->_table_cache = [];
129
        $this->_current_user = [];
130
        $this->types = new Types($this);
131
        $this->relation = new Relation($this);
132
    }
133
134
    /**
135
     * Checks whether database extension is loaded
136
     *
137
     * @param string $extension mysql extension to check
138
     *
139
     * @return bool
140
     */
141
    public static function checkDbExtension(string $extension = 'mysqli'): bool
142
    {
143
        return function_exists($extension . '_connect');
144
    }
145
146
    /**
147
     * runs a query
148
     *
149
     * @param string $query               SQL query to execute
150
     * @param mixed  $link                optional database link to use
151
     * @param int    $options             optional query options
152
     * @param bool   $cache_affected_rows whether to cache affected rows
153
     *
154
     * @return mixed
155
     */
156
    public function query(
157
        string $query,
158
        $link = DatabaseInterface::CONNECT_USER,
159
        int $options = 0,
160
        bool $cache_affected_rows = true
161
    ) {
162
        $res = $this->tryQuery($query, $link, $options, $cache_affected_rows)
163
           or Util::mysqlDie($this->getError($link), $query);
164
165
        return $res;
166
    }
167
168
    /**
169
     * Get a cached value from table cache.
170
     *
171
     * @param array $contentPath Array of the name of the target value
172
     * @param mixed $default     Return value on cache miss
173
     *
174
     * @return mixed cached value or default
175
     */
176
    public function getCachedTableContent(array $contentPath, $default = null)
177
    {
178
        return Util::getValueByKey($this->_table_cache, $contentPath, $default);
179
    }
180
181
    /**
182
     * Set an item in table cache using dot notation.
183
     *
184
     * @param array $contentPath Array with the target path
185
     * @param mixed $value       Target value
186
     *
187
     * @return void
188
     */
189
    public function cacheTableContent(array $contentPath, $value): void
190
    {
191
        $loc = &$this->_table_cache;
192
193
        if (!isset($contentPath)) {
194
            $loc = $value;
195
            return;
196
        }
197
198
        while (count($contentPath) > 1) {
199
            $key = array_shift($contentPath);
200
201
            // If the key doesn't exist at this depth, we will just create an empty
202
            // array to hold the next value, allowing us to create the arrays to hold
203
            // final values at the correct depth. Then we'll keep digging into the
204
            // array.
205
            if (!isset($loc[$key]) || !is_array($loc[$key])) {
206
                $loc[$key] = [];
207
            }
208
            $loc = &$loc[$key];
209
        }
210
211
        $loc[array_shift($contentPath)] = $value;
212
    }
213
214
    /**
215
     * Clear the table cache.
216
     *
217
     * @return void
218
     */
219
    public function clearTableCache(): void
220
    {
221
        $this->_table_cache = [];
222
    }
223
224
    /**
225
     * Caches table data so Table does not require to issue
226
     * SHOW TABLE STATUS again
227
     *
228
     * @param array       $tables information for tables of some databases
229
     * @param string|bool $table  table name
230
     *
231
     * @return void
232
     */
233
    private function _cacheTableData(array $tables, $table): void
234
    {
235
        // Note: I don't see why we would need array_merge_recursive() here,
236
        // as it creates double entries for the same table (for example a double
237
        // entry for Comment when changing the storage engine in Operations)
238
        // Note 2: Instead of array_merge(), simply use the + operator because
239
        //  array_merge() renumbers numeric keys starting with 0, therefore
240
        //  we would lose a db name that consists only of numbers
241
242
        foreach ($tables as $one_database => $its_tables) {
243
            if (isset($this->_table_cache[$one_database])) {
244
                // the + operator does not do the intended effect
245
                // when the cache for one table already exists
246
                if ($table
247
                    && isset($this->_table_cache[$one_database][$table])
248
                ) {
249
                    unset($this->_table_cache[$one_database][$table]);
250
                }
251
                $this->_table_cache[$one_database]
252
                    += $tables[$one_database];
253
            } else {
254
                $this->_table_cache[$one_database] = $tables[$one_database];
255
            }
256
        }
257
    }
258
259
    /**
260
     * Stores query data into session data for debugging purposes
261
     *
262
     * @param string         $query  Query text
263
     * @param mixed          $link   link type
264
     * @param object|boolean $result Query result
265
     * @param integer|float  $time   Time to execute query
266
     *
267
     * @return void
268
     */
269
    private function _dbgQuery(string $query, $link, $result, $time): void
270
    {
271
        $dbgInfo = [];
272
        $error_message = $this->getError($link);
273
        if ($result == false && is_string($error_message)) {
274
            $dbgInfo['error']
275
                = '<span style="color:red">'
276
                . htmlspecialchars($error_message) . '</span>';
277
        }
278
        $dbgInfo['query'] = htmlspecialchars($query);
279
        $dbgInfo['time'] = $time;
280
        // Get and slightly format backtrace, this is used
281
        // in the javascript console.
282
        // Strip call to _dbgQuery
283
        $dbgInfo['trace'] = Error::processBacktrace(
284
            array_slice(debug_backtrace(), 1)
285
        );
286
        $dbgInfo['hash'] = md5($query);
287
288
        $_SESSION['debug']['queries'][] = $dbgInfo;
289
    }
290
291
    /**
292
     * runs a query and returns the result
293
     *
294
     * @param string  $query               query to run
295
     * @param mixed   $link                link type
296
     * @param integer $options             query options
297
     * @param bool    $cache_affected_rows whether to cache affected row
298
     *
299
     * @return mixed
300
     */
301
    public function tryQuery(
302
        string $query,
303
        $link = DatabaseInterface::CONNECT_USER,
304
        int $options = 0,
305
        bool $cache_affected_rows = true
306
    ) {
307
        $debug = $GLOBALS['cfg']['DBG']['sql'];
308
        if (! isset($this->_links[$link])) {
309
            return false;
310
        }
311
312
        if ($debug) {
313
            $time = microtime(true);
314
        }
315
316
        $result = $this->_extension->realQuery($query, $this->_links[$link], $options);
317
318
        if ($cache_affected_rows) {
319
            $GLOBALS['cached_affected_rows'] = $this->affectedRows($link, false);
320
        }
321
322
        if ($debug) {
323
            $time = microtime(true) - $time;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $time does not seem to be defined for all execution paths leading up to this point.
Loading history...
324
            $this->_dbgQuery($query, $link, $result, $time);
325
            if ($GLOBALS['cfg']['DBG']['sqllog']) {
326
                if ($options & DatabaseInterface::QUERY_STORE == DatabaseInterface::QUERY_STORE) {
327
                    $tmp = $this->_extension->realQuery('
328
                        SHOW COUNT(*) WARNINGS', $this->_links[$link], DatabaseInterface::QUERY_STORE);
329
                    $warnings = $this->fetchRow($tmp);
330
                } else {
331
                    $warnings = 0;
332
                }
333
334
                openlog('phpMyAdmin', LOG_NDELAY | LOG_PID, LOG_USER);
335
336
                syslog(
337
                    LOG_INFO,
338
                    'SQL[' . basename($_SERVER['SCRIPT_NAME']) . ']: '
339
                    . sprintf('%0.3f', $time) . '(W:' . $warnings[0] . ') > ' . $query
340
                );
341
                closelog();
342
            }
343
        }
344
345
        if ($result !== false && Tracker::isActive()) {
346
            Tracker::handleQuery($query);
347
        }
348
349
        return $result;
350
    }
351
352
    /**
353
     * Run multi query statement and return results
354
     *
355
     * @param string  $multi_query multi query statement to execute
356
     * @param \mysqli $link        mysqli object
357
     *
358
     * @return \mysqli_result[] | boolean(false)
359
     */
360
    public function tryMultiQuery(
361
        string $multi_query = '',
362
        $link = DatabaseInterface::CONNECT_USER
363
    ) {
364
        if (! isset($this->_links[$link])) {
365
            return false;
366
        }
367
        return $this->_extension->realMultiQuery($this->_links[$link], $multi_query);
368
    }
369
370
    /**
371
     * returns array with table names for given db
372
     *
373
     * @param string $database name of database
374
     * @param mixed  $link     mysql link resource|object
375
     *
376
     * @return array   tables names
377
     */
378
    public function getTables(string $database, $link = DatabaseInterface::CONNECT_USER): array
379
    {
380
        $tables = $this->fetchResult(
381
            'SHOW TABLES FROM ' . Util::backquote($database) . ';',
382
            null,
383
            0,
384
            $link,
385
            self::QUERY_STORE
386
        );
387
        if ($GLOBALS['cfg']['NaturalOrder']) {
388
            usort($tables, 'strnatcasecmp');
389
        }
390
        return $tables;
391
    }
392
393
394
    /**
395
     * returns
396
     *
397
     * @param string $database name of database
398
     * @param array  $tables   list of tables to search for for relations
399
     * @param int    $link     mysql link resource|object
400
     *
401
     * @return array           array of found foreign keys
402
     */
403
    public function getForeignKeyConstrains(string $database, array $tables, $link = DatabaseInterface::CONNECT_USER): array
404
    {
405
        $tablesListForQuery = '';
406
        foreach($tables as $table){
407
            $tablesListForQuery .= "'" . $this->escapeString($table) . "',";
408
        }
409
        $tablesListForQuery = rtrim($tablesListForQuery, ',');
410
411
        $foreignKeyConstrains = $this->fetchResult(
412
            "SELECT"
413
                    . " TABLE_NAME,"
414
                    . " COLUMN_NAME,"
415
                    . " REFERENCED_TABLE_NAME,"
416
                    . " REFERENCED_COLUMN_NAME"
417
                . " FROM information_schema.key_column_usage"
418
                . " WHERE referenced_table_name IS NOT NULL"
419
                    . " AND TABLE_SCHEMA = '" . $this->escapeString($database) . "'"
420
                    . " AND TABLE_NAME IN (" . $tablesListForQuery . ")"
421
                    . " AND REFERENCED_TABLE_NAME IN (" . $tablesListForQuery . ");",
422
            null,
423
            null,
424
            $link,
425
            self::QUERY_STORE
426
        );
427
        return $foreignKeyConstrains;
428
    }
429
430
    /**
431
     * returns a segment of the SQL WHERE clause regarding table name and type
432
     *
433
     * @param array|string $table        table(s)
434
     * @param boolean      $tbl_is_group $table is a table group
435
     * @param string       $table_type   whether table or view
436
     *
437
     * @return string a segment of the WHERE clause
438
     */
439
    private function _getTableCondition(
440
        $table,
441
        bool $tbl_is_group,
442
        ?string $table_type
443
    ): string {
444
        // get table information from information_schema
445
        if ($table) {
446
            if (is_array($table)) {
447
                $sql_where_table = 'AND t.`TABLE_NAME` '
448
                    . Util::getCollateForIS() . ' IN (\''
449
                    . implode(
450
                        '\', \'',
451
                        array_map(
452
                            [$this, 'escapeString'],
453
                            $table
454
                        )
455
                    )
456
                    . '\')';
457
            } elseif (true === $tbl_is_group) {
458
                $sql_where_table = 'AND t.`TABLE_NAME` LIKE \''
459
                    . Util::escapeMysqlWildcards(
460
                        $GLOBALS['dbi']->escapeString($table)
461
                    )
462
                    . '%\'';
463
            } else {
464
                $sql_where_table = 'AND t.`TABLE_NAME` '
465
                    . Util::getCollateForIS() . ' = \''
466
                    . $GLOBALS['dbi']->escapeString($table) . '\'';
467
            }
468
        } else {
469
            $sql_where_table = '';
470
        }
471
472
        if ($table_type) {
473
            if ($table_type == 'view') {
474
                $sql_where_table .= " AND t.`TABLE_TYPE` != 'BASE TABLE'";
475
            } elseif ($table_type == 'table') {
476
                $sql_where_table .= " AND t.`TABLE_TYPE` = 'BASE TABLE'";
477
            }
478
        }
479
        return $sql_where_table;
480
    }
481
482
    /**
483
     * returns the beginning of the SQL statement to fetch the list of tables
484
     *
485
     * @param string[] $this_databases  databases to list
486
     * @param string   $sql_where_table additional condition
487
     *
488
     * @return string the SQL statement
489
     */
490
    private function _getSqlForTablesFull($this_databases, string $sql_where_table): string
491
    {
492
        return '
493
            SELECT *,
494
                `TABLE_SCHEMA`       AS `Db`,
495
                `TABLE_NAME`         AS `Name`,
496
                `TABLE_TYPE`         AS `TABLE_TYPE`,
497
                `ENGINE`             AS `Engine`,
498
                `ENGINE`             AS `Type`,
499
                `VERSION`            AS `Version`,
500
                `ROW_FORMAT`         AS `Row_format`,
501
                `TABLE_ROWS`         AS `Rows`,
502
                `AVG_ROW_LENGTH`     AS `Avg_row_length`,
503
                `DATA_LENGTH`        AS `Data_length`,
504
                `MAX_DATA_LENGTH`    AS `Max_data_length`,
505
                `INDEX_LENGTH`       AS `Index_length`,
506
                `DATA_FREE`          AS `Data_free`,
507
                `AUTO_INCREMENT`     AS `Auto_increment`,
508
                `CREATE_TIME`        AS `Create_time`,
509
                `UPDATE_TIME`        AS `Update_time`,
510
                `CHECK_TIME`         AS `Check_time`,
511
                `TABLE_COLLATION`    AS `Collation`,
512
                `CHECKSUM`           AS `Checksum`,
513
                `CREATE_OPTIONS`     AS `Create_options`,
514
                `TABLE_COMMENT`      AS `Comment`
515
            FROM `information_schema`.`TABLES` t
516
            WHERE `TABLE_SCHEMA` ' . Util::getCollateForIS() . '
517
                IN (\'' . implode("', '", $this_databases) . '\')
518
                ' . $sql_where_table;
519
    }
520
521
    /**
522
     * returns array of all tables in given db or dbs
523
     * this function expects unquoted names:
524
     * RIGHT: my_database
525
     * WRONG: `my_database`
526
     * WRONG: my\_database
527
     * if $tbl_is_group is true, $table is used as filter for table names
528
     *
529
     * <code>
530
     * $GLOBALS['dbi']->getTablesFull('my_database');
531
     * $GLOBALS['dbi']->getTablesFull('my_database', 'my_table'));
532
     * $GLOBALS['dbi']->getTablesFull('my_database', 'my_tables_', true));
533
     * </code>
534
     *
535
     * @param string          $database     database
536
     * @param string|array    $table        table name(s)
537
     * @param boolean         $tbl_is_group $table is a table group
538
     * @param integer         $limit_offset zero-based offset for the count
539
     * @param boolean|integer $limit_count  number of tables to return
540
     * @param string          $sort_by      table attribute to sort by
541
     * @param string          $sort_order   direction to sort (ASC or DESC)
542
     * @param string          $table_type   whether table or view
543
     * @param mixed           $link         link type
544
     *
545
     * @todo    move into Table
546
     *
547
     * @return array           list of tables in given db(s)
548
     */
549
    public function getTablesFull(
550
        string $database,
551
        $table = '',
552
        bool $tbl_is_group = false,
553
        int $limit_offset = 0,
554
        $limit_count = false,
555
        string $sort_by = 'Name',
556
        string $sort_order = 'ASC',
557
        ?string $table_type = null,
558
        $link = DatabaseInterface::CONNECT_USER
559
    ): array {
560
        if (true === $limit_count) {
561
            $limit_count = $GLOBALS['cfg']['MaxTableList'];
562
        }
563
        // prepare and check parameters
564
        if (! is_array($database)) {
0 ignored issues
show
introduced by
The condition is_array($database) is always false.
Loading history...
565
            $databases = [$database];
566
        } else {
567
            $databases = $database;
568
        }
569
570
        $tables = [];
571
572
        if (! $GLOBALS['cfg']['Server']['DisableIS']) {
573
            $sql_where_table = $this->_getTableCondition(
574
                $table,
575
                $tbl_is_group,
576
                $table_type
577
            );
578
579
            // for PMA bc:
580
            // `SCHEMA_FIELD_NAME` AS `SHOW_TABLE_STATUS_FIELD_NAME`
581
            //
582
            // on non-Windows servers,
583
            // added BINARY in the WHERE clause to force a case sensitive
584
            // comparison (if we are looking for the db Aa we don't want
585
            // to find the db aa)
586
            $this_databases = array_map(
587
                [$this, 'escapeString'],
588
                $databases
589
            );
590
591
            $sql = $this->_getSqlForTablesFull($this_databases, $sql_where_table);
592
593
            // Sort the tables
594
            $sql .= " ORDER BY $sort_by $sort_order";
595
596
            if ($limit_count) {
597
                $sql .= ' LIMIT ' . $limit_count . ' OFFSET ' . $limit_offset;
598
            }
599
600
            $tables = $this->fetchResult(
601
                $sql,
602
                ['TABLE_SCHEMA', 'TABLE_NAME'],
603
                null,
604
                $link
605
            );
606
607
            if ($sort_by == 'Name' && $GLOBALS['cfg']['NaturalOrder']) {
608
                // here, the array's first key is by schema name
609
                foreach ($tables as $one_database_name => $one_database_tables) {
610
                    uksort($one_database_tables, 'strnatcasecmp');
611
612
                    if ($sort_order == 'DESC') {
613
                        $one_database_tables = array_reverse($one_database_tables);
614
                    }
615
                    $tables[$one_database_name] = $one_database_tables;
616
                }
617
            } elseif ($sort_by == 'Data_length') {
618
                // Size = Data_length + Index_length
619
                foreach ($tables as $one_database_name => $one_database_tables) {
620
                    uasort(
621
                        $one_database_tables,
622
                        function ($a, $b) {
623
                            $aLength = $a['Data_length'] + $a['Index_length'];
624
                            $bLength = $b['Data_length'] + $b['Index_length'];
625
                            return $aLength == $bLength
626
                                ? 0
627
                                : $aLength < $bLength ? -1 : 1;
628
                        }
629
                    );
630
631
                    if ($sort_order == 'DESC') {
632
                        $one_database_tables = array_reverse($one_database_tables);
633
                    }
634
                    $tables[$one_database_name] = $one_database_tables;
635
                }
636
            }
637
        } // end (get information from table schema)
638
639
        // If permissions are wrong on even one database directory,
640
        // information_schema does not return any table info for any database
641
        // this is why we fall back to SHOW TABLE STATUS even for MySQL >= 50002
642
        if (empty($tables)) {
643
            foreach ($databases as $each_database) {
644
                if ($table || (true === $tbl_is_group) || ! empty($table_type)) {
645
                    $sql = 'SHOW TABLE STATUS FROM '
646
                        . Util::backquote($each_database)
647
                        . ' WHERE';
648
                    $needAnd = false;
649
                    if ($table || (true === $tbl_is_group)) {
650
                        if (is_array($table)) {
651
                            $sql .= ' `Name` IN (\''
652
                                . implode(
653
                                    '\', \'',
654
                                    array_map(
655
                                        [$this, 'escapeString'],
656
                                        $table,
657
                                        $link
0 ignored issues
show
Bug introduced by
It seems like $link can also be of type integer; however, parameter $_ of array_map() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

657
                                        /** @scrutinizer ignore-type */ $link
Loading history...
658
                                    )
659
                                ) . '\')';
660
                        } else {
661
                            $sql .= " `Name` LIKE '"
662
                                . Util::escapeMysqlWildcards(
663
                                    $this->escapeString($table, $link)
664
                                )
665
                                . "%'";
666
                        }
667
                        $needAnd = true;
668
                    }
669
                    if (! empty($table_type)) {
670
                        if ($needAnd) {
671
                            $sql .= " AND";
672
                        }
673
                        if ($table_type == 'view') {
674
                            $sql .= " `Comment` = 'VIEW'";
675
                        } elseif ($table_type == 'table') {
676
                            $sql .= " `Comment` != 'VIEW'";
677
                        }
678
                    }
679
                } else {
680
                    $sql = 'SHOW TABLE STATUS FROM '
681
                        . Util::backquote($each_database);
682
                }
683
684
                $each_tables = $this->fetchResult($sql, 'Name', null, $link);
685
686
                // Sort naturally if the config allows it and we're sorting
687
                // the Name column.
688
                if ($sort_by == 'Name' && $GLOBALS['cfg']['NaturalOrder']) {
689
                    uksort($each_tables, 'strnatcasecmp');
690
691
                    if ($sort_order == 'DESC') {
692
                        $each_tables = array_reverse($each_tables);
693
                    }
694
                } else {
695
                    // Prepare to sort by creating array of the selected sort
696
                    // value to pass to array_multisort
697
698
                    // Size = Data_length + Index_length
699
                    if ($sort_by == 'Data_length') {
700
                        foreach ($each_tables as $table_name => $table_data) {
701
                            ${$sort_by}[$table_name] = strtolower(
702
                                $table_data['Data_length']
703
                                + $table_data['Index_length']
704
                            );
705
                        }
706
                    } else {
707
                        foreach ($each_tables as $table_name => $table_data) {
708
                            ${$sort_by}[$table_name]
709
                                = strtolower($table_data[$sort_by]);
710
                        }
711
                    }
712
713
                    if (! empty($$sort_by)) {
714
                        if ($sort_order == 'DESC') {
715
                            array_multisort($$sort_by, SORT_DESC, $each_tables);
716
                        } else {
717
                            array_multisort($$sort_by, SORT_ASC, $each_tables);
718
                        }
719
                    }
720
721
                    // cleanup the temporary sort array
722
                    unset($$sort_by);
723
                }
724
725
                if ($limit_count) {
726
                    $each_tables = array_slice(
727
                        $each_tables,
728
                        $limit_offset,
729
                        $limit_count
730
                    );
731
                }
732
733
                foreach ($each_tables as $table_name => $each_table) {
734
                    if (! isset($each_tables[$table_name]['Type'])
735
                        && isset($each_tables[$table_name]['Engine'])
736
                    ) {
737
                        // pma BC, same parts of PMA still uses 'Type'
738
                        $each_tables[$table_name]['Type']
739
                            =& $each_tables[$table_name]['Engine'];
740
                    } elseif (! isset($each_tables[$table_name]['Engine'])
741
                        && isset($each_tables[$table_name]['Type'])
742
                    ) {
743
                        // old MySQL reports Type, newer MySQL reports Engine
744
                        $each_tables[$table_name]['Engine']
745
                            =& $each_tables[$table_name]['Type'];
746
                    }
747
748
                    // Compatibility with INFORMATION_SCHEMA output
749
                    $each_tables[$table_name]['TABLE_SCHEMA']
750
                        = $each_database;
751
                    $each_tables[$table_name]['TABLE_NAME']
752
                        =& $each_tables[$table_name]['Name'];
753
                    $each_tables[$table_name]['ENGINE']
754
                        =& $each_tables[$table_name]['Engine'];
755
                    $each_tables[$table_name]['VERSION']
756
                        =& $each_tables[$table_name]['Version'];
757
                    $each_tables[$table_name]['ROW_FORMAT']
758
                        =& $each_tables[$table_name]['Row_format'];
759
                    $each_tables[$table_name]['TABLE_ROWS']
760
                        =& $each_tables[$table_name]['Rows'];
761
                    $each_tables[$table_name]['AVG_ROW_LENGTH']
762
                        =& $each_tables[$table_name]['Avg_row_length'];
763
                    $each_tables[$table_name]['DATA_LENGTH']
764
                        =& $each_tables[$table_name]['Data_length'];
765
                    $each_tables[$table_name]['MAX_DATA_LENGTH']
766
                        =& $each_tables[$table_name]['Max_data_length'];
767
                    $each_tables[$table_name]['INDEX_LENGTH']
768
                        =& $each_tables[$table_name]['Index_length'];
769
                    $each_tables[$table_name]['DATA_FREE']
770
                        =& $each_tables[$table_name]['Data_free'];
771
                    $each_tables[$table_name]['AUTO_INCREMENT']
772
                        =& $each_tables[$table_name]['Auto_increment'];
773
                    $each_tables[$table_name]['CREATE_TIME']
774
                        =& $each_tables[$table_name]['Create_time'];
775
                    $each_tables[$table_name]['UPDATE_TIME']
776
                        =& $each_tables[$table_name]['Update_time'];
777
                    $each_tables[$table_name]['CHECK_TIME']
778
                        =& $each_tables[$table_name]['Check_time'];
779
                    $each_tables[$table_name]['TABLE_COLLATION']
780
                        =& $each_tables[$table_name]['Collation'];
781
                    $each_tables[$table_name]['CHECKSUM']
782
                        =& $each_tables[$table_name]['Checksum'];
783
                    $each_tables[$table_name]['CREATE_OPTIONS']
784
                        =& $each_tables[$table_name]['Create_options'];
785
                    $each_tables[$table_name]['TABLE_COMMENT']
786
                        =& $each_tables[$table_name]['Comment'];
787
788
                    if (strtoupper($each_tables[$table_name]['Comment']) === 'VIEW'
789
                        && $each_tables[$table_name]['Engine'] == null
790
                    ) {
791
                        $each_tables[$table_name]['TABLE_TYPE'] = 'VIEW';
792
                    } elseif ($each_database == 'information_schema') {
793
                        $each_tables[$table_name]['TABLE_TYPE'] = 'SYSTEM VIEW';
794
                    } else {
795
                        /**
796
                         * @todo difference between 'TEMPORARY' and 'BASE TABLE'
797
                         * but how to detect?
798
                         */
799
                        $each_tables[$table_name]['TABLE_TYPE'] = 'BASE TABLE';
800
                    }
801
                }
802
803
                $tables[$each_database] = $each_tables;
804
            }
805
        }
806
807
        // cache table data
808
        // so Table does not require to issue SHOW TABLE STATUS again
809
        $this->_cacheTableData($tables, $table);
0 ignored issues
show
Bug introduced by
It seems like $table can also be of type array; however, parameter $table of PhpMyAdmin\DatabaseInterface::_cacheTableData() does only seem to accept boolean|string, maybe add an additional type check? ( Ignorable by Annotation )

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

809
        $this->_cacheTableData($tables, /** @scrutinizer ignore-type */ $table);
Loading history...
810
811
        if (is_array($database)) {
0 ignored issues
show
introduced by
The condition is_array($database) is always false.
Loading history...
812
            return $tables;
813
        }
814
815
        if (isset($tables[$database])) {
816
            return $tables[$database];
817
        }
818
819
        if (isset($tables[mb_strtolower($database)])) {
820
            // on windows with lower_case_table_names = 1
821
            // MySQL returns
822
            // with SHOW DATABASES or information_schema.SCHEMATA: `Test`
823
            // but information_schema.TABLES gives `test`
824
            // see https://github.com/phpmyadmin/phpmyadmin/issues/8402
825
            return $tables[mb_strtolower($database)];
826
        }
827
828
        return $tables;
829
    }
830
831
    /**
832
     * Get VIEWs in a particular database
833
     *
834
     * @param string $db Database name to look in
835
     *
836
     * @return array Set of VIEWs inside the database
837
     */
838
    public function getVirtualTables(string $db): array
839
    {
840
        $tables_full = $this->getTablesFull($db);
841
        $views = [];
842
843
        foreach ($tables_full as $table => $tmp) {
844
            $_table = $this->getTable($db, (string)$table);
845
            if ($_table->isView()) {
846
                $views[] = $table;
847
            }
848
        }
849
850
        return $views;
851
    }
852
853
854
    /**
855
     * returns array with databases containing extended infos about them
856
     *
857
     * @param string   $database     database
858
     * @param boolean  $force_stats  retrieve stats also for MySQL < 5
859
     * @param integer  $link         link type
860
     * @param string   $sort_by      column to order by
861
     * @param string   $sort_order   ASC or DESC
862
     * @param integer  $limit_offset starting offset for LIMIT
863
     * @param bool|int $limit_count  row count for LIMIT or true
864
     *                               for $GLOBALS['cfg']['MaxDbList']
865
     *
866
     * @todo    move into ListDatabase?
867
     *
868
     * @return array
869
     */
870
    public function getDatabasesFull(
871
        ?string $database = null,
872
        bool $force_stats = false,
873
        $link = DatabaseInterface::CONNECT_USER,
874
        string $sort_by = 'SCHEMA_NAME',
875
        string $sort_order = 'ASC',
876
        int $limit_offset = 0,
877
        $limit_count = false
878
    ): array {
879
        $sort_order = strtoupper($sort_order);
880
881
        if (true === $limit_count) {
882
            $limit_count = $GLOBALS['cfg']['MaxDbList'];
883
        }
884
885
        $apply_limit_and_order_manual = true;
886
887
        if (! $GLOBALS['cfg']['Server']['DisableIS']) {
888
            /**
889
             * if $GLOBALS['cfg']['NaturalOrder'] is enabled, we cannot use LIMIT
890
             * cause MySQL does not support natural ordering,
891
             * we have to do it afterward
892
             */
893
            $limit = '';
894
            if (! $GLOBALS['cfg']['NaturalOrder']) {
895
                if ($limit_count) {
896
                    $limit = ' LIMIT ' . $limit_count . ' OFFSET ' . $limit_offset;
897
                }
898
899
                $apply_limit_and_order_manual = false;
900
            }
901
902
            // get table information from information_schema
903
            if (! empty($database)) {
904
                $sql_where_schema = 'WHERE `SCHEMA_NAME` LIKE \''
905
                    . $this->escapeString($database, $link) . '\'';
906
            } else {
907
                $sql_where_schema = '';
908
            }
909
910
            $sql  = 'SELECT *,
911
                    CAST(BIN_NAME AS CHAR CHARACTER SET utf8) AS SCHEMA_NAME
912
                FROM (';
913
            $sql .= 'SELECT
914
                BINARY s.SCHEMA_NAME AS BIN_NAME,
915
                s.DEFAULT_COLLATION_NAME';
916
            if ($force_stats) {
917
                $sql .= ',
918
                    COUNT(t.TABLE_SCHEMA)  AS SCHEMA_TABLES,
919
                    SUM(t.TABLE_ROWS)      AS SCHEMA_TABLE_ROWS,
920
                    SUM(t.DATA_LENGTH)     AS SCHEMA_DATA_LENGTH,
921
                    SUM(t.MAX_DATA_LENGTH) AS SCHEMA_MAX_DATA_LENGTH,
922
                    SUM(t.INDEX_LENGTH)    AS SCHEMA_INDEX_LENGTH,
923
                    SUM(t.DATA_LENGTH + t.INDEX_LENGTH)
924
                                           AS SCHEMA_LENGTH,
925
                    SUM(IF(t.ENGINE <> \'InnoDB\', t.DATA_FREE, 0))
926
                                           AS SCHEMA_DATA_FREE';
927
            }
928
            $sql .= '
929
                   FROM `information_schema`.SCHEMATA s ';
930
            if ($force_stats) {
931
                $sql .= '
932
                    LEFT JOIN `information_schema`.TABLES t
933
                        ON BINARY t.TABLE_SCHEMA = BINARY s.SCHEMA_NAME';
934
            }
935
            $sql .= $sql_where_schema . '
936
                    GROUP BY BINARY s.SCHEMA_NAME, s.DEFAULT_COLLATION_NAME
937
                    ORDER BY ';
938
            if ($sort_by == 'SCHEMA_NAME'
939
                || $sort_by == 'DEFAULT_COLLATION_NAME'
940
            ) {
941
                $sql .= 'BINARY ';
942
            }
943
            $sql .= Util::backquote($sort_by)
944
                . ' ' . $sort_order
945
                . $limit;
946
            $sql .= ') a';
947
948
            $databases = $this->fetchResult($sql, 'SCHEMA_NAME', null, $link);
949
950
            $mysql_error = $this->getError($link);
951
            if (! count($databases) && $GLOBALS['errno']) {
952
                Util::mysqlDie($mysql_error, $sql);
953
            }
954
955
            // display only databases also in official database list
956
            // f.e. to apply hide_db and only_db
957
            $drops = array_diff(
958
                array_keys($databases),
959
                (array) $GLOBALS['dblist']->databases
960
            );
961
            foreach ($drops as $drop) {
962
                unset($databases[$drop]);
963
            }
964
        } else {
965
            $databases = [];
966
            foreach ($GLOBALS['dblist']->databases as $database_name) {
967
                // Compatibility with INFORMATION_SCHEMA output
968
                $databases[$database_name]['SCHEMA_NAME']      = $database_name;
969
970
                $databases[$database_name]['DEFAULT_COLLATION_NAME']
971
                    = $this->getDbCollation($database_name);
972
973
                if (!$force_stats) {
974
                    continue;
975
                }
976
977
                // get additional info about tables
978
                $databases[$database_name]['SCHEMA_TABLES']          = 0;
979
                $databases[$database_name]['SCHEMA_TABLE_ROWS']      = 0;
980
                $databases[$database_name]['SCHEMA_DATA_LENGTH']     = 0;
981
                $databases[$database_name]['SCHEMA_MAX_DATA_LENGTH'] = 0;
982
                $databases[$database_name]['SCHEMA_INDEX_LENGTH']    = 0;
983
                $databases[$database_name]['SCHEMA_LENGTH']          = 0;
984
                $databases[$database_name]['SCHEMA_DATA_FREE']       = 0;
985
986
                $res = $this->query(
987
                    'SHOW TABLE STATUS FROM '
988
                    . Util::backquote($database_name) . ';'
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($database_name) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

988
                    . /** @scrutinizer ignore-type */ Util::backquote($database_name) . ';'
Loading history...
989
                );
990
991
                if ($res === false) {
992
                    unset($res);
993
                    continue;
994
                }
995
996
                while ($row = $this->fetchAssoc($res)) {
997
                    $databases[$database_name]['SCHEMA_TABLES']++;
998
                    $databases[$database_name]['SCHEMA_TABLE_ROWS']
999
                        += $row['Rows'];
1000
                    $databases[$database_name]['SCHEMA_DATA_LENGTH']
1001
                        += $row['Data_length'];
1002
                    $databases[$database_name]['SCHEMA_MAX_DATA_LENGTH']
1003
                        += $row['Max_data_length'];
1004
                    $databases[$database_name]['SCHEMA_INDEX_LENGTH']
1005
                        += $row['Index_length'];
1006
1007
                    // for InnoDB, this does not contain the number of
1008
                    // overhead bytes but the total free space
1009
                    if ('InnoDB' != $row['Engine']) {
1010
                        $databases[$database_name]['SCHEMA_DATA_FREE']
1011
                            += $row['Data_free'];
1012
                    }
1013
                    $databases[$database_name]['SCHEMA_LENGTH']
1014
                        += $row['Data_length'] + $row['Index_length'];
1015
                }
1016
                $this->freeResult($res);
1017
                unset($res);
1018
            }
1019
        }
1020
1021
        /**
1022
         * apply limit and order manually now
1023
         * (caused by older MySQL < 5 or $GLOBALS['cfg']['NaturalOrder'])
1024
         */
1025
        if ($apply_limit_and_order_manual) {
1026
            $GLOBALS['callback_sort_order'] = $sort_order;
1027
            $GLOBALS['callback_sort_by'] = $sort_by;
1028
            usort(
1029
                $databases,
1030
                [self::class, '_usortComparisonCallback']
1031
            );
1032
            unset($GLOBALS['callback_sort_order'], $GLOBALS['callback_sort_by']);
1033
1034
            /**
1035
             * now apply limit
1036
             */
1037
            if ($limit_count) {
1038
                $databases = array_slice($databases, $limit_offset, $limit_count);
1039
            }
1040
        }
1041
1042
        return $databases;
1043
    }
1044
1045
    /**
1046
     * usort comparison callback
1047
     *
1048
     * @param string $a first argument to sort
1049
     * @param string $b second argument to sort
1050
     *
1051
     * @return integer  a value representing whether $a should be before $b in the
1052
     *                   sorted array or not
1053
     *
1054
     * @access  private
1055
     */
1056
    private static function _usortComparisonCallback($a, $b): int
1057
    {
1058
        if ($GLOBALS['cfg']['NaturalOrder']) {
1059
            $sorter = 'strnatcasecmp';
1060
        } else {
1061
            $sorter = 'strcasecmp';
1062
        }
1063
        /* No sorting when key is not present */
1064
        if (! isset($a[$GLOBALS['callback_sort_by']])
1065
            || ! isset($b[$GLOBALS['callback_sort_by']])
1066
        ) {
1067
            return 0;
1068
        }
1069
        // produces f.e.:
1070
        // return -1 * strnatcasecmp($a["SCHEMA_TABLES"], $b["SCHEMA_TABLES"])
1071
        return ($GLOBALS['callback_sort_order'] == 'ASC' ? 1 : -1) * $sorter(
1072
            $a[$GLOBALS['callback_sort_by']],
1073
            $b[$GLOBALS['callback_sort_by']]
1074
        );
1075
    }
1076
1077
    /**
1078
     * returns detailed array with all columns for sql
1079
     *
1080
     * @param string $sql_query    target SQL query to get columns
1081
     * @param array  $view_columns alias for columns
1082
     *
1083
     * @return array
1084
     */
1085
    public function getColumnMapFromSql(string $sql_query, array $view_columns = []): array
1086
    {
1087
        $result = $this->tryQuery($sql_query);
1088
1089
        if ($result === false) {
1090
            return [];
1091
        }
1092
1093
        $meta = $this->getFieldsMeta(
1094
            $result
1095
        );
1096
1097
        $nbFields = count($meta);
1098
        if ($nbFields <= 0) {
1099
            return [];
1100
        }
1101
1102
        $column_map = [];
1103
        $nbColumns = count($view_columns);
1104
1105
        for ($i = 0; $i < $nbFields; $i++) {
1106
            $map = [];
1107
            $map['table_name'] = $meta[$i]->table;
1108
            $map['refering_column'] = $meta[$i]->name;
1109
1110
            if ($nbColumns > 1) {
1111
                $map['real_column'] = $view_columns[$i];
1112
            }
1113
1114
            $column_map[] = $map;
1115
        }
1116
1117
        return $column_map;
1118
    }
1119
1120
    /**
1121
     * returns detailed array with all columns for given table in database,
1122
     * or all tables/databases
1123
     *
1124
     * @param string $database name of database
1125
     * @param string $table    name of table to retrieve columns from
1126
     * @param string $column   name of specific column
1127
     * @param mixed  $link     mysql link resource
1128
     *
1129
     * @return array
1130
     */
1131
    public function getColumnsFull(
1132
        ?string $database = null,
1133
        ?string $table = null,
1134
        ?string $column = null,
1135
        $link = DatabaseInterface::CONNECT_USER
1136
    ): array {
1137
        if (! $GLOBALS['cfg']['Server']['DisableIS']) {
1138
            $sql_wheres = [];
1139
            $array_keys = [];
1140
1141
            // get columns information from information_schema
1142
            if (null !== $database) {
1143
                $sql_wheres[] = '`TABLE_SCHEMA` = \''
1144
                    . $this->escapeString($database, $link) . '\' ';
1145
            } else {
1146
                $array_keys[] = 'TABLE_SCHEMA';
1147
            }
1148
            if (null !== $table) {
1149
                $sql_wheres[] = '`TABLE_NAME` = \''
1150
                    . $this->escapeString($table, $link) . '\' ';
1151
            } else {
1152
                $array_keys[] = 'TABLE_NAME';
1153
            }
1154
            if (null !== $column) {
1155
                $sql_wheres[] = '`COLUMN_NAME` = \''
1156
                    . $this->escapeString($column, $link) . '\' ';
1157
            } else {
1158
                $array_keys[] = 'COLUMN_NAME';
1159
            }
1160
1161
            // for PMA bc:
1162
            // `[SCHEMA_FIELD_NAME]` AS `[SHOW_FULL_COLUMNS_FIELD_NAME]`
1163
            $sql = '
1164
                 SELECT *,
1165
                        `COLUMN_NAME`       AS `Field`,
1166
                        `COLUMN_TYPE`       AS `Type`,
1167
                        `COLLATION_NAME`    AS `Collation`,
1168
                        `IS_NULLABLE`       AS `Null`,
1169
                        `COLUMN_KEY`        AS `Key`,
1170
                        `COLUMN_DEFAULT`    AS `Default`,
1171
                        `EXTRA`             AS `Extra`,
1172
                        `PRIVILEGES`        AS `Privileges`,
1173
                        `COLUMN_COMMENT`    AS `Comment`
1174
                   FROM `information_schema`.`COLUMNS`';
1175
1176
            if (count($sql_wheres)) {
1177
                $sql .= "\n" . ' WHERE ' . implode(' AND ', $sql_wheres);
1178
            }
1179
            return $this->fetchResult($sql, $array_keys, null, $link);
1180
        }
1181
1182
        $columns = [];
1183
        if (null === $database) {
1184
            foreach ($GLOBALS['dblist']->databases as $database) {
1185
                $columns[$database] = $this->getColumnsFull(
1186
                    $database,
1187
                    null,
1188
                    null,
1189
                    $link
1190
                );
1191
            }
1192
            return $columns;
1193
        } elseif (null === $table) {
1194
            $tables = $this->getTables($database);
1195
            foreach ($tables as $table) {
1196
                $columns[$table] = $this->getColumnsFull(
1197
                    $database,
1198
                    $table,
1199
                    null,
1200
                    $link
1201
                );
1202
            }
1203
            return $columns;
1204
        }
1205
        $sql = 'SHOW FULL COLUMNS FROM '
1206
            . Util::backquote($database) . '.' . Util::backquote($table);
1207
        if (null !== $column) {
1208
            $sql .= " LIKE '" . $this->escapeString($column, $link) . "'";
1209
        }
1210
1211
        $columns = $this->fetchResult($sql, 'Field', null, $link);
1212
        $ordinal_position = 1;
1213
        foreach ($columns as $column_name => $each_column) {
1214
            // Compatibility with INFORMATION_SCHEMA output
1215
            $columns[$column_name]['COLUMN_NAME']
1216
                =& $columns[$column_name]['Field'];
1217
            $columns[$column_name]['COLUMN_TYPE']
1218
                =& $columns[$column_name]['Type'];
1219
            $columns[$column_name]['COLLATION_NAME']
1220
                =& $columns[$column_name]['Collation'];
1221
            $columns[$column_name]['IS_NULLABLE']
1222
                =& $columns[$column_name]['Null'];
1223
            $columns[$column_name]['COLUMN_KEY']
1224
                =& $columns[$column_name]['Key'];
1225
            $columns[$column_name]['COLUMN_DEFAULT']
1226
                =& $columns[$column_name]['Default'];
1227
            $columns[$column_name]['EXTRA']
1228
                =& $columns[$column_name]['Extra'];
1229
            $columns[$column_name]['PRIVILEGES']
1230
                =& $columns[$column_name]['Privileges'];
1231
            $columns[$column_name]['COLUMN_COMMENT']
1232
                =& $columns[$column_name]['Comment'];
1233
1234
            $columns[$column_name]['TABLE_CATALOG'] = null;
1235
            $columns[$column_name]['TABLE_SCHEMA'] = $database;
1236
            $columns[$column_name]['TABLE_NAME'] = $table;
1237
            $columns[$column_name]['ORDINAL_POSITION'] = $ordinal_position;
1238
            $columns[$column_name]['DATA_TYPE']
1239
                = substr(
1240
                    $columns[$column_name]['COLUMN_TYPE'],
1241
                    0,
1242
                    strpos($columns[$column_name]['COLUMN_TYPE'], '(')
1243
                );
1244
            /**
1245
             * @todo guess CHARACTER_MAXIMUM_LENGTH from COLUMN_TYPE
1246
            */
1247
            $columns[$column_name]['CHARACTER_MAXIMUM_LENGTH'] = null;
1248
            /**
1249
             * @todo guess CHARACTER_OCTET_LENGTH from CHARACTER_MAXIMUM_LENGTH
1250
             */
1251
            $columns[$column_name]['CHARACTER_OCTET_LENGTH'] = null;
1252
            $columns[$column_name]['NUMERIC_PRECISION'] = null;
1253
            $columns[$column_name]['NUMERIC_SCALE'] = null;
1254
            $columns[$column_name]['CHARACTER_SET_NAME']
1255
                = substr(
1256
                    $columns[$column_name]['COLLATION_NAME'],
1257
                    0,
1258
                    strpos($columns[$column_name]['COLLATION_NAME'], '_')
1259
                );
1260
1261
            $ordinal_position++;
1262
        }
1263
1264
        if (null !== $column) {
1265
            return reset($columns);
1266
        }
1267
1268
        return $columns;
1269
    }
1270
1271
    /**
1272
     * Returns SQL query for fetching columns for a table
1273
     *
1274
     * The 'Key' column is not calculated properly, use $GLOBALS['dbi']->getColumns()
1275
     * to get correct values.
1276
     *
1277
     * @param string  $database name of database
1278
     * @param string  $table    name of table to retrieve columns from
1279
     * @param string  $column   name of column, null to show all columns
1280
     * @param boolean $full     whether to return full info or only column names
1281
     *
1282
     * @see getColumns()
1283
     *
1284
     * @return string
1285
     */
1286
    public function getColumnsSql(
1287
        string $database,
1288
        string $table,
1289
        ?string $column = null,
1290
        bool $full = false
1291
    ): string {
1292
        $sql = 'SHOW ' . ($full ? 'FULL' : '') . ' COLUMNS FROM '
1293
            . Util::backquote($database) . '.' . Util::backquote($table)
1294
            . ($column !== null ? "LIKE '"
1295
            . $GLOBALS['dbi']->escapeString($column) . "'" : '');
1296
1297
        return $sql;
1298
    }
1299
1300
    /**
1301
     * Returns descriptions of columns in given table (all or given by $column)
1302
     *
1303
     * @param string  $database name of database
1304
     * @param string  $table    name of table to retrieve columns from
1305
     * @param string  $column   name of column, null to show all columns
1306
     * @param boolean $full     whether to return full info or only column names
1307
     * @param integer $link     link type
1308
     *
1309
     * @return array array indexed by column names or,
1310
     *               if $column is given, flat array description
1311
     */
1312
    public function getColumns(
1313
        string $database,
1314
        string $table,
1315
        ?string $column = null,
1316
        bool $full = false,
1317
        $link = DatabaseInterface::CONNECT_USER
1318
    ): array {
1319
        $sql = $this->getColumnsSql($database, $table, $column, $full);
1320
        $fields = $this->fetchResult($sql, 'Field', null, $link);
1321
        if (! is_array($fields) || count($fields) == 0) {
0 ignored issues
show
introduced by
The condition is_array($fields) is always true.
Loading history...
1322
            return [];
1323
        }
1324
        // Check if column is a part of multiple-column index and set its 'Key'.
1325
        $indexes = Index::getFromTable($table, $database);
1326
        foreach ($fields as $field => $field_data) {
1327
            if (!empty($field_data['Key'])) {
1328
                continue;
1329
            }
1330
1331
            foreach ($indexes as $index) {
1332
                /** @var Index $index */
1333
                if (!$index->hasColumn($field)) {
1334
                    continue;
1335
                }
1336
1337
                $index_columns = $index->getColumns();
1338
                if ($index_columns[$field]->getSeqInIndex() > 1) {
1339
                    if ($index->isUnique()) {
1340
                        $fields[$field]['Key'] = 'UNI';
1341
                    } else {
1342
                        $fields[$field]['Key'] = 'MUL';
1343
                    }
1344
                }
1345
            }
1346
        }
1347
1348
        return $column != null ? array_shift($fields) : $fields;
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $column of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
1349
    }
1350
1351
    /**
1352
     * Returns all column names in given table
1353
     *
1354
     * @param string $database name of database
1355
     * @param string $table    name of table to retrieve columns from
1356
     * @param mixed  $link     mysql link resource
1357
     *
1358
     * @return null|array
1359
     */
1360
    public function getColumnNames(
1361
        string $database,
1362
        string $table,
1363
        $link = DatabaseInterface::CONNECT_USER
1364
    ): ?array {
1365
        $sql = $this->getColumnsSql($database, $table);
1366
        // We only need the 'Field' column which contains the table's column names
1367
        $fields = array_keys($this->fetchResult($sql, 'Field', null, $link));
1368
1369
        if (! is_array($fields) || count($fields) == 0) {
0 ignored issues
show
introduced by
The condition is_array($fields) is always true.
Loading history...
1370
            return null;
1371
        }
1372
        return $fields;
1373
    }
1374
1375
    /**
1376
    * Returns SQL for fetching information on table indexes (SHOW INDEXES)
1377
    *
1378
    * @param string $database name of database
1379
    * @param string $table    name of the table whose indexes are to be retrieved
1380
    * @param string $where    additional conditions for WHERE
1381
    *
1382
    * @return string SQL for getting indexes
1383
    */
1384
    public function getTableIndexesSql(
1385
        string $database,
1386
        string $table,
1387
        ?string $where = null
1388
    ): string {
1389
        $sql = 'SHOW INDEXES FROM ' . Util::backquote($database) . '.'
1390
            . Util::backquote($table);
1391
        if ($where) {
1392
            $sql .= ' WHERE (' . $where . ')';
1393
        }
1394
        return $sql;
1395
    }
1396
1397
    /**
1398
     * Returns indexes of a table
1399
     *
1400
     * @param string $database name of database
1401
     * @param string $table    name of the table whose indexes are to be retrieved
1402
     * @param mixed  $link     mysql link resource
1403
     *
1404
     * @return array
1405
     */
1406
    public function getTableIndexes(
1407
        string $database,
1408
        string $table,
1409
        $link = DatabaseInterface::CONNECT_USER
1410
    ): array {
1411
        $sql = $this->getTableIndexesSql($database, $table);
1412
        $indexes = $this->fetchResult($sql, null, null, $link);
1413
1414
        if (! is_array($indexes) || count($indexes) < 1) {
0 ignored issues
show
introduced by
The condition is_array($indexes) is always true.
Loading history...
1415
            return [];
1416
        }
1417
        return $indexes;
1418
    }
1419
1420
    /**
1421
     * returns value of given mysql server variable
1422
     *
1423
     * @param string $var  mysql server variable name
1424
     * @param int    $type DatabaseInterface::GETVAR_SESSION |
1425
     *                     DatabaseInterface::GETVAR_GLOBAL
1426
     * @param mixed  $link mysql link resource|object
1427
     *
1428
     * @return mixed   value for mysql server variable
1429
     */
1430
    public function getVariable(
1431
        string $var,
1432
        int $type = self::GETVAR_SESSION,
1433
        $link = DatabaseInterface::CONNECT_USER
1434
    ) {
1435
        switch ($type) {
1436
            case self::GETVAR_SESSION:
1437
                $modifier = ' SESSION';
1438
                break;
1439
            case self::GETVAR_GLOBAL:
1440
                $modifier = ' GLOBAL';
1441
                break;
1442
            default:
1443
                $modifier = '';
1444
        }
1445
        return $this->fetchValue(
1446
            'SHOW' . $modifier . ' VARIABLES LIKE \'' . $var . '\';',
1447
            0,
1448
            1,
1449
            $link
1450
        );
1451
    }
1452
1453
    /**
1454
     * Sets new value for a variable if it is different from the current value
1455
     *
1456
     * @param string $var   variable name
1457
     * @param string $value value to set
1458
     * @param mixed  $link  mysql link resource|object
1459
     *
1460
     * @return bool whether query was a successful
1461
     */
1462
    public function setVariable(
1463
        string $var,
1464
        string $value,
1465
        $link = DatabaseInterface::CONNECT_USER
1466
    ): bool {
1467
        $current_value = $this->getVariable(
1468
            $var,
1469
            self::GETVAR_SESSION,
1470
            $link
1471
        );
1472
        if ($current_value == $value) {
1473
            return true;
1474
        }
1475
1476
        return $this->query("SET " . $var . " = " . $value . ';', $link);
1477
    }
1478
1479
    /**
1480
     * Convert version string to integer.
1481
     *
1482
     * @param string $version MySQL server version
1483
     *
1484
     * @return int
1485
     */
1486
    public static function versionToInt(string $version): int
1487
    {
1488
        $match = explode('.', $version);
1489
        return (int) sprintf('%d%02d%02d', $match[0], $match[1], intval($match[2]));
1490
    }
1491
1492
    /**
1493
     * Function called just after a connection to the MySQL database server has
1494
     * been established. It sets the connection collation, and determines the
1495
     * version of MySQL which is running.
1496
     *
1497
     * @return void
1498
     */
1499
    public function postConnect(): void
1500
    {
1501
        $version = $this->fetchSingleRow(
1502
            'SELECT @@version, @@version_comment',
1503
            'ASSOC',
1504
            DatabaseInterface::CONNECT_USER
1505
        );
1506
1507
        if ($version) {
1508
            $this->_version_int = self::versionToInt($version['@@version']);
1509
            $this->_version_str = $version['@@version'];
1510
            $this->_version_comment = $version['@@version_comment'];
1511
            if (stripos($version['@@version'], 'mariadb') !== false) {
1512
                $this->_is_mariadb = true;
1513
            }
1514
            if (stripos($version['@@version_comment'], 'percona') !== false) {
1515
                $this->_is_percona = true;
1516
            }
1517
        }
1518
1519
        if ($this->_version_int > 50503) {
1520
            $default_charset = 'utf8mb4';
1521
            $default_collation = 'utf8mb4_general_ci';
1522
        } else {
1523
            $default_charset = 'utf8';
1524
            $default_collation = 'utf8_general_ci';
1525
        }
1526
        $GLOBALS['collation_connection'] = $default_collation;
1527
        $GLOBALS['charset_connection'] = $default_charset;
1528
        $this->query(
1529
            "SET NAMES '$default_charset' COLLATE '$default_collation';",
1530
            DatabaseInterface::CONNECT_USER,
1531
            self::QUERY_STORE
1532
        );
1533
1534
        /* Locale for messages */
1535
        $locale = LanguageManager::getInstance()->getCurrentLanguage()->getMySQLLocale();
1536
        if (! empty($locale)) {
1537
            $this->query(
1538
                "SET lc_messages = '" . $locale . "';",
1539
                DatabaseInterface::CONNECT_USER,
1540
                self::QUERY_STORE
1541
            );
1542
        }
1543
1544
        // Set timezone for the session, if required.
1545
        if ($GLOBALS['cfg']['Server']['SessionTimeZone'] != '') {
1546
            $sql_query_tz = 'SET ' . Util::backquote('time_zone') . ' = '
1547
                . '\''
1548
                . $this->escapeString($GLOBALS['cfg']['Server']['SessionTimeZone'])
1549
                . '\'';
1550
1551
            if (! $this->tryQuery($sql_query_tz)) {
1552
                $error_message_tz = sprintf(
1553
                    __(
1554
                        'Unable to use timezone "%1$s" for server %2$d. '
1555
                        . 'Please check your configuration setting for '
1556
                        . '[em]$cfg[\'Servers\'][%3$d][\'SessionTimeZone\'][/em]. '
1557
                        . 'phpMyAdmin is currently using the default time zone '
1558
                        . 'of the database server.'
1559
                    ),
1560
                    $GLOBALS['cfg']['Server']['SessionTimeZone'],
1561
                    $GLOBALS['server'],
1562
                    $GLOBALS['server']
1563
                );
1564
1565
                trigger_error($error_message_tz, E_USER_WARNING);
1566
            }
1567
        }
1568
1569
        /* Loads closest context to this version. */
1570
        \PhpMyAdmin\SqlParser\Context::loadClosest(
1571
            ($this->_is_mariadb ? 'MariaDb' : 'MySql') . $this->_version_int
1572
        );
1573
1574
        /**
1575
         * the DatabaseList class as a stub for the ListDatabase class
1576
         */
1577
        $GLOBALS['dblist'] = new DatabaseList();
1578
    }
1579
1580
    /**
1581
     * Sets collation connection for user link
1582
     *
1583
     * @param string $collation collation to set
1584
     *
1585
     * @return void
1586
     */
1587
    public function setCollation(string $collation): void
1588
    {
1589
        $charset = $GLOBALS['charset_connection'];
1590
        /* Automatically adjust collation if not supported by server */
1591
        if ($charset == 'utf8' && strncmp('utf8mb4_', $collation, 8) == 0) {
1592
            $collation = 'utf8_' . substr($collation, 8);
1593
        }
1594
        $result = $this->tryQuery(
1595
            "SET collation_connection = '"
1596
            . $this->escapeString($collation, DatabaseInterface::CONNECT_USER)
1597
            . "';",
1598
            DatabaseInterface::CONNECT_USER,
1599
            self::QUERY_STORE
1600
        );
1601
        if ($result === false) {
1602
            trigger_error(
1603
                __('Failed to set configured collation connection!'),
1604
                E_USER_WARNING
1605
            );
1606
        } else {
1607
            $GLOBALS['collation_connection'] = $collation;
1608
        }
1609
    }
1610
1611
    /**
1612
     * Function called just after a connection to the MySQL database server has
1613
     * been established. It sets the connection collation, and determines the
1614
     * version of MySQL which is running.
1615
     *
1616
     * @return void
1617
     */
1618
    public function postConnectControl(): void
1619
    {
1620
        // If Zero configuration mode enabled, check PMA tables in current db.
1621
        if ($GLOBALS['cfg']['ZeroConf'] == true) {
1622
            /**
1623
             * the DatabaseList class as a stub for the ListDatabase class
1624
             */
1625
            $GLOBALS['dblist'] = new DatabaseList();
1626
1627
            if (strlen($GLOBALS['db'])) {
1628
                $cfgRelation = $this->relation->getRelationsParam();
1629
                if (empty($cfgRelation['db'])) {
1630
                    $this->relation->fixPmaTables($GLOBALS['db'], false);
1631
                }
1632
            }
1633
            $cfgRelation = $this->relation->getRelationsParam();
1634
            if (empty($cfgRelation['db'])) {
1635
                if ($GLOBALS['dblist']->databases->exists('phpmyadmin')) {
1636
                    $this->relation->fixPmaTables('phpmyadmin', false);
1637
                }
1638
            }
1639
        }
1640
    }
1641
1642
    /**
1643
     * returns a single value from the given result or query,
1644
     * if the query or the result has more than one row or field
1645
     * the first field of the first row is returned
1646
     *
1647
     * <code>
1648
     * $sql = 'SELECT `name` FROM `user` WHERE `id` = 123';
1649
     * $user_name = $GLOBALS['dbi']->fetchValue($sql);
1650
     * // produces
1651
     * // $user_name = 'John Doe'
1652
     * </code>
1653
     *
1654
     * @param string         $query      The query to execute
1655
     * @param integer        $row_number row to fetch the value from,
1656
     *                                   starting at 0, with 0 being default
1657
     * @param integer|string $field      field to fetch the value from,
1658
     *                                   starting at 0, with 0 being default
1659
     * @param integer        $link       link type
1660
     *
1661
     * @return mixed value of first field in first row from result
1662
     *               or false if not found
1663
     */
1664
    public function fetchValue(
1665
        string $query,
1666
        int $row_number = 0,
1667
        $field = 0,
1668
        $link = DatabaseInterface::CONNECT_USER
1669
    ) {
1670
        $value = false;
1671
1672
        $result = $this->tryQuery(
1673
            $query,
1674
            $link,
1675
            self::QUERY_STORE,
1676
            false
1677
        );
1678
        if ($result === false) {
1679
            return false;
1680
        }
1681
1682
        // return false if result is empty or false
1683
        // or requested row is larger than rows in result
1684
        if ($this->numRows($result) < ($row_number + 1)) {
1685
            return $value;
1686
        }
1687
1688
        // if $field is an integer use non associative mysql fetch function
1689
        if (is_int($field)) {
1690
            $fetch_function = 'fetchRow';
1691
        } else {
1692
            $fetch_function = 'fetchAssoc';
1693
        }
1694
1695
        // get requested row
1696
        for ($i = 0; $i <= $row_number; $i++) {
1697
            $row = $this->$fetch_function($result);
1698
        }
1699
        $this->freeResult($result);
1700
1701
        // return requested field
1702
        if (isset($row[$field])) {
1703
            $value = $row[$field];
1704
        }
1705
1706
        return $value;
1707
    }
1708
1709
    /**
1710
     * returns only the first row from the result
1711
     *
1712
     * <code>
1713
     * $sql = 'SELECT * FROM `user` WHERE `id` = 123';
1714
     * $user = $GLOBALS['dbi']->fetchSingleRow($sql);
1715
     * // produces
1716
     * // $user = array('id' => 123, 'name' => 'John Doe')
1717
     * </code>
1718
     *
1719
     * @param string  $query The query to execute
1720
     * @param string  $type  NUM|ASSOC|BOTH returned array should either numeric
1721
     *                       associative or both
1722
     * @param integer $link  link type
1723
     *
1724
     * @return array|boolean first row from result
1725
     *                       or false if result is empty
1726
     */
1727
    public function fetchSingleRow(
1728
        string $query,
1729
        string $type = 'ASSOC',
1730
        $link = DatabaseInterface::CONNECT_USER
1731
    ) {
1732
        $result = $this->tryQuery(
1733
            $query,
1734
            $link,
1735
            self::QUERY_STORE,
1736
            false
1737
        );
1738
        if ($result === false) {
1739
            return false;
1740
        }
1741
1742
        // return false if result is empty or false
1743
        if (! $this->numRows($result)) {
1744
            return false;
1745
        }
1746
1747
        switch ($type) {
1748
            case 'NUM':
1749
                $fetch_function = 'fetchRow';
1750
                break;
1751
            case 'ASSOC':
1752
                $fetch_function = 'fetchAssoc';
1753
                break;
1754
            case 'BOTH':
1755
            default:
1756
                $fetch_function = 'fetchArray';
1757
                break;
1758
        }
1759
1760
        $row = $this->$fetch_function($result);
1761
        $this->freeResult($result);
1762
        return $row;
1763
    }
1764
1765
    /**
1766
     * Returns row or element of a row
1767
     *
1768
     * @param array           $row   Row to process
1769
     * @param string|null|int $value Which column to return
1770
     *
1771
     * @return mixed
1772
     */
1773
    private function _fetchValue(array $row, $value)
1774
    {
1775
        if (is_null($value)) {
1776
            return $row;
1777
        }
1778
1779
        return $row[$value];
1780
    }
1781
1782
    /**
1783
     * returns all rows in the resultset in one array
1784
     *
1785
     * <code>
1786
     * $sql = 'SELECT * FROM `user`';
1787
     * $users = $GLOBALS['dbi']->fetchResult($sql);
1788
     * // produces
1789
     * // $users[] = array('id' => 123, 'name' => 'John Doe')
1790
     *
1791
     * $sql = 'SELECT `id`, `name` FROM `user`';
1792
     * $users = $GLOBALS['dbi']->fetchResult($sql, 'id');
1793
     * // produces
1794
     * // $users['123'] = array('id' => 123, 'name' => 'John Doe')
1795
     *
1796
     * $sql = 'SELECT `id`, `name` FROM `user`';
1797
     * $users = $GLOBALS['dbi']->fetchResult($sql, 0);
1798
     * // produces
1799
     * // $users['123'] = array(0 => 123, 1 => 'John Doe')
1800
     *
1801
     * $sql = 'SELECT `id`, `name` FROM `user`';
1802
     * $users = $GLOBALS['dbi']->fetchResult($sql, 'id', 'name');
1803
     * // or
1804
     * $users = $GLOBALS['dbi']->fetchResult($sql, 0, 1);
1805
     * // produces
1806
     * // $users['123'] = 'John Doe'
1807
     *
1808
     * $sql = 'SELECT `name` FROM `user`';
1809
     * $users = $GLOBALS['dbi']->fetchResult($sql);
1810
     * // produces
1811
     * // $users[] = 'John Doe'
1812
     *
1813
     * $sql = 'SELECT `group`, `name` FROM `user`'
1814
     * $users = $GLOBALS['dbi']->fetchResult($sql, array('group', null), 'name');
1815
     * // produces
1816
     * // $users['admin'][] = 'John Doe'
1817
     *
1818
     * $sql = 'SELECT `group`, `name` FROM `user`'
1819
     * $users = $GLOBALS['dbi']->fetchResult($sql, array('group', 'name'), 'id');
1820
     * // produces
1821
     * // $users['admin']['John Doe'] = '123'
1822
     * </code>
1823
     *
1824
     * @param string               $query   query to execute
1825
     * @param string|integer|array $key     field-name or offset
1826
     *                                      used as key for array
1827
     *                                      or array of those
1828
     * @param string|integer       $value   value-name or offset
1829
     *                                      used as value for array
1830
     * @param integer              $link    link type
1831
     * @param integer              $options query options
1832
     *
1833
     * @return array resultrows or values indexed by $key
1834
     */
1835
    public function fetchResult(
1836
        string $query,
1837
        $key = null,
1838
        $value = null,
1839
        $link = DatabaseInterface::CONNECT_USER,
1840
        int $options = 0
1841
    ) {
1842
        $resultrows = [];
1843
1844
        $result = $this->tryQuery($query, $link, $options, false);
1845
1846
        // return empty array if result is empty or false
1847
        if ($result === false) {
1848
            return $resultrows;
1849
        }
1850
1851
        $fetch_function = 'fetchAssoc';
1852
1853
        // no nested array if only one field is in result
1854
        if (null === $key && 1 === $this->numFields($result)) {
1855
            $value = 0;
1856
            $fetch_function = 'fetchRow';
1857
        }
1858
1859
        // if $key is an integer use non associative mysql fetch function
1860
        if (is_int($key)) {
1861
            $fetch_function = 'fetchRow';
1862
        }
1863
1864
        if (null === $key) {
1865
            while ($row = $this->$fetch_function($result)) {
1866
                $resultrows[] = $this->_fetchValue($row, $value);
1867
            }
1868
        } else {
1869
            if (is_array($key)) {
1870
                while ($row = $this->$fetch_function($result)) {
1871
                    $result_target =& $resultrows;
1872
                    foreach ($key as $key_index) {
1873
                        if (null === $key_index) {
1874
                            $result_target =& $result_target[];
1875
                            continue;
1876
                        }
1877
1878
                        if (! isset($result_target[$row[$key_index]])) {
1879
                            $result_target[$row[$key_index]] = [];
1880
                        }
1881
                        $result_target =& $result_target[$row[$key_index]];
1882
                    }
1883
                    $result_target = $this->_fetchValue($row, $value);
1884
                }
1885
            } else {
1886
                while ($row = $this->$fetch_function($result)) {
1887
                    $resultrows[$row[$key]] = $this->_fetchValue($row, $value);
1888
                }
1889
            }
1890
        }
1891
1892
        $this->freeResult($result);
1893
        return $resultrows;
1894
    }
1895
1896
    /**
1897
     * Get supported SQL compatibility modes
1898
     *
1899
     * @return array supported SQL compatibility modes
1900
     */
1901
    public function getCompatibilities(): array
1902
    {
1903
        $compats = ['NONE'];
1904
        $compats[] = 'ANSI';
1905
        $compats[] = 'DB2';
1906
        $compats[] = 'MAXDB';
1907
        $compats[] = 'MYSQL323';
1908
        $compats[] = 'MYSQL40';
1909
        $compats[] = 'MSSQL';
1910
        $compats[] = 'ORACLE';
1911
        // removed; in MySQL 5.0.33, this produces exports that
1912
        // can't be read by POSTGRESQL (see our bug #1596328)
1913
        //$compats[] = 'POSTGRESQL';
1914
        $compats[] = 'TRADITIONAL';
1915
1916
        return $compats;
1917
    }
1918
1919
    /**
1920
     * returns warnings for last query
1921
     *
1922
     * @param integer $link link type
1923
     *
1924
     * @return array warnings
1925
     */
1926
    public function getWarnings($link = DatabaseInterface::CONNECT_USER): array
1927
    {
1928
        return $this->fetchResult('SHOW WARNINGS', null, null, $link);
1929
    }
1930
1931
    /**
1932
     * returns an array of PROCEDURE or FUNCTION names for a db
1933
     *
1934
     * @param string  $db    db name
1935
     * @param string  $which PROCEDURE | FUNCTION
1936
     * @param integer $link  link type
1937
     *
1938
     * @return array the procedure names or function names
1939
     */
1940
    public function getProceduresOrFunctions(
1941
        string $db,
1942
        string $which,
1943
        $link = DatabaseInterface::CONNECT_USER
1944
    ): array {
1945
        $shows = $this->fetchResult(
1946
            'SHOW ' . $which . ' STATUS;',
1947
            null,
1948
            null,
1949
            $link
1950
        );
1951
        $result = [];
1952
        foreach ($shows as $one_show) {
1953
            if ($one_show['Db'] == $db && $one_show['Type'] == $which) {
1954
                $result[] = $one_show['Name'];
1955
            }
1956
        }
1957
        return $result;
1958
    }
1959
1960
    /**
1961
     * returns the definition of a specific PROCEDURE, FUNCTION, EVENT or VIEW
1962
     *
1963
     * @param string  $db    db name
1964
     * @param string  $which PROCEDURE | FUNCTION | EVENT | VIEW
1965
     * @param string  $name  the procedure|function|event|view name
1966
     * @param integer $link  link type
1967
     *
1968
     * @return string the definition
1969
     */
1970
    public function getDefinition(
1971
        string $db,
1972
        string $which,
1973
        string $name,
1974
        $link = DatabaseInterface::CONNECT_USER
1975
    ): string {
1976
        $returned_field = [
1977
            'PROCEDURE' => 'Create Procedure',
1978
            'FUNCTION'  => 'Create Function',
1979
            'EVENT'     => 'Create Event',
1980
            'VIEW'      => 'Create View'
1981
        ];
1982
        $query = 'SHOW CREATE ' . $which . ' '
1983
            . Util::backquote($db) . '.'
1984
            . Util::backquote($name);
1985
        return $this->fetchValue($query, 0, $returned_field[$which], $link);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->fetchValue...d_field[$which], $link) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
1986
    }
1987
1988
    /**
1989
     * returns details about the PROCEDUREs or FUNCTIONs for a specific database
1990
     * or details about a specific routine
1991
     *
1992
     * @param string $db    db name
1993
     * @param string $which PROCEDURE | FUNCTION or null for both
1994
     * @param string $name  name of the routine (to fetch a specific routine)
1995
     *
1996
     * @return array information about ROCEDUREs or FUNCTIONs
1997
     */
1998
    public function getRoutines(
1999
        string $db,
2000
        ?string $which = null,
2001
        string $name = ''
2002
    ): array {
2003
        $routines = [];
2004
        if (! $GLOBALS['cfg']['Server']['DisableIS']) {
2005
            $query = "SELECT"
2006
                . " `ROUTINE_SCHEMA` AS `Db`,"
2007
                . " `SPECIFIC_NAME` AS `Name`,"
2008
                . " `ROUTINE_TYPE` AS `Type`,"
2009
                . " `DEFINER` AS `Definer`,"
2010
                . " `LAST_ALTERED` AS `Modified`,"
2011
                . " `CREATED` AS `Created`,"
2012
                . " `SECURITY_TYPE` AS `Security_type`,"
2013
                . " `ROUTINE_COMMENT` AS `Comment`,"
2014
                . " `CHARACTER_SET_CLIENT` AS `character_set_client`,"
2015
                . " `COLLATION_CONNECTION` AS `collation_connection`,"
2016
                . " `DATABASE_COLLATION` AS `Database Collation`,"
2017
                . " `DTD_IDENTIFIER`"
2018
                . " FROM `information_schema`.`ROUTINES`"
2019
                . " WHERE `ROUTINE_SCHEMA` " . Util::getCollateForIS()
2020
                . " = '" . $GLOBALS['dbi']->escapeString($db) . "'";
2021
            if (Core::isValid($which, ['FUNCTION','PROCEDURE'])) {
2022
                $query .= " AND `ROUTINE_TYPE` = '" . $which . "'";
2023
            }
2024
            if (! empty($name)) {
2025
                $query .= " AND `SPECIFIC_NAME`"
2026
                    . " = '" . $GLOBALS['dbi']->escapeString($name) . "'";
2027
            }
2028
            $result = $this->fetchResult($query);
2029
            if (!empty($result)) {
2030
                $routines = $result;
2031
            }
2032
        } else {
2033
            if ($which == 'FUNCTION' || $which == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $which of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
2034
                $query = "SHOW FUNCTION STATUS"
2035
                    . " WHERE `Db` = '" . $GLOBALS['dbi']->escapeString($db) . "'";
2036
                if (! empty($name)) {
2037
                    $query .= " AND `Name` = '"
2038
                        . $GLOBALS['dbi']->escapeString($name) . "'";
2039
                }
2040
                $result = $this->fetchResult($query);
2041
                if (!empty($result)) {
2042
                    $routines = array_merge($routines, $result);
2043
                }
2044
            }
2045
            if ($which == 'PROCEDURE' || $which == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $which of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
2046
                $query = "SHOW PROCEDURE STATUS"
2047
                    . " WHERE `Db` = '" . $GLOBALS['dbi']->escapeString($db) . "'";
2048
                if (! empty($name)) {
2049
                    $query .= " AND `Name` = '"
2050
                        . $GLOBALS['dbi']->escapeString($name) . "'";
2051
                }
2052
                $result = $this->fetchResult($query);
2053
                if (!empty($result)) {
2054
                    $routines = array_merge($routines, $result);
2055
                }
2056
            }
2057
        }
2058
2059
        $ret = [];
2060
        foreach ($routines as $routine) {
2061
            $one_result = [];
2062
            $one_result['db'] = $routine['Db'];
2063
            $one_result['name'] = $routine['Name'];
2064
            $one_result['type'] = $routine['Type'];
2065
            $one_result['definer'] = $routine['Definer'];
2066
            $one_result['returns'] = isset($routine['DTD_IDENTIFIER'])
2067
                ? $routine['DTD_IDENTIFIER'] : "";
2068
            $ret[] = $one_result;
2069
        }
2070
2071
        // Sort results by name
2072
        $name = [];
2073
        foreach ($ret as $value) {
2074
            $name[] = $value['name'];
2075
        }
2076
        array_multisort($name, SORT_ASC, $ret);
2077
2078
        return $ret;
2079
    }
2080
2081
    /**
2082
     * returns details about the EVENTs for a specific database
2083
     *
2084
     * @param string $db   db name
2085
     * @param string $name event name
2086
     *
2087
     * @return array information about EVENTs
2088
     */
2089
    public function getEvents(string $db, string $name = ''): array
2090
    {
2091
        if (! $GLOBALS['cfg']['Server']['DisableIS']) {
2092
            $query = "SELECT"
2093
                . " `EVENT_SCHEMA` AS `Db`,"
2094
                . " `EVENT_NAME` AS `Name`,"
2095
                . " `DEFINER` AS `Definer`,"
2096
                . " `TIME_ZONE` AS `Time zone`,"
2097
                . " `EVENT_TYPE` AS `Type`,"
2098
                . " `EXECUTE_AT` AS `Execute at`,"
2099
                . " `INTERVAL_VALUE` AS `Interval value`,"
2100
                . " `INTERVAL_FIELD` AS `Interval field`,"
2101
                . " `STARTS` AS `Starts`,"
2102
                . " `ENDS` AS `Ends`,"
2103
                . " `STATUS` AS `Status`,"
2104
                . " `ORIGINATOR` AS `Originator`,"
2105
                . " `CHARACTER_SET_CLIENT` AS `character_set_client`,"
2106
                . " `COLLATION_CONNECTION` AS `collation_connection`, "
2107
                . "`DATABASE_COLLATION` AS `Database Collation`"
2108
                . " FROM `information_schema`.`EVENTS`"
2109
                . " WHERE `EVENT_SCHEMA` " . Util::getCollateForIS()
2110
                . " = '" . $GLOBALS['dbi']->escapeString($db) . "'";
2111
            if (! empty($name)) {
2112
                $query .= " AND `EVENT_NAME`"
2113
                    . " = '" . $GLOBALS['dbi']->escapeString($name) . "'";
2114
            }
2115
        } else {
2116
            $query = "SHOW EVENTS FROM " . Util::backquote($db);
2117
            if (! empty($name)) {
2118
                $query .= " AND `Name` = '"
2119
                    . $GLOBALS['dbi']->escapeString($name) . "'";
2120
            }
2121
        }
2122
2123
        $result = [];
2124
        if ($events = $this->fetchResult($query)) {
2125
            foreach ($events as $event) {
2126
                $one_result = [];
2127
                $one_result['name'] = $event['Name'];
2128
                $one_result['type'] = $event['Type'];
2129
                $one_result['status'] = $event['Status'];
2130
                $result[] = $one_result;
2131
            }
2132
        }
2133
2134
        // Sort results by name
2135
        $name = [];
2136
        foreach ($result as $value) {
2137
            $name[] = $value['name'];
2138
        }
2139
        array_multisort($name, SORT_ASC, $result);
2140
2141
        return $result;
2142
    }
2143
2144
    /**
2145
     * returns details about the TRIGGERs for a specific table or database
2146
     *
2147
     * @param string $db        db name
2148
     * @param string $table     table name
2149
     * @param string $delimiter the delimiter to use (may be empty)
2150
     *
2151
     * @return mixed information about triggers (may be empty)
2152
     */
2153
    public function getTriggers(string $db, string $table = '', $delimiter = '//')
2154
    {
2155
        $result = [];
2156
        if (! $GLOBALS['cfg']['Server']['DisableIS']) {
2157
            $query = 'SELECT TRIGGER_SCHEMA, TRIGGER_NAME, EVENT_MANIPULATION'
2158
                . ', EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT'
2159
                . ', EVENT_OBJECT_SCHEMA, EVENT_OBJECT_TABLE, DEFINER'
2160
                . ' FROM information_schema.TRIGGERS'
2161
                . ' WHERE EVENT_OBJECT_SCHEMA ' . Util::getCollateForIS() . '='
2162
                . ' \'' . $GLOBALS['dbi']->escapeString($db) . '\'';
2163
2164
            if (! empty($table)) {
2165
                $query .= " AND EVENT_OBJECT_TABLE " . Util::getCollateForIS()
2166
                    . " = '" . $GLOBALS['dbi']->escapeString($table) . "';";
2167
            }
2168
        } else {
2169
            $query = "SHOW TRIGGERS FROM " . Util::backquote($db);
2170
            if (! empty($table)) {
2171
                $query .= " LIKE '" . $GLOBALS['dbi']->escapeString($table) . "';";
2172
            }
2173
        }
2174
2175
        if ($triggers = $this->fetchResult($query)) {
2176
            foreach ($triggers as $trigger) {
2177
                if ($GLOBALS['cfg']['Server']['DisableIS']) {
2178
                    $trigger['TRIGGER_NAME'] = $trigger['Trigger'];
2179
                    $trigger['ACTION_TIMING'] = $trigger['Timing'];
2180
                    $trigger['EVENT_MANIPULATION'] = $trigger['Event'];
2181
                    $trigger['EVENT_OBJECT_TABLE'] = $trigger['Table'];
2182
                    $trigger['ACTION_STATEMENT'] = $trigger['Statement'];
2183
                    $trigger['DEFINER'] = $trigger['Definer'];
2184
                }
2185
                $one_result = [];
2186
                $one_result['name'] = $trigger['TRIGGER_NAME'];
2187
                $one_result['table'] = $trigger['EVENT_OBJECT_TABLE'];
2188
                $one_result['action_timing'] = $trigger['ACTION_TIMING'];
2189
                $one_result['event_manipulation'] = $trigger['EVENT_MANIPULATION'];
2190
                $one_result['definition'] = $trigger['ACTION_STATEMENT'];
2191
                $one_result['definer'] = $trigger['DEFINER'];
2192
2193
                // do not prepend the schema name; this way, importing the
2194
                // definition into another schema will work
2195
                $one_result['full_trigger_name'] = Util::backquote(
2196
                    $trigger['TRIGGER_NAME']
2197
                );
2198
                $one_result['drop'] = 'DROP TRIGGER IF EXISTS '
2199
                    . $one_result['full_trigger_name'];
0 ignored issues
show
Bug introduced by
Are you sure $one_result['full_trigger_name'] of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

2199
                    . /** @scrutinizer ignore-type */ $one_result['full_trigger_name'];
Loading history...
2200
                $one_result['create'] = 'CREATE TRIGGER '
2201
                    . $one_result['full_trigger_name'] . ' '
2202
                    . $trigger['ACTION_TIMING'] . ' '
2203
                    . $trigger['EVENT_MANIPULATION']
2204
                    . ' ON ' . Util::backquote($trigger['EVENT_OBJECT_TABLE'])
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquo...['EVENT_OBJECT_TABLE']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

2204
                    . ' ON ' . /** @scrutinizer ignore-type */ Util::backquote($trigger['EVENT_OBJECT_TABLE'])
Loading history...
2205
                    . "\n" . ' FOR EACH ROW '
2206
                    . $trigger['ACTION_STATEMENT'] . "\n" . $delimiter . "\n";
2207
2208
                $result[] = $one_result;
2209
            }
2210
        }
2211
2212
        // Sort results by name
2213
        $name = [];
2214
        foreach ($result as $value) {
2215
            $name[] = $value['name'];
2216
        }
2217
        array_multisort($name, SORT_ASC, $result);
2218
2219
        return $result;
2220
    }
2221
2222
    /**
2223
     * Formats database error message in a friendly way.
2224
     * This is needed because some errors messages cannot
2225
     * be obtained by mysql_error().
2226
     *
2227
     * @param int    $error_number  Error code
2228
     * @param string $error_message Error message as returned by server
2229
     *
2230
     * @return string HML text with error details
2231
     */
2232
    public static function formatError(int $error_number, string $error_message): string
2233
    {
2234
        $error_message = htmlspecialchars($error_message);
2235
2236
        $error = '#' . ((string) $error_number);
2237
        $separator = ' &mdash; ';
2238
2239
        if ($error_number == 2002) {
2240
            $error .= ' - ' . $error_message;
2241
            $error .= $separator;
2242
            $error .= __(
2243
                'The server is not responding (or the local server\'s socket'
2244
                . ' is not correctly configured).'
2245
            );
2246
        } elseif ($error_number == 2003) {
2247
            $error .= ' - ' . $error_message;
2248
            $error .= $separator . __('The server is not responding.');
2249
        } elseif ($error_number == 1698) {
2250
            $error .= ' - ' . $error_message;
2251
            $error .= $separator . '<a href="logout.php' . Url::getCommon() . '">';
2252
            $error .= __('Logout and try as another user.') . '</a>';
2253
        } elseif ($error_number == 1005) {
2254
            if (strpos($error_message, 'errno: 13') !== false) {
2255
                $error .= ' - ' . $error_message;
2256
                $error .= $separator
2257
                    . __(
2258
                        'Please check privileges of directory containing database.'
2259
                    );
2260
            } else {
2261
                /* InnoDB constraints, see
2262
                 * https://dev.mysql.com/doc/refman/5.0/en/
2263
                 *  innodb-foreign-key-constraints.html
2264
                 */
2265
                $error .= ' - ' . $error_message .
2266
                    ' (<a href="server_engines.php' .
2267
                    Url::getCommon(
2268
                        ['engine' => 'InnoDB', 'page' => 'Status']
2269
                    ) . '">' . __('Details…') . '</a>)';
2270
            }
2271
        } else {
2272
            $error .= ' - ' . $error_message;
2273
        }
2274
2275
        return $error;
2276
    }
2277
2278
    /**
2279
     * gets the current user with host
2280
     *
2281
     * @return string the current user i.e. user@host
2282
     */
2283
    public function getCurrentUser(): string
2284
    {
2285
        if (Util::cacheExists('mysql_cur_user')) {
2286
            return Util::cacheGet('mysql_cur_user');
0 ignored issues
show
Bug Best Practice introduced by
The expression return PhpMyAdmin\Util::...heGet('mysql_cur_user') could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
2287
        }
2288
        $user = $this->fetchValue('SELECT CURRENT_USER();');
2289
        if ($user !== false) {
2290
            Util::cacheSet('mysql_cur_user', $user);
2291
            return $user;
2292
        }
2293
        return '@';
2294
    }
2295
2296
    /**
2297
     * Checks if current user is superuser
2298
     *
2299
     * @return bool Whether user is a superuser
2300
     */
2301
    public function isSuperuser(): bool
2302
    {
2303
        return self::isUserType('super');
0 ignored issues
show
Bug Best Practice introduced by
The method PhpMyAdmin\DatabaseInterface::isUserType() is not static, but was called statically. ( Ignorable by Annotation )

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

2303
        return self::/** @scrutinizer ignore-call */ isUserType('super');
Loading history...
2304
    }
2305
2306
    /**
2307
     * Checks if current user has global create user/grant privilege
2308
     * or is a superuser (i.e. SELECT on mysql.users)
2309
     * while caching the result in session.
2310
     *
2311
     * @param string $type type of user to check for
2312
     *                     i.e. 'create', 'grant', 'super'
2313
     *
2314
     * @return bool Whether user is a given type of user
2315
     */
2316
    public function isUserType(string $type): bool
2317
    {
2318
        if (Util::cacheExists('is_' . $type . 'user')) {
2319
            return Util::cacheGet('is_' . $type . 'user');
0 ignored issues
show
Bug Best Practice introduced by
The expression return PhpMyAdmin\Util::...'is_' . $type . 'user') could return the type null which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
2320
        }
2321
2322
        // when connection failed we don't have a $userlink
2323
        if (! isset($this->_links[DatabaseInterface::CONNECT_USER])) {
2324
            return false;
2325
        }
2326
2327
        // checking if user is logged in
2328
        if ($type === 'logged') {
2329
            return true;
2330
        }
2331
2332
        if (! $GLOBALS['cfg']['Server']['DisableIS'] || $type === 'super') {
2333
            // Prepare query for each user type check
2334
            $query = '';
2335
            if ($type === 'super') {
2336
                $query = 'SELECT 1 FROM mysql.user LIMIT 1';
2337
            } elseif ($type === 'create') {
2338
                list($user, $host) = $this->getCurrentUserAndHost();
2339
                $query = "SELECT 1 FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES` "
2340
                    . "WHERE `PRIVILEGE_TYPE` = 'CREATE USER' AND "
2341
                    . "'''" . $user . "''@''" . $host . "''' LIKE `GRANTEE` LIMIT 1";
2342
            } elseif ($type === 'grant') {
2343
                list($user, $host) = $this->getCurrentUserAndHost();
2344
                $query = "SELECT 1 FROM ("
2345
                    . "SELECT `GRANTEE`, `IS_GRANTABLE` FROM "
2346
                    . "`INFORMATION_SCHEMA`.`COLUMN_PRIVILEGES` UNION "
2347
                    . "SELECT `GRANTEE`, `IS_GRANTABLE` FROM "
2348
                    . "`INFORMATION_SCHEMA`.`TABLE_PRIVILEGES` UNION "
2349
                    . "SELECT `GRANTEE`, `IS_GRANTABLE` FROM "
2350
                    . "`INFORMATION_SCHEMA`.`SCHEMA_PRIVILEGES` UNION "
2351
                    . "SELECT `GRANTEE`, `IS_GRANTABLE` FROM "
2352
                    . "`INFORMATION_SCHEMA`.`USER_PRIVILEGES`) t "
2353
                    . "WHERE `IS_GRANTABLE` = 'YES' AND "
2354
                    . "'''" . $user . "''@''" . $host . "''' LIKE `GRANTEE` LIMIT 1";
2355
            }
2356
2357
            $is = false;
2358
            $result = $this->tryQuery(
2359
                $query,
2360
                self::CONNECT_USER,
2361
                self::QUERY_STORE
2362
            );
2363
            if ($result) {
2364
                $is = (bool) $this->numRows($result);
2365
            }
2366
            $this->freeResult($result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::freeResult() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

2366
            $this->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
2367
        } else {
2368
            $is = false;
2369
            $grants = $this->fetchResult(
2370
                "SHOW GRANTS FOR CURRENT_USER();",
2371
                null,
2372
                null,
2373
                self::CONNECT_USER,
2374
                self::QUERY_STORE
2375
            );
2376
            if ($grants) {
2377
                foreach ($grants as $grant) {
2378
                    if ($type === 'create') {
2379
                        if (strpos($grant, "ALL PRIVILEGES ON *.*") !== false
0 ignored issues
show
Bug introduced by
It seems like $grant can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2379
                        if (strpos(/** @scrutinizer ignore-type */ $grant, "ALL PRIVILEGES ON *.*") !== false
Loading history...
2380
                            || strpos($grant, "CREATE USER") !== false
2381
                        ) {
2382
                            $is = true;
2383
                            break;
2384
                        }
2385
                    } elseif ($type === 'grant') {
2386
                        if (strpos($grant, "WITH GRANT OPTION") !== false) {
2387
                            $is = true;
2388
                            break;
2389
                        }
2390
                    }
2391
                }
2392
            }
2393
        }
2394
2395
        Util::cacheSet('is_' . $type . 'user', $is);
2396
        return $is;
2397
    }
2398
2399
    /**
2400
     * Get the current user and host
2401
     *
2402
     * @return array array of username and hostname
2403
     */
2404
    public function getCurrentUserAndHost(): array
2405
    {
2406
        if (count($this->_current_user) == 0) {
2407
            $user = $this->getCurrentUser();
2408
            $this->_current_user = explode("@", $user);
2409
        }
2410
        return $this->_current_user;
2411
    }
2412
2413
    /**
2414
     * Returns value for lower_case_table_names variable
2415
     *
2416
     * @return string|bool
2417
     */
2418
    public function getLowerCaseNames()
2419
    {
2420
        if (is_null($this->_lower_case_table_names)) {
2421
            $this->_lower_case_table_names = $this->fetchValue(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->fetchValue('SELEC...ower_case_table_names') can also be of type false. However, the property $_lower_case_table_names is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2422
                "SELECT @@lower_case_table_names"
2423
            );
2424
        }
2425
        return $this->_lower_case_table_names;
2426
    }
2427
2428
    /**
2429
     * Get the list of system schemas
2430
     *
2431
     * @return array list of system schemas
2432
     */
2433
    public function getSystemSchemas(): array
2434
    {
2435
        $schemas = [
2436
            'information_schema', 'performance_schema', 'mysql', 'sys'
2437
        ];
2438
        $systemSchemas = [];
2439
        foreach ($schemas as $schema) {
2440
            if ($this->isSystemSchema($schema, true)) {
2441
                $systemSchemas[] = $schema;
2442
            }
2443
        }
2444
        return $systemSchemas;
2445
    }
2446
2447
    /**
2448
     * Checks whether given schema is a system schema
2449
     *
2450
     * @param string $schema_name        Name of schema (database) to test
2451
     * @param bool   $testForMysqlSchema Whether 'mysql' schema should
2452
     *                                   be treated the same as IS and DD
2453
     *
2454
     * @return bool
2455
     */
2456
    public function isSystemSchema(
2457
        string $schema_name,
2458
        bool $testForMysqlSchema = false
2459
    ): bool {
2460
        $schema_name = strtolower($schema_name);
2461
        return $schema_name == 'information_schema'
2462
            || $schema_name == 'performance_schema'
2463
            || ($schema_name == 'mysql' && $testForMysqlSchema)
2464
            || $schema_name == 'sys';
2465
    }
2466
2467
    /**
2468
     * Return connection parameters for the database server
2469
     *
2470
     * @param integer    $mode   Connection mode on of CONNECT_USER, CONNECT_CONTROL
2471
     *                           or CONNECT_AUXILIARY.
2472
     * @param array|null $server Server information like host/port/socket/persistent
2473
     *
2474
     * @return array user, host and server settings array
2475
     */
2476
    public function getConnectionParams(int $mode, ?array $server = null): array
2477
    {
2478
        global $cfg;
2479
2480
        $user = null;
2481
        $password = null;
2482
2483
        if ($mode == DatabaseInterface::CONNECT_USER) {
2484
            $user = $cfg['Server']['user'];
2485
            $password = $cfg['Server']['password'];
2486
            $server = $cfg['Server'];
2487
        } elseif ($mode == DatabaseInterface::CONNECT_CONTROL) {
2488
            $user = $cfg['Server']['controluser'];
2489
            $password = $cfg['Server']['controlpass'];
2490
2491
            $server = [];
2492
2493
            if (! empty($cfg['Server']['controlhost'])) {
2494
                $server['host'] = $cfg['Server']['controlhost'];
2495
            } else {
2496
                $server['host'] = $cfg['Server']['host'];
2497
            }
2498
            // Share the settings if the host is same
2499
            if ($server['host'] == $cfg['Server']['host']) {
2500
                $shared = [
2501
                    'port', 'socket', 'compress',
2502
                    'ssl', 'ssl_key', 'ssl_cert', 'ssl_ca',
2503
                    'ssl_ca_path',  'ssl_ciphers', 'ssl_verify',
2504
                ];
2505
                foreach ($shared as $item) {
2506
                    if (isset($cfg['Server'][$item])) {
2507
                        $server[$item] = $cfg['Server'][$item];
2508
                    }
2509
                }
2510
            }
2511
            // Set configured port
2512
            if (! empty($cfg['Server']['controlport'])) {
2513
                $server['port'] = $cfg['Server']['controlport'];
2514
            }
2515
            // Set any configuration with control_ prefix
2516
            foreach ($cfg['Server'] as $key => $val) {
2517
                if (substr($key, 0, 8) === 'control_') {
2518
                    $server[substr($key, 8)] = $val;
2519
                }
2520
            }
2521
        } else {
2522
            if (is_null($server)) {
2523
                return [null, null, null];
2524
            }
2525
            if (isset($server['user'])) {
2526
                $user = $server['user'];
2527
            }
2528
            if (isset($server['password'])) {
2529
                $password = $server['password'];
2530
            }
2531
        }
2532
2533
        // Perform sanity checks on some variables
2534
        if (empty($server['port'])) {
2535
            $server['port'] = 0;
2536
        } else {
2537
            $server['port'] = intval($server['port']);
2538
        }
2539
        if (empty($server['socket'])) {
2540
            $server['socket'] = null;
2541
        }
2542
        if (empty($server['host'])) {
2543
            $server['host'] = 'localhost';
2544
        }
2545
        if (!isset($server['ssl'])) {
2546
            $server['ssl'] = false;
2547
        }
2548
        if (!isset($server['compress'])) {
2549
            $server['compress'] = false;
2550
        }
2551
2552
        return [$user, $password, $server];
2553
    }
2554
2555
    /**
2556
     * connects to the database server
2557
     *
2558
     * @param integer    $mode   Connection mode on of CONNECT_USER, CONNECT_CONTROL
2559
     *                           or CONNECT_AUXILIARY.
2560
     * @param array|null $server Server information like host/port/socket/persistent
2561
     * @param integer    $target How to store connection link, defaults to $mode
2562
     *
2563
     * @return mixed false on error or a connection object on success
2564
     */
2565
    public function connect(int $mode, ?array $server = null, ?int $target = null)
2566
    {
2567
        list($user, $password, $server) = $this->getConnectionParams($mode, $server);
2568
2569
        if (is_null($target)) {
2570
            $target = $mode;
2571
        }
2572
2573
        if (is_null($user) || is_null($password)) {
2574
            trigger_error(
2575
                __('Missing connection parameters!'),
2576
                E_USER_WARNING
2577
            );
2578
            return false;
2579
        }
2580
2581
        // Do not show location and backtrace for connection errors
2582
        $GLOBALS['error_handler']->setHideLocation(true);
2583
        $result = $this->_extension->connect(
2584
            $user,
2585
            $password,
2586
            $server
2587
        );
2588
        $GLOBALS['error_handler']->setHideLocation(false);
2589
2590
        if ($result) {
2591
            $this->_links[$target] = $result;
2592
            /* Run post connect for user connections */
2593
            if ($target == DatabaseInterface::CONNECT_USER) {
2594
                $this->postConnect();
2595
            } elseif ($target == DatabaseInterface::CONNECT_CONTROL) {
2596
                $this->postConnectControl();
2597
            }
2598
            return $result;
2599
        }
2600
2601
        if ($mode == DatabaseInterface::CONNECT_CONTROL) {
2602
            trigger_error(
2603
                __(
2604
                    'Connection for controluser as defined in your '
2605
                    . 'configuration failed.'
2606
                ),
2607
                E_USER_WARNING
2608
            );
2609
            return false;
2610
        } elseif ($mode == DatabaseInterface::CONNECT_AUXILIARY) {
2611
            // Do not go back to main login if connection failed
2612
            // (currently used only in unit testing)
2613
            return false;
2614
        }
2615
2616
        return $result;
2617
    }
2618
2619
    /**
2620
     * selects given database
2621
     *
2622
     * @param string  $dbname database name to select
2623
     * @param integer $link   link type
2624
     *
2625
     * @return boolean
2626
     */
2627
    public function selectDb(string $dbname, $link = DatabaseInterface::CONNECT_USER): bool
2628
    {
2629
        if (! isset($this->_links[$link])) {
2630
            return false;
2631
        }
2632
        return $this->_extension->selectDb($dbname, $this->_links[$link]);
2633
    }
2634
2635
    /**
2636
     * returns array of rows with associative and numeric keys from $result
2637
     *
2638
     * @param object $result result set identifier
2639
     *
2640
     * @return array
2641
     */
2642
    public function fetchArray($result)
2643
    {
2644
        return $this->_extension->fetchArray($result);
2645
    }
2646
2647
    /**
2648
     * returns array of rows with associative keys from $result
2649
     *
2650
     * @param object $result result set identifier
2651
     *
2652
     * @return array|bool
2653
     */
2654
    public function fetchAssoc($result)
2655
    {
2656
        return $this->_extension->fetchAssoc($result);
2657
    }
2658
2659
    /**
2660
     * returns array of rows with numeric keys from $result
2661
     *
2662
     * @param object $result result set identifier
2663
     *
2664
     * @return array|bool
2665
     */
2666
    public function fetchRow($result)
2667
    {
2668
        return $this->_extension->fetchRow($result);
2669
    }
2670
2671
    /**
2672
     * Adjusts the result pointer to an arbitrary row in the result
2673
     *
2674
     * @param object  $result database result
2675
     * @param integer $offset offset to seek
2676
     *
2677
     * @return bool true on success, false on failure
2678
     */
2679
    public function dataSeek($result, int $offset): bool
2680
    {
2681
        return $this->_extension->dataSeek($result, $offset);
2682
    }
2683
2684
    /**
2685
     * Frees memory associated with the result
2686
     *
2687
     * @param object $result database result
2688
     *
2689
     * @return void
2690
     */
2691
    public function freeResult($result): void
2692
    {
2693
        $this->_extension->freeResult($result);
2694
    }
2695
2696
    /**
2697
     * Check if there are any more query results from a multi query
2698
     *
2699
     * @param integer $link link type
2700
     *
2701
     * @return bool true or false
2702
     */
2703
    public function moreResults($link = DatabaseInterface::CONNECT_USER): bool
2704
    {
2705
        if (! isset($this->_links[$link])) {
2706
            return false;
2707
        }
2708
        return $this->_extension->moreResults($this->_links[$link]);
2709
    }
2710
2711
    /**
2712
     * Prepare next result from multi_query
2713
     *
2714
     * @param integer $link link type
2715
     *
2716
     * @return bool true or false
2717
     */
2718
    public function nextResult($link = DatabaseInterface::CONNECT_USER): bool
2719
    {
2720
        if (! isset($this->_links[$link])) {
2721
            return false;
2722
        }
2723
        return $this->_extension->nextResult($this->_links[$link]);
2724
    }
2725
2726
    /**
2727
     * Store the result returned from multi query
2728
     *
2729
     * @param integer $link link type
2730
     *
2731
     * @return mixed false when empty results / result set when not empty
2732
     */
2733
    public function storeResult($link = DatabaseInterface::CONNECT_USER)
2734
    {
2735
        if (! isset($this->_links[$link])) {
2736
            return false;
2737
        }
2738
        return $this->_extension->storeResult($this->_links[$link]);
2739
    }
2740
2741
    /**
2742
     * Returns a string representing the type of connection used
2743
     *
2744
     * @param integer $link link type
2745
     *
2746
     * @return string|bool type of connection used
2747
     */
2748
    public function getHostInfo($link = DatabaseInterface::CONNECT_USER)
2749
    {
2750
        if (! isset($this->_links[$link])) {
2751
            return false;
2752
        }
2753
        return $this->_extension->getHostInfo($this->_links[$link]);
2754
    }
2755
2756
    /**
2757
     * Returns the version of the MySQL protocol used
2758
     *
2759
     * @param integer $link link type
2760
     *
2761
     * @return int|bool version of the MySQL protocol used
2762
     */
2763
    public function getProtoInfo($link = DatabaseInterface::CONNECT_USER)
2764
    {
2765
        if (! isset($this->_links[$link])) {
2766
            return false;
2767
        }
2768
        return $this->_extension->getProtoInfo($this->_links[$link]);
2769
    }
2770
2771
    /**
2772
     * returns a string that represents the client library version
2773
     *
2774
     * @return string MySQL client library version
2775
     */
2776
    public function getClientInfo(): string
2777
    {
2778
        return $this->_extension->getClientInfo();
2779
    }
2780
2781
    /**
2782
     * returns last error message or false if no errors occurred
2783
     *
2784
     * @param integer $link link type
2785
     *
2786
     * @return string|bool error or false
2787
     */
2788
    public function getError($link = DatabaseInterface::CONNECT_USER)
2789
    {
2790
        if (! isset($this->_links[$link])) {
2791
            return false;
2792
        }
2793
        return $this->_extension->getError($this->_links[$link]);
2794
    }
2795
2796
    /**
2797
     * returns the number of rows returned by last query
2798
     *
2799
     * @param object $result result set identifier
2800
     *
2801
     * @return string|int
2802
     */
2803
    public function numRows($result)
2804
    {
2805
        return $this->_extension->numRows($result);
2806
    }
2807
2808
    /**
2809
     * returns last inserted auto_increment id for given $link
2810
     * or $GLOBALS['userlink']
2811
     *
2812
     * @param integer $link link type
2813
     *
2814
     * @return int|boolean
2815
     */
2816
    public function insertId($link = DatabaseInterface::CONNECT_USER)
2817
    {
2818
        // If the primary key is BIGINT we get an incorrect result
2819
        // (sometimes negative, sometimes positive)
2820
        // and in the present function we don't know if the PK is BIGINT
2821
        // so better play safe and use LAST_INSERT_ID()
2822
        //
2823
        // When no controluser is defined, using mysqli_insert_id($link)
2824
        // does not always return the last insert id due to a mixup with
2825
        // the tracking mechanism, but this works:
2826
        return $this->fetchValue('SELECT LAST_INSERT_ID();', 0, 0, $link);
2827
    }
2828
2829
    /**
2830
     * returns the number of rows affected by last query
2831
     *
2832
     * @param integer $link           link type
2833
     * @param bool    $get_from_cache whether to retrieve from cache
2834
     *
2835
     * @return int|boolean
2836
     */
2837
    public function affectedRows(
2838
        $link = DatabaseInterface::CONNECT_USER,
2839
        bool $get_from_cache = true
2840
    ) {
2841
        if (! isset($this->_links[$link])) {
2842
            return false;
2843
        }
2844
2845
        if ($get_from_cache) {
2846
            return $GLOBALS['cached_affected_rows'];
2847
        }
2848
2849
        return $this->_extension->affectedRows($this->_links[$link]);
2850
    }
2851
2852
    /**
2853
     * returns metainfo for fields in $result
2854
     *
2855
     * @param object $result result set identifier
2856
     *
2857
     * @return mixed meta info for fields in $result
2858
     */
2859
    public function getFieldsMeta($result)
2860
    {
2861
        $result = $this->_extension->getFieldsMeta($result);
2862
2863
        if ($this->getLowerCaseNames() === '2') {
2864
            /**
2865
             * Fixup orgtable for lower_case_table_names = 2
2866
             *
2867
             * In this setup MySQL server reports table name lower case
2868
             * but we still need to operate on original case to properly
2869
             * match existing strings
2870
             */
2871
            foreach ($result as $value) {
2872
                if (strlen($value->orgtable) !== 0 &&
2873
                        mb_strtolower($value->orgtable) === mb_strtolower($value->table)) {
2874
                    $value->orgtable = $value->table;
2875
                }
2876
            }
2877
        }
2878
2879
        return $result;
2880
    }
2881
2882
    /**
2883
     * return number of fields in given $result
2884
     *
2885
     * @param object $result result set identifier
2886
     *
2887
     * @return int field count
2888
     */
2889
    public function numFields($result): int
2890
    {
2891
        return $this->_extension->numFields($result);
2892
    }
2893
2894
    /**
2895
     * returns the length of the given field $i in $result
2896
     *
2897
     * @param object $result result set identifier
2898
     * @param int    $i      field
2899
     *
2900
     * @return int length of field
2901
     */
2902
    public function fieldLen($result, int $i): int
2903
    {
2904
        return $this->_extension->fieldLen($result, $i);
2905
    }
2906
2907
    /**
2908
     * returns name of $i. field in $result
2909
     *
2910
     * @param object $result result set identifier
2911
     * @param int    $i      field
2912
     *
2913
     * @return string name of $i. field in $result
2914
     */
2915
    public function fieldName($result, int $i): string
2916
    {
2917
        return $this->_extension->fieldName($result, $i);
2918
    }
2919
2920
    /**
2921
     * returns concatenated string of human readable field flags
2922
     *
2923
     * @param object $result result set identifier
2924
     * @param int    $i      field
2925
     *
2926
     * @return string field flags
2927
     */
2928
    public function fieldFlags($result, $i): string
2929
    {
2930
        return $this->_extension->fieldFlags($result, $i);
2931
    }
2932
2933
    /**
2934
     * returns properly escaped string for use in MySQL queries
2935
     *
2936
     * @param string $str  string to be escaped
2937
     * @param mixed  $link optional database link to use
2938
     *
2939
     * @return string a MySQL escaped string
2940
     */
2941
    public function escapeString(string $str, $link = DatabaseInterface::CONNECT_USER)
2942
    {
2943
        if ($this->_extension === null || !isset($this->_links[$link])) {
2944
            return $str;
2945
        }
2946
2947
        return $this->_extension->escapeString($this->_links[$link], $str);
2948
    }
2949
2950
    /**
2951
     * Checks if this database server is running on Amazon RDS.
2952
     *
2953
     * @return boolean
2954
     */
2955
    public function isAmazonRds(): bool
2956
    {
2957
        if (Util::cacheExists('is_amazon_rds')) {
2958
            return Util::cacheGet('is_amazon_rds');
0 ignored issues
show
Bug Best Practice introduced by
The expression return PhpMyAdmin\Util::cacheGet('is_amazon_rds') could return the type null which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
2959
        }
2960
        $sql = 'SELECT @@basedir';
2961
        $result = $this->fetchValue($sql);
2962
        $rds = (substr($result, 0, 10) == '/rdsdbbin/');
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type false; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2962
        $rds = (substr(/** @scrutinizer ignore-type */ $result, 0, 10) == '/rdsdbbin/');
Loading history...
2963
        Util::cacheSet('is_amazon_rds', $rds);
2964
2965
        return $rds;
2966
    }
2967
2968
    /**
2969
     * Gets SQL for killing a process.
2970
     *
2971
     * @param int $process Process ID
2972
     *
2973
     * @return string
2974
     */
2975
    public function getKillQuery(int $process): string
2976
    {
2977
        if ($this->isAmazonRds()) {
2978
            return 'CALL mysql.rds_kill(' . $process . ');';
2979
        }
2980
2981
        return 'KILL ' . $process . ';';
2982
    }
2983
2984
    /**
2985
     * Get the phpmyadmin database manager
2986
     *
2987
     * @return SystemDatabase
2988
     */
2989
    public function getSystemDatabase(): SystemDatabase
2990
    {
2991
        return new SystemDatabase($this);
2992
    }
2993
2994
    /**
2995
     * Get a table with database name and table name
2996
     *
2997
     * @param string $db_name    DB name
2998
     * @param string $table_name Table name
2999
     *
3000
     * @return Table
3001
     */
3002
    public function getTable(string $db_name, string $table_name): Table
3003
    {
3004
        return new Table($table_name, $db_name, $this);
3005
    }
3006
3007
    /**
3008
     * returns collation of given db
3009
     *
3010
     * @param string $db name of db
3011
     *
3012
     * @return string  collation of $db
3013
     */
3014
    public function getDbCollation(string $db): string
3015
    {
3016
        if ($this->isSystemSchema($db)) {
3017
            // We don't have to check the collation of the virtual
3018
            // information_schema database: We know it!
3019
            return 'utf8_general_ci';
3020
        }
3021
3022
        if (! $GLOBALS['cfg']['Server']['DisableIS']) {
3023
            // this is slow with thousands of databases
3024
            $sql = 'SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA'
3025
                . ' WHERE SCHEMA_NAME = \'' . $this->escapeString($db)
3026
                . '\' LIMIT 1';
3027
            return $this->fetchValue($sql);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->fetchValue($sql) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
3028
        }
3029
3030
        $this->selectDb($db);
3031
        $return = $this->fetchValue('SELECT @@collation_database');
3032
        if ($db !== $GLOBALS['db']) {
3033
            $this->selectDb($GLOBALS['db']);
3034
        }
3035
        return $return;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $return could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
3036
    }
3037
3038
    /**
3039
     * returns default server collation from show variables
3040
     *
3041
     * @return string
3042
     */
3043
    public function getServerCollation(): string
3044
    {
3045
        return $this->fetchValue('SELECT @@collation_server');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->fetchValue...CT @@collation_server') could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
3046
    }
3047
3048
    /**
3049
     * Server version as number
3050
     *
3051
     * @return integer
3052
     */
3053
    public function getVersion(): int
3054
    {
3055
        return $this->_version_int;
3056
    }
3057
3058
    /**
3059
     * Server version
3060
     *
3061
     * @return string
3062
     */
3063
    public function getVersionString(): string
3064
    {
3065
        return $this->_version_str;
3066
    }
3067
3068
    /**
3069
     * Server version comment
3070
     *
3071
     * @return string
3072
     */
3073
    public function getVersionComment(): string
3074
    {
3075
        return $this->_version_comment;
3076
    }
3077
3078
    /**
3079
     * Whether connection is MariaDB
3080
     *
3081
     * @return boolean
3082
     */
3083
    public function isMariaDB(): bool
3084
    {
3085
        return $this->_is_mariadb;
3086
    }
3087
3088
    /**
3089
     * Whether connection is Percona
3090
     *
3091
     * @return boolean
3092
     */
3093
    public function isPercona(): bool
3094
    {
3095
        return $this->_is_percona;
3096
    }
3097
3098
    /**
3099
     * Load correct database driver
3100
     *
3101
     * @return void
3102
     */
3103
    public static function load(): void
3104
    {
3105
        if (defined('TESTSUITE')) {
3106
            /**
3107
             * For testsuite we use dummy driver which can fake some queries.
3108
             */
3109
            $extension = new DbiDummy();
3110
        } else {
3111
            if (! self::checkDbExtension('mysqli')) {
3112
                $docurl = Util::getDocuLink('faq', 'faqmysql');
3113
                $doclink = sprintf(
3114
                    __('See %sour documentation%s for more information.'),
3115
                    '[a@' . $docurl . '@documentation]',
3116
                    '[/a]'
3117
                );
3118
                Core::warnMissingExtension(
3119
                    'mysqli',
3120
                    true,
3121
                    $doclink
3122
                );
3123
            }
3124
            $extension = new DbiMysqli();
3125
        }
3126
        $GLOBALS['dbi'] = new DatabaseInterface($extension);
3127
3128
        $container = Container::getDefaultContainer();
3129
        $container->set('PMA_DatabaseInterface', $GLOBALS['dbi']);
3130
        $container->alias('dbi', 'PMA_DatabaseInterface');
3131
    }
3132
}
3133