Passed
Push — master ( b6e168...dd5d6d )
by Maurício
10:27
created

Privileges::getHtmlForSpecificDbPrivileges()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 41
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
1405
        }
1406
1407
        return $this->template->render('server/privileges/database', [
1408
            'is_superuser' => $this->dbi->isSuperuser(),
1409
            'db' => $db,
1410
            'database_url' => $scriptName,
1411
            'pma_theme_image' => $pmaThemeImage,
1412
            'text_dir' => $text_dir,
1413
            'table_body' => $tableBody,
1414
            'is_createuser' => $is_createuser,
1415
        ]);
1416
    }
1417
1418
    /**
1419
     * Get the HTML for user form and check the privileges for a particular table.
1420
     *
1421
     * @param string $db    database name
1422
     * @param string $table table name
1423
     *
1424
     * @return string
1425
     */
1426
    public function getHtmlForSpecificTablePrivileges(string $db, string $table): string
1427
    {
1428
        global $cfg, $pmaThemeImage, $text_dir, $is_createuser;
1429
1430
        $scriptName = Util::getScriptNameForOption(
1431
            $cfg['DefaultTabTable'],
1432
            'table'
1433
        );
1434
1435
        $tableBody = '';
1436
        if ($this->dbi->isSuperuser()) {
1437
            $privileges = $this->getDatabasePrivileges($db);
1438
1439
            $tablePrivileges = $this->getTablePrivileges($db, $table);
1440
            foreach ($tablePrivileges as $user => $hosts) {
1441
                foreach ($hosts as $host => $privs) {
1442
                    $privileges[$user] = $privileges[$user] ?? [];
1443
                    $privileges[$user][$host] = array_merge($privileges[$user][$host] ?? [], $privs);
1444
                }
1445
            }
1446
1447
            $routinePrivileges = $this->getRoutinesPrivileges($db);
1448
            foreach ($routinePrivileges as $user => $hosts) {
1449
                foreach ($hosts as $host => $privs) {
1450
                    $privileges[$user] = $privileges[$user] ?? [];
1451
                    $privileges[$user][$host] = array_merge($privileges[$user][$host] ?? [], $privs);
1452
                }
1453
            }
1454
1455
            $tableBody = $this->getHtmlTableBodyForSpecificDbOrTablePrivs($privileges, $db);
1456
        }
1457
1458
        return $this->template->render('server/privileges/table', [
1459
            'db' => $db,
1460
            'table' => $table,
1461
            'is_superuser' => $this->dbi->isSuperuser(),
1462
            'table_url' => $scriptName,
1463
            'pma_theme_image' => $pmaThemeImage,
1464
            'text_dir' => $text_dir,
1465
            'table_body' => $tableBody,
1466
            'is_createuser' => $is_createuser,
1467
        ]);
1468
    }
1469
1470
    /**
1471
     * @param string $db database name
1472
     *
1473
     * @return array
1474
     */
1475
    private function getDatabasePrivileges(string $db): array
1476
    {
1477
        $listOfPrivileges = '`Select_priv`,
1478
            `Insert_priv`,
1479
            `Update_priv`,
1480
            `Delete_priv`,
1481
            `Create_priv`,
1482
            `Drop_priv`,
1483
            `Grant_priv`,
1484
            `Index_priv`,
1485
            `Alter_priv`,
1486
            `References_priv`,
1487
            `Create_tmp_table_priv`,
1488
            `Lock_tables_priv`,
1489
            `Create_view_priv`,
1490
            `Show_view_priv`,
1491
            `Create_routine_priv`,
1492
            `Alter_routine_priv`,
1493
            `Execute_priv`,
1494
            `Event_priv`,
1495
            `Trigger_priv`,';
1496
1497
        $listOfComparedPrivileges = '`Select_priv` = \'N\' AND
1498
            `Insert_priv` = \'N\' AND
1499
            `Update_priv` = \'N\' AND
1500
            `Delete_priv` = \'N\' AND
1501
            `Create_priv` = \'N\' AND
1502
            `Drop_priv` = \'N\' AND
1503
            `Grant_priv` = \'N\' AND
1504
            `References_priv` = \'N\' AND
1505
            `Create_tmp_table_priv` = \'N\' AND
1506
            `Lock_tables_priv` = \'N\' AND
1507
            `Create_view_priv` = \'N\' AND
1508
            `Show_view_priv` = \'N\' AND
1509
            `Create_routine_priv` = \'N\' AND
1510
            `Alter_routine_priv` = \'N\' AND
1511
            `Execute_priv` = \'N\' AND
1512
            `Event_priv` = \'N\' AND
1513
            `Trigger_priv` = \'N\'';
1514
1515
        $query = '
1516
            (
1517
                SELECT `User`, `Host`, ' . $listOfPrivileges . ' \'*\' AS `Db`, \'g\' AS `Type`
1518
                FROM `mysql`.`user`
1519
                WHERE NOT (' . $listOfComparedPrivileges . ')
1520
            )
1521
            UNION
1522
            (
1523
                SELECT `User`, `Host`, ' . $listOfPrivileges . ' `Db`, \'d\' AS `Type`
1524
                FROM `mysql`.`db`
1525
                WHERE \'' . $this->dbi->escapeString($db) . '\' LIKE `Db` AND NOT (' . $listOfComparedPrivileges . ')
1526
            )
1527
            ORDER BY `User` ASC, `Host` ASC, `Db` ASC;
1528
        ';
1529
        $result = $this->dbi->query($query);
1530
        if ($result === false) {
1531
            return [];
1532
        }
1533
1534
        $privileges = [];
1535
        while ($row = $this->dbi->fetchAssoc($result)) {
1536
            $privileges[$row['User']] = $privileges[$row['User']] ?? [];
1537
            $privileges[$row['User']][$row['Host']] = $privileges[$row['User']][$row['Host']] ?? [];
1538
            $privileges[$row['User']][$row['Host']][] = $row;
1539
        }
1540
        return $privileges;
1541
    }
1542
1543
    /**
1544
     * @param string $db    database name
1545
     * @param string $table table name
1546
     *
1547
     * @return array
1548
     */
1549
    private function getTablePrivileges(string $db, string $table): array
1550
    {
1551
        $query = '
1552
            SELECT `User`, `Host`, `Db`, \'t\' AS `Type`, `Table_name`, `Table_priv`
1553
            FROM `mysql`.`tables_priv`
1554
            WHERE
1555
                \'' . $this->dbi->escapeString($db) . '\' LIKE `Db` AND
1556
                \'' . $this->dbi->escapeString($table) . '\' LIKE `Table_name` AND
1557
                NOT (`Table_priv` = \'\' AND Column_priv = \'\')
1558
            ORDER BY `User` ASC, `Host` ASC, `Db` ASC, `Table_priv` ASC;
1559
        ';
1560
        $result = $this->dbi->query($query);
1561
        if ($result === false) {
1562
            return [];
1563
        }
1564
1565
        $privileges = [];
1566
        while ($row = $this->dbi->fetchAssoc($result)) {
1567
            $privileges[$row['User']] = $privileges[$row['User']] ?? [];
1568
            $privileges[$row['User']][$row['Host']] = $privileges[$row['User']][$row['Host']] ?? [];
1569
            $privileges[$row['User']][$row['Host']][] = $row;
1570
        }
1571
        return $privileges;
1572
    }
1573
1574
    /**
1575
     * @param string $db database name
1576
     *
1577
     * @return array
1578
     */
1579
    private function getRoutinesPrivileges(string $db): array
1580
    {
1581
        $query = '
1582
            SELECT *, \'r\' AS `Type`, \'Y\' AS `Grant_priv`
1583
            FROM `mysql`.`procs_priv`
1584
            WHERE Db = \'' . $this->dbi->escapeString($db) . '\';
1585
        ';
1586
        $result = $this->dbi->query($query);
1587
        if ($result === false) {
1588
            return [];
1589
        }
1590
1591
        $privileges = [];
1592
        while ($row = $this->dbi->fetchAssoc($result)) {
1593
            $privileges[$row['User']] = $privileges[$row['User']] ?? [];
1594
            $privileges[$row['User']][$row['Host']] = $privileges[$row['User']][$row['Host']] ?? [];
1595
            $privileges[$row['User']][$row['Host']][] = $row;
1596
        }
1597
        return $privileges;
1598
    }
1599
1600
    /**
1601
     * Get HTML error for View Users form
1602
     * For non superusers such as grant/create users
1603
     *
1604
     * @return string
1605
     */
1606
    public function getHtmlForViewUsersError()
1607
    {
1608
        return Message::error(
1609
            __('Not enough privilege to view users.')
1610
        )->getDisplay();
1611
    }
1612
1613
    /**
1614
     * Get HTML snippet for table body of specific database or table privileges
1615
     *
1616
     * @param array  $privMap privilege map
1617
     * @param string $db      database
1618
     *
1619
     * @return string
1620
     */
1621
    public function getHtmlTableBodyForSpecificDbOrTablePrivs($privMap, $db)
1622
    {
1623
        $html_output = '<tbody>';
1624
        $index_checkbox = 0;
1625
        if (empty($privMap)) {
1626
            $html_output .= '<tr>'
1627
                . '<td colspan="6">'
1628
                . __('No user found.')
1629
                . '</td>'
1630
                . '</tr>'
1631
                . '</tbody>';
1632
            return $html_output;
1633
        }
1634
1635
        foreach ($privMap as $current_user => $val) {
1636
            foreach ($val as $current_host => $current_privileges) {
1637
                $nbPrivileges = count($current_privileges);
1638
                $html_output .= '<tr>';
1639
1640
                $value = htmlspecialchars($current_user . '&amp;#27;' . $current_host);
1641
                $html_output .= '<td';
1642
                if ($nbPrivileges > 1) {
1643
                    $html_output .= ' rowspan="' . $nbPrivileges . '"';
1644
                }
1645
                $html_output .= '>';
1646
                $html_output .= '<input type="checkbox" class="checkall" '
1647
                    . 'name="selected_usr[]" '
1648
                    . 'id="checkbox_sel_users_' . ($index_checkbox++) . '" '
1649
                    . 'value="' . $value . '"></td>' . "\n";
1650
1651
                // user
1652
                $html_output .= '<td';
1653
                if ($nbPrivileges > 1) {
1654
                    $html_output .= ' rowspan="' . $nbPrivileges . '"';
1655
                }
1656
                $html_output .= '>';
1657
                if (empty($current_user)) {
1658
                    $html_output .= '<span style="color: #FF0000">'
1659
                        . __('Any') . '</span>';
1660
                } else {
1661
                    $html_output .= htmlspecialchars($current_user);
1662
                }
1663
                $html_output .= '</td>';
1664
1665
                // host
1666
                $html_output .= '<td';
1667
                if ($nbPrivileges > 1) {
1668
                    $html_output .= ' rowspan="' . $nbPrivileges . '"';
1669
                }
1670
                $html_output .= '>';
1671
                $html_output .= htmlspecialchars($current_host);
1672
                $html_output .= '</td>';
1673
1674
                $html_output .= $this->getHtmlListOfPrivs(
1675
                    $db,
1676
                    $current_privileges,
1677
                    $current_user,
1678
                    $current_host
1679
                );
1680
            }
1681
        }
1682
        $html_output .= '</tbody>';
1683
1684
        return $html_output;
1685
    }
1686
1687
    /**
1688
     * Get HTML to display privileges
1689
     *
1690
     * @param string $db                 Database name
1691
     * @param array  $current_privileges List of privileges
1692
     * @param string $current_user       Current user
1693
     * @param string $current_host       Current host
1694
     *
1695
     * @return string HTML to display privileges
1696
     */
1697
    public function getHtmlListOfPrivs(
1698
        $db,
1699
        array $current_privileges,
1700
        $current_user,
1701
        $current_host
1702
    ) {
1703
        $nbPrivileges = count($current_privileges);
1704
        $html_output = null;
1705
        for ($i = 0; $i < $nbPrivileges; $i++) {
1706
            $current = $current_privileges[$i];
1707
1708
            // type
1709
            $html_output .= '<td>';
1710
            if ($current['Type'] == 'g') {
1711
                $html_output .= __('global');
1712
            } elseif ($current['Type'] == 'd') {
1713
                if ($current['Db'] == Util::escapeMysqlWildcards($db)) {
1714
                    $html_output .= __('database-specific');
1715
                } else {
1716
                    $html_output .= __('wildcard') . ': '
1717
                        . '<code>'
1718
                        . htmlspecialchars($current['Db'])
1719
                        . '</code>';
1720
                }
1721
            } elseif ($current['Type'] == 't') {
1722
                $html_output .= __('table-specific');
1723
            } elseif ($current['Type'] == 'r') {
1724
                $html_output .= __('routine');
1725
            }
1726
            $html_output .= '</td>';
1727
1728
            // privileges
1729
            $html_output .= '<td>';
1730
            if (isset($current['Routine_name'])) {
1731
                $html_output .= '<code>';
1732
                $html_output .= htmlspecialchars($current['Routine_name']);
1733
                $html_output .= ' (';
1734
                $html_output .= strtoupper(htmlspecialchars($current['Proc_priv']));
1735
                $html_output .= ')</code>';
1736
            } elseif (isset($current['Table_name'])) {
1737
                $privList = explode(',', $current['Table_priv']);
1738
                $privs = [];
1739
                $grantsArr = $this->getTableGrantsArray();
1740
                foreach ($grantsArr as $grant) {
1741
                    $privs[$grant[0]] = 'N';
1742
                    foreach ($privList as $priv) {
1743
                        if ($grant[0] == $priv) {
1744
                            $privs[$grant[0]] = 'Y';
1745
                        }
1746
                    }
1747
                }
1748
                $html_output .= '<code>'
1749
                    . implode(
1750
                        ',',
1751
                        $this->extractPrivInfo($privs, true, true)
1752
                    )
1753
                    . '</code>';
1754
            } else {
1755
                $html_output .= '<code>'
1756
                    . implode(
1757
                        ',',
1758
                        $this->extractPrivInfo($current, true, false)
1759
                    )
1760
                    . '</code>';
1761
            }
1762
            $html_output .= '</td>';
1763
1764
            // grant
1765
            $html_output .= '<td>';
1766
            $containsGrant = false;
1767
            if (isset($current['Table_name'])) {
1768
                $privList = explode(',', $current['Table_priv']);
1769
                foreach ($privList as $priv) {
1770
                    if ($priv == 'Grant') {
1771
                        $containsGrant = true;
1772
                    }
1773
                }
1774
            } else {
1775
                $containsGrant = $current['Grant_priv'] == 'Y';
1776
            }
1777
            $html_output .= ($containsGrant ? __('Yes') : __('No'));
1778
            $html_output .= '</td>';
1779
1780
            // action
1781
            $html_output .= '<td>';
1782
            $specific_db = isset($current['Db']) && $current['Db'] != '*'
1783
                ? $current['Db'] : '';
1784
            $specific_table = isset($current['Table_name'])
1785
                && $current['Table_name'] != '*'
1786
                ? $current['Table_name'] : '';
1787
            $specificRoutine = $current['Routine_name'] ?? '';
1788
            if ($GLOBALS['is_grantuser']) {
1789
                $html_output .= $this->getUserLink(
1790
                    'edit',
1791
                    $current_user,
1792
                    $current_host,
1793
                    $specific_db,
1794
                    $specific_table,
1795
                    $specificRoutine
1796
                );
1797
            }
1798
            $html_output .= '</td>';
1799
            $html_output .= '<td class="center">'
1800
                . $this->getUserLink(
1801
                    'export',
1802
                    $current_user,
1803
                    $current_host,
1804
                    $specific_db,
1805
                    $specific_table,
1806
                    $specificRoutine
1807
                )
1808
                . '</td>';
1809
1810
            $html_output .= '</tr>';
1811
            if (($i + 1) < $nbPrivileges) {
1812
                $html_output .= '<tr class="noclick">';
1813
            }
1814
        }
1815
        return $html_output;
1816
    }
1817
1818
    /**
1819
     * Returns edit, revoke or export link for a user.
1820
     *
1821
     * @param string $linktype    The link type (edit | revoke | export)
1822
     * @param string $username    User name
1823
     * @param string $hostname    Host name
1824
     * @param string $dbname      Database name
1825
     * @param string $tablename   Table name
1826
     * @param string $routinename Routine name
1827
     * @param string $initial     Initial value
1828
     *
1829
     * @return string HTML code with link
1830
     */
1831
    public function getUserLink(
1832
        $linktype,
1833
        $username,
1834
        $hostname,
1835
        $dbname = '',
1836
        $tablename = '',
1837
        $routinename = '',
1838
        $initial = ''
1839
    ) {
1840
        $html = '<a';
1841
        switch ($linktype) {
1842
            case 'edit':
1843
                $html .= ' class="edit_user_anchor"';
1844
                break;
1845
            case 'export':
1846
                $html .= ' class="export_user_anchor ajax"';
1847
                break;
1848
        }
1849
        $params = [
1850
            'username' => $username,
1851
            'hostname' => $hostname,
1852
        ];
1853
        switch ($linktype) {
1854
            case 'edit':
1855
                $params['dbname'] = $dbname;
1856
                $params['tablename'] = $tablename;
1857
                $params['routinename'] = $routinename;
1858
                break;
1859
            case 'revoke':
1860
                $params['dbname'] = $dbname;
1861
                $params['tablename'] = $tablename;
1862
                $params['routinename'] = $routinename;
1863
                $params['revokeall'] = 1;
1864
                break;
1865
            case 'export':
1866
                $params['initial'] = $initial;
1867
                $params['export'] = 1;
1868
                break;
1869
        }
1870
1871
        $html .= ' href="' . Url::getFromRoute('/server/privileges');
1872
        if ($linktype == 'revoke') {
1873
            $html .= '" data-post="' . Url::getCommon($params, '');
1874
        } else {
1875
            $html .= Url::getCommon($params, '&');
1876
        }
1877
        $html .= '">';
1878
1879
        switch ($linktype) {
1880
            case 'edit':
1881
                $html .= Util::getIcon('b_usredit', __('Edit privileges'));
1882
                break;
1883
            case 'revoke':
1884
                $html .= Util::getIcon('b_usrdrop', __('Revoke'));
1885
                break;
1886
            case 'export':
1887
                $html .= Util::getIcon('b_tblexport', __('Export'));
1888
                break;
1889
        }
1890
        $html .= '</a>';
1891
1892
        return $html;
1893
    }
1894
1895
    /**
1896
     * Returns user group edit link
1897
     *
1898
     * @param string $username User name
1899
     *
1900
     * @return string HTML code with link
1901
     */
1902
    public function getUserGroupEditLink($username)
1903
    {
1904
         return '<a class="edit_user_group_anchor ajax"'
1905
            . ' href="' . Url::getFromRoute('/server/privileges', ['username' => $username])
1906
            . '">'
1907
            . Util::getIcon('b_usrlist', __('Edit user group'))
1908
            . '</a>';
1909
    }
1910
1911
    /**
1912
     * Returns number of defined user groups
1913
     *
1914
     * @return integer
1915
     */
1916
    public function getUserGroupCount()
1917
    {
1918
        $cfgRelation = $this->relation->getRelationsParam();
1919
        $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

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

1920
            . '.' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['usergroups']);
Loading history...
1921
        $sql_query = 'SELECT COUNT(*) FROM ' . $user_group_table;
1922
        $user_group_count = $this->dbi->fetchValue(
1923
            $sql_query,
1924
            0,
1925
            0,
1926
            DatabaseInterface::CONNECT_CONTROL
1927
        );
1928
1929
        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...
1930
    }
1931
1932
    /**
1933
     * Returns name of user group that user is part of
1934
     *
1935
     * @param string $username User name
1936
     *
1937
     * @return mixed usergroup if found or null if not found
1938
     */
1939
    public function getUserGroupForUser($username)
1940
    {
1941
        $cfgRelation = $this->relation->getRelationsParam();
1942
1943
        if (empty($cfgRelation['db'])
1944
            || empty($cfgRelation['users'])
1945
        ) {
1946
            return null;
1947
        }
1948
1949
        $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

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

1950
            . '.' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['users']);
Loading history...
1951
        $sql_query = 'SELECT `usergroup` FROM ' . $user_table
1952
            . ' WHERE `username` = \'' . $username . '\''
1953
            . ' LIMIT 1';
1954
1955
        $usergroup = $this->dbi->fetchValue(
1956
            $sql_query,
1957
            0,
1958
            0,
1959
            DatabaseInterface::CONNECT_CONTROL
1960
        );
1961
1962
        if ($usergroup === false) {
1963
            return null;
1964
        }
1965
1966
        return $usergroup;
1967
    }
1968
1969
    /**
1970
     * This function return the extra data array for the ajax behavior
1971
     *
1972
     * @param string $password  password
1973
     * @param string $sql_query sql query
1974
     * @param string $hostname  hostname
1975
     * @param string $username  username
1976
     *
1977
     * @return array
1978
     */
1979
    public function getExtraDataForAjaxBehavior(
1980
        $password,
1981
        $sql_query,
1982
        $hostname,
1983
        $username
1984
    ) {
1985
        if (isset($GLOBALS['dbname'])) {
1986
            //if (preg_match('/\\\\(?:_|%)/i', $dbname)) {
1987
            if (preg_match('/(?<!\\\\)(?:_|%)/', $GLOBALS['dbname'])) {
1988
                $dbname_is_wildcard = true;
1989
            } else {
1990
                $dbname_is_wildcard = false;
1991
            }
1992
        }
1993
1994
        $user_group_count = 0;
1995
        if ($GLOBALS['cfgRelation']['menuswork']) {
1996
            $user_group_count = $this->getUserGroupCount();
1997
        }
1998
1999
        $extra_data = [];
2000
        if (strlen($sql_query) > 0) {
2001
            $extra_data['sql_query'] = Util::getMessage(null, $sql_query);
2002
        }
2003
2004
        if (isset($_POST['change_copy'])) {
2005
            /**
2006
             * generate html on the fly for the new user that was just created.
2007
             */
2008
            $new_user_string = '<tr>' . "\n"
2009
                . '<td> <input type="checkbox" name="selected_usr[]" '
2010
                . 'id="checkbox_sel_users_"'
2011
                . 'value="'
2012
                . htmlspecialchars($username)
2013
                . '&amp;#27;' . htmlspecialchars($hostname) . '">'
2014
                . '</td>' . "\n"
2015
                . '<td><label for="checkbox_sel_users_">'
2016
                . (empty($_POST['username'])
2017
                        ? '<span style="color: #FF0000">' . __('Any') . '</span>'
2018
                        : htmlspecialchars($username) ) . '</label></td>' . "\n"
2019
                . '<td>' . htmlspecialchars($hostname) . '</td>' . "\n";
2020
2021
            $new_user_string .= '<td>';
2022
2023
            if (! empty($password) || isset($_POST['pma_pw'])) {
2024
                $new_user_string .= __('Yes');
2025
            } else {
2026
                $new_user_string .= '<span style="color: #FF0000">'
2027
                    . __('No')
2028
                . '</span>';
2029
            }
2030
2031
            $new_user_string .= '</td>' . "\n";
2032
            $new_user_string .= '<td>'
2033
                . '<code>' . implode(', ', $this->extractPrivInfo(null, true)) . '</code>'
2034
                . '</td>'; //Fill in privileges here
2035
2036
            // if $cfg['Servers'][$i]['users'] and $cfg['Servers'][$i]['usergroups'] are
2037
            // enabled
2038
            $cfgRelation = $this->relation->getRelationsParam();
2039
            if (! empty($cfgRelation['users']) && ! empty($cfgRelation['usergroups'])) {
2040
                $new_user_string .= '<td class="usrGroup"></td>';
2041
            }
2042
2043
            $new_user_string .= '<td>';
2044
            if (isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y') {
2045
                $new_user_string .= __('Yes');
2046
            } else {
2047
                $new_user_string .= __('No');
2048
            }
2049
            $new_user_string .= '</td>';
2050
2051
            if ($GLOBALS['is_grantuser']) {
2052
                $new_user_string .= '<td>'
2053
                    . $this->getUserLink('edit', $username, $hostname)
2054
                    . '</td>' . "\n";
2055
            }
2056
2057
            if ($cfgRelation['menuswork'] && $user_group_count > 0) {
2058
                $new_user_string .= '<td>'
2059
                    . $this->getUserGroupEditLink($username)
2060
                    . '</td>' . "\n";
2061
            }
2062
2063
            $new_user_string .= '<td>'
2064
                . $this->getUserLink(
2065
                    'export',
2066
                    $username,
2067
                    $hostname,
2068
                    '',
2069
                    '',
2070
                    '',
2071
                    isset($_GET['initial']) ? $_GET['initial'] : ''
2072
                )
2073
                . '</td>' . "\n";
2074
2075
            $new_user_string .= '</tr>';
2076
2077
            $extra_data['new_user_string'] = $new_user_string;
2078
2079
            /**
2080
             * Generate the string for this alphabet's initial, to update the user
2081
             * pagination
2082
             */
2083
            $new_user_initial = mb_strtoupper(
2084
                mb_substr($username, 0, 1)
2085
            );
2086
            $newUserInitialString = '<a href="' . Url::getFromRoute('/server/privileges', ['initial' => $new_user_initial]) . '">'
2087
                . $new_user_initial . '</a>';
2088
            $extra_data['new_user_initial'] = $new_user_initial;
2089
            $extra_data['new_user_initial_string'] = $newUserInitialString;
2090
        }
2091
2092
        if (isset($_POST['update_privs'])) {
2093
            $extra_data['db_specific_privs'] = false;
2094
            $extra_data['db_wildcard_privs'] = false;
2095
            if (isset($dbname_is_wildcard)) {
2096
                $extra_data['db_specific_privs'] = ! $dbname_is_wildcard;
2097
                $extra_data['db_wildcard_privs'] = $dbname_is_wildcard;
2098
            }
2099
            $new_privileges = implode(', ', $this->extractPrivInfo(null, true));
2100
2101
            $extra_data['new_privileges'] = $new_privileges;
2102
        }
2103
2104
        if (isset($_GET['validate_username'])) {
2105
            $sql_query = "SELECT * FROM `mysql`.`user` WHERE `User` = '"
2106
                . $_GET['username'] . "';";
2107
            $res = $this->dbi->query($sql_query);
2108
            $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

2108
            $row = $this->dbi->fetchRow(/** @scrutinizer ignore-type */ $res);
Loading history...
2109
            if (empty($row)) {
2110
                $extra_data['user_exists'] = false;
2111
            } else {
2112
                $extra_data['user_exists'] = true;
2113
            }
2114
        }
2115
2116
        return $extra_data;
2117
    }
2118
2119
    /**
2120
     * Get the HTML snippet for change user login information
2121
     *
2122
     * @param string $username username
2123
     * @param string $hostname host name
2124
     *
2125
     * @return string HTML snippet
2126
     */
2127
    public function getChangeLoginInformationHtmlForm($username, $hostname)
2128
    {
2129
        $choices = [
2130
            '4' => __('… keep the old one.'),
2131
            '1' => __('… delete the old one from the user tables.'),
2132
            '2' => __(
2133
                '… revoke all active privileges from '
2134
                . 'the old one and delete it afterwards.'
2135
            ),
2136
            '3' => __(
2137
                '… delete the old one from the user tables '
2138
                . 'and reload the privileges afterwards.'
2139
            ),
2140
        ];
2141
2142
        $html_output = '<form action="' . Url::getFromRoute('/server/privileges')
2143
            . '" onsubmit="return checkAddUser(this);" '
2144
            . 'method="post" class="copyUserForm submenu-item">' . "\n"
2145
            . Url::getHiddenInputs('', '')
2146
            . '<input type="hidden" name="old_username" '
2147
            . 'value="' . htmlspecialchars($username) . '">' . "\n"
2148
            . '<input type="hidden" name="old_hostname" '
2149
            . 'value="' . htmlspecialchars($hostname) . '">' . "\n";
2150
2151
        $usergroup = $this->getUserGroupForUser($username);
2152
        if ($usergroup !== null) {
2153
            $html_output .= '<input type="hidden" name="old_usergroup" '
2154
            . 'value="' . htmlspecialchars($usergroup) . '">' . "\n";
2155
        }
2156
2157
        $html_output .= '<fieldset id="fieldset_change_copy_user">' . "\n"
2158
            . '<legend data-submenu-label="' . __('Login Information') . '">' . "\n"
2159
            . __('Change login information / Copy user account')
2160
            . '</legend>' . "\n"
2161
            . $this->getHtmlForLoginInformationFields('change', $username, $hostname);
2162
2163
        $html_output .= '<fieldset id="fieldset_mode">' . "\n"
2164
            . ' <legend>'
2165
            . __('Create a new user account with the same privileges and …')
2166
            . '</legend>' . "\n";
2167
        $html_output .= Util::getRadioFields(
2168
            'mode',
2169
            $choices,
2170
            '4',
2171
            true
2172
        );
2173
        $html_output .= '</fieldset>' . "\n"
2174
           . '</fieldset>' . "\n";
2175
2176
        $html_output .= '<fieldset id="fieldset_change_copy_user_footer" '
2177
            . 'class="tblFooters">' . "\n"
2178
            . '<input type="hidden" name="change_copy" value="1">' . "\n"
2179
            . '<input class="btn btn-primary" type="submit" value="' . __('Go') . '">' . "\n"
2180
            . '</fieldset>' . "\n"
2181
            . '</form>' . "\n";
2182
2183
        return $html_output;
2184
    }
2185
2186
    /**
2187
     * Provide a line with links to the relevant database and table
2188
     *
2189
     * @param string $url_dbname url database name that urlencode() string
2190
     * @param string $dbname     database name
2191
     * @param string $tablename  table name
2192
     *
2193
     * @return string HTML snippet
2194
     */
2195
    public function getLinkToDbAndTable($url_dbname, $dbname, $tablename)
2196
    {
2197
        $scriptName = Util::getScriptNameForOption(
2198
            $GLOBALS['cfg']['DefaultTabDatabase'],
2199
            'database'
2200
        );
2201
        $html_output = '[ ' . __('Database')
2202
            . ' <a href="' . $scriptName
2203
            . Url::getCommon([
2204
                'db' => $url_dbname,
2205
                'reload' => 1,
2206
            ], strpos($scriptName, '?') === false ? '?' : '&')
2207
            . '">'
2208
            . htmlspecialchars(Util::unescapeMysqlWildcards($dbname)) . ': '
2209
            . Util::getTitleForTarget(
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::getTitl...['DefaultTabDatabase']) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

2209
            . /** @scrutinizer ignore-type */ Util::getTitleForTarget(
Loading history...
2210
                $GLOBALS['cfg']['DefaultTabDatabase']
2211
            )
2212
            . "</a> ]\n";
2213
2214
        if (strlen($tablename) > 0) {
2215
            $scriptName = Util::getScriptNameForOption(
2216
                $GLOBALS['cfg']['DefaultTabTable'],
2217
                'table'
2218
            );
2219
            $html_output .= ' [ ' . __('Table') . ' <a href="'
2220
                . $scriptName
2221
                . Url::getCommon([
2222
                    'db' => $url_dbname,
2223
                    'table' => $tablename,
2224
                    'reload' => 1,
2225
                ], strpos($scriptName, '?') === false ? '?' : '&')
2226
                . '">' . htmlspecialchars($tablename) . ': '
2227
                . Util::getTitleForTarget(
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::getTitl...g']['DefaultTabTable']) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

2227
                . /** @scrutinizer ignore-type */ Util::getTitleForTarget(
Loading history...
2228
                    $GLOBALS['cfg']['DefaultTabTable']
2229
                )
2230
                . "</a> ]\n";
2231
        }
2232
        return $html_output;
2233
    }
2234
2235
    /**
2236
     * no db name given, so we want all privs for the given user
2237
     * db name was given, so we want all user specific rights for this db
2238
     * So this function returns user rights as an array
2239
     *
2240
     * @param string $username username
2241
     * @param string $hostname host name
2242
     * @param string $type     database or table
2243
     * @param string $dbname   database name
2244
     *
2245
     * @return array database rights
2246
     */
2247
    public function getUserSpecificRights($username, $hostname, $type, $dbname = '')
2248
    {
2249
        $user_host_condition = " WHERE `User`"
2250
            . " = '" . $this->dbi->escapeString($username) . "'"
2251
            . " AND `Host`"
2252
            . " = '" . $this->dbi->escapeString($hostname) . "'";
2253
2254
        if ($type == 'database') {
2255
            $tables_to_search_for_users = [
2256
                'tables_priv',
2257
                'columns_priv',
2258
                'procs_priv',
2259
            ];
2260
            $dbOrTableName = 'Db';
2261
        } elseif ($type == 'table') {
2262
            $user_host_condition .= " AND `Db` LIKE '"
2263
                . $this->dbi->escapeString($dbname) . "'";
2264
            $tables_to_search_for_users = ['columns_priv'];
2265
            $dbOrTableName = 'Table_name';
2266
        } else { // routine
2267
            $user_host_condition .= " AND `Db` LIKE '"
2268
                . $this->dbi->escapeString($dbname) . "'";
2269
            $tables_to_search_for_users = ['procs_priv'];
2270
            $dbOrTableName = 'Routine_name';
2271
        }
2272
2273
        // we also want privileges for this user not in table `db` but in other table
2274
        $tables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`;');
2275
2276
        $db_rights_sqls = [];
2277
        foreach ($tables_to_search_for_users as $table_search_in) {
2278
            if (in_array($table_search_in, $tables)) {
2279
                $db_rights_sqls[] = '
2280
                    SELECT DISTINCT `' . $dbOrTableName . '`
2281
                    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

2281
                    FROM `mysql`.' . /** @scrutinizer ignore-type */ Util::backquote($table_search_in)
Loading history...
2282
                   . $user_host_condition;
2283
            }
2284
        }
2285
2286
        $user_defaults = [
2287
            $dbOrTableName  => '',
2288
            'Grant_priv'    => 'N',
2289
            'privs'         => ['USAGE'],
2290
            'Column_priv'   => true,
2291
        ];
2292
2293
        // for the rights
2294
        $db_rights = [];
2295
2296
        $db_rights_sql = '(' . implode(') UNION (', $db_rights_sqls) . ')'
2297
            . ' ORDER BY `' . $dbOrTableName . '` ASC';
2298
2299
        $db_rights_result = $this->dbi->query($db_rights_sql);
2300
2301
        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

2301
        while ($db_rights_row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $db_rights_result)) {
Loading history...
2302
            $db_rights_row = array_merge($user_defaults, $db_rights_row);
2303
            if ($type == 'database') {
2304
                // only Db names in the table `mysql`.`db` uses wildcards
2305
                // as we are in the db specific rights display we want
2306
                // all db names escaped, also from other sources
2307
                $db_rights_row['Db'] = Util::escapeMysqlWildcards(
2308
                    $db_rights_row['Db']
2309
                );
2310
            }
2311
            $db_rights[$db_rights_row[$dbOrTableName]] = $db_rights_row;
2312
        }
2313
2314
        $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

2314
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $db_rights_result);
Loading history...
2315
2316
        if ($type == 'database') {
2317
            $sql_query = 'SELECT * FROM `mysql`.`db`'
2318
                . $user_host_condition . ' ORDER BY `Db` ASC';
2319
        } elseif ($type == 'table') {
2320
            $sql_query = 'SELECT `Table_name`,'
2321
                . ' `Table_priv`,'
2322
                . ' IF(`Column_priv` = _latin1 \'\', 0, 1)'
2323
                . ' AS \'Column_priv\''
2324
                . ' FROM `mysql`.`tables_priv`'
2325
                . $user_host_condition
2326
                . ' ORDER BY `Table_name` ASC;';
2327
        } else {
2328
            $sql_query = "SELECT `Routine_name`, `Proc_priv`"
2329
                . " FROM `mysql`.`procs_priv`"
2330
                . $user_host_condition
2331
                . " ORDER BY `Routine_name`";
2332
        }
2333
2334
        $result = $this->dbi->query($sql_query);
2335
2336
        while ($row = $this->dbi->fetchAssoc($result)) {
2337
            if (isset($db_rights[$row[$dbOrTableName]])) {
2338
                $db_rights[$row[$dbOrTableName]]
2339
                    = array_merge($db_rights[$row[$dbOrTableName]], $row);
2340
            } else {
2341
                $db_rights[$row[$dbOrTableName]] = $row;
2342
            }
2343
            if ($type == 'database') {
2344
                // there are db specific rights for this user
2345
                // so we can drop this db rights
2346
                $db_rights[$row['Db']]['can_delete'] = true;
2347
            }
2348
        }
2349
        $this->dbi->freeResult($result);
2350
        return $db_rights;
2351
    }
2352
2353
    /**
2354
     * Parses Proc_priv data
2355
     *
2356
     * @param string $privs Proc_priv
2357
     *
2358
     * @return array
2359
     */
2360
    public function parseProcPriv($privs)
2361
    {
2362
        $result = [
2363
            'Alter_routine_priv' => 'N',
2364
            'Execute_priv'       => 'N',
2365
            'Grant_priv'         => 'N',
2366
        ];
2367
        foreach (explode(',', (string) $privs) as $priv) {
2368
            if ($priv == 'Alter Routine') {
2369
                $result['Alter_routine_priv'] = 'Y';
2370
            } else {
2371
                $result[$priv . '_priv'] = 'Y';
2372
            }
2373
        }
2374
        return $result;
2375
    }
2376
2377
    /**
2378
     * Get a HTML table for display user's tabel specific or database specific rights
2379
     *
2380
     * @param string $username username
2381
     * @param string $hostname host name
2382
     * @param string $type     database, table or routine
2383
     * @param string $dbname   database name
2384
     *
2385
     * @return string
2386
     */
2387
    public function getHtmlForAllTableSpecificRights(
2388
        $username,
2389
        $hostname,
2390
        $type,
2391
        $dbname = ''
2392
    ) {
2393
        $uiData = [
2394
            'database' => [
2395
                'form_id'        => 'database_specific_priv',
2396
                'sub_menu_label' => __('Database'),
2397
                'legend'         => __('Database-specific privileges'),
2398
                'type_label'     => __('Database'),
2399
            ],
2400
            'table' => [
2401
                'form_id'        => 'table_specific_priv',
2402
                'sub_menu_label' => __('Table'),
2403
                'legend'         => __('Table-specific privileges'),
2404
                'type_label'     => __('Table'),
2405
            ],
2406
            'routine' => [
2407
                'form_id'        => 'routine_specific_priv',
2408
                'sub_menu_label' => __('Routine'),
2409
                'legend'         => __('Routine-specific privileges'),
2410
                'type_label'     => __('Routine'),
2411
            ],
2412
        ];
2413
2414
        /**
2415
         * no db name given, so we want all privs for the given user
2416
         * db name was given, so we want all user specific rights for this db
2417
         */
2418
        $db_rights = $this->getUserSpecificRights($username, $hostname, $type, $dbname);
2419
        ksort($db_rights);
2420
2421
        $foundRows = [];
2422
        $privileges = [];
2423
        foreach ($db_rights as $row) {
2424
            $onePrivilege = [];
2425
2426
            $paramTableName = '';
2427
            $paramRoutineName = '';
2428
2429
            if ($type == 'database') {
2430
                $name = $row['Db'];
2431
                $onePrivilege['grant']        = $row['Grant_priv'] == 'Y';
2432
                $onePrivilege['table_privs']   = ! empty($row['Table_priv'])
2433
                    || ! empty($row['Column_priv']);
2434
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
2435
2436
                $paramDbName = $row['Db'];
2437
            } elseif ($type == 'table') {
2438
                $name = $row['Table_name'];
2439
                $onePrivilege['grant'] = in_array(
2440
                    'Grant',
2441
                    explode(',', $row['Table_priv'])
2442
                );
2443
                $onePrivilege['column_privs']  = ! empty($row['Column_priv']);
2444
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
2445
2446
                $paramDbName = $dbname;
2447
                $paramTableName = $row['Table_name'];
2448
            } else { // routine
2449
                $name = $row['Routine_name'];
2450
                $onePrivilege['grant'] = in_array(
2451
                    'Grant',
2452
                    explode(',', $row['Proc_priv'])
2453
                );
2454
2455
                $privs = $this->parseProcPriv($row['Proc_priv']);
2456
                $onePrivilege['privileges'] = implode(
2457
                    ',',
2458
                    $this->extractPrivInfo($privs, true)
2459
                );
2460
2461
                $paramDbName = $dbname;
2462
                $paramRoutineName = $row['Routine_name'];
2463
            }
2464
2465
            $foundRows[] = $name;
2466
            $onePrivilege['name'] = $name;
2467
2468
            $onePrivilege['edit_link'] = '';
2469
            if ($GLOBALS['is_grantuser']) {
2470
                $onePrivilege['edit_link'] = $this->getUserLink(
2471
                    'edit',
2472
                    $username,
2473
                    $hostname,
2474
                    $paramDbName,
2475
                    $paramTableName,
2476
                    $paramRoutineName
2477
                );
2478
            }
2479
2480
            $onePrivilege['revoke_link'] = '';
2481
            if ($type != 'database' || ! empty($row['can_delete'])) {
2482
                $onePrivilege['revoke_link'] = $this->getUserLink(
2483
                    'revoke',
2484
                    $username,
2485
                    $hostname,
2486
                    $paramDbName,
2487
                    $paramTableName,
2488
                    $paramRoutineName
2489
                );
2490
            }
2491
2492
            $privileges[] = $onePrivilege;
2493
        }
2494
2495
        $data = $uiData[$type];
2496
        $data['privileges'] = $privileges;
2497
        $data['username']   = $username;
2498
        $data['hostname']   = $hostname;
2499
        $data['database']   = $dbname;
2500
        $data['type']       = $type;
2501
2502
        if ($type == 'database') {
2503
            // we already have the list of databases from libraries/common.inc.php
2504
            // via $pma = new PMA;
2505
            $pred_db_array = $GLOBALS['dblist']->databases;
2506
            $databases_to_skip = [
2507
                'information_schema',
2508
                'performance_schema',
2509
            ];
2510
2511
            $databases = [];
2512
            if (! empty($pred_db_array)) {
2513
                foreach ($pred_db_array as $current_db) {
2514
                    if (in_array($current_db, $databases_to_skip)) {
2515
                        continue;
2516
                    }
2517
                    $current_db_escaped = Util::escapeMysqlWildcards($current_db);
2518
                    // cannot use array_diff() once, outside of the loop,
2519
                    // because the list of databases has special characters
2520
                    // already escaped in $foundRows,
2521
                    // contrary to the output of SHOW DATABASES
2522
                    if (! in_array($current_db_escaped, $foundRows)) {
2523
                        $databases[] = $current_db;
2524
                    }
2525
                }
2526
            }
2527
            $data['databases'] = $databases;
2528
        } elseif ($type == 'table') {
2529
            $result = @$this->dbi->tryQuery(
2530
                "SHOW TABLES FROM " . Util::backquote($dbname),
2531
                DatabaseInterface::CONNECT_USER,
2532
                DatabaseInterface::QUERY_STORE
2533
            );
2534
2535
            $tables = [];
2536
            if ($result) {
2537
                while ($row = $this->dbi->fetchRow($result)) {
2538
                    if (! in_array($row[0], $foundRows)) {
2539
                        $tables[] = $row[0];
2540
                    }
2541
                }
2542
                $this->dbi->freeResult($result);
2543
            }
2544
            $data['tables'] = $tables;
2545
        } else { // routine
2546
            $routineData = $this->dbi->getRoutines($dbname);
2547
2548
            $routines = [];
2549
            foreach ($routineData as $routine) {
2550
                if (! in_array($routine['name'], $foundRows)) {
2551
                    $routines[] = $routine['name'];
2552
                }
2553
            }
2554
            $data['routines'] = $routines;
2555
        }
2556
2557
        return $this->template->render('server/privileges/privileges_summary', $data);
2558
    }
2559
2560
    /**
2561
     * Get HTML for display the users overview
2562
     * (if less than 50 users, display them immediately)
2563
     *
2564
     * @param array  $result        ran sql query
2565
     * @param array  $db_rights     user's database rights array
2566
     * @param string $pmaThemeImage a image source link
2567
     * @param string $text_dir      text directory
2568
     *
2569
     * @return string HTML snippet
2570
     */
2571
    public function getUsersOverview($result, array $db_rights, $pmaThemeImage, $text_dir)
2572
    {
2573
        global $is_grantuser, $is_createuser;
2574
2575
        $cfgRelation = $this->relation->getRelationsParam();
2576
2577
        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

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

2581
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
2582
2583
        $user_group_count = 0;
2584
        if ($cfgRelation['menuswork']) {
2585
            $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

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

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

2590
                while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $result)) {
Loading history...
2591
                    $group_assignment[$row['username']] = $row['usergroup'];
2592
                }
2593
            }
2594
            $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

2594
            $this->dbi->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
2595
2596
            $user_group_count = $this->getUserGroupCount();
2597
        }
2598
2599
        $hosts = [];
2600
        foreach ($db_rights as $user) {
2601
            ksort($user);
2602
            foreach ($user as $host) {
2603
                $check_plugin_query = "SELECT * FROM `mysql`.`user` WHERE "
2604
                    . "`User` = '" . $host['User'] . "' AND `Host` = '"
2605
                    . $host['Host'] . "'";
2606
                $res = $this->dbi->fetchSingleRow($check_plugin_query);
2607
2608
                $hasPassword = false;
2609
                if ((isset($res['authentication_string'])
2610
                    && ! empty($res['authentication_string']))
2611
                    || (isset($res['Password'])
2612
                    && ! empty($res['Password']))
2613
                ) {
2614
                    $hasPassword = true;
2615
                }
2616
2617
                $hosts[] = [
2618
                    'user' => $host['User'],
2619
                    'host' => $host['Host'],
2620
                    'has_password' => $hasPassword,
2621
                    'has_select_priv' => isset($host['Select_priv']),
2622
                    'privileges' => $host['privs'],
2623
                    'group' => $group_assignment[$host['User']] ?? '',
2624
                    'has_grant' => $host['Grant_priv'] == 'Y',
2625
                ];
2626
            }
2627
        }
2628
2629
        return $this->template->render('server/privileges/users_overview', [
2630
            'menus_work' => $cfgRelation['menuswork'],
2631
            'user_group_count' => $user_group_count,
2632
            'pma_theme_image' => $pmaThemeImage,
2633
            'text_dir' => $text_dir,
2634
            'initial' => $_GET['initial'] ?? '',
2635
            'hosts' => $hosts,
2636
            'is_grantuser' => $is_grantuser,
2637
            'is_createuser' => $is_createuser,
2638
        ]);
2639
    }
2640
2641
    /**
2642
     * Get HTML for Displays the initials
2643
     *
2644
     * @param array $array_initials array for all initials, even non A-Z
2645
     *
2646
     * @return string HTML snippet
2647
     */
2648
    public function getHtmlForInitials(array $array_initials)
2649
    {
2650
        // initialize to false the letters A-Z
2651
        for ($letter_counter = 1; $letter_counter < 27; $letter_counter++) {
2652
            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

2652
            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...
2653
                $array_initials[mb_chr($letter_counter + 64)] = false;
2654
            }
2655
        }
2656
2657
        $initials = $this->dbi->tryQuery(
2658
            'SELECT DISTINCT UPPER(LEFT(`User`,1)) FROM `user`'
2659
            . ' ORDER BY UPPER(LEFT(`User`,1)) ASC',
2660
            DatabaseInterface::CONNECT_USER,
2661
            DatabaseInterface::QUERY_STORE
2662
        );
2663
        if ($initials) {
2664
            while (list($tmp_initial) = $this->dbi->fetchRow($initials)) {
2665
                $array_initials[$tmp_initial] = true;
2666
            }
2667
        }
2668
2669
        // Display the initials, which can be any characters, not
2670
        // just letters. For letters A-Z, we add the non-used letters
2671
        // as greyed out.
2672
2673
        uksort($array_initials, "strnatcasecmp");
2674
2675
        return $this->template->render('server/privileges/initials_row', [
2676
            'array_initials' => $array_initials,
2677
            'initial' => isset($_GET['initial']) ? $_GET['initial'] : null,
2678
        ]);
2679
    }
2680
2681
    /**
2682
     * Get the database rights array for Display user overview
2683
     *
2684
     * @return array    database rights array
2685
     */
2686
    public function getDbRightsForUserOverview()
2687
    {
2688
        // we also want users not in table `user` but in other table
2689
        $tables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`;');
2690
2691
        $tablesSearchForUsers = [
2692
            'user',
2693
            'db',
2694
            'tables_priv',
2695
            'columns_priv',
2696
            'procs_priv',
2697
        ];
2698
2699
        $db_rights_sqls = [];
2700
        foreach ($tablesSearchForUsers as $table_search_in) {
2701
            if (in_array($table_search_in, $tables)) {
2702
                $db_rights_sqls[] = 'SELECT DISTINCT `User`, `Host` FROM `mysql`.`'
2703
                    . $table_search_in . '` '
2704
                    . (isset($_GET['initial'])
2705
                    ? $this->rangeOfUsers($_GET['initial'])
2706
                    : '');
2707
            }
2708
        }
2709
        $user_defaults = [
2710
            'User'       => '',
2711
            'Host'       => '%',
2712
            'Password'   => '?',
2713
            'Grant_priv' => 'N',
2714
            'privs'      => ['USAGE'],
2715
        ];
2716
2717
        // for the rights
2718
        $db_rights = [];
2719
2720
        $db_rights_sql = '(' . implode(') UNION (', $db_rights_sqls) . ')'
2721
            . ' ORDER BY `User` ASC, `Host` ASC';
2722
2723
        $db_rights_result = $this->dbi->query($db_rights_sql);
2724
2725
        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

2725
        while ($db_rights_row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $db_rights_result)) {
Loading history...
2726
            $db_rights_row = array_merge($user_defaults, $db_rights_row);
2727
            $db_rights[$db_rights_row['User']][$db_rights_row['Host']]
2728
                = $db_rights_row;
2729
        }
2730
        $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

2730
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $db_rights_result);
Loading history...
2731
        ksort($db_rights);
2732
2733
        return $db_rights;
2734
    }
2735
2736
    /**
2737
     * Delete user and get message and sql query for delete user in privileges
2738
     *
2739
     * @param array $queries queries
2740
     *
2741
     * @return array Message
2742
     */
2743
    public function deleteUser(array $queries)
2744
    {
2745
        $sql_query = '';
2746
        if (empty($queries)) {
2747
            $message = Message::error(__('No users selected for deleting!'));
2748
        } else {
2749
            if ($_POST['mode'] == 3) {
2750
                $queries[] = '# ' . __('Reloading the privileges') . ' …';
2751
                $queries[] = 'FLUSH PRIVILEGES;';
2752
            }
2753
            $drop_user_error = '';
2754
            foreach ($queries as $sql_query) {
2755
                if ($sql_query[0] != '#') {
2756
                    if (! $this->dbi->tryQuery($sql_query)) {
2757
                        $drop_user_error .= $this->dbi->getError() . "\n";
2758
                    }
2759
                }
2760
            }
2761
            // tracking sets this, causing the deleted db to be shown in navi
2762
            unset($GLOBALS['db']);
2763
2764
            $sql_query = implode("\n", $queries);
2765
            if (! empty($drop_user_error)) {
2766
                $message = Message::rawError($drop_user_error);
2767
            } else {
2768
                $message = Message::success(
2769
                    __('The selected users have been deleted successfully.')
2770
                );
2771
            }
2772
        }
2773
        return [
2774
            $sql_query,
2775
            $message,
2776
        ];
2777
    }
2778
2779
    /**
2780
     * Update the privileges and return the success or error message
2781
     *
2782
     * @param string $username  username
2783
     * @param string $hostname  host name
2784
     * @param string $tablename table name
2785
     * @param string $dbname    database name
2786
     * @param string $itemType  item type
2787
     *
2788
     * @return array success message or error message for update
2789
     */
2790
    public function updatePrivileges($username, $hostname, $tablename, $dbname, $itemType)
2791
    {
2792
        $db_and_table = $this->wildcardEscapeForGrant($dbname, $tablename);
2793
2794
        $sql_query0 = 'REVOKE ALL PRIVILEGES ON ' . $itemType . ' ' . $db_and_table
2795
            . ' FROM \'' . $this->dbi->escapeString($username)
2796
            . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
2797
2798
        if (! isset($_POST['Grant_priv']) || $_POST['Grant_priv'] != 'Y') {
2799
            $sql_query1 = 'REVOKE GRANT OPTION ON ' . $itemType . ' ' . $db_and_table
2800
                . ' FROM \'' . $this->dbi->escapeString($username) . '\'@\''
2801
                . $this->dbi->escapeString($hostname) . '\';';
2802
        } else {
2803
            $sql_query1 = '';
2804
        }
2805
2806
        // Should not do a GRANT USAGE for a table-specific privilege, it
2807
        // causes problems later (cannot revoke it)
2808
        if (! (strlen($tablename) > 0
2809
            && 'USAGE' == implode('', $this->extractPrivInfo()))
2810
        ) {
2811
            $sql_query2 = 'GRANT ' . implode(', ', $this->extractPrivInfo())
2812
                . ' ON ' . $itemType . ' ' . $db_and_table
2813
                . ' TO \'' . $this->dbi->escapeString($username) . '\'@\''
2814
                . $this->dbi->escapeString($hostname) . '\'';
2815
2816
            if (strlen($dbname) === 0) {
2817
                // add REQUIRE clause
2818
                $sql_query2 .= $this->getRequireClause();
2819
            }
2820
2821
            if ((isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y')
2822
                || (strlen($dbname) === 0
2823
                && (isset($_POST['max_questions']) || isset($_POST['max_connections'])
2824
                || isset($_POST['max_updates'])
2825
                || isset($_POST['max_user_connections'])))
2826
            ) {
2827
                $sql_query2 .= $this->getWithClauseForAddUserAndUpdatePrivs();
2828
            }
2829
            $sql_query2 .= ';';
2830
        }
2831
        if (! $this->dbi->tryQuery($sql_query0)) {
2832
            // This might fail when the executing user does not have
2833
            // ALL PRIVILEGES himself.
2834
            // See https://github.com/phpmyadmin/phpmyadmin/issues/9673
2835
            $sql_query0 = '';
2836
        }
2837
        if (! empty($sql_query1) && ! $this->dbi->tryQuery($sql_query1)) {
2838
            // this one may fail, too...
2839
            $sql_query1 = '';
2840
        }
2841
        if (! empty($sql_query2)) {
2842
            $this->dbi->query($sql_query2);
2843
        } else {
2844
            $sql_query2 = '';
2845
        }
2846
        $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...
2847
        $message = Message::success(__('You have updated the privileges for %s.'));
2848
        $message->addParam('\'' . $username . '\'@\'' . $hostname . '\'');
2849
2850
        return [
2851
            $sql_query,
2852
            $message,
2853
        ];
2854
    }
2855
2856
    /**
2857
     * Get List of information: Changes / copies a user
2858
     *
2859
     * @return array
2860
     */
2861
    public function getDataForChangeOrCopyUser()
2862
    {
2863
        $queries = null;
2864
        $password = null;
2865
2866
        if (isset($_POST['change_copy'])) {
2867
            $user_host_condition = ' WHERE `User` = '
2868
                . "'" . $this->dbi->escapeString($_POST['old_username']) . "'"
2869
                . ' AND `Host` = '
2870
                . "'" . $this->dbi->escapeString($_POST['old_hostname']) . "';";
2871
            $row = $this->dbi->fetchSingleRow(
2872
                'SELECT * FROM `mysql`.`user` ' . $user_host_condition
2873
            );
2874
            if (! $row) {
2875
                $response = Response::getInstance();
2876
                $response->addHTML(
2877
                    Message::notice(__('No user found.'))->getDisplay()
2878
                );
2879
                unset($_POST['change_copy']);
2880
            } else {
2881
                foreach ($row as $key => $value) {
2882
                    $GLOBALS[$key] = $value;
2883
                }
2884
                $serverVersion = $this->dbi->getVersion();
2885
                // Recent MySQL versions have the field "Password" in mysql.user,
2886
                // so the previous extract creates $row['Password'] but this script
2887
                // uses $password
2888
                if (! isset($row['password']) && isset($row['Password'])) {
2889
                    $row['password'] = $row['Password'];
2890
                }
2891
                if (Util::getServerType() == 'MySQL'
2892
                    && $serverVersion >= 50606
2893
                    && $serverVersion < 50706
2894
                    && ((isset($row['authentication_string'])
2895
                    && empty($row['password']))
2896
                    || (isset($row['plugin'])
2897
                    && $row['plugin'] == 'sha256_password'))
2898
                ) {
2899
                    $row['password'] = $row['authentication_string'];
2900
                }
2901
2902
                if (Util::getServerType() == 'MariaDB'
2903
                    && $serverVersion >= 50500
2904
                    && isset($row['authentication_string'])
2905
                    && empty($row['password'])
2906
                ) {
2907
                    $row['password'] = $row['authentication_string'];
2908
                }
2909
2910
                // Always use 'authentication_string' column
2911
                // for MySQL 5.7.6+ since it does not have
2912
                // the 'password' column at all
2913
                if (in_array(Util::getServerType(), ['MySQL', 'Percona Server'])
2914
                    && $serverVersion >= 50706
2915
                    && isset($row['authentication_string'])
2916
                ) {
2917
                    $row['password'] = $row['authentication_string'];
2918
                }
2919
                $password = $row['password'];
2920
                $queries = [];
2921
            }
2922
        }
2923
2924
        return [
2925
            $queries,
2926
            $password,
2927
        ];
2928
    }
2929
2930
    /**
2931
     * Update Data for information: Deletes users
2932
     *
2933
     * @param array $queries queries array
2934
     *
2935
     * @return array
2936
     */
2937
    public function getDataForDeleteUsers($queries)
2938
    {
2939
        if (isset($_POST['change_copy'])) {
2940
            $selected_usr = [
2941
                $_POST['old_username'] . '&amp;#27;' . $_POST['old_hostname'],
2942
            ];
2943
        } else {
2944
            $selected_usr = $_POST['selected_usr'];
2945
            $queries = [];
2946
        }
2947
2948
        // this happens, was seen in https://reports.phpmyadmin.net/reports/view/17146
2949
        if (! is_array($selected_usr)) {
2950
            return [];
2951
        }
2952
2953
        foreach ($selected_usr as $each_user) {
2954
            list($this_user, $this_host) = explode('&amp;#27;', $each_user);
2955
            $queries[] = '# '
2956
                . sprintf(
2957
                    __('Deleting %s'),
2958
                    '\'' . $this_user . '\'@\'' . $this_host . '\''
2959
                )
2960
                . ' ...';
2961
            $queries[] = 'DROP USER \''
2962
                . $this->dbi->escapeString($this_user)
2963
                . '\'@\'' . $this->dbi->escapeString($this_host) . '\';';
2964
            $this->relationCleanup->user($this_user);
2965
2966
            if (isset($_POST['drop_users_db'])) {
2967
                $queries[] = 'DROP DATABASE IF EXISTS '
2968
                    . Util::backquote($this_user) . ';';
2969
                $GLOBALS['reload'] = true;
2970
            }
2971
        }
2972
        return $queries;
2973
    }
2974
2975
    /**
2976
     * update Message For Reload
2977
     *
2978
     * @return Message|null
2979
     */
2980
    public function updateMessageForReload(): ?Message
2981
    {
2982
        $message = null;
2983
        if (isset($_GET['flush_privileges'])) {
2984
            $sql_query = 'FLUSH PRIVILEGES;';
2985
            $this->dbi->query($sql_query);
2986
            $message = Message::success(
2987
                __('The privileges were reloaded successfully.')
2988
            );
2989
        }
2990
2991
        if (isset($_GET['validate_username'])) {
2992
            $message = Message::success();
2993
        }
2994
2995
        return $message;
2996
    }
2997
2998
    /**
2999
     * update Data For Queries from queries_for_display
3000
     *
3001
     * @param array      $queries             queries array
3002
     * @param array|null $queries_for_display queries array for display
3003
     *
3004
     * @return array
3005
     */
3006
    public function getDataForQueries(array $queries, $queries_for_display)
3007
    {
3008
        $tmp_count = 0;
3009
        foreach ($queries as $sql_query) {
3010
            if ($sql_query[0] != '#') {
3011
                $this->dbi->query($sql_query);
3012
            }
3013
            // when there is a query containing a hidden password, take it
3014
            // instead of the real query sent
3015
            if (isset($queries_for_display[$tmp_count])) {
3016
                $queries[$tmp_count] = $queries_for_display[$tmp_count];
3017
            }
3018
            $tmp_count++;
3019
        }
3020
3021
        return $queries;
3022
    }
3023
3024
    /**
3025
     * update Data for information: Adds a user
3026
     *
3027
     * @param string|array|null $dbname      db name
3028
     * @param string            $username    user name
3029
     * @param string            $hostname    host name
3030
     * @param string|null       $password    password
3031
     * @param bool              $is_menuwork is_menuwork set?
3032
     *
3033
     * @return array
3034
     */
3035
    public function addUser(
3036
        $dbname,
3037
        $username,
3038
        $hostname,
3039
        ?string $password,
3040
        $is_menuwork
3041
    ) {
3042
        $_add_user_error = false;
3043
        $message = null;
3044
        $queries = null;
3045
        $queries_for_display = null;
3046
        $sql_query = null;
3047
3048
        if (! isset($_POST['adduser_submit']) && ! isset($_POST['change_copy'])) {
3049
            return [
3050
                $message,
3051
                $queries,
3052
                $queries_for_display,
3053
                $sql_query,
3054
                $_add_user_error,
3055
            ];
3056
        }
3057
3058
        $sql_query = '';
3059
        if ($_POST['pred_username'] == 'any') {
3060
            $username = '';
3061
        }
3062
        switch ($_POST['pred_hostname']) {
3063
            case 'any':
3064
                $hostname = '%';
3065
                break;
3066
            case 'localhost':
3067
                $hostname = 'localhost';
3068
                break;
3069
            case 'hosttable':
3070
                $hostname = '';
3071
                break;
3072
            case 'thishost':
3073
                $_user_name = $this->dbi->fetchValue('SELECT USER()');
3074
                $hostname = mb_substr(
3075
                    $_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

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

3076
                    mb_strrpos(/** @scrutinizer ignore-type */ $_user_name, '@') + 1
Loading history...
3077
                );
3078
                unset($_user_name);
3079
                break;
3080
        }
3081
        $sql = "SELECT '1' FROM `mysql`.`user`"
3082
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
3083
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';";
3084
        if ($this->dbi->fetchValue($sql) == 1) {
3085
            $message = Message::error(__('The user %s already exists!'));
3086
            $message->addParam('[em]\'' . $username . '\'@\'' . $hostname . '\'[/em]');
3087
            $_GET['adduser'] = true;
3088
            $_add_user_error = true;
3089
3090
            return [
3091
                $message,
3092
                $queries,
3093
                $queries_for_display,
3094
                $sql_query,
3095
                $_add_user_error,
3096
            ];
3097
        }
3098
3099
        list(
3100
            $create_user_real,
3101
            $create_user_show,
3102
            $real_sql_query,
3103
            $sql_query,
3104
            $password_set_real,
3105
            $password_set_show,
3106
            $alter_real_sql_query,
3107
            $alter_sql_query
3108
        ) = $this->getSqlQueriesForDisplayAndAddUser(
3109
            $username,
3110
            $hostname,
3111
            (isset($password) ? $password : '')
3112
        );
3113
3114
        if (empty($_POST['change_copy'])) {
3115
            $_error = false;
3116
3117
            if ($create_user_real !== null) {
0 ignored issues
show
introduced by
The condition $create_user_real !== null is always true.
Loading history...
3118
                if (! $this->dbi->tryQuery($create_user_real)) {
3119
                    $_error = true;
3120
                }
3121
                if (isset($password_set_real, $_POST['authentication_plugin']) && ! empty($password_set_real)) {
3122
                    $this->setProperPasswordHashing(
3123
                        $_POST['authentication_plugin']
3124
                    );
3125
                    if ($this->dbi->tryQuery($password_set_real)) {
3126
                        $sql_query .= $password_set_show;
3127
                    }
3128
                }
3129
                $sql_query = $create_user_show . $sql_query;
3130
            }
3131
3132
            list($sql_query, $message) = $this->addUserAndCreateDatabase(
3133
                $_error,
3134
                $real_sql_query,
3135
                $sql_query,
3136
                $username,
3137
                $hostname,
3138
                $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

3138
                /** @scrutinizer ignore-type */ $dbname,
Loading history...
3139
                $alter_real_sql_query,
3140
                $alter_sql_query
3141
            );
3142
            if (! empty($_POST['userGroup']) && $is_menuwork) {
3143
                $this->setUserGroup($GLOBALS['username'], $_POST['userGroup']);
3144
            }
3145
3146
            return [
3147
                $message,
3148
                $queries,
3149
                $queries_for_display,
3150
                $sql_query,
3151
                $_add_user_error,
3152
            ];
3153
        }
3154
3155
        // Copy the user group while copying a user
3156
        $old_usergroup =
3157
            isset($_POST['old_usergroup']) ? $_POST['old_usergroup'] : null;
3158
        $this->setUserGroup($_POST['username'], $old_usergroup);
3159
3160
        if ($create_user_real === null) {
0 ignored issues
show
introduced by
The condition $create_user_real === null is always false.
Loading history...
3161
            $queries[] = $create_user_real;
3162
        }
3163
        $queries[] = $real_sql_query;
3164
3165
        if (isset($password_set_real, $_POST['authentication_plugin']) && ! empty($password_set_real)) {
3166
            $this->setProperPasswordHashing(
3167
                $_POST['authentication_plugin']
3168
            );
3169
3170
            $queries[] = $password_set_real;
3171
        }
3172
        // we put the query containing the hidden password in
3173
        // $queries_for_display, at the same position occupied
3174
        // by the real query in $queries
3175
        $tmp_count = count($queries);
3176
        if (isset($create_user_real)) {
3177
            $queries_for_display[$tmp_count - 2] = $create_user_show;
3178
        }
3179
        if (isset($password_set_real) && ! empty($password_set_real)) {
3180
            $queries_for_display[$tmp_count - 3] = $create_user_show;
3181
            $queries_for_display[$tmp_count - 2] = $sql_query;
3182
            $queries_for_display[$tmp_count - 1] = $password_set_show;
3183
        } else {
3184
            $queries_for_display[$tmp_count - 1] = $sql_query;
3185
        }
3186
3187
        return [
3188
            $message,
3189
            $queries,
3190
            $queries_for_display,
3191
            $sql_query,
3192
            $_add_user_error,
3193
        ];
3194
    }
3195
3196
    /**
3197
     * Sets proper value of `old_passwords` according to
3198
     * the authentication plugin selected
3199
     *
3200
     * @param string $auth_plugin authentication plugin selected
3201
     *
3202
     * @return void
3203
     */
3204
    public function setProperPasswordHashing($auth_plugin)
3205
    {
3206
        // Set the hashing method used by PASSWORD()
3207
        // to be of type depending upon $authentication_plugin
3208
        if ($auth_plugin == 'sha256_password') {
3209
            $this->dbi->tryQuery('SET `old_passwords` = 2;');
3210
        } elseif ($auth_plugin == 'mysql_old_password') {
3211
            $this->dbi->tryQuery('SET `old_passwords` = 1;');
3212
        } else {
3213
            $this->dbi->tryQuery('SET `old_passwords` = 0;');
3214
        }
3215
    }
3216
3217
    /**
3218
     * Update DB information: DB, Table, isWildcard
3219
     *
3220
     * @return array
3221
     */
3222
    public function getDataForDBInfo()
3223
    {
3224
        $username = null;
3225
        $hostname = null;
3226
        $dbname = null;
3227
        $tablename = null;
3228
        $routinename = null;
3229
        $dbname_is_wildcard = null;
3230
3231
        if (isset($_REQUEST['username'])) {
3232
            $username = $_REQUEST['username'];
3233
        }
3234
        if (isset($_REQUEST['hostname'])) {
3235
            $hostname = $_REQUEST['hostname'];
3236
        }
3237
        /**
3238
         * Checks if a dropdown box has been used for selecting a database / table
3239
         */
3240
        if (Core::isValid($_POST['pred_tablename'])) {
3241
            $tablename = $_POST['pred_tablename'];
3242
        } elseif (Core::isValid($_REQUEST['tablename'])) {
3243
            $tablename = $_REQUEST['tablename'];
3244
        } else {
3245
            unset($tablename);
3246
        }
3247
3248
        if (Core::isValid($_POST['pred_routinename'])) {
3249
            $routinename = $_POST['pred_routinename'];
3250
        } elseif (Core::isValid($_REQUEST['routinename'])) {
3251
            $routinename = $_REQUEST['routinename'];
3252
        } else {
3253
            unset($routinename);
3254
        }
3255
3256
        if (isset($_POST['pred_dbname'])) {
3257
            $is_valid_pred_dbname = true;
3258
            foreach ($_POST['pred_dbname'] as $key => $db_name) {
3259
                if (! Core::isValid($db_name)) {
3260
                    $is_valid_pred_dbname = false;
3261
                    break;
3262
                }
3263
            }
3264
        }
3265
3266
        if (isset($_REQUEST['dbname'])) {
3267
            $is_valid_dbname = true;
3268
            if (is_array($_REQUEST['dbname'])) {
3269
                foreach ($_REQUEST['dbname'] as $key => $db_name) {
3270
                    if (! Core::isValid($db_name)) {
3271
                        $is_valid_dbname = false;
3272
                        break;
3273
                    }
3274
                }
3275
            } else {
3276
                if (! Core::isValid($_REQUEST['dbname'])) {
3277
                    $is_valid_dbname = false;
3278
                }
3279
            }
3280
        }
3281
3282
        if (isset($is_valid_pred_dbname) && $is_valid_pred_dbname) {
3283
            $dbname = $_POST['pred_dbname'];
3284
            // If dbname contains only one database.
3285
            if (count($dbname) === 1) {
3286
                $dbname = $dbname[0];
3287
            }
3288
        } elseif (isset($is_valid_dbname) && $is_valid_dbname) {
3289
            $dbname = $_REQUEST['dbname'];
3290
        } else {
3291
            unset($dbname, $tablename);
3292
        }
3293
3294
        if (isset($dbname)) {
3295
            if (is_array($dbname)) {
3296
                $db_and_table = $dbname;
3297
                foreach ($db_and_table as $key => $db_name) {
3298
                    $db_and_table[$key] .= '.';
3299
                }
3300
            } else {
3301
                $unescaped_db = Util::unescapeMysqlWildcards($dbname);
3302
                $db_and_table = Util::backquote($unescaped_db) . '.';
3303
            }
3304
            if (isset($tablename)) {
3305
                $db_and_table .= Util::backquote($tablename);
3306
            } else {
3307
                if (is_array($db_and_table)) {
3308
                    foreach ($db_and_table as $key => $db_name) {
3309
                        $db_and_table[$key] .= '*';
3310
                    }
3311
                } else {
3312
                    $db_and_table .= '*';
3313
                }
3314
            }
3315
        } else {
3316
            $db_and_table = '*.*';
3317
        }
3318
3319
        // check if given $dbname is a wildcard or not
3320
        if (isset($dbname)) {
3321
            //if (preg_match('/\\\\(?:_|%)/i', $dbname)) {
3322
            if (! is_array($dbname) && preg_match('/(?<!\\\\)(?:_|%)/', $dbname)) {
3323
                $dbname_is_wildcard = true;
3324
            } else {
3325
                $dbname_is_wildcard = false;
3326
            }
3327
        }
3328
3329
        return [
3330
            $username,
3331
            $hostname,
3332
            isset($dbname) ? $dbname : null,
3333
            isset($tablename) ? $tablename : null,
3334
            isset($routinename) ? $routinename : null,
3335
            $db_and_table,
3336
            $dbname_is_wildcard,
3337
        ];
3338
    }
3339
3340
    /**
3341
     * Get title and textarea for export user definition in Privileges
3342
     *
3343
     * @param string $username username
3344
     * @param string $hostname host name
3345
     *
3346
     * @return array ($title, $export)
3347
     */
3348
    public function getListForExportUserDefinition($username, $hostname)
3349
    {
3350
        $export = '<textarea class="export" cols="60" rows="15">';
3351
3352
        if (isset($_POST['selected_usr'])) {
3353
            // export privileges for selected users
3354
            $title = __('Privileges');
3355
3356
            //For removing duplicate entries of users
3357
            $_POST['selected_usr'] = array_unique($_POST['selected_usr']);
3358
3359
            foreach ($_POST['selected_usr'] as $export_user) {
3360
                $export_username = mb_substr(
3361
                    $export_user,
3362
                    0,
3363
                    mb_strpos($export_user, '&')
3364
                );
3365
                $export_hostname = mb_substr(
3366
                    $export_user,
3367
                    mb_strrpos($export_user, ';') + 1
3368
                );
3369
                $export .= '# '
3370
                    . sprintf(
3371
                        __('Privileges for %s'),
3372
                        '`' . htmlspecialchars($export_username)
3373
                        . '`@`' . htmlspecialchars($export_hostname) . '`'
3374
                    )
3375
                    . "\n\n";
3376
                $export .= $this->getGrants($export_username, $export_hostname) . "\n";
3377
            }
3378
        } else {
3379
            // export privileges for a single user
3380
            $title = __('User') . ' `' . htmlspecialchars($username)
3381
                . '`@`' . htmlspecialchars($hostname) . '`';
3382
            $export .= $this->getGrants($username, $hostname);
3383
        }
3384
        // remove trailing whitespace
3385
        $export = trim($export);
3386
3387
        $export .= '</textarea>';
3388
3389
        return [
3390
            $title,
3391
            $export,
3392
        ];
3393
    }
3394
3395
    /**
3396
     * Get HTML for display Add userfieldset
3397
     *
3398
     * @param string $db    the database
3399
     * @param string $table the table name
3400
     *
3401
     * @return string html output
3402
     */
3403
    public function getAddUserHtmlFieldset($db = '', $table = '')
3404
    {
3405
        if (! $GLOBALS['is_createuser']) {
3406
            return '';
3407
        }
3408
        $rel_params = [];
3409
        $url_params = [
3410
            'adduser' => 1,
3411
        ];
3412
        if (! empty($db)) {
3413
            $url_params['dbname']
3414
                = $rel_params['checkprivsdb']
3415
                    = $db;
3416
        }
3417
        if (! empty($table)) {
3418
            $url_params['tablename']
3419
                = $rel_params['checkprivstable']
3420
                    = $table;
3421
        }
3422
3423
        return $this->template->render('server/privileges/add_user_fieldset', [
3424
            'url_params' => $url_params,
3425
            'rel_params' => $rel_params,
3426
        ]);
3427
    }
3428
3429
    /**
3430
     * Get HTML snippet for display user overview page
3431
     *
3432
     * @param string $pmaThemeImage a image source link
3433
     * @param string $text_dir      text directory
3434
     *
3435
     * @return string
3436
     */
3437
    public function getHtmlForUserOverview($pmaThemeImage, $text_dir)
3438
    {
3439
        global $is_createuser;
3440
3441
        $password_column = 'Password';
3442
        $server_type = Util::getServerType();
3443
        $serverVersion = $this->dbi->getVersion();
3444
        if (($server_type == 'MySQL' || $server_type == 'Percona Server')
3445
            && $serverVersion >= 50706
3446
        ) {
3447
            $password_column = 'authentication_string';
3448
        }
3449
        // $sql_query is for the initial-filtered,
3450
        // $sql_query_all is for counting the total no. of users
3451
3452
        $sql_query = $sql_query_all = 'SELECT *,' .
3453
            " IF(`" . $password_column . "` = _latin1 '', 'N', 'Y') AS 'Password'" .
3454
            ' FROM `mysql`.`user`';
3455
3456
        $sql_query .= (isset($_GET['initial'])
3457
            ? $this->rangeOfUsers($_GET['initial'])
3458
            : '');
3459
3460
        $sql_query .= ' ORDER BY `User` ASC, `Host` ASC;';
3461
        $sql_query_all .= ' ;';
3462
3463
        $res = $this->dbi->tryQuery(
3464
            $sql_query,
3465
            DatabaseInterface::CONNECT_USER,
3466
            DatabaseInterface::QUERY_STORE
3467
        );
3468
        $res_all = $this->dbi->tryQuery(
3469
            $sql_query_all,
3470
            DatabaseInterface::CONNECT_USER,
3471
            DatabaseInterface::QUERY_STORE
3472
        );
3473
3474
        $errorMessages = '';
3475
        if (! $res) {
3476
            // the query failed! This may have two reasons:
3477
            // - the user does not have enough privileges
3478
            // - the privilege tables use a structure of an earlier version.
3479
            // so let's try a more simple query
3480
3481
            $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

3481
            $this->dbi->freeResult(/** @scrutinizer ignore-type */ $res);
Loading history...
3482
            $this->dbi->freeResult($res_all);
3483
            $sql_query = 'SELECT * FROM `mysql`.`user`';
3484
            $res = $this->dbi->tryQuery(
3485
                $sql_query,
3486
                DatabaseInterface::CONNECT_USER,
3487
                DatabaseInterface::QUERY_STORE
3488
            );
3489
3490
            if (! $res) {
3491
                $errorMessages .= $this->getHtmlForViewUsersError();
3492
                $errorMessages .= $this->getAddUserHtmlFieldset();
3493
            } else {
3494
                // This message is hardcoded because I will replace it by
3495
                // a automatic repair feature soon.
3496
                $raw = 'Your privilege table structure seems to be older than'
3497
                    . ' this MySQL version!<br>'
3498
                    . 'Please run the <code>mysql_upgrade</code> command'
3499
                    . ' that should be included in your MySQL server distribution'
3500
                    . ' to solve this problem!';
3501
                $errorMessages .= Message::rawError($raw)->getDisplay();
3502
            }
3503
            $this->dbi->freeResult($res);
3504
        } else {
3505
            $db_rights = $this->getDbRightsForUserOverview();
3506
            // for all initials, even non A-Z
3507
            $array_initials = [];
3508
3509
            foreach ($db_rights as $right) {
3510
                foreach ($right as $account) {
3511
                    if (empty($account['User']) && $account['Host'] == 'localhost') {
3512
                        $emptyUserNotice = Message::notice(
3513
                            __(
3514
                                'A user account allowing any user from localhost to '
3515
                                . 'connect is present. This will prevent other users '
3516
                                . 'from connecting if the host part of their account '
3517
                                . 'allows a connection from any (%) host.'
3518
                            )
3519
                            . Util::showMySQLDocu('problems-connecting')
3520
                        )->getDisplay();
3521
                        break 2;
3522
                    }
3523
                }
3524
            }
3525
3526
            /**
3527
             * Displays the initials
3528
             * Also not necessary if there is less than 20 privileges
3529
             */
3530
            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

3530
            if ($this->dbi->numRows(/** @scrutinizer ignore-type */ $res_all) > 20) {
Loading history...
3531
                $initials = $this->getHtmlForInitials($array_initials);
3532
            }
3533
3534
            /**
3535
            * Display the user overview
3536
            * (if less than 50 users, display them immediately)
3537
            */
3538
            if (isset($_GET['initial'])
3539
                || isset($_GET['showall'])
3540
                || $this->dbi->numRows($res) < 50
3541
            ) {
3542
                $usersOverview = $this->getUsersOverview(
3543
                    $res,
3544
                    $db_rights,
3545
                    $pmaThemeImage,
3546
                    $text_dir
3547
                );
3548
            }
3549
3550
            $response = Response::getInstance();
3551
            if (! $response->isAjax()
3552
                || ! empty($_REQUEST['ajax_page_request'])
3553
            ) {
3554
                if ($GLOBALS['is_reload_priv']) {
3555
                    $flushnote = new Message(
3556
                        __(
3557
                            'Note: phpMyAdmin gets the users’ privileges directly '
3558
                            . 'from MySQL’s privilege tables. The content of these '
3559
                            . 'tables may differ from the privileges the server uses, '
3560
                            . 'if they have been changed manually. In this case, '
3561
                            . 'you should %sreload the privileges%s before you continue.'
3562
                        ),
3563
                        Message::NOTICE
3564
                    );
3565
                    $flushnote->addParamHtml(
3566
                        '<a href="' . Url::getFromRoute('/server/privileges', ['flush_privileges' => 1])
3567
                        . '" id="reload_privileges_anchor">'
3568
                    );
3569
                    $flushnote->addParamHtml('</a>');
3570
                } else {
3571
                    $flushnote = new Message(
3572
                        __(
3573
                            'Note: phpMyAdmin gets the users’ privileges directly '
3574
                            . 'from MySQL’s privilege tables. The content of these '
3575
                            . 'tables may differ from the privileges the server uses, '
3576
                            . 'if they have been changed manually. In this case, '
3577
                            . 'the privileges have to be reloaded but currently, you '
3578
                            . 'don\'t have the RELOAD privilege.'
3579
                        )
3580
                        . Util::showMySQLDocu(
3581
                            'privileges-provided',
3582
                            false,
3583
                            null,
3584
                            null,
3585
                            'priv_reload'
3586
                        ),
3587
                        Message::NOTICE
3588
                    );
3589
                }
3590
                $flushNotice = $flushnote->getDisplay();
3591
            }
3592
        }
3593
3594
        return $this->template->render('server/privileges/user_overview', [
3595
            'error_messages' => $errorMessages,
3596
            'empty_user_notice' => $emptyUserNotice ?? '',
3597
            'initials' => $initials ?? '',
3598
            'users_overview' => $usersOverview ?? '',
3599
            'is_createuser' => $is_createuser,
3600
            'flush_notice' => $flushNotice ?? '',
3601
        ]);
3602
    }
3603
3604
    /**
3605
     * Get HTML snippet for display user properties
3606
     *
3607
     * @param boolean      $dbname_is_wildcard whether database name is wildcard or not
3608
     * @param string       $url_dbname         url database name that urlencode() string
3609
     * @param string       $username           username
3610
     * @param string       $hostname           host name
3611
     * @param string|array $dbname             database name
3612
     * @param string       $tablename          table name
3613
     *
3614
     * @return string
3615
     */
3616
    public function getHtmlForUserProperties(
3617
        $dbname_is_wildcard,
3618
        $url_dbname,
3619
        $username,
3620
        $hostname,
3621
        $dbname,
3622
        $tablename
3623
    ) {
3624
        $sql = "SELECT '1' FROM `mysql`.`user`"
3625
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
3626
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';";
3627
3628
        $user_does_not_exists = (bool) ! $this->dbi->fetchValue($sql);
3629
3630
        $loginInformationFields = '';
3631
        if ($user_does_not_exists) {
3632
            $loginInformationFields = $this->getHtmlForLoginInformationFields();
3633
        }
3634
3635
        $_params = [
3636
            'username' => $username,
3637
            'hostname' => $hostname,
3638
        ];
3639
        if (! is_array($dbname) && strlen($dbname) > 0) {
3640
            $_params['dbname'] = $dbname;
3641
            if (strlen($tablename) > 0) {
3642
                $_params['tablename'] = $tablename;
3643
            }
3644
        } else {
3645
            $_params['dbname'] = $dbname;
3646
        }
3647
3648
        $privilegesTable = $this->getHtmlToDisplayPrivilegesTable(
3649
            // If $dbname is an array, pass any one db as all have same privs.
3650
            Core::ifSetOr($dbname, is_array($dbname) ? $dbname[0] : '*', 'length'),
3651
            Core::ifSetOr($tablename, '*', 'length')
3652
        );
3653
3654
        $tableSpecificRights = '';
3655
        if (! is_array($dbname) && strlen($tablename) === 0
3656
            && empty($dbname_is_wildcard)
3657
        ) {
3658
            // no table name was given, display all table specific rights
3659
            // but only if $dbname contains no wildcards
3660
            if (strlen($dbname) === 0) {
3661
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3662
                    $username,
3663
                    $hostname,
3664
                    'database'
3665
                );
3666
            } else {
3667
                // unescape wildcards in dbname at table level
3668
                $unescaped_db = Util::unescapeMysqlWildcards($dbname);
3669
3670
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3671
                    $username,
3672
                    $hostname,
3673
                    'table',
3674
                    $unescaped_db
3675
                );
3676
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3677
                    $username,
3678
                    $hostname,
3679
                    'routine',
3680
                    $unescaped_db
3681
                );
3682
            }
3683
        }
3684
3685
        // Provide a line with links to the relevant database and table
3686
        $linkToDatabaseAndTable = '';
3687
        if (! is_array($dbname) && strlen($dbname) > 0 && empty($dbname_is_wildcard)) {
3688
            $linkToDatabaseAndTable = $this->getLinkToDbAndTable($url_dbname, $dbname, $tablename);
3689
        }
3690
3691
        $changePassword = '';
3692
        $changeLoginInformation = '';
3693
        if (! is_array($dbname) && strlen($dbname) === 0 && ! $user_does_not_exists) {
3694
            //change login information
3695
            $changePassword = ChangePassword::getHtml(
3696
                'edit_other',
3697
                $username,
3698
                $hostname
3699
            );
3700
            $changeLoginInformation = $this->getChangeLoginInformationHtmlForm($username, $hostname);
3701
        }
3702
3703
        return $this->template->render('server/privileges/user_properties', [
3704
            'user_does_not_exists' => $user_does_not_exists,
3705
            'login_information_fields' => $loginInformationFields,
3706
            'params' => $_params,
3707
            'privileges_table' => $privilegesTable,
3708
            'table_specific_rights' => $tableSpecificRights,
3709
            'link_to_database_and_table' => $linkToDatabaseAndTable,
3710
            'change_password' => $changePassword,
3711
            'change_login_information' => $changeLoginInformation,
3712
            'database' => $dbname,
3713
            'dbname' => $url_dbname,
3714
            'username' => $username,
3715
            'hostname' => $hostname,
3716
            'is_databases' => $dbname_is_wildcard || is_array($dbname) && count($dbname) > 1,
3717
            'table' => $tablename,
3718
            'current_user' => $this->dbi->getCurrentUser(),
3719
        ]);
3720
    }
3721
3722
    /**
3723
     * Get queries for Table privileges to change or copy user
3724
     *
3725
     * @param string $user_host_condition user host condition to
3726
     *                                    select relevant table privileges
3727
     * @param array  $queries             queries array
3728
     * @param string $username            username
3729
     * @param string $hostname            host name
3730
     *
3731
     * @return array
3732
     */
3733
    public function getTablePrivsQueriesForChangeOrCopyUser(
3734
        $user_host_condition,
3735
        array $queries,
3736
        $username,
3737
        $hostname
3738
    ) {
3739
        $res = $this->dbi->query(
3740
            'SELECT `Db`, `Table_name`, `Table_priv` FROM `mysql`.`tables_priv`'
3741
            . $user_host_condition,
3742
            DatabaseInterface::CONNECT_USER,
3743
            DatabaseInterface::QUERY_STORE
3744
        );
3745
        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

3745
        while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $res)) {
Loading history...
3746
            $res2 = $this->dbi->query(
3747
                'SELECT `Column_name`, `Column_priv`'
3748
                . ' FROM `mysql`.`columns_priv`'
3749
                . ' WHERE `User`'
3750
                . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . "'"
3751
                . ' AND `Host`'
3752
                . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . '\''
3753
                . ' AND `Db`'
3754
                . ' = \'' . $this->dbi->escapeString($row['Db']) . "'"
3755
                . ' AND `Table_name`'
3756
                . ' = \'' . $this->dbi->escapeString($row['Table_name']) . "'"
3757
                . ';',
3758
                DatabaseInterface::CONNECT_USER,
3759
                DatabaseInterface::QUERY_STORE
3760
            );
3761
3762
            $tmp_privs1 = $this->extractPrivInfo($row);
3763
            $tmp_privs2 = [
3764
                'Select' => [],
3765
                'Insert' => [],
3766
                'Update' => [],
3767
                'References' => [],
3768
            ];
3769
3770
            while ($row2 = $this->dbi->fetchAssoc($res2)) {
3771
                $tmp_array = explode(',', $row2['Column_priv']);
3772
                if (in_array('Select', $tmp_array)) {
3773
                    $tmp_privs2['Select'][] = $row2['Column_name'];
3774
                }
3775
                if (in_array('Insert', $tmp_array)) {
3776
                    $tmp_privs2['Insert'][] = $row2['Column_name'];
3777
                }
3778
                if (in_array('Update', $tmp_array)) {
3779
                    $tmp_privs2['Update'][] = $row2['Column_name'];
3780
                }
3781
                if (in_array('References', $tmp_array)) {
3782
                    $tmp_privs2['References'][] = $row2['Column_name'];
3783
                }
3784
            }
3785
            if (count($tmp_privs2['Select']) > 0 && ! in_array('SELECT', $tmp_privs1)) {
3786
                $tmp_privs1[] = 'SELECT (`' . implode('`, `', $tmp_privs2['Select']) . '`)';
3787
            }
3788
            if (count($tmp_privs2['Insert']) > 0 && ! in_array('INSERT', $tmp_privs1)) {
3789
                $tmp_privs1[] = 'INSERT (`' . implode('`, `', $tmp_privs2['Insert']) . '`)';
3790
            }
3791
            if (count($tmp_privs2['Update']) > 0 && ! in_array('UPDATE', $tmp_privs1)) {
3792
                $tmp_privs1[] = 'UPDATE (`' . implode('`, `', $tmp_privs2['Update']) . '`)';
3793
            }
3794
            if (count($tmp_privs2['References']) > 0
3795
                && ! in_array('REFERENCES', $tmp_privs1)
3796
            ) {
3797
                $tmp_privs1[]
3798
                    = 'REFERENCES (`' . implode('`, `', $tmp_privs2['References']) . '`)';
3799
            }
3800
3801
            $queries[] = 'GRANT ' . implode(', ', $tmp_privs1)
3802
                . ' 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

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

3803
                . /** @scrutinizer ignore-type */ Util::backquote($row['Table_name'])
Loading history...
3804
                . ' TO \'' . $this->dbi->escapeString($username)
3805
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\''
3806
                . (in_array('Grant', explode(',', $row['Table_priv']))
3807
                ? ' WITH GRANT OPTION;'
3808
                : ';');
3809
        }
3810
        return $queries;
3811
    }
3812
3813
    /**
3814
     * Get queries for database specific privileges for change or copy user
3815
     *
3816
     * @param array  $queries  queries array with string
3817
     * @param string $username username
3818
     * @param string $hostname host name
3819
     *
3820
     * @return array
3821
     */
3822
    public function getDbSpecificPrivsQueriesForChangeOrCopyUser(
3823
        array $queries,
3824
        $username,
3825
        $hostname
3826
    ) {
3827
        $user_host_condition = ' WHERE `User`'
3828
            . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . "'"
3829
            . ' AND `Host`'
3830
            . ' = \'' . $this->dbi->escapeString($_POST['old_hostname']) . '\';';
3831
3832
        $res = $this->dbi->query(
3833
            'SELECT * FROM `mysql`.`db`' . $user_host_condition
3834
        );
3835
3836
        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

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

3838
                . ' ON ' . /** @scrutinizer ignore-type */ Util::backquote($row['Db']) . '.*'
Loading history...
3839
                . ' TO \'' . $this->dbi->escapeString($username)
3840
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\''
3841
                . ($row['Grant_priv'] == 'Y' ? ' WITH GRANT OPTION;' : ';');
3842
        }
3843
        $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

3843
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $res);
Loading history...
3844
3845
        $queries = $this->getTablePrivsQueriesForChangeOrCopyUser(
3846
            $user_host_condition,
3847
            $queries,
3848
            $username,
3849
            $hostname
3850
        );
3851
3852
        return $queries;
3853
    }
3854
3855
    /**
3856
     * Prepares queries for adding users and
3857
     * also create database and return query and message
3858
     *
3859
     * @param boolean $_error               whether user create or not
3860
     * @param string  $real_sql_query       SQL query for add a user
3861
     * @param string  $sql_query            SQL query to be displayed
3862
     * @param string  $username             username
3863
     * @param string  $hostname             host name
3864
     * @param string  $dbname               database name
3865
     * @param string  $alter_real_sql_query SQL query for ALTER USER
3866
     * @param string  $alter_sql_query      SQL query for ALTER USER to be displayed
3867
     *
3868
     * @return array, $message
3869
     */
3870
    public function addUserAndCreateDatabase(
3871
        $_error,
3872
        $real_sql_query,
3873
        $sql_query,
3874
        $username,
3875
        $hostname,
3876
        $dbname,
3877
        $alter_real_sql_query,
3878
        $alter_sql_query
3879
    ) {
3880
        if ($_error || (! empty($real_sql_query)
3881
            && ! $this->dbi->tryQuery($real_sql_query))
3882
        ) {
3883
            $_POST['createdb-1'] = $_POST['createdb-2']
3884
                = $_POST['createdb-3'] = null;
3885
            $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

3885
            $message = Message::rawError(/** @scrutinizer ignore-type */ $this->dbi->getError());
Loading history...
3886
        } elseif ($alter_real_sql_query !== '' && ! $this->dbi->tryQuery($alter_real_sql_query)) {
3887
            $_POST['createdb-1'] = $_POST['createdb-2']
3888
                = $_POST['createdb-3'] = null;
3889
            $message = Message::rawError($this->dbi->getError());
3890
        } else {
3891
            $sql_query .= $alter_sql_query;
3892
            $message = Message::success(__('You have added a new user.'));
3893
        }
3894
3895
        if (isset($_POST['createdb-1'])) {
3896
            // Create database with same name and grant all privileges
3897
            $q = 'CREATE DATABASE IF NOT EXISTS '
3898
                . Util::backquote(
3899
                    $this->dbi->escapeString($username)
3900
                ) . ';';
3901
            $sql_query .= $q;
3902
            if (! $this->dbi->tryQuery($q)) {
3903
                $message = Message::rawError($this->dbi->getError());
3904
            }
3905
3906
            /**
3907
             * Reload the navigation
3908
             */
3909
            $GLOBALS['reload'] = true;
3910
            $GLOBALS['db'] = $username;
3911
3912
            $q = 'GRANT ALL PRIVILEGES ON '
3913
                . Util::backquote(
3914
                    Util::escapeMysqlWildcards(
3915
                        $this->dbi->escapeString($username)
3916
                    )
3917
                ) . '.* TO \''
3918
                . $this->dbi->escapeString($username)
3919
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
3920
            $sql_query .= $q;
3921
            if (! $this->dbi->tryQuery($q)) {
3922
                $message = Message::rawError($this->dbi->getError());
3923
            }
3924
        }
3925
3926
        if (isset($_POST['createdb-2'])) {
3927
            // Grant all privileges on wildcard name (username\_%)
3928
            $q = 'GRANT ALL PRIVILEGES ON '
3929
                . Util::backquote(
3930
                    Util::escapeMysqlWildcards(
3931
                        $this->dbi->escapeString($username)
3932
                    ) . '\_%'
3933
                ) . '.* TO \''
3934
                . $this->dbi->escapeString($username)
3935
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
3936
            $sql_query .= $q;
3937
            if (! $this->dbi->tryQuery($q)) {
3938
                $message = Message::rawError($this->dbi->getError());
3939
            }
3940
        }
3941
3942
        if (isset($_POST['createdb-3'])) {
3943
            // Grant all privileges on the specified database to the new user
3944
            $q = 'GRANT ALL PRIVILEGES ON '
3945
            . Util::backquote(
3946
                $this->dbi->escapeString($dbname)
3947
            ) . '.* TO \''
3948
            . $this->dbi->escapeString($username)
3949
            . '\'@\'' . $this->dbi->escapeString($hostname) . '\';';
3950
            $sql_query .= $q;
3951
            if (! $this->dbi->tryQuery($q)) {
3952
                $message = Message::rawError($this->dbi->getError());
3953
            }
3954
        }
3955
        return [
3956
            $sql_query,
3957
            $message,
3958
        ];
3959
    }
3960
3961
    /**
3962
     * Get the hashed string for password
3963
     *
3964
     * @param string $password password
3965
     *
3966
     * @return string
3967
     */
3968
    public function getHashedPassword($password)
3969
    {
3970
        $password = $this->dbi->escapeString($password);
3971
        $result = $this->dbi->fetchSingleRow(
3972
            "SELECT PASSWORD('" . $password . "') AS `password`;"
3973
        );
3974
3975
        return $result['password'];
3976
    }
3977
3978
    /**
3979
     * Check if MariaDB's 'simple_password_check'
3980
     * OR 'cracklib_password_check' is ACTIVE
3981
     *
3982
     * @return boolean if atleast one of the plugins is ACTIVE
3983
     */
3984
    public function checkIfMariaDBPwdCheckPluginActive()
3985
    {
3986
        $serverVersion = $this->dbi->getVersion();
3987
        if (! (Util::getServerType() == 'MariaDB' && $serverVersion >= 100002)) {
3988
            return false;
3989
        }
3990
3991
        $result = $this->dbi->tryQuery(
3992
            'SHOW PLUGINS SONAME LIKE \'%_password_check%\''
3993
        );
3994
3995
        /* Plugins are not working, for example directory does not exists */
3996
        if ($result === false) {
3997
            return false;
3998
        }
3999
4000
        while ($row = $this->dbi->fetchAssoc($result)) {
4001
            if ($row['Status'] === 'ACTIVE') {
4002
                return true;
4003
            }
4004
        }
4005
4006
        return false;
4007
    }
4008
4009
4010
    /**
4011
     * Get SQL queries for Display and Add user
4012
     *
4013
     * @param string $username username
4014
     * @param string $hostname host name
4015
     * @param string $password password
4016
     *
4017
     * @return array ($create_user_real, $create_user_show, $real_sql_query, $sql_query
4018
     *                $password_set_real, $password_set_show, $alter_real_sql_query, $alter_sql_query)
4019
     */
4020
    public function getSqlQueriesForDisplayAndAddUser($username, $hostname, $password)
4021
    {
4022
        $slashedUsername = $this->dbi->escapeString($username);
4023
        $slashedHostname = $this->dbi->escapeString($hostname);
4024
        $slashedPassword = $this->dbi->escapeString($password);
4025
        $serverType = Util::getServerType();
4026
        $serverVersion = $this->dbi->getVersion();
4027
4028
        $create_user_stmt = sprintf(
4029
            'CREATE USER \'%s\'@\'%s\'',
4030
            $slashedUsername,
4031
            $slashedHostname
4032
        );
4033
        $isMariaDBPwdPluginActive = $this->checkIfMariaDBPwdCheckPluginActive();
4034
4035
        // See https://github.com/phpmyadmin/phpmyadmin/pull/11560#issuecomment-147158219
4036
        // for details regarding details of syntax usage for various versions
4037
4038
        // 'IDENTIFIED WITH auth_plugin'
4039
        // is supported by MySQL 5.5.7+
4040
        if (($serverType == 'MySQL' || $serverType == 'Percona Server')
4041
            && $serverVersion >= 50507
4042
            && isset($_POST['authentication_plugin'])
4043
        ) {
4044
            $create_user_stmt .= ' IDENTIFIED WITH '
4045
                . $_POST['authentication_plugin'];
4046
        }
4047
4048
        // 'IDENTIFIED VIA auth_plugin'
4049
        // is supported by MariaDB 5.2+
4050
        if ($serverType == 'MariaDB'
4051
            && $serverVersion >= 50200
4052
            && isset($_POST['authentication_plugin'])
4053
            && ! $isMariaDBPwdPluginActive
4054
        ) {
4055
            $create_user_stmt .= ' IDENTIFIED VIA '
4056
                . $_POST['authentication_plugin'];
4057
        }
4058
4059
        $create_user_real = $create_user_stmt;
4060
        $create_user_show = $create_user_stmt;
4061
4062
        $password_set_stmt = 'SET PASSWORD FOR \'%s\'@\'%s\' = \'%s\'';
4063
        $password_set_show = sprintf(
4064
            $password_set_stmt,
4065
            $slashedUsername,
4066
            $slashedHostname,
4067
            '***'
4068
        );
4069
4070
        $sql_query_stmt = sprintf(
4071
            'GRANT %s ON *.* TO \'%s\'@\'%s\'',
4072
            implode(', ', $this->extractPrivInfo()),
4073
            $slashedUsername,
4074
            $slashedHostname
4075
        );
4076
        $real_sql_query = $sql_query = $sql_query_stmt;
4077
4078
        // Set the proper hashing method
4079
        if (isset($_POST['authentication_plugin'])) {
4080
            $this->setProperPasswordHashing(
4081
                $_POST['authentication_plugin']
4082
            );
4083
        }
4084
4085
        // Use 'CREATE USER ... WITH ... AS ..' syntax for
4086
        // newer MySQL versions
4087
        // and 'CREATE USER ... VIA .. USING ..' syntax for
4088
        // newer MariaDB versions
4089
        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...
4090
            && $serverVersion >= 50706)
4091
            || ($serverType == 'MariaDB'
4092
            && $serverVersion >= 50200)
4093
        ) {
4094
            $password_set_real = null;
4095
4096
            // Required for binding '%' with '%s'
4097
            $create_user_stmt = str_replace(
4098
                '%',
4099
                '%%',
4100
                $create_user_stmt
4101
            );
4102
4103
            // MariaDB uses 'USING' whereas MySQL uses 'AS'
4104
            // but MariaDB with validation plugin needs cleartext password
4105
            if ($serverType == 'MariaDB'
4106
                && ! $isMariaDBPwdPluginActive
4107
            ) {
4108
                $create_user_stmt .= ' USING \'%s\'';
4109
            } elseif ($serverType == 'MariaDB') {
4110
                $create_user_stmt .= ' IDENTIFIED BY \'%s\'';
4111
            } elseif (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) {
4112
                $create_user_stmt .= ' BY \'%s\'';
4113
            } else {
4114
                $create_user_stmt .= ' AS \'%s\'';
4115
            }
4116
4117
            if ($_POST['pred_password'] == 'keep') {
4118
                $create_user_real = sprintf(
4119
                    $create_user_stmt,
4120
                    $slashedPassword
4121
                );
4122
                $create_user_show = sprintf(
4123
                    $create_user_stmt,
4124
                    '***'
4125
                );
4126
            } elseif ($_POST['pred_password'] == 'none') {
4127
                $create_user_real = sprintf(
4128
                    $create_user_stmt,
4129
                    null
4130
                );
4131
                $create_user_show = sprintf(
4132
                    $create_user_stmt,
4133
                    '***'
4134
                );
4135
            } else {
4136
                if (! (($serverType == 'MariaDB' && $isMariaDBPwdPluginActive)
4137
                    || ($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011)) {
4138
                    $hashedPassword = $this->getHashedPassword($_POST['pma_pw']);
4139
                } else {
4140
                    // MariaDB with validation plugin needs cleartext password
4141
                    $hashedPassword = $_POST['pma_pw'];
4142
                }
4143
                $create_user_real = sprintf(
4144
                    $create_user_stmt,
4145
                    $hashedPassword
4146
                );
4147
                $create_user_show = sprintf(
4148
                    $create_user_stmt,
4149
                    '***'
4150
                );
4151
            }
4152
        } else {
4153
            // Use 'SET PASSWORD' syntax for pre-5.7.6 MySQL versions
4154
            // and pre-5.2.0 MariaDB versions
4155
            if ($_POST['pred_password'] == 'keep') {
4156
                $password_set_real = sprintf(
4157
                    $password_set_stmt,
4158
                    $slashedUsername,
4159
                    $slashedHostname,
4160
                    $slashedPassword
4161
                );
4162
            } elseif ($_POST['pred_password'] == 'none') {
4163
                $password_set_real = sprintf(
4164
                    $password_set_stmt,
4165
                    $slashedUsername,
4166
                    $slashedHostname,
4167
                    null
4168
                );
4169
            } else {
4170
                $hashedPassword = $this->getHashedPassword($_POST['pma_pw']);
4171
                $password_set_real = sprintf(
4172
                    $password_set_stmt,
4173
                    $slashedUsername,
4174
                    $slashedHostname,
4175
                    $hashedPassword
4176
                );
4177
            }
4178
        }
4179
4180
        $alter_real_sql_query = '';
4181
        $alter_sql_query = '';
4182
        if (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) {
4183
            $sql_query_stmt = '';
4184
            if ((isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y')
4185
                || (isset($GLOBALS['Grant_priv']) && $GLOBALS['Grant_priv'] == 'Y')
4186
            ) {
4187
                $sql_query_stmt = ' WITH GRANT OPTION';
4188
            }
4189
            $real_sql_query .= $sql_query_stmt;
4190
            $sql_query .= $sql_query_stmt;
4191
4192
            $alter_sql_query_stmt = sprintf(
4193
                'ALTER USER \'%s\'@\'%s\'',
4194
                $slashedUsername,
4195
                $slashedHostname
4196
            );
4197
            $alter_real_sql_query = $alter_sql_query_stmt;
4198
            $alter_sql_query = $alter_sql_query_stmt;
4199
        }
4200
4201
        // add REQUIRE clause
4202
        $require_clause = $this->getRequireClause();
4203
        $with_clause = $this->getWithClauseForAddUserAndUpdatePrivs();
4204
4205
        if (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) {
4206
            $alter_real_sql_query .= $require_clause;
4207
            $alter_sql_query .= $require_clause;
4208
            $alter_real_sql_query .= $with_clause;
4209
            $alter_sql_query .= $with_clause;
4210
        } else {
4211
            $real_sql_query .= $require_clause;
4212
            $sql_query .= $require_clause;
4213
            $real_sql_query .= $with_clause;
4214
            $sql_query .= $with_clause;
4215
        }
4216
4217
        if ($alter_real_sql_query !== '') {
4218
            $alter_real_sql_query .= ';';
4219
            $alter_sql_query .= ';';
4220
        }
4221
        $create_user_real .= ';';
4222
        $create_user_show .= ';';
4223
        $real_sql_query .= ';';
4224
        $sql_query .= ';';
4225
        // No Global GRANT_OPTION privilege
4226
        if (! $GLOBALS['is_grantuser']) {
4227
            $real_sql_query = '';
4228
            $sql_query = '';
4229
        }
4230
4231
        // Use 'SET PASSWORD' for pre-5.7.6 MySQL versions
4232
        // and pre-5.2.0 MariaDB
4233
        if (($serverType == 'MySQL'
4234
            && $serverVersion >= 50706)
4235
            || ($serverType == 'MariaDB'
4236
            && $serverVersion >= 50200)
4237
        ) {
4238
            $password_set_real = null;
4239
            $password_set_show = null;
4240
        } else {
4241
            if ($password_set_real !== null) {
4242
                $password_set_real .= ";";
4243
            }
4244
            $password_set_show .= ";";
4245
        }
4246
4247
        return [
4248
            $create_user_real,
4249
            $create_user_show,
4250
            $real_sql_query,
4251
            $sql_query,
4252
            $password_set_real,
4253
            $password_set_show,
4254
            $alter_real_sql_query,
4255
            $alter_sql_query,
4256
        ];
4257
    }
4258
4259
    /**
4260
     * Returns the type ('PROCEDURE' or 'FUNCTION') of the routine
4261
     *
4262
     * @param string $dbname      database
4263
     * @param string $routineName routine
4264
     *
4265
     * @return string type
4266
     */
4267
    public function getRoutineType($dbname, $routineName)
4268
    {
4269
        $routineData = $this->dbi->getRoutines($dbname);
4270
4271
        foreach ($routineData as $routine) {
4272
            if ($routine['name'] === $routineName) {
4273
                return $routine['type'];
4274
            }
4275
        }
4276
        return '';
4277
    }
4278
4279
    /**
4280
     * @param string $username User name
4281
     * @param string $hostname Host name
4282
     * @param string $database Database name
4283
     * @param string $routine  Routine name
4284
     *
4285
     * @return array
4286
     */
4287
    private function getRoutinePrivileges(
4288
        string $username,
4289
        string $hostname,
4290
        string $database,
4291
        string $routine
4292
    ): array {
4293
        $sql = "SELECT `Proc_priv`"
4294
            . " FROM `mysql`.`procs_priv`"
4295
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
4296
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "'"
4297
            . " AND `Db` = '"
4298
            . $this->dbi->escapeString(Util::unescapeMysqlWildcards($database)) . "'"
4299
            . " AND `Routine_name` LIKE '" . $this->dbi->escapeString($routine) . "';";
4300
        $privileges = $this->dbi->fetchValue($sql);
4301
        if ($privileges === false) {
4302
            $privileges = '';
4303
        }
4304
        return $this->parseProcPriv($privileges);
4305
    }
4306
}
4307