Completed
Push — master ( d7bc90...5e5d0a )
by Maurício
01:36 queued 22s
created

DatabaseInterface::prepare()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

2201
                    . /** @scrutinizer ignore-type */ $one_result['full_trigger_name'];
Loading history...
2202
                $one_result['create'] = 'CREATE TRIGGER '
2203
                    . $one_result['full_trigger_name'] . ' '
2204
                    . $trigger['ACTION_TIMING'] . ' '
2205
                    . $trigger['EVENT_MANIPULATION']
2206
                    . ' 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

2206
                    . ' ON ' . /** @scrutinizer ignore-type */ Util::backquote($trigger['EVENT_OBJECT_TABLE'])
Loading history...
2207
                    . "\n" . ' FOR EACH ROW '
2208
                    . $trigger['ACTION_STATEMENT'] . "\n" . $delimiter . "\n";
2209
2210
                $result[] = $one_result;
2211
            }
2212
        }
2213
2214
        // Sort results by name
2215
        $name = [];
2216
        foreach ($result as $value) {
2217
            $name[] = $value['name'];
2218
        }
2219
        array_multisort($name, SORT_ASC, $result);
2220
2221
        return $result;
2222
    }
2223
2224
    /**
2225
     * Formats database error message in a friendly way.
2226
     * This is needed because some errors messages cannot
2227
     * be obtained by mysql_error().
2228
     *
2229
     * @param int    $error_number  Error code
2230
     * @param string $error_message Error message as returned by server
2231
     *
2232
     * @return string HML text with error details
2233
     */
2234
    public static function formatError(int $error_number, string $error_message): string
2235
    {
2236
        $error_message = htmlspecialchars($error_message);
2237
2238
        $error = '#' . ((string) $error_number);
2239
        $separator = ' &mdash; ';
2240
2241
        if ($error_number == 2002) {
2242
            $error .= ' - ' . $error_message;
2243
            $error .= $separator;
2244
            $error .= __(
2245
                'The server is not responding (or the local server\'s socket'
2246
                . ' is not correctly configured).'
2247
            );
2248
        } elseif ($error_number == 2003) {
2249
            $error .= ' - ' . $error_message;
2250
            $error .= $separator . __('The server is not responding.');
2251
        } elseif ($error_number == 1698) {
2252
            $error .= ' - ' . $error_message;
2253
            $error .= $separator . '<a href="' . Url::getFromRoute('/logout') . '" class="disableAjax">';
2254
            $error .= __('Logout and try as another user.') . '</a>';
2255
        } elseif ($error_number == 1005) {
2256
            if (strpos($error_message, 'errno: 13') !== false) {
2257
                $error .= ' - ' . $error_message;
2258
                $error .= $separator
2259
                    . __(
2260
                        'Please check privileges of directory containing database.'
2261
                    );
2262
            } else {
2263
                /**
2264
                 * InnoDB constraints, see
2265
                 * https://dev.mysql.com/doc/refman/5.0/en/
2266
                 * innodb-foreign-key-constraints.html
2267
                 */
2268
                $error .= ' - ' . $error_message .
2269
                    ' (<a href="' .
2270
                    Url::getFromRoute('/server/engines', [
2271
                        'engine' => 'InnoDB',
2272
                        'page' => 'Status',
2273
                    ]) .
2274
                    '">' . __('Details…') . '</a>)';
2275
            }
2276
        } else {
2277
            $error .= ' - ' . $error_message;
2278
        }
2279
2280
        return $error;
2281
    }
2282
2283
    /**
2284
     * gets the current user with host
2285
     *
2286
     * @return string the current user i.e. user@host
2287
     */
2288
    public function getCurrentUser(): string
2289
    {
2290
        if (Util::cacheExists('mysql_cur_user')) {
2291
            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...
2292
        }
2293
        $user = $this->fetchValue('SELECT CURRENT_USER();');
2294
        if ($user !== false) {
2295
            Util::cacheSet('mysql_cur_user', $user);
2296
            return $user;
2297
        }
2298
        return '@';
2299
    }
2300
2301
    /**
2302
     * Checks if current user is superuser
2303
     *
2304
     * @return bool Whether user is a superuser
2305
     */
2306
    public function isSuperuser(): bool
2307
    {
2308
        return $this->isUserType('super');
2309
    }
2310
2311
    /**
2312
     * Checks if current user has global create user/grant privilege
2313
     * or is a superuser (i.e. SELECT on mysql.users)
2314
     * while caching the result in session.
2315
     *
2316
     * @param string $type type of user to check for
2317
     *                     i.e. 'create', 'grant', 'super'
2318
     *
2319
     * @return bool Whether user is a given type of user
2320
     */
2321
    public function isUserType(string $type): bool
2322
    {
2323
        if (Util::cacheExists('is_' . $type . 'user')) {
2324
            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...
2325
        }
2326
2327
        // when connection failed we don't have a $userlink
2328
        if (! isset($this->_links[DatabaseInterface::CONNECT_USER])) {
2329
            return false;
2330
        }
2331
2332
        // checking if user is logged in
2333
        if ($type === 'logged') {
2334
            return true;
2335
        }
2336
2337
        if (! $GLOBALS['cfg']['Server']['DisableIS'] || $type === 'super') {
2338
            // Prepare query for each user type check
2339
            $query = '';
2340
            if ($type === 'super') {
2341
                $query = 'SELECT 1 FROM mysql.user LIMIT 1';
2342
            } elseif ($type === 'create') {
2343
                list($user, $host) = $this->getCurrentUserAndHost();
2344
                $query = "SELECT 1 FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES` "
2345
                    . "WHERE `PRIVILEGE_TYPE` = 'CREATE USER' AND "
2346
                    . "'''" . $user . "''@''" . $host . "''' LIKE `GRANTEE` LIMIT 1";
2347
            } elseif ($type === 'grant') {
2348
                list($user, $host) = $this->getCurrentUserAndHost();
2349
                $query = "SELECT 1 FROM ("
2350
                    . "SELECT `GRANTEE`, `IS_GRANTABLE` FROM "
2351
                    . "`INFORMATION_SCHEMA`.`COLUMN_PRIVILEGES` UNION "
2352
                    . "SELECT `GRANTEE`, `IS_GRANTABLE` FROM "
2353
                    . "`INFORMATION_SCHEMA`.`TABLE_PRIVILEGES` UNION "
2354
                    . "SELECT `GRANTEE`, `IS_GRANTABLE` FROM "
2355
                    . "`INFORMATION_SCHEMA`.`SCHEMA_PRIVILEGES` UNION "
2356
                    . "SELECT `GRANTEE`, `IS_GRANTABLE` FROM "
2357
                    . "`INFORMATION_SCHEMA`.`USER_PRIVILEGES`) t "
2358
                    . "WHERE `IS_GRANTABLE` = 'YES' AND "
2359
                    . "'''" . $user . "''@''" . $host . "''' LIKE `GRANTEE` LIMIT 1";
2360
            }
2361
2362
            $is = false;
2363
            $result = $this->tryQuery(
2364
                $query,
2365
                self::CONNECT_USER,
2366
                self::QUERY_STORE
2367
            );
2368
            if ($result) {
2369
                $is = (bool) $this->numRows($result);
2370
            }
2371
            $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

2371
            $this->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
2372
        } else {
2373
            $is = false;
2374
            $grants = $this->fetchResult(
2375
                "SHOW GRANTS FOR CURRENT_USER();",
2376
                null,
2377
                null,
2378
                self::CONNECT_USER,
2379
                self::QUERY_STORE
2380
            );
2381
            if ($grants) {
2382
                foreach ($grants as $grant) {
2383
                    if ($type === 'create') {
2384
                        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

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

2990
        $rds = (substr(/** @scrutinizer ignore-type */ $result, 0, 10) == '/rdsdbbin/');
Loading history...
2991
        Util::cacheSet('is_amazon_rds', $rds);
2992
2993
        return $rds;
2994
    }
2995
2996
    /**
2997
     * Gets SQL for killing a process.
2998
     *
2999
     * @param int $process Process ID
3000
     *
3001
     * @return string
3002
     */
3003
    public function getKillQuery(int $process): string
3004
    {
3005
        if ($this->isAmazonRds()) {
3006
            return 'CALL mysql.rds_kill(' . $process . ');';
3007
        }
3008
3009
        return 'KILL ' . $process . ';';
3010
    }
3011
3012
    /**
3013
     * Get the phpmyadmin database manager
3014
     *
3015
     * @return SystemDatabase
3016
     */
3017
    public function getSystemDatabase(): SystemDatabase
3018
    {
3019
        return new SystemDatabase($this);
3020
    }
3021
3022
    /**
3023
     * Get a table with database name and table name
3024
     *
3025
     * @param string $db_name    DB name
3026
     * @param string $table_name Table name
3027
     *
3028
     * @return Table
3029
     */
3030
    public function getTable(string $db_name, string $table_name): Table
3031
    {
3032
        return new Table($table_name, $db_name, $this);
3033
    }
3034
3035
    /**
3036
     * returns collation of given db
3037
     *
3038
     * @param string $db name of db
3039
     *
3040
     * @return string  collation of $db
3041
     */
3042
    public function getDbCollation(string $db): string
3043
    {
3044
        if ($this->isSystemSchema($db)) {
3045
            // We don't have to check the collation of the virtual
3046
            // information_schema database: We know it!
3047
            return 'utf8_general_ci';
3048
        }
3049
3050
        if (! $GLOBALS['cfg']['Server']['DisableIS']) {
3051
            // this is slow with thousands of databases
3052
            $sql = 'SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA'
3053
                . ' WHERE SCHEMA_NAME = \'' . $this->escapeString($db)
3054
                . '\' LIMIT 1';
3055
            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...
3056
        }
3057
3058
        $this->selectDb($db);
3059
        $return = $this->fetchValue('SELECT @@collation_database');
3060
        if ($db !== $GLOBALS['db']) {
3061
            $this->selectDb($GLOBALS['db']);
3062
        }
3063
        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...
3064
    }
3065
3066
    /**
3067
     * returns default server collation from show variables
3068
     *
3069
     * @return string
3070
     */
3071
    public function getServerCollation(): string
3072
    {
3073
        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...
3074
    }
3075
3076
    /**
3077
     * Server version as number
3078
     *
3079
     * @return integer
3080
     */
3081
    public function getVersion(): int
3082
    {
3083
        return $this->_version_int;
3084
    }
3085
3086
    /**
3087
     * Server version
3088
     *
3089
     * @return string
3090
     */
3091
    public function getVersionString(): string
3092
    {
3093
        return $this->_version_str;
3094
    }
3095
3096
    /**
3097
     * Server version comment
3098
     *
3099
     * @return string
3100
     */
3101
    public function getVersionComment(): string
3102
    {
3103
        return $this->_version_comment;
3104
    }
3105
3106
    /**
3107
     * Whether connection is MariaDB
3108
     *
3109
     * @return boolean
3110
     */
3111
    public function isMariaDB(): bool
3112
    {
3113
        return $this->_is_mariadb;
3114
    }
3115
3116
    /**
3117
     * Whether connection is Percona
3118
     *
3119
     * @return boolean
3120
     */
3121
    public function isPercona(): bool
3122
    {
3123
        return $this->_is_percona;
3124
    }
3125
3126
    /**
3127
     * Load correct database driver
3128
     *
3129
     * @param DbiExtension|null $extension Force the use of an alternative extension
3130
     *
3131
     * @return self
3132
     */
3133
    public static function load(?DbiExtension $extension = null): self
3134
    {
3135
        global $dbi;
3136
3137
        if ($extension !== null) {
3138
            $dbi = new self($extension);
3139
            return $dbi;
3140
        }
3141
3142
        if (! self::checkDbExtension('mysqli')) {
3143
            $docUrl = Util::getDocuLink('faq', 'faqmysql');
3144
            $docLink = sprintf(
3145
                __('See %sour documentation%s for more information.'),
3146
                '[a@' . $docUrl . '@documentation]',
3147
                '[/a]'
3148
            );
3149
            Core::warnMissingExtension(
3150
                'mysqli',
3151
                true,
3152
                $docLink
3153
            );
3154
        }
3155
3156
        $dbi = new self(new DbiMysqli());
3157
        return $dbi;
3158
    }
3159
3160
    /**
3161
     * Prepare an SQL statement for execution.
3162
     *
3163
     * @param string $query The query, as a string.
3164
     * @param int    $link  Link type.
3165
     *
3166
     * @return object|false A statement object or false.
3167
     */
3168
    public function prepare(string $query, $link = DatabaseInterface::CONNECT_USER)
3169
    {
3170
        return $this->_extension->prepare($this->_links[$link], $query);
3171
    }
3172
}
3173