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

Privileges::getHtmlForSpecificDbPrivileges()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 32
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 22
c 0
b 0
f 0
dl 0
loc 32
rs 9.568
cc 4
nc 4
nop 1
1
<?php
2
/**
3
 * set of functions with the Privileges section in pma
4
 *
5
 * @package PhpMyAdmin
6
 */
7
declare(strict_types=1);
8
9
namespace PhpMyAdmin\Server;
10
11
use PhpMyAdmin\Core;
12
use PhpMyAdmin\DatabaseInterface;
13
use PhpMyAdmin\Display\ChangePassword;
14
use PhpMyAdmin\Message;
15
use PhpMyAdmin\Relation;
16
use PhpMyAdmin\RelationCleanup;
17
use PhpMyAdmin\Response;
18
use PhpMyAdmin\Template;
19
use PhpMyAdmin\Url;
20
use PhpMyAdmin\Util;
21
22
/**
23
 * Privileges class
24
 *
25
 * @package PhpMyAdmin
26
 */
27
class Privileges
28
{
29
    /**
30
     * @var Template
31
     */
32
    public $template;
33
34
    /**
35
     * @var RelationCleanup
36
     */
37
    private $relationCleanup;
38
39
    /**
40
     * @var DatabaseInterface
41
     */
42
    public $dbi;
43
44
    /**
45
     * @var Relation
46
     */
47
    public $relation;
48
49
    /**
50
     * Privileges constructor.
51
     *
52
     * @param Template          $template        Template object
53
     * @param DatabaseInterface $dbi             DatabaseInterface object
54
     * @param Relation          $relation        Relation object
55
     * @param RelationCleanup   $relationCleanup RelationCleanup object
56
     */
57
    public function __construct(
58
        Template $template,
59
        $dbi,
60
        Relation $relation,
61
        RelationCleanup $relationCleanup
62
    ) {
63
        $this->template = $template;
64
        $this->dbi = $dbi;
65
        $this->relation = $relation;
66
        $this->relationCleanup = $relationCleanup;
67
    }
68
69
    /**
70
     * Get Html for User Group Dialog
71
     *
72
     * @param string $username     username
73
     * @param bool   $is_menuswork Is menuswork set in configuration
74
     *
75
     * @return string html
76
     */
77
    public function getHtmlForUserGroupDialog($username, $is_menuswork)
78
    {
79
        $html = '';
80
        if (! empty($_GET['edit_user_group_dialog']) && $is_menuswork) {
81
            $dialog = $this->getHtmlToChooseUserGroup($username);
82
            $response = Response::getInstance();
83
            if ($response->isAjax()) {
84
                $response->addJSON('message', $dialog);
85
                exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
86
            } else {
87
                $html .= $dialog;
88
            }
89
        }
90
91
        return $html;
92
    }
93
94
    /**
95
     * Escapes wildcard in a database+table specification
96
     * before using it in a GRANT statement.
97
     *
98
     * Escaping a wildcard character in a GRANT is only accepted at the global
99
     * or database level, not at table level; this is why I remove
100
     * the escaping character. Internally, in mysql.tables_priv.Db there are
101
     * no escaping (for example test_db) but in mysql.db you'll see test\_db
102
     * for a db-specific privilege.
103
     *
104
     * @param string $dbname    Database name
105
     * @param string $tablename Table name
106
     *
107
     * @return string the escaped (if necessary) database.table
108
     */
109
    public function wildcardEscapeForGrant($dbname, $tablename)
110
    {
111
        if (strlen($dbname) === 0) {
112
            $db_and_table = '*.*';
113
        } else {
114
            if (strlen($tablename) > 0) {
115
                $db_and_table = Util::backquote(
116
                    Util::unescapeMysqlWildcards($dbname)
117
                )
118
                . '.' . Util::backquote($tablename);
119
            } else {
120
                $db_and_table = Util::backquote($dbname) . '.*';
121
            }
122
        }
123
        return $db_and_table;
124
    }
125
126
    /**
127
     * Generates a condition on the user name
128
     *
129
     * @param string $initial the user's initial
130
     *
131
     * @return string   the generated condition
132
     */
133
    public function rangeOfUsers($initial = '')
134
    {
135
        // strtolower() is used because the User field
136
        // might be BINARY, so LIKE would be case sensitive
137
        if ($initial === null || $initial === '') {
138
            return '';
139
        }
140
141
        $ret = " WHERE `User` LIKE '"
142
            . $this->dbi->escapeString($initial) . "%'"
143
            . " OR `User` LIKE '"
144
            . $this->dbi->escapeString(mb_strtolower($initial))
145
            . "%'";
146
        return $ret;
147
    }
148
149
    /**
150
     * Parses privileges into an array, it modifies the array
151
     *
152
     * @param array $row Results row from
153
     *
154
     * @return void
155
     */
156
    public function fillInTablePrivileges(array &$row)
157
    {
158
        $row1 = $this->dbi->fetchSingleRow(
159
            'SHOW COLUMNS FROM `mysql`.`tables_priv` LIKE \'Table_priv\';',
160
            'ASSOC'
161
        );
162
        // note: in MySQL 5.0.3 we get "Create View', 'Show view';
163
        // the View for Create is spelled with uppercase V
164
        // the view for Show is spelled with lowercase v
165
        // and there is a space between the words
166
167
        $av_grants = explode(
168
            '\',\'',
169
            mb_substr(
170
                $row1['Type'],
171
                mb_strpos($row1['Type'], '(') + 2,
172
                mb_strpos($row1['Type'], ')')
173
                - mb_strpos($row1['Type'], '(') - 3
174
            )
175
        );
176
177
        $users_grants = explode(',', $row['Table_priv']);
178
179
        foreach ($av_grants as $current_grant) {
180
            $row[$current_grant . '_priv']
181
                = in_array($current_grant, $users_grants) ? 'Y' : 'N';
182
        }
183
        unset($row['Table_priv']);
184
    }
185
186
187
    /**
188
     * Extracts the privilege information of a priv table row
189
     *
190
     * @param array|null $row        the row
191
     * @param boolean    $enableHTML add <dfn> tag with tooltips
192
     * @param boolean    $tablePrivs whether row contains table privileges
193
     *
194
     * @global  resource $user_link the database connection
195
     *
196
     * @return array
197
     */
198
    public function extractPrivInfo($row = null, $enableHTML = false, $tablePrivs = false)
199
    {
200
        if ($tablePrivs) {
201
            $grants = $this->getTableGrantsArray();
202
        } else {
203
            $grants = $this->getGrantsArray();
204
        }
205
206
        if ($row !== null && isset($row['Table_priv'])) {
207
            $this->fillInTablePrivileges($row);
208
        }
209
210
        $privs = [];
211
        $allPrivileges = true;
212
        foreach ($grants as $current_grant) {
213
            if (($row !== null && isset($row[$current_grant[0]]))
214
                || ($row === null && isset($GLOBALS[$current_grant[0]]))
215
            ) {
216
                if (($row !== null && $row[$current_grant[0]] == 'Y')
217
                    || ($row === null
218
                    && ($GLOBALS[$current_grant[0]] == 'Y'
219
                    || (is_array($GLOBALS[$current_grant[0]])
220
                    && count($GLOBALS[$current_grant[0]]) == $_REQUEST['column_count']
221
                    && empty($GLOBALS[$current_grant[0] . '_none']))))
222
                ) {
223
                    if ($enableHTML) {
224
                        $privs[] = '<dfn title="' . $current_grant[2] . '">'
225
                        . $current_grant[1] . '</dfn>';
226
                    } else {
227
                        $privs[] = $current_grant[1];
228
                    }
229
                } elseif (! empty($GLOBALS[$current_grant[0]])
230
                    && is_array($GLOBALS[$current_grant[0]])
231
                    && empty($GLOBALS[$current_grant[0] . '_none'])
232
                ) {
233
                    // Required for proper escaping of ` (backtick) in a column name
234
                    $grant_cols = array_map(
235
                        function ($val) {
236
                            return Util::backquote($val);
237
                        },
238
                        $GLOBALS[$current_grant[0]]
239
                    );
240
241
                    if ($enableHTML) {
242
                        $privs[] = '<dfn title="' . $current_grant[2] . '">'
243
                            . $current_grant[1] . '</dfn>'
244
                            . ' (' . implode(', ', $grant_cols) . ')';
245
                    } else {
246
                        $privs[] = $current_grant[1]
247
                            . ' (' . implode(', ', $grant_cols) . ')';
248
                    }
249
                } else {
250
                    $allPrivileges = false;
251
                }
252
            }
253
        }
254
        if (empty($privs)) {
255
            if ($enableHTML) {
256
                $privs[] = '<dfn title="' . __('No privileges.') . '">USAGE</dfn>';
257
            } else {
258
                $privs[] = 'USAGE';
259
            }
260
        } elseif ($allPrivileges
261
            && (! isset($_POST['grant_count']) || count($privs) == $_POST['grant_count'])
262
        ) {
263
            if ($enableHTML) {
264
                $privs = ['<dfn title="'
265
                    . __('Includes all privileges except GRANT.')
266
                    . '">ALL PRIVILEGES</dfn>',
267
                ];
268
            } else {
269
                $privs = ['ALL PRIVILEGES'];
270
            }
271
        }
272
        return $privs;
273
    }
274
275
    /**
276
     * Returns an array of table grants and their descriptions
277
     *
278
     * @return array array of table grants
279
     */
280
    public function getTableGrantsArray()
281
    {
282
        return [
283
            [
284
                'Delete',
285
                'DELETE',
286
                $GLOBALS['strPrivDescDelete'],
287
            ],
288
            [
289
                'Create',
290
                'CREATE',
291
                $GLOBALS['strPrivDescCreateTbl'],
292
            ],
293
            [
294
                'Drop',
295
                'DROP',
296
                $GLOBALS['strPrivDescDropTbl'],
297
            ],
298
            [
299
                'Index',
300
                'INDEX',
301
                $GLOBALS['strPrivDescIndex'],
302
            ],
303
            [
304
                'Alter',
305
                'ALTER',
306
                $GLOBALS['strPrivDescAlter'],
307
            ],
308
            [
309
                'Create View',
310
                'CREATE_VIEW',
311
                $GLOBALS['strPrivDescCreateView'],
312
            ],
313
            [
314
                'Show view',
315
                'SHOW_VIEW',
316
                $GLOBALS['strPrivDescShowView'],
317
            ],
318
            [
319
                'Trigger',
320
                'TRIGGER',
321
                $GLOBALS['strPrivDescTrigger'],
322
            ],
323
        ];
324
    }
325
326
    /**
327
     * Get the grants array which contains all the privilege types
328
     * and relevant grant messages
329
     *
330
     * @return array
331
     */
332
    public function getGrantsArray()
333
    {
334
        return [
335
            [
336
                'Select_priv',
337
                'SELECT',
338
                __('Allows reading data.'),
339
            ],
340
            [
341
                'Insert_priv',
342
                'INSERT',
343
                __('Allows inserting and replacing data.'),
344
            ],
345
            [
346
                'Update_priv',
347
                'UPDATE',
348
                __('Allows changing data.'),
349
            ],
350
            [
351
                'Delete_priv',
352
                'DELETE',
353
                __('Allows deleting data.'),
354
            ],
355
            [
356
                'Create_priv',
357
                'CREATE',
358
                __('Allows creating new databases and tables.'),
359
            ],
360
            [
361
                'Drop_priv',
362
                'DROP',
363
                __('Allows dropping databases and tables.'),
364
            ],
365
            [
366
                'Reload_priv',
367
                'RELOAD',
368
                __('Allows reloading server settings and flushing the server\'s caches.'),
369
            ],
370
            [
371
                'Shutdown_priv',
372
                'SHUTDOWN',
373
                __('Allows shutting down the server.'),
374
            ],
375
            [
376
                'Process_priv',
377
                'PROCESS',
378
                __('Allows viewing processes of all users.'),
379
            ],
380
            [
381
                'File_priv',
382
                'FILE',
383
                __('Allows importing data from and exporting data into files.'),
384
            ],
385
            [
386
                'References_priv',
387
                'REFERENCES',
388
                __('Has no effect in this MySQL version.'),
389
            ],
390
            [
391
                'Index_priv',
392
                'INDEX',
393
                __('Allows creating and dropping indexes.'),
394
            ],
395
            [
396
                'Alter_priv',
397
                'ALTER',
398
                __('Allows altering the structure of existing tables.'),
399
            ],
400
            [
401
                'Show_db_priv',
402
                'SHOW DATABASES',
403
                __('Gives access to the complete list of databases.'),
404
            ],
405
            [
406
                'Super_priv',
407
                'SUPER',
408
                __(
409
                    'Allows connecting, even if maximum number of connections '
410
                    . 'is reached; required for most administrative operations '
411
                    . 'like setting global variables or killing threads of other users.'
412
                ),
413
            ],
414
            [
415
                'Create_tmp_table_priv',
416
                'CREATE TEMPORARY TABLES',
417
                __('Allows creating temporary tables.'),
418
            ],
419
            [
420
                'Lock_tables_priv',
421
                'LOCK TABLES',
422
                __('Allows locking tables for the current thread.'),
423
            ],
424
            [
425
                'Repl_slave_priv',
426
                'REPLICATION SLAVE',
427
                __('Needed for the replication slaves.'),
428
            ],
429
            [
430
                'Repl_client_priv',
431
                'REPLICATION CLIENT',
432
                __('Allows the user to ask where the slaves / masters are.'),
433
            ],
434
            [
435
                'Create_view_priv',
436
                'CREATE VIEW',
437
                __('Allows creating new views.'),
438
            ],
439
            [
440
                'Event_priv',
441
                'EVENT',
442
                __('Allows to set up events for the event scheduler.'),
443
            ],
444
            [
445
                'Trigger_priv',
446
                'TRIGGER',
447
                __('Allows creating and dropping triggers.'),
448
            ],
449
            // for table privs:
450
            [
451
                'Create View_priv',
452
                'CREATE VIEW',
453
                __('Allows creating new views.'),
454
            ],
455
            [
456
                'Show_view_priv',
457
                'SHOW VIEW',
458
                __('Allows performing SHOW CREATE VIEW queries.'),
459
            ],
460
            // for table privs:
461
            [
462
                'Show view_priv',
463
                'SHOW VIEW',
464
                __('Allows performing SHOW CREATE VIEW queries.'),
465
            ],
466
            [
467
                'Delete_history_priv',
468
                'DELETE HISTORY',
469
                __('Allows deleting historical rows.'),
470
            ],
471
            [
472
                'Delete versioning rows_priv',
473
                'DELETE HISTORY',
474
                __('Allows deleting historical rows.'),
475
            ],
476
            [
477
                'Create_routine_priv',
478
                'CREATE ROUTINE',
479
                __('Allows creating stored routines.'),
480
            ],
481
            [
482
                'Alter_routine_priv',
483
                'ALTER ROUTINE',
484
                __('Allows altering and dropping stored routines.'),
485
            ],
486
            [
487
                'Create_user_priv',
488
                'CREATE USER',
489
                __('Allows creating, dropping and renaming user accounts.'),
490
            ],
491
            [
492
                'Execute_priv',
493
                'EXECUTE',
494
                __('Allows executing stored routines.'),
495
            ],
496
        ];
497
    }
498
499
    /**
500
     * Get sql query for display privileges table
501
     *
502
     * @param string $db       the database
503
     * @param string $table    the table
504
     * @param string $username username for database connection
505
     * @param string $hostname hostname for database connection
506
     *
507
     * @return string sql query
508
     */
509
    public function getSqlQueryForDisplayPrivTable($db, $table, $username, $hostname)
510
    {
511
        if ($db == '*') {
512
            return "SELECT * FROM `mysql`.`user`"
513
                . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
514
                . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';";
515
        } elseif ($table == '*') {
516
            return "SELECT * FROM `mysql`.`db`"
517
                . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
518
                . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "'"
519
                . " AND '" . $this->dbi->escapeString(Util::unescapeMysqlWildcards($db)) . "'"
520
                . " LIKE `Db`;";
521
        }
522
        return "SELECT `Table_priv`"
523
            . " FROM `mysql`.`tables_priv`"
524
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
525
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "'"
526
            . " AND `Db` = '" . $this->dbi->escapeString(Util::unescapeMysqlWildcards($db)) . "'"
527
            . " AND `Table_name` = '" . $this->dbi->escapeString($table) . "';";
528
    }
529
530
    /**
531
     * Displays a dropdown to select the user group
532
     * with menu items configured to each of them.
533
     *
534
     * @param string $username username
535
     *
536
     * @return string html to select the user group
537
     */
538
    public function getHtmlToChooseUserGroup($username)
539
    {
540
        $cfgRelation = $this->relation->getRelationsParam();
541
        $groupTable = Util::backquote($cfgRelation['db'])
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($cfgRelation['db']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

541
        $groupTable = /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['db'])
Loading history...
542
            . "." . Util::backquote($cfgRelation['usergroups']);
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquo...Relation['usergroups']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

542
            . "." . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['usergroups']);
Loading history...
543
        $userTable = Util::backquote($cfgRelation['db'])
544
            . "." . Util::backquote($cfgRelation['users']);
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($cfgRelation['users']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

544
            . "." . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['users']);
Loading history...
545
546
        $userGroup = '';
547
        if (isset($GLOBALS['username'])) {
548
            $sql_query = "SELECT `usergroup` FROM " . $userTable
549
                . " WHERE `username` = '" . $this->dbi->escapeString($username) . "'";
550
            $userGroup = $this->dbi->fetchValue(
551
                $sql_query,
552
                0,
553
                0,
554
                DatabaseInterface::CONNECT_CONTROL
555
            );
556
        }
557
558
        $allUserGroups = ['' => ''];
559
        $sql_query = "SELECT DISTINCT `usergroup` FROM " . $groupTable;
560
        $result = $this->relation->queryAsControlUser($sql_query, false);
561
        if ($result) {
562
            while ($row = $this->dbi->fetchRow($result)) {
0 ignored issues
show
Bug introduced by
$result of type resource|true is incompatible with the type object expected by parameter $result of PhpMyAdmin\DatabaseInterface::fetchRow(). ( Ignorable by Annotation )

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

562
            while ($row = $this->dbi->fetchRow(/** @scrutinizer ignore-type */ $result)) {
Loading history...
563
                $allUserGroups[$row[0]] = $row[0];
564
            }
565
        }
566
        $this->dbi->freeResult($result);
0 ignored issues
show
Bug introduced by
$result of type boolean|resource is incompatible with the type object expected by parameter $result of PhpMyAdmin\DatabaseInterface::freeResult(). ( Ignorable by Annotation )

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

566
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
567
568
        return $this->template->render('server/privileges/choose_user_group', [
569
            'all_user_groups' => $allUserGroups,
570
            'user_group' => $userGroup,
571
            'params' => ['username' => $username],
572
        ]);
573
    }
574
575
    /**
576
     * Sets the user group from request values
577
     *
578
     * @param string $username  username
579
     * @param string $userGroup user group to set
580
     *
581
     * @return void
582
     */
583
    public function setUserGroup($username, $userGroup)
584
    {
585
        $userGroup = $userGroup === null ? '' : $userGroup;
0 ignored issues
show
introduced by
The condition $userGroup === null is always false.
Loading history...
586
        $cfgRelation = $this->relation->getRelationsParam();
587
        if (empty($cfgRelation['db']) || empty($cfgRelation['users']) || empty($cfgRelation['usergroups'])) {
588
            return;
589
        }
590
591
        $userTable = Util::backquote($cfgRelation['db'])
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($cfgRelation['db']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

591
        $userTable = /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['db'])
Loading history...
592
            . "." . Util::backquote($cfgRelation['users']);
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($cfgRelation['users']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

592
            . "." . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['users']);
Loading history...
593
594
        $sql_query = "SELECT `usergroup` FROM " . $userTable
595
            . " WHERE `username` = '" . $this->dbi->escapeString($username) . "'";
596
        $oldUserGroup = $this->dbi->fetchValue(
597
            $sql_query,
598
            0,
599
            0,
600
            DatabaseInterface::CONNECT_CONTROL
601
        );
602
603
        if ($oldUserGroup === false) {
604
            $upd_query = "INSERT INTO " . $userTable . "(`username`, `usergroup`)"
605
                . " VALUES ('" . $this->dbi->escapeString($username) . "', "
606
                . "'" . $this->dbi->escapeString($userGroup) . "')";
607
        } else {
608
            if (empty($userGroup)) {
609
                $upd_query = "DELETE FROM " . $userTable
610
                    . " WHERE `username`='" . $this->dbi->escapeString($username) . "'";
611
            } elseif ($oldUserGroup != $userGroup) {
612
                $upd_query = "UPDATE " . $userTable
613
                    . " SET `usergroup`='" . $this->dbi->escapeString($userGroup) . "'"
614
                    . " WHERE `username`='" . $this->dbi->escapeString($username) . "'";
615
            }
616
        }
617
        if (isset($upd_query)) {
618
            $this->relation->queryAsControlUser($upd_query);
619
        }
620
    }
621
622
    /**
623
     * Displays the privileges form table
624
     *
625
     * @param string  $db     the database
626
     * @param string  $table  the table
627
     * @param boolean $submit whether to display the submit button or not
628
     *
629
     * @global  array     $cfg         the phpMyAdmin configuration
630
     * @global  resource  $user_link   the database connection
631
     *
632
     * @return string html snippet
633
     */
634
    public function getHtmlToDisplayPrivilegesTable(
635
        $db = '*',
636
        $table = '*',
637
        $submit = true
638
    ) {
639
        $sql_query = '';
640
641
        if ($db == '*') {
642
            $table = '*';
643
        }
644
        $username = '';
645
        $hostname = '';
646
        if (isset($GLOBALS['username'])) {
647
            $username = $GLOBALS['username'];
648
            $hostname = $GLOBALS['hostname'];
649
            $sql_query = $this->getSqlQueryForDisplayPrivTable(
650
                $db,
651
                $table,
652
                $username,
653
                $hostname
654
            );
655
            $row = $this->dbi->fetchSingleRow($sql_query);
656
        }
657
        if (empty($row)) {
658
            if ($table == '*' && $this->dbi->isSuperuser()) {
659
                $row = [];
660
                if ($db == '*') {
661
                    $sql_query = 'SHOW COLUMNS FROM `mysql`.`user`;';
662
                } elseif ($table == '*') {
663
                    $sql_query = 'SHOW COLUMNS FROM `mysql`.`db`;';
664
                }
665
                $res = $this->dbi->query($sql_query);
666
                while ($row1 = $this->dbi->fetchRow($res)) {
0 ignored issues
show
Bug introduced by
It seems like $res can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::fetchRow() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

666
                while ($row1 = $this->dbi->fetchRow(/** @scrutinizer ignore-type */ $res)) {
Loading history...
667
                    if (mb_substr($row1[0], 0, 4) == 'max_') {
668
                        $row[$row1[0]] = 0;
669
                    } elseif (mb_substr($row1[0], 0, 5) == 'x509_'
670
                        || mb_substr($row1[0], 0, 4) == 'ssl_'
671
                    ) {
672
                        $row[$row1[0]] = '';
673
                    } else {
674
                        $row[$row1[0]] = 'N';
675
                    }
676
                }
677
                $this->dbi->freeResult($res);
0 ignored issues
show
Bug introduced by
It seems like $res can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::freeResult() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

677
                $this->dbi->freeResult(/** @scrutinizer ignore-type */ $res);
Loading history...
678
            } elseif ($table == '*') {
679
                $row = [];
680
            } else {
681
                $row = ['Table_priv' => ''];
682
            }
683
        }
684
        if (isset($row['Table_priv'])) {
685
            $this->fillInTablePrivileges($row);
686
687
            // get columns
688
            $res = $this->dbi->tryQuery(
689
                'SHOW COLUMNS FROM '
690
                . Util::backquote(
691
                    Util::unescapeMysqlWildcards($db)
692
                )
693
                . '.' . Util::backquote($table) . ';'
694
            );
695
            $columns = [];
696
            if ($res) {
697
                while ($row1 = $this->dbi->fetchRow($res)) {
698
                    $columns[$row1[0]] = [
699
                        'Select' => false,
700
                        'Insert' => false,
701
                        'Update' => false,
702
                        'References' => false,
703
                    ];
704
                }
705
                $this->dbi->freeResult($res);
706
            }
707
        }
708
709
        if (! empty($columns)) {
710
            $res = $this->dbi->query(
711
                'SELECT `Column_name`, `Column_priv`'
712
                . ' FROM `mysql`.`columns_priv`'
713
                . ' WHERE `User`'
714
                . ' = \'' . $this->dbi->escapeString($username) . "'"
715
                . ' AND `Host`'
716
                . ' = \'' . $this->dbi->escapeString($hostname) . "'"
717
                . ' AND `Db`'
718
                . ' = \'' . $this->dbi->escapeString(
719
                    Util::unescapeMysqlWildcards($db)
720
                ) . "'"
721
                . ' AND `Table_name`'
722
                . ' = \'' . $this->dbi->escapeString($table) . '\';'
723
            );
724
725
            while ($row1 = $this->dbi->fetchRow($res)) {
726
                $row1[1] = explode(',', $row1[1]);
727
                foreach ($row1[1] as $current) {
728
                    $columns[$row1[0]][$current] = true;
729
                }
730
            }
731
            $this->dbi->freeResult($res);
732
        }
733
734
        return $this->template->render('server/privileges/privileges_table', [
735
            'is_global' => $db === '*',
736
            'is_database' => $table === '*',
737
            'row' => $row,
738
            'columns' => $columns ?? [],
739
            'has_submit' => $submit,
740
        ]);
741
    }
742
743
    /**
744
     * Get the HTML snippet for routine specific privileges
745
     *
746
     * @param string $username   username for database connection
747
     * @param string $hostname   hostname for database connection
748
     * @param string $db         the database
749
     * @param string $routine    the routine
750
     * @param string $url_dbname url encoded db name
751
     *
752
     * @return string
753
     */
754
    public function getHtmlForRoutineSpecificPrivileges(
755
        $username,
756
        $hostname,
757
        $db,
758
        $routine,
759
        $url_dbname
760
    ) {
761
        $privileges = $this->getRoutinePrivileges($username, $hostname, $db, $routine);
762
763
        return $this->template->render('server/privileges/edit_routine_privileges', [
764
            'username' => $username,
765
            'hostname' => $hostname,
766
            'database' => $db,
767
            'routine' => $routine,
768
            'privileges' => $privileges,
769
            'dbname' => $url_dbname,
770
            'current_user' => $this->dbi->getCurrentUser(),
771
        ]);
772
    }
773
774
    /**
775
     * Gets the currently active authentication plugins
776
     *
777
     * @param string $orig_auth_plugin Default Authentication plugin
778
     * @param string $mode             are we creating a new user or are we just
779
     *                                 changing  one?
780
     *                                 (allowed values: 'new', 'edit', 'change_pw')
781
     * @param string $versions         Is MySQL version newer or older than 5.5.7
782
     *
783
     * @return string
784
     */
785
    public function getHtmlForAuthPluginsDropdown(
786
        $orig_auth_plugin,
787
        $mode = 'new',
788
        $versions = 'new'
789
    ) {
790
        $select_id = 'select_authentication_plugin'
791
            . ($mode == 'change_pw' ? '_cp' : '');
792
793
        if ($versions == 'new') {
794
            $active_auth_plugins = $this->getActiveAuthPlugins();
795
796
            if (isset($active_auth_plugins['mysql_old_password'])) {
797
                unset($active_auth_plugins['mysql_old_password']);
798
            }
799
        } else {
800
            $active_auth_plugins = [
801
                'mysql_native_password' => __('Native MySQL authentication'),
802
            ];
803
        }
804
805
        $html_output = Util::getDropdown(
806
            'authentication_plugin',
807
            $active_auth_plugins,
808
            $orig_auth_plugin,
809
            $select_id
810
        );
811
812
        return $html_output;
813
    }
814
815
    /**
816
     * Gets the currently active authentication plugins
817
     *
818
     * @return array  array of plugin names and descriptions
819
     */
820
    public function getActiveAuthPlugins()
821
    {
822
        $get_plugins_query = "SELECT `PLUGIN_NAME`, `PLUGIN_DESCRIPTION`"
823
            . " FROM `information_schema`.`PLUGINS` "
824
            . "WHERE `PLUGIN_TYPE` = 'AUTHENTICATION';";
825
        $resultset = $this->dbi->query($get_plugins_query);
826
827
        $result = [];
828
829
        while ($row = $this->dbi->fetchAssoc($resultset)) {
0 ignored issues
show
Bug introduced by
It seems like $resultset can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::fetchAssoc() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

829
        while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $resultset)) {
Loading history...
830
            // if description is known, enable its translation
831
            if ('mysql_native_password' == $row['PLUGIN_NAME']) {
832
                $row['PLUGIN_DESCRIPTION'] = __('Native MySQL authentication');
833
            } elseif ('sha256_password' == $row['PLUGIN_NAME']) {
834
                $row['PLUGIN_DESCRIPTION'] = __('SHA256 password authentication');
835
            }
836
837
            $result[$row['PLUGIN_NAME']] = $row['PLUGIN_DESCRIPTION'];
838
        }
839
840
        return $result;
841
    }
842
843
    /**
844
     * Displays the fields used by the "new user" form as well as the
845
     * "change login information / copy user" form.
846
     *
847
     * @param string $mode are we creating a new user or are we just
848
     *                     changing  one? (allowed values: 'new', 'change')
849
     * @param string $user User name
850
     * @param string $host Host name
851
     *
852
     * @return string  a HTML snippet
853
     */
854
    public function getHtmlForLoginInformationFields(
855
        $mode = 'new',
856
        $user = null,
857
        $host = null
858
    ) {
859
        global $pred_username, $pred_hostname, $username, $hostname, $new_username;
860
861
        list($usernameLength, $hostnameLength) = $this->getUsernameAndHostnameLength();
862
863
        if (isset($username) && strlen($username) === 0) {
864
            $pred_username = 'any';
865
        }
866
867
        $currentUser = $this->dbi->fetchValue('SELECT USER();');
868
        $thisHost = null;
869
        if (! empty($currentUser)) {
870
            $thisHost = str_replace(
871
                '\'',
872
                '',
873
                mb_substr(
874
                    $currentUser,
875
                    mb_strrpos($currentUser, '@') + 1
876
                )
877
            );
878
        }
879
880
        if (! isset($pred_hostname) && isset($hostname)) {
881
            switch (mb_strtolower($hostname)) {
882
                case 'localhost':
883
                case '127.0.0.1':
884
                    $pred_hostname = 'localhost';
885
                    break;
886
                case '%':
887
                    $pred_hostname = 'any';
888
                    break;
889
                default:
890
                    $pred_hostname = 'userdefined';
891
                    break;
892
            }
893
        }
894
895
        $serverType = Util::getServerType();
896
        $serverVersion = $this->dbi->getVersion();
897
        $authPlugin = $this->getCurrentAuthenticationPlugin(
898
            $mode,
899
            $user,
900
            $host
901
        );
902
903
        if (($serverType == 'MySQL'
904
            && $serverVersion >= 50507)
905
            || ($serverType == 'MariaDB'
906
            && $serverVersion >= 50200)
907
        ) {
908
            $isNew = true;
909
            $authPluginDropdown = $this->getHtmlForAuthPluginsDropdown(
910
                $authPlugin,
911
                $mode,
912
                'new'
913
            );
914
        } else {
915
            $isNew = false;
916
            $authPluginDropdown = $this->getHtmlForAuthPluginsDropdown(
917
                $authPlugin,
918
                $mode,
919
                'old'
920
            );
921
        }
922
923
        return $this->template->render('server/privileges/login_information_fields', [
924
            'pred_username' => $pred_username ?? null,
925
            'pred_hostname' => $pred_hostname ?? null,
926
            'username_length' => $usernameLength,
927
            'hostname_length' => $hostnameLength,
928
            'username' => $username ?? null,
929
            'new_username' => $new_username ?? null,
930
            'hostname' => $hostname ?? null,
931
            'this_host' => $thisHost,
932
            'is_change' => $mode === 'change',
933
            'auth_plugin' => $authPlugin,
934
            'auth_plugin_dropdown' => $authPluginDropdown,
935
            'is_new' => $isNew,
936
        ]);
937
    }
938
939
    /**
940
     * Get username and hostname length
941
     *
942
     * @return array username length and hostname length
943
     */
944
    public function getUsernameAndHostnameLength()
945
    {
946
        /* Fallback values */
947
        $username_length = 16;
948
        $hostname_length = 41;
949
950
        /* Try to get real lengths from the database */
951
        $fields_info = $this->dbi->fetchResult(
952
            'SELECT COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH '
953
            . 'FROM information_schema.columns '
954
            . "WHERE table_schema = 'mysql' AND table_name = 'user' "
955
            . "AND COLUMN_NAME IN ('User', 'Host')"
956
        );
957
        foreach ($fields_info as $val) {
958
            if ($val['COLUMN_NAME'] == 'User') {
959
                $username_length = $val['CHARACTER_MAXIMUM_LENGTH'];
960
            } elseif ($val['COLUMN_NAME'] == 'Host') {
961
                $hostname_length = $val['CHARACTER_MAXIMUM_LENGTH'];
962
            }
963
        }
964
        return [
965
            $username_length,
966
            $hostname_length,
967
        ];
968
    }
969
970
    /**
971
     * Get current authentication plugin in use - for a user or globally
972
     *
973
     * @param string $mode     are we creating a new user or are we just
974
     *                         changing  one? (allowed values: 'new', 'change')
975
     * @param string $username User name
976
     * @param string $hostname Host name
977
     *
978
     * @return string authentication plugin in use
979
     */
980
    public function getCurrentAuthenticationPlugin(
981
        $mode = 'new',
982
        $username = null,
983
        $hostname = null
984
    ) {
985
        /* Fallback (standard) value */
986
        $authentication_plugin = 'mysql_native_password';
987
        $serverVersion = $this->dbi->getVersion();
988
989
        if (isset($username, $hostname) && $mode == 'change') {
990
            $row = $this->dbi->fetchSingleRow(
991
                'SELECT `plugin` FROM `mysql`.`user` WHERE '
992
                . '`User` = "' . $username . '" AND `Host` = "' . $hostname . '" LIMIT 1'
993
            );
994
            // Table 'mysql'.'user' may not exist for some previous
995
            // versions of MySQL - in that case consider fallback value
996
            if (is_array($row) && isset($row['plugin'])) {
997
                $authentication_plugin = $row['plugin'];
998
            }
999
        } elseif ($mode == 'change') {
1000
            list($username, $hostname) = $this->dbi->getCurrentUserAndHost();
1001
1002
            $row = $this->dbi->fetchSingleRow(
1003
                'SELECT `plugin` FROM `mysql`.`user` WHERE '
1004
                . '`User` = "' . $username . '" AND `Host` = "' . $hostname . '"'
1005
            );
1006
            if (is_array($row) && isset($row['plugin'])) {
1007
                $authentication_plugin = $row['plugin'];
1008
            }
1009
        } elseif ($serverVersion >= 50702) {
1010
            $row = $this->dbi->fetchSingleRow(
1011
                'SELECT @@default_authentication_plugin'
1012
            );
1013
            $authentication_plugin = is_array($row) ? $row['@@default_authentication_plugin'] : null;
1014
        }
1015
1016
        return $authentication_plugin;
1017
    }
1018
1019
    /**
1020
     * Returns all the grants for a certain user on a certain host
1021
     * Used in the export privileges for all users section
1022
     *
1023
     * @param string $user User name
1024
     * @param string $host Host name
1025
     *
1026
     * @return string containing all the grants text
1027
     */
1028
    public function getGrants($user, $host)
1029
    {
1030
        $grants = $this->dbi->fetchResult(
1031
            "SHOW GRANTS FOR '"
1032
            . $this->dbi->escapeString($user) . "'@'"
1033
            . $this->dbi->escapeString($host) . "'"
1034
        );
1035
        $response = '';
1036
        foreach ($grants as $one_grant) {
1037
            $response .= $one_grant . ";\n\n";
1038
        }
1039
        return $response;
1040
    }
1041
1042
    /**
1043
     * Update password and get message for password updating
1044
     *
1045
     * @param string $err_url  error url
1046
     * @param string $username username
1047
     * @param string $hostname hostname
1048
     *
1049
     * @return Message success or error message after updating password
1050
     */
1051
    public function updatePassword($err_url, $username, $hostname)
1052
    {
1053
        // similar logic in /user_password
1054
        $message = null;
1055
1056
        if (isset($_POST['pma_pw'], $_POST['pma_pw2']) && empty($_POST['nopass'])) {
1057
            if ($_POST['pma_pw'] != $_POST['pma_pw2']) {
1058
                $message = Message::error(__('The passwords aren\'t the same!'));
1059
            } elseif (empty($_POST['pma_pw']) || empty($_POST['pma_pw2'])) {
1060
                $message = Message::error(__('The password is empty!'));
1061
            }
1062
        }
1063
1064
        // here $nopass could be == 1
1065
        if ($message === null) {
1066
            $hashing_function = 'PASSWORD';
1067
            $serverType = Util::getServerType();
1068
            $serverVersion = $this->dbi->getVersion();
1069
            $authentication_plugin
1070
                = (isset($_POST['authentication_plugin'])
1071
                ? $_POST['authentication_plugin']
1072
                : $this->getCurrentAuthenticationPlugin(
1073
                    'change',
1074
                    $username,
1075
                    $hostname
1076
                ));
1077
1078
            // Use 'ALTER USER ...' syntax for MySQL 5.7.6+
1079
            if ($serverType == 'MySQL'
1080
                && $serverVersion >= 50706
1081
            ) {
1082
                if ($authentication_plugin != 'mysql_old_password') {
1083
                    $query_prefix = "ALTER USER '"
1084
                        . $this->dbi->escapeString($username)
1085
                        . "'@'" . $this->dbi->escapeString($hostname) . "'"
1086
                        . " IDENTIFIED WITH "
1087
                        . $authentication_plugin
1088
                        . " BY '";
1089
                } else {
1090
                    $query_prefix = "ALTER USER '"
1091
                        . $this->dbi->escapeString($username)
1092
                        . "'@'" . $this->dbi->escapeString($hostname) . "'"
1093
                        . " IDENTIFIED BY '";
1094
                }
1095
1096
                // in $sql_query which will be displayed, hide the password
1097
                $sql_query = $query_prefix . "*'";
1098
1099
                $local_query = $query_prefix
1100
                    . $this->dbi->escapeString($_POST['pma_pw']) . "'";
1101
            } elseif ($serverType == 'MariaDB' && $serverVersion >= 10000) {
1102
                // MariaDB uses "SET PASSWORD" syntax to change user password.
1103
                // On Galera cluster only DDL queries are replicated, since
1104
                // users are stored in MyISAM storage engine.
1105
                $query_prefix = "SET PASSWORD FOR  '"
1106
                    . $this->dbi->escapeString($username)
1107
                    . "'@'" . $this->dbi->escapeString($hostname) . "'"
1108
                    . " = PASSWORD ('";
1109
                $sql_query = $local_query = $query_prefix
1110
                    . $this->dbi->escapeString($_POST['pma_pw']) . "')";
1111
            } elseif ($serverType == 'MariaDB'
1112
                && $serverVersion >= 50200
1113
                && $this->dbi->isSuperuser()
1114
            ) {
1115
                // Use 'UPDATE `mysql`.`user` ...' Syntax for MariaDB 5.2+
1116
                if ($authentication_plugin == 'mysql_native_password') {
1117
                    // Set the hashing method used by PASSWORD()
1118
                    // to be 'mysql_native_password' type
1119
                    $this->dbi->tryQuery('SET old_passwords = 0;');
1120
                } elseif ($authentication_plugin == 'sha256_password') {
1121
                    // Set the hashing method used by PASSWORD()
1122
                    // to be 'sha256_password' type
1123
                    $this->dbi->tryQuery('SET `old_passwords` = 2;');
1124
                }
1125
1126
                $hashedPassword = $this->getHashedPassword($_POST['pma_pw']);
1127
1128
                $sql_query        = 'SET PASSWORD FOR \''
1129
                    . $this->dbi->escapeString($username)
1130
                    . '\'@\'' . $this->dbi->escapeString($hostname) . '\' = '
1131
                    . ($_POST['pma_pw'] == ''
1132
                        ? '\'\''
1133
                        : $hashing_function . '(\''
1134
                        . preg_replace('@.@s', '*', $_POST['pma_pw']) . '\')');
1135
1136
                $local_query = "UPDATE `mysql`.`user` SET "
1137
                    . " `authentication_string` = '" . $hashedPassword
1138
                    . "', `Password` = '', "
1139
                    . " `plugin` = '" . $authentication_plugin . "'"
1140
                    . " WHERE `User` = '" . $username . "' AND Host = '"
1141
                    . $hostname . "';";
1142
            } else {
1143
                // USE 'SET PASSWORD ...' syntax for rest of the versions
1144
                // Backup the old value, to be reset later
1145
                $row = $this->dbi->fetchSingleRow(
1146
                    'SELECT @@old_passwords;'
1147
                );
1148
                $orig_value = $row['@@old_passwords'];
1149
                $update_plugin_query = "UPDATE `mysql`.`user` SET"
1150
                    . " `plugin` = '" . $authentication_plugin . "'"
1151
                    . " WHERE `User` = '" . $username . "' AND Host = '"
1152
                    . $hostname . "';";
1153
1154
                // Update the plugin for the user
1155
                if (! $this->dbi->tryQuery($update_plugin_query)) {
1156
                    Util::mysqlDie(
1157
                        $this->dbi->getError(),
1158
                        $update_plugin_query,
1159
                        false,
1160
                        $err_url
1161
                    );
1162
                }
1163
                $this->dbi->tryQuery("FLUSH PRIVILEGES;");
1164
1165
                if ($authentication_plugin == 'mysql_native_password') {
1166
                    // Set the hashing method used by PASSWORD()
1167
                    // to be 'mysql_native_password' type
1168
                    $this->dbi->tryQuery('SET old_passwords = 0;');
1169
                } elseif ($authentication_plugin == 'sha256_password') {
1170
                    // Set the hashing method used by PASSWORD()
1171
                    // to be 'sha256_password' type
1172
                    $this->dbi->tryQuery('SET `old_passwords` = 2;');
1173
                }
1174
                $sql_query        = 'SET PASSWORD FOR \''
1175
                    . $this->dbi->escapeString($username)
1176
                    . '\'@\'' . $this->dbi->escapeString($hostname) . '\' = '
1177
                    . ($_POST['pma_pw'] == ''
1178
                        ? '\'\''
1179
                        : $hashing_function . '(\''
1180
                        . preg_replace('@.@s', '*', $_POST['pma_pw']) . '\')');
1181
1182
                $local_query      = 'SET PASSWORD FOR \''
1183
                    . $this->dbi->escapeString($username)
1184
                    . '\'@\'' . $this->dbi->escapeString($hostname) . '\' = '
1185
                    . ($_POST['pma_pw'] == '' ? '\'\'' : $hashing_function
1186
                    . '(\'' . $this->dbi->escapeString($_POST['pma_pw']) . '\')');
1187
            }
1188
1189
            if (! $this->dbi->tryQuery($local_query)) {
1190
                Util::mysqlDie(
1191
                    $this->dbi->getError(),
1192
                    $sql_query,
1193
                    false,
1194
                    $err_url
1195
                );
1196
            }
1197
            // Flush privileges after successful password change
1198
            $this->dbi->tryQuery("FLUSH PRIVILEGES;");
1199
1200
            $message = Message::success(
1201
                __('The password for %s was changed successfully.')
1202
            );
1203
            $message->addParam('\'' . $username . '\'@\'' . $hostname . '\'');
1204
            if (isset($orig_value)) {
1205
                $this->dbi->tryQuery(
1206
                    'SET `old_passwords` = ' . $orig_value . ';'
1207
                );
1208
            }
1209
        }
1210
        return $message;
1211
    }
1212
1213
    /**
1214
     * Revokes privileges and get message and SQL query for privileges revokes
1215
     *
1216
     * @param string $dbname    database name
1217
     * @param string $tablename table name
1218
     * @param string $username  username
1219
     * @param string $hostname  host name
1220
     * @param string $itemType  item type
1221
     *
1222
     * @return array ($message, $sql_query)
1223
     */
1224
    public function getMessageAndSqlQueryForPrivilegesRevoke(
1225
        $dbname,
1226
        $tablename,
1227
        $username,
1228
        $hostname,
1229
        $itemType
1230
    ) {
1231
        $db_and_table = $this->wildcardEscapeForGrant($dbname, $tablename);
1232
1233
        $sql_query0 = 'REVOKE ALL PRIVILEGES ON ' . $itemType . ' ' . $db_and_table
1234
            . ' FROM \''
1235
            . $this->dbi->escapeString($username) . '\'@\''
1236
            . $this->dbi->escapeString($hostname) . '\';';
1237
1238
        $sql_query1 = 'REVOKE GRANT OPTION ON ' . $itemType . ' ' . $db_and_table
1239
            . ' FROM \'' . $this->dbi->escapeString($username) . '\'@\''
1240
            . $this->dbi->escapeString($hostname) . '\';';
1241
1242
        $this->dbi->query($sql_query0);
1243
        if (! $this->dbi->tryQuery($sql_query1)) {
1244
            // this one may fail, too...
1245
            $sql_query1 = '';
1246
        }
1247
        $sql_query = $sql_query0 . ' ' . $sql_query1;
1248
        $message = Message::success(
1249
            __('You have revoked the privileges for %s.')
1250
        );
1251
        $message->addParam('\'' . $username . '\'@\'' . $hostname . '\'');
1252
1253
        return [
1254
            $message,
1255
            $sql_query,
1256
        ];
1257
    }
1258
1259
    /**
1260
     * Get REQUIRE cluase
1261
     *
1262
     * @return string REQUIRE clause
1263
     */
1264
    public function getRequireClause()
1265
    {
1266
        $arr = isset($_POST['ssl_type']) ? $_POST : $GLOBALS;
1267
        if (isset($arr['ssl_type']) && $arr['ssl_type'] == 'SPECIFIED') {
1268
            $require = [];
1269
            if (! empty($arr['ssl_cipher'])) {
1270
                $require[] = "CIPHER '"
1271
                        . $this->dbi->escapeString($arr['ssl_cipher']) . "'";
1272
            }
1273
            if (! empty($arr['x509_issuer'])) {
1274
                $require[] = "ISSUER '"
1275
                        . $this->dbi->escapeString($arr['x509_issuer']) . "'";
1276
            }
1277
            if (! empty($arr['x509_subject'])) {
1278
                $require[] = "SUBJECT '"
1279
                        . $this->dbi->escapeString($arr['x509_subject']) . "'";
1280
            }
1281
            if (count($require)) {
1282
                $require_clause = " REQUIRE " . implode(" AND ", $require);
1283
            } else {
1284
                $require_clause = " REQUIRE NONE";
1285
            }
1286
        } elseif (isset($arr['ssl_type']) && $arr['ssl_type'] == 'X509') {
1287
            $require_clause = " REQUIRE X509";
1288
        } elseif (isset($arr['ssl_type']) && $arr['ssl_type'] == 'ANY') {
1289
            $require_clause = " REQUIRE SSL";
1290
        } else {
1291
            $require_clause = " REQUIRE NONE";
1292
        }
1293
1294
        return $require_clause;
1295
    }
1296
1297
    /**
1298
     * Get a WITH clause for 'update privileges' and 'add user'
1299
     *
1300
     * @return string
1301
     */
1302
    public function getWithClauseForAddUserAndUpdatePrivs()
1303
    {
1304
        $sql_query = '';
1305
        if (((isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y')
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && $_POST['Gr...->getVersion() >= 80011, Probably Intended Meaning: IssetNode && $_POST['Gra...>getVersion() >= 80011)
Loading history...
1306
            || (isset($GLOBALS['Grant_priv']) && $GLOBALS['Grant_priv'] == 'Y'))
1307
            && ! ((Util::getServerType() == 'MySQL' || Util::getServerType() == 'Percona Server')
1308
                && $this->dbi->getVersion() >= 80011)
1309
        ) {
1310
            $sql_query .= ' GRANT OPTION';
1311
        }
1312
        if (isset($_POST['max_questions']) || isset($GLOBALS['max_questions'])) {
1313
            $max_questions = isset($_POST['max_questions'])
1314
                ? (int) $_POST['max_questions'] : (int) $GLOBALS['max_questions'];
1315
            $max_questions = max(0, $max_questions);
1316
            $sql_query .= ' MAX_QUERIES_PER_HOUR ' . $max_questions;
1317
        }
1318
        if (isset($_POST['max_connections']) || isset($GLOBALS['max_connections'])) {
1319
            $max_connections = isset($_POST['max_connections'])
1320
                ? (int) $_POST['max_connections'] : (int) $GLOBALS['max_connections'];
1321
            $max_connections = max(0, $max_connections);
1322
            $sql_query .= ' MAX_CONNECTIONS_PER_HOUR ' . $max_connections;
1323
        }
1324
        if (isset($_POST['max_updates']) || isset($GLOBALS['max_updates'])) {
1325
            $max_updates = isset($_POST['max_updates'])
1326
                ? (int) $_POST['max_updates'] : (int) $GLOBALS['max_updates'];
1327
            $max_updates = max(0, $max_updates);
1328
            $sql_query .= ' MAX_UPDATES_PER_HOUR ' . $max_updates;
1329
        }
1330
        if (isset($_POST['max_user_connections'])
1331
            || isset($GLOBALS['max_user_connections'])
1332
        ) {
1333
            $max_user_connections = isset($_POST['max_user_connections'])
1334
                ? (int) $_POST['max_user_connections']
1335
                : (int) $GLOBALS['max_user_connections'];
1336
            $max_user_connections = max(0, $max_user_connections);
1337
            $sql_query .= ' MAX_USER_CONNECTIONS ' . $max_user_connections;
1338
        }
1339
        return (! empty($sql_query) ? ' WITH' . $sql_query : '');
1340
    }
1341
1342
    /**
1343
     * Get HTML for addUsersForm, This function call if isset($_GET['adduser'])
1344
     *
1345
     * @param string $dbname database name
1346
     *
1347
     * @return string HTML for addUserForm
1348
     */
1349
    public function getHtmlForAddUser($dbname)
1350
    {
1351
        global $is_grantuser;
1352
1353
        $loginInformationFieldsNew = $this->getHtmlForLoginInformationFields('new');
1354
        $privilegesTable = '';
1355
        if ($is_grantuser) {
1356
            $privilegesTable = $this->getHtmlToDisplayPrivilegesTable('*', '*', false);
1357
        }
1358
1359
        return $this->template->render('server/privileges/add_user', [
1360
            'database' => $dbname,
1361
            'login_information_fields_new' => $loginInformationFieldsNew,
1362
            'is_grant_user' => $is_grantuser,
1363
            'privileges_table' => $privilegesTable,
1364
        ]);
1365
    }
1366
1367
    /**
1368
     * Get the HTML for user form and check the privileges for a particular database.
1369
     *
1370
     * @param string $db database name
1371
     *
1372
     * @return string
1373
     */
1374
    public function getHtmlForSpecificDbPrivileges(string $db): string
1375
    {
1376
        global $cfg, $pmaThemeImage, $text_dir, $is_createuser, $is_grantuser;
1377
1378
        $scriptName = Util::getScriptNameForOption(
1379
            $cfg['DefaultTabDatabase'],
1380
            'database'
1381
        );
1382
1383
        $privileges = [];
1384
        if ($this->dbi->isSuperuser()) {
1385
            $privileges = $this->getAllPrivileges($db);
1386
        }
1387
1388
        $response = Response::getInstance();
1389
        if ($response->isAjax() === true
1390
            && empty($_REQUEST['ajax_page_request'])
1391
        ) {
1392
            $message = Message::success(__('User has been added.'));
1393
            $response->addJSON('message', $message);
1394
            exit;
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1395
        }
1396
1397
        return $this->template->render('server/privileges/database', [
1398
            'is_superuser' => $this->dbi->isSuperuser(),
1399
            'db' => $db,
1400
            'database_url' => $scriptName,
1401
            'pma_theme_image' => $pmaThemeImage,
1402
            'text_dir' => $text_dir,
1403
            'is_createuser' => $is_createuser,
1404
            'is_grantuser' => $is_grantuser,
1405
            'privileges' => $privileges,
1406
        ]);
1407
    }
1408
1409
    /**
1410
     * Get the HTML for user form and check the privileges for a particular table.
1411
     *
1412
     * @param string $db    database name
1413
     * @param string $table table name
1414
     *
1415
     * @return string
1416
     */
1417
    public function getHtmlForSpecificTablePrivileges(string $db, string $table): string
1418
    {
1419
        global $cfg, $pmaThemeImage, $text_dir, $is_createuser, $is_grantuser;
1420
1421
        $scriptName = Util::getScriptNameForOption(
1422
            $cfg['DefaultTabTable'],
1423
            'table'
1424
        );
1425
1426
        $privileges = [];
1427
        if ($this->dbi->isSuperuser()) {
1428
            $privileges = $this->getAllPrivileges($db, $table);
1429
        }
1430
1431
        return $this->template->render('server/privileges/table', [
1432
            'db' => $db,
1433
            'table' => $table,
1434
            'is_superuser' => $this->dbi->isSuperuser(),
1435
            'table_url' => $scriptName,
1436
            'pma_theme_image' => $pmaThemeImage,
1437
            'text_dir' => $text_dir,
1438
            'is_createuser' => $is_createuser,
1439
            'is_grantuser' => $is_grantuser,
1440
            'privileges' => $privileges,
1441
        ]);
1442
    }
1443
1444
    /**
1445
     * @param string $db    database name
1446
     * @param string $table table name
1447
     *
1448
     * @return array
1449
     */
1450
    private function getAllPrivileges(string $db, string $table = ''): array
1451
    {
1452
        $privileges = [];
1453
        $databasePrivileges = $this->getGlobalAndDatabasePrivileges($db);
1454
        $tablePrivileges = [];
1455
        if ($table !== '') {
1456
            $tablePrivileges = $this->getTablePrivileges($db, $table);
1457
        }
1458
        $routinePrivileges = $this->getRoutinesPrivileges($db);
1459
        $allPrivileges = array_merge($databasePrivileges, $tablePrivileges, $routinePrivileges);
1460
1461
        foreach ($allPrivileges as $priv) {
1462
            $privilege = [
1463
                'type' => $priv['Type'],
1464
                'database' => $priv['Db'],
1465
            ];
1466
            if ($priv['Type'] === 'r') {
1467
                $privilege['routine'] = $priv['Routine_name'];
1468
                $privilege['has_grant'] = strpos(
1469
                    $priv['Proc_priv'],
1470
                    'Grant'
1471
                ) !== false;
1472
                $privilege['privileges'] = explode(',', $priv['Proc_priv']);
1473
            } elseif ($priv['Type'] === 't') {
1474
                $privilege['table'] = $priv['Table_name'];
1475
                $privilege['has_grant'] = strpos(
1476
                    $priv['Table_priv'],
1477
                    'Grant'
1478
                ) !== false;
1479
                $tablePrivs = explode(',', $priv['Table_priv']);
1480
                $specificPrivileges = [];
1481
                $grantsArr = $this->getTableGrantsArray();
1482
                foreach ($grantsArr as $grant) {
1483
                    $specificPrivileges[$grant[0]] = 'N';
1484
                    foreach ($tablePrivs as $tablePriv) {
1485
                        if ($grant[0] == $tablePriv) {
1486
                            $specificPrivileges[$grant[0]] = 'Y';
1487
                        }
1488
                    }
1489
                }
1490
                $privilege['privileges'] = $this->extractPrivInfo(
1491
                    $specificPrivileges,
1492
                    true,
1493
                    true
1494
                );
1495
            } else {
1496
                $privilege['has_grant'] = $priv['Grant_priv'] === 'Y';
1497
                $privilege['privileges'] = $this->extractPrivInfo(
1498
                    $priv,
1499
                    true
1500
                );
1501
            }
1502
1503
            $userHost = $priv['User'] . '@' . $priv['Host'];
1504
            $privileges[$userHost] = $privileges[$userHost] ?? [];
1505
            $privileges[$userHost]['user'] = $priv['User'];
1506
            $privileges[$userHost]['host'] = $priv['Host'];
1507
            $privileges[$userHost]['privileges'] = $privileges[$userHost]['privileges'] ?? [];
1508
            $privileges[$userHost]['privileges'][] = $privilege;
1509
        }
1510
        return $privileges;
1511
    }
1512
1513
    /**
1514
     * @param string $db database name
1515
     *
1516
     * @return array
1517
     */
1518
    private function getGlobalAndDatabasePrivileges(string $db): array
1519
    {
1520
        $listOfPrivileges = '`Select_priv`,
1521
            `Insert_priv`,
1522
            `Update_priv`,
1523
            `Delete_priv`,
1524
            `Create_priv`,
1525
            `Drop_priv`,
1526
            `Grant_priv`,
1527
            `Index_priv`,
1528
            `Alter_priv`,
1529
            `References_priv`,
1530
            `Create_tmp_table_priv`,
1531
            `Lock_tables_priv`,
1532
            `Create_view_priv`,
1533
            `Show_view_priv`,
1534
            `Create_routine_priv`,
1535
            `Alter_routine_priv`,
1536
            `Execute_priv`,
1537
            `Event_priv`,
1538
            `Trigger_priv`,';
1539
1540
        $listOfComparedPrivileges = '`Select_priv` = \'N\' AND
1541
            `Insert_priv` = \'N\' AND
1542
            `Update_priv` = \'N\' AND
1543
            `Delete_priv` = \'N\' AND
1544
            `Create_priv` = \'N\' AND
1545
            `Drop_priv` = \'N\' AND
1546
            `Grant_priv` = \'N\' AND
1547
            `References_priv` = \'N\' AND
1548
            `Create_tmp_table_priv` = \'N\' AND
1549
            `Lock_tables_priv` = \'N\' AND
1550
            `Create_view_priv` = \'N\' AND
1551
            `Show_view_priv` = \'N\' AND
1552
            `Create_routine_priv` = \'N\' AND
1553
            `Alter_routine_priv` = \'N\' AND
1554
            `Execute_priv` = \'N\' AND
1555
            `Event_priv` = \'N\' AND
1556
            `Trigger_priv` = \'N\'';
1557
1558
        $query = '
1559
            (
1560
                SELECT `User`, `Host`, ' . $listOfPrivileges . ' \'*\' AS `Db`, \'g\' AS `Type`
1561
                FROM `mysql`.`user`
1562
                WHERE NOT (' . $listOfComparedPrivileges . ')
1563
            )
1564
            UNION
1565
            (
1566
                SELECT `User`, `Host`, ' . $listOfPrivileges . ' `Db`, \'d\' AS `Type`
1567
                FROM `mysql`.`db`
1568
                WHERE \'' . $this->dbi->escapeString($db) . '\' LIKE `Db` AND NOT (' . $listOfComparedPrivileges . ')
1569
            )
1570
            ORDER BY `User` ASC, `Host` ASC, `Db` ASC;
1571
        ';
1572
        $result = $this->dbi->query($query);
1573
        if ($result === false) {
1574
            return [];
1575
        }
1576
1577
        $privileges = [];
1578
        while ($row = $this->dbi->fetchAssoc($result)) {
1579
            $privileges[] = $row;
1580
        }
1581
        return $privileges;
1582
    }
1583
1584
    /**
1585
     * @param string $db    database name
1586
     * @param string $table table name
1587
     *
1588
     * @return array
1589
     */
1590
    private function getTablePrivileges(string $db, string $table): array
1591
    {
1592
        $query = '
1593
            SELECT `User`, `Host`, `Db`, \'t\' AS `Type`, `Table_name`, `Table_priv`
1594
            FROM `mysql`.`tables_priv`
1595
            WHERE
1596
                \'' . $this->dbi->escapeString($db) . '\' LIKE `Db` AND
1597
                \'' . $this->dbi->escapeString($table) . '\' LIKE `Table_name` AND
1598
                NOT (`Table_priv` = \'\' AND Column_priv = \'\')
1599
            ORDER BY `User` ASC, `Host` ASC, `Db` ASC, `Table_priv` ASC;
1600
        ';
1601
        $result = $this->dbi->query($query);
1602
        if ($result === false) {
1603
            return [];
1604
        }
1605
1606
        $privileges = [];
1607
        while ($row = $this->dbi->fetchAssoc($result)) {
1608
            $privileges[] = $row;
1609
        }
1610
        return $privileges;
1611
    }
1612
1613
    /**
1614
     * @param string $db database name
1615
     *
1616
     * @return array
1617
     */
1618
    private function getRoutinesPrivileges(string $db): array
1619
    {
1620
        $query = '
1621
            SELECT *, \'r\' AS `Type`
1622
            FROM `mysql`.`procs_priv`
1623
            WHERE Db = \'' . $this->dbi->escapeString($db) . '\';
1624
        ';
1625
        $result = $this->dbi->query($query);
1626
        if ($result === false) {
1627
            return [];
1628
        }
1629
1630
        $privileges = [];
1631
        while ($row = $this->dbi->fetchAssoc($result)) {
1632
            $privileges[] = $row;
1633
        }
1634
        return $privileges;
1635
    }
1636
1637
    /**
1638
     * Get HTML error for View Users form
1639
     * For non superusers such as grant/create users
1640
     *
1641
     * @return string
1642
     */
1643
    public function getHtmlForViewUsersError()
1644
    {
1645
        return Message::error(
1646
            __('Not enough privilege to view users.')
1647
        )->getDisplay();
1648
    }
1649
1650
    /**
1651
     * Returns edit, revoke or export link for a user.
1652
     *
1653
     * @param string $linktype    The link type (edit | revoke | export)
1654
     * @param string $username    User name
1655
     * @param string $hostname    Host name
1656
     * @param string $dbname      Database name
1657
     * @param string $tablename   Table name
1658
     * @param string $routinename Routine name
1659
     * @param string $initial     Initial value
1660
     *
1661
     * @return string HTML code with link
1662
     */
1663
    public function getUserLink(
1664
        $linktype,
1665
        $username,
1666
        $hostname,
1667
        $dbname = '',
1668
        $tablename = '',
1669
        $routinename = '',
1670
        $initial = ''
1671
    ) {
1672
        $html = '<a';
1673
        switch ($linktype) {
1674
            case 'edit':
1675
                $html .= ' class="edit_user_anchor"';
1676
                break;
1677
            case 'export':
1678
                $html .= ' class="export_user_anchor ajax"';
1679
                break;
1680
        }
1681
        $params = [
1682
            'username' => $username,
1683
            'hostname' => $hostname,
1684
        ];
1685
        switch ($linktype) {
1686
            case 'edit':
1687
                $params['dbname'] = $dbname;
1688
                $params['tablename'] = $tablename;
1689
                $params['routinename'] = $routinename;
1690
                break;
1691
            case 'revoke':
1692
                $params['dbname'] = $dbname;
1693
                $params['tablename'] = $tablename;
1694
                $params['routinename'] = $routinename;
1695
                $params['revokeall'] = 1;
1696
                break;
1697
            case 'export':
1698
                $params['initial'] = $initial;
1699
                $params['export'] = 1;
1700
                break;
1701
        }
1702
1703
        $html .= ' href="' . Url::getFromRoute('/server/privileges');
1704
        if ($linktype == 'revoke') {
1705
            $html .= '" data-post="' . Url::getCommon($params, '');
1706
        } else {
1707
            $html .= Url::getCommon($params, '&');
1708
        }
1709
        $html .= '">';
1710
1711
        switch ($linktype) {
1712
            case 'edit':
1713
                $html .= Util::getIcon('b_usredit', __('Edit privileges'));
1714
                break;
1715
            case 'revoke':
1716
                $html .= Util::getIcon('b_usrdrop', __('Revoke'));
1717
                break;
1718
            case 'export':
1719
                $html .= Util::getIcon('b_tblexport', __('Export'));
1720
                break;
1721
        }
1722
        $html .= '</a>';
1723
1724
        return $html;
1725
    }
1726
1727
    /**
1728
     * Returns user group edit link
1729
     *
1730
     * @param string $username User name
1731
     *
1732
     * @return string HTML code with link
1733
     */
1734
    public function getUserGroupEditLink($username)
1735
    {
1736
         return '<a class="edit_user_group_anchor ajax"'
1737
            . ' href="' . Url::getFromRoute('/server/privileges', ['username' => $username])
1738
            . '">'
1739
            . Util::getIcon('b_usrlist', __('Edit user group'))
1740
            . '</a>';
1741
    }
1742
1743
    /**
1744
     * Returns number of defined user groups
1745
     *
1746
     * @return integer
1747
     */
1748
    public function getUserGroupCount()
1749
    {
1750
        $cfgRelation = $this->relation->getRelationsParam();
1751
        $user_group_table = Util::backquote($cfgRelation['db'])
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($cfgRelation['db']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

1751
        $user_group_table = /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['db'])
Loading history...
1752
            . '.' . Util::backquote($cfgRelation['usergroups']);
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquo...Relation['usergroups']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

1752
            . '.' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['usergroups']);
Loading history...
1753
        $sql_query = 'SELECT COUNT(*) FROM ' . $user_group_table;
1754
        $user_group_count = $this->dbi->fetchValue(
1755
            $sql_query,
1756
            0,
1757
            0,
1758
            DatabaseInterface::CONNECT_CONTROL
1759
        );
1760
1761
        return $user_group_count;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $user_group_count could also return false which is incompatible with the documented return type integer. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
1762
    }
1763
1764
    /**
1765
     * Returns name of user group that user is part of
1766
     *
1767
     * @param string $username User name
1768
     *
1769
     * @return mixed usergroup if found or null if not found
1770
     */
1771
    public function getUserGroupForUser($username)
1772
    {
1773
        $cfgRelation = $this->relation->getRelationsParam();
1774
1775
        if (empty($cfgRelation['db'])
1776
            || empty($cfgRelation['users'])
1777
        ) {
1778
            return null;
1779
        }
1780
1781
        $user_table = Util::backquote($cfgRelation['db'])
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($cfgRelation['db']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

1781
        $user_table = /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['db'])
Loading history...
1782
            . '.' . Util::backquote($cfgRelation['users']);
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($cfgRelation['users']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

1782
            . '.' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['users']);
Loading history...
1783
        $sql_query = 'SELECT `usergroup` FROM ' . $user_table
1784
            . ' WHERE `username` = \'' . $username . '\''
1785
            . ' LIMIT 1';
1786
1787
        $usergroup = $this->dbi->fetchValue(
1788
            $sql_query,
1789
            0,
1790
            0,
1791
            DatabaseInterface::CONNECT_CONTROL
1792
        );
1793
1794
        if ($usergroup === false) {
1795
            return null;
1796
        }
1797
1798
        return $usergroup;
1799
    }
1800
1801
    /**
1802
     * This function return the extra data array for the ajax behavior
1803
     *
1804
     * @param string $password  password
1805
     * @param string $sql_query sql query
1806
     * @param string $hostname  hostname
1807
     * @param string $username  username
1808
     *
1809
     * @return array
1810
     */
1811
    public function getExtraDataForAjaxBehavior(
1812
        $password,
1813
        $sql_query,
1814
        $hostname,
1815
        $username
1816
    ) {
1817
        if (isset($GLOBALS['dbname'])) {
1818
            //if (preg_match('/\\\\(?:_|%)/i', $dbname)) {
1819
            if (preg_match('/(?<!\\\\)(?:_|%)/', $GLOBALS['dbname'])) {
1820
                $dbname_is_wildcard = true;
1821
            } else {
1822
                $dbname_is_wildcard = false;
1823
            }
1824
        }
1825
1826
        $user_group_count = 0;
1827
        if ($GLOBALS['cfgRelation']['menuswork']) {
1828
            $user_group_count = $this->getUserGroupCount();
1829
        }
1830
1831
        $extra_data = [];
1832
        if (strlen($sql_query) > 0) {
1833
            $extra_data['sql_query'] = Util::getMessage(null, $sql_query);
1834
        }
1835
1836
        if (isset($_POST['change_copy'])) {
1837
            /**
1838
             * generate html on the fly for the new user that was just created.
1839
             */
1840
            $new_user_string = '<tr>' . "\n"
1841
                . '<td> <input type="checkbox" name="selected_usr[]" '
1842
                . 'id="checkbox_sel_users_"'
1843
                . 'value="'
1844
                . htmlspecialchars($username)
1845
                . '&amp;#27;' . htmlspecialchars($hostname) . '">'
1846
                . '</td>' . "\n"
1847
                . '<td><label for="checkbox_sel_users_">'
1848
                . (empty($_POST['username'])
1849
                        ? '<span style="color: #FF0000">' . __('Any') . '</span>'
1850
                        : htmlspecialchars($username) ) . '</label></td>' . "\n"
1851
                . '<td>' . htmlspecialchars($hostname) . '</td>' . "\n";
1852
1853
            $new_user_string .= '<td>';
1854
1855
            if (! empty($password) || isset($_POST['pma_pw'])) {
1856
                $new_user_string .= __('Yes');
1857
            } else {
1858
                $new_user_string .= '<span style="color: #FF0000">'
1859
                    . __('No')
1860
                . '</span>';
1861
            }
1862
1863
            $new_user_string .= '</td>' . "\n";
1864
            $new_user_string .= '<td>'
1865
                . '<code>' . implode(', ', $this->extractPrivInfo(null, true)) . '</code>'
1866
                . '</td>'; //Fill in privileges here
1867
1868
            // if $cfg['Servers'][$i]['users'] and $cfg['Servers'][$i]['usergroups'] are
1869
            // enabled
1870
            $cfgRelation = $this->relation->getRelationsParam();
1871
            if (! empty($cfgRelation['users']) && ! empty($cfgRelation['usergroups'])) {
1872
                $new_user_string .= '<td class="usrGroup"></td>';
1873
            }
1874
1875
            $new_user_string .= '<td>';
1876
            if (isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y') {
1877
                $new_user_string .= __('Yes');
1878
            } else {
1879
                $new_user_string .= __('No');
1880
            }
1881
            $new_user_string .= '</td>';
1882
1883
            if ($GLOBALS['is_grantuser']) {
1884
                $new_user_string .= '<td>'
1885
                    . $this->getUserLink('edit', $username, $hostname)
1886
                    . '</td>' . "\n";
1887
            }
1888
1889
            if ($cfgRelation['menuswork'] && $user_group_count > 0) {
1890
                $new_user_string .= '<td>'
1891
                    . $this->getUserGroupEditLink($username)
1892
                    . '</td>' . "\n";
1893
            }
1894
1895
            $new_user_string .= '<td>'
1896
                . $this->getUserLink(
1897
                    'export',
1898
                    $username,
1899
                    $hostname,
1900
                    '',
1901
                    '',
1902
                    '',
1903
                    isset($_GET['initial']) ? $_GET['initial'] : ''
1904
                )
1905
                . '</td>' . "\n";
1906
1907
            $new_user_string .= '</tr>';
1908
1909
            $extra_data['new_user_string'] = $new_user_string;
1910
1911
            /**
1912
             * Generate the string for this alphabet's initial, to update the user
1913
             * pagination
1914
             */
1915
            $new_user_initial = mb_strtoupper(
1916
                mb_substr($username, 0, 1)
1917
            );
1918
            $newUserInitialString = '<a href="' . Url::getFromRoute('/server/privileges', ['initial' => $new_user_initial]) . '">'
1919
                . $new_user_initial . '</a>';
1920
            $extra_data['new_user_initial'] = $new_user_initial;
1921
            $extra_data['new_user_initial_string'] = $newUserInitialString;
1922
        }
1923
1924
        if (isset($_POST['update_privs'])) {
1925
            $extra_data['db_specific_privs'] = false;
1926
            $extra_data['db_wildcard_privs'] = false;
1927
            if (isset($dbname_is_wildcard)) {
1928
                $extra_data['db_specific_privs'] = ! $dbname_is_wildcard;
1929
                $extra_data['db_wildcard_privs'] = $dbname_is_wildcard;
1930
            }
1931
            $new_privileges = implode(', ', $this->extractPrivInfo(null, true));
1932
1933
            $extra_data['new_privileges'] = $new_privileges;
1934
        }
1935
1936
        if (isset($_GET['validate_username'])) {
1937
            $sql_query = "SELECT * FROM `mysql`.`user` WHERE `User` = '"
1938
                . $_GET['username'] . "';";
1939
            $res = $this->dbi->query($sql_query);
1940
            $row = $this->dbi->fetchRow($res);
0 ignored issues
show
Bug introduced by
It seems like $res can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::fetchRow() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

1940
            $row = $this->dbi->fetchRow(/** @scrutinizer ignore-type */ $res);
Loading history...
1941
            if (empty($row)) {
1942
                $extra_data['user_exists'] = false;
1943
            } else {
1944
                $extra_data['user_exists'] = true;
1945
            }
1946
        }
1947
1948
        return $extra_data;
1949
    }
1950
1951
    /**
1952
     * Get the HTML snippet for change user login information
1953
     *
1954
     * @param string $username username
1955
     * @param string $hostname host name
1956
     *
1957
     * @return string HTML snippet
1958
     */
1959
    public function getChangeLoginInformationHtmlForm($username, $hostname)
1960
    {
1961
        $choices = [
1962
            '4' => __('… keep the old one.'),
1963
            '1' => __('… delete the old one from the user tables.'),
1964
            '2' => __(
1965
                '… revoke all active privileges from '
1966
                . 'the old one and delete it afterwards.'
1967
            ),
1968
            '3' => __(
1969
                '… delete the old one from the user tables '
1970
                . 'and reload the privileges afterwards.'
1971
            ),
1972
        ];
1973
1974
        $html_output = '<form action="' . Url::getFromRoute('/server/privileges')
1975
            . '" onsubmit="return checkAddUser(this);" '
1976
            . 'method="post" class="copyUserForm submenu-item">' . "\n"
1977
            . Url::getHiddenInputs('', '')
1978
            . '<input type="hidden" name="old_username" '
1979
            . 'value="' . htmlspecialchars($username) . '">' . "\n"
1980
            . '<input type="hidden" name="old_hostname" '
1981
            . 'value="' . htmlspecialchars($hostname) . '">' . "\n";
1982
1983
        $usergroup = $this->getUserGroupForUser($username);
1984
        if ($usergroup !== null) {
1985
            $html_output .= '<input type="hidden" name="old_usergroup" '
1986
            . 'value="' . htmlspecialchars($usergroup) . '">' . "\n";
1987
        }
1988
1989
        $html_output .= '<fieldset id="fieldset_change_copy_user">' . "\n"
1990
            . '<legend data-submenu-label="' . __('Login Information') . '">' . "\n"
1991
            . __('Change login information / Copy user account')
1992
            . '</legend>' . "\n"
1993
            . $this->getHtmlForLoginInformationFields('change', $username, $hostname);
1994
1995
        $html_output .= '<fieldset id="fieldset_mode">' . "\n"
1996
            . ' <legend>'
1997
            . __('Create a new user account with the same privileges and …')
1998
            . '</legend>' . "\n";
1999
        $html_output .= Util::getRadioFields(
2000
            'mode',
2001
            $choices,
2002
            '4',
2003
            true
2004
        );
2005
        $html_output .= '</fieldset>' . "\n"
2006
           . '</fieldset>' . "\n";
2007
2008
        $html_output .= '<fieldset id="fieldset_change_copy_user_footer" '
2009
            . 'class="tblFooters">' . "\n"
2010
            . '<input type="hidden" name="change_copy" value="1">' . "\n"
2011
            . '<input class="btn btn-primary" type="submit" value="' . __('Go') . '">' . "\n"
2012
            . '</fieldset>' . "\n"
2013
            . '</form>' . "\n";
2014
2015
        return $html_output;
2016
    }
2017
2018
    /**
2019
     * Provide a line with links to the relevant database and table
2020
     *
2021
     * @param string $url_dbname url database name that urlencode() string
2022
     * @param string $dbname     database name
2023
     * @param string $tablename  table name
2024
     *
2025
     * @return string HTML snippet
2026
     */
2027
    public function getLinkToDbAndTable($url_dbname, $dbname, $tablename)
2028
    {
2029
        $scriptName = Util::getScriptNameForOption(
2030
            $GLOBALS['cfg']['DefaultTabDatabase'],
2031
            'database'
2032
        );
2033
        $html_output = '[ ' . __('Database')
2034
            . ' <a href="' . $scriptName
2035
            . Url::getCommon([
2036
                'db' => $url_dbname,
2037
                'reload' => 1,
2038
            ], strpos($scriptName, '?') === false ? '?' : '&')
2039
            . '">'
2040
            . htmlspecialchars(Util::unescapeMysqlWildcards($dbname)) . ': '
2041
            . Util::getTitleForTarget(
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::getTitl...['DefaultTabDatabase']) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

2041
            . /** @scrutinizer ignore-type */ Util::getTitleForTarget(
Loading history...
2042
                $GLOBALS['cfg']['DefaultTabDatabase']
2043
            )
2044
            . "</a> ]\n";
2045
2046
        if (strlen($tablename) > 0) {
2047
            $scriptName = Util::getScriptNameForOption(
2048
                $GLOBALS['cfg']['DefaultTabTable'],
2049
                'table'
2050
            );
2051
            $html_output .= ' [ ' . __('Table') . ' <a href="'
2052
                . $scriptName
2053
                . Url::getCommon([
2054
                    'db' => $url_dbname,
2055
                    'table' => $tablename,
2056
                    'reload' => 1,
2057
                ], strpos($scriptName, '?') === false ? '?' : '&')
2058
                . '">' . htmlspecialchars($tablename) . ': '
2059
                . Util::getTitleForTarget(
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::getTitl...g']['DefaultTabTable']) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

2059
                . /** @scrutinizer ignore-type */ Util::getTitleForTarget(
Loading history...
2060
                    $GLOBALS['cfg']['DefaultTabTable']
2061
                )
2062
                . "</a> ]\n";
2063
        }
2064
        return $html_output;
2065
    }
2066
2067
    /**
2068
     * no db name given, so we want all privs for the given user
2069
     * db name was given, so we want all user specific rights for this db
2070
     * So this function returns user rights as an array
2071
     *
2072
     * @param string $username username
2073
     * @param string $hostname host name
2074
     * @param string $type     database or table
2075
     * @param string $dbname   database name
2076
     *
2077
     * @return array database rights
2078
     */
2079
    public function getUserSpecificRights($username, $hostname, $type, $dbname = '')
2080
    {
2081
        $user_host_condition = " WHERE `User`"
2082
            . " = '" . $this->dbi->escapeString($username) . "'"
2083
            . " AND `Host`"
2084
            . " = '" . $this->dbi->escapeString($hostname) . "'";
2085
2086
        if ($type == 'database') {
2087
            $tables_to_search_for_users = [
2088
                'tables_priv',
2089
                'columns_priv',
2090
                'procs_priv',
2091
            ];
2092
            $dbOrTableName = 'Db';
2093
        } elseif ($type == 'table') {
2094
            $user_host_condition .= " AND `Db` LIKE '"
2095
                . $this->dbi->escapeString($dbname) . "'";
2096
            $tables_to_search_for_users = ['columns_priv'];
2097
            $dbOrTableName = 'Table_name';
2098
        } else { // routine
2099
            $user_host_condition .= " AND `Db` LIKE '"
2100
                . $this->dbi->escapeString($dbname) . "'";
2101
            $tables_to_search_for_users = ['procs_priv'];
2102
            $dbOrTableName = 'Routine_name';
2103
        }
2104
2105
        // we also want privileges for this user not in table `db` but in other table
2106
        $tables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`;');
2107
2108
        $db_rights_sqls = [];
2109
        foreach ($tables_to_search_for_users as $table_search_in) {
2110
            if (in_array($table_search_in, $tables)) {
2111
                $db_rights_sqls[] = '
2112
                    SELECT DISTINCT `' . $dbOrTableName . '`
2113
                    FROM `mysql`.' . Util::backquote($table_search_in)
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($table_search_in) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

2113
                    FROM `mysql`.' . /** @scrutinizer ignore-type */ Util::backquote($table_search_in)
Loading history...
2114
                   . $user_host_condition;
2115
            }
2116
        }
2117
2118
        $user_defaults = [
2119
            $dbOrTableName  => '',
2120
            'Grant_priv'    => 'N',
2121
            'privs'         => ['USAGE'],
2122
            'Column_priv'   => true,
2123
        ];
2124
2125
        // for the rights
2126
        $db_rights = [];
2127
2128
        $db_rights_sql = '(' . implode(') UNION (', $db_rights_sqls) . ')'
2129
            . ' ORDER BY `' . $dbOrTableName . '` ASC';
2130
2131
        $db_rights_result = $this->dbi->query($db_rights_sql);
2132
2133
        while ($db_rights_row = $this->dbi->fetchAssoc($db_rights_result)) {
0 ignored issues
show
Bug introduced by
It seems like $db_rights_result can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::fetchAssoc() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

2133
        while ($db_rights_row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $db_rights_result)) {
Loading history...
2134
            $db_rights_row = array_merge($user_defaults, $db_rights_row);
2135
            if ($type == 'database') {
2136
                // only Db names in the table `mysql`.`db` uses wildcards
2137
                // as we are in the db specific rights display we want
2138
                // all db names escaped, also from other sources
2139
                $db_rights_row['Db'] = Util::escapeMysqlWildcards(
2140
                    $db_rights_row['Db']
2141
                );
2142
            }
2143
            $db_rights[$db_rights_row[$dbOrTableName]] = $db_rights_row;
2144
        }
2145
2146
        $this->dbi->freeResult($db_rights_result);
0 ignored issues
show
Bug introduced by
It seems like $db_rights_result can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::freeResult() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

2146
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $db_rights_result);
Loading history...
2147
2148
        if ($type == 'database') {
2149
            $sql_query = 'SELECT * FROM `mysql`.`db`'
2150
                . $user_host_condition . ' ORDER BY `Db` ASC';
2151
        } elseif ($type == 'table') {
2152
            $sql_query = 'SELECT `Table_name`,'
2153
                . ' `Table_priv`,'
2154
                . ' IF(`Column_priv` = _latin1 \'\', 0, 1)'
2155
                . ' AS \'Column_priv\''
2156
                . ' FROM `mysql`.`tables_priv`'
2157
                . $user_host_condition
2158
                . ' ORDER BY `Table_name` ASC;';
2159
        } else {
2160
            $sql_query = "SELECT `Routine_name`, `Proc_priv`"
2161
                . " FROM `mysql`.`procs_priv`"
2162
                . $user_host_condition
2163
                . " ORDER BY `Routine_name`";
2164
        }
2165
2166
        $result = $this->dbi->query($sql_query);
2167
2168
        while ($row = $this->dbi->fetchAssoc($result)) {
2169
            if (isset($db_rights[$row[$dbOrTableName]])) {
2170
                $db_rights[$row[$dbOrTableName]]
2171
                    = array_merge($db_rights[$row[$dbOrTableName]], $row);
2172
            } else {
2173
                $db_rights[$row[$dbOrTableName]] = $row;
2174
            }
2175
            if ($type == 'database') {
2176
                // there are db specific rights for this user
2177
                // so we can drop this db rights
2178
                $db_rights[$row['Db']]['can_delete'] = true;
2179
            }
2180
        }
2181
        $this->dbi->freeResult($result);
2182
        return $db_rights;
2183
    }
2184
2185
    /**
2186
     * Parses Proc_priv data
2187
     *
2188
     * @param string $privs Proc_priv
2189
     *
2190
     * @return array
2191
     */
2192
    public function parseProcPriv($privs)
2193
    {
2194
        $result = [
2195
            'Alter_routine_priv' => 'N',
2196
            'Execute_priv'       => 'N',
2197
            'Grant_priv'         => 'N',
2198
        ];
2199
        foreach (explode(',', (string) $privs) as $priv) {
2200
            if ($priv == 'Alter Routine') {
2201
                $result['Alter_routine_priv'] = 'Y';
2202
            } else {
2203
                $result[$priv . '_priv'] = 'Y';
2204
            }
2205
        }
2206
        return $result;
2207
    }
2208
2209
    /**
2210
     * Get a HTML table for display user's tabel specific or database specific rights
2211
     *
2212
     * @param string $username username
2213
     * @param string $hostname host name
2214
     * @param string $type     database, table or routine
2215
     * @param string $dbname   database name
2216
     *
2217
     * @return string
2218
     */
2219
    public function getHtmlForAllTableSpecificRights(
2220
        $username,
2221
        $hostname,
2222
        $type,
2223
        $dbname = ''
2224
    ) {
2225
        $uiData = [
2226
            'database' => [
2227
                'form_id'        => 'database_specific_priv',
2228
                'sub_menu_label' => __('Database'),
2229
                'legend'         => __('Database-specific privileges'),
2230
                'type_label'     => __('Database'),
2231
            ],
2232
            'table' => [
2233
                'form_id'        => 'table_specific_priv',
2234
                'sub_menu_label' => __('Table'),
2235
                'legend'         => __('Table-specific privileges'),
2236
                'type_label'     => __('Table'),
2237
            ],
2238
            'routine' => [
2239
                'form_id'        => 'routine_specific_priv',
2240
                'sub_menu_label' => __('Routine'),
2241
                'legend'         => __('Routine-specific privileges'),
2242
                'type_label'     => __('Routine'),
2243
            ],
2244
        ];
2245
2246
        /**
2247
         * no db name given, so we want all privs for the given user
2248
         * db name was given, so we want all user specific rights for this db
2249
         */
2250
        $db_rights = $this->getUserSpecificRights($username, $hostname, $type, $dbname);
2251
        ksort($db_rights);
2252
2253
        $foundRows = [];
2254
        $privileges = [];
2255
        foreach ($db_rights as $row) {
2256
            $onePrivilege = [];
2257
2258
            $paramTableName = '';
2259
            $paramRoutineName = '';
2260
2261
            if ($type == 'database') {
2262
                $name = $row['Db'];
2263
                $onePrivilege['grant']        = $row['Grant_priv'] == 'Y';
2264
                $onePrivilege['table_privs']   = ! empty($row['Table_priv'])
2265
                    || ! empty($row['Column_priv']);
2266
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
2267
2268
                $paramDbName = $row['Db'];
2269
            } elseif ($type == 'table') {
2270
                $name = $row['Table_name'];
2271
                $onePrivilege['grant'] = in_array(
2272
                    'Grant',
2273
                    explode(',', $row['Table_priv'])
2274
                );
2275
                $onePrivilege['column_privs']  = ! empty($row['Column_priv']);
2276
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
2277
2278
                $paramDbName = $dbname;
2279
                $paramTableName = $row['Table_name'];
2280
            } else { // routine
2281
                $name = $row['Routine_name'];
2282
                $onePrivilege['grant'] = in_array(
2283
                    'Grant',
2284
                    explode(',', $row['Proc_priv'])
2285
                );
2286
2287
                $privs = $this->parseProcPriv($row['Proc_priv']);
2288
                $onePrivilege['privileges'] = implode(
2289
                    ',',
2290
                    $this->extractPrivInfo($privs, true)
2291
                );
2292
2293
                $paramDbName = $dbname;
2294
                $paramRoutineName = $row['Routine_name'];
2295
            }
2296
2297
            $foundRows[] = $name;
2298
            $onePrivilege['name'] = $name;
2299
2300
            $onePrivilege['edit_link'] = '';
2301
            if ($GLOBALS['is_grantuser']) {
2302
                $onePrivilege['edit_link'] = $this->getUserLink(
2303
                    'edit',
2304
                    $username,
2305
                    $hostname,
2306
                    $paramDbName,
2307
                    $paramTableName,
2308
                    $paramRoutineName
2309
                );
2310
            }
2311
2312
            $onePrivilege['revoke_link'] = '';
2313
            if ($type != 'database' || ! empty($row['can_delete'])) {
2314
                $onePrivilege['revoke_link'] = $this->getUserLink(
2315
                    'revoke',
2316
                    $username,
2317
                    $hostname,
2318
                    $paramDbName,
2319
                    $paramTableName,
2320
                    $paramRoutineName
2321
                );
2322
            }
2323
2324
            $privileges[] = $onePrivilege;
2325
        }
2326
2327
        $data = $uiData[$type];
2328
        $data['privileges'] = $privileges;
2329
        $data['username']   = $username;
2330
        $data['hostname']   = $hostname;
2331
        $data['database']   = $dbname;
2332
        $data['type']       = $type;
2333
2334
        if ($type == 'database') {
2335
            // we already have the list of databases from libraries/common.inc.php
2336
            // via $pma = new PMA;
2337
            $pred_db_array = $GLOBALS['dblist']->databases;
2338
            $databases_to_skip = [
2339
                'information_schema',
2340
                'performance_schema',
2341
            ];
2342
2343
            $databases = [];
2344
            if (! empty($pred_db_array)) {
2345
                foreach ($pred_db_array as $current_db) {
2346
                    if (in_array($current_db, $databases_to_skip)) {
2347
                        continue;
2348
                    }
2349
                    $current_db_escaped = Util::escapeMysqlWildcards($current_db);
2350
                    // cannot use array_diff() once, outside of the loop,
2351
                    // because the list of databases has special characters
2352
                    // already escaped in $foundRows,
2353
                    // contrary to the output of SHOW DATABASES
2354
                    if (! in_array($current_db_escaped, $foundRows)) {
2355
                        $databases[] = $current_db;
2356
                    }
2357
                }
2358
            }
2359
            $data['databases'] = $databases;
2360
        } elseif ($type == 'table') {
2361
            $result = @$this->dbi->tryQuery(
2362
                "SHOW TABLES FROM " . Util::backquote($dbname),
2363
                DatabaseInterface::CONNECT_USER,
2364
                DatabaseInterface::QUERY_STORE
2365
            );
2366
2367
            $tables = [];
2368
            if ($result) {
2369
                while ($row = $this->dbi->fetchRow($result)) {
2370
                    if (! in_array($row[0], $foundRows)) {
2371
                        $tables[] = $row[0];
2372
                    }
2373
                }
2374
                $this->dbi->freeResult($result);
2375
            }
2376
            $data['tables'] = $tables;
2377
        } else { // routine
2378
            $routineData = $this->dbi->getRoutines($dbname);
2379
2380
            $routines = [];
2381
            foreach ($routineData as $routine) {
2382
                if (! in_array($routine['name'], $foundRows)) {
2383
                    $routines[] = $routine['name'];
2384
                }
2385
            }
2386
            $data['routines'] = $routines;
2387
        }
2388
2389
        return $this->template->render('server/privileges/privileges_summary', $data);
2390
    }
2391
2392
    /**
2393
     * Get HTML for display the users overview
2394
     * (if less than 50 users, display them immediately)
2395
     *
2396
     * @param array  $result        ran sql query
2397
     * @param array  $db_rights     user's database rights array
2398
     * @param string $pmaThemeImage a image source link
2399
     * @param string $text_dir      text directory
2400
     *
2401
     * @return string HTML snippet
2402
     */
2403
    public function getUsersOverview($result, array $db_rights, $pmaThemeImage, $text_dir)
2404
    {
2405
        global $is_grantuser, $is_createuser;
2406
2407
        $cfgRelation = $this->relation->getRelationsParam();
2408
2409
        while ($row = $this->dbi->fetchAssoc($result)) {
0 ignored issues
show
Bug introduced by
$result of type array is incompatible with the type object expected by parameter $result of PhpMyAdmin\DatabaseInterface::fetchAssoc(). ( Ignorable by Annotation )

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

2409
        while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $result)) {
Loading history...
2410
            $row['privs'] = $this->extractPrivInfo($row, true);
2411
            $db_rights[$row['User']][$row['Host']] = $row;
2412
        }
2413
        $this->dbi->freeResult($result);
0 ignored issues
show
Bug introduced by
$result of type array is incompatible with the type object expected by parameter $result of PhpMyAdmin\DatabaseInterface::freeResult(). ( Ignorable by Annotation )

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

2413
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
2414
2415
        $user_group_count = 0;
2416
        if ($cfgRelation['menuswork']) {
2417
            $sql_query = 'SELECT * FROM ' . Util::backquote($cfgRelation['db'])
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($cfgRelation['db']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

2417
            $sql_query = 'SELECT * FROM ' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['db'])
Loading history...
2418
                . '.' . Util::backquote($cfgRelation['users']);
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($cfgRelation['users']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

2418
                . '.' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['users']);
Loading history...
2419
            $result = $this->relation->queryAsControlUser($sql_query, false);
2420
            $group_assignment = [];
2421
            if ($result) {
2422
                while ($row = $this->dbi->fetchAssoc($result)) {
0 ignored issues
show
Bug introduced by
$result of type resource|true is incompatible with the type object expected by parameter $result of PhpMyAdmin\DatabaseInterface::fetchAssoc(). ( Ignorable by Annotation )

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

2422
                while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $result)) {
Loading history...
2423
                    $group_assignment[$row['username']] = $row['usergroup'];
2424
                }
2425
            }
2426
            $this->dbi->freeResult($result);
0 ignored issues
show
Bug introduced by
$result of type boolean|resource is incompatible with the type object expected by parameter $result of PhpMyAdmin\DatabaseInterface::freeResult(). ( Ignorable by Annotation )

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

2426
            $this->dbi->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
2427
2428
            $user_group_count = $this->getUserGroupCount();
2429
        }
2430
2431
        $hosts = [];
2432
        foreach ($db_rights as $user) {
2433
            ksort($user);
2434
            foreach ($user as $host) {
2435
                $check_plugin_query = "SELECT * FROM `mysql`.`user` WHERE "
2436
                    . "`User` = '" . $host['User'] . "' AND `Host` = '"
2437
                    . $host['Host'] . "'";
2438
                $res = $this->dbi->fetchSingleRow($check_plugin_query);
2439
2440
                $hasPassword = false;
2441
                if ((isset($res['authentication_string'])
2442
                    && ! empty($res['authentication_string']))
2443
                    || (isset($res['Password'])
2444
                    && ! empty($res['Password']))
2445
                ) {
2446
                    $hasPassword = true;
2447
                }
2448
2449
                $hosts[] = [
2450
                    'user' => $host['User'],
2451
                    'host' => $host['Host'],
2452
                    'has_password' => $hasPassword,
2453
                    'has_select_priv' => isset($host['Select_priv']),
2454
                    'privileges' => $host['privs'],
2455
                    'group' => $group_assignment[$host['User']] ?? '',
2456
                    'has_grant' => $host['Grant_priv'] == 'Y',
2457
                ];
2458
            }
2459
        }
2460
2461
        return $this->template->render('server/privileges/users_overview', [
2462
            'menus_work' => $cfgRelation['menuswork'],
2463
            'user_group_count' => $user_group_count,
2464
            'pma_theme_image' => $pmaThemeImage,
2465
            'text_dir' => $text_dir,
2466
            'initial' => $_GET['initial'] ?? '',
2467
            'hosts' => $hosts,
2468
            'is_grantuser' => $is_grantuser,
2469
            'is_createuser' => $is_createuser,
2470
        ]);
2471
    }
2472
2473
    /**
2474
     * Get HTML for Displays the initials
2475
     *
2476
     * @param array $array_initials array for all initials, even non A-Z
2477
     *
2478
     * @return string HTML snippet
2479
     */
2480
    public function getHtmlForInitials(array $array_initials)
2481
    {
2482
        // initialize to false the letters A-Z
2483
        for ($letter_counter = 1; $letter_counter < 27; $letter_counter++) {
2484
            if (! isset($array_initials[mb_chr($letter_counter + 64)])) {
0 ignored issues
show
Bug introduced by
The call to mb_chr() has too few arguments starting with encoding. ( Ignorable by Annotation )

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

2484
            if (! isset($array_initials[/** @scrutinizer ignore-call */ mb_chr($letter_counter + 64)])) {

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
2485
                $array_initials[mb_chr($letter_counter + 64)] = false;
2486
            }
2487
        }
2488
2489
        $initials = $this->dbi->tryQuery(
2490
            'SELECT DISTINCT UPPER(LEFT(`User`,1)) FROM `user`'
2491
            . ' ORDER BY UPPER(LEFT(`User`,1)) ASC',
2492
            DatabaseInterface::CONNECT_USER,
2493
            DatabaseInterface::QUERY_STORE
2494
        );
2495
        if ($initials) {
2496
            while (list($tmp_initial) = $this->dbi->fetchRow($initials)) {
2497
                $array_initials[$tmp_initial] = true;
2498
            }
2499
        }
2500
2501
        // Display the initials, which can be any characters, not
2502
        // just letters. For letters A-Z, we add the non-used letters
2503
        // as greyed out.
2504
2505
        uksort($array_initials, "strnatcasecmp");
2506
2507
        return $this->template->render('server/privileges/initials_row', [
2508
            'array_initials' => $array_initials,
2509
            'initial' => isset($_GET['initial']) ? $_GET['initial'] : null,
2510
        ]);
2511
    }
2512
2513
    /**
2514
     * Get the database rights array for Display user overview
2515
     *
2516
     * @return array    database rights array
2517
     */
2518
    public function getDbRightsForUserOverview()
2519
    {
2520
        // we also want users not in table `user` but in other table
2521
        $tables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`;');
2522
2523
        $tablesSearchForUsers = [
2524
            'user',
2525
            'db',
2526
            'tables_priv',
2527
            'columns_priv',
2528
            'procs_priv',
2529
        ];
2530
2531
        $db_rights_sqls = [];
2532
        foreach ($tablesSearchForUsers as $table_search_in) {
2533
            if (in_array($table_search_in, $tables)) {
2534
                $db_rights_sqls[] = 'SELECT DISTINCT `User`, `Host` FROM `mysql`.`'
2535
                    . $table_search_in . '` '
2536
                    . (isset($_GET['initial'])
2537
                    ? $this->rangeOfUsers($_GET['initial'])
2538
                    : '');
2539
            }
2540
        }
2541
        $user_defaults = [
2542
            'User'       => '',
2543
            'Host'       => '%',
2544
            'Password'   => '?',
2545
            'Grant_priv' => 'N',
2546
            'privs'      => ['USAGE'],
2547
        ];
2548
2549
        // for the rights
2550
        $db_rights = [];
2551
2552
        $db_rights_sql = '(' . implode(') UNION (', $db_rights_sqls) . ')'
2553
            . ' ORDER BY `User` ASC, `Host` ASC';
2554
2555
        $db_rights_result = $this->dbi->query($db_rights_sql);
2556
2557
        while ($db_rights_row = $this->dbi->fetchAssoc($db_rights_result)) {
0 ignored issues
show
Bug introduced by
It seems like $db_rights_result can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::fetchAssoc() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

2557
        while ($db_rights_row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $db_rights_result)) {
Loading history...
2558
            $db_rights_row = array_merge($user_defaults, $db_rights_row);
2559
            $db_rights[$db_rights_row['User']][$db_rights_row['Host']]
2560
                = $db_rights_row;
2561
        }
2562
        $this->dbi->freeResult($db_rights_result);
0 ignored issues
show
Bug introduced by
It seems like $db_rights_result can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::freeResult() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

2562
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $db_rights_result);
Loading history...
2563
        ksort($db_rights);
2564
2565
        return $db_rights;
2566
    }
2567
2568
    /**
2569
     * Delete user and get message and sql query for delete user in privileges
2570
     *
2571
     * @param array $queries queries
2572
     *
2573
     * @return array Message
2574
     */
2575
    public function deleteUser(array $queries)
2576
    {
2577
        $sql_query = '';
2578
        if (empty($queries)) {
2579
            $message = Message::error(__('No users selected for deleting!'));
2580
        } else {
2581
            if ($_POST['mode'] == 3) {
2582
                $queries[] = '# ' . __('Reloading the privileges') . ' …';
2583
                $queries[] = 'FLUSH PRIVILEGES;';
2584
            }
2585
            $drop_user_error = '';
2586
            foreach ($queries as $sql_query) {
2587
                if ($sql_query[0] != '#') {
2588
                    if (! $this->dbi->tryQuery($sql_query)) {
2589
                        $drop_user_error .= $this->dbi->getError() . "\n";
2590
                    }
2591
                }
2592
            }
2593
            // tracking sets this, causing the deleted db to be shown in navi
2594
            unset($GLOBALS['db']);
2595
2596
            $sql_query = implode("\n", $queries);
2597
            if (! empty($drop_user_error)) {
2598
                $message = Message::rawError($drop_user_error);
2599
            } else {
2600
                $message = Message::success(
2601
                    __('The selected users have been deleted successfully.')
2602
                );
2603
            }
2604
        }
2605
        return [
2606
            $sql_query,
2607
            $message,
2608
        ];
2609
    }
2610
2611
    /**
2612
     * Update the privileges and return the success or error message
2613
     *
2614
     * @param string $username  username
2615
     * @param string $hostname  host name
2616
     * @param string $tablename table name
2617
     * @param string $dbname    database name
2618
     * @param string $itemType  item type
2619
     *
2620
     * @return array success message or error message for update
2621
     */
2622
    public function updatePrivileges($username, $hostname, $tablename, $dbname, $itemType)
2623
    {
2624
        $db_and_table = $this->wildcardEscapeForGrant($dbname, $tablename);
2625
2626
        $sql_query0 = 'REVOKE ALL PRIVILEGES ON ' . $itemType . ' ' . $db_and_table
2627
            . ' FROM \'' . $this->dbi->escapeString($username)
2628
            . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
2629
2630
        if (! isset($_POST['Grant_priv']) || $_POST['Grant_priv'] != 'Y') {
2631
            $sql_query1 = 'REVOKE GRANT OPTION ON ' . $itemType . ' ' . $db_and_table
2632
                . ' FROM \'' . $this->dbi->escapeString($username) . '\'@\''
2633
                . $this->dbi->escapeString($hostname) . '\';';
2634
        } else {
2635
            $sql_query1 = '';
2636
        }
2637
2638
        // Should not do a GRANT USAGE for a table-specific privilege, it
2639
        // causes problems later (cannot revoke it)
2640
        if (! (strlen($tablename) > 0
2641
            && 'USAGE' == implode('', $this->extractPrivInfo()))
2642
        ) {
2643
            $sql_query2 = 'GRANT ' . implode(', ', $this->extractPrivInfo())
2644
                . ' ON ' . $itemType . ' ' . $db_and_table
2645
                . ' TO \'' . $this->dbi->escapeString($username) . '\'@\''
2646
                . $this->dbi->escapeString($hostname) . '\'';
2647
2648
            if (strlen($dbname) === 0) {
2649
                // add REQUIRE clause
2650
                $sql_query2 .= $this->getRequireClause();
2651
            }
2652
2653
            if ((isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y')
2654
                || (strlen($dbname) === 0
2655
                && (isset($_POST['max_questions']) || isset($_POST['max_connections'])
2656
                || isset($_POST['max_updates'])
2657
                || isset($_POST['max_user_connections'])))
2658
            ) {
2659
                $sql_query2 .= $this->getWithClauseForAddUserAndUpdatePrivs();
2660
            }
2661
            $sql_query2 .= ';';
2662
        }
2663
        if (! $this->dbi->tryQuery($sql_query0)) {
2664
            // This might fail when the executing user does not have
2665
            // ALL PRIVILEGES himself.
2666
            // See https://github.com/phpmyadmin/phpmyadmin/issues/9673
2667
            $sql_query0 = '';
2668
        }
2669
        if (! empty($sql_query1) && ! $this->dbi->tryQuery($sql_query1)) {
2670
            // this one may fail, too...
2671
            $sql_query1 = '';
2672
        }
2673
        if (! empty($sql_query2)) {
2674
            $this->dbi->query($sql_query2);
2675
        } else {
2676
            $sql_query2 = '';
2677
        }
2678
        $sql_query = $sql_query0 . ' ' . $sql_query1 . ' ' . $sql_query2;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sql_query2 does not seem to be defined for all execution paths leading up to this point.
Loading history...
2679
        $message = Message::success(__('You have updated the privileges for %s.'));
2680
        $message->addParam('\'' . $username . '\'@\'' . $hostname . '\'');
2681
2682
        return [
2683
            $sql_query,
2684
            $message,
2685
        ];
2686
    }
2687
2688
    /**
2689
     * Get List of information: Changes / copies a user
2690
     *
2691
     * @return array
2692
     */
2693
    public function getDataForChangeOrCopyUser()
2694
    {
2695
        $queries = null;
2696
        $password = null;
2697
2698
        if (isset($_POST['change_copy'])) {
2699
            $user_host_condition = ' WHERE `User` = '
2700
                . "'" . $this->dbi->escapeString($_POST['old_username']) . "'"
2701
                . ' AND `Host` = '
2702
                . "'" . $this->dbi->escapeString($_POST['old_hostname']) . "';";
2703
            $row = $this->dbi->fetchSingleRow(
2704
                'SELECT * FROM `mysql`.`user` ' . $user_host_condition
2705
            );
2706
            if (! $row) {
2707
                $response = Response::getInstance();
2708
                $response->addHTML(
2709
                    Message::notice(__('No user found.'))->getDisplay()
2710
                );
2711
                unset($_POST['change_copy']);
2712
            } else {
2713
                foreach ($row as $key => $value) {
2714
                    $GLOBALS[$key] = $value;
2715
                }
2716
                $serverVersion = $this->dbi->getVersion();
2717
                // Recent MySQL versions have the field "Password" in mysql.user,
2718
                // so the previous extract creates $row['Password'] but this script
2719
                // uses $password
2720
                if (! isset($row['password']) && isset($row['Password'])) {
2721
                    $row['password'] = $row['Password'];
2722
                }
2723
                if (Util::getServerType() == 'MySQL'
2724
                    && $serverVersion >= 50606
2725
                    && $serverVersion < 50706
2726
                    && ((isset($row['authentication_string'])
2727
                    && empty($row['password']))
2728
                    || (isset($row['plugin'])
2729
                    && $row['plugin'] == 'sha256_password'))
2730
                ) {
2731
                    $row['password'] = $row['authentication_string'];
2732
                }
2733
2734
                if (Util::getServerType() == 'MariaDB'
2735
                    && $serverVersion >= 50500
2736
                    && isset($row['authentication_string'])
2737
                    && empty($row['password'])
2738
                ) {
2739
                    $row['password'] = $row['authentication_string'];
2740
                }
2741
2742
                // Always use 'authentication_string' column
2743
                // for MySQL 5.7.6+ since it does not have
2744
                // the 'password' column at all
2745
                if (in_array(Util::getServerType(), ['MySQL', 'Percona Server'])
2746
                    && $serverVersion >= 50706
2747
                    && isset($row['authentication_string'])
2748
                ) {
2749
                    $row['password'] = $row['authentication_string'];
2750
                }
2751
                $password = $row['password'];
2752
                $queries = [];
2753
            }
2754
        }
2755
2756
        return [
2757
            $queries,
2758
            $password,
2759
        ];
2760
    }
2761
2762
    /**
2763
     * Update Data for information: Deletes users
2764
     *
2765
     * @param array $queries queries array
2766
     *
2767
     * @return array
2768
     */
2769
    public function getDataForDeleteUsers($queries)
2770
    {
2771
        if (isset($_POST['change_copy'])) {
2772
            $selected_usr = [
2773
                $_POST['old_username'] . '&amp;#27;' . $_POST['old_hostname'],
2774
            ];
2775
        } else {
2776
            $selected_usr = $_POST['selected_usr'];
2777
            $queries = [];
2778
        }
2779
2780
        // this happens, was seen in https://reports.phpmyadmin.net/reports/view/17146
2781
        if (! is_array($selected_usr)) {
2782
            return [];
2783
        }
2784
2785
        foreach ($selected_usr as $each_user) {
2786
            list($this_user, $this_host) = explode('&amp;#27;', $each_user);
2787
            $queries[] = '# '
2788
                . sprintf(
2789
                    __('Deleting %s'),
2790
                    '\'' . $this_user . '\'@\'' . $this_host . '\''
2791
                )
2792
                . ' ...';
2793
            $queries[] = 'DROP USER \''
2794
                . $this->dbi->escapeString($this_user)
2795
                . '\'@\'' . $this->dbi->escapeString($this_host) . '\';';
2796
            $this->relationCleanup->user($this_user);
2797
2798
            if (isset($_POST['drop_users_db'])) {
2799
                $queries[] = 'DROP DATABASE IF EXISTS '
2800
                    . Util::backquote($this_user) . ';';
2801
                $GLOBALS['reload'] = true;
2802
            }
2803
        }
2804
        return $queries;
2805
    }
2806
2807
    /**
2808
     * update Message For Reload
2809
     *
2810
     * @return Message|null
2811
     */
2812
    public function updateMessageForReload(): ?Message
2813
    {
2814
        $message = null;
2815
        if (isset($_GET['flush_privileges'])) {
2816
            $sql_query = 'FLUSH PRIVILEGES;';
2817
            $this->dbi->query($sql_query);
2818
            $message = Message::success(
2819
                __('The privileges were reloaded successfully.')
2820
            );
2821
        }
2822
2823
        if (isset($_GET['validate_username'])) {
2824
            $message = Message::success();
2825
        }
2826
2827
        return $message;
2828
    }
2829
2830
    /**
2831
     * update Data For Queries from queries_for_display
2832
     *
2833
     * @param array      $queries             queries array
2834
     * @param array|null $queries_for_display queries array for display
2835
     *
2836
     * @return array
2837
     */
2838
    public function getDataForQueries(array $queries, $queries_for_display)
2839
    {
2840
        $tmp_count = 0;
2841
        foreach ($queries as $sql_query) {
2842
            if ($sql_query[0] != '#') {
2843
                $this->dbi->query($sql_query);
2844
            }
2845
            // when there is a query containing a hidden password, take it
2846
            // instead of the real query sent
2847
            if (isset($queries_for_display[$tmp_count])) {
2848
                $queries[$tmp_count] = $queries_for_display[$tmp_count];
2849
            }
2850
            $tmp_count++;
2851
        }
2852
2853
        return $queries;
2854
    }
2855
2856
    /**
2857
     * update Data for information: Adds a user
2858
     *
2859
     * @param string|array|null $dbname      db name
2860
     * @param string            $username    user name
2861
     * @param string            $hostname    host name
2862
     * @param string|null       $password    password
2863
     * @param bool              $is_menuwork is_menuwork set?
2864
     *
2865
     * @return array
2866
     */
2867
    public function addUser(
2868
        $dbname,
2869
        $username,
2870
        $hostname,
2871
        ?string $password,
2872
        $is_menuwork
2873
    ) {
2874
        $_add_user_error = false;
2875
        $message = null;
2876
        $queries = null;
2877
        $queries_for_display = null;
2878
        $sql_query = null;
2879
2880
        if (! isset($_POST['adduser_submit']) && ! isset($_POST['change_copy'])) {
2881
            return [
2882
                $message,
2883
                $queries,
2884
                $queries_for_display,
2885
                $sql_query,
2886
                $_add_user_error,
2887
            ];
2888
        }
2889
2890
        $sql_query = '';
2891
        if ($_POST['pred_username'] == 'any') {
2892
            $username = '';
2893
        }
2894
        switch ($_POST['pred_hostname']) {
2895
            case 'any':
2896
                $hostname = '%';
2897
                break;
2898
            case 'localhost':
2899
                $hostname = 'localhost';
2900
                break;
2901
            case 'hosttable':
2902
                $hostname = '';
2903
                break;
2904
            case 'thishost':
2905
                $_user_name = $this->dbi->fetchValue('SELECT USER()');
2906
                $hostname = mb_substr(
2907
                    $_user_name,
0 ignored issues
show
Bug introduced by
It seems like $_user_name can also be of type false; however, parameter $str of mb_substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2907
                    /** @scrutinizer ignore-type */ $_user_name,
Loading history...
2908
                    mb_strrpos($_user_name, '@') + 1
0 ignored issues
show
Bug introduced by
It seems like $_user_name can also be of type false; however, parameter $haystack of mb_strrpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2908
                    mb_strrpos(/** @scrutinizer ignore-type */ $_user_name, '@') + 1
Loading history...
2909
                );
2910
                unset($_user_name);
2911
                break;
2912
        }
2913
        $sql = "SELECT '1' FROM `mysql`.`user`"
2914
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
2915
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';";
2916
        if ($this->dbi->fetchValue($sql) == 1) {
2917
            $message = Message::error(__('The user %s already exists!'));
2918
            $message->addParam('[em]\'' . $username . '\'@\'' . $hostname . '\'[/em]');
2919
            $_GET['adduser'] = true;
2920
            $_add_user_error = true;
2921
2922
            return [
2923
                $message,
2924
                $queries,
2925
                $queries_for_display,
2926
                $sql_query,
2927
                $_add_user_error,
2928
            ];
2929
        }
2930
2931
        list(
2932
            $create_user_real,
2933
            $create_user_show,
2934
            $real_sql_query,
2935
            $sql_query,
2936
            $password_set_real,
2937
            $password_set_show,
2938
            $alter_real_sql_query,
2939
            $alter_sql_query
2940
        ) = $this->getSqlQueriesForDisplayAndAddUser(
2941
            $username,
2942
            $hostname,
2943
            (isset($password) ? $password : '')
2944
        );
2945
2946
        if (empty($_POST['change_copy'])) {
2947
            $_error = false;
2948
2949
            if ($create_user_real !== null) {
0 ignored issues
show
introduced by
The condition $create_user_real !== null is always true.
Loading history...
2950
                if (! $this->dbi->tryQuery($create_user_real)) {
2951
                    $_error = true;
2952
                }
2953
                if (isset($password_set_real, $_POST['authentication_plugin']) && ! empty($password_set_real)) {
2954
                    $this->setProperPasswordHashing(
2955
                        $_POST['authentication_plugin']
2956
                    );
2957
                    if ($this->dbi->tryQuery($password_set_real)) {
2958
                        $sql_query .= $password_set_show;
2959
                    }
2960
                }
2961
                $sql_query = $create_user_show . $sql_query;
2962
            }
2963
2964
            list($sql_query, $message) = $this->addUserAndCreateDatabase(
2965
                $_error,
2966
                $real_sql_query,
2967
                $sql_query,
2968
                $username,
2969
                $hostname,
2970
                $dbname,
0 ignored issues
show
Bug introduced by
It seems like $dbname can also be of type array; however, parameter $dbname of PhpMyAdmin\Server\Privil...UserAndCreateDatabase() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2970
                /** @scrutinizer ignore-type */ $dbname,
Loading history...
2971
                $alter_real_sql_query,
2972
                $alter_sql_query
2973
            );
2974
            if (! empty($_POST['userGroup']) && $is_menuwork) {
2975
                $this->setUserGroup($GLOBALS['username'], $_POST['userGroup']);
2976
            }
2977
2978
            return [
2979
                $message,
2980
                $queries,
2981
                $queries_for_display,
2982
                $sql_query,
2983
                $_add_user_error,
2984
            ];
2985
        }
2986
2987
        // Copy the user group while copying a user
2988
        $old_usergroup =
2989
            isset($_POST['old_usergroup']) ? $_POST['old_usergroup'] : null;
2990
        $this->setUserGroup($_POST['username'], $old_usergroup);
2991
2992
        if ($create_user_real === null) {
0 ignored issues
show
introduced by
The condition $create_user_real === null is always false.
Loading history...
2993
            $queries[] = $create_user_real;
2994
        }
2995
        $queries[] = $real_sql_query;
2996
2997
        if (isset($password_set_real, $_POST['authentication_plugin']) && ! empty($password_set_real)) {
2998
            $this->setProperPasswordHashing(
2999
                $_POST['authentication_plugin']
3000
            );
3001
3002
            $queries[] = $password_set_real;
3003
        }
3004
        // we put the query containing the hidden password in
3005
        // $queries_for_display, at the same position occupied
3006
        // by the real query in $queries
3007
        $tmp_count = count($queries);
3008
        if (isset($create_user_real)) {
3009
            $queries_for_display[$tmp_count - 2] = $create_user_show;
3010
        }
3011
        if (isset($password_set_real) && ! empty($password_set_real)) {
3012
            $queries_for_display[$tmp_count - 3] = $create_user_show;
3013
            $queries_for_display[$tmp_count - 2] = $sql_query;
3014
            $queries_for_display[$tmp_count - 1] = $password_set_show;
3015
        } else {
3016
            $queries_for_display[$tmp_count - 1] = $sql_query;
3017
        }
3018
3019
        return [
3020
            $message,
3021
            $queries,
3022
            $queries_for_display,
3023
            $sql_query,
3024
            $_add_user_error,
3025
        ];
3026
    }
3027
3028
    /**
3029
     * Sets proper value of `old_passwords` according to
3030
     * the authentication plugin selected
3031
     *
3032
     * @param string $auth_plugin authentication plugin selected
3033
     *
3034
     * @return void
3035
     */
3036
    public function setProperPasswordHashing($auth_plugin)
3037
    {
3038
        // Set the hashing method used by PASSWORD()
3039
        // to be of type depending upon $authentication_plugin
3040
        if ($auth_plugin == 'sha256_password') {
3041
            $this->dbi->tryQuery('SET `old_passwords` = 2;');
3042
        } elseif ($auth_plugin == 'mysql_old_password') {
3043
            $this->dbi->tryQuery('SET `old_passwords` = 1;');
3044
        } else {
3045
            $this->dbi->tryQuery('SET `old_passwords` = 0;');
3046
        }
3047
    }
3048
3049
    /**
3050
     * Update DB information: DB, Table, isWildcard
3051
     *
3052
     * @return array
3053
     */
3054
    public function getDataForDBInfo()
3055
    {
3056
        $username = null;
3057
        $hostname = null;
3058
        $dbname = null;
3059
        $tablename = null;
3060
        $routinename = null;
3061
        $dbname_is_wildcard = null;
3062
3063
        if (isset($_REQUEST['username'])) {
3064
            $username = $_REQUEST['username'];
3065
        }
3066
        if (isset($_REQUEST['hostname'])) {
3067
            $hostname = $_REQUEST['hostname'];
3068
        }
3069
        /**
3070
         * Checks if a dropdown box has been used for selecting a database / table
3071
         */
3072
        if (Core::isValid($_POST['pred_tablename'])) {
3073
            $tablename = $_POST['pred_tablename'];
3074
        } elseif (Core::isValid($_REQUEST['tablename'])) {
3075
            $tablename = $_REQUEST['tablename'];
3076
        } else {
3077
            unset($tablename);
3078
        }
3079
3080
        if (Core::isValid($_POST['pred_routinename'])) {
3081
            $routinename = $_POST['pred_routinename'];
3082
        } elseif (Core::isValid($_REQUEST['routinename'])) {
3083
            $routinename = $_REQUEST['routinename'];
3084
        } else {
3085
            unset($routinename);
3086
        }
3087
3088
        if (isset($_POST['pred_dbname'])) {
3089
            $is_valid_pred_dbname = true;
3090
            foreach ($_POST['pred_dbname'] as $key => $db_name) {
3091
                if (! Core::isValid($db_name)) {
3092
                    $is_valid_pred_dbname = false;
3093
                    break;
3094
                }
3095
            }
3096
        }
3097
3098
        if (isset($_REQUEST['dbname'])) {
3099
            $is_valid_dbname = true;
3100
            if (is_array($_REQUEST['dbname'])) {
3101
                foreach ($_REQUEST['dbname'] as $key => $db_name) {
3102
                    if (! Core::isValid($db_name)) {
3103
                        $is_valid_dbname = false;
3104
                        break;
3105
                    }
3106
                }
3107
            } else {
3108
                if (! Core::isValid($_REQUEST['dbname'])) {
3109
                    $is_valid_dbname = false;
3110
                }
3111
            }
3112
        }
3113
3114
        if (isset($is_valid_pred_dbname) && $is_valid_pred_dbname) {
3115
            $dbname = $_POST['pred_dbname'];
3116
            // If dbname contains only one database.
3117
            if (count($dbname) === 1) {
3118
                $dbname = $dbname[0];
3119
            }
3120
        } elseif (isset($is_valid_dbname) && $is_valid_dbname) {
3121
            $dbname = $_REQUEST['dbname'];
3122
        } else {
3123
            unset($dbname, $tablename);
3124
        }
3125
3126
        if (isset($dbname)) {
3127
            if (is_array($dbname)) {
3128
                $db_and_table = $dbname;
3129
                foreach ($db_and_table as $key => $db_name) {
3130
                    $db_and_table[$key] .= '.';
3131
                }
3132
            } else {
3133
                $unescaped_db = Util::unescapeMysqlWildcards($dbname);
3134
                $db_and_table = Util::backquote($unescaped_db) . '.';
3135
            }
3136
            if (isset($tablename)) {
3137
                $db_and_table .= Util::backquote($tablename);
3138
            } else {
3139
                if (is_array($db_and_table)) {
3140
                    foreach ($db_and_table as $key => $db_name) {
3141
                        $db_and_table[$key] .= '*';
3142
                    }
3143
                } else {
3144
                    $db_and_table .= '*';
3145
                }
3146
            }
3147
        } else {
3148
            $db_and_table = '*.*';
3149
        }
3150
3151
        // check if given $dbname is a wildcard or not
3152
        if (isset($dbname)) {
3153
            //if (preg_match('/\\\\(?:_|%)/i', $dbname)) {
3154
            if (! is_array($dbname) && preg_match('/(?<!\\\\)(?:_|%)/', $dbname)) {
3155
                $dbname_is_wildcard = true;
3156
            } else {
3157
                $dbname_is_wildcard = false;
3158
            }
3159
        }
3160
3161
        return [
3162
            $username,
3163
            $hostname,
3164
            isset($dbname) ? $dbname : null,
3165
            isset($tablename) ? $tablename : null,
3166
            isset($routinename) ? $routinename : null,
3167
            $db_and_table,
3168
            $dbname_is_wildcard,
3169
        ];
3170
    }
3171
3172
    /**
3173
     * Get title and textarea for export user definition in Privileges
3174
     *
3175
     * @param string $username username
3176
     * @param string $hostname host name
3177
     *
3178
     * @return array ($title, $export)
3179
     */
3180
    public function getListForExportUserDefinition($username, $hostname)
3181
    {
3182
        $export = '<textarea class="export" cols="60" rows="15">';
3183
3184
        if (isset($_POST['selected_usr'])) {
3185
            // export privileges for selected users
3186
            $title = __('Privileges');
3187
3188
            //For removing duplicate entries of users
3189
            $_POST['selected_usr'] = array_unique($_POST['selected_usr']);
3190
3191
            foreach ($_POST['selected_usr'] as $export_user) {
3192
                $export_username = mb_substr(
3193
                    $export_user,
3194
                    0,
3195
                    mb_strpos($export_user, '&')
3196
                );
3197
                $export_hostname = mb_substr(
3198
                    $export_user,
3199
                    mb_strrpos($export_user, ';') + 1
3200
                );
3201
                $export .= '# '
3202
                    . sprintf(
3203
                        __('Privileges for %s'),
3204
                        '`' . htmlspecialchars($export_username)
3205
                        . '`@`' . htmlspecialchars($export_hostname) . '`'
3206
                    )
3207
                    . "\n\n";
3208
                $export .= $this->getGrants($export_username, $export_hostname) . "\n";
3209
            }
3210
        } else {
3211
            // export privileges for a single user
3212
            $title = __('User') . ' `' . htmlspecialchars($username)
3213
                . '`@`' . htmlspecialchars($hostname) . '`';
3214
            $export .= $this->getGrants($username, $hostname);
3215
        }
3216
        // remove trailing whitespace
3217
        $export = trim($export);
3218
3219
        $export .= '</textarea>';
3220
3221
        return [
3222
            $title,
3223
            $export,
3224
        ];
3225
    }
3226
3227
    /**
3228
     * Get HTML for display Add userfieldset
3229
     *
3230
     * @param string $db    the database
3231
     * @param string $table the table name
3232
     *
3233
     * @return string html output
3234
     */
3235
    public function getAddUserHtmlFieldset($db = '', $table = '')
3236
    {
3237
        if (! $GLOBALS['is_createuser']) {
3238
            return '';
3239
        }
3240
        $rel_params = [];
3241
        $url_params = [
3242
            'adduser' => 1,
3243
        ];
3244
        if (! empty($db)) {
3245
            $url_params['dbname']
3246
                = $rel_params['checkprivsdb']
3247
                    = $db;
3248
        }
3249
        if (! empty($table)) {
3250
            $url_params['tablename']
3251
                = $rel_params['checkprivstable']
3252
                    = $table;
3253
        }
3254
3255
        return $this->template->render('server/privileges/add_user_fieldset', [
3256
            'url_params' => $url_params,
3257
            'rel_params' => $rel_params,
3258
        ]);
3259
    }
3260
3261
    /**
3262
     * Get HTML snippet for display user overview page
3263
     *
3264
     * @param string $pmaThemeImage a image source link
3265
     * @param string $text_dir      text directory
3266
     *
3267
     * @return string
3268
     */
3269
    public function getHtmlForUserOverview($pmaThemeImage, $text_dir)
3270
    {
3271
        global $is_createuser;
3272
3273
        $password_column = 'Password';
3274
        $server_type = Util::getServerType();
3275
        $serverVersion = $this->dbi->getVersion();
3276
        if (($server_type == 'MySQL' || $server_type == 'Percona Server')
3277
            && $serverVersion >= 50706
3278
        ) {
3279
            $password_column = 'authentication_string';
3280
        }
3281
        // $sql_query is for the initial-filtered,
3282
        // $sql_query_all is for counting the total no. of users
3283
3284
        $sql_query = $sql_query_all = 'SELECT *,' .
3285
            " IF(`" . $password_column . "` = _latin1 '', 'N', 'Y') AS 'Password'" .
3286
            ' FROM `mysql`.`user`';
3287
3288
        $sql_query .= (isset($_GET['initial'])
3289
            ? $this->rangeOfUsers($_GET['initial'])
3290
            : '');
3291
3292
        $sql_query .= ' ORDER BY `User` ASC, `Host` ASC;';
3293
        $sql_query_all .= ' ;';
3294
3295
        $res = $this->dbi->tryQuery(
3296
            $sql_query,
3297
            DatabaseInterface::CONNECT_USER,
3298
            DatabaseInterface::QUERY_STORE
3299
        );
3300
        $res_all = $this->dbi->tryQuery(
3301
            $sql_query_all,
3302
            DatabaseInterface::CONNECT_USER,
3303
            DatabaseInterface::QUERY_STORE
3304
        );
3305
3306
        $errorMessages = '';
3307
        if (! $res) {
3308
            // the query failed! This may have two reasons:
3309
            // - the user does not have enough privileges
3310
            // - the privilege tables use a structure of an earlier version.
3311
            // so let's try a more simple query
3312
3313
            $this->dbi->freeResult($res);
0 ignored issues
show
Bug introduced by
It seems like $res can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::freeResult() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

3313
            $this->dbi->freeResult(/** @scrutinizer ignore-type */ $res);
Loading history...
3314
            $this->dbi->freeResult($res_all);
3315
            $sql_query = 'SELECT * FROM `mysql`.`user`';
3316
            $res = $this->dbi->tryQuery(
3317
                $sql_query,
3318
                DatabaseInterface::CONNECT_USER,
3319
                DatabaseInterface::QUERY_STORE
3320
            );
3321
3322
            if (! $res) {
3323
                $errorMessages .= $this->getHtmlForViewUsersError();
3324
                $errorMessages .= $this->getAddUserHtmlFieldset();
3325
            } else {
3326
                // This message is hardcoded because I will replace it by
3327
                // a automatic repair feature soon.
3328
                $raw = 'Your privilege table structure seems to be older than'
3329
                    . ' this MySQL version!<br>'
3330
                    . 'Please run the <code>mysql_upgrade</code> command'
3331
                    . ' that should be included in your MySQL server distribution'
3332
                    . ' to solve this problem!';
3333
                $errorMessages .= Message::rawError($raw)->getDisplay();
3334
            }
3335
            $this->dbi->freeResult($res);
3336
        } else {
3337
            $db_rights = $this->getDbRightsForUserOverview();
3338
            // for all initials, even non A-Z
3339
            $array_initials = [];
3340
3341
            foreach ($db_rights as $right) {
3342
                foreach ($right as $account) {
3343
                    if (empty($account['User']) && $account['Host'] == 'localhost') {
3344
                        $emptyUserNotice = Message::notice(
3345
                            __(
3346
                                'A user account allowing any user from localhost to '
3347
                                . 'connect is present. This will prevent other users '
3348
                                . 'from connecting if the host part of their account '
3349
                                . 'allows a connection from any (%) host.'
3350
                            )
3351
                            . Util::showMySQLDocu('problems-connecting')
3352
                        )->getDisplay();
3353
                        break 2;
3354
                    }
3355
                }
3356
            }
3357
3358
            /**
3359
             * Displays the initials
3360
             * Also not necessary if there is less than 20 privileges
3361
             */
3362
            if ($this->dbi->numRows($res_all) > 20) {
0 ignored issues
show
Bug introduced by
It seems like $res_all can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::numRows() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

3362
            if ($this->dbi->numRows(/** @scrutinizer ignore-type */ $res_all) > 20) {
Loading history...
3363
                $initials = $this->getHtmlForInitials($array_initials);
3364
            }
3365
3366
            /**
3367
            * Display the user overview
3368
            * (if less than 50 users, display them immediately)
3369
            */
3370
            if (isset($_GET['initial'])
3371
                || isset($_GET['showall'])
3372
                || $this->dbi->numRows($res) < 50
3373
            ) {
3374
                $usersOverview = $this->getUsersOverview(
3375
                    $res,
3376
                    $db_rights,
3377
                    $pmaThemeImage,
3378
                    $text_dir
3379
                );
3380
            }
3381
3382
            $response = Response::getInstance();
3383
            if (! $response->isAjax()
3384
                || ! empty($_REQUEST['ajax_page_request'])
3385
            ) {
3386
                if ($GLOBALS['is_reload_priv']) {
3387
                    $flushnote = new Message(
3388
                        __(
3389
                            'Note: phpMyAdmin gets the users’ privileges directly '
3390
                            . 'from MySQL’s privilege tables. The content of these '
3391
                            . 'tables may differ from the privileges the server uses, '
3392
                            . 'if they have been changed manually. In this case, '
3393
                            . 'you should %sreload the privileges%s before you continue.'
3394
                        ),
3395
                        Message::NOTICE
3396
                    );
3397
                    $flushnote->addParamHtml(
3398
                        '<a href="' . Url::getFromRoute('/server/privileges', ['flush_privileges' => 1])
3399
                        . '" id="reload_privileges_anchor">'
3400
                    );
3401
                    $flushnote->addParamHtml('</a>');
3402
                } else {
3403
                    $flushnote = new Message(
3404
                        __(
3405
                            'Note: phpMyAdmin gets the users’ privileges directly '
3406
                            . 'from MySQL’s privilege tables. The content of these '
3407
                            . 'tables may differ from the privileges the server uses, '
3408
                            . 'if they have been changed manually. In this case, '
3409
                            . 'the privileges have to be reloaded but currently, you '
3410
                            . 'don\'t have the RELOAD privilege.'
3411
                        )
3412
                        . Util::showMySQLDocu(
3413
                            'privileges-provided',
3414
                            false,
3415
                            null,
3416
                            null,
3417
                            'priv_reload'
3418
                        ),
3419
                        Message::NOTICE
3420
                    );
3421
                }
3422
                $flushNotice = $flushnote->getDisplay();
3423
            }
3424
        }
3425
3426
        return $this->template->render('server/privileges/user_overview', [
3427
            'error_messages' => $errorMessages,
3428
            'empty_user_notice' => $emptyUserNotice ?? '',
3429
            'initials' => $initials ?? '',
3430
            'users_overview' => $usersOverview ?? '',
3431
            'is_createuser' => $is_createuser,
3432
            'flush_notice' => $flushNotice ?? '',
3433
        ]);
3434
    }
3435
3436
    /**
3437
     * Get HTML snippet for display user properties
3438
     *
3439
     * @param boolean      $dbname_is_wildcard whether database name is wildcard or not
3440
     * @param string       $url_dbname         url database name that urlencode() string
3441
     * @param string       $username           username
3442
     * @param string       $hostname           host name
3443
     * @param string|array $dbname             database name
3444
     * @param string       $tablename          table name
3445
     *
3446
     * @return string
3447
     */
3448
    public function getHtmlForUserProperties(
3449
        $dbname_is_wildcard,
3450
        $url_dbname,
3451
        $username,
3452
        $hostname,
3453
        $dbname,
3454
        $tablename
3455
    ) {
3456
        $sql = "SELECT '1' FROM `mysql`.`user`"
3457
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
3458
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';";
3459
3460
        $user_does_not_exists = (bool) ! $this->dbi->fetchValue($sql);
3461
3462
        $loginInformationFields = '';
3463
        if ($user_does_not_exists) {
3464
            $loginInformationFields = $this->getHtmlForLoginInformationFields();
3465
        }
3466
3467
        $_params = [
3468
            'username' => $username,
3469
            'hostname' => $hostname,
3470
        ];
3471
        if (! is_array($dbname) && strlen($dbname) > 0) {
3472
            $_params['dbname'] = $dbname;
3473
            if (strlen($tablename) > 0) {
3474
                $_params['tablename'] = $tablename;
3475
            }
3476
        } else {
3477
            $_params['dbname'] = $dbname;
3478
        }
3479
3480
        $privilegesTable = $this->getHtmlToDisplayPrivilegesTable(
3481
            // If $dbname is an array, pass any one db as all have same privs.
3482
            Core::ifSetOr($dbname, is_array($dbname) ? $dbname[0] : '*', 'length'),
3483
            Core::ifSetOr($tablename, '*', 'length')
3484
        );
3485
3486
        $tableSpecificRights = '';
3487
        if (! is_array($dbname) && strlen($tablename) === 0
3488
            && empty($dbname_is_wildcard)
3489
        ) {
3490
            // no table name was given, display all table specific rights
3491
            // but only if $dbname contains no wildcards
3492
            if (strlen($dbname) === 0) {
3493
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3494
                    $username,
3495
                    $hostname,
3496
                    'database'
3497
                );
3498
            } else {
3499
                // unescape wildcards in dbname at table level
3500
                $unescaped_db = Util::unescapeMysqlWildcards($dbname);
3501
3502
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3503
                    $username,
3504
                    $hostname,
3505
                    'table',
3506
                    $unescaped_db
3507
                );
3508
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3509
                    $username,
3510
                    $hostname,
3511
                    'routine',
3512
                    $unescaped_db
3513
                );
3514
            }
3515
        }
3516
3517
        // Provide a line with links to the relevant database and table
3518
        $linkToDatabaseAndTable = '';
3519
        if (! is_array($dbname) && strlen($dbname) > 0 && empty($dbname_is_wildcard)) {
3520
            $linkToDatabaseAndTable = $this->getLinkToDbAndTable($url_dbname, $dbname, $tablename);
3521
        }
3522
3523
        $changePassword = '';
3524
        $changeLoginInformation = '';
3525
        if (! is_array($dbname) && strlen($dbname) === 0 && ! $user_does_not_exists) {
3526
            //change login information
3527
            $changePassword = ChangePassword::getHtml(
3528
                'edit_other',
3529
                $username,
3530
                $hostname
3531
            );
3532
            $changeLoginInformation = $this->getChangeLoginInformationHtmlForm($username, $hostname);
3533
        }
3534
3535
        return $this->template->render('server/privileges/user_properties', [
3536
            'user_does_not_exists' => $user_does_not_exists,
3537
            'login_information_fields' => $loginInformationFields,
3538
            'params' => $_params,
3539
            'privileges_table' => $privilegesTable,
3540
            'table_specific_rights' => $tableSpecificRights,
3541
            'link_to_database_and_table' => $linkToDatabaseAndTable,
3542
            'change_password' => $changePassword,
3543
            'change_login_information' => $changeLoginInformation,
3544
            'database' => $dbname,
3545
            'dbname' => $url_dbname,
3546
            'username' => $username,
3547
            'hostname' => $hostname,
3548
            'is_databases' => $dbname_is_wildcard || is_array($dbname) && count($dbname) > 1,
3549
            'table' => $tablename,
3550
            'current_user' => $this->dbi->getCurrentUser(),
3551
        ]);
3552
    }
3553
3554
    /**
3555
     * Get queries for Table privileges to change or copy user
3556
     *
3557
     * @param string $user_host_condition user host condition to
3558
     *                                    select relevant table privileges
3559
     * @param array  $queries             queries array
3560
     * @param string $username            username
3561
     * @param string $hostname            host name
3562
     *
3563
     * @return array
3564
     */
3565
    public function getTablePrivsQueriesForChangeOrCopyUser(
3566
        $user_host_condition,
3567
        array $queries,
3568
        $username,
3569
        $hostname
3570
    ) {
3571
        $res = $this->dbi->query(
3572
            'SELECT `Db`, `Table_name`, `Table_priv` FROM `mysql`.`tables_priv`'
3573
            . $user_host_condition,
3574
            DatabaseInterface::CONNECT_USER,
3575
            DatabaseInterface::QUERY_STORE
3576
        );
3577
        while ($row = $this->dbi->fetchAssoc($res)) {
0 ignored issues
show
Bug introduced by
It seems like $res can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::fetchAssoc() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

3577
        while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $res)) {
Loading history...
3578
            $res2 = $this->dbi->query(
3579
                'SELECT `Column_name`, `Column_priv`'
3580
                . ' FROM `mysql`.`columns_priv`'
3581
                . ' WHERE `User`'
3582
                . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . "'"
3583
                . ' AND `Host`'
3584
                . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . '\''
3585
                . ' AND `Db`'
3586
                . ' = \'' . $this->dbi->escapeString($row['Db']) . "'"
3587
                . ' AND `Table_name`'
3588
                . ' = \'' . $this->dbi->escapeString($row['Table_name']) . "'"
3589
                . ';',
3590
                DatabaseInterface::CONNECT_USER,
3591
                DatabaseInterface::QUERY_STORE
3592
            );
3593
3594
            $tmp_privs1 = $this->extractPrivInfo($row);
3595
            $tmp_privs2 = [
3596
                'Select' => [],
3597
                'Insert' => [],
3598
                'Update' => [],
3599
                'References' => [],
3600
            ];
3601
3602
            while ($row2 = $this->dbi->fetchAssoc($res2)) {
3603
                $tmp_array = explode(',', $row2['Column_priv']);
3604
                if (in_array('Select', $tmp_array)) {
3605
                    $tmp_privs2['Select'][] = $row2['Column_name'];
3606
                }
3607
                if (in_array('Insert', $tmp_array)) {
3608
                    $tmp_privs2['Insert'][] = $row2['Column_name'];
3609
                }
3610
                if (in_array('Update', $tmp_array)) {
3611
                    $tmp_privs2['Update'][] = $row2['Column_name'];
3612
                }
3613
                if (in_array('References', $tmp_array)) {
3614
                    $tmp_privs2['References'][] = $row2['Column_name'];
3615
                }
3616
            }
3617
            if (count($tmp_privs2['Select']) > 0 && ! in_array('SELECT', $tmp_privs1)) {
3618
                $tmp_privs1[] = 'SELECT (`' . implode('`, `', $tmp_privs2['Select']) . '`)';
3619
            }
3620
            if (count($tmp_privs2['Insert']) > 0 && ! in_array('INSERT', $tmp_privs1)) {
3621
                $tmp_privs1[] = 'INSERT (`' . implode('`, `', $tmp_privs2['Insert']) . '`)';
3622
            }
3623
            if (count($tmp_privs2['Update']) > 0 && ! in_array('UPDATE', $tmp_privs1)) {
3624
                $tmp_privs1[] = 'UPDATE (`' . implode('`, `', $tmp_privs2['Update']) . '`)';
3625
            }
3626
            if (count($tmp_privs2['References']) > 0
3627
                && ! in_array('REFERENCES', $tmp_privs1)
3628
            ) {
3629
                $tmp_privs1[]
3630
                    = 'REFERENCES (`' . implode('`, `', $tmp_privs2['References']) . '`)';
3631
            }
3632
3633
            $queries[] = 'GRANT ' . implode(', ', $tmp_privs1)
3634
                . ' ON ' . Util::backquote($row['Db']) . '.'
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($row['Db']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

3634
                . ' ON ' . /** @scrutinizer ignore-type */ Util::backquote($row['Db']) . '.'
Loading history...
3635
                . Util::backquote($row['Table_name'])
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($row['Table_name']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

3635
                . /** @scrutinizer ignore-type */ Util::backquote($row['Table_name'])
Loading history...
3636
                . ' TO \'' . $this->dbi->escapeString($username)
3637
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\''
3638
                . (in_array('Grant', explode(',', $row['Table_priv']))
3639
                ? ' WITH GRANT OPTION;'
3640
                : ';');
3641
        }
3642
        return $queries;
3643
    }
3644
3645
    /**
3646
     * Get queries for database specific privileges for change or copy user
3647
     *
3648
     * @param array  $queries  queries array with string
3649
     * @param string $username username
3650
     * @param string $hostname host name
3651
     *
3652
     * @return array
3653
     */
3654
    public function getDbSpecificPrivsQueriesForChangeOrCopyUser(
3655
        array $queries,
3656
        $username,
3657
        $hostname
3658
    ) {
3659
        $user_host_condition = ' WHERE `User`'
3660
            . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . "'"
3661
            . ' AND `Host`'
3662
            . ' = \'' . $this->dbi->escapeString($_POST['old_hostname']) . '\';';
3663
3664
        $res = $this->dbi->query(
3665
            'SELECT * FROM `mysql`.`db`' . $user_host_condition
3666
        );
3667
3668
        while ($row = $this->dbi->fetchAssoc($res)) {
0 ignored issues
show
Bug introduced by
It seems like $res can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::fetchAssoc() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

3668
        while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $res)) {
Loading history...
3669
            $queries[] = 'GRANT ' . implode(', ', $this->extractPrivInfo($row))
3670
                . ' ON ' . Util::backquote($row['Db']) . '.*'
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($row['Db']) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

3670
                . ' ON ' . /** @scrutinizer ignore-type */ Util::backquote($row['Db']) . '.*'
Loading history...
3671
                . ' TO \'' . $this->dbi->escapeString($username)
3672
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\''
3673
                . ($row['Grant_priv'] == 'Y' ? ' WITH GRANT OPTION;' : ';');
3674
        }
3675
        $this->dbi->freeResult($res);
0 ignored issues
show
Bug introduced by
It seems like $res can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::freeResult() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

3675
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $res);
Loading history...
3676
3677
        $queries = $this->getTablePrivsQueriesForChangeOrCopyUser(
3678
            $user_host_condition,
3679
            $queries,
3680
            $username,
3681
            $hostname
3682
        );
3683
3684
        return $queries;
3685
    }
3686
3687
    /**
3688
     * Prepares queries for adding users and
3689
     * also create database and return query and message
3690
     *
3691
     * @param boolean $_error               whether user create or not
3692
     * @param string  $real_sql_query       SQL query for add a user
3693
     * @param string  $sql_query            SQL query to be displayed
3694
     * @param string  $username             username
3695
     * @param string  $hostname             host name
3696
     * @param string  $dbname               database name
3697
     * @param string  $alter_real_sql_query SQL query for ALTER USER
3698
     * @param string  $alter_sql_query      SQL query for ALTER USER to be displayed
3699
     *
3700
     * @return array, $message
3701
     */
3702
    public function addUserAndCreateDatabase(
3703
        $_error,
3704
        $real_sql_query,
3705
        $sql_query,
3706
        $username,
3707
        $hostname,
3708
        $dbname,
3709
        $alter_real_sql_query,
3710
        $alter_sql_query
3711
    ) {
3712
        if ($_error || (! empty($real_sql_query)
3713
            && ! $this->dbi->tryQuery($real_sql_query))
3714
        ) {
3715
            $_POST['createdb-1'] = $_POST['createdb-2']
3716
                = $_POST['createdb-3'] = null;
3717
            $message = Message::rawError($this->dbi->getError());
0 ignored issues
show
Bug introduced by
It seems like $this->dbi->getError() can also be of type boolean; however, parameter $message of PhpMyAdmin\Message::rawError() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

3717
            $message = Message::rawError(/** @scrutinizer ignore-type */ $this->dbi->getError());
Loading history...
3718
        } elseif ($alter_real_sql_query !== '' && ! $this->dbi->tryQuery($alter_real_sql_query)) {
3719
            $_POST['createdb-1'] = $_POST['createdb-2']
3720
                = $_POST['createdb-3'] = null;
3721
            $message = Message::rawError($this->dbi->getError());
3722
        } else {
3723
            $sql_query .= $alter_sql_query;
3724
            $message = Message::success(__('You have added a new user.'));
3725
        }
3726
3727
        if (isset($_POST['createdb-1'])) {
3728
            // Create database with same name and grant all privileges
3729
            $q = 'CREATE DATABASE IF NOT EXISTS '
3730
                . Util::backquote(
3731
                    $this->dbi->escapeString($username)
3732
                ) . ';';
3733
            $sql_query .= $q;
3734
            if (! $this->dbi->tryQuery($q)) {
3735
                $message = Message::rawError($this->dbi->getError());
3736
            }
3737
3738
            /**
3739
             * Reload the navigation
3740
             */
3741
            $GLOBALS['reload'] = true;
3742
            $GLOBALS['db'] = $username;
3743
3744
            $q = 'GRANT ALL PRIVILEGES ON '
3745
                . Util::backquote(
3746
                    Util::escapeMysqlWildcards(
3747
                        $this->dbi->escapeString($username)
3748
                    )
3749
                ) . '.* TO \''
3750
                . $this->dbi->escapeString($username)
3751
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
3752
            $sql_query .= $q;
3753
            if (! $this->dbi->tryQuery($q)) {
3754
                $message = Message::rawError($this->dbi->getError());
3755
            }
3756
        }
3757
3758
        if (isset($_POST['createdb-2'])) {
3759
            // Grant all privileges on wildcard name (username\_%)
3760
            $q = 'GRANT ALL PRIVILEGES ON '
3761
                . Util::backquote(
3762
                    Util::escapeMysqlWildcards(
3763
                        $this->dbi->escapeString($username)
3764
                    ) . '\_%'
3765
                ) . '.* TO \''
3766
                . $this->dbi->escapeString($username)
3767
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
3768
            $sql_query .= $q;
3769
            if (! $this->dbi->tryQuery($q)) {
3770
                $message = Message::rawError($this->dbi->getError());
3771
            }
3772
        }
3773
3774
        if (isset($_POST['createdb-3'])) {
3775
            // Grant all privileges on the specified database to the new user
3776
            $q = 'GRANT ALL PRIVILEGES ON '
3777
            . Util::backquote(
3778
                $this->dbi->escapeString($dbname)
3779
            ) . '.* TO \''
3780
            . $this->dbi->escapeString($username)
3781
            . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
3782
            $sql_query .= $q;
3783
            if (! $this->dbi->tryQuery($q)) {
3784
                $message = Message::rawError($this->dbi->getError());
3785
            }
3786
        }
3787
        return [
3788
            $sql_query,
3789
            $message,
3790
        ];
3791
    }
3792
3793
    /**
3794
     * Get the hashed string for password
3795
     *
3796
     * @param string $password password
3797
     *
3798
     * @return string
3799
     */
3800
    public function getHashedPassword($password)
3801
    {
3802
        $password = $this->dbi->escapeString($password);
3803
        $result = $this->dbi->fetchSingleRow(
3804
            "SELECT PASSWORD('" . $password . "') AS `password`;"
3805
        );
3806
3807
        return $result['password'];
3808
    }
3809
3810
    /**
3811
     * Check if MariaDB's 'simple_password_check'
3812
     * OR 'cracklib_password_check' is ACTIVE
3813
     *
3814
     * @return boolean if atleast one of the plugins is ACTIVE
3815
     */
3816
    public function checkIfMariaDBPwdCheckPluginActive()
3817
    {
3818
        $serverVersion = $this->dbi->getVersion();
3819
        if (! (Util::getServerType() == 'MariaDB' && $serverVersion >= 100002)) {
3820
            return false;
3821
        }
3822
3823
        $result = $this->dbi->tryQuery(
3824
            'SHOW PLUGINS SONAME LIKE \'%_password_check%\''
3825
        );
3826
3827
        /* Plugins are not working, for example directory does not exists */
3828
        if ($result === false) {
3829
            return false;
3830
        }
3831
3832
        while ($row = $this->dbi->fetchAssoc($result)) {
3833
            if ($row['Status'] === 'ACTIVE') {
3834
                return true;
3835
            }
3836
        }
3837
3838
        return false;
3839
    }
3840
3841
3842
    /**
3843
     * Get SQL queries for Display and Add user
3844
     *
3845
     * @param string $username username
3846
     * @param string $hostname host name
3847
     * @param string $password password
3848
     *
3849
     * @return array ($create_user_real, $create_user_show, $real_sql_query, $sql_query
3850
     *                $password_set_real, $password_set_show, $alter_real_sql_query, $alter_sql_query)
3851
     */
3852
    public function getSqlQueriesForDisplayAndAddUser($username, $hostname, $password)
3853
    {
3854
        $slashedUsername = $this->dbi->escapeString($username);
3855
        $slashedHostname = $this->dbi->escapeString($hostname);
3856
        $slashedPassword = $this->dbi->escapeString($password);
3857
        $serverType = Util::getServerType();
3858
        $serverVersion = $this->dbi->getVersion();
3859
3860
        $create_user_stmt = sprintf(
3861
            'CREATE USER \'%s\'@\'%s\'',
3862
            $slashedUsername,
3863
            $slashedHostname
3864
        );
3865
        $isMariaDBPwdPluginActive = $this->checkIfMariaDBPwdCheckPluginActive();
3866
3867
        // See https://github.com/phpmyadmin/phpmyadmin/pull/11560#issuecomment-147158219
3868
        // for details regarding details of syntax usage for various versions
3869
3870
        // 'IDENTIFIED WITH auth_plugin'
3871
        // is supported by MySQL 5.5.7+
3872
        if (($serverType == 'MySQL' || $serverType == 'Percona Server')
3873
            && $serverVersion >= 50507
3874
            && isset($_POST['authentication_plugin'])
3875
        ) {
3876
            $create_user_stmt .= ' IDENTIFIED WITH '
3877
                . $_POST['authentication_plugin'];
3878
        }
3879
3880
        // 'IDENTIFIED VIA auth_plugin'
3881
        // is supported by MariaDB 5.2+
3882
        if ($serverType == 'MariaDB'
3883
            && $serverVersion >= 50200
3884
            && isset($_POST['authentication_plugin'])
3885
            && ! $isMariaDBPwdPluginActive
3886
        ) {
3887
            $create_user_stmt .= ' IDENTIFIED VIA '
3888
                . $_POST['authentication_plugin'];
3889
        }
3890
3891
        $create_user_real = $create_user_stmt;
3892
        $create_user_show = $create_user_stmt;
3893
3894
        $password_set_stmt = 'SET PASSWORD FOR \'%s\'@\'%s\' = \'%s\'';
3895
        $password_set_show = sprintf(
3896
            $password_set_stmt,
3897
            $slashedUsername,
3898
            $slashedHostname,
3899
            '***'
3900
        );
3901
3902
        $sql_query_stmt = sprintf(
3903
            'GRANT %s ON *.* TO \'%s\'@\'%s\'',
3904
            implode(', ', $this->extractPrivInfo()),
3905
            $slashedUsername,
3906
            $slashedHostname
3907
        );
3908
        $real_sql_query = $sql_query = $sql_query_stmt;
3909
3910
        // Set the proper hashing method
3911
        if (isset($_POST['authentication_plugin'])) {
3912
            $this->setProperPasswordHashing(
3913
                $_POST['authentication_plugin']
3914
            );
3915
        }
3916
3917
        // Use 'CREATE USER ... WITH ... AS ..' syntax for
3918
        // newer MySQL versions
3919
        // and 'CREATE USER ... VIA .. USING ..' syntax for
3920
        // newer MariaDB versions
3921
        if ((($serverType == 'MySQL' || $serverType == 'Percona Server')
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($serverType == 'MySQL' ...$serverVersion >= 50200, Probably Intended Meaning: $serverType == 'MySQL' |...serverVersion >= 50200)
Loading history...
3922
            && $serverVersion >= 50706)
3923
            || ($serverType == 'MariaDB'
3924
            && $serverVersion >= 50200)
3925
        ) {
3926
            $password_set_real = null;
3927
3928
            // Required for binding '%' with '%s'
3929
            $create_user_stmt = str_replace(
3930
                '%',
3931
                '%%',
3932
                $create_user_stmt
3933
            );
3934
3935
            // MariaDB uses 'USING' whereas MySQL uses 'AS'
3936
            // but MariaDB with validation plugin needs cleartext password
3937
            if ($serverType == 'MariaDB'
3938
                && ! $isMariaDBPwdPluginActive
3939
            ) {
3940
                $create_user_stmt .= ' USING \'%s\'';
3941
            } elseif ($serverType == 'MariaDB') {
3942
                $create_user_stmt .= ' IDENTIFIED BY \'%s\'';
3943
            } elseif (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) {
3944
                $create_user_stmt .= ' BY \'%s\'';
3945
            } else {
3946
                $create_user_stmt .= ' AS \'%s\'';
3947
            }
3948
3949
            if ($_POST['pred_password'] == 'keep') {
3950
                $create_user_real = sprintf(
3951
                    $create_user_stmt,
3952
                    $slashedPassword
3953
                );
3954
                $create_user_show = sprintf(
3955
                    $create_user_stmt,
3956
                    '***'
3957
                );
3958
            } elseif ($_POST['pred_password'] == 'none') {
3959
                $create_user_real = sprintf(
3960
                    $create_user_stmt,
3961
                    null
3962
                );
3963
                $create_user_show = sprintf(
3964
                    $create_user_stmt,
3965
                    '***'
3966
                );
3967
            } else {
3968
                if (! (($serverType == 'MariaDB' && $isMariaDBPwdPluginActive)
3969
                    || ($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011)) {
3970
                    $hashedPassword = $this->getHashedPassword($_POST['pma_pw']);
3971
                } else {
3972
                    // MariaDB with validation plugin needs cleartext password
3973
                    $hashedPassword = $_POST['pma_pw'];
3974
                }
3975
                $create_user_real = sprintf(
3976
                    $create_user_stmt,
3977
                    $hashedPassword
3978
                );
3979
                $create_user_show = sprintf(
3980
                    $create_user_stmt,
3981
                    '***'
3982
                );
3983
            }
3984
        } else {
3985
            // Use 'SET PASSWORD' syntax for pre-5.7.6 MySQL versions
3986
            // and pre-5.2.0 MariaDB versions
3987
            if ($_POST['pred_password'] == 'keep') {
3988
                $password_set_real = sprintf(
3989
                    $password_set_stmt,
3990
                    $slashedUsername,
3991
                    $slashedHostname,
3992
                    $slashedPassword
3993
                );
3994
            } elseif ($_POST['pred_password'] == 'none') {
3995
                $password_set_real = sprintf(
3996
                    $password_set_stmt,
3997
                    $slashedUsername,
3998
                    $slashedHostname,
3999
                    null
4000
                );
4001
            } else {
4002
                $hashedPassword = $this->getHashedPassword($_POST['pma_pw']);
4003
                $password_set_real = sprintf(
4004
                    $password_set_stmt,
4005
                    $slashedUsername,
4006
                    $slashedHostname,
4007
                    $hashedPassword
4008
                );
4009
            }
4010
        }
4011
4012
        $alter_real_sql_query = '';
4013
        $alter_sql_query = '';
4014
        if (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) {
4015
            $sql_query_stmt = '';
4016
            if ((isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y')
4017
                || (isset($GLOBALS['Grant_priv']) && $GLOBALS['Grant_priv'] == 'Y')
4018
            ) {
4019
                $sql_query_stmt = ' WITH GRANT OPTION';
4020
            }
4021
            $real_sql_query .= $sql_query_stmt;
4022
            $sql_query .= $sql_query_stmt;
4023
4024
            $alter_sql_query_stmt = sprintf(
4025
                'ALTER USER \'%s\'@\'%s\'',
4026
                $slashedUsername,
4027
                $slashedHostname
4028
            );
4029
            $alter_real_sql_query = $alter_sql_query_stmt;
4030
            $alter_sql_query = $alter_sql_query_stmt;
4031
        }
4032
4033
        // add REQUIRE clause
4034
        $require_clause = $this->getRequireClause();
4035
        $with_clause = $this->getWithClauseForAddUserAndUpdatePrivs();
4036
4037
        if (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) {
4038
            $alter_real_sql_query .= $require_clause;
4039
            $alter_sql_query .= $require_clause;
4040
            $alter_real_sql_query .= $with_clause;
4041
            $alter_sql_query .= $with_clause;
4042
        } else {
4043
            $real_sql_query .= $require_clause;
4044
            $sql_query .= $require_clause;
4045
            $real_sql_query .= $with_clause;
4046
            $sql_query .= $with_clause;
4047
        }
4048
4049
        if ($alter_real_sql_query !== '') {
4050
            $alter_real_sql_query .= ';';
4051
            $alter_sql_query .= ';';
4052
        }
4053
        $create_user_real .= ';';
4054
        $create_user_show .= ';';
4055
        $real_sql_query .= ';';
4056
        $sql_query .= ';';
4057
        // No Global GRANT_OPTION privilege
4058
        if (! $GLOBALS['is_grantuser']) {
4059
            $real_sql_query = '';
4060
            $sql_query = '';
4061
        }
4062
4063
        // Use 'SET PASSWORD' for pre-5.7.6 MySQL versions
4064
        // and pre-5.2.0 MariaDB
4065
        if (($serverType == 'MySQL'
4066
            && $serverVersion >= 50706)
4067
            || ($serverType == 'MariaDB'
4068
            && $serverVersion >= 50200)
4069
        ) {
4070
            $password_set_real = null;
4071
            $password_set_show = null;
4072
        } else {
4073
            if ($password_set_real !== null) {
4074
                $password_set_real .= ";";
4075
            }
4076
            $password_set_show .= ";";
4077
        }
4078
4079
        return [
4080
            $create_user_real,
4081
            $create_user_show,
4082
            $real_sql_query,
4083
            $sql_query,
4084
            $password_set_real,
4085
            $password_set_show,
4086
            $alter_real_sql_query,
4087
            $alter_sql_query,
4088
        ];
4089
    }
4090
4091
    /**
4092
     * Returns the type ('PROCEDURE' or 'FUNCTION') of the routine
4093
     *
4094
     * @param string $dbname      database
4095
     * @param string $routineName routine
4096
     *
4097
     * @return string type
4098
     */
4099
    public function getRoutineType($dbname, $routineName)
4100
    {
4101
        $routineData = $this->dbi->getRoutines($dbname);
4102
4103
        foreach ($routineData as $routine) {
4104
            if ($routine['name'] === $routineName) {
4105
                return $routine['type'];
4106
            }
4107
        }
4108
        return '';
4109
    }
4110
4111
    /**
4112
     * @param string $username User name
4113
     * @param string $hostname Host name
4114
     * @param string $database Database name
4115
     * @param string $routine  Routine name
4116
     *
4117
     * @return array
4118
     */
4119
    private function getRoutinePrivileges(
4120
        string $username,
4121
        string $hostname,
4122
        string $database,
4123
        string $routine
4124
    ): array {
4125
        $sql = "SELECT `Proc_priv`"
4126
            . " FROM `mysql`.`procs_priv`"
4127
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
4128
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "'"
4129
            . " AND `Db` = '"
4130
            . $this->dbi->escapeString(Util::unescapeMysqlWildcards($database)) . "'"
4131
            . " AND `Routine_name` LIKE '" . $this->dbi->escapeString($routine) . "';";
4132
        $privileges = $this->dbi->fetchValue($sql);
4133
        if ($privileges === false) {
4134
            $privileges = '';
4135
        }
4136
        return $this->parseProcPriv($privileges);
4137
    }
4138
}
4139