Passed
Push — master ( 6ea135...03b0c0 )
by Maurício
07:45
created

getHtmlTableBodyForSpecificDbRoutinePrivs()   A

Complexity

Conditions 6
Paths 17

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1441
        while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $res)) {
Loading history...
1442
            $routines[] = [
1443
                'user' => $row['User'],
1444
                'host' => $row['Host'],
1445
                'name' => $row['Routine_name'],
1446
                'database' => isset($row['Db']) && $row['Db'] != '*' ? $row['Db'] : '',
1447
                'table' => isset($row['Table_name']) && $row['Table_name'] != '*' ? $row['Table_name'] : '',
1448
            ];
1449
        }
1450
1451
        return $this->template->render('server/privileges/routine', [
1452
            'routines' => $routines,
1453
            'is_grantuser' => $is_grantuser,
1454
            'index' => $index_checkbox,
1455
        ]);
1456
    }
1457
1458
    /**
1459
     * Get the HTML for user form and check the privileges for a particular database.
1460
     *
1461
     * @param string $db database name
1462
     *
1463
     * @return string
1464
     */
1465
    public function getHtmlForSpecificDbPrivileges(string $db): string
1466
    {
1467
        global $cfg, $pmaThemeImage, $text_dir, $is_createuser;
1468
1469
        $scriptName = Util::getScriptNameForOption(
1470
            $cfg['DefaultTabDatabase'],
1471
            'database'
1472
        );
1473
1474
        $tableBody = '';
1475
        if ($this->dbi->isSuperuser()) {
1476
            $privMap = $this->getPrivMap($db);
1477
            $tableBody = $this->getHtmlTableBodyForSpecificDbOrTablePrivs($privMap, $db);
1478
        }
1479
1480
        $response = Response::getInstance();
1481
        if ($response->isAjax() === true
1482
            && empty($_REQUEST['ajax_page_request'])
1483
        ) {
1484
            $message = Message::success(__('User has been added.'));
1485
            $response->addJSON('message', $message);
1486
            exit;
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

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

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

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

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

Loading history...
1487
        }
1488
1489
        return $this->template->render('server/privileges/database', [
1490
            'is_superuser' => $this->dbi->isSuperuser(),
1491
            'db' => $db,
1492
            'database_url' => $scriptName,
1493
            'pma_theme_image' => $pmaThemeImage,
1494
            'text_dir' => $text_dir,
1495
            'table_body' => $tableBody,
1496
            'is_createuser' => $is_createuser,
1497
        ]);
1498
    }
1499
1500
    /**
1501
     * Get the HTML for user form and check the privileges for a particular table.
1502
     *
1503
     * @param string $db    database name
1504
     * @param string $table table name
1505
     *
1506
     * @return string
1507
     */
1508
    public function getHtmlForSpecificTablePrivileges(string $db, string $table): string
1509
    {
1510
        global $cfg, $pmaThemeImage, $text_dir, $is_createuser;
1511
1512
        $scriptName = Util::getScriptNameForOption(
1513
            $cfg['DefaultTabTable'],
1514
            'table'
1515
        );
1516
1517
        $tableBody = '';
1518
        if ($this->dbi->isSuperuser()) {
1519
            $privMap = $this->getPrivMap($db);
1520
            $sql_query = "SELECT `User`, `Host`, `Db`,"
1521
                . " 't' AS `Type`, `Table_name`, `Table_priv`"
1522
                . " FROM `mysql`.`tables_priv`"
1523
                . " WHERE '" . $this->dbi->escapeString($db) . "' LIKE `Db`"
1524
                . "     AND '" . $this->dbi->escapeString($table) . "' LIKE `Table_name`"
1525
                . "     AND NOT (`Table_priv` = '' AND Column_priv = '')"
1526
                . " ORDER BY `User` ASC, `Host` ASC, `Db` ASC, `Table_priv` ASC;";
1527
            $res = $this->dbi->query($sql_query);
1528
            $this->mergePrivMapFromResult($privMap, $res);
0 ignored issues
show
Bug introduced by
It seems like $res can also be of type false; however, parameter $result of PhpMyAdmin\Server\Privil...ergePrivMapFromResult() 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

1528
            $this->mergePrivMapFromResult($privMap, /** @scrutinizer ignore-type */ $res);
Loading history...
1529
1530
            $tableBody = $this->getHtmlTableBodyForSpecificDbOrTablePrivs($privMap, $db);
1531
        }
1532
1533
        return $this->template->render('server/privileges/table', [
1534
            'db' => $db,
1535
            'table' => $table,
1536
            'is_superuser' => $this->dbi->isSuperuser(),
1537
            'table_url' => $scriptName,
1538
            'pma_theme_image' => $pmaThemeImage,
1539
            'text_dir' => $text_dir,
1540
            'table_body' => $tableBody,
1541
            'is_createuser' => $is_createuser,
1542
        ]);
1543
    }
1544
1545
    /**
1546
     * gets privilege map
1547
     *
1548
     * @param string $db the database
1549
     *
1550
     * @return array the privilege map
1551
     */
1552
    public function getPrivMap($db)
1553
    {
1554
        list($listOfPrivs, $listOfComparedPrivs)
1555
            = $this->getListOfPrivilegesAndComparedPrivileges();
1556
        $sql_query
1557
            = "("
1558
            . " SELECT " . $listOfPrivs . ", '*' AS `Db`, 'g' AS `Type`"
1559
            . " FROM `mysql`.`user`"
1560
            . " WHERE NOT (" . $listOfComparedPrivs . ")"
1561
            . ")"
1562
            . " UNION "
1563
            . "("
1564
            . " SELECT " . $listOfPrivs . ", `Db`, 'd' AS `Type`"
1565
            . " FROM `mysql`.`db`"
1566
            . " WHERE '" . $this->dbi->escapeString($db) . "' LIKE `Db`"
1567
            . "     AND NOT (" . $listOfComparedPrivs . ")"
1568
            . ")"
1569
            . " ORDER BY `User` ASC, `Host` ASC, `Db` ASC;";
1570
        $res = $this->dbi->query($sql_query);
1571
        $privMap = [];
1572
        $this->mergePrivMapFromResult($privMap, $res);
0 ignored issues
show
Bug introduced by
It seems like $res can also be of type false; however, parameter $result of PhpMyAdmin\Server\Privil...ergePrivMapFromResult() 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

1572
        $this->mergePrivMapFromResult($privMap, /** @scrutinizer ignore-type */ $res);
Loading history...
1573
        return $privMap;
1574
    }
1575
1576
    /**
1577
     * merge privilege map and rows from resultset
1578
     *
1579
     * @param array  $privMap the privilege map reference
1580
     * @param object $result  the resultset of query
1581
     *
1582
     * @return void
1583
     */
1584
    public function mergePrivMapFromResult(array &$privMap, $result)
1585
    {
1586
        while ($row = $this->dbi->fetchAssoc($result)) {
1587
            $user = $row['User'];
1588
            $host = $row['Host'];
1589
            if (! isset($privMap[$user])) {
1590
                $privMap[$user] = [];
1591
            }
1592
            if (! isset($privMap[$user][$host])) {
1593
                $privMap[$user][$host] = [];
1594
            }
1595
            $privMap[$user][$host][] = $row;
1596
        }
1597
    }
1598
1599
    /**
1600
     * Get HTML error for View Users form
1601
     * For non superusers such as grant/create users
1602
     *
1603
     * @return string
1604
     */
1605
    public function getHtmlForViewUsersError()
1606
    {
1607
        return Message::error(
1608
            __('Not enough privilege to view users.')
1609
        )->getDisplay();
1610
    }
1611
1612
    /**
1613
     * Get HTML snippet for table body of specific database or table privileges
1614
     *
1615
     * @param array  $privMap privilege map
1616
     * @param string $db      database
1617
     *
1618
     * @return string
1619
     */
1620
    public function getHtmlTableBodyForSpecificDbOrTablePrivs($privMap, $db)
1621
    {
1622
        $html_output = '<tbody>';
1623
        $index_checkbox = 0;
1624
        if (empty($privMap)) {
1625
            $html_output .= '<tr>'
1626
                . '<td colspan="6">'
1627
                . __('No user found.')
1628
                . '</td>'
1629
                . '</tr>'
1630
                . '</tbody>';
1631
            return $html_output;
1632
        }
1633
1634
        foreach ($privMap as $current_user => $val) {
1635
            foreach ($val as $current_host => $current_privileges) {
1636
                $nbPrivileges = count($current_privileges);
1637
                $html_output .= '<tr>';
1638
1639
                $value = htmlspecialchars($current_user . '&amp;#27;' . $current_host);
1640
                $html_output .= '<td';
1641
                if ($nbPrivileges > 1) {
1642
                    $html_output .= ' rowspan="' . $nbPrivileges . '"';
1643
                }
1644
                $html_output .= '>';
1645
                $html_output .= '<input type="checkbox" class="checkall" '
1646
                    . 'name="selected_usr[]" '
1647
                    . 'id="checkbox_sel_users_' . ($index_checkbox++) . '" '
1648
                    . 'value="' . $value . '"></td>' . "\n";
1649
1650
                // user
1651
                $html_output .= '<td';
1652
                if ($nbPrivileges > 1) {
1653
                    $html_output .= ' rowspan="' . $nbPrivileges . '"';
1654
                }
1655
                $html_output .= '>';
1656
                if (empty($current_user)) {
1657
                    $html_output .= '<span style="color: #FF0000">'
1658
                        . __('Any') . '</span>';
1659
                } else {
1660
                    $html_output .= htmlspecialchars($current_user);
1661
                }
1662
                $html_output .= '</td>';
1663
1664
                // host
1665
                $html_output .= '<td';
1666
                if ($nbPrivileges > 1) {
1667
                    $html_output .= ' rowspan="' . $nbPrivileges . '"';
1668
                }
1669
                $html_output .= '>';
1670
                $html_output .= htmlspecialchars($current_host);
1671
                $html_output .= '</td>';
1672
1673
                $html_output .= $this->getHtmlListOfPrivs(
1674
                    $db,
1675
                    $current_privileges,
1676
                    $current_user,
1677
                    $current_host
1678
                );
1679
            }
1680
        }
1681
1682
        //For fetching routine based privileges
1683
        $html_output .= $this->getHtmlTableBodyForSpecificDbRoutinePrivs($db, $index_checkbox);
1684
        $html_output .= '</tbody>';
1685
1686
        return $html_output;
1687
    }
1688
1689
    /**
1690
     * Get HTML to display privileges
1691
     *
1692
     * @param string $db                 Database name
1693
     * @param array  $current_privileges List of privileges
1694
     * @param string $current_user       Current user
1695
     * @param string $current_host       Current host
1696
     *
1697
     * @return string HTML to display privileges
1698
     */
1699
    public function getHtmlListOfPrivs(
1700
        $db,
1701
        array $current_privileges,
1702
        $current_user,
1703
        $current_host
1704
    ) {
1705
        $nbPrivileges = count($current_privileges);
1706
        $html_output = null;
1707
        for ($i = 0; $i < $nbPrivileges; $i++) {
1708
            $current = $current_privileges[$i];
1709
1710
            // type
1711
            $html_output .= '<td>';
1712
            if ($current['Type'] == 'g') {
1713
                $html_output .= __('global');
1714
            } elseif ($current['Type'] == 'd') {
1715
                if ($current['Db'] == Util::escapeMysqlWildcards($db)) {
1716
                    $html_output .= __('database-specific');
1717
                } else {
1718
                    $html_output .= __('wildcard') . ': '
1719
                        . '<code>'
1720
                        . htmlspecialchars($current['Db'])
1721
                        . '</code>';
1722
                }
1723
            } elseif ($current['Type'] == 't') {
1724
                $html_output .= __('table-specific');
1725
            }
1726
            $html_output .= '</td>';
1727
1728
            // privileges
1729
            $html_output .= '<td>';
1730
            if (isset($current['Table_name'])) {
1731
                $privList = explode(',', $current['Table_priv']);
1732
                $privs = [];
1733
                $grantsArr = $this->getTableGrantsArray();
1734
                foreach ($grantsArr as $grant) {
1735
                    $privs[$grant[0]] = 'N';
1736
                    foreach ($privList as $priv) {
1737
                        if ($grant[0] == $priv) {
1738
                            $privs[$grant[0]] = 'Y';
1739
                        }
1740
                    }
1741
                }
1742
                $html_output .= '<code>'
1743
                    . implode(
1744
                        ',',
1745
                        $this->extractPrivInfo($privs, true, true)
1746
                    )
1747
                    . '</code>';
1748
            } else {
1749
                $html_output .= '<code>'
1750
                    . implode(
1751
                        ',',
1752
                        $this->extractPrivInfo($current, true, false)
1753
                    )
1754
                    . '</code>';
1755
            }
1756
            $html_output .= '</td>';
1757
1758
            // grant
1759
            $html_output .= '<td>';
1760
            $containsGrant = false;
1761
            if (isset($current['Table_name'])) {
1762
                $privList = explode(',', $current['Table_priv']);
1763
                foreach ($privList as $priv) {
1764
                    if ($priv == 'Grant') {
1765
                        $containsGrant = true;
1766
                    }
1767
                }
1768
            } else {
1769
                $containsGrant = $current['Grant_priv'] == 'Y';
1770
            }
1771
            $html_output .= ($containsGrant ? __('Yes') : __('No'));
1772
            $html_output .= '</td>';
1773
1774
            // action
1775
            $html_output .= '<td>';
1776
            $specific_db = isset($current['Db']) && $current['Db'] != '*'
1777
                ? $current['Db'] : '';
1778
            $specific_table = isset($current['Table_name'])
1779
                && $current['Table_name'] != '*'
1780
                ? $current['Table_name'] : '';
1781
            if ($GLOBALS['is_grantuser']) {
1782
                $html_output .= $this->getUserLink(
1783
                    'edit',
1784
                    $current_user,
1785
                    $current_host,
1786
                    $specific_db,
1787
                    $specific_table
1788
                );
1789
            }
1790
            $html_output .= '</td>';
1791
            $html_output .= '<td class="center">'
1792
                . $this->getUserLink(
1793
                    'export',
1794
                    $current_user,
1795
                    $current_host,
1796
                    $specific_db,
1797
                    $specific_table
1798
                )
1799
                . '</td>';
1800
1801
            $html_output .= '</tr>';
1802
            if (($i + 1) < $nbPrivileges) {
1803
                $html_output .= '<tr class="noclick">';
1804
            }
1805
        }
1806
        return $html_output;
1807
    }
1808
1809
    /**
1810
     * Returns edit, revoke or export link for a user.
1811
     *
1812
     * @param string $linktype    The link type (edit | revoke | export)
1813
     * @param string $username    User name
1814
     * @param string $hostname    Host name
1815
     * @param string $dbname      Database name
1816
     * @param string $tablename   Table name
1817
     * @param string $routinename Routine name
1818
     * @param string $initial     Initial value
1819
     *
1820
     * @return string HTML code with link
1821
     */
1822
    public function getUserLink(
1823
        $linktype,
1824
        $username,
1825
        $hostname,
1826
        $dbname = '',
1827
        $tablename = '',
1828
        $routinename = '',
1829
        $initial = ''
1830
    ) {
1831
        $html = '<a';
1832
        switch ($linktype) {
1833
            case 'edit':
1834
                $html .= ' class="edit_user_anchor"';
1835
                break;
1836
            case 'export':
1837
                $html .= ' class="export_user_anchor ajax"';
1838
                break;
1839
        }
1840
        $params = [
1841
            'username' => $username,
1842
            'hostname' => $hostname,
1843
        ];
1844
        switch ($linktype) {
1845
            case 'edit':
1846
                $params['dbname'] = $dbname;
1847
                $params['tablename'] = $tablename;
1848
                $params['routinename'] = $routinename;
1849
                break;
1850
            case 'revoke':
1851
                $params['dbname'] = $dbname;
1852
                $params['tablename'] = $tablename;
1853
                $params['routinename'] = $routinename;
1854
                $params['revokeall'] = 1;
1855
                break;
1856
            case 'export':
1857
                $params['initial'] = $initial;
1858
                $params['export'] = 1;
1859
                break;
1860
        }
1861
1862
        $html .= ' href="' . Url::getFromRoute('/server/privileges');
1863
        if ($linktype == 'revoke') {
1864
            $html .= '" data-post="' . Url::getCommon($params, '');
1865
        } else {
1866
            $html .= Url::getCommon($params, '&');
1867
        }
1868
        $html .= '">';
1869
1870
        switch ($linktype) {
1871
            case 'edit':
1872
                $html .= Util::getIcon('b_usredit', __('Edit privileges'));
1873
                break;
1874
            case 'revoke':
1875
                $html .= Util::getIcon('b_usrdrop', __('Revoke'));
1876
                break;
1877
            case 'export':
1878
                $html .= Util::getIcon('b_tblexport', __('Export'));
1879
                break;
1880
        }
1881
        $html .= '</a>';
1882
1883
        return $html;
1884
    }
1885
1886
    /**
1887
     * Returns user group edit link
1888
     *
1889
     * @param string $username User name
1890
     *
1891
     * @return string HTML code with link
1892
     */
1893
    public function getUserGroupEditLink($username)
1894
    {
1895
         return '<a class="edit_user_group_anchor ajax"'
1896
            . ' href="' . Url::getFromRoute('/server/privileges', ['username' => $username])
1897
            . '">'
1898
            . Util::getIcon('b_usrlist', __('Edit user group'))
1899
            . '</a>';
1900
    }
1901
1902
    /**
1903
     * Returns number of defined user groups
1904
     *
1905
     * @return integer
1906
     */
1907
    public function getUserGroupCount()
1908
    {
1909
        $cfgRelation = $this->relation->getRelationsParam();
1910
        $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

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

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

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

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

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

2200
            . /** @scrutinizer ignore-type */ Util::getTitleForTarget(
Loading history...
2201
                $GLOBALS['cfg']['DefaultTabDatabase']
2202
            )
2203
            . "</a> ]\n";
2204
2205
        if (strlen($tablename) > 0) {
2206
            $scriptName = Util::getScriptNameForOption(
2207
                $GLOBALS['cfg']['DefaultTabTable'],
2208
                'table'
2209
            );
2210
            $html_output .= ' [ ' . __('Table') . ' <a href="'
2211
                . $scriptName
2212
                . Url::getCommon([
2213
                    'db' => $url_dbname,
2214
                    'table' => $tablename,
2215
                    'reload' => 1,
2216
                ], strpos($scriptName, '?') === false ? '?' : '&')
2217
                . '">' . htmlspecialchars($tablename) . ': '
2218
                . 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

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

2272
                    FROM `mysql`.' . /** @scrutinizer ignore-type */ Util::backquote($table_search_in)
Loading history...
2273
                   . $user_host_condition;
2274
            }
2275
        }
2276
2277
        $user_defaults = [
2278
            $dbOrTableName  => '',
2279
            'Grant_priv'    => 'N',
2280
            'privs'         => ['USAGE'],
2281
            'Column_priv'   => true,
2282
        ];
2283
2284
        // for the rights
2285
        $db_rights = [];
2286
2287
        $db_rights_sql = '(' . implode(') UNION (', $db_rights_sqls) . ')'
2288
            . ' ORDER BY `' . $dbOrTableName . '` ASC';
2289
2290
        $db_rights_result = $this->dbi->query($db_rights_sql);
2291
2292
        while ($db_rights_row = $this->dbi->fetchAssoc($db_rights_result)) {
0 ignored issues
show
Bug introduced by
It seems like $db_rights_result can also be of type false; however, parameter $result of PhpMyAdmin\DatabaseInterface::fetchAssoc() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

2572
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
2573
2574
        $user_group_count = 0;
2575
        if ($cfgRelation['menuswork']) {
2576
            $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

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

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

2581
                while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $result)) {
Loading history...
2582
                    $group_assignment[$row['username']] = $row['usergroup'];
2583
                }
2584
            }
2585
            $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

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

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

2716
        while ($db_rights_row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $db_rights_result)) {
Loading history...
2717
            $db_rights_row = array_merge($user_defaults, $db_rights_row);
2718
            $db_rights[$db_rights_row['User']][$db_rights_row['Host']]
2719
                = $db_rights_row;
2720
        }
2721
        $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

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

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

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

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

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

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

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

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

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

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

3829
                . ' ON ' . /** @scrutinizer ignore-type */ Util::backquote($row['Db']) . '.*'
Loading history...
3830
                . ' TO \'' . $this->dbi->escapeString($username)
3831
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\''
3832
                . ($row['Grant_priv'] == 'Y' ? ' WITH GRANT OPTION;' : ';');
3833
        }
3834
        $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

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

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