Passed
Push — master ( cc44bd...d7bc90 )
by Maurício
07:05
created

Privileges::getHtmlForUserProperties()   C

Complexity

Conditions 15
Paths 36

Size

Total Lines 121
Code Lines 81

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 81
c 0
b 0
f 0
dl 0
loc 121
rs 5.1478
cc 15
nc 36
nop 6

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 number of defined user groups
1652
     *
1653
     * @return integer
1654
     */
1655
    public function getUserGroupCount()
1656
    {
1657
        $cfgRelation = $this->relation->getRelationsParam();
1658
        $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

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

1659
            . '.' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['usergroups']);
Loading history...
1660
        $sql_query = 'SELECT COUNT(*) FROM ' . $user_group_table;
1661
        $user_group_count = $this->dbi->fetchValue(
1662
            $sql_query,
1663
            0,
1664
            0,
1665
            DatabaseInterface::CONNECT_CONTROL
1666
        );
1667
1668
        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...
1669
    }
1670
1671
    /**
1672
     * Returns name of user group that user is part of
1673
     *
1674
     * @param string $username User name
1675
     *
1676
     * @return mixed usergroup if found or null if not found
1677
     */
1678
    public function getUserGroupForUser($username)
1679
    {
1680
        $cfgRelation = $this->relation->getRelationsParam();
1681
1682
        if (empty($cfgRelation['db'])
1683
            || empty($cfgRelation['users'])
1684
        ) {
1685
            return null;
1686
        }
1687
1688
        $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

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

1689
            . '.' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['users']);
Loading history...
1690
        $sql_query = 'SELECT `usergroup` FROM ' . $user_table
1691
            . ' WHERE `username` = \'' . $username . '\''
1692
            . ' LIMIT 1';
1693
1694
        $usergroup = $this->dbi->fetchValue(
1695
            $sql_query,
1696
            0,
1697
            0,
1698
            DatabaseInterface::CONNECT_CONTROL
1699
        );
1700
1701
        if ($usergroup === false) {
1702
            return null;
1703
        }
1704
1705
        return $usergroup;
1706
    }
1707
1708
    /**
1709
     * This function return the extra data array for the ajax behavior
1710
     *
1711
     * @param string $password  password
1712
     * @param string $sql_query sql query
1713
     * @param string $hostname  hostname
1714
     * @param string $username  username
1715
     *
1716
     * @return array
1717
     */
1718
    public function getExtraDataForAjaxBehavior(
1719
        $password,
1720
        $sql_query,
1721
        $hostname,
1722
        $username
1723
    ) {
1724
        global $is_grantuser;
1725
1726
        if (isset($GLOBALS['dbname'])) {
1727
            //if (preg_match('/\\\\(?:_|%)/i', $dbname)) {
1728
            if (preg_match('/(?<!\\\\)(?:_|%)/', $GLOBALS['dbname'])) {
1729
                $dbname_is_wildcard = true;
1730
            } else {
1731
                $dbname_is_wildcard = false;
1732
            }
1733
        }
1734
1735
        $user_group_count = 0;
1736
        if ($GLOBALS['cfgRelation']['menuswork']) {
1737
            $user_group_count = $this->getUserGroupCount();
1738
        }
1739
1740
        $extra_data = [];
1741
        if (strlen($sql_query) > 0) {
1742
            $extra_data['sql_query'] = Util::getMessage(null, $sql_query);
1743
        }
1744
1745
        if (isset($_POST['change_copy'])) {
1746
            $cfgRelation = $this->relation->getRelationsParam();
1747
            $user = [
1748
                'name' => $username,
1749
                'host' => $hostname,
1750
                'has_password' => ! empty($password) || isset($_POST['pma_pw']),
1751
                'privileges' => implode(', ', $this->extractPrivInfo(null, true)),
1752
                'has_group' => ! empty($cfgRelation['users']) && ! empty($cfgRelation['usergroups']),
1753
                'has_group_edit' => $cfgRelation['menuswork'] && $user_group_count > 0,
1754
                'has_grant' => isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y',
1755
            ];
1756
            $extra_data['new_user_string'] = $this->template->render('server/privileges/new_user_ajax', [
1757
                'user' => $user,
1758
                'is_grantuser' => $is_grantuser,
1759
                'initial' => $_GET['initial'] ?? '',
1760
            ]);
1761
1762
            /**
1763
             * Generate the string for this alphabet's initial, to update the user
1764
             * pagination
1765
             */
1766
            $new_user_initial = mb_strtoupper(
1767
                mb_substr($username, 0, 1)
1768
            );
1769
            $newUserInitialString = '<a href="' . Url::getFromRoute('/server/privileges', ['initial' => $new_user_initial]) . '">'
1770
                . $new_user_initial . '</a>';
1771
            $extra_data['new_user_initial'] = $new_user_initial;
1772
            $extra_data['new_user_initial_string'] = $newUserInitialString;
1773
        }
1774
1775
        if (isset($_POST['update_privs'])) {
1776
            $extra_data['db_specific_privs'] = false;
1777
            $extra_data['db_wildcard_privs'] = false;
1778
            if (isset($dbname_is_wildcard)) {
1779
                $extra_data['db_specific_privs'] = ! $dbname_is_wildcard;
1780
                $extra_data['db_wildcard_privs'] = $dbname_is_wildcard;
1781
            }
1782
            $new_privileges = implode(', ', $this->extractPrivInfo(null, true));
1783
1784
            $extra_data['new_privileges'] = $new_privileges;
1785
        }
1786
1787
        if (isset($_GET['validate_username'])) {
1788
            $sql_query = "SELECT * FROM `mysql`.`user` WHERE `User` = '"
1789
                . $_GET['username'] . "';";
1790
            $res = $this->dbi->query($sql_query);
1791
            $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

1791
            $row = $this->dbi->fetchRow(/** @scrutinizer ignore-type */ $res);
Loading history...
1792
            if (empty($row)) {
1793
                $extra_data['user_exists'] = false;
1794
            } else {
1795
                $extra_data['user_exists'] = true;
1796
            }
1797
        }
1798
1799
        return $extra_data;
1800
    }
1801
1802
    /**
1803
     * no db name given, so we want all privs for the given user
1804
     * db name was given, so we want all user specific rights for this db
1805
     * So this function returns user rights as an array
1806
     *
1807
     * @param string $username username
1808
     * @param string $hostname host name
1809
     * @param string $type     database or table
1810
     * @param string $dbname   database name
1811
     *
1812
     * @return array database rights
1813
     */
1814
    public function getUserSpecificRights($username, $hostname, $type, $dbname = '')
1815
    {
1816
        $user_host_condition = " WHERE `User`"
1817
            . " = '" . $this->dbi->escapeString($username) . "'"
1818
            . " AND `Host`"
1819
            . " = '" . $this->dbi->escapeString($hostname) . "'";
1820
1821
        if ($type == 'database') {
1822
            $tables_to_search_for_users = [
1823
                'tables_priv',
1824
                'columns_priv',
1825
                'procs_priv',
1826
            ];
1827
            $dbOrTableName = 'Db';
1828
        } elseif ($type == 'table') {
1829
            $user_host_condition .= " AND `Db` LIKE '"
1830
                . $this->dbi->escapeString($dbname) . "'";
1831
            $tables_to_search_for_users = ['columns_priv'];
1832
            $dbOrTableName = 'Table_name';
1833
        } else { // routine
1834
            $user_host_condition .= " AND `Db` LIKE '"
1835
                . $this->dbi->escapeString($dbname) . "'";
1836
            $tables_to_search_for_users = ['procs_priv'];
1837
            $dbOrTableName = 'Routine_name';
1838
        }
1839
1840
        // we also want privileges for this user not in table `db` but in other table
1841
        $tables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`;');
1842
1843
        $db_rights_sqls = [];
1844
        foreach ($tables_to_search_for_users as $table_search_in) {
1845
            if (in_array($table_search_in, $tables)) {
1846
                $db_rights_sqls[] = '
1847
                    SELECT DISTINCT `' . $dbOrTableName . '`
1848
                    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

1848
                    FROM `mysql`.' . /** @scrutinizer ignore-type */ Util::backquote($table_search_in)
Loading history...
1849
                   . $user_host_condition;
1850
            }
1851
        }
1852
1853
        $user_defaults = [
1854
            $dbOrTableName  => '',
1855
            'Grant_priv'    => 'N',
1856
            'privs'         => ['USAGE'],
1857
            'Column_priv'   => true,
1858
        ];
1859
1860
        // for the rights
1861
        $db_rights = [];
1862
1863
        $db_rights_sql = '(' . implode(') UNION (', $db_rights_sqls) . ')'
1864
            . ' ORDER BY `' . $dbOrTableName . '` ASC';
1865
1866
        $db_rights_result = $this->dbi->query($db_rights_sql);
1867
1868
        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

1868
        while ($db_rights_row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $db_rights_result)) {
Loading history...
1869
            $db_rights_row = array_merge($user_defaults, $db_rights_row);
1870
            if ($type == 'database') {
1871
                // only Db names in the table `mysql`.`db` uses wildcards
1872
                // as we are in the db specific rights display we want
1873
                // all db names escaped, also from other sources
1874
                $db_rights_row['Db'] = Util::escapeMysqlWildcards(
1875
                    $db_rights_row['Db']
1876
                );
1877
            }
1878
            $db_rights[$db_rights_row[$dbOrTableName]] = $db_rights_row;
1879
        }
1880
1881
        $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

1881
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $db_rights_result);
Loading history...
1882
1883
        if ($type == 'database') {
1884
            $sql_query = 'SELECT * FROM `mysql`.`db`'
1885
                . $user_host_condition . ' ORDER BY `Db` ASC';
1886
        } elseif ($type == 'table') {
1887
            $sql_query = 'SELECT `Table_name`,'
1888
                . ' `Table_priv`,'
1889
                . ' IF(`Column_priv` = _latin1 \'\', 0, 1)'
1890
                . ' AS \'Column_priv\''
1891
                . ' FROM `mysql`.`tables_priv`'
1892
                . $user_host_condition
1893
                . ' ORDER BY `Table_name` ASC;';
1894
        } else {
1895
            $sql_query = "SELECT `Routine_name`, `Proc_priv`"
1896
                . " FROM `mysql`.`procs_priv`"
1897
                . $user_host_condition
1898
                . " ORDER BY `Routine_name`";
1899
        }
1900
1901
        $result = $this->dbi->query($sql_query);
1902
1903
        while ($row = $this->dbi->fetchAssoc($result)) {
1904
            if (isset($db_rights[$row[$dbOrTableName]])) {
1905
                $db_rights[$row[$dbOrTableName]]
1906
                    = array_merge($db_rights[$row[$dbOrTableName]], $row);
1907
            } else {
1908
                $db_rights[$row[$dbOrTableName]] = $row;
1909
            }
1910
            if ($type == 'database') {
1911
                // there are db specific rights for this user
1912
                // so we can drop this db rights
1913
                $db_rights[$row['Db']]['can_delete'] = true;
1914
            }
1915
        }
1916
        $this->dbi->freeResult($result);
1917
        return $db_rights;
1918
    }
1919
1920
    /**
1921
     * Parses Proc_priv data
1922
     *
1923
     * @param string $privs Proc_priv
1924
     *
1925
     * @return array
1926
     */
1927
    public function parseProcPriv($privs)
1928
    {
1929
        $result = [
1930
            'Alter_routine_priv' => 'N',
1931
            'Execute_priv'       => 'N',
1932
            'Grant_priv'         => 'N',
1933
        ];
1934
        foreach (explode(',', (string) $privs) as $priv) {
1935
            if ($priv == 'Alter Routine') {
1936
                $result['Alter_routine_priv'] = 'Y';
1937
            } else {
1938
                $result[$priv . '_priv'] = 'Y';
1939
            }
1940
        }
1941
        return $result;
1942
    }
1943
1944
    /**
1945
     * Get a HTML table for display user's tabel specific or database specific rights
1946
     *
1947
     * @param string $username username
1948
     * @param string $hostname host name
1949
     * @param string $type     database, table or routine
1950
     * @param string $dbname   database name
1951
     *
1952
     * @return string
1953
     */
1954
    public function getHtmlForAllTableSpecificRights(
1955
        $username,
1956
        $hostname,
1957
        $type,
1958
        $dbname = ''
1959
    ) {
1960
        $uiData = [
1961
            'database' => [
1962
                'form_id'        => 'database_specific_priv',
1963
                'sub_menu_label' => __('Database'),
1964
                'legend'         => __('Database-specific privileges'),
1965
                'type_label'     => __('Database'),
1966
            ],
1967
            'table' => [
1968
                'form_id'        => 'table_specific_priv',
1969
                'sub_menu_label' => __('Table'),
1970
                'legend'         => __('Table-specific privileges'),
1971
                'type_label'     => __('Table'),
1972
            ],
1973
            'routine' => [
1974
                'form_id'        => 'routine_specific_priv',
1975
                'sub_menu_label' => __('Routine'),
1976
                'legend'         => __('Routine-specific privileges'),
1977
                'type_label'     => __('Routine'),
1978
            ],
1979
        ];
1980
1981
        /**
1982
         * no db name given, so we want all privs for the given user
1983
         * db name was given, so we want all user specific rights for this db
1984
         */
1985
        $db_rights = $this->getUserSpecificRights($username, $hostname, $type, $dbname);
1986
        ksort($db_rights);
1987
1988
        $foundRows = [];
1989
        $privileges = [];
1990
        foreach ($db_rights as $row) {
1991
            $onePrivilege = [];
1992
1993
            $paramTableName = '';
1994
            $paramRoutineName = '';
1995
1996
            if ($type == 'database') {
1997
                $name = $row['Db'];
1998
                $onePrivilege['grant']        = $row['Grant_priv'] == 'Y';
1999
                $onePrivilege['table_privs']   = ! empty($row['Table_priv'])
2000
                    || ! empty($row['Column_priv']);
2001
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
2002
2003
                $paramDbName = $row['Db'];
2004
            } elseif ($type == 'table') {
2005
                $name = $row['Table_name'];
2006
                $onePrivilege['grant'] = in_array(
2007
                    'Grant',
2008
                    explode(',', $row['Table_priv'])
2009
                );
2010
                $onePrivilege['column_privs']  = ! empty($row['Column_priv']);
2011
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
2012
2013
                $paramDbName = $dbname;
2014
                $paramTableName = $row['Table_name'];
2015
            } else { // routine
2016
                $name = $row['Routine_name'];
2017
                $onePrivilege['grant'] = in_array(
2018
                    'Grant',
2019
                    explode(',', $row['Proc_priv'])
2020
                );
2021
2022
                $privs = $this->parseProcPriv($row['Proc_priv']);
2023
                $onePrivilege['privileges'] = implode(
2024
                    ',',
2025
                    $this->extractPrivInfo($privs, true)
2026
                );
2027
2028
                $paramDbName = $dbname;
2029
                $paramRoutineName = $row['Routine_name'];
2030
            }
2031
2032
            $foundRows[] = $name;
2033
            $onePrivilege['name'] = $name;
2034
2035
            $onePrivilege['edit_link'] = '';
2036
            if ($GLOBALS['is_grantuser']) {
2037
                $onePrivilege['edit_link'] = $this->getUserLink(
2038
                    'edit',
2039
                    $username,
2040
                    $hostname,
2041
                    $paramDbName,
2042
                    $paramTableName,
2043
                    $paramRoutineName
2044
                );
2045
            }
2046
2047
            $onePrivilege['revoke_link'] = '';
2048
            if ($type != 'database' || ! empty($row['can_delete'])) {
2049
                $onePrivilege['revoke_link'] = $this->getUserLink(
2050
                    'revoke',
2051
                    $username,
2052
                    $hostname,
2053
                    $paramDbName,
2054
                    $paramTableName,
2055
                    $paramRoutineName
2056
                );
2057
            }
2058
2059
            $privileges[] = $onePrivilege;
2060
        }
2061
2062
        $data = $uiData[$type];
2063
        $data['privileges'] = $privileges;
2064
        $data['username']   = $username;
2065
        $data['hostname']   = $hostname;
2066
        $data['database']   = $dbname;
2067
        $data['type']       = $type;
2068
2069
        if ($type == 'database') {
2070
            // we already have the list of databases from libraries/common.inc.php
2071
            // via $pma = new PMA;
2072
            $pred_db_array = $GLOBALS['dblist']->databases;
2073
            $databases_to_skip = [
2074
                'information_schema',
2075
                'performance_schema',
2076
            ];
2077
2078
            $databases = [];
2079
            if (! empty($pred_db_array)) {
2080
                foreach ($pred_db_array as $current_db) {
2081
                    if (in_array($current_db, $databases_to_skip)) {
2082
                        continue;
2083
                    }
2084
                    $current_db_escaped = Util::escapeMysqlWildcards($current_db);
2085
                    // cannot use array_diff() once, outside of the loop,
2086
                    // because the list of databases has special characters
2087
                    // already escaped in $foundRows,
2088
                    // contrary to the output of SHOW DATABASES
2089
                    if (! in_array($current_db_escaped, $foundRows)) {
2090
                        $databases[] = $current_db;
2091
                    }
2092
                }
2093
            }
2094
            $data['databases'] = $databases;
2095
        } elseif ($type == 'table') {
2096
            $result = @$this->dbi->tryQuery(
2097
                "SHOW TABLES FROM " . Util::backquote($dbname),
2098
                DatabaseInterface::CONNECT_USER,
2099
                DatabaseInterface::QUERY_STORE
2100
            );
2101
2102
            $tables = [];
2103
            if ($result) {
2104
                while ($row = $this->dbi->fetchRow($result)) {
2105
                    if (! in_array($row[0], $foundRows)) {
2106
                        $tables[] = $row[0];
2107
                    }
2108
                }
2109
                $this->dbi->freeResult($result);
2110
            }
2111
            $data['tables'] = $tables;
2112
        } else { // routine
2113
            $routineData = $this->dbi->getRoutines($dbname);
2114
2115
            $routines = [];
2116
            foreach ($routineData as $routine) {
2117
                if (! in_array($routine['name'], $foundRows)) {
2118
                    $routines[] = $routine['name'];
2119
                }
2120
            }
2121
            $data['routines'] = $routines;
2122
        }
2123
2124
        return $this->template->render('server/privileges/privileges_summary', $data);
2125
    }
2126
2127
    /**
2128
     * Get HTML for display the users overview
2129
     * (if less than 50 users, display them immediately)
2130
     *
2131
     * @param array  $result        ran sql query
2132
     * @param array  $db_rights     user's database rights array
2133
     * @param string $pmaThemeImage a image source link
2134
     * @param string $text_dir      text directory
2135
     *
2136
     * @return string HTML snippet
2137
     */
2138
    public function getUsersOverview($result, array $db_rights, $pmaThemeImage, $text_dir)
2139
    {
2140
        global $is_grantuser, $is_createuser;
2141
2142
        $cfgRelation = $this->relation->getRelationsParam();
2143
2144
        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

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

2148
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
2149
2150
        $user_group_count = 0;
2151
        if ($cfgRelation['menuswork']) {
2152
            $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

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

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

2157
                while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $result)) {
Loading history...
2158
                    $group_assignment[$row['username']] = $row['usergroup'];
2159
                }
2160
            }
2161
            $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

2161
            $this->dbi->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
2162
2163
            $user_group_count = $this->getUserGroupCount();
2164
        }
2165
2166
        $hosts = [];
2167
        foreach ($db_rights as $user) {
2168
            ksort($user);
2169
            foreach ($user as $host) {
2170
                $check_plugin_query = "SELECT * FROM `mysql`.`user` WHERE "
2171
                    . "`User` = '" . $host['User'] . "' AND `Host` = '"
2172
                    . $host['Host'] . "'";
2173
                $res = $this->dbi->fetchSingleRow($check_plugin_query);
2174
2175
                $hasPassword = false;
2176
                if ((isset($res['authentication_string'])
2177
                    && ! empty($res['authentication_string']))
2178
                    || (isset($res['Password'])
2179
                    && ! empty($res['Password']))
2180
                ) {
2181
                    $hasPassword = true;
2182
                }
2183
2184
                $hosts[] = [
2185
                    'user' => $host['User'],
2186
                    'host' => $host['Host'],
2187
                    'has_password' => $hasPassword,
2188
                    'has_select_priv' => isset($host['Select_priv']),
2189
                    'privileges' => $host['privs'],
2190
                    'group' => $group_assignment[$host['User']] ?? '',
2191
                    'has_grant' => $host['Grant_priv'] == 'Y',
2192
                ];
2193
            }
2194
        }
2195
2196
        return $this->template->render('server/privileges/users_overview', [
2197
            'menus_work' => $cfgRelation['menuswork'],
2198
            'user_group_count' => $user_group_count,
2199
            'pma_theme_image' => $pmaThemeImage,
2200
            'text_dir' => $text_dir,
2201
            'initial' => $_GET['initial'] ?? '',
2202
            'hosts' => $hosts,
2203
            'is_grantuser' => $is_grantuser,
2204
            'is_createuser' => $is_createuser,
2205
        ]);
2206
    }
2207
2208
    /**
2209
     * Get HTML for Displays the initials
2210
     *
2211
     * @param array $array_initials array for all initials, even non A-Z
2212
     *
2213
     * @return string HTML snippet
2214
     */
2215
    public function getHtmlForInitials(array $array_initials)
2216
    {
2217
        // initialize to false the letters A-Z
2218
        for ($letter_counter = 1; $letter_counter < 27; $letter_counter++) {
2219
            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

2219
            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...
2220
                $array_initials[mb_chr($letter_counter + 64)] = false;
2221
            }
2222
        }
2223
2224
        $initials = $this->dbi->tryQuery(
2225
            'SELECT DISTINCT UPPER(LEFT(`User`,1)) FROM `user`'
2226
            . ' ORDER BY UPPER(LEFT(`User`,1)) ASC',
2227
            DatabaseInterface::CONNECT_USER,
2228
            DatabaseInterface::QUERY_STORE
2229
        );
2230
        if ($initials) {
2231
            while (list($tmp_initial) = $this->dbi->fetchRow($initials)) {
2232
                $array_initials[$tmp_initial] = true;
2233
            }
2234
        }
2235
2236
        // Display the initials, which can be any characters, not
2237
        // just letters. For letters A-Z, we add the non-used letters
2238
        // as greyed out.
2239
2240
        uksort($array_initials, "strnatcasecmp");
2241
2242
        return $this->template->render('server/privileges/initials_row', [
2243
            'array_initials' => $array_initials,
2244
            'initial' => isset($_GET['initial']) ? $_GET['initial'] : null,
2245
        ]);
2246
    }
2247
2248
    /**
2249
     * Get the database rights array for Display user overview
2250
     *
2251
     * @return array    database rights array
2252
     */
2253
    public function getDbRightsForUserOverview()
2254
    {
2255
        // we also want users not in table `user` but in other table
2256
        $tables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`;');
2257
2258
        $tablesSearchForUsers = [
2259
            'user',
2260
            'db',
2261
            'tables_priv',
2262
            'columns_priv',
2263
            'procs_priv',
2264
        ];
2265
2266
        $db_rights_sqls = [];
2267
        foreach ($tablesSearchForUsers as $table_search_in) {
2268
            if (in_array($table_search_in, $tables)) {
2269
                $db_rights_sqls[] = 'SELECT DISTINCT `User`, `Host` FROM `mysql`.`'
2270
                    . $table_search_in . '` '
2271
                    . (isset($_GET['initial'])
2272
                    ? $this->rangeOfUsers($_GET['initial'])
2273
                    : '');
2274
            }
2275
        }
2276
        $user_defaults = [
2277
            'User'       => '',
2278
            'Host'       => '%',
2279
            'Password'   => '?',
2280
            'Grant_priv' => 'N',
2281
            'privs'      => ['USAGE'],
2282
        ];
2283
2284
        // for the rights
2285
        $db_rights = [];
2286
2287
        $db_rights_sql = '(' . implode(') UNION (', $db_rights_sqls) . ')'
2288
            . ' ORDER BY `User` ASC, `Host` ASC';
2289
2290
        $db_rights_result = $this->dbi->query($db_rights_sql);
2291
2292
        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

2292
        while ($db_rights_row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $db_rights_result)) {
Loading history...
2293
            $db_rights_row = array_merge($user_defaults, $db_rights_row);
2294
            $db_rights[$db_rights_row['User']][$db_rights_row['Host']]
2295
                = $db_rights_row;
2296
        }
2297
        $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

2297
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $db_rights_result);
Loading history...
2298
        ksort($db_rights);
2299
2300
        return $db_rights;
2301
    }
2302
2303
    /**
2304
     * Delete user and get message and sql query for delete user in privileges
2305
     *
2306
     * @param array $queries queries
2307
     *
2308
     * @return array Message
2309
     */
2310
    public function deleteUser(array $queries)
2311
    {
2312
        $sql_query = '';
2313
        if (empty($queries)) {
2314
            $message = Message::error(__('No users selected for deleting!'));
2315
        } else {
2316
            if ($_POST['mode'] == 3) {
2317
                $queries[] = '# ' . __('Reloading the privileges') . ' …';
2318
                $queries[] = 'FLUSH PRIVILEGES;';
2319
            }
2320
            $drop_user_error = '';
2321
            foreach ($queries as $sql_query) {
2322
                if ($sql_query[0] != '#') {
2323
                    if (! $this->dbi->tryQuery($sql_query)) {
2324
                        $drop_user_error .= $this->dbi->getError() . "\n";
2325
                    }
2326
                }
2327
            }
2328
            // tracking sets this, causing the deleted db to be shown in navi
2329
            unset($GLOBALS['db']);
2330
2331
            $sql_query = implode("\n", $queries);
2332
            if (! empty($drop_user_error)) {
2333
                $message = Message::rawError($drop_user_error);
2334
            } else {
2335
                $message = Message::success(
2336
                    __('The selected users have been deleted successfully.')
2337
                );
2338
            }
2339
        }
2340
        return [
2341
            $sql_query,
2342
            $message,
2343
        ];
2344
    }
2345
2346
    /**
2347
     * Update the privileges and return the success or error message
2348
     *
2349
     * @param string $username  username
2350
     * @param string $hostname  host name
2351
     * @param string $tablename table name
2352
     * @param string $dbname    database name
2353
     * @param string $itemType  item type
2354
     *
2355
     * @return array success message or error message for update
2356
     */
2357
    public function updatePrivileges($username, $hostname, $tablename, $dbname, $itemType)
2358
    {
2359
        $db_and_table = $this->wildcardEscapeForGrant($dbname, $tablename);
2360
2361
        $sql_query0 = 'REVOKE ALL PRIVILEGES ON ' . $itemType . ' ' . $db_and_table
2362
            . ' FROM \'' . $this->dbi->escapeString($username)
2363
            . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
2364
2365
        if (! isset($_POST['Grant_priv']) || $_POST['Grant_priv'] != 'Y') {
2366
            $sql_query1 = 'REVOKE GRANT OPTION ON ' . $itemType . ' ' . $db_and_table
2367
                . ' FROM \'' . $this->dbi->escapeString($username) . '\'@\''
2368
                . $this->dbi->escapeString($hostname) . '\';';
2369
        } else {
2370
            $sql_query1 = '';
2371
        }
2372
2373
        // Should not do a GRANT USAGE for a table-specific privilege, it
2374
        // causes problems later (cannot revoke it)
2375
        if (! (strlen($tablename) > 0
2376
            && 'USAGE' == implode('', $this->extractPrivInfo()))
2377
        ) {
2378
            $sql_query2 = 'GRANT ' . implode(', ', $this->extractPrivInfo())
2379
                . ' ON ' . $itemType . ' ' . $db_and_table
2380
                . ' TO \'' . $this->dbi->escapeString($username) . '\'@\''
2381
                . $this->dbi->escapeString($hostname) . '\'';
2382
2383
            if (strlen($dbname) === 0) {
2384
                // add REQUIRE clause
2385
                $sql_query2 .= $this->getRequireClause();
2386
            }
2387
2388
            if ((isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y')
2389
                || (strlen($dbname) === 0
2390
                && (isset($_POST['max_questions']) || isset($_POST['max_connections'])
2391
                || isset($_POST['max_updates'])
2392
                || isset($_POST['max_user_connections'])))
2393
            ) {
2394
                $sql_query2 .= $this->getWithClauseForAddUserAndUpdatePrivs();
2395
            }
2396
            $sql_query2 .= ';';
2397
        }
2398
        if (! $this->dbi->tryQuery($sql_query0)) {
2399
            // This might fail when the executing user does not have
2400
            // ALL PRIVILEGES himself.
2401
            // See https://github.com/phpmyadmin/phpmyadmin/issues/9673
2402
            $sql_query0 = '';
2403
        }
2404
        if (! empty($sql_query1) && ! $this->dbi->tryQuery($sql_query1)) {
2405
            // this one may fail, too...
2406
            $sql_query1 = '';
2407
        }
2408
        if (! empty($sql_query2)) {
2409
            $this->dbi->query($sql_query2);
2410
        } else {
2411
            $sql_query2 = '';
2412
        }
2413
        $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...
2414
        $message = Message::success(__('You have updated the privileges for %s.'));
2415
        $message->addParam('\'' . $username . '\'@\'' . $hostname . '\'');
2416
2417
        return [
2418
            $sql_query,
2419
            $message,
2420
        ];
2421
    }
2422
2423
    /**
2424
     * Get List of information: Changes / copies a user
2425
     *
2426
     * @return array
2427
     */
2428
    public function getDataForChangeOrCopyUser()
2429
    {
2430
        $queries = null;
2431
        $password = null;
2432
2433
        if (isset($_POST['change_copy'])) {
2434
            $user_host_condition = ' WHERE `User` = '
2435
                . "'" . $this->dbi->escapeString($_POST['old_username']) . "'"
2436
                . ' AND `Host` = '
2437
                . "'" . $this->dbi->escapeString($_POST['old_hostname']) . "';";
2438
            $row = $this->dbi->fetchSingleRow(
2439
                'SELECT * FROM `mysql`.`user` ' . $user_host_condition
2440
            );
2441
            if (! $row) {
2442
                $response = Response::getInstance();
2443
                $response->addHTML(
2444
                    Message::notice(__('No user found.'))->getDisplay()
2445
                );
2446
                unset($_POST['change_copy']);
2447
            } else {
2448
                foreach ($row as $key => $value) {
2449
                    $GLOBALS[$key] = $value;
2450
                }
2451
                $serverVersion = $this->dbi->getVersion();
2452
                // Recent MySQL versions have the field "Password" in mysql.user,
2453
                // so the previous extract creates $row['Password'] but this script
2454
                // uses $password
2455
                if (! isset($row['password']) && isset($row['Password'])) {
2456
                    $row['password'] = $row['Password'];
2457
                }
2458
                if (Util::getServerType() == 'MySQL'
2459
                    && $serverVersion >= 50606
2460
                    && $serverVersion < 50706
2461
                    && ((isset($row['authentication_string'])
2462
                    && empty($row['password']))
2463
                    || (isset($row['plugin'])
2464
                    && $row['plugin'] == 'sha256_password'))
2465
                ) {
2466
                    $row['password'] = $row['authentication_string'];
2467
                }
2468
2469
                if (Util::getServerType() == 'MariaDB'
2470
                    && $serverVersion >= 50500
2471
                    && isset($row['authentication_string'])
2472
                    && empty($row['password'])
2473
                ) {
2474
                    $row['password'] = $row['authentication_string'];
2475
                }
2476
2477
                // Always use 'authentication_string' column
2478
                // for MySQL 5.7.6+ since it does not have
2479
                // the 'password' column at all
2480
                if (in_array(Util::getServerType(), ['MySQL', 'Percona Server'])
2481
                    && $serverVersion >= 50706
2482
                    && isset($row['authentication_string'])
2483
                ) {
2484
                    $row['password'] = $row['authentication_string'];
2485
                }
2486
                $password = $row['password'];
2487
                $queries = [];
2488
            }
2489
        }
2490
2491
        return [
2492
            $queries,
2493
            $password,
2494
        ];
2495
    }
2496
2497
    /**
2498
     * Update Data for information: Deletes users
2499
     *
2500
     * @param array $queries queries array
2501
     *
2502
     * @return array
2503
     */
2504
    public function getDataForDeleteUsers($queries)
2505
    {
2506
        if (isset($_POST['change_copy'])) {
2507
            $selected_usr = [
2508
                $_POST['old_username'] . '&amp;#27;' . $_POST['old_hostname'],
2509
            ];
2510
        } else {
2511
            $selected_usr = $_POST['selected_usr'];
2512
            $queries = [];
2513
        }
2514
2515
        // this happens, was seen in https://reports.phpmyadmin.net/reports/view/17146
2516
        if (! is_array($selected_usr)) {
2517
            return [];
2518
        }
2519
2520
        foreach ($selected_usr as $each_user) {
2521
            list($this_user, $this_host) = explode('&amp;#27;', $each_user);
2522
            $queries[] = '# '
2523
                . sprintf(
2524
                    __('Deleting %s'),
2525
                    '\'' . $this_user . '\'@\'' . $this_host . '\''
2526
                )
2527
                . ' ...';
2528
            $queries[] = 'DROP USER \''
2529
                . $this->dbi->escapeString($this_user)
2530
                . '\'@\'' . $this->dbi->escapeString($this_host) . '\';';
2531
            $this->relationCleanup->user($this_user);
2532
2533
            if (isset($_POST['drop_users_db'])) {
2534
                $queries[] = 'DROP DATABASE IF EXISTS '
2535
                    . Util::backquote($this_user) . ';';
2536
                $GLOBALS['reload'] = true;
2537
            }
2538
        }
2539
        return $queries;
2540
    }
2541
2542
    /**
2543
     * update Message For Reload
2544
     *
2545
     * @return Message|null
2546
     */
2547
    public function updateMessageForReload(): ?Message
2548
    {
2549
        $message = null;
2550
        if (isset($_GET['flush_privileges'])) {
2551
            $sql_query = 'FLUSH PRIVILEGES;';
2552
            $this->dbi->query($sql_query);
2553
            $message = Message::success(
2554
                __('The privileges were reloaded successfully.')
2555
            );
2556
        }
2557
2558
        if (isset($_GET['validate_username'])) {
2559
            $message = Message::success();
2560
        }
2561
2562
        return $message;
2563
    }
2564
2565
    /**
2566
     * update Data For Queries from queries_for_display
2567
     *
2568
     * @param array      $queries             queries array
2569
     * @param array|null $queries_for_display queries array for display
2570
     *
2571
     * @return array
2572
     */
2573
    public function getDataForQueries(array $queries, $queries_for_display)
2574
    {
2575
        $tmp_count = 0;
2576
        foreach ($queries as $sql_query) {
2577
            if ($sql_query[0] != '#') {
2578
                $this->dbi->query($sql_query);
2579
            }
2580
            // when there is a query containing a hidden password, take it
2581
            // instead of the real query sent
2582
            if (isset($queries_for_display[$tmp_count])) {
2583
                $queries[$tmp_count] = $queries_for_display[$tmp_count];
2584
            }
2585
            $tmp_count++;
2586
        }
2587
2588
        return $queries;
2589
    }
2590
2591
    /**
2592
     * update Data for information: Adds a user
2593
     *
2594
     * @param string|array|null $dbname      db name
2595
     * @param string            $username    user name
2596
     * @param string            $hostname    host name
2597
     * @param string|null       $password    password
2598
     * @param bool              $is_menuwork is_menuwork set?
2599
     *
2600
     * @return array
2601
     */
2602
    public function addUser(
2603
        $dbname,
2604
        $username,
2605
        $hostname,
2606
        ?string $password,
2607
        $is_menuwork
2608
    ) {
2609
        $_add_user_error = false;
2610
        $message = null;
2611
        $queries = null;
2612
        $queries_for_display = null;
2613
        $sql_query = null;
2614
2615
        if (! isset($_POST['adduser_submit']) && ! isset($_POST['change_copy'])) {
2616
            return [
2617
                $message,
2618
                $queries,
2619
                $queries_for_display,
2620
                $sql_query,
2621
                $_add_user_error,
2622
            ];
2623
        }
2624
2625
        $sql_query = '';
2626
        if ($_POST['pred_username'] == 'any') {
2627
            $username = '';
2628
        }
2629
        switch ($_POST['pred_hostname']) {
2630
            case 'any':
2631
                $hostname = '%';
2632
                break;
2633
            case 'localhost':
2634
                $hostname = 'localhost';
2635
                break;
2636
            case 'hosttable':
2637
                $hostname = '';
2638
                break;
2639
            case 'thishost':
2640
                $_user_name = $this->dbi->fetchValue('SELECT USER()');
2641
                $hostname = mb_substr(
2642
                    $_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

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

2643
                    mb_strrpos(/** @scrutinizer ignore-type */ $_user_name, '@') + 1
Loading history...
2644
                );
2645
                unset($_user_name);
2646
                break;
2647
        }
2648
        $sql = "SELECT '1' FROM `mysql`.`user`"
2649
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
2650
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';";
2651
        if ($this->dbi->fetchValue($sql) == 1) {
2652
            $message = Message::error(__('The user %s already exists!'));
2653
            $message->addParam('[em]\'' . $username . '\'@\'' . $hostname . '\'[/em]');
2654
            $_GET['adduser'] = true;
2655
            $_add_user_error = true;
2656
2657
            return [
2658
                $message,
2659
                $queries,
2660
                $queries_for_display,
2661
                $sql_query,
2662
                $_add_user_error,
2663
            ];
2664
        }
2665
2666
        list(
2667
            $create_user_real,
2668
            $create_user_show,
2669
            $real_sql_query,
2670
            $sql_query,
2671
            $password_set_real,
2672
            $password_set_show,
2673
            $alter_real_sql_query,
2674
            $alter_sql_query
2675
        ) = $this->getSqlQueriesForDisplayAndAddUser(
2676
            $username,
2677
            $hostname,
2678
            (isset($password) ? $password : '')
2679
        );
2680
2681
        if (empty($_POST['change_copy'])) {
2682
            $_error = false;
2683
2684
            if ($create_user_real !== null) {
0 ignored issues
show
introduced by
The condition $create_user_real !== null is always true.
Loading history...
2685
                if (! $this->dbi->tryQuery($create_user_real)) {
2686
                    $_error = true;
2687
                }
2688
                if (isset($password_set_real, $_POST['authentication_plugin']) && ! empty($password_set_real)) {
2689
                    $this->setProperPasswordHashing(
2690
                        $_POST['authentication_plugin']
2691
                    );
2692
                    if ($this->dbi->tryQuery($password_set_real)) {
2693
                        $sql_query .= $password_set_show;
2694
                    }
2695
                }
2696
                $sql_query = $create_user_show . $sql_query;
2697
            }
2698
2699
            list($sql_query, $message) = $this->addUserAndCreateDatabase(
2700
                $_error,
2701
                $real_sql_query,
2702
                $sql_query,
2703
                $username,
2704
                $hostname,
2705
                $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

2705
                /** @scrutinizer ignore-type */ $dbname,
Loading history...
2706
                $alter_real_sql_query,
2707
                $alter_sql_query
2708
            );
2709
            if (! empty($_POST['userGroup']) && $is_menuwork) {
2710
                $this->setUserGroup($GLOBALS['username'], $_POST['userGroup']);
2711
            }
2712
2713
            return [
2714
                $message,
2715
                $queries,
2716
                $queries_for_display,
2717
                $sql_query,
2718
                $_add_user_error,
2719
            ];
2720
        }
2721
2722
        // Copy the user group while copying a user
2723
        $old_usergroup =
2724
            isset($_POST['old_usergroup']) ? $_POST['old_usergroup'] : null;
2725
        $this->setUserGroup($_POST['username'], $old_usergroup);
2726
2727
        if ($create_user_real === null) {
0 ignored issues
show
introduced by
The condition $create_user_real === null is always false.
Loading history...
2728
            $queries[] = $create_user_real;
2729
        }
2730
        $queries[] = $real_sql_query;
2731
2732
        if (isset($password_set_real, $_POST['authentication_plugin']) && ! empty($password_set_real)) {
2733
            $this->setProperPasswordHashing(
2734
                $_POST['authentication_plugin']
2735
            );
2736
2737
            $queries[] = $password_set_real;
2738
        }
2739
        // we put the query containing the hidden password in
2740
        // $queries_for_display, at the same position occupied
2741
        // by the real query in $queries
2742
        $tmp_count = count($queries);
2743
        if (isset($create_user_real)) {
2744
            $queries_for_display[$tmp_count - 2] = $create_user_show;
2745
        }
2746
        if (isset($password_set_real) && ! empty($password_set_real)) {
2747
            $queries_for_display[$tmp_count - 3] = $create_user_show;
2748
            $queries_for_display[$tmp_count - 2] = $sql_query;
2749
            $queries_for_display[$tmp_count - 1] = $password_set_show;
2750
        } else {
2751
            $queries_for_display[$tmp_count - 1] = $sql_query;
2752
        }
2753
2754
        return [
2755
            $message,
2756
            $queries,
2757
            $queries_for_display,
2758
            $sql_query,
2759
            $_add_user_error,
2760
        ];
2761
    }
2762
2763
    /**
2764
     * Sets proper value of `old_passwords` according to
2765
     * the authentication plugin selected
2766
     *
2767
     * @param string $auth_plugin authentication plugin selected
2768
     *
2769
     * @return void
2770
     */
2771
    public function setProperPasswordHashing($auth_plugin)
2772
    {
2773
        // Set the hashing method used by PASSWORD()
2774
        // to be of type depending upon $authentication_plugin
2775
        if ($auth_plugin == 'sha256_password') {
2776
            $this->dbi->tryQuery('SET `old_passwords` = 2;');
2777
        } elseif ($auth_plugin == 'mysql_old_password') {
2778
            $this->dbi->tryQuery('SET `old_passwords` = 1;');
2779
        } else {
2780
            $this->dbi->tryQuery('SET `old_passwords` = 0;');
2781
        }
2782
    }
2783
2784
    /**
2785
     * Update DB information: DB, Table, isWildcard
2786
     *
2787
     * @return array
2788
     */
2789
    public function getDataForDBInfo()
2790
    {
2791
        $username = null;
2792
        $hostname = null;
2793
        $dbname = null;
2794
        $tablename = null;
2795
        $routinename = null;
2796
2797
        if (isset($_REQUEST['username'])) {
2798
            $username = $_REQUEST['username'];
2799
        }
2800
        if (isset($_REQUEST['hostname'])) {
2801
            $hostname = $_REQUEST['hostname'];
2802
        }
2803
        /**
2804
         * Checks if a dropdown box has been used for selecting a database / table
2805
         */
2806
        if (Core::isValid($_POST['pred_tablename'])) {
2807
            $tablename = $_POST['pred_tablename'];
2808
        } elseif (Core::isValid($_REQUEST['tablename'])) {
2809
            $tablename = $_REQUEST['tablename'];
2810
        } else {
2811
            unset($tablename);
2812
        }
2813
2814
        if (Core::isValid($_POST['pred_routinename'])) {
2815
            $routinename = $_POST['pred_routinename'];
2816
        } elseif (Core::isValid($_REQUEST['routinename'])) {
2817
            $routinename = $_REQUEST['routinename'];
2818
        } else {
2819
            unset($routinename);
2820
        }
2821
2822
        if (isset($_POST['pred_dbname'])) {
2823
            $is_valid_pred_dbname = true;
2824
            foreach ($_POST['pred_dbname'] as $key => $db_name) {
2825
                if (! Core::isValid($db_name)) {
2826
                    $is_valid_pred_dbname = false;
2827
                    break;
2828
                }
2829
            }
2830
        }
2831
2832
        if (isset($_REQUEST['dbname'])) {
2833
            $is_valid_dbname = true;
2834
            if (is_array($_REQUEST['dbname'])) {
2835
                foreach ($_REQUEST['dbname'] as $key => $db_name) {
2836
                    if (! Core::isValid($db_name)) {
2837
                        $is_valid_dbname = false;
2838
                        break;
2839
                    }
2840
                }
2841
            } else {
2842
                if (! Core::isValid($_REQUEST['dbname'])) {
2843
                    $is_valid_dbname = false;
2844
                }
2845
            }
2846
        }
2847
2848
        if (isset($is_valid_pred_dbname) && $is_valid_pred_dbname) {
2849
            $dbname = $_POST['pred_dbname'];
2850
            // If dbname contains only one database.
2851
            if (count($dbname) === 1) {
2852
                $dbname = $dbname[0];
2853
            }
2854
        } elseif (isset($is_valid_dbname) && $is_valid_dbname) {
2855
            $dbname = $_REQUEST['dbname'];
2856
        } else {
2857
            unset($dbname, $tablename);
2858
        }
2859
2860
        if (isset($dbname)) {
2861
            if (is_array($dbname)) {
2862
                $db_and_table = $dbname;
2863
                foreach ($db_and_table as $key => $db_name) {
2864
                    $db_and_table[$key] .= '.';
2865
                }
2866
            } else {
2867
                $unescaped_db = Util::unescapeMysqlWildcards($dbname);
2868
                $db_and_table = Util::backquote($unescaped_db) . '.';
2869
            }
2870
            if (isset($tablename)) {
2871
                $db_and_table .= Util::backquote($tablename);
2872
            } else {
2873
                if (is_array($db_and_table)) {
2874
                    foreach ($db_and_table as $key => $db_name) {
2875
                        $db_and_table[$key] .= '*';
2876
                    }
2877
                } else {
2878
                    $db_and_table .= '*';
2879
                }
2880
            }
2881
        } else {
2882
            $db_and_table = '*.*';
2883
        }
2884
2885
        // check if given $dbname is a wildcard or not
2886
        $databaseNameIsWildcard = ! is_array($dbname ?? '') && preg_match(
2887
            '/(?<!\\\\)(?:_|%)/',
2888
            $dbname ?? ''
2889
        );
2890
2891
        return [
2892
            $username,
2893
            $hostname,
2894
            isset($dbname) ? $dbname : null,
2895
            isset($tablename) ? $tablename : null,
2896
            isset($routinename) ? $routinename : null,
2897
            $db_and_table,
2898
            $databaseNameIsWildcard,
2899
        ];
2900
    }
2901
2902
    /**
2903
     * Get title and textarea for export user definition in Privileges
2904
     *
2905
     * @param string $username username
2906
     * @param string $hostname host name
2907
     *
2908
     * @return array ($title, $export)
2909
     */
2910
    public function getListForExportUserDefinition($username, $hostname)
2911
    {
2912
        $export = '<textarea class="export" cols="60" rows="15">';
2913
2914
        if (isset($_POST['selected_usr'])) {
2915
            // export privileges for selected users
2916
            $title = __('Privileges');
2917
2918
            //For removing duplicate entries of users
2919
            $_POST['selected_usr'] = array_unique($_POST['selected_usr']);
2920
2921
            foreach ($_POST['selected_usr'] as $export_user) {
2922
                $export_username = mb_substr(
2923
                    $export_user,
2924
                    0,
2925
                    mb_strpos($export_user, '&')
2926
                );
2927
                $export_hostname = mb_substr(
2928
                    $export_user,
2929
                    mb_strrpos($export_user, ';') + 1
2930
                );
2931
                $export .= '# '
2932
                    . sprintf(
2933
                        __('Privileges for %s'),
2934
                        '`' . htmlspecialchars($export_username)
2935
                        . '`@`' . htmlspecialchars($export_hostname) . '`'
2936
                    )
2937
                    . "\n\n";
2938
                $export .= $this->getGrants($export_username, $export_hostname) . "\n";
2939
            }
2940
        } else {
2941
            // export privileges for a single user
2942
            $title = __('User') . ' `' . htmlspecialchars($username)
2943
                . '`@`' . htmlspecialchars($hostname) . '`';
2944
            $export .= $this->getGrants($username, $hostname);
2945
        }
2946
        // remove trailing whitespace
2947
        $export = trim($export);
2948
2949
        $export .= '</textarea>';
2950
2951
        return [
2952
            $title,
2953
            $export,
2954
        ];
2955
    }
2956
2957
    /**
2958
     * Get HTML for display Add userfieldset
2959
     *
2960
     * @param string $db    the database
2961
     * @param string $table the table name
2962
     *
2963
     * @return string html output
2964
     */
2965
    public function getAddUserHtmlFieldset($db = '', $table = '')
2966
    {
2967
        if (! $GLOBALS['is_createuser']) {
2968
            return '';
2969
        }
2970
        $rel_params = [];
2971
        $url_params = [
2972
            'adduser' => 1,
2973
        ];
2974
        if (! empty($db)) {
2975
            $url_params['dbname']
2976
                = $rel_params['checkprivsdb']
2977
                    = $db;
2978
        }
2979
        if (! empty($table)) {
2980
            $url_params['tablename']
2981
                = $rel_params['checkprivstable']
2982
                    = $table;
2983
        }
2984
2985
        return $this->template->render('server/privileges/add_user_fieldset', [
2986
            'url_params' => $url_params,
2987
            'rel_params' => $rel_params,
2988
        ]);
2989
    }
2990
2991
    /**
2992
     * Get HTML snippet for display user overview page
2993
     *
2994
     * @param string $pmaThemeImage a image source link
2995
     * @param string $text_dir      text directory
2996
     *
2997
     * @return string
2998
     */
2999
    public function getHtmlForUserOverview($pmaThemeImage, $text_dir)
3000
    {
3001
        global $is_createuser;
3002
3003
        $password_column = 'Password';
3004
        $server_type = Util::getServerType();
3005
        $serverVersion = $this->dbi->getVersion();
3006
        if (($server_type == 'MySQL' || $server_type == 'Percona Server')
3007
            && $serverVersion >= 50706
3008
        ) {
3009
            $password_column = 'authentication_string';
3010
        }
3011
        // $sql_query is for the initial-filtered,
3012
        // $sql_query_all is for counting the total no. of users
3013
3014
        $sql_query = $sql_query_all = 'SELECT *,' .
3015
            " IF(`" . $password_column . "` = _latin1 '', 'N', 'Y') AS 'Password'" .
3016
            ' FROM `mysql`.`user`';
3017
3018
        $sql_query .= (isset($_GET['initial'])
3019
            ? $this->rangeOfUsers($_GET['initial'])
3020
            : '');
3021
3022
        $sql_query .= ' ORDER BY `User` ASC, `Host` ASC;';
3023
        $sql_query_all .= ' ;';
3024
3025
        $res = $this->dbi->tryQuery(
3026
            $sql_query,
3027
            DatabaseInterface::CONNECT_USER,
3028
            DatabaseInterface::QUERY_STORE
3029
        );
3030
        $res_all = $this->dbi->tryQuery(
3031
            $sql_query_all,
3032
            DatabaseInterface::CONNECT_USER,
3033
            DatabaseInterface::QUERY_STORE
3034
        );
3035
3036
        $errorMessages = '';
3037
        if (! $res) {
3038
            // the query failed! This may have two reasons:
3039
            // - the user does not have enough privileges
3040
            // - the privilege tables use a structure of an earlier version.
3041
            // so let's try a more simple query
3042
3043
            $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

3043
            $this->dbi->freeResult(/** @scrutinizer ignore-type */ $res);
Loading history...
3044
            $this->dbi->freeResult($res_all);
3045
            $sql_query = 'SELECT * FROM `mysql`.`user`';
3046
            $res = $this->dbi->tryQuery(
3047
                $sql_query,
3048
                DatabaseInterface::CONNECT_USER,
3049
                DatabaseInterface::QUERY_STORE
3050
            );
3051
3052
            if (! $res) {
3053
                $errorMessages .= $this->getHtmlForViewUsersError();
3054
                $errorMessages .= $this->getAddUserHtmlFieldset();
3055
            } else {
3056
                // This message is hardcoded because I will replace it by
3057
                // a automatic repair feature soon.
3058
                $raw = 'Your privilege table structure seems to be older than'
3059
                    . ' this MySQL version!<br>'
3060
                    . 'Please run the <code>mysql_upgrade</code> command'
3061
                    . ' that should be included in your MySQL server distribution'
3062
                    . ' to solve this problem!';
3063
                $errorMessages .= Message::rawError($raw)->getDisplay();
3064
            }
3065
            $this->dbi->freeResult($res);
3066
        } else {
3067
            $db_rights = $this->getDbRightsForUserOverview();
3068
            // for all initials, even non A-Z
3069
            $array_initials = [];
3070
3071
            foreach ($db_rights as $right) {
3072
                foreach ($right as $account) {
3073
                    if (empty($account['User']) && $account['Host'] == 'localhost') {
3074
                        $emptyUserNotice = Message::notice(
3075
                            __(
3076
                                'A user account allowing any user from localhost to '
3077
                                . 'connect is present. This will prevent other users '
3078
                                . 'from connecting if the host part of their account '
3079
                                . 'allows a connection from any (%) host.'
3080
                            )
3081
                            . Util::showMySQLDocu('problems-connecting')
3082
                        )->getDisplay();
3083
                        break 2;
3084
                    }
3085
                }
3086
            }
3087
3088
            /**
3089
             * Displays the initials
3090
             * Also not necessary if there is less than 20 privileges
3091
             */
3092
            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

3092
            if ($this->dbi->numRows(/** @scrutinizer ignore-type */ $res_all) > 20) {
Loading history...
3093
                $initials = $this->getHtmlForInitials($array_initials);
3094
            }
3095
3096
            /**
3097
            * Display the user overview
3098
            * (if less than 50 users, display them immediately)
3099
            */
3100
            if (isset($_GET['initial'])
3101
                || isset($_GET['showall'])
3102
                || $this->dbi->numRows($res) < 50
3103
            ) {
3104
                $usersOverview = $this->getUsersOverview(
3105
                    $res,
3106
                    $db_rights,
3107
                    $pmaThemeImage,
3108
                    $text_dir
3109
                );
3110
            }
3111
3112
            $response = Response::getInstance();
3113
            if (! $response->isAjax()
3114
                || ! empty($_REQUEST['ajax_page_request'])
3115
            ) {
3116
                if ($GLOBALS['is_reload_priv']) {
3117
                    $flushnote = new Message(
3118
                        __(
3119
                            'Note: phpMyAdmin gets the users’ privileges directly '
3120
                            . 'from MySQL’s privilege tables. The content of these '
3121
                            . 'tables may differ from the privileges the server uses, '
3122
                            . 'if they have been changed manually. In this case, '
3123
                            . 'you should %sreload the privileges%s before you continue.'
3124
                        ),
3125
                        Message::NOTICE
3126
                    );
3127
                    $flushnote->addParamHtml(
3128
                        '<a href="' . Url::getFromRoute('/server/privileges', ['flush_privileges' => 1])
3129
                        . '" id="reload_privileges_anchor">'
3130
                    );
3131
                    $flushnote->addParamHtml('</a>');
3132
                } else {
3133
                    $flushnote = new Message(
3134
                        __(
3135
                            'Note: phpMyAdmin gets the users’ privileges directly '
3136
                            . 'from MySQL’s privilege tables. The content of these '
3137
                            . 'tables may differ from the privileges the server uses, '
3138
                            . 'if they have been changed manually. In this case, '
3139
                            . 'the privileges have to be reloaded but currently, you '
3140
                            . 'don\'t have the RELOAD privilege.'
3141
                        )
3142
                        . Util::showMySQLDocu(
3143
                            'privileges-provided',
3144
                            false,
3145
                            null,
3146
                            null,
3147
                            'priv_reload'
3148
                        ),
3149
                        Message::NOTICE
3150
                    );
3151
                }
3152
                $flushNotice = $flushnote->getDisplay();
3153
            }
3154
        }
3155
3156
        return $this->template->render('server/privileges/user_overview', [
3157
            'error_messages' => $errorMessages,
3158
            'empty_user_notice' => $emptyUserNotice ?? '',
3159
            'initials' => $initials ?? '',
3160
            'users_overview' => $usersOverview ?? '',
3161
            'is_createuser' => $is_createuser,
3162
            'flush_notice' => $flushNotice ?? '',
3163
        ]);
3164
    }
3165
3166
    /**
3167
     * Get HTML snippet for display user properties
3168
     *
3169
     * @param boolean      $dbname_is_wildcard whether database name is wildcard or not
3170
     * @param string       $url_dbname         url database name that urlencode() string
3171
     * @param string       $username           username
3172
     * @param string       $hostname           host name
3173
     * @param string|array $dbname             database name
3174
     * @param string       $tablename          table name
3175
     *
3176
     * @return string
3177
     */
3178
    public function getHtmlForUserProperties(
3179
        $dbname_is_wildcard,
3180
        $url_dbname,
3181
        $username,
3182
        $hostname,
3183
        $dbname,
3184
        $tablename
3185
    ) {
3186
        global $cfg;
3187
3188
        $sql = "SELECT '1' FROM `mysql`.`user`"
3189
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
3190
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';";
3191
3192
        $user_does_not_exists = (bool) ! $this->dbi->fetchValue($sql);
3193
3194
        $loginInformationFields = '';
3195
        if ($user_does_not_exists) {
3196
            $loginInformationFields = $this->getHtmlForLoginInformationFields();
3197
        }
3198
3199
        $_params = [
3200
            'username' => $username,
3201
            'hostname' => $hostname,
3202
        ];
3203
        if (! is_array($dbname) && strlen($dbname) > 0) {
3204
            $_params['dbname'] = $dbname;
3205
            if (strlen($tablename) > 0) {
3206
                $_params['tablename'] = $tablename;
3207
            }
3208
        } else {
3209
            $_params['dbname'] = $dbname;
3210
        }
3211
3212
        $privilegesTable = $this->getHtmlToDisplayPrivilegesTable(
3213
            // If $dbname is an array, pass any one db as all have same privs.
3214
            Core::ifSetOr($dbname, is_array($dbname) ? $dbname[0] : '*', 'length'),
3215
            Core::ifSetOr($tablename, '*', 'length')
3216
        );
3217
3218
        $tableSpecificRights = '';
3219
        if (! is_array($dbname) && strlen($tablename) === 0
3220
            && empty($dbname_is_wildcard)
3221
        ) {
3222
            // no table name was given, display all table specific rights
3223
            // but only if $dbname contains no wildcards
3224
            if (strlen($dbname) === 0) {
3225
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3226
                    $username,
3227
                    $hostname,
3228
                    'database'
3229
                );
3230
            } else {
3231
                // unescape wildcards in dbname at table level
3232
                $unescaped_db = Util::unescapeMysqlWildcards($dbname);
3233
3234
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3235
                    $username,
3236
                    $hostname,
3237
                    'table',
3238
                    $unescaped_db
3239
                );
3240
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3241
                    $username,
3242
                    $hostname,
3243
                    'routine',
3244
                    $unescaped_db
3245
                );
3246
            }
3247
        }
3248
3249
        $databaseUrl = Util::getScriptNameForOption(
3250
            $cfg['DefaultTabDatabase'],
3251
            'database'
3252
        );
3253
        $databaseUrlTitle = Util::getTitleForTarget(
3254
            $cfg['DefaultTabDatabase']
3255
        );
3256
        $tableUrl = Util::getScriptNameForOption(
3257
            $cfg['DefaultTabTable'],
3258
            'table'
3259
        );
3260
        $tableUrlTitle = Util::getTitleForTarget(
3261
            $cfg['DefaultTabTable']
3262
        );
3263
3264
        $changePassword = '';
3265
        $userGroup = '';
3266
        $changeLoginInfoFields = '';
3267
        if (! is_array($dbname) && strlen($dbname) === 0 && ! $user_does_not_exists) {
3268
            //change login information
3269
            $changePassword = ChangePassword::getHtml(
3270
                'edit_other',
3271
                $username,
3272
                $hostname
3273
            );
3274
            $userGroup = $this->getUserGroupForUser($username);
3275
            $changeLoginInfoFields = $this->getHtmlForLoginInformationFields('change', $username, $hostname);
3276
        }
3277
3278
        return $this->template->render('server/privileges/user_properties', [
3279
            'user_does_not_exists' => $user_does_not_exists,
3280
            'login_information_fields' => $loginInformationFields,
3281
            'params' => $_params,
3282
            'privileges_table' => $privilegesTable,
3283
            'table_specific_rights' => $tableSpecificRights,
3284
            'change_password' => $changePassword,
3285
            'database' => $dbname,
3286
            'dbname' => $url_dbname,
3287
            'username' => $username,
3288
            'hostname' => $hostname,
3289
            'is_databases' => $dbname_is_wildcard || is_array($dbname) && count($dbname) > 1,
3290
            'is_wildcard' => $dbname_is_wildcard,
3291
            'table' => $tablename,
3292
            'current_user' => $this->dbi->getCurrentUser(),
3293
            'user_group' => $userGroup,
3294
            'change_login_info_fields' => $changeLoginInfoFields,
3295
            'database_url' => $databaseUrl,
3296
            'database_url_title' => $databaseUrlTitle,
3297
            'table_url' => $tableUrl,
3298
            'table_url_title' => $tableUrlTitle,
3299
        ]);
3300
    }
3301
3302
    /**
3303
     * Get queries for Table privileges to change or copy user
3304
     *
3305
     * @param string $user_host_condition user host condition to
3306
     *                                    select relevant table privileges
3307
     * @param array  $queries             queries array
3308
     * @param string $username            username
3309
     * @param string $hostname            host name
3310
     *
3311
     * @return array
3312
     */
3313
    public function getTablePrivsQueriesForChangeOrCopyUser(
3314
        $user_host_condition,
3315
        array $queries,
3316
        $username,
3317
        $hostname
3318
    ) {
3319
        $res = $this->dbi->query(
3320
            'SELECT `Db`, `Table_name`, `Table_priv` FROM `mysql`.`tables_priv`'
3321
            . $user_host_condition,
3322
            DatabaseInterface::CONNECT_USER,
3323
            DatabaseInterface::QUERY_STORE
3324
        );
3325
        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

3325
        while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $res)) {
Loading history...
3326
            $res2 = $this->dbi->query(
3327
                'SELECT `Column_name`, `Column_priv`'
3328
                . ' FROM `mysql`.`columns_priv`'
3329
                . ' WHERE `User`'
3330
                . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . "'"
3331
                . ' AND `Host`'
3332
                . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . '\''
3333
                . ' AND `Db`'
3334
                . ' = \'' . $this->dbi->escapeString($row['Db']) . "'"
3335
                . ' AND `Table_name`'
3336
                . ' = \'' . $this->dbi->escapeString($row['Table_name']) . "'"
3337
                . ';',
3338
                DatabaseInterface::CONNECT_USER,
3339
                DatabaseInterface::QUERY_STORE
3340
            );
3341
3342
            $tmp_privs1 = $this->extractPrivInfo($row);
3343
            $tmp_privs2 = [
3344
                'Select' => [],
3345
                'Insert' => [],
3346
                'Update' => [],
3347
                'References' => [],
3348
            ];
3349
3350
            while ($row2 = $this->dbi->fetchAssoc($res2)) {
3351
                $tmp_array = explode(',', $row2['Column_priv']);
3352
                if (in_array('Select', $tmp_array)) {
3353
                    $tmp_privs2['Select'][] = $row2['Column_name'];
3354
                }
3355
                if (in_array('Insert', $tmp_array)) {
3356
                    $tmp_privs2['Insert'][] = $row2['Column_name'];
3357
                }
3358
                if (in_array('Update', $tmp_array)) {
3359
                    $tmp_privs2['Update'][] = $row2['Column_name'];
3360
                }
3361
                if (in_array('References', $tmp_array)) {
3362
                    $tmp_privs2['References'][] = $row2['Column_name'];
3363
                }
3364
            }
3365
            if (count($tmp_privs2['Select']) > 0 && ! in_array('SELECT', $tmp_privs1)) {
3366
                $tmp_privs1[] = 'SELECT (`' . implode('`, `', $tmp_privs2['Select']) . '`)';
3367
            }
3368
            if (count($tmp_privs2['Insert']) > 0 && ! in_array('INSERT', $tmp_privs1)) {
3369
                $tmp_privs1[] = 'INSERT (`' . implode('`, `', $tmp_privs2['Insert']) . '`)';
3370
            }
3371
            if (count($tmp_privs2['Update']) > 0 && ! in_array('UPDATE', $tmp_privs1)) {
3372
                $tmp_privs1[] = 'UPDATE (`' . implode('`, `', $tmp_privs2['Update']) . '`)';
3373
            }
3374
            if (count($tmp_privs2['References']) > 0
3375
                && ! in_array('REFERENCES', $tmp_privs1)
3376
            ) {
3377
                $tmp_privs1[]
3378
                    = 'REFERENCES (`' . implode('`, `', $tmp_privs2['References']) . '`)';
3379
            }
3380
3381
            $queries[] = 'GRANT ' . implode(', ', $tmp_privs1)
3382
                . ' 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

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

3383
                . /** @scrutinizer ignore-type */ Util::backquote($row['Table_name'])
Loading history...
3384
                . ' TO \'' . $this->dbi->escapeString($username)
3385
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\''
3386
                . (in_array('Grant', explode(',', $row['Table_priv']))
3387
                ? ' WITH GRANT OPTION;'
3388
                : ';');
3389
        }
3390
        return $queries;
3391
    }
3392
3393
    /**
3394
     * Get queries for database specific privileges for change or copy user
3395
     *
3396
     * @param array  $queries  queries array with string
3397
     * @param string $username username
3398
     * @param string $hostname host name
3399
     *
3400
     * @return array
3401
     */
3402
    public function getDbSpecificPrivsQueriesForChangeOrCopyUser(
3403
        array $queries,
3404
        $username,
3405
        $hostname
3406
    ) {
3407
        $user_host_condition = ' WHERE `User`'
3408
            . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . "'"
3409
            . ' AND `Host`'
3410
            . ' = \'' . $this->dbi->escapeString($_POST['old_hostname']) . '\';';
3411
3412
        $res = $this->dbi->query(
3413
            'SELECT * FROM `mysql`.`db`' . $user_host_condition
3414
        );
3415
3416
        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

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

3418
                . ' ON ' . /** @scrutinizer ignore-type */ Util::backquote($row['Db']) . '.*'
Loading history...
3419
                . ' TO \'' . $this->dbi->escapeString($username)
3420
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\''
3421
                . ($row['Grant_priv'] == 'Y' ? ' WITH GRANT OPTION;' : ';');
3422
        }
3423
        $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

3423
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $res);
Loading history...
3424
3425
        $queries = $this->getTablePrivsQueriesForChangeOrCopyUser(
3426
            $user_host_condition,
3427
            $queries,
3428
            $username,
3429
            $hostname
3430
        );
3431
3432
        return $queries;
3433
    }
3434
3435
    /**
3436
     * Prepares queries for adding users and
3437
     * also create database and return query and message
3438
     *
3439
     * @param boolean $_error               whether user create or not
3440
     * @param string  $real_sql_query       SQL query for add a user
3441
     * @param string  $sql_query            SQL query to be displayed
3442
     * @param string  $username             username
3443
     * @param string  $hostname             host name
3444
     * @param string  $dbname               database name
3445
     * @param string  $alter_real_sql_query SQL query for ALTER USER
3446
     * @param string  $alter_sql_query      SQL query for ALTER USER to be displayed
3447
     *
3448
     * @return array, $message
3449
     */
3450
    public function addUserAndCreateDatabase(
3451
        $_error,
3452
        $real_sql_query,
3453
        $sql_query,
3454
        $username,
3455
        $hostname,
3456
        $dbname,
3457
        $alter_real_sql_query,
3458
        $alter_sql_query
3459
    ) {
3460
        if ($_error || (! empty($real_sql_query)
3461
            && ! $this->dbi->tryQuery($real_sql_query))
3462
        ) {
3463
            $_POST['createdb-1'] = $_POST['createdb-2']
3464
                = $_POST['createdb-3'] = null;
3465
            $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

3465
            $message = Message::rawError(/** @scrutinizer ignore-type */ $this->dbi->getError());
Loading history...
3466
        } elseif ($alter_real_sql_query !== '' && ! $this->dbi->tryQuery($alter_real_sql_query)) {
3467
            $_POST['createdb-1'] = $_POST['createdb-2']
3468
                = $_POST['createdb-3'] = null;
3469
            $message = Message::rawError($this->dbi->getError());
3470
        } else {
3471
            $sql_query .= $alter_sql_query;
3472
            $message = Message::success(__('You have added a new user.'));
3473
        }
3474
3475
        if (isset($_POST['createdb-1'])) {
3476
            // Create database with same name and grant all privileges
3477
            $q = 'CREATE DATABASE IF NOT EXISTS '
3478
                . Util::backquote(
3479
                    $this->dbi->escapeString($username)
3480
                ) . ';';
3481
            $sql_query .= $q;
3482
            if (! $this->dbi->tryQuery($q)) {
3483
                $message = Message::rawError($this->dbi->getError());
3484
            }
3485
3486
            /**
3487
             * Reload the navigation
3488
             */
3489
            $GLOBALS['reload'] = true;
3490
            $GLOBALS['db'] = $username;
3491
3492
            $q = 'GRANT ALL PRIVILEGES ON '
3493
                . Util::backquote(
3494
                    Util::escapeMysqlWildcards(
3495
                        $this->dbi->escapeString($username)
3496
                    )
3497
                ) . '.* TO \''
3498
                . $this->dbi->escapeString($username)
3499
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
3500
            $sql_query .= $q;
3501
            if (! $this->dbi->tryQuery($q)) {
3502
                $message = Message::rawError($this->dbi->getError());
3503
            }
3504
        }
3505
3506
        if (isset($_POST['createdb-2'])) {
3507
            // Grant all privileges on wildcard name (username\_%)
3508
            $q = 'GRANT ALL PRIVILEGES ON '
3509
                . Util::backquote(
3510
                    Util::escapeMysqlWildcards(
3511
                        $this->dbi->escapeString($username)
3512
                    ) . '\_%'
3513
                ) . '.* TO \''
3514
                . $this->dbi->escapeString($username)
3515
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
3516
            $sql_query .= $q;
3517
            if (! $this->dbi->tryQuery($q)) {
3518
                $message = Message::rawError($this->dbi->getError());
3519
            }
3520
        }
3521
3522
        if (isset($_POST['createdb-3'])) {
3523
            // Grant all privileges on the specified database to the new user
3524
            $q = 'GRANT ALL PRIVILEGES ON '
3525
            . Util::backquote(
3526
                $this->dbi->escapeString($dbname)
3527
            ) . '.* TO \''
3528
            . $this->dbi->escapeString($username)
3529
            . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
3530
            $sql_query .= $q;
3531
            if (! $this->dbi->tryQuery($q)) {
3532
                $message = Message::rawError($this->dbi->getError());
3533
            }
3534
        }
3535
        return [
3536
            $sql_query,
3537
            $message,
3538
        ];
3539
    }
3540
3541
    /**
3542
     * Get the hashed string for password
3543
     *
3544
     * @param string $password password
3545
     *
3546
     * @return string
3547
     */
3548
    public function getHashedPassword($password)
3549
    {
3550
        $password = $this->dbi->escapeString($password);
3551
        $result = $this->dbi->fetchSingleRow(
3552
            "SELECT PASSWORD('" . $password . "') AS `password`;"
3553
        );
3554
3555
        return $result['password'];
3556
    }
3557
3558
    /**
3559
     * Check if MariaDB's 'simple_password_check'
3560
     * OR 'cracklib_password_check' is ACTIVE
3561
     *
3562
     * @return boolean if atleast one of the plugins is ACTIVE
3563
     */
3564
    public function checkIfMariaDBPwdCheckPluginActive()
3565
    {
3566
        $serverVersion = $this->dbi->getVersion();
3567
        if (! (Util::getServerType() == 'MariaDB' && $serverVersion >= 100002)) {
3568
            return false;
3569
        }
3570
3571
        $result = $this->dbi->tryQuery(
3572
            'SHOW PLUGINS SONAME LIKE \'%_password_check%\''
3573
        );
3574
3575
        /* Plugins are not working, for example directory does not exists */
3576
        if ($result === false) {
3577
            return false;
3578
        }
3579
3580
        while ($row = $this->dbi->fetchAssoc($result)) {
3581
            if ($row['Status'] === 'ACTIVE') {
3582
                return true;
3583
            }
3584
        }
3585
3586
        return false;
3587
    }
3588
3589
3590
    /**
3591
     * Get SQL queries for Display and Add user
3592
     *
3593
     * @param string $username username
3594
     * @param string $hostname host name
3595
     * @param string $password password
3596
     *
3597
     * @return array ($create_user_real, $create_user_show, $real_sql_query, $sql_query
3598
     *                $password_set_real, $password_set_show, $alter_real_sql_query, $alter_sql_query)
3599
     */
3600
    public function getSqlQueriesForDisplayAndAddUser($username, $hostname, $password)
3601
    {
3602
        $slashedUsername = $this->dbi->escapeString($username);
3603
        $slashedHostname = $this->dbi->escapeString($hostname);
3604
        $slashedPassword = $this->dbi->escapeString($password);
3605
        $serverType = Util::getServerType();
3606
        $serverVersion = $this->dbi->getVersion();
3607
3608
        $create_user_stmt = sprintf(
3609
            'CREATE USER \'%s\'@\'%s\'',
3610
            $slashedUsername,
3611
            $slashedHostname
3612
        );
3613
        $isMariaDBPwdPluginActive = $this->checkIfMariaDBPwdCheckPluginActive();
3614
3615
        // See https://github.com/phpmyadmin/phpmyadmin/pull/11560#issuecomment-147158219
3616
        // for details regarding details of syntax usage for various versions
3617
3618
        // 'IDENTIFIED WITH auth_plugin'
3619
        // is supported by MySQL 5.5.7+
3620
        if (($serverType == 'MySQL' || $serverType == 'Percona Server')
3621
            && $serverVersion >= 50507
3622
            && isset($_POST['authentication_plugin'])
3623
        ) {
3624
            $create_user_stmt .= ' IDENTIFIED WITH '
3625
                . $_POST['authentication_plugin'];
3626
        }
3627
3628
        // 'IDENTIFIED VIA auth_plugin'
3629
        // is supported by MariaDB 5.2+
3630
        if ($serverType == 'MariaDB'
3631
            && $serverVersion >= 50200
3632
            && isset($_POST['authentication_plugin'])
3633
            && ! $isMariaDBPwdPluginActive
3634
        ) {
3635
            $create_user_stmt .= ' IDENTIFIED VIA '
3636
                . $_POST['authentication_plugin'];
3637
        }
3638
3639
        $create_user_real = $create_user_stmt;
3640
        $create_user_show = $create_user_stmt;
3641
3642
        $password_set_stmt = 'SET PASSWORD FOR \'%s\'@\'%s\' = \'%s\'';
3643
        $password_set_show = sprintf(
3644
            $password_set_stmt,
3645
            $slashedUsername,
3646
            $slashedHostname,
3647
            '***'
3648
        );
3649
3650
        $sql_query_stmt = sprintf(
3651
            'GRANT %s ON *.* TO \'%s\'@\'%s\'',
3652
            implode(', ', $this->extractPrivInfo()),
3653
            $slashedUsername,
3654
            $slashedHostname
3655
        );
3656
        $real_sql_query = $sql_query = $sql_query_stmt;
3657
3658
        // Set the proper hashing method
3659
        if (isset($_POST['authentication_plugin'])) {
3660
            $this->setProperPasswordHashing(
3661
                $_POST['authentication_plugin']
3662
            );
3663
        }
3664
3665
        // Use 'CREATE USER ... WITH ... AS ..' syntax for
3666
        // newer MySQL versions
3667
        // and 'CREATE USER ... VIA .. USING ..' syntax for
3668
        // newer MariaDB versions
3669
        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...
3670
            && $serverVersion >= 50706)
3671
            || ($serverType == 'MariaDB'
3672
            && $serverVersion >= 50200)
3673
        ) {
3674
            $password_set_real = null;
3675
3676
            // Required for binding '%' with '%s'
3677
            $create_user_stmt = str_replace(
3678
                '%',
3679
                '%%',
3680
                $create_user_stmt
3681
            );
3682
3683
            // MariaDB uses 'USING' whereas MySQL uses 'AS'
3684
            // but MariaDB with validation plugin needs cleartext password
3685
            if ($serverType == 'MariaDB'
3686
                && ! $isMariaDBPwdPluginActive
3687
            ) {
3688
                $create_user_stmt .= ' USING \'%s\'';
3689
            } elseif ($serverType == 'MariaDB') {
3690
                $create_user_stmt .= ' IDENTIFIED BY \'%s\'';
3691
            } elseif (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) {
3692
                $create_user_stmt .= ' BY \'%s\'';
3693
            } else {
3694
                $create_user_stmt .= ' AS \'%s\'';
3695
            }
3696
3697
            if ($_POST['pred_password'] == 'keep') {
3698
                $create_user_real = sprintf(
3699
                    $create_user_stmt,
3700
                    $slashedPassword
3701
                );
3702
                $create_user_show = sprintf(
3703
                    $create_user_stmt,
3704
                    '***'
3705
                );
3706
            } elseif ($_POST['pred_password'] == 'none') {
3707
                $create_user_real = sprintf(
3708
                    $create_user_stmt,
3709
                    null
3710
                );
3711
                $create_user_show = sprintf(
3712
                    $create_user_stmt,
3713
                    '***'
3714
                );
3715
            } else {
3716
                if (! (($serverType == 'MariaDB' && $isMariaDBPwdPluginActive)
3717
                    || ($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011)) {
3718
                    $hashedPassword = $this->getHashedPassword($_POST['pma_pw']);
3719
                } else {
3720
                    // MariaDB with validation plugin needs cleartext password
3721
                    $hashedPassword = $_POST['pma_pw'];
3722
                }
3723
                $create_user_real = sprintf(
3724
                    $create_user_stmt,
3725
                    $hashedPassword
3726
                );
3727
                $create_user_show = sprintf(
3728
                    $create_user_stmt,
3729
                    '***'
3730
                );
3731
            }
3732
        } else {
3733
            // Use 'SET PASSWORD' syntax for pre-5.7.6 MySQL versions
3734
            // and pre-5.2.0 MariaDB versions
3735
            if ($_POST['pred_password'] == 'keep') {
3736
                $password_set_real = sprintf(
3737
                    $password_set_stmt,
3738
                    $slashedUsername,
3739
                    $slashedHostname,
3740
                    $slashedPassword
3741
                );
3742
            } elseif ($_POST['pred_password'] == 'none') {
3743
                $password_set_real = sprintf(
3744
                    $password_set_stmt,
3745
                    $slashedUsername,
3746
                    $slashedHostname,
3747
                    null
3748
                );
3749
            } else {
3750
                $hashedPassword = $this->getHashedPassword($_POST['pma_pw']);
3751
                $password_set_real = sprintf(
3752
                    $password_set_stmt,
3753
                    $slashedUsername,
3754
                    $slashedHostname,
3755
                    $hashedPassword
3756
                );
3757
            }
3758
        }
3759
3760
        $alter_real_sql_query = '';
3761
        $alter_sql_query = '';
3762
        if (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) {
3763
            $sql_query_stmt = '';
3764
            if ((isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y')
3765
                || (isset($GLOBALS['Grant_priv']) && $GLOBALS['Grant_priv'] == 'Y')
3766
            ) {
3767
                $sql_query_stmt = ' WITH GRANT OPTION';
3768
            }
3769
            $real_sql_query .= $sql_query_stmt;
3770
            $sql_query .= $sql_query_stmt;
3771
3772
            $alter_sql_query_stmt = sprintf(
3773
                'ALTER USER \'%s\'@\'%s\'',
3774
                $slashedUsername,
3775
                $slashedHostname
3776
            );
3777
            $alter_real_sql_query = $alter_sql_query_stmt;
3778
            $alter_sql_query = $alter_sql_query_stmt;
3779
        }
3780
3781
        // add REQUIRE clause
3782
        $require_clause = $this->getRequireClause();
3783
        $with_clause = $this->getWithClauseForAddUserAndUpdatePrivs();
3784
3785
        if (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) {
3786
            $alter_real_sql_query .= $require_clause;
3787
            $alter_sql_query .= $require_clause;
3788
            $alter_real_sql_query .= $with_clause;
3789
            $alter_sql_query .= $with_clause;
3790
        } else {
3791
            $real_sql_query .= $require_clause;
3792
            $sql_query .= $require_clause;
3793
            $real_sql_query .= $with_clause;
3794
            $sql_query .= $with_clause;
3795
        }
3796
3797
        if ($alter_real_sql_query !== '') {
3798
            $alter_real_sql_query .= ';';
3799
            $alter_sql_query .= ';';
3800
        }
3801
        $create_user_real .= ';';
3802
        $create_user_show .= ';';
3803
        $real_sql_query .= ';';
3804
        $sql_query .= ';';
3805
        // No Global GRANT_OPTION privilege
3806
        if (! $GLOBALS['is_grantuser']) {
3807
            $real_sql_query = '';
3808
            $sql_query = '';
3809
        }
3810
3811
        // Use 'SET PASSWORD' for pre-5.7.6 MySQL versions
3812
        // and pre-5.2.0 MariaDB
3813
        if (($serverType == 'MySQL'
3814
            && $serverVersion >= 50706)
3815
            || ($serverType == 'MariaDB'
3816
            && $serverVersion >= 50200)
3817
        ) {
3818
            $password_set_real = null;
3819
            $password_set_show = null;
3820
        } else {
3821
            if ($password_set_real !== null) {
3822
                $password_set_real .= ";";
3823
            }
3824
            $password_set_show .= ";";
3825
        }
3826
3827
        return [
3828
            $create_user_real,
3829
            $create_user_show,
3830
            $real_sql_query,
3831
            $sql_query,
3832
            $password_set_real,
3833
            $password_set_show,
3834
            $alter_real_sql_query,
3835
            $alter_sql_query,
3836
        ];
3837
    }
3838
3839
    /**
3840
     * Returns the type ('PROCEDURE' or 'FUNCTION') of the routine
3841
     *
3842
     * @param string $dbname      database
3843
     * @param string $routineName routine
3844
     *
3845
     * @return string type
3846
     */
3847
    public function getRoutineType($dbname, $routineName)
3848
    {
3849
        $routineData = $this->dbi->getRoutines($dbname);
3850
3851
        foreach ($routineData as $routine) {
3852
            if ($routine['name'] === $routineName) {
3853
                return $routine['type'];
3854
            }
3855
        }
3856
        return '';
3857
    }
3858
3859
    /**
3860
     * @param string $username User name
3861
     * @param string $hostname Host name
3862
     * @param string $database Database name
3863
     * @param string $routine  Routine name
3864
     *
3865
     * @return array
3866
     */
3867
    private function getRoutinePrivileges(
3868
        string $username,
3869
        string $hostname,
3870
        string $database,
3871
        string $routine
3872
    ): array {
3873
        $sql = "SELECT `Proc_priv`"
3874
            . " FROM `mysql`.`procs_priv`"
3875
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
3876
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "'"
3877
            . " AND `Db` = '"
3878
            . $this->dbi->escapeString(Util::unescapeMysqlWildcards($database)) . "'"
3879
            . " AND `Routine_name` LIKE '" . $this->dbi->escapeString($routine) . "';";
3880
        $privileges = $this->dbi->fetchValue($sql);
3881
        if ($privileges === false) {
3882
            $privileges = '';
3883
        }
3884
        return $this->parseProcPriv($privileges);
3885
    }
3886
}
3887