Passed
Push — master ( 233021...9b36c0 )
by Maurício
08:43
created

libraries/classes/DatabaseInterface.php (1 issue)

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
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);
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) . ';'
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) {
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;
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) {
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) {
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) {
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) {
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'];
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'])
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');
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');
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);
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
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(
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');
2987
        }
2988
        $sql = 'SELECT @@basedir';
2989
        $result = $this->fetchValue($sql);
2990
        $rds = (substr($result, 0, 10) == '/rdsdbbin/');
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);
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');
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