Passed
Push — master ( 672aee...b075f0 )
by Maurício
08:51
created

Privileges::getAllPrivileges()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 15
c 1
b 0
f 0
dl 0
loc 20
rs 9.7666
cc 3
nc 4
nop 2
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
     * @param string $db    database name
1369
     * @param string $table table name
1370
     *
1371
     * @return array
1372
     */
1373
    public function getAllPrivileges(string $db, string $table = ''): array
1374
    {
1375
        $databasePrivileges = $this->getGlobalAndDatabasePrivileges($db);
1376
        $tablePrivileges = [];
1377
        if ($table !== '') {
1378
            $tablePrivileges = $this->getTablePrivileges($db, $table);
1379
        }
1380
        $routinePrivileges = $this->getRoutinesPrivileges($db);
1381
        $allPrivileges = array_merge($databasePrivileges, $tablePrivileges, $routinePrivileges);
1382
1383
        $privileges = [];
1384
        foreach ($allPrivileges as $privilege) {
1385
            $userHost = $privilege['User'] . '@' . $privilege['Host'];
1386
            $privileges[$userHost] = $privileges[$userHost] ?? [];
1387
            $privileges[$userHost]['user'] = $privilege['User'];
1388
            $privileges[$userHost]['host'] = $privilege['Host'];
1389
            $privileges[$userHost]['privileges'] = $privileges[$userHost]['privileges'] ?? [];
1390
            $privileges[$userHost]['privileges'][] = $this->getSpecificPrivilege($privilege);
1391
        }
1392
        return $privileges;
1393
    }
1394
1395
    /**
1396
     * @param array $row Array with user privileges
1397
     *
1398
     * @return array
1399
     */
1400
    private function getSpecificPrivilege(array $row): array
1401
    {
1402
        $privilege = [
1403
            'type' => $row['Type'],
1404
            'database' => $row['Db'],
1405
        ];
1406
        if ($row['Type'] === 'r') {
1407
            $privilege['routine'] = $row['Routine_name'];
1408
            $privilege['has_grant'] = strpos($row['Proc_priv'], 'Grant') !== false;
1409
            $privilege['privileges'] = explode(',', $row['Proc_priv']);
1410
        } elseif ($row['Type'] === 't') {
1411
            $privilege['table'] = $row['Table_name'];
1412
            $privilege['has_grant'] = strpos($row['Table_priv'], 'Grant') !== false;
1413
            $tablePrivs = explode(',', $row['Table_priv']);
1414
            $specificPrivileges = [];
1415
            $grantsArr = $this->getTableGrantsArray();
1416
            foreach ($grantsArr as $grant) {
1417
                $specificPrivileges[$grant[0]] = 'N';
1418
                foreach ($tablePrivs as $tablePriv) {
1419
                    if ($grant[0] == $tablePriv) {
1420
                        $specificPrivileges[$grant[0]] = 'Y';
1421
                    }
1422
                }
1423
            }
1424
            $privilege['privileges'] = $this->extractPrivInfo(
1425
                $specificPrivileges,
1426
                true,
1427
                true
1428
            );
1429
        } else {
1430
            $privilege['has_grant'] = $row['Grant_priv'] === 'Y';
1431
            $privilege['privileges'] = $this->extractPrivInfo($row, true);
1432
        }
1433
        return $privilege;
1434
    }
1435
1436
    /**
1437
     * @param string $db database name
1438
     *
1439
     * @return array
1440
     */
1441
    private function getGlobalAndDatabasePrivileges(string $db): array
1442
    {
1443
        $listOfPrivileges = '`Select_priv`,
1444
            `Insert_priv`,
1445
            `Update_priv`,
1446
            `Delete_priv`,
1447
            `Create_priv`,
1448
            `Drop_priv`,
1449
            `Grant_priv`,
1450
            `Index_priv`,
1451
            `Alter_priv`,
1452
            `References_priv`,
1453
            `Create_tmp_table_priv`,
1454
            `Lock_tables_priv`,
1455
            `Create_view_priv`,
1456
            `Show_view_priv`,
1457
            `Create_routine_priv`,
1458
            `Alter_routine_priv`,
1459
            `Execute_priv`,
1460
            `Event_priv`,
1461
            `Trigger_priv`,';
1462
1463
        $listOfComparedPrivileges = '`Select_priv` = \'N\' AND
1464
            `Insert_priv` = \'N\' AND
1465
            `Update_priv` = \'N\' AND
1466
            `Delete_priv` = \'N\' AND
1467
            `Create_priv` = \'N\' AND
1468
            `Drop_priv` = \'N\' AND
1469
            `Grant_priv` = \'N\' AND
1470
            `References_priv` = \'N\' AND
1471
            `Create_tmp_table_priv` = \'N\' AND
1472
            `Lock_tables_priv` = \'N\' AND
1473
            `Create_view_priv` = \'N\' AND
1474
            `Show_view_priv` = \'N\' AND
1475
            `Create_routine_priv` = \'N\' AND
1476
            `Alter_routine_priv` = \'N\' AND
1477
            `Execute_priv` = \'N\' AND
1478
            `Event_priv` = \'N\' AND
1479
            `Trigger_priv` = \'N\'';
1480
1481
        $query = '
1482
            (
1483
                SELECT `User`, `Host`, ' . $listOfPrivileges . ' \'*\' AS `Db`, \'g\' AS `Type`
1484
                FROM `mysql`.`user`
1485
                WHERE NOT (' . $listOfComparedPrivileges . ')
1486
            )
1487
            UNION
1488
            (
1489
                SELECT `User`, `Host`, ' . $listOfPrivileges . ' `Db`, \'d\' AS `Type`
1490
                FROM `mysql`.`db`
1491
                WHERE \'' . $this->dbi->escapeString($db) . '\' LIKE `Db` AND NOT (' . $listOfComparedPrivileges . ')
1492
            )
1493
            ORDER BY `User` ASC, `Host` ASC, `Db` ASC;
1494
        ';
1495
        $result = $this->dbi->query($query);
1496
        if ($result === false) {
1497
            return [];
1498
        }
1499
1500
        $privileges = [];
1501
        while ($row = $this->dbi->fetchAssoc($result)) {
1502
            $privileges[] = $row;
1503
        }
1504
        return $privileges;
1505
    }
1506
1507
    /**
1508
     * @param string $db    database name
1509
     * @param string $table table name
1510
     *
1511
     * @return array
1512
     */
1513
    private function getTablePrivileges(string $db, string $table): array
1514
    {
1515
        $query = '
1516
            SELECT `User`, `Host`, `Db`, \'t\' AS `Type`, `Table_name`, `Table_priv`
1517
            FROM `mysql`.`tables_priv`
1518
            WHERE
1519
                \'' . $this->dbi->escapeString($db) . '\' LIKE `Db` AND
1520
                \'' . $this->dbi->escapeString($table) . '\' LIKE `Table_name` AND
1521
                NOT (`Table_priv` = \'\' AND Column_priv = \'\')
1522
            ORDER BY `User` ASC, `Host` ASC, `Db` ASC, `Table_priv` ASC;
1523
        ';
1524
        $result = $this->dbi->query($query);
1525
        if ($result === false) {
1526
            return [];
1527
        }
1528
1529
        $privileges = [];
1530
        while ($row = $this->dbi->fetchAssoc($result)) {
1531
            $privileges[] = $row;
1532
        }
1533
        return $privileges;
1534
    }
1535
1536
    /**
1537
     * @param string $db database name
1538
     *
1539
     * @return array
1540
     */
1541
    private function getRoutinesPrivileges(string $db): array
1542
    {
1543
        $query = '
1544
            SELECT *, \'r\' AS `Type`
1545
            FROM `mysql`.`procs_priv`
1546
            WHERE Db = \'' . $this->dbi->escapeString($db) . '\';
1547
        ';
1548
        $result = $this->dbi->query($query);
1549
        if ($result === false) {
1550
            return [];
1551
        }
1552
1553
        $privileges = [];
1554
        while ($row = $this->dbi->fetchAssoc($result)) {
1555
            $privileges[] = $row;
1556
        }
1557
        return $privileges;
1558
    }
1559
1560
    /**
1561
     * Get HTML error for View Users form
1562
     * For non superusers such as grant/create users
1563
     *
1564
     * @return string
1565
     */
1566
    public function getHtmlForViewUsersError()
1567
    {
1568
        return Message::error(
1569
            __('Not enough privilege to view users.')
1570
        )->getDisplay();
1571
    }
1572
1573
    /**
1574
     * Returns edit, revoke or export link for a user.
1575
     *
1576
     * @param string $linktype    The link type (edit | revoke | export)
1577
     * @param string $username    User name
1578
     * @param string $hostname    Host name
1579
     * @param string $dbname      Database name
1580
     * @param string $tablename   Table name
1581
     * @param string $routinename Routine name
1582
     * @param string $initial     Initial value
1583
     *
1584
     * @return string HTML code with link
1585
     */
1586
    public function getUserLink(
1587
        $linktype,
1588
        $username,
1589
        $hostname,
1590
        $dbname = '',
1591
        $tablename = '',
1592
        $routinename = '',
1593
        $initial = ''
1594
    ) {
1595
        $html = '<a';
1596
        switch ($linktype) {
1597
            case 'edit':
1598
                $html .= ' class="edit_user_anchor"';
1599
                break;
1600
            case 'export':
1601
                $html .= ' class="export_user_anchor ajax"';
1602
                break;
1603
        }
1604
        $params = [
1605
            'username' => $username,
1606
            'hostname' => $hostname,
1607
        ];
1608
        switch ($linktype) {
1609
            case 'edit':
1610
                $params['dbname'] = $dbname;
1611
                $params['tablename'] = $tablename;
1612
                $params['routinename'] = $routinename;
1613
                break;
1614
            case 'revoke':
1615
                $params['dbname'] = $dbname;
1616
                $params['tablename'] = $tablename;
1617
                $params['routinename'] = $routinename;
1618
                $params['revokeall'] = 1;
1619
                break;
1620
            case 'export':
1621
                $params['initial'] = $initial;
1622
                $params['export'] = 1;
1623
                break;
1624
        }
1625
1626
        $html .= ' href="' . Url::getFromRoute('/server/privileges');
1627
        if ($linktype == 'revoke') {
1628
            $html .= '" data-post="' . Url::getCommon($params, '');
1629
        } else {
1630
            $html .= Url::getCommon($params, '&');
1631
        }
1632
        $html .= '">';
1633
1634
        switch ($linktype) {
1635
            case 'edit':
1636
                $html .= Util::getIcon('b_usredit', __('Edit privileges'));
1637
                break;
1638
            case 'revoke':
1639
                $html .= Util::getIcon('b_usrdrop', __('Revoke'));
1640
                break;
1641
            case 'export':
1642
                $html .= Util::getIcon('b_tblexport', __('Export'));
1643
                break;
1644
        }
1645
        $html .= '</a>';
1646
1647
        return $html;
1648
    }
1649
1650
    /**
1651
     * Returns user group edit link
1652
     *
1653
     * @param string $username User name
1654
     *
1655
     * @return string HTML code with link
1656
     */
1657
    public function getUserGroupEditLink($username)
1658
    {
1659
         return '<a class="edit_user_group_anchor ajax"'
1660
            . ' href="' . Url::getFromRoute('/server/privileges', ['username' => $username])
1661
            . '">'
1662
            . Util::getIcon('b_usrlist', __('Edit user group'))
1663
            . '</a>';
1664
    }
1665
1666
    /**
1667
     * Returns number of defined user groups
1668
     *
1669
     * @return integer
1670
     */
1671
    public function getUserGroupCount()
1672
    {
1673
        $cfgRelation = $this->relation->getRelationsParam();
1674
        $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

1674
        $user_group_table = /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['db'])
Loading history...
1675
            . '.' . 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

1675
            . '.' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['usergroups']);
Loading history...
1676
        $sql_query = 'SELECT COUNT(*) FROM ' . $user_group_table;
1677
        $user_group_count = $this->dbi->fetchValue(
1678
            $sql_query,
1679
            0,
1680
            0,
1681
            DatabaseInterface::CONNECT_CONTROL
1682
        );
1683
1684
        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...
1685
    }
1686
1687
    /**
1688
     * Returns name of user group that user is part of
1689
     *
1690
     * @param string $username User name
1691
     *
1692
     * @return mixed usergroup if found or null if not found
1693
     */
1694
    public function getUserGroupForUser($username)
1695
    {
1696
        $cfgRelation = $this->relation->getRelationsParam();
1697
1698
        if (empty($cfgRelation['db'])
1699
            || empty($cfgRelation['users'])
1700
        ) {
1701
            return null;
1702
        }
1703
1704
        $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

1704
        $user_table = /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['db'])
Loading history...
1705
            . '.' . 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

1705
            . '.' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['users']);
Loading history...
1706
        $sql_query = 'SELECT `usergroup` FROM ' . $user_table
1707
            . ' WHERE `username` = \'' . $username . '\''
1708
            . ' LIMIT 1';
1709
1710
        $usergroup = $this->dbi->fetchValue(
1711
            $sql_query,
1712
            0,
1713
            0,
1714
            DatabaseInterface::CONNECT_CONTROL
1715
        );
1716
1717
        if ($usergroup === false) {
1718
            return null;
1719
        }
1720
1721
        return $usergroup;
1722
    }
1723
1724
    /**
1725
     * This function return the extra data array for the ajax behavior
1726
     *
1727
     * @param string $password  password
1728
     * @param string $sql_query sql query
1729
     * @param string $hostname  hostname
1730
     * @param string $username  username
1731
     *
1732
     * @return array
1733
     */
1734
    public function getExtraDataForAjaxBehavior(
1735
        $password,
1736
        $sql_query,
1737
        $hostname,
1738
        $username
1739
    ) {
1740
        if (isset($GLOBALS['dbname'])) {
1741
            //if (preg_match('/\\\\(?:_|%)/i', $dbname)) {
1742
            if (preg_match('/(?<!\\\\)(?:_|%)/', $GLOBALS['dbname'])) {
1743
                $dbname_is_wildcard = true;
1744
            } else {
1745
                $dbname_is_wildcard = false;
1746
            }
1747
        }
1748
1749
        $user_group_count = 0;
1750
        if ($GLOBALS['cfgRelation']['menuswork']) {
1751
            $user_group_count = $this->getUserGroupCount();
1752
        }
1753
1754
        $extra_data = [];
1755
        if (strlen($sql_query) > 0) {
1756
            $extra_data['sql_query'] = Util::getMessage(null, $sql_query);
1757
        }
1758
1759
        if (isset($_POST['change_copy'])) {
1760
            /**
1761
             * generate html on the fly for the new user that was just created.
1762
             */
1763
            $new_user_string = '<tr>' . "\n"
1764
                . '<td> <input type="checkbox" name="selected_usr[]" '
1765
                . 'id="checkbox_sel_users_"'
1766
                . 'value="'
1767
                . htmlspecialchars($username)
1768
                . '&amp;#27;' . htmlspecialchars($hostname) . '">'
1769
                . '</td>' . "\n"
1770
                . '<td><label for="checkbox_sel_users_">'
1771
                . (empty($_POST['username'])
1772
                        ? '<span style="color: #FF0000">' . __('Any') . '</span>'
1773
                        : htmlspecialchars($username) ) . '</label></td>' . "\n"
1774
                . '<td>' . htmlspecialchars($hostname) . '</td>' . "\n";
1775
1776
            $new_user_string .= '<td>';
1777
1778
            if (! empty($password) || isset($_POST['pma_pw'])) {
1779
                $new_user_string .= __('Yes');
1780
            } else {
1781
                $new_user_string .= '<span style="color: #FF0000">'
1782
                    . __('No')
1783
                . '</span>';
1784
            }
1785
1786
            $new_user_string .= '</td>' . "\n";
1787
            $new_user_string .= '<td>'
1788
                . '<code>' . implode(', ', $this->extractPrivInfo(null, true)) . '</code>'
1789
                . '</td>'; //Fill in privileges here
1790
1791
            // if $cfg['Servers'][$i]['users'] and $cfg['Servers'][$i]['usergroups'] are
1792
            // enabled
1793
            $cfgRelation = $this->relation->getRelationsParam();
1794
            if (! empty($cfgRelation['users']) && ! empty($cfgRelation['usergroups'])) {
1795
                $new_user_string .= '<td class="usrGroup"></td>';
1796
            }
1797
1798
            $new_user_string .= '<td>';
1799
            if (isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y') {
1800
                $new_user_string .= __('Yes');
1801
            } else {
1802
                $new_user_string .= __('No');
1803
            }
1804
            $new_user_string .= '</td>';
1805
1806
            if ($GLOBALS['is_grantuser']) {
1807
                $new_user_string .= '<td>'
1808
                    . $this->getUserLink('edit', $username, $hostname)
1809
                    . '</td>' . "\n";
1810
            }
1811
1812
            if ($cfgRelation['menuswork'] && $user_group_count > 0) {
1813
                $new_user_string .= '<td>'
1814
                    . $this->getUserGroupEditLink($username)
1815
                    . '</td>' . "\n";
1816
            }
1817
1818
            $new_user_string .= '<td>'
1819
                . $this->getUserLink(
1820
                    'export',
1821
                    $username,
1822
                    $hostname,
1823
                    '',
1824
                    '',
1825
                    '',
1826
                    isset($_GET['initial']) ? $_GET['initial'] : ''
1827
                )
1828
                . '</td>' . "\n";
1829
1830
            $new_user_string .= '</tr>';
1831
1832
            $extra_data['new_user_string'] = $new_user_string;
1833
1834
            /**
1835
             * Generate the string for this alphabet's initial, to update the user
1836
             * pagination
1837
             */
1838
            $new_user_initial = mb_strtoupper(
1839
                mb_substr($username, 0, 1)
1840
            );
1841
            $newUserInitialString = '<a href="' . Url::getFromRoute('/server/privileges', ['initial' => $new_user_initial]) . '">'
1842
                . $new_user_initial . '</a>';
1843
            $extra_data['new_user_initial'] = $new_user_initial;
1844
            $extra_data['new_user_initial_string'] = $newUserInitialString;
1845
        }
1846
1847
        if (isset($_POST['update_privs'])) {
1848
            $extra_data['db_specific_privs'] = false;
1849
            $extra_data['db_wildcard_privs'] = false;
1850
            if (isset($dbname_is_wildcard)) {
1851
                $extra_data['db_specific_privs'] = ! $dbname_is_wildcard;
1852
                $extra_data['db_wildcard_privs'] = $dbname_is_wildcard;
1853
            }
1854
            $new_privileges = implode(', ', $this->extractPrivInfo(null, true));
1855
1856
            $extra_data['new_privileges'] = $new_privileges;
1857
        }
1858
1859
        if (isset($_GET['validate_username'])) {
1860
            $sql_query = "SELECT * FROM `mysql`.`user` WHERE `User` = '"
1861
                . $_GET['username'] . "';";
1862
            $res = $this->dbi->query($sql_query);
1863
            $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

1863
            $row = $this->dbi->fetchRow(/** @scrutinizer ignore-type */ $res);
Loading history...
1864
            if (empty($row)) {
1865
                $extra_data['user_exists'] = false;
1866
            } else {
1867
                $extra_data['user_exists'] = true;
1868
            }
1869
        }
1870
1871
        return $extra_data;
1872
    }
1873
1874
    /**
1875
     * Get the HTML snippet for change user login information
1876
     *
1877
     * @param string $username username
1878
     * @param string $hostname host name
1879
     *
1880
     * @return string HTML snippet
1881
     */
1882
    public function getChangeLoginInformationHtmlForm($username, $hostname)
1883
    {
1884
        $choices = [
1885
            '4' => __('… keep the old one.'),
1886
            '1' => __('… delete the old one from the user tables.'),
1887
            '2' => __(
1888
                '… revoke all active privileges from '
1889
                . 'the old one and delete it afterwards.'
1890
            ),
1891
            '3' => __(
1892
                '… delete the old one from the user tables '
1893
                . 'and reload the privileges afterwards.'
1894
            ),
1895
        ];
1896
1897
        $html_output = '<form action="' . Url::getFromRoute('/server/privileges')
1898
            . '" onsubmit="return checkAddUser(this);" '
1899
            . 'method="post" class="copyUserForm submenu-item">' . "\n"
1900
            . Url::getHiddenInputs('', '')
1901
            . '<input type="hidden" name="old_username" '
1902
            . 'value="' . htmlspecialchars($username) . '">' . "\n"
1903
            . '<input type="hidden" name="old_hostname" '
1904
            . 'value="' . htmlspecialchars($hostname) . '">' . "\n";
1905
1906
        $usergroup = $this->getUserGroupForUser($username);
1907
        if ($usergroup !== null) {
1908
            $html_output .= '<input type="hidden" name="old_usergroup" '
1909
            . 'value="' . htmlspecialchars($usergroup) . '">' . "\n";
1910
        }
1911
1912
        $html_output .= '<fieldset id="fieldset_change_copy_user">' . "\n"
1913
            . '<legend data-submenu-label="' . __('Login Information') . '">' . "\n"
1914
            . __('Change login information / Copy user account')
1915
            . '</legend>' . "\n"
1916
            . $this->getHtmlForLoginInformationFields('change', $username, $hostname);
1917
1918
        $html_output .= '<fieldset id="fieldset_mode">' . "\n"
1919
            . ' <legend>'
1920
            . __('Create a new user account with the same privileges and …')
1921
            . '</legend>' . "\n";
1922
        $html_output .= Util::getRadioFields(
1923
            'mode',
1924
            $choices,
1925
            '4',
1926
            true
1927
        );
1928
        $html_output .= '</fieldset>' . "\n"
1929
           . '</fieldset>' . "\n";
1930
1931
        $html_output .= '<fieldset id="fieldset_change_copy_user_footer" '
1932
            . 'class="tblFooters">' . "\n"
1933
            . '<input type="hidden" name="change_copy" value="1">' . "\n"
1934
            . '<input class="btn btn-primary" type="submit" value="' . __('Go') . '">' . "\n"
1935
            . '</fieldset>' . "\n"
1936
            . '</form>' . "\n";
1937
1938
        return $html_output;
1939
    }
1940
1941
    /**
1942
     * Provide a line with links to the relevant database and table
1943
     *
1944
     * @param string $url_dbname url database name that urlencode() string
1945
     * @param string $dbname     database name
1946
     * @param string $tablename  table name
1947
     *
1948
     * @return string HTML snippet
1949
     */
1950
    public function getLinkToDbAndTable($url_dbname, $dbname, $tablename)
1951
    {
1952
        $scriptName = Util::getScriptNameForOption(
1953
            $GLOBALS['cfg']['DefaultTabDatabase'],
1954
            'database'
1955
        );
1956
        $html_output = '[ ' . __('Database')
1957
            . ' <a href="' . $scriptName
1958
            . Url::getCommon([
1959
                'db' => $url_dbname,
1960
                'reload' => 1,
1961
            ], strpos($scriptName, '?') === false ? '?' : '&')
1962
            . '">'
1963
            . htmlspecialchars(Util::unescapeMysqlWildcards($dbname)) . ': '
1964
            . 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

1964
            . /** @scrutinizer ignore-type */ Util::getTitleForTarget(
Loading history...
1965
                $GLOBALS['cfg']['DefaultTabDatabase']
1966
            )
1967
            . "</a> ]\n";
1968
1969
        if (strlen($tablename) > 0) {
1970
            $scriptName = Util::getScriptNameForOption(
1971
                $GLOBALS['cfg']['DefaultTabTable'],
1972
                'table'
1973
            );
1974
            $html_output .= ' [ ' . __('Table') . ' <a href="'
1975
                . $scriptName
1976
                . Url::getCommon([
1977
                    'db' => $url_dbname,
1978
                    'table' => $tablename,
1979
                    'reload' => 1,
1980
                ], strpos($scriptName, '?') === false ? '?' : '&')
1981
                . '">' . htmlspecialchars($tablename) . ': '
1982
                . 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

1982
                . /** @scrutinizer ignore-type */ Util::getTitleForTarget(
Loading history...
1983
                    $GLOBALS['cfg']['DefaultTabTable']
1984
                )
1985
                . "</a> ]\n";
1986
        }
1987
        return $html_output;
1988
    }
1989
1990
    /**
1991
     * no db name given, so we want all privs for the given user
1992
     * db name was given, so we want all user specific rights for this db
1993
     * So this function returns user rights as an array
1994
     *
1995
     * @param string $username username
1996
     * @param string $hostname host name
1997
     * @param string $type     database or table
1998
     * @param string $dbname   database name
1999
     *
2000
     * @return array database rights
2001
     */
2002
    public function getUserSpecificRights($username, $hostname, $type, $dbname = '')
2003
    {
2004
        $user_host_condition = " WHERE `User`"
2005
            . " = '" . $this->dbi->escapeString($username) . "'"
2006
            . " AND `Host`"
2007
            . " = '" . $this->dbi->escapeString($hostname) . "'";
2008
2009
        if ($type == 'database') {
2010
            $tables_to_search_for_users = [
2011
                'tables_priv',
2012
                'columns_priv',
2013
                'procs_priv',
2014
            ];
2015
            $dbOrTableName = 'Db';
2016
        } elseif ($type == 'table') {
2017
            $user_host_condition .= " AND `Db` LIKE '"
2018
                . $this->dbi->escapeString($dbname) . "'";
2019
            $tables_to_search_for_users = ['columns_priv'];
2020
            $dbOrTableName = 'Table_name';
2021
        } else { // routine
2022
            $user_host_condition .= " AND `Db` LIKE '"
2023
                . $this->dbi->escapeString($dbname) . "'";
2024
            $tables_to_search_for_users = ['procs_priv'];
2025
            $dbOrTableName = 'Routine_name';
2026
        }
2027
2028
        // we also want privileges for this user not in table `db` but in other table
2029
        $tables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`;');
2030
2031
        $db_rights_sqls = [];
2032
        foreach ($tables_to_search_for_users as $table_search_in) {
2033
            if (in_array($table_search_in, $tables)) {
2034
                $db_rights_sqls[] = '
2035
                    SELECT DISTINCT `' . $dbOrTableName . '`
2036
                    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

2036
                    FROM `mysql`.' . /** @scrutinizer ignore-type */ Util::backquote($table_search_in)
Loading history...
2037
                   . $user_host_condition;
2038
            }
2039
        }
2040
2041
        $user_defaults = [
2042
            $dbOrTableName  => '',
2043
            'Grant_priv'    => 'N',
2044
            'privs'         => ['USAGE'],
2045
            'Column_priv'   => true,
2046
        ];
2047
2048
        // for the rights
2049
        $db_rights = [];
2050
2051
        $db_rights_sql = '(' . implode(') UNION (', $db_rights_sqls) . ')'
2052
            . ' ORDER BY `' . $dbOrTableName . '` ASC';
2053
2054
        $db_rights_result = $this->dbi->query($db_rights_sql);
2055
2056
        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

2056
        while ($db_rights_row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $db_rights_result)) {
Loading history...
2057
            $db_rights_row = array_merge($user_defaults, $db_rights_row);
2058
            if ($type == 'database') {
2059
                // only Db names in the table `mysql`.`db` uses wildcards
2060
                // as we are in the db specific rights display we want
2061
                // all db names escaped, also from other sources
2062
                $db_rights_row['Db'] = Util::escapeMysqlWildcards(
2063
                    $db_rights_row['Db']
2064
                );
2065
            }
2066
            $db_rights[$db_rights_row[$dbOrTableName]] = $db_rights_row;
2067
        }
2068
2069
        $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

2069
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $db_rights_result);
Loading history...
2070
2071
        if ($type == 'database') {
2072
            $sql_query = 'SELECT * FROM `mysql`.`db`'
2073
                . $user_host_condition . ' ORDER BY `Db` ASC';
2074
        } elseif ($type == 'table') {
2075
            $sql_query = 'SELECT `Table_name`,'
2076
                . ' `Table_priv`,'
2077
                . ' IF(`Column_priv` = _latin1 \'\', 0, 1)'
2078
                . ' AS \'Column_priv\''
2079
                . ' FROM `mysql`.`tables_priv`'
2080
                . $user_host_condition
2081
                . ' ORDER BY `Table_name` ASC;';
2082
        } else {
2083
            $sql_query = "SELECT `Routine_name`, `Proc_priv`"
2084
                . " FROM `mysql`.`procs_priv`"
2085
                . $user_host_condition
2086
                . " ORDER BY `Routine_name`";
2087
        }
2088
2089
        $result = $this->dbi->query($sql_query);
2090
2091
        while ($row = $this->dbi->fetchAssoc($result)) {
2092
            if (isset($db_rights[$row[$dbOrTableName]])) {
2093
                $db_rights[$row[$dbOrTableName]]
2094
                    = array_merge($db_rights[$row[$dbOrTableName]], $row);
2095
            } else {
2096
                $db_rights[$row[$dbOrTableName]] = $row;
2097
            }
2098
            if ($type == 'database') {
2099
                // there are db specific rights for this user
2100
                // so we can drop this db rights
2101
                $db_rights[$row['Db']]['can_delete'] = true;
2102
            }
2103
        }
2104
        $this->dbi->freeResult($result);
2105
        return $db_rights;
2106
    }
2107
2108
    /**
2109
     * Parses Proc_priv data
2110
     *
2111
     * @param string $privs Proc_priv
2112
     *
2113
     * @return array
2114
     */
2115
    public function parseProcPriv($privs)
2116
    {
2117
        $result = [
2118
            'Alter_routine_priv' => 'N',
2119
            'Execute_priv'       => 'N',
2120
            'Grant_priv'         => 'N',
2121
        ];
2122
        foreach (explode(',', (string) $privs) as $priv) {
2123
            if ($priv == 'Alter Routine') {
2124
                $result['Alter_routine_priv'] = 'Y';
2125
            } else {
2126
                $result[$priv . '_priv'] = 'Y';
2127
            }
2128
        }
2129
        return $result;
2130
    }
2131
2132
    /**
2133
     * Get a HTML table for display user's tabel specific or database specific rights
2134
     *
2135
     * @param string $username username
2136
     * @param string $hostname host name
2137
     * @param string $type     database, table or routine
2138
     * @param string $dbname   database name
2139
     *
2140
     * @return string
2141
     */
2142
    public function getHtmlForAllTableSpecificRights(
2143
        $username,
2144
        $hostname,
2145
        $type,
2146
        $dbname = ''
2147
    ) {
2148
        $uiData = [
2149
            'database' => [
2150
                'form_id'        => 'database_specific_priv',
2151
                'sub_menu_label' => __('Database'),
2152
                'legend'         => __('Database-specific privileges'),
2153
                'type_label'     => __('Database'),
2154
            ],
2155
            'table' => [
2156
                'form_id'        => 'table_specific_priv',
2157
                'sub_menu_label' => __('Table'),
2158
                'legend'         => __('Table-specific privileges'),
2159
                'type_label'     => __('Table'),
2160
            ],
2161
            'routine' => [
2162
                'form_id'        => 'routine_specific_priv',
2163
                'sub_menu_label' => __('Routine'),
2164
                'legend'         => __('Routine-specific privileges'),
2165
                'type_label'     => __('Routine'),
2166
            ],
2167
        ];
2168
2169
        /**
2170
         * no db name given, so we want all privs for the given user
2171
         * db name was given, so we want all user specific rights for this db
2172
         */
2173
        $db_rights = $this->getUserSpecificRights($username, $hostname, $type, $dbname);
2174
        ksort($db_rights);
2175
2176
        $foundRows = [];
2177
        $privileges = [];
2178
        foreach ($db_rights as $row) {
2179
            $onePrivilege = [];
2180
2181
            $paramTableName = '';
2182
            $paramRoutineName = '';
2183
2184
            if ($type == 'database') {
2185
                $name = $row['Db'];
2186
                $onePrivilege['grant']        = $row['Grant_priv'] == 'Y';
2187
                $onePrivilege['table_privs']   = ! empty($row['Table_priv'])
2188
                    || ! empty($row['Column_priv']);
2189
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
2190
2191
                $paramDbName = $row['Db'];
2192
            } elseif ($type == 'table') {
2193
                $name = $row['Table_name'];
2194
                $onePrivilege['grant'] = in_array(
2195
                    'Grant',
2196
                    explode(',', $row['Table_priv'])
2197
                );
2198
                $onePrivilege['column_privs']  = ! empty($row['Column_priv']);
2199
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
2200
2201
                $paramDbName = $dbname;
2202
                $paramTableName = $row['Table_name'];
2203
            } else { // routine
2204
                $name = $row['Routine_name'];
2205
                $onePrivilege['grant'] = in_array(
2206
                    'Grant',
2207
                    explode(',', $row['Proc_priv'])
2208
                );
2209
2210
                $privs = $this->parseProcPriv($row['Proc_priv']);
2211
                $onePrivilege['privileges'] = implode(
2212
                    ',',
2213
                    $this->extractPrivInfo($privs, true)
2214
                );
2215
2216
                $paramDbName = $dbname;
2217
                $paramRoutineName = $row['Routine_name'];
2218
            }
2219
2220
            $foundRows[] = $name;
2221
            $onePrivilege['name'] = $name;
2222
2223
            $onePrivilege['edit_link'] = '';
2224
            if ($GLOBALS['is_grantuser']) {
2225
                $onePrivilege['edit_link'] = $this->getUserLink(
2226
                    'edit',
2227
                    $username,
2228
                    $hostname,
2229
                    $paramDbName,
2230
                    $paramTableName,
2231
                    $paramRoutineName
2232
                );
2233
            }
2234
2235
            $onePrivilege['revoke_link'] = '';
2236
            if ($type != 'database' || ! empty($row['can_delete'])) {
2237
                $onePrivilege['revoke_link'] = $this->getUserLink(
2238
                    'revoke',
2239
                    $username,
2240
                    $hostname,
2241
                    $paramDbName,
2242
                    $paramTableName,
2243
                    $paramRoutineName
2244
                );
2245
            }
2246
2247
            $privileges[] = $onePrivilege;
2248
        }
2249
2250
        $data = $uiData[$type];
2251
        $data['privileges'] = $privileges;
2252
        $data['username']   = $username;
2253
        $data['hostname']   = $hostname;
2254
        $data['database']   = $dbname;
2255
        $data['type']       = $type;
2256
2257
        if ($type == 'database') {
2258
            // we already have the list of databases from libraries/common.inc.php
2259
            // via $pma = new PMA;
2260
            $pred_db_array = $GLOBALS['dblist']->databases;
2261
            $databases_to_skip = [
2262
                'information_schema',
2263
                'performance_schema',
2264
            ];
2265
2266
            $databases = [];
2267
            if (! empty($pred_db_array)) {
2268
                foreach ($pred_db_array as $current_db) {
2269
                    if (in_array($current_db, $databases_to_skip)) {
2270
                        continue;
2271
                    }
2272
                    $current_db_escaped = Util::escapeMysqlWildcards($current_db);
2273
                    // cannot use array_diff() once, outside of the loop,
2274
                    // because the list of databases has special characters
2275
                    // already escaped in $foundRows,
2276
                    // contrary to the output of SHOW DATABASES
2277
                    if (! in_array($current_db_escaped, $foundRows)) {
2278
                        $databases[] = $current_db;
2279
                    }
2280
                }
2281
            }
2282
            $data['databases'] = $databases;
2283
        } elseif ($type == 'table') {
2284
            $result = @$this->dbi->tryQuery(
2285
                "SHOW TABLES FROM " . Util::backquote($dbname),
2286
                DatabaseInterface::CONNECT_USER,
2287
                DatabaseInterface::QUERY_STORE
2288
            );
2289
2290
            $tables = [];
2291
            if ($result) {
2292
                while ($row = $this->dbi->fetchRow($result)) {
2293
                    if (! in_array($row[0], $foundRows)) {
2294
                        $tables[] = $row[0];
2295
                    }
2296
                }
2297
                $this->dbi->freeResult($result);
2298
            }
2299
            $data['tables'] = $tables;
2300
        } else { // routine
2301
            $routineData = $this->dbi->getRoutines($dbname);
2302
2303
            $routines = [];
2304
            foreach ($routineData as $routine) {
2305
                if (! in_array($routine['name'], $foundRows)) {
2306
                    $routines[] = $routine['name'];
2307
                }
2308
            }
2309
            $data['routines'] = $routines;
2310
        }
2311
2312
        return $this->template->render('server/privileges/privileges_summary', $data);
2313
    }
2314
2315
    /**
2316
     * Get HTML for display the users overview
2317
     * (if less than 50 users, display them immediately)
2318
     *
2319
     * @param array  $result        ran sql query
2320
     * @param array  $db_rights     user's database rights array
2321
     * @param string $pmaThemeImage a image source link
2322
     * @param string $text_dir      text directory
2323
     *
2324
     * @return string HTML snippet
2325
     */
2326
    public function getUsersOverview($result, array $db_rights, $pmaThemeImage, $text_dir)
2327
    {
2328
        global $is_grantuser, $is_createuser;
2329
2330
        $cfgRelation = $this->relation->getRelationsParam();
2331
2332
        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

2332
        while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $result)) {
Loading history...
2333
            $row['privs'] = $this->extractPrivInfo($row, true);
2334
            $db_rights[$row['User']][$row['Host']] = $row;
2335
        }
2336
        $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

2336
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
2337
2338
        $user_group_count = 0;
2339
        if ($cfgRelation['menuswork']) {
2340
            $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

2340
            $sql_query = 'SELECT * FROM ' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['db'])
Loading history...
2341
                . '.' . 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

2341
                . '.' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['users']);
Loading history...
2342
            $result = $this->relation->queryAsControlUser($sql_query, false);
2343
            $group_assignment = [];
2344
            if ($result) {
2345
                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

2345
                while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $result)) {
Loading history...
2346
                    $group_assignment[$row['username']] = $row['usergroup'];
2347
                }
2348
            }
2349
            $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

2349
            $this->dbi->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
2350
2351
            $user_group_count = $this->getUserGroupCount();
2352
        }
2353
2354
        $hosts = [];
2355
        foreach ($db_rights as $user) {
2356
            ksort($user);
2357
            foreach ($user as $host) {
2358
                $check_plugin_query = "SELECT * FROM `mysql`.`user` WHERE "
2359
                    . "`User` = '" . $host['User'] . "' AND `Host` = '"
2360
                    . $host['Host'] . "'";
2361
                $res = $this->dbi->fetchSingleRow($check_plugin_query);
2362
2363
                $hasPassword = false;
2364
                if ((isset($res['authentication_string'])
2365
                    && ! empty($res['authentication_string']))
2366
                    || (isset($res['Password'])
2367
                    && ! empty($res['Password']))
2368
                ) {
2369
                    $hasPassword = true;
2370
                }
2371
2372
                $hosts[] = [
2373
                    'user' => $host['User'],
2374
                    'host' => $host['Host'],
2375
                    'has_password' => $hasPassword,
2376
                    'has_select_priv' => isset($host['Select_priv']),
2377
                    'privileges' => $host['privs'],
2378
                    'group' => $group_assignment[$host['User']] ?? '',
2379
                    'has_grant' => $host['Grant_priv'] == 'Y',
2380
                ];
2381
            }
2382
        }
2383
2384
        return $this->template->render('server/privileges/users_overview', [
2385
            'menus_work' => $cfgRelation['menuswork'],
2386
            'user_group_count' => $user_group_count,
2387
            'pma_theme_image' => $pmaThemeImage,
2388
            'text_dir' => $text_dir,
2389
            'initial' => $_GET['initial'] ?? '',
2390
            'hosts' => $hosts,
2391
            'is_grantuser' => $is_grantuser,
2392
            'is_createuser' => $is_createuser,
2393
        ]);
2394
    }
2395
2396
    /**
2397
     * Get HTML for Displays the initials
2398
     *
2399
     * @param array $array_initials array for all initials, even non A-Z
2400
     *
2401
     * @return string HTML snippet
2402
     */
2403
    public function getHtmlForInitials(array $array_initials)
2404
    {
2405
        // initialize to false the letters A-Z
2406
        for ($letter_counter = 1; $letter_counter < 27; $letter_counter++) {
2407
            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

2407
            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...
2408
                $array_initials[mb_chr($letter_counter + 64)] = false;
2409
            }
2410
        }
2411
2412
        $initials = $this->dbi->tryQuery(
2413
            'SELECT DISTINCT UPPER(LEFT(`User`,1)) FROM `user`'
2414
            . ' ORDER BY UPPER(LEFT(`User`,1)) ASC',
2415
            DatabaseInterface::CONNECT_USER,
2416
            DatabaseInterface::QUERY_STORE
2417
        );
2418
        if ($initials) {
2419
            while (list($tmp_initial) = $this->dbi->fetchRow($initials)) {
2420
                $array_initials[$tmp_initial] = true;
2421
            }
2422
        }
2423
2424
        // Display the initials, which can be any characters, not
2425
        // just letters. For letters A-Z, we add the non-used letters
2426
        // as greyed out.
2427
2428
        uksort($array_initials, "strnatcasecmp");
2429
2430
        return $this->template->render('server/privileges/initials_row', [
2431
            'array_initials' => $array_initials,
2432
            'initial' => isset($_GET['initial']) ? $_GET['initial'] : null,
2433
        ]);
2434
    }
2435
2436
    /**
2437
     * Get the database rights array for Display user overview
2438
     *
2439
     * @return array    database rights array
2440
     */
2441
    public function getDbRightsForUserOverview()
2442
    {
2443
        // we also want users not in table `user` but in other table
2444
        $tables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`;');
2445
2446
        $tablesSearchForUsers = [
2447
            'user',
2448
            'db',
2449
            'tables_priv',
2450
            'columns_priv',
2451
            'procs_priv',
2452
        ];
2453
2454
        $db_rights_sqls = [];
2455
        foreach ($tablesSearchForUsers as $table_search_in) {
2456
            if (in_array($table_search_in, $tables)) {
2457
                $db_rights_sqls[] = 'SELECT DISTINCT `User`, `Host` FROM `mysql`.`'
2458
                    . $table_search_in . '` '
2459
                    . (isset($_GET['initial'])
2460
                    ? $this->rangeOfUsers($_GET['initial'])
2461
                    : '');
2462
            }
2463
        }
2464
        $user_defaults = [
2465
            'User'       => '',
2466
            'Host'       => '%',
2467
            'Password'   => '?',
2468
            'Grant_priv' => 'N',
2469
            'privs'      => ['USAGE'],
2470
        ];
2471
2472
        // for the rights
2473
        $db_rights = [];
2474
2475
        $db_rights_sql = '(' . implode(') UNION (', $db_rights_sqls) . ')'
2476
            . ' ORDER BY `User` ASC, `Host` ASC';
2477
2478
        $db_rights_result = $this->dbi->query($db_rights_sql);
2479
2480
        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

2480
        while ($db_rights_row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $db_rights_result)) {
Loading history...
2481
            $db_rights_row = array_merge($user_defaults, $db_rights_row);
2482
            $db_rights[$db_rights_row['User']][$db_rights_row['Host']]
2483
                = $db_rights_row;
2484
        }
2485
        $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

2485
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $db_rights_result);
Loading history...
2486
        ksort($db_rights);
2487
2488
        return $db_rights;
2489
    }
2490
2491
    /**
2492
     * Delete user and get message and sql query for delete user in privileges
2493
     *
2494
     * @param array $queries queries
2495
     *
2496
     * @return array Message
2497
     */
2498
    public function deleteUser(array $queries)
2499
    {
2500
        $sql_query = '';
2501
        if (empty($queries)) {
2502
            $message = Message::error(__('No users selected for deleting!'));
2503
        } else {
2504
            if ($_POST['mode'] == 3) {
2505
                $queries[] = '# ' . __('Reloading the privileges') . ' …';
2506
                $queries[] = 'FLUSH PRIVILEGES;';
2507
            }
2508
            $drop_user_error = '';
2509
            foreach ($queries as $sql_query) {
2510
                if ($sql_query[0] != '#') {
2511
                    if (! $this->dbi->tryQuery($sql_query)) {
2512
                        $drop_user_error .= $this->dbi->getError() . "\n";
2513
                    }
2514
                }
2515
            }
2516
            // tracking sets this, causing the deleted db to be shown in navi
2517
            unset($GLOBALS['db']);
2518
2519
            $sql_query = implode("\n", $queries);
2520
            if (! empty($drop_user_error)) {
2521
                $message = Message::rawError($drop_user_error);
2522
            } else {
2523
                $message = Message::success(
2524
                    __('The selected users have been deleted successfully.')
2525
                );
2526
            }
2527
        }
2528
        return [
2529
            $sql_query,
2530
            $message,
2531
        ];
2532
    }
2533
2534
    /**
2535
     * Update the privileges and return the success or error message
2536
     *
2537
     * @param string $username  username
2538
     * @param string $hostname  host name
2539
     * @param string $tablename table name
2540
     * @param string $dbname    database name
2541
     * @param string $itemType  item type
2542
     *
2543
     * @return array success message or error message for update
2544
     */
2545
    public function updatePrivileges($username, $hostname, $tablename, $dbname, $itemType)
2546
    {
2547
        $db_and_table = $this->wildcardEscapeForGrant($dbname, $tablename);
2548
2549
        $sql_query0 = 'REVOKE ALL PRIVILEGES ON ' . $itemType . ' ' . $db_and_table
2550
            . ' FROM \'' . $this->dbi->escapeString($username)
2551
            . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
2552
2553
        if (! isset($_POST['Grant_priv']) || $_POST['Grant_priv'] != 'Y') {
2554
            $sql_query1 = 'REVOKE GRANT OPTION ON ' . $itemType . ' ' . $db_and_table
2555
                . ' FROM \'' . $this->dbi->escapeString($username) . '\'@\''
2556
                . $this->dbi->escapeString($hostname) . '\';';
2557
        } else {
2558
            $sql_query1 = '';
2559
        }
2560
2561
        // Should not do a GRANT USAGE for a table-specific privilege, it
2562
        // causes problems later (cannot revoke it)
2563
        if (! (strlen($tablename) > 0
2564
            && 'USAGE' == implode('', $this->extractPrivInfo()))
2565
        ) {
2566
            $sql_query2 = 'GRANT ' . implode(', ', $this->extractPrivInfo())
2567
                . ' ON ' . $itemType . ' ' . $db_and_table
2568
                . ' TO \'' . $this->dbi->escapeString($username) . '\'@\''
2569
                . $this->dbi->escapeString($hostname) . '\'';
2570
2571
            if (strlen($dbname) === 0) {
2572
                // add REQUIRE clause
2573
                $sql_query2 .= $this->getRequireClause();
2574
            }
2575
2576
            if ((isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y')
2577
                || (strlen($dbname) === 0
2578
                && (isset($_POST['max_questions']) || isset($_POST['max_connections'])
2579
                || isset($_POST['max_updates'])
2580
                || isset($_POST['max_user_connections'])))
2581
            ) {
2582
                $sql_query2 .= $this->getWithClauseForAddUserAndUpdatePrivs();
2583
            }
2584
            $sql_query2 .= ';';
2585
        }
2586
        if (! $this->dbi->tryQuery($sql_query0)) {
2587
            // This might fail when the executing user does not have
2588
            // ALL PRIVILEGES himself.
2589
            // See https://github.com/phpmyadmin/phpmyadmin/issues/9673
2590
            $sql_query0 = '';
2591
        }
2592
        if (! empty($sql_query1) && ! $this->dbi->tryQuery($sql_query1)) {
2593
            // this one may fail, too...
2594
            $sql_query1 = '';
2595
        }
2596
        if (! empty($sql_query2)) {
2597
            $this->dbi->query($sql_query2);
2598
        } else {
2599
            $sql_query2 = '';
2600
        }
2601
        $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...
2602
        $message = Message::success(__('You have updated the privileges for %s.'));
2603
        $message->addParam('\'' . $username . '\'@\'' . $hostname . '\'');
2604
2605
        return [
2606
            $sql_query,
2607
            $message,
2608
        ];
2609
    }
2610
2611
    /**
2612
     * Get List of information: Changes / copies a user
2613
     *
2614
     * @return array
2615
     */
2616
    public function getDataForChangeOrCopyUser()
2617
    {
2618
        $queries = null;
2619
        $password = null;
2620
2621
        if (isset($_POST['change_copy'])) {
2622
            $user_host_condition = ' WHERE `User` = '
2623
                . "'" . $this->dbi->escapeString($_POST['old_username']) . "'"
2624
                . ' AND `Host` = '
2625
                . "'" . $this->dbi->escapeString($_POST['old_hostname']) . "';";
2626
            $row = $this->dbi->fetchSingleRow(
2627
                'SELECT * FROM `mysql`.`user` ' . $user_host_condition
2628
            );
2629
            if (! $row) {
2630
                $response = Response::getInstance();
2631
                $response->addHTML(
2632
                    Message::notice(__('No user found.'))->getDisplay()
2633
                );
2634
                unset($_POST['change_copy']);
2635
            } else {
2636
                foreach ($row as $key => $value) {
2637
                    $GLOBALS[$key] = $value;
2638
                }
2639
                $serverVersion = $this->dbi->getVersion();
2640
                // Recent MySQL versions have the field "Password" in mysql.user,
2641
                // so the previous extract creates $row['Password'] but this script
2642
                // uses $password
2643
                if (! isset($row['password']) && isset($row['Password'])) {
2644
                    $row['password'] = $row['Password'];
2645
                }
2646
                if (Util::getServerType() == 'MySQL'
2647
                    && $serverVersion >= 50606
2648
                    && $serverVersion < 50706
2649
                    && ((isset($row['authentication_string'])
2650
                    && empty($row['password']))
2651
                    || (isset($row['plugin'])
2652
                    && $row['plugin'] == 'sha256_password'))
2653
                ) {
2654
                    $row['password'] = $row['authentication_string'];
2655
                }
2656
2657
                if (Util::getServerType() == 'MariaDB'
2658
                    && $serverVersion >= 50500
2659
                    && isset($row['authentication_string'])
2660
                    && empty($row['password'])
2661
                ) {
2662
                    $row['password'] = $row['authentication_string'];
2663
                }
2664
2665
                // Always use 'authentication_string' column
2666
                // for MySQL 5.7.6+ since it does not have
2667
                // the 'password' column at all
2668
                if (in_array(Util::getServerType(), ['MySQL', 'Percona Server'])
2669
                    && $serverVersion >= 50706
2670
                    && isset($row['authentication_string'])
2671
                ) {
2672
                    $row['password'] = $row['authentication_string'];
2673
                }
2674
                $password = $row['password'];
2675
                $queries = [];
2676
            }
2677
        }
2678
2679
        return [
2680
            $queries,
2681
            $password,
2682
        ];
2683
    }
2684
2685
    /**
2686
     * Update Data for information: Deletes users
2687
     *
2688
     * @param array $queries queries array
2689
     *
2690
     * @return array
2691
     */
2692
    public function getDataForDeleteUsers($queries)
2693
    {
2694
        if (isset($_POST['change_copy'])) {
2695
            $selected_usr = [
2696
                $_POST['old_username'] . '&amp;#27;' . $_POST['old_hostname'],
2697
            ];
2698
        } else {
2699
            $selected_usr = $_POST['selected_usr'];
2700
            $queries = [];
2701
        }
2702
2703
        // this happens, was seen in https://reports.phpmyadmin.net/reports/view/17146
2704
        if (! is_array($selected_usr)) {
2705
            return [];
2706
        }
2707
2708
        foreach ($selected_usr as $each_user) {
2709
            list($this_user, $this_host) = explode('&amp;#27;', $each_user);
2710
            $queries[] = '# '
2711
                . sprintf(
2712
                    __('Deleting %s'),
2713
                    '\'' . $this_user . '\'@\'' . $this_host . '\''
2714
                )
2715
                . ' ...';
2716
            $queries[] = 'DROP USER \''
2717
                . $this->dbi->escapeString($this_user)
2718
                . '\'@\'' . $this->dbi->escapeString($this_host) . '\';';
2719
            $this->relationCleanup->user($this_user);
2720
2721
            if (isset($_POST['drop_users_db'])) {
2722
                $queries[] = 'DROP DATABASE IF EXISTS '
2723
                    . Util::backquote($this_user) . ';';
2724
                $GLOBALS['reload'] = true;
2725
            }
2726
        }
2727
        return $queries;
2728
    }
2729
2730
    /**
2731
     * update Message For Reload
2732
     *
2733
     * @return Message|null
2734
     */
2735
    public function updateMessageForReload(): ?Message
2736
    {
2737
        $message = null;
2738
        if (isset($_GET['flush_privileges'])) {
2739
            $sql_query = 'FLUSH PRIVILEGES;';
2740
            $this->dbi->query($sql_query);
2741
            $message = Message::success(
2742
                __('The privileges were reloaded successfully.')
2743
            );
2744
        }
2745
2746
        if (isset($_GET['validate_username'])) {
2747
            $message = Message::success();
2748
        }
2749
2750
        return $message;
2751
    }
2752
2753
    /**
2754
     * update Data For Queries from queries_for_display
2755
     *
2756
     * @param array      $queries             queries array
2757
     * @param array|null $queries_for_display queries array for display
2758
     *
2759
     * @return array
2760
     */
2761
    public function getDataForQueries(array $queries, $queries_for_display)
2762
    {
2763
        $tmp_count = 0;
2764
        foreach ($queries as $sql_query) {
2765
            if ($sql_query[0] != '#') {
2766
                $this->dbi->query($sql_query);
2767
            }
2768
            // when there is a query containing a hidden password, take it
2769
            // instead of the real query sent
2770
            if (isset($queries_for_display[$tmp_count])) {
2771
                $queries[$tmp_count] = $queries_for_display[$tmp_count];
2772
            }
2773
            $tmp_count++;
2774
        }
2775
2776
        return $queries;
2777
    }
2778
2779
    /**
2780
     * update Data for information: Adds a user
2781
     *
2782
     * @param string|array|null $dbname      db name
2783
     * @param string            $username    user name
2784
     * @param string            $hostname    host name
2785
     * @param string|null       $password    password
2786
     * @param bool              $is_menuwork is_menuwork set?
2787
     *
2788
     * @return array
2789
     */
2790
    public function addUser(
2791
        $dbname,
2792
        $username,
2793
        $hostname,
2794
        ?string $password,
2795
        $is_menuwork
2796
    ) {
2797
        $_add_user_error = false;
2798
        $message = null;
2799
        $queries = null;
2800
        $queries_for_display = null;
2801
        $sql_query = null;
2802
2803
        if (! isset($_POST['adduser_submit']) && ! isset($_POST['change_copy'])) {
2804
            return [
2805
                $message,
2806
                $queries,
2807
                $queries_for_display,
2808
                $sql_query,
2809
                $_add_user_error,
2810
            ];
2811
        }
2812
2813
        $sql_query = '';
2814
        if ($_POST['pred_username'] == 'any') {
2815
            $username = '';
2816
        }
2817
        switch ($_POST['pred_hostname']) {
2818
            case 'any':
2819
                $hostname = '%';
2820
                break;
2821
            case 'localhost':
2822
                $hostname = 'localhost';
2823
                break;
2824
            case 'hosttable':
2825
                $hostname = '';
2826
                break;
2827
            case 'thishost':
2828
                $_user_name = $this->dbi->fetchValue('SELECT USER()');
2829
                $hostname = mb_substr(
2830
                    $_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

2830
                    /** @scrutinizer ignore-type */ $_user_name,
Loading history...
2831
                    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

2831
                    mb_strrpos(/** @scrutinizer ignore-type */ $_user_name, '@') + 1
Loading history...
2832
                );
2833
                unset($_user_name);
2834
                break;
2835
        }
2836
        $sql = "SELECT '1' FROM `mysql`.`user`"
2837
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
2838
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';";
2839
        if ($this->dbi->fetchValue($sql) == 1) {
2840
            $message = Message::error(__('The user %s already exists!'));
2841
            $message->addParam('[em]\'' . $username . '\'@\'' . $hostname . '\'[/em]');
2842
            $_GET['adduser'] = true;
2843
            $_add_user_error = true;
2844
2845
            return [
2846
                $message,
2847
                $queries,
2848
                $queries_for_display,
2849
                $sql_query,
2850
                $_add_user_error,
2851
            ];
2852
        }
2853
2854
        list(
2855
            $create_user_real,
2856
            $create_user_show,
2857
            $real_sql_query,
2858
            $sql_query,
2859
            $password_set_real,
2860
            $password_set_show,
2861
            $alter_real_sql_query,
2862
            $alter_sql_query
2863
        ) = $this->getSqlQueriesForDisplayAndAddUser(
2864
            $username,
2865
            $hostname,
2866
            (isset($password) ? $password : '')
2867
        );
2868
2869
        if (empty($_POST['change_copy'])) {
2870
            $_error = false;
2871
2872
            if ($create_user_real !== null) {
0 ignored issues
show
introduced by
The condition $create_user_real !== null is always true.
Loading history...
2873
                if (! $this->dbi->tryQuery($create_user_real)) {
2874
                    $_error = true;
2875
                }
2876
                if (isset($password_set_real, $_POST['authentication_plugin']) && ! empty($password_set_real)) {
2877
                    $this->setProperPasswordHashing(
2878
                        $_POST['authentication_plugin']
2879
                    );
2880
                    if ($this->dbi->tryQuery($password_set_real)) {
2881
                        $sql_query .= $password_set_show;
2882
                    }
2883
                }
2884
                $sql_query = $create_user_show . $sql_query;
2885
            }
2886
2887
            list($sql_query, $message) = $this->addUserAndCreateDatabase(
2888
                $_error,
2889
                $real_sql_query,
2890
                $sql_query,
2891
                $username,
2892
                $hostname,
2893
                $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

2893
                /** @scrutinizer ignore-type */ $dbname,
Loading history...
2894
                $alter_real_sql_query,
2895
                $alter_sql_query
2896
            );
2897
            if (! empty($_POST['userGroup']) && $is_menuwork) {
2898
                $this->setUserGroup($GLOBALS['username'], $_POST['userGroup']);
2899
            }
2900
2901
            return [
2902
                $message,
2903
                $queries,
2904
                $queries_for_display,
2905
                $sql_query,
2906
                $_add_user_error,
2907
            ];
2908
        }
2909
2910
        // Copy the user group while copying a user
2911
        $old_usergroup =
2912
            isset($_POST['old_usergroup']) ? $_POST['old_usergroup'] : null;
2913
        $this->setUserGroup($_POST['username'], $old_usergroup);
2914
2915
        if ($create_user_real === null) {
0 ignored issues
show
introduced by
The condition $create_user_real === null is always false.
Loading history...
2916
            $queries[] = $create_user_real;
2917
        }
2918
        $queries[] = $real_sql_query;
2919
2920
        if (isset($password_set_real, $_POST['authentication_plugin']) && ! empty($password_set_real)) {
2921
            $this->setProperPasswordHashing(
2922
                $_POST['authentication_plugin']
2923
            );
2924
2925
            $queries[] = $password_set_real;
2926
        }
2927
        // we put the query containing the hidden password in
2928
        // $queries_for_display, at the same position occupied
2929
        // by the real query in $queries
2930
        $tmp_count = count($queries);
2931
        if (isset($create_user_real)) {
2932
            $queries_for_display[$tmp_count - 2] = $create_user_show;
2933
        }
2934
        if (isset($password_set_real) && ! empty($password_set_real)) {
2935
            $queries_for_display[$tmp_count - 3] = $create_user_show;
2936
            $queries_for_display[$tmp_count - 2] = $sql_query;
2937
            $queries_for_display[$tmp_count - 1] = $password_set_show;
2938
        } else {
2939
            $queries_for_display[$tmp_count - 1] = $sql_query;
2940
        }
2941
2942
        return [
2943
            $message,
2944
            $queries,
2945
            $queries_for_display,
2946
            $sql_query,
2947
            $_add_user_error,
2948
        ];
2949
    }
2950
2951
    /**
2952
     * Sets proper value of `old_passwords` according to
2953
     * the authentication plugin selected
2954
     *
2955
     * @param string $auth_plugin authentication plugin selected
2956
     *
2957
     * @return void
2958
     */
2959
    public function setProperPasswordHashing($auth_plugin)
2960
    {
2961
        // Set the hashing method used by PASSWORD()
2962
        // to be of type depending upon $authentication_plugin
2963
        if ($auth_plugin == 'sha256_password') {
2964
            $this->dbi->tryQuery('SET `old_passwords` = 2;');
2965
        } elseif ($auth_plugin == 'mysql_old_password') {
2966
            $this->dbi->tryQuery('SET `old_passwords` = 1;');
2967
        } else {
2968
            $this->dbi->tryQuery('SET `old_passwords` = 0;');
2969
        }
2970
    }
2971
2972
    /**
2973
     * Update DB information: DB, Table, isWildcard
2974
     *
2975
     * @return array
2976
     */
2977
    public function getDataForDBInfo()
2978
    {
2979
        $username = null;
2980
        $hostname = null;
2981
        $dbname = null;
2982
        $tablename = null;
2983
        $routinename = null;
2984
        $dbname_is_wildcard = null;
2985
2986
        if (isset($_REQUEST['username'])) {
2987
            $username = $_REQUEST['username'];
2988
        }
2989
        if (isset($_REQUEST['hostname'])) {
2990
            $hostname = $_REQUEST['hostname'];
2991
        }
2992
        /**
2993
         * Checks if a dropdown box has been used for selecting a database / table
2994
         */
2995
        if (Core::isValid($_POST['pred_tablename'])) {
2996
            $tablename = $_POST['pred_tablename'];
2997
        } elseif (Core::isValid($_REQUEST['tablename'])) {
2998
            $tablename = $_REQUEST['tablename'];
2999
        } else {
3000
            unset($tablename);
3001
        }
3002
3003
        if (Core::isValid($_POST['pred_routinename'])) {
3004
            $routinename = $_POST['pred_routinename'];
3005
        } elseif (Core::isValid($_REQUEST['routinename'])) {
3006
            $routinename = $_REQUEST['routinename'];
3007
        } else {
3008
            unset($routinename);
3009
        }
3010
3011
        if (isset($_POST['pred_dbname'])) {
3012
            $is_valid_pred_dbname = true;
3013
            foreach ($_POST['pred_dbname'] as $key => $db_name) {
3014
                if (! Core::isValid($db_name)) {
3015
                    $is_valid_pred_dbname = false;
3016
                    break;
3017
                }
3018
            }
3019
        }
3020
3021
        if (isset($_REQUEST['dbname'])) {
3022
            $is_valid_dbname = true;
3023
            if (is_array($_REQUEST['dbname'])) {
3024
                foreach ($_REQUEST['dbname'] as $key => $db_name) {
3025
                    if (! Core::isValid($db_name)) {
3026
                        $is_valid_dbname = false;
3027
                        break;
3028
                    }
3029
                }
3030
            } else {
3031
                if (! Core::isValid($_REQUEST['dbname'])) {
3032
                    $is_valid_dbname = false;
3033
                }
3034
            }
3035
        }
3036
3037
        if (isset($is_valid_pred_dbname) && $is_valid_pred_dbname) {
3038
            $dbname = $_POST['pred_dbname'];
3039
            // If dbname contains only one database.
3040
            if (count($dbname) === 1) {
3041
                $dbname = $dbname[0];
3042
            }
3043
        } elseif (isset($is_valid_dbname) && $is_valid_dbname) {
3044
            $dbname = $_REQUEST['dbname'];
3045
        } else {
3046
            unset($dbname, $tablename);
3047
        }
3048
3049
        if (isset($dbname)) {
3050
            if (is_array($dbname)) {
3051
                $db_and_table = $dbname;
3052
                foreach ($db_and_table as $key => $db_name) {
3053
                    $db_and_table[$key] .= '.';
3054
                }
3055
            } else {
3056
                $unescaped_db = Util::unescapeMysqlWildcards($dbname);
3057
                $db_and_table = Util::backquote($unescaped_db) . '.';
3058
            }
3059
            if (isset($tablename)) {
3060
                $db_and_table .= Util::backquote($tablename);
3061
            } else {
3062
                if (is_array($db_and_table)) {
3063
                    foreach ($db_and_table as $key => $db_name) {
3064
                        $db_and_table[$key] .= '*';
3065
                    }
3066
                } else {
3067
                    $db_and_table .= '*';
3068
                }
3069
            }
3070
        } else {
3071
            $db_and_table = '*.*';
3072
        }
3073
3074
        // check if given $dbname is a wildcard or not
3075
        if (isset($dbname)) {
3076
            //if (preg_match('/\\\\(?:_|%)/i', $dbname)) {
3077
            if (! is_array($dbname) && preg_match('/(?<!\\\\)(?:_|%)/', $dbname)) {
3078
                $dbname_is_wildcard = true;
3079
            } else {
3080
                $dbname_is_wildcard = false;
3081
            }
3082
        }
3083
3084
        return [
3085
            $username,
3086
            $hostname,
3087
            isset($dbname) ? $dbname : null,
3088
            isset($tablename) ? $tablename : null,
3089
            isset($routinename) ? $routinename : null,
3090
            $db_and_table,
3091
            $dbname_is_wildcard,
3092
        ];
3093
    }
3094
3095
    /**
3096
     * Get title and textarea for export user definition in Privileges
3097
     *
3098
     * @param string $username username
3099
     * @param string $hostname host name
3100
     *
3101
     * @return array ($title, $export)
3102
     */
3103
    public function getListForExportUserDefinition($username, $hostname)
3104
    {
3105
        $export = '<textarea class="export" cols="60" rows="15">';
3106
3107
        if (isset($_POST['selected_usr'])) {
3108
            // export privileges for selected users
3109
            $title = __('Privileges');
3110
3111
            //For removing duplicate entries of users
3112
            $_POST['selected_usr'] = array_unique($_POST['selected_usr']);
3113
3114
            foreach ($_POST['selected_usr'] as $export_user) {
3115
                $export_username = mb_substr(
3116
                    $export_user,
3117
                    0,
3118
                    mb_strpos($export_user, '&')
3119
                );
3120
                $export_hostname = mb_substr(
3121
                    $export_user,
3122
                    mb_strrpos($export_user, ';') + 1
3123
                );
3124
                $export .= '# '
3125
                    . sprintf(
3126
                        __('Privileges for %s'),
3127
                        '`' . htmlspecialchars($export_username)
3128
                        . '`@`' . htmlspecialchars($export_hostname) . '`'
3129
                    )
3130
                    . "\n\n";
3131
                $export .= $this->getGrants($export_username, $export_hostname) . "\n";
3132
            }
3133
        } else {
3134
            // export privileges for a single user
3135
            $title = __('User') . ' `' . htmlspecialchars($username)
3136
                . '`@`' . htmlspecialchars($hostname) . '`';
3137
            $export .= $this->getGrants($username, $hostname);
3138
        }
3139
        // remove trailing whitespace
3140
        $export = trim($export);
3141
3142
        $export .= '</textarea>';
3143
3144
        return [
3145
            $title,
3146
            $export,
3147
        ];
3148
    }
3149
3150
    /**
3151
     * Get HTML for display Add userfieldset
3152
     *
3153
     * @param string $db    the database
3154
     * @param string $table the table name
3155
     *
3156
     * @return string html output
3157
     */
3158
    public function getAddUserHtmlFieldset($db = '', $table = '')
3159
    {
3160
        if (! $GLOBALS['is_createuser']) {
3161
            return '';
3162
        }
3163
        $rel_params = [];
3164
        $url_params = [
3165
            'adduser' => 1,
3166
        ];
3167
        if (! empty($db)) {
3168
            $url_params['dbname']
3169
                = $rel_params['checkprivsdb']
3170
                    = $db;
3171
        }
3172
        if (! empty($table)) {
3173
            $url_params['tablename']
3174
                = $rel_params['checkprivstable']
3175
                    = $table;
3176
        }
3177
3178
        return $this->template->render('server/privileges/add_user_fieldset', [
3179
            'url_params' => $url_params,
3180
            'rel_params' => $rel_params,
3181
        ]);
3182
    }
3183
3184
    /**
3185
     * Get HTML snippet for display user overview page
3186
     *
3187
     * @param string $pmaThemeImage a image source link
3188
     * @param string $text_dir      text directory
3189
     *
3190
     * @return string
3191
     */
3192
    public function getHtmlForUserOverview($pmaThemeImage, $text_dir)
3193
    {
3194
        global $is_createuser;
3195
3196
        $password_column = 'Password';
3197
        $server_type = Util::getServerType();
3198
        $serverVersion = $this->dbi->getVersion();
3199
        if (($server_type == 'MySQL' || $server_type == 'Percona Server')
3200
            && $serverVersion >= 50706
3201
        ) {
3202
            $password_column = 'authentication_string';
3203
        }
3204
        // $sql_query is for the initial-filtered,
3205
        // $sql_query_all is for counting the total no. of users
3206
3207
        $sql_query = $sql_query_all = 'SELECT *,' .
3208
            " IF(`" . $password_column . "` = _latin1 '', 'N', 'Y') AS 'Password'" .
3209
            ' FROM `mysql`.`user`';
3210
3211
        $sql_query .= (isset($_GET['initial'])
3212
            ? $this->rangeOfUsers($_GET['initial'])
3213
            : '');
3214
3215
        $sql_query .= ' ORDER BY `User` ASC, `Host` ASC;';
3216
        $sql_query_all .= ' ;';
3217
3218
        $res = $this->dbi->tryQuery(
3219
            $sql_query,
3220
            DatabaseInterface::CONNECT_USER,
3221
            DatabaseInterface::QUERY_STORE
3222
        );
3223
        $res_all = $this->dbi->tryQuery(
3224
            $sql_query_all,
3225
            DatabaseInterface::CONNECT_USER,
3226
            DatabaseInterface::QUERY_STORE
3227
        );
3228
3229
        $errorMessages = '';
3230
        if (! $res) {
3231
            // the query failed! This may have two reasons:
3232
            // - the user does not have enough privileges
3233
            // - the privilege tables use a structure of an earlier version.
3234
            // so let's try a more simple query
3235
3236
            $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

3236
            $this->dbi->freeResult(/** @scrutinizer ignore-type */ $res);
Loading history...
3237
            $this->dbi->freeResult($res_all);
3238
            $sql_query = 'SELECT * FROM `mysql`.`user`';
3239
            $res = $this->dbi->tryQuery(
3240
                $sql_query,
3241
                DatabaseInterface::CONNECT_USER,
3242
                DatabaseInterface::QUERY_STORE
3243
            );
3244
3245
            if (! $res) {
3246
                $errorMessages .= $this->getHtmlForViewUsersError();
3247
                $errorMessages .= $this->getAddUserHtmlFieldset();
3248
            } else {
3249
                // This message is hardcoded because I will replace it by
3250
                // a automatic repair feature soon.
3251
                $raw = 'Your privilege table structure seems to be older than'
3252
                    . ' this MySQL version!<br>'
3253
                    . 'Please run the <code>mysql_upgrade</code> command'
3254
                    . ' that should be included in your MySQL server distribution'
3255
                    . ' to solve this problem!';
3256
                $errorMessages .= Message::rawError($raw)->getDisplay();
3257
            }
3258
            $this->dbi->freeResult($res);
3259
        } else {
3260
            $db_rights = $this->getDbRightsForUserOverview();
3261
            // for all initials, even non A-Z
3262
            $array_initials = [];
3263
3264
            foreach ($db_rights as $right) {
3265
                foreach ($right as $account) {
3266
                    if (empty($account['User']) && $account['Host'] == 'localhost') {
3267
                        $emptyUserNotice = Message::notice(
3268
                            __(
3269
                                'A user account allowing any user from localhost to '
3270
                                . 'connect is present. This will prevent other users '
3271
                                . 'from connecting if the host part of their account '
3272
                                . 'allows a connection from any (%) host.'
3273
                            )
3274
                            . Util::showMySQLDocu('problems-connecting')
3275
                        )->getDisplay();
3276
                        break 2;
3277
                    }
3278
                }
3279
            }
3280
3281
            /**
3282
             * Displays the initials
3283
             * Also not necessary if there is less than 20 privileges
3284
             */
3285
            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

3285
            if ($this->dbi->numRows(/** @scrutinizer ignore-type */ $res_all) > 20) {
Loading history...
3286
                $initials = $this->getHtmlForInitials($array_initials);
3287
            }
3288
3289
            /**
3290
            * Display the user overview
3291
            * (if less than 50 users, display them immediately)
3292
            */
3293
            if (isset($_GET['initial'])
3294
                || isset($_GET['showall'])
3295
                || $this->dbi->numRows($res) < 50
3296
            ) {
3297
                $usersOverview = $this->getUsersOverview(
3298
                    $res,
3299
                    $db_rights,
3300
                    $pmaThemeImage,
3301
                    $text_dir
3302
                );
3303
            }
3304
3305
            $response = Response::getInstance();
3306
            if (! $response->isAjax()
3307
                || ! empty($_REQUEST['ajax_page_request'])
3308
            ) {
3309
                if ($GLOBALS['is_reload_priv']) {
3310
                    $flushnote = new Message(
3311
                        __(
3312
                            'Note: phpMyAdmin gets the users’ privileges directly '
3313
                            . 'from MySQL’s privilege tables. The content of these '
3314
                            . 'tables may differ from the privileges the server uses, '
3315
                            . 'if they have been changed manually. In this case, '
3316
                            . 'you should %sreload the privileges%s before you continue.'
3317
                        ),
3318
                        Message::NOTICE
3319
                    );
3320
                    $flushnote->addParamHtml(
3321
                        '<a href="' . Url::getFromRoute('/server/privileges', ['flush_privileges' => 1])
3322
                        . '" id="reload_privileges_anchor">'
3323
                    );
3324
                    $flushnote->addParamHtml('</a>');
3325
                } else {
3326
                    $flushnote = new Message(
3327
                        __(
3328
                            'Note: phpMyAdmin gets the users’ privileges directly '
3329
                            . 'from MySQL’s privilege tables. The content of these '
3330
                            . 'tables may differ from the privileges the server uses, '
3331
                            . 'if they have been changed manually. In this case, '
3332
                            . 'the privileges have to be reloaded but currently, you '
3333
                            . 'don\'t have the RELOAD privilege.'
3334
                        )
3335
                        . Util::showMySQLDocu(
3336
                            'privileges-provided',
3337
                            false,
3338
                            null,
3339
                            null,
3340
                            'priv_reload'
3341
                        ),
3342
                        Message::NOTICE
3343
                    );
3344
                }
3345
                $flushNotice = $flushnote->getDisplay();
3346
            }
3347
        }
3348
3349
        return $this->template->render('server/privileges/user_overview', [
3350
            'error_messages' => $errorMessages,
3351
            'empty_user_notice' => $emptyUserNotice ?? '',
3352
            'initials' => $initials ?? '',
3353
            'users_overview' => $usersOverview ?? '',
3354
            'is_createuser' => $is_createuser,
3355
            'flush_notice' => $flushNotice ?? '',
3356
        ]);
3357
    }
3358
3359
    /**
3360
     * Get HTML snippet for display user properties
3361
     *
3362
     * @param boolean      $dbname_is_wildcard whether database name is wildcard or not
3363
     * @param string       $url_dbname         url database name that urlencode() string
3364
     * @param string       $username           username
3365
     * @param string       $hostname           host name
3366
     * @param string|array $dbname             database name
3367
     * @param string       $tablename          table name
3368
     *
3369
     * @return string
3370
     */
3371
    public function getHtmlForUserProperties(
3372
        $dbname_is_wildcard,
3373
        $url_dbname,
3374
        $username,
3375
        $hostname,
3376
        $dbname,
3377
        $tablename
3378
    ) {
3379
        $sql = "SELECT '1' FROM `mysql`.`user`"
3380
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
3381
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';";
3382
3383
        $user_does_not_exists = (bool) ! $this->dbi->fetchValue($sql);
3384
3385
        $loginInformationFields = '';
3386
        if ($user_does_not_exists) {
3387
            $loginInformationFields = $this->getHtmlForLoginInformationFields();
3388
        }
3389
3390
        $_params = [
3391
            'username' => $username,
3392
            'hostname' => $hostname,
3393
        ];
3394
        if (! is_array($dbname) && strlen($dbname) > 0) {
3395
            $_params['dbname'] = $dbname;
3396
            if (strlen($tablename) > 0) {
3397
                $_params['tablename'] = $tablename;
3398
            }
3399
        } else {
3400
            $_params['dbname'] = $dbname;
3401
        }
3402
3403
        $privilegesTable = $this->getHtmlToDisplayPrivilegesTable(
3404
            // If $dbname is an array, pass any one db as all have same privs.
3405
            Core::ifSetOr($dbname, is_array($dbname) ? $dbname[0] : '*', 'length'),
3406
            Core::ifSetOr($tablename, '*', 'length')
3407
        );
3408
3409
        $tableSpecificRights = '';
3410
        if (! is_array($dbname) && strlen($tablename) === 0
3411
            && empty($dbname_is_wildcard)
3412
        ) {
3413
            // no table name was given, display all table specific rights
3414
            // but only if $dbname contains no wildcards
3415
            if (strlen($dbname) === 0) {
3416
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3417
                    $username,
3418
                    $hostname,
3419
                    'database'
3420
                );
3421
            } else {
3422
                // unescape wildcards in dbname at table level
3423
                $unescaped_db = Util::unescapeMysqlWildcards($dbname);
3424
3425
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3426
                    $username,
3427
                    $hostname,
3428
                    'table',
3429
                    $unescaped_db
3430
                );
3431
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3432
                    $username,
3433
                    $hostname,
3434
                    'routine',
3435
                    $unescaped_db
3436
                );
3437
            }
3438
        }
3439
3440
        // Provide a line with links to the relevant database and table
3441
        $linkToDatabaseAndTable = '';
3442
        if (! is_array($dbname) && strlen($dbname) > 0 && empty($dbname_is_wildcard)) {
3443
            $linkToDatabaseAndTable = $this->getLinkToDbAndTable($url_dbname, $dbname, $tablename);
3444
        }
3445
3446
        $changePassword = '';
3447
        $changeLoginInformation = '';
3448
        if (! is_array($dbname) && strlen($dbname) === 0 && ! $user_does_not_exists) {
3449
            //change login information
3450
            $changePassword = ChangePassword::getHtml(
3451
                'edit_other',
3452
                $username,
3453
                $hostname
3454
            );
3455
            $changeLoginInformation = $this->getChangeLoginInformationHtmlForm($username, $hostname);
3456
        }
3457
3458
        return $this->template->render('server/privileges/user_properties', [
3459
            'user_does_not_exists' => $user_does_not_exists,
3460
            'login_information_fields' => $loginInformationFields,
3461
            'params' => $_params,
3462
            'privileges_table' => $privilegesTable,
3463
            'table_specific_rights' => $tableSpecificRights,
3464
            'link_to_database_and_table' => $linkToDatabaseAndTable,
3465
            'change_password' => $changePassword,
3466
            'change_login_information' => $changeLoginInformation,
3467
            'database' => $dbname,
3468
            'dbname' => $url_dbname,
3469
            'username' => $username,
3470
            'hostname' => $hostname,
3471
            'is_databases' => $dbname_is_wildcard || is_array($dbname) && count($dbname) > 1,
3472
            'table' => $tablename,
3473
            'current_user' => $this->dbi->getCurrentUser(),
3474
        ]);
3475
    }
3476
3477
    /**
3478
     * Get queries for Table privileges to change or copy user
3479
     *
3480
     * @param string $user_host_condition user host condition to
3481
     *                                    select relevant table privileges
3482
     * @param array  $queries             queries array
3483
     * @param string $username            username
3484
     * @param string $hostname            host name
3485
     *
3486
     * @return array
3487
     */
3488
    public function getTablePrivsQueriesForChangeOrCopyUser(
3489
        $user_host_condition,
3490
        array $queries,
3491
        $username,
3492
        $hostname
3493
    ) {
3494
        $res = $this->dbi->query(
3495
            'SELECT `Db`, `Table_name`, `Table_priv` FROM `mysql`.`tables_priv`'
3496
            . $user_host_condition,
3497
            DatabaseInterface::CONNECT_USER,
3498
            DatabaseInterface::QUERY_STORE
3499
        );
3500
        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

3500
        while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $res)) {
Loading history...
3501
            $res2 = $this->dbi->query(
3502
                'SELECT `Column_name`, `Column_priv`'
3503
                . ' FROM `mysql`.`columns_priv`'
3504
                . ' WHERE `User`'
3505
                . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . "'"
3506
                . ' AND `Host`'
3507
                . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . '\''
3508
                . ' AND `Db`'
3509
                . ' = \'' . $this->dbi->escapeString($row['Db']) . "'"
3510
                . ' AND `Table_name`'
3511
                . ' = \'' . $this->dbi->escapeString($row['Table_name']) . "'"
3512
                . ';',
3513
                DatabaseInterface::CONNECT_USER,
3514
                DatabaseInterface::QUERY_STORE
3515
            );
3516
3517
            $tmp_privs1 = $this->extractPrivInfo($row);
3518
            $tmp_privs2 = [
3519
                'Select' => [],
3520
                'Insert' => [],
3521
                'Update' => [],
3522
                'References' => [],
3523
            ];
3524
3525
            while ($row2 = $this->dbi->fetchAssoc($res2)) {
3526
                $tmp_array = explode(',', $row2['Column_priv']);
3527
                if (in_array('Select', $tmp_array)) {
3528
                    $tmp_privs2['Select'][] = $row2['Column_name'];
3529
                }
3530
                if (in_array('Insert', $tmp_array)) {
3531
                    $tmp_privs2['Insert'][] = $row2['Column_name'];
3532
                }
3533
                if (in_array('Update', $tmp_array)) {
3534
                    $tmp_privs2['Update'][] = $row2['Column_name'];
3535
                }
3536
                if (in_array('References', $tmp_array)) {
3537
                    $tmp_privs2['References'][] = $row2['Column_name'];
3538
                }
3539
            }
3540
            if (count($tmp_privs2['Select']) > 0 && ! in_array('SELECT', $tmp_privs1)) {
3541
                $tmp_privs1[] = 'SELECT (`' . implode('`, `', $tmp_privs2['Select']) . '`)';
3542
            }
3543
            if (count($tmp_privs2['Insert']) > 0 && ! in_array('INSERT', $tmp_privs1)) {
3544
                $tmp_privs1[] = 'INSERT (`' . implode('`, `', $tmp_privs2['Insert']) . '`)';
3545
            }
3546
            if (count($tmp_privs2['Update']) > 0 && ! in_array('UPDATE', $tmp_privs1)) {
3547
                $tmp_privs1[] = 'UPDATE (`' . implode('`, `', $tmp_privs2['Update']) . '`)';
3548
            }
3549
            if (count($tmp_privs2['References']) > 0
3550
                && ! in_array('REFERENCES', $tmp_privs1)
3551
            ) {
3552
                $tmp_privs1[]
3553
                    = 'REFERENCES (`' . implode('`, `', $tmp_privs2['References']) . '`)';
3554
            }
3555
3556
            $queries[] = 'GRANT ' . implode(', ', $tmp_privs1)
3557
                . ' 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

3557
                . ' ON ' . /** @scrutinizer ignore-type */ Util::backquote($row['Db']) . '.'
Loading history...
3558
                . 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

3558
                . /** @scrutinizer ignore-type */ Util::backquote($row['Table_name'])
Loading history...
3559
                . ' TO \'' . $this->dbi->escapeString($username)
3560
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\''
3561
                . (in_array('Grant', explode(',', $row['Table_priv']))
3562
                ? ' WITH GRANT OPTION;'
3563
                : ';');
3564
        }
3565
        return $queries;
3566
    }
3567
3568
    /**
3569
     * Get queries for database specific privileges for change or copy user
3570
     *
3571
     * @param array  $queries  queries array with string
3572
     * @param string $username username
3573
     * @param string $hostname host name
3574
     *
3575
     * @return array
3576
     */
3577
    public function getDbSpecificPrivsQueriesForChangeOrCopyUser(
3578
        array $queries,
3579
        $username,
3580
        $hostname
3581
    ) {
3582
        $user_host_condition = ' WHERE `User`'
3583
            . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . "'"
3584
            . ' AND `Host`'
3585
            . ' = \'' . $this->dbi->escapeString($_POST['old_hostname']) . '\';';
3586
3587
        $res = $this->dbi->query(
3588
            'SELECT * FROM `mysql`.`db`' . $user_host_condition
3589
        );
3590
3591
        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

3591
        while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $res)) {
Loading history...
3592
            $queries[] = 'GRANT ' . implode(', ', $this->extractPrivInfo($row))
3593
                . ' 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

3593
                . ' ON ' . /** @scrutinizer ignore-type */ Util::backquote($row['Db']) . '.*'
Loading history...
3594
                . ' TO \'' . $this->dbi->escapeString($username)
3595
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\''
3596
                . ($row['Grant_priv'] == 'Y' ? ' WITH GRANT OPTION;' : ';');
3597
        }
3598
        $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

3598
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $res);
Loading history...
3599
3600
        $queries = $this->getTablePrivsQueriesForChangeOrCopyUser(
3601
            $user_host_condition,
3602
            $queries,
3603
            $username,
3604
            $hostname
3605
        );
3606
3607
        return $queries;
3608
    }
3609
3610
    /**
3611
     * Prepares queries for adding users and
3612
     * also create database and return query and message
3613
     *
3614
     * @param boolean $_error               whether user create or not
3615
     * @param string  $real_sql_query       SQL query for add a user
3616
     * @param string  $sql_query            SQL query to be displayed
3617
     * @param string  $username             username
3618
     * @param string  $hostname             host name
3619
     * @param string  $dbname               database name
3620
     * @param string  $alter_real_sql_query SQL query for ALTER USER
3621
     * @param string  $alter_sql_query      SQL query for ALTER USER to be displayed
3622
     *
3623
     * @return array, $message
3624
     */
3625
    public function addUserAndCreateDatabase(
3626
        $_error,
3627
        $real_sql_query,
3628
        $sql_query,
3629
        $username,
3630
        $hostname,
3631
        $dbname,
3632
        $alter_real_sql_query,
3633
        $alter_sql_query
3634
    ) {
3635
        if ($_error || (! empty($real_sql_query)
3636
            && ! $this->dbi->tryQuery($real_sql_query))
3637
        ) {
3638
            $_POST['createdb-1'] = $_POST['createdb-2']
3639
                = $_POST['createdb-3'] = null;
3640
            $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

3640
            $message = Message::rawError(/** @scrutinizer ignore-type */ $this->dbi->getError());
Loading history...
3641
        } elseif ($alter_real_sql_query !== '' && ! $this->dbi->tryQuery($alter_real_sql_query)) {
3642
            $_POST['createdb-1'] = $_POST['createdb-2']
3643
                = $_POST['createdb-3'] = null;
3644
            $message = Message::rawError($this->dbi->getError());
3645
        } else {
3646
            $sql_query .= $alter_sql_query;
3647
            $message = Message::success(__('You have added a new user.'));
3648
        }
3649
3650
        if (isset($_POST['createdb-1'])) {
3651
            // Create database with same name and grant all privileges
3652
            $q = 'CREATE DATABASE IF NOT EXISTS '
3653
                . Util::backquote(
3654
                    $this->dbi->escapeString($username)
3655
                ) . ';';
3656
            $sql_query .= $q;
3657
            if (! $this->dbi->tryQuery($q)) {
3658
                $message = Message::rawError($this->dbi->getError());
3659
            }
3660
3661
            /**
3662
             * Reload the navigation
3663
             */
3664
            $GLOBALS['reload'] = true;
3665
            $GLOBALS['db'] = $username;
3666
3667
            $q = 'GRANT ALL PRIVILEGES ON '
3668
                . Util::backquote(
3669
                    Util::escapeMysqlWildcards(
3670
                        $this->dbi->escapeString($username)
3671
                    )
3672
                ) . '.* TO \''
3673
                . $this->dbi->escapeString($username)
3674
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
3675
            $sql_query .= $q;
3676
            if (! $this->dbi->tryQuery($q)) {
3677
                $message = Message::rawError($this->dbi->getError());
3678
            }
3679
        }
3680
3681
        if (isset($_POST['createdb-2'])) {
3682
            // Grant all privileges on wildcard name (username\_%)
3683
            $q = 'GRANT ALL PRIVILEGES ON '
3684
                . Util::backquote(
3685
                    Util::escapeMysqlWildcards(
3686
                        $this->dbi->escapeString($username)
3687
                    ) . '\_%'
3688
                ) . '.* TO \''
3689
                . $this->dbi->escapeString($username)
3690
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
3691
            $sql_query .= $q;
3692
            if (! $this->dbi->tryQuery($q)) {
3693
                $message = Message::rawError($this->dbi->getError());
3694
            }
3695
        }
3696
3697
        if (isset($_POST['createdb-3'])) {
3698
            // Grant all privileges on the specified database to the new user
3699
            $q = 'GRANT ALL PRIVILEGES ON '
3700
            . Util::backquote(
3701
                $this->dbi->escapeString($dbname)
3702
            ) . '.* TO \''
3703
            . $this->dbi->escapeString($username)
3704
            . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
3705
            $sql_query .= $q;
3706
            if (! $this->dbi->tryQuery($q)) {
3707
                $message = Message::rawError($this->dbi->getError());
3708
            }
3709
        }
3710
        return [
3711
            $sql_query,
3712
            $message,
3713
        ];
3714
    }
3715
3716
    /**
3717
     * Get the hashed string for password
3718
     *
3719
     * @param string $password password
3720
     *
3721
     * @return string
3722
     */
3723
    public function getHashedPassword($password)
3724
    {
3725
        $password = $this->dbi->escapeString($password);
3726
        $result = $this->dbi->fetchSingleRow(
3727
            "SELECT PASSWORD('" . $password . "') AS `password`;"
3728
        );
3729
3730
        return $result['password'];
3731
    }
3732
3733
    /**
3734
     * Check if MariaDB's 'simple_password_check'
3735
     * OR 'cracklib_password_check' is ACTIVE
3736
     *
3737
     * @return boolean if atleast one of the plugins is ACTIVE
3738
     */
3739
    public function checkIfMariaDBPwdCheckPluginActive()
3740
    {
3741
        $serverVersion = $this->dbi->getVersion();
3742
        if (! (Util::getServerType() == 'MariaDB' && $serverVersion >= 100002)) {
3743
            return false;
3744
        }
3745
3746
        $result = $this->dbi->tryQuery(
3747
            'SHOW PLUGINS SONAME LIKE \'%_password_check%\''
3748
        );
3749
3750
        /* Plugins are not working, for example directory does not exists */
3751
        if ($result === false) {
3752
            return false;
3753
        }
3754
3755
        while ($row = $this->dbi->fetchAssoc($result)) {
3756
            if ($row['Status'] === 'ACTIVE') {
3757
                return true;
3758
            }
3759
        }
3760
3761
        return false;
3762
    }
3763
3764
3765
    /**
3766
     * Get SQL queries for Display and Add user
3767
     *
3768
     * @param string $username username
3769
     * @param string $hostname host name
3770
     * @param string $password password
3771
     *
3772
     * @return array ($create_user_real, $create_user_show, $real_sql_query, $sql_query
3773
     *                $password_set_real, $password_set_show, $alter_real_sql_query, $alter_sql_query)
3774
     */
3775
    public function getSqlQueriesForDisplayAndAddUser($username, $hostname, $password)
3776
    {
3777
        $slashedUsername = $this->dbi->escapeString($username);
3778
        $slashedHostname = $this->dbi->escapeString($hostname);
3779
        $slashedPassword = $this->dbi->escapeString($password);
3780
        $serverType = Util::getServerType();
3781
        $serverVersion = $this->dbi->getVersion();
3782
3783
        $create_user_stmt = sprintf(
3784
            'CREATE USER \'%s\'@\'%s\'',
3785
            $slashedUsername,
3786
            $slashedHostname
3787
        );
3788
        $isMariaDBPwdPluginActive = $this->checkIfMariaDBPwdCheckPluginActive();
3789
3790
        // See https://github.com/phpmyadmin/phpmyadmin/pull/11560#issuecomment-147158219
3791
        // for details regarding details of syntax usage for various versions
3792
3793
        // 'IDENTIFIED WITH auth_plugin'
3794
        // is supported by MySQL 5.5.7+
3795
        if (($serverType == 'MySQL' || $serverType == 'Percona Server')
3796
            && $serverVersion >= 50507
3797
            && isset($_POST['authentication_plugin'])
3798
        ) {
3799
            $create_user_stmt .= ' IDENTIFIED WITH '
3800
                . $_POST['authentication_plugin'];
3801
        }
3802
3803
        // 'IDENTIFIED VIA auth_plugin'
3804
        // is supported by MariaDB 5.2+
3805
        if ($serverType == 'MariaDB'
3806
            && $serverVersion >= 50200
3807
            && isset($_POST['authentication_plugin'])
3808
            && ! $isMariaDBPwdPluginActive
3809
        ) {
3810
            $create_user_stmt .= ' IDENTIFIED VIA '
3811
                . $_POST['authentication_plugin'];
3812
        }
3813
3814
        $create_user_real = $create_user_stmt;
3815
        $create_user_show = $create_user_stmt;
3816
3817
        $password_set_stmt = 'SET PASSWORD FOR \'%s\'@\'%s\' = \'%s\'';
3818
        $password_set_show = sprintf(
3819
            $password_set_stmt,
3820
            $slashedUsername,
3821
            $slashedHostname,
3822
            '***'
3823
        );
3824
3825
        $sql_query_stmt = sprintf(
3826
            'GRANT %s ON *.* TO \'%s\'@\'%s\'',
3827
            implode(', ', $this->extractPrivInfo()),
3828
            $slashedUsername,
3829
            $slashedHostname
3830
        );
3831
        $real_sql_query = $sql_query = $sql_query_stmt;
3832
3833
        // Set the proper hashing method
3834
        if (isset($_POST['authentication_plugin'])) {
3835
            $this->setProperPasswordHashing(
3836
                $_POST['authentication_plugin']
3837
            );
3838
        }
3839
3840
        // Use 'CREATE USER ... WITH ... AS ..' syntax for
3841
        // newer MySQL versions
3842
        // and 'CREATE USER ... VIA .. USING ..' syntax for
3843
        // newer MariaDB versions
3844
        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...
3845
            && $serverVersion >= 50706)
3846
            || ($serverType == 'MariaDB'
3847
            && $serverVersion >= 50200)
3848
        ) {
3849
            $password_set_real = null;
3850
3851
            // Required for binding '%' with '%s'
3852
            $create_user_stmt = str_replace(
3853
                '%',
3854
                '%%',
3855
                $create_user_stmt
3856
            );
3857
3858
            // MariaDB uses 'USING' whereas MySQL uses 'AS'
3859
            // but MariaDB with validation plugin needs cleartext password
3860
            if ($serverType == 'MariaDB'
3861
                && ! $isMariaDBPwdPluginActive
3862
            ) {
3863
                $create_user_stmt .= ' USING \'%s\'';
3864
            } elseif ($serverType == 'MariaDB') {
3865
                $create_user_stmt .= ' IDENTIFIED BY \'%s\'';
3866
            } elseif (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) {
3867
                $create_user_stmt .= ' BY \'%s\'';
3868
            } else {
3869
                $create_user_stmt .= ' AS \'%s\'';
3870
            }
3871
3872
            if ($_POST['pred_password'] == 'keep') {
3873
                $create_user_real = sprintf(
3874
                    $create_user_stmt,
3875
                    $slashedPassword
3876
                );
3877
                $create_user_show = sprintf(
3878
                    $create_user_stmt,
3879
                    '***'
3880
                );
3881
            } elseif ($_POST['pred_password'] == 'none') {
3882
                $create_user_real = sprintf(
3883
                    $create_user_stmt,
3884
                    null
3885
                );
3886
                $create_user_show = sprintf(
3887
                    $create_user_stmt,
3888
                    '***'
3889
                );
3890
            } else {
3891
                if (! (($serverType == 'MariaDB' && $isMariaDBPwdPluginActive)
3892
                    || ($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011)) {
3893
                    $hashedPassword = $this->getHashedPassword($_POST['pma_pw']);
3894
                } else {
3895
                    // MariaDB with validation plugin needs cleartext password
3896
                    $hashedPassword = $_POST['pma_pw'];
3897
                }
3898
                $create_user_real = sprintf(
3899
                    $create_user_stmt,
3900
                    $hashedPassword
3901
                );
3902
                $create_user_show = sprintf(
3903
                    $create_user_stmt,
3904
                    '***'
3905
                );
3906
            }
3907
        } else {
3908
            // Use 'SET PASSWORD' syntax for pre-5.7.6 MySQL versions
3909
            // and pre-5.2.0 MariaDB versions
3910
            if ($_POST['pred_password'] == 'keep') {
3911
                $password_set_real = sprintf(
3912
                    $password_set_stmt,
3913
                    $slashedUsername,
3914
                    $slashedHostname,
3915
                    $slashedPassword
3916
                );
3917
            } elseif ($_POST['pred_password'] == 'none') {
3918
                $password_set_real = sprintf(
3919
                    $password_set_stmt,
3920
                    $slashedUsername,
3921
                    $slashedHostname,
3922
                    null
3923
                );
3924
            } else {
3925
                $hashedPassword = $this->getHashedPassword($_POST['pma_pw']);
3926
                $password_set_real = sprintf(
3927
                    $password_set_stmt,
3928
                    $slashedUsername,
3929
                    $slashedHostname,
3930
                    $hashedPassword
3931
                );
3932
            }
3933
        }
3934
3935
        $alter_real_sql_query = '';
3936
        $alter_sql_query = '';
3937
        if (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) {
3938
            $sql_query_stmt = '';
3939
            if ((isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y')
3940
                || (isset($GLOBALS['Grant_priv']) && $GLOBALS['Grant_priv'] == 'Y')
3941
            ) {
3942
                $sql_query_stmt = ' WITH GRANT OPTION';
3943
            }
3944
            $real_sql_query .= $sql_query_stmt;
3945
            $sql_query .= $sql_query_stmt;
3946
3947
            $alter_sql_query_stmt = sprintf(
3948
                'ALTER USER \'%s\'@\'%s\'',
3949
                $slashedUsername,
3950
                $slashedHostname
3951
            );
3952
            $alter_real_sql_query = $alter_sql_query_stmt;
3953
            $alter_sql_query = $alter_sql_query_stmt;
3954
        }
3955
3956
        // add REQUIRE clause
3957
        $require_clause = $this->getRequireClause();
3958
        $with_clause = $this->getWithClauseForAddUserAndUpdatePrivs();
3959
3960
        if (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) {
3961
            $alter_real_sql_query .= $require_clause;
3962
            $alter_sql_query .= $require_clause;
3963
            $alter_real_sql_query .= $with_clause;
3964
            $alter_sql_query .= $with_clause;
3965
        } else {
3966
            $real_sql_query .= $require_clause;
3967
            $sql_query .= $require_clause;
3968
            $real_sql_query .= $with_clause;
3969
            $sql_query .= $with_clause;
3970
        }
3971
3972
        if ($alter_real_sql_query !== '') {
3973
            $alter_real_sql_query .= ';';
3974
            $alter_sql_query .= ';';
3975
        }
3976
        $create_user_real .= ';';
3977
        $create_user_show .= ';';
3978
        $real_sql_query .= ';';
3979
        $sql_query .= ';';
3980
        // No Global GRANT_OPTION privilege
3981
        if (! $GLOBALS['is_grantuser']) {
3982
            $real_sql_query = '';
3983
            $sql_query = '';
3984
        }
3985
3986
        // Use 'SET PASSWORD' for pre-5.7.6 MySQL versions
3987
        // and pre-5.2.0 MariaDB
3988
        if (($serverType == 'MySQL'
3989
            && $serverVersion >= 50706)
3990
            || ($serverType == 'MariaDB'
3991
            && $serverVersion >= 50200)
3992
        ) {
3993
            $password_set_real = null;
3994
            $password_set_show = null;
3995
        } else {
3996
            if ($password_set_real !== null) {
3997
                $password_set_real .= ";";
3998
            }
3999
            $password_set_show .= ";";
4000
        }
4001
4002
        return [
4003
            $create_user_real,
4004
            $create_user_show,
4005
            $real_sql_query,
4006
            $sql_query,
4007
            $password_set_real,
4008
            $password_set_show,
4009
            $alter_real_sql_query,
4010
            $alter_sql_query,
4011
        ];
4012
    }
4013
4014
    /**
4015
     * Returns the type ('PROCEDURE' or 'FUNCTION') of the routine
4016
     *
4017
     * @param string $dbname      database
4018
     * @param string $routineName routine
4019
     *
4020
     * @return string type
4021
     */
4022
    public function getRoutineType($dbname, $routineName)
4023
    {
4024
        $routineData = $this->dbi->getRoutines($dbname);
4025
4026
        foreach ($routineData as $routine) {
4027
            if ($routine['name'] === $routineName) {
4028
                return $routine['type'];
4029
            }
4030
        }
4031
        return '';
4032
    }
4033
4034
    /**
4035
     * @param string $username User name
4036
     * @param string $hostname Host name
4037
     * @param string $database Database name
4038
     * @param string $routine  Routine name
4039
     *
4040
     * @return array
4041
     */
4042
    private function getRoutinePrivileges(
4043
        string $username,
4044
        string $hostname,
4045
        string $database,
4046
        string $routine
4047
    ): array {
4048
        $sql = "SELECT `Proc_priv`"
4049
            . " FROM `mysql`.`procs_priv`"
4050
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
4051
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "'"
4052
            . " AND `Db` = '"
4053
            . $this->dbi->escapeString(Util::unescapeMysqlWildcards($database)) . "'"
4054
            . " AND `Routine_name` LIKE '" . $this->dbi->escapeString($routine) . "';";
4055
        $privileges = $this->dbi->fetchValue($sql);
4056
        if ($privileges === false) {
4057
            $privileges = '';
4058
        }
4059
        return $this->parseProcPriv($privileges);
4060
    }
4061
}
4062