Passed
Push — master ( ecd9b0...860290 )
by Maurício
09:46
created

Privileges::getHtmlForSpecificTablePrivileges()   C

Complexity

Conditions 14
Paths 2

Size

Total Lines 99
Code Lines 72

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 72
c 0
b 0
f 0
dl 0
loc 99
rs 5.6242
cc 14
nc 2
nop 2

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

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

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

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

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

1978
            . '.' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['usergroups']);
Loading history...
1979
        $sql_query = 'SELECT COUNT(*) FROM ' . $user_group_table;
1980
        $user_group_count = $this->dbi->fetchValue(
1981
            $sql_query,
1982
            0,
1983
            0,
1984
            DatabaseInterface::CONNECT_CONTROL
1985
        );
1986
1987
        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...
1988
    }
1989
1990
    /**
1991
     * Returns name of user group that user is part of
1992
     *
1993
     * @param string $username User name
1994
     *
1995
     * @return mixed usergroup if found or null if not found
1996
     */
1997
    public function getUserGroupForUser($username)
1998
    {
1999
        $cfgRelation = $this->relation->getRelationsParam();
2000
2001
        if (empty($cfgRelation['db'])
2002
            || empty($cfgRelation['users'])
2003
        ) {
2004
            return null;
2005
        }
2006
2007
        $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

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

2008
            . '.' . /** @scrutinizer ignore-type */ Util::backquote($cfgRelation['users']);
Loading history...
2009
        $sql_query = 'SELECT `usergroup` FROM ' . $user_table
2010
            . ' WHERE `username` = \'' . $username . '\''
2011
            . ' LIMIT 1';
2012
2013
        $usergroup = $this->dbi->fetchValue(
2014
            $sql_query,
2015
            0,
2016
            0,
2017
            DatabaseInterface::CONNECT_CONTROL
2018
        );
2019
2020
        if ($usergroup === false) {
2021
            return null;
2022
        }
2023
2024
        return $usergroup;
2025
    }
2026
2027
    /**
2028
     * This function return the extra data array for the ajax behavior
2029
     *
2030
     * @param string $password  password
2031
     * @param string $sql_query sql query
2032
     * @param string $hostname  hostname
2033
     * @param string $username  username
2034
     *
2035
     * @return array
2036
     */
2037
    public function getExtraDataForAjaxBehavior(
2038
        $password,
2039
        $sql_query,
2040
        $hostname,
2041
        $username
2042
    ) {
2043
        if (isset($GLOBALS['dbname'])) {
2044
            //if (preg_match('/\\\\(?:_|%)/i', $dbname)) {
2045
            if (preg_match('/(?<!\\\\)(?:_|%)/', $GLOBALS['dbname'])) {
2046
                $dbname_is_wildcard = true;
2047
            } else {
2048
                $dbname_is_wildcard = false;
2049
            }
2050
        }
2051
2052
        $user_group_count = 0;
2053
        if ($GLOBALS['cfgRelation']['menuswork']) {
2054
            $user_group_count = $this->getUserGroupCount();
2055
        }
2056
2057
        $extra_data = [];
2058
        if (strlen($sql_query) > 0) {
2059
            $extra_data['sql_query'] = Util::getMessage(null, $sql_query);
2060
        }
2061
2062
        if (isset($_POST['change_copy'])) {
2063
            /**
2064
             * generate html on the fly for the new user that was just created.
2065
             */
2066
            $new_user_string = '<tr>' . "\n"
2067
                . '<td> <input type="checkbox" name="selected_usr[]" '
2068
                . 'id="checkbox_sel_users_"'
2069
                . 'value="'
2070
                . htmlspecialchars($username)
2071
                . '&amp;#27;' . htmlspecialchars($hostname) . '">'
2072
                . '</td>' . "\n"
2073
                . '<td><label for="checkbox_sel_users_">'
2074
                . (empty($_POST['username'])
2075
                        ? '<span style="color: #FF0000">' . __('Any') . '</span>'
2076
                        : htmlspecialchars($username) ) . '</label></td>' . "\n"
2077
                . '<td>' . htmlspecialchars($hostname) . '</td>' . "\n";
2078
2079
            $new_user_string .= '<td>';
2080
2081
            if (! empty($password) || isset($_POST['pma_pw'])) {
2082
                $new_user_string .= __('Yes');
2083
            } else {
2084
                $new_user_string .= '<span style="color: #FF0000">'
2085
                    . __('No')
2086
                . '</span>';
2087
            }
2088
2089
            $new_user_string .= '</td>' . "\n";
2090
            $new_user_string .= '<td>'
2091
                . '<code>' . implode(', ', $this->extractPrivInfo(null, true)) . '</code>'
2092
                . '</td>'; //Fill in privileges here
2093
2094
            // if $cfg['Servers'][$i]['users'] and $cfg['Servers'][$i]['usergroups'] are
2095
            // enabled
2096
            $cfgRelation = $this->relation->getRelationsParam();
2097
            if (! empty($cfgRelation['users']) && ! empty($cfgRelation['usergroups'])) {
2098
                $new_user_string .= '<td class="usrGroup"></td>';
2099
            }
2100
2101
            $new_user_string .= '<td>';
2102
            if (isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y') {
2103
                $new_user_string .= __('Yes');
2104
            } else {
2105
                $new_user_string .= __('No');
2106
            }
2107
            $new_user_string .= '</td>';
2108
2109
            if ($GLOBALS['is_grantuser']) {
2110
                $new_user_string .= '<td>'
2111
                    . $this->getUserLink('edit', $username, $hostname)
2112
                    . '</td>' . "\n";
2113
            }
2114
2115
            if ($cfgRelation['menuswork'] && $user_group_count > 0) {
2116
                $new_user_string .= '<td>'
2117
                    . $this->getUserGroupEditLink($username)
2118
                    . '</td>' . "\n";
2119
            }
2120
2121
            $new_user_string .= '<td>'
2122
                . $this->getUserLink(
2123
                    'export',
2124
                    $username,
2125
                    $hostname,
2126
                    '',
2127
                    '',
2128
                    '',
2129
                    isset($_GET['initial']) ? $_GET['initial'] : ''
2130
                )
2131
                . '</td>' . "\n";
2132
2133
            $new_user_string .= '</tr>';
2134
2135
            $extra_data['new_user_string'] = $new_user_string;
2136
2137
            /**
2138
             * Generate the string for this alphabet's initial, to update the user
2139
             * pagination
2140
             */
2141
            $new_user_initial = mb_strtoupper(
2142
                mb_substr($username, 0, 1)
2143
            );
2144
            $newUserInitialString = '<a href="' . Url::getFromRoute('/server/privileges', ['initial' => $new_user_initial]) . '">'
2145
                . $new_user_initial . '</a>';
2146
            $extra_data['new_user_initial'] = $new_user_initial;
2147
            $extra_data['new_user_initial_string'] = $newUserInitialString;
2148
        }
2149
2150
        if (isset($_POST['update_privs'])) {
2151
            $extra_data['db_specific_privs'] = false;
2152
            $extra_data['db_wildcard_privs'] = false;
2153
            if (isset($dbname_is_wildcard)) {
2154
                $extra_data['db_specific_privs'] = ! $dbname_is_wildcard;
2155
                $extra_data['db_wildcard_privs'] = $dbname_is_wildcard;
2156
            }
2157
            $new_privileges = implode(', ', $this->extractPrivInfo(null, true));
2158
2159
            $extra_data['new_privileges'] = $new_privileges;
2160
        }
2161
2162
        if (isset($_GET['validate_username'])) {
2163
            $sql_query = "SELECT * FROM `mysql`.`user` WHERE `User` = '"
2164
                . $_GET['username'] . "';";
2165
            $res = $this->dbi->query($sql_query);
2166
            $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

2166
            $row = $this->dbi->fetchRow(/** @scrutinizer ignore-type */ $res);
Loading history...
2167
            if (empty($row)) {
2168
                $extra_data['user_exists'] = false;
2169
            } else {
2170
                $extra_data['user_exists'] = true;
2171
            }
2172
        }
2173
2174
        return $extra_data;
2175
    }
2176
2177
    /**
2178
     * Get the HTML snippet for change user login information
2179
     *
2180
     * @param string $username username
2181
     * @param string $hostname host name
2182
     *
2183
     * @return string HTML snippet
2184
     */
2185
    public function getChangeLoginInformationHtmlForm($username, $hostname)
2186
    {
2187
        $choices = [
2188
            '4' => __('… keep the old one.'),
2189
            '1' => __('… delete the old one from the user tables.'),
2190
            '2' => __(
2191
                '… revoke all active privileges from '
2192
                . 'the old one and delete it afterwards.'
2193
            ),
2194
            '3' => __(
2195
                '… delete the old one from the user tables '
2196
                . 'and reload the privileges afterwards.'
2197
            ),
2198
        ];
2199
2200
        $html_output = '<form action="' . Url::getFromRoute('/server/privileges')
2201
            . '" onsubmit="return checkAddUser(this);" '
2202
            . 'method="post" class="copyUserForm submenu-item">' . "\n"
2203
            . Url::getHiddenInputs('', '')
2204
            . '<input type="hidden" name="old_username" '
2205
            . 'value="' . htmlspecialchars($username) . '">' . "\n"
2206
            . '<input type="hidden" name="old_hostname" '
2207
            . 'value="' . htmlspecialchars($hostname) . '">' . "\n";
2208
2209
        $usergroup = $this->getUserGroupForUser($username);
2210
        if ($usergroup !== null) {
2211
            $html_output .= '<input type="hidden" name="old_usergroup" '
2212
            . 'value="' . htmlspecialchars($usergroup) . '">' . "\n";
2213
        }
2214
2215
        $html_output .= '<fieldset id="fieldset_change_copy_user">' . "\n"
2216
            . '<legend data-submenu-label="' . __('Login Information') . '">' . "\n"
2217
            . __('Change login information / Copy user account')
2218
            . '</legend>' . "\n"
2219
            . $this->getHtmlForLoginInformationFields('change', $username, $hostname);
2220
2221
        $html_output .= '<fieldset id="fieldset_mode">' . "\n"
2222
            . ' <legend>'
2223
            . __('Create a new user account with the same privileges and …')
2224
            . '</legend>' . "\n";
2225
        $html_output .= Util::getRadioFields(
2226
            'mode',
2227
            $choices,
2228
            '4',
2229
            true
2230
        );
2231
        $html_output .= '</fieldset>' . "\n"
2232
           . '</fieldset>' . "\n";
2233
2234
        $html_output .= '<fieldset id="fieldset_change_copy_user_footer" '
2235
            . 'class="tblFooters">' . "\n"
2236
            . '<input type="hidden" name="change_copy" value="1">' . "\n"
2237
            . '<input class="btn btn-primary" type="submit" value="' . __('Go') . '">' . "\n"
2238
            . '</fieldset>' . "\n"
2239
            . '</form>' . "\n";
2240
2241
        return $html_output;
2242
    }
2243
2244
    /**
2245
     * Provide a line with links to the relevant database and table
2246
     *
2247
     * @param string $url_dbname url database name that urlencode() string
2248
     * @param string $dbname     database name
2249
     * @param string $tablename  table name
2250
     *
2251
     * @return string HTML snippet
2252
     */
2253
    public function getLinkToDbAndTable($url_dbname, $dbname, $tablename)
2254
    {
2255
        $scriptName = Util::getScriptNameForOption(
2256
            $GLOBALS['cfg']['DefaultTabDatabase'],
2257
            'database'
2258
        );
2259
        $html_output = '[ ' . __('Database')
2260
            . ' <a href="' . $scriptName
2261
            . Url::getCommon([
2262
                'db' => $url_dbname,
2263
                'reload' => 1,
2264
            ], strpos($scriptName, '?') === false ? '?' : '&')
2265
            . '">'
2266
            . htmlspecialchars(Util::unescapeMysqlWildcards($dbname)) . ': '
2267
            . 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

2267
            . /** @scrutinizer ignore-type */ Util::getTitleForTarget(
Loading history...
2268
                $GLOBALS['cfg']['DefaultTabDatabase']
2269
            )
2270
            . "</a> ]\n";
2271
2272
        if (strlen($tablename) > 0) {
2273
            $scriptName = Util::getScriptNameForOption(
2274
                $GLOBALS['cfg']['DefaultTabTable'],
2275
                'table'
2276
            );
2277
            $html_output .= ' [ ' . __('Table') . ' <a href="'
2278
                . $scriptName
2279
                . Url::getCommon([
2280
                    'db' => $url_dbname,
2281
                    'table' => $tablename,
2282
                    'reload' => 1,
2283
                ], strpos($scriptName, '?') === false ? '?' : '&')
2284
                . '">' . htmlspecialchars($tablename) . ': '
2285
                . 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

2285
                . /** @scrutinizer ignore-type */ Util::getTitleForTarget(
Loading history...
2286
                    $GLOBALS['cfg']['DefaultTabTable']
2287
                )
2288
                . "</a> ]\n";
2289
        }
2290
        return $html_output;
2291
    }
2292
2293
    /**
2294
     * no db name given, so we want all privs for the given user
2295
     * db name was given, so we want all user specific rights for this db
2296
     * So this function returns user rights as an array
2297
     *
2298
     * @param string $username username
2299
     * @param string $hostname host name
2300
     * @param string $type     database or table
2301
     * @param string $dbname   database name
2302
     *
2303
     * @return array database rights
2304
     */
2305
    public function getUserSpecificRights($username, $hostname, $type, $dbname = '')
2306
    {
2307
        $user_host_condition = " WHERE `User`"
2308
            . " = '" . $this->dbi->escapeString($username) . "'"
2309
            . " AND `Host`"
2310
            . " = '" . $this->dbi->escapeString($hostname) . "'";
2311
2312
        if ($type == 'database') {
2313
            $tables_to_search_for_users = [
2314
                'tables_priv',
2315
                'columns_priv',
2316
                'procs_priv',
2317
            ];
2318
            $dbOrTableName = 'Db';
2319
        } elseif ($type == 'table') {
2320
            $user_host_condition .= " AND `Db` LIKE '"
2321
                . $this->dbi->escapeString($dbname) . "'";
2322
            $tables_to_search_for_users = ['columns_priv'];
2323
            $dbOrTableName = 'Table_name';
2324
        } else { // routine
2325
            $user_host_condition .= " AND `Db` LIKE '"
2326
                . $this->dbi->escapeString($dbname) . "'";
2327
            $tables_to_search_for_users = ['procs_priv'];
2328
            $dbOrTableName = 'Routine_name';
2329
        }
2330
2331
        // we also want privileges for this user not in table `db` but in other table
2332
        $tables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`;');
2333
2334
        $db_rights_sqls = [];
2335
        foreach ($tables_to_search_for_users as $table_search_in) {
2336
            if (in_array($table_search_in, $tables)) {
2337
                $db_rights_sqls[] = '
2338
                    SELECT DISTINCT `' . $dbOrTableName . '`
2339
                    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

2339
                    FROM `mysql`.' . /** @scrutinizer ignore-type */ Util::backquote($table_search_in)
Loading history...
2340
                   . $user_host_condition;
2341
            }
2342
        }
2343
2344
        $user_defaults = [
2345
            $dbOrTableName  => '',
2346
            'Grant_priv'    => 'N',
2347
            'privs'         => ['USAGE'],
2348
            'Column_priv'   => true,
2349
        ];
2350
2351
        // for the rights
2352
        $db_rights = [];
2353
2354
        $db_rights_sql = '(' . implode(') UNION (', $db_rights_sqls) . ')'
2355
            . ' ORDER BY `' . $dbOrTableName . '` ASC';
2356
2357
        $db_rights_result = $this->dbi->query($db_rights_sql);
2358
2359
        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

2359
        while ($db_rights_row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $db_rights_result)) {
Loading history...
2360
            $db_rights_row = array_merge($user_defaults, $db_rights_row);
2361
            if ($type == 'database') {
2362
                // only Db names in the table `mysql`.`db` uses wildcards
2363
                // as we are in the db specific rights display we want
2364
                // all db names escaped, also from other sources
2365
                $db_rights_row['Db'] = Util::escapeMysqlWildcards(
2366
                    $db_rights_row['Db']
2367
                );
2368
            }
2369
            $db_rights[$db_rights_row[$dbOrTableName]] = $db_rights_row;
2370
        }
2371
2372
        $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

2372
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $db_rights_result);
Loading history...
2373
2374
        if ($type == 'database') {
2375
            $sql_query = 'SELECT * FROM `mysql`.`db`'
2376
                . $user_host_condition . ' ORDER BY `Db` ASC';
2377
        } elseif ($type == 'table') {
2378
            $sql_query = 'SELECT `Table_name`,'
2379
                . ' `Table_priv`,'
2380
                . ' IF(`Column_priv` = _latin1 \'\', 0, 1)'
2381
                . ' AS \'Column_priv\''
2382
                . ' FROM `mysql`.`tables_priv`'
2383
                . $user_host_condition
2384
                . ' ORDER BY `Table_name` ASC;';
2385
        } else {
2386
            $sql_query = "SELECT `Routine_name`, `Proc_priv`"
2387
                . " FROM `mysql`.`procs_priv`"
2388
                . $user_host_condition
2389
                . " ORDER BY `Routine_name`";
2390
        }
2391
2392
        $result = $this->dbi->query($sql_query);
2393
2394
        while ($row = $this->dbi->fetchAssoc($result)) {
2395
            if (isset($db_rights[$row[$dbOrTableName]])) {
2396
                $db_rights[$row[$dbOrTableName]]
2397
                    = array_merge($db_rights[$row[$dbOrTableName]], $row);
2398
            } else {
2399
                $db_rights[$row[$dbOrTableName]] = $row;
2400
            }
2401
            if ($type == 'database') {
2402
                // there are db specific rights for this user
2403
                // so we can drop this db rights
2404
                $db_rights[$row['Db']]['can_delete'] = true;
2405
            }
2406
        }
2407
        $this->dbi->freeResult($result);
2408
        return $db_rights;
2409
    }
2410
2411
    /**
2412
     * Parses Proc_priv data
2413
     *
2414
     * @param string $privs Proc_priv
2415
     *
2416
     * @return array
2417
     */
2418
    public function parseProcPriv($privs)
2419
    {
2420
        $result = [
2421
            'Alter_routine_priv' => 'N',
2422
            'Execute_priv'       => 'N',
2423
            'Grant_priv'         => 'N',
2424
        ];
2425
        foreach (explode(',', (string) $privs) as $priv) {
2426
            if ($priv == 'Alter Routine') {
2427
                $result['Alter_routine_priv'] = 'Y';
2428
            } else {
2429
                $result[$priv . '_priv'] = 'Y';
2430
            }
2431
        }
2432
        return $result;
2433
    }
2434
2435
    /**
2436
     * Get a HTML table for display user's tabel specific or database specific rights
2437
     *
2438
     * @param string $username username
2439
     * @param string $hostname host name
2440
     * @param string $type     database, table or routine
2441
     * @param string $dbname   database name
2442
     *
2443
     * @return string
2444
     */
2445
    public function getHtmlForAllTableSpecificRights(
2446
        $username,
2447
        $hostname,
2448
        $type,
2449
        $dbname = ''
2450
    ) {
2451
        $uiData = [
2452
            'database' => [
2453
                'form_id'        => 'database_specific_priv',
2454
                'sub_menu_label' => __('Database'),
2455
                'legend'         => __('Database-specific privileges'),
2456
                'type_label'     => __('Database'),
2457
            ],
2458
            'table' => [
2459
                'form_id'        => 'table_specific_priv',
2460
                'sub_menu_label' => __('Table'),
2461
                'legend'         => __('Table-specific privileges'),
2462
                'type_label'     => __('Table'),
2463
            ],
2464
            'routine' => [
2465
                'form_id'        => 'routine_specific_priv',
2466
                'sub_menu_label' => __('Routine'),
2467
                'legend'         => __('Routine-specific privileges'),
2468
                'type_label'     => __('Routine'),
2469
            ],
2470
        ];
2471
2472
        /**
2473
         * no db name given, so we want all privs for the given user
2474
         * db name was given, so we want all user specific rights for this db
2475
         */
2476
        $db_rights = $this->getUserSpecificRights($username, $hostname, $type, $dbname);
2477
        ksort($db_rights);
2478
2479
        $foundRows = [];
2480
        $privileges = [];
2481
        foreach ($db_rights as $row) {
2482
            $onePrivilege = [];
2483
2484
            $paramTableName = '';
2485
            $paramRoutineName = '';
2486
2487
            if ($type == 'database') {
2488
                $name = $row['Db'];
2489
                $onePrivilege['grant']        = $row['Grant_priv'] == 'Y';
2490
                $onePrivilege['table_privs']   = ! empty($row['Table_priv'])
2491
                    || ! empty($row['Column_priv']);
2492
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
2493
2494
                $paramDbName = $row['Db'];
2495
            } elseif ($type == 'table') {
2496
                $name = $row['Table_name'];
2497
                $onePrivilege['grant'] = in_array(
2498
                    'Grant',
2499
                    explode(',', $row['Table_priv'])
2500
                );
2501
                $onePrivilege['column_privs']  = ! empty($row['Column_priv']);
2502
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
2503
2504
                $paramDbName = $dbname;
2505
                $paramTableName = $row['Table_name'];
2506
            } else { // routine
2507
                $name = $row['Routine_name'];
2508
                $onePrivilege['grant'] = in_array(
2509
                    'Grant',
2510
                    explode(',', $row['Proc_priv'])
2511
                );
2512
2513
                $privs = $this->parseProcPriv($row['Proc_priv']);
2514
                $onePrivilege['privileges'] = implode(
2515
                    ',',
2516
                    $this->extractPrivInfo($privs, true)
2517
                );
2518
2519
                $paramDbName = $dbname;
2520
                $paramRoutineName = $row['Routine_name'];
2521
            }
2522
2523
            $foundRows[] = $name;
2524
            $onePrivilege['name'] = $name;
2525
2526
            $onePrivilege['edit_link'] = '';
2527
            if ($GLOBALS['is_grantuser']) {
2528
                $onePrivilege['edit_link'] = $this->getUserLink(
2529
                    'edit',
2530
                    $username,
2531
                    $hostname,
2532
                    $paramDbName,
2533
                    $paramTableName,
2534
                    $paramRoutineName
2535
                );
2536
            }
2537
2538
            $onePrivilege['revoke_link'] = '';
2539
            if ($type != 'database' || ! empty($row['can_delete'])) {
2540
                $onePrivilege['revoke_link'] = $this->getUserLink(
2541
                    'revoke',
2542
                    $username,
2543
                    $hostname,
2544
                    $paramDbName,
2545
                    $paramTableName,
2546
                    $paramRoutineName
2547
                );
2548
            }
2549
2550
            $privileges[] = $onePrivilege;
2551
        }
2552
2553
        $data = $uiData[$type];
2554
        $data['privileges'] = $privileges;
2555
        $data['username']   = $username;
2556
        $data['hostname']   = $hostname;
2557
        $data['database']   = $dbname;
2558
        $data['type']       = $type;
2559
2560
        if ($type == 'database') {
2561
            // we already have the list of databases from libraries/common.inc.php
2562
            // via $pma = new PMA;
2563
            $pred_db_array = $GLOBALS['dblist']->databases;
2564
            $databases_to_skip = [
2565
                'information_schema',
2566
                'performance_schema',
2567
            ];
2568
2569
            $databases = [];
2570
            if (! empty($pred_db_array)) {
2571
                foreach ($pred_db_array as $current_db) {
2572
                    if (in_array($current_db, $databases_to_skip)) {
2573
                        continue;
2574
                    }
2575
                    $current_db_escaped = Util::escapeMysqlWildcards($current_db);
2576
                    // cannot use array_diff() once, outside of the loop,
2577
                    // because the list of databases has special characters
2578
                    // already escaped in $foundRows,
2579
                    // contrary to the output of SHOW DATABASES
2580
                    if (! in_array($current_db_escaped, $foundRows)) {
2581
                        $databases[] = $current_db;
2582
                    }
2583
                }
2584
            }
2585
            $data['databases'] = $databases;
2586
        } elseif ($type == 'table') {
2587
            $result = @$this->dbi->tryQuery(
2588
                "SHOW TABLES FROM " . Util::backquote($dbname),
2589
                DatabaseInterface::CONNECT_USER,
2590
                DatabaseInterface::QUERY_STORE
2591
            );
2592
2593
            $tables = [];
2594
            if ($result) {
2595
                while ($row = $this->dbi->fetchRow($result)) {
2596
                    if (! in_array($row[0], $foundRows)) {
2597
                        $tables[] = $row[0];
2598
                    }
2599
                }
2600
                $this->dbi->freeResult($result);
2601
            }
2602
            $data['tables'] = $tables;
2603
        } else { // routine
2604
            $routineData = $this->dbi->getRoutines($dbname);
2605
2606
            $routines = [];
2607
            foreach ($routineData as $routine) {
2608
                if (! in_array($routine['name'], $foundRows)) {
2609
                    $routines[] = $routine['name'];
2610
                }
2611
            }
2612
            $data['routines'] = $routines;
2613
        }
2614
2615
        return $this->template->render('server/privileges/privileges_summary', $data);
2616
    }
2617
2618
    /**
2619
     * Get HTML for display the users overview
2620
     * (if less than 50 users, display them immediately)
2621
     *
2622
     * @param array  $result        ran sql query
2623
     * @param array  $db_rights     user's database rights array
2624
     * @param string $pmaThemeImage a image source link
2625
     * @param string $text_dir      text directory
2626
     *
2627
     * @return string HTML snippet
2628
     */
2629
    public function getUsersOverview($result, array $db_rights, $pmaThemeImage, $text_dir)
2630
    {
2631
        global $is_grantuser, $is_createuser;
2632
2633
        $cfgRelation = $this->relation->getRelationsParam();
2634
2635
        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

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

2639
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
2640
2641
        $user_group_count = 0;
2642
        if ($cfgRelation['menuswork']) {
2643
            $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

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

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

2648
                while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $result)) {
Loading history...
2649
                    $group_assignment[$row['username']] = $row['usergroup'];
2650
                }
2651
            }
2652
            $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

2652
            $this->dbi->freeResult(/** @scrutinizer ignore-type */ $result);
Loading history...
2653
2654
            $user_group_count = $this->getUserGroupCount();
2655
        }
2656
2657
        $hosts = [];
2658
        foreach ($db_rights as $user) {
2659
            ksort($user);
2660
            foreach ($user as $host) {
2661
                $check_plugin_query = "SELECT * FROM `mysql`.`user` WHERE "
2662
                    . "`User` = '" . $host['User'] . "' AND `Host` = '"
2663
                    . $host['Host'] . "'";
2664
                $res = $this->dbi->fetchSingleRow($check_plugin_query);
2665
2666
                $hasPassword = false;
2667
                if ((isset($res['authentication_string'])
2668
                    && ! empty($res['authentication_string']))
2669
                    || (isset($res['Password'])
2670
                    && ! empty($res['Password']))
2671
                ) {
2672
                    $hasPassword = true;
2673
                }
2674
2675
                $hosts[] = [
2676
                    'user' => $host['User'],
2677
                    'host' => $host['Host'],
2678
                    'has_password' => $hasPassword,
2679
                    'has_select_priv' => isset($host['Select_priv']),
2680
                    'privileges' => $host['privs'],
2681
                    'group' => $group_assignment[$host['User']] ?? '',
2682
                    'has_grant' => $host['Grant_priv'] == 'Y',
2683
                ];
2684
            }
2685
        }
2686
2687
        return $this->template->render('server/privileges/users_overview', [
2688
            'menus_work' => $cfgRelation['menuswork'],
2689
            'user_group_count' => $user_group_count,
2690
            'pma_theme_image' => $pmaThemeImage,
2691
            'text_dir' => $text_dir,
2692
            'initial' => $_GET['initial'] ?? '',
2693
            'hosts' => $hosts,
2694
            'is_grantuser' => $is_grantuser,
2695
            'is_createuser' => $is_createuser,
2696
        ]);
2697
    }
2698
2699
    /**
2700
     * Get HTML for Displays the initials
2701
     *
2702
     * @param array $array_initials array for all initials, even non A-Z
2703
     *
2704
     * @return string HTML snippet
2705
     */
2706
    public function getHtmlForInitials(array $array_initials)
2707
    {
2708
        // initialize to false the letters A-Z
2709
        for ($letter_counter = 1; $letter_counter < 27; $letter_counter++) {
2710
            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

2710
            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...
2711
                $array_initials[mb_chr($letter_counter + 64)] = false;
2712
            }
2713
        }
2714
2715
        $initials = $this->dbi->tryQuery(
2716
            'SELECT DISTINCT UPPER(LEFT(`User`,1)) FROM `user`'
2717
            . ' ORDER BY UPPER(LEFT(`User`,1)) ASC',
2718
            DatabaseInterface::CONNECT_USER,
2719
            DatabaseInterface::QUERY_STORE
2720
        );
2721
        if ($initials) {
2722
            while (list($tmp_initial) = $this->dbi->fetchRow($initials)) {
2723
                $array_initials[$tmp_initial] = true;
2724
            }
2725
        }
2726
2727
        // Display the initials, which can be any characters, not
2728
        // just letters. For letters A-Z, we add the non-used letters
2729
        // as greyed out.
2730
2731
        uksort($array_initials, "strnatcasecmp");
2732
2733
        return $this->template->render('server/privileges/initials_row', [
2734
            'array_initials' => $array_initials,
2735
            'initial' => isset($_GET['initial']) ? $_GET['initial'] : null,
2736
        ]);
2737
    }
2738
2739
    /**
2740
     * Get the database rights array for Display user overview
2741
     *
2742
     * @return array    database rights array
2743
     */
2744
    public function getDbRightsForUserOverview()
2745
    {
2746
        // we also want users not in table `user` but in other table
2747
        $tables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`;');
2748
2749
        $tablesSearchForUsers = [
2750
            'user',
2751
            'db',
2752
            'tables_priv',
2753
            'columns_priv',
2754
            'procs_priv',
2755
        ];
2756
2757
        $db_rights_sqls = [];
2758
        foreach ($tablesSearchForUsers as $table_search_in) {
2759
            if (in_array($table_search_in, $tables)) {
2760
                $db_rights_sqls[] = 'SELECT DISTINCT `User`, `Host` FROM `mysql`.`'
2761
                    . $table_search_in . '` '
2762
                    . (isset($_GET['initial'])
2763
                    ? $this->rangeOfUsers($_GET['initial'])
2764
                    : '');
2765
            }
2766
        }
2767
        $user_defaults = [
2768
            'User'       => '',
2769
            'Host'       => '%',
2770
            'Password'   => '?',
2771
            'Grant_priv' => 'N',
2772
            'privs'      => ['USAGE'],
2773
        ];
2774
2775
        // for the rights
2776
        $db_rights = [];
2777
2778
        $db_rights_sql = '(' . implode(') UNION (', $db_rights_sqls) . ')'
2779
            . ' ORDER BY `User` ASC, `Host` ASC';
2780
2781
        $db_rights_result = $this->dbi->query($db_rights_sql);
2782
2783
        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

2783
        while ($db_rights_row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $db_rights_result)) {
Loading history...
2784
            $db_rights_row = array_merge($user_defaults, $db_rights_row);
2785
            $db_rights[$db_rights_row['User']][$db_rights_row['Host']]
2786
                = $db_rights_row;
2787
        }
2788
        $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

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

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

3134
                    mb_strrpos(/** @scrutinizer ignore-type */ $_user_name, '@') + 1
Loading history...
3135
                );
3136
                unset($_user_name);
3137
                break;
3138
        }
3139
        $sql = "SELECT '1' FROM `mysql`.`user`"
3140
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
3141
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';";
3142
        if ($this->dbi->fetchValue($sql) == 1) {
3143
            $message = Message::error(__('The user %s already exists!'));
3144
            $message->addParam('[em]\'' . $username . '\'@\'' . $hostname . '\'[/em]');
3145
            $_GET['adduser'] = true;
3146
            $_add_user_error = true;
3147
3148
            return [
3149
                $message,
3150
                $queries,
3151
                $queries_for_display,
3152
                $sql_query,
3153
                $_add_user_error,
3154
            ];
3155
        }
3156
3157
        list(
3158
            $create_user_real,
3159
            $create_user_show,
3160
            $real_sql_query,
3161
            $sql_query,
3162
            $password_set_real,
3163
            $password_set_show,
3164
            $alter_real_sql_query,
3165
            $alter_sql_query
3166
        ) = $this->getSqlQueriesForDisplayAndAddUser(
3167
            $username,
3168
            $hostname,
3169
            (isset($password) ? $password : '')
3170
        );
3171
3172
        if (empty($_POST['change_copy'])) {
3173
            $_error = false;
3174
3175
            if ($create_user_real !== null) {
0 ignored issues
show
introduced by
The condition $create_user_real !== null is always true.
Loading history...
3176
                if (! $this->dbi->tryQuery($create_user_real)) {
3177
                    $_error = true;
3178
                }
3179
                if (isset($password_set_real, $_POST['authentication_plugin']) && ! empty($password_set_real)) {
3180
                    $this->setProperPasswordHashing(
3181
                        $_POST['authentication_plugin']
3182
                    );
3183
                    if ($this->dbi->tryQuery($password_set_real)) {
3184
                        $sql_query .= $password_set_show;
3185
                    }
3186
                }
3187
                $sql_query = $create_user_show . $sql_query;
3188
            }
3189
3190
            list($sql_query, $message) = $this->addUserAndCreateDatabase(
3191
                $_error,
3192
                $real_sql_query,
3193
                $sql_query,
3194
                $username,
3195
                $hostname,
3196
                $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

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

3539
            $this->dbi->freeResult(/** @scrutinizer ignore-type */ $res);
Loading history...
3540
            $this->dbi->freeResult($res_all);
3541
            $sql_query = 'SELECT * FROM `mysql`.`user`';
3542
            $res = $this->dbi->tryQuery(
3543
                $sql_query,
3544
                DatabaseInterface::CONNECT_USER,
3545
                DatabaseInterface::QUERY_STORE
3546
            );
3547
3548
            if (! $res) {
3549
                $errorMessages .= $this->getHtmlForViewUsersError();
3550
                $errorMessages .= $this->getAddUserHtmlFieldset();
3551
            } else {
3552
                // This message is hardcoded because I will replace it by
3553
                // a automatic repair feature soon.
3554
                $raw = 'Your privilege table structure seems to be older than'
3555
                    . ' this MySQL version!<br>'
3556
                    . 'Please run the <code>mysql_upgrade</code> command'
3557
                    . ' that should be included in your MySQL server distribution'
3558
                    . ' to solve this problem!';
3559
                $errorMessages .= Message::rawError($raw)->getDisplay();
3560
            }
3561
            $this->dbi->freeResult($res);
3562
        } else {
3563
            $db_rights = $this->getDbRightsForUserOverview();
3564
            // for all initials, even non A-Z
3565
            $array_initials = [];
3566
3567
            foreach ($db_rights as $right) {
3568
                foreach ($right as $account) {
3569
                    if (empty($account['User']) && $account['Host'] == 'localhost') {
3570
                        $emptyUserNotice = Message::notice(
3571
                            __(
3572
                                'A user account allowing any user from localhost to '
3573
                                . 'connect is present. This will prevent other users '
3574
                                . 'from connecting if the host part of their account '
3575
                                . 'allows a connection from any (%) host.'
3576
                            )
3577
                            . Util::showMySQLDocu('problems-connecting')
3578
                        )->getDisplay();
3579
                        break 2;
3580
                    }
3581
                }
3582
            }
3583
3584
            /**
3585
             * Displays the initials
3586
             * Also not necessary if there is less than 20 privileges
3587
             */
3588
            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

3588
            if ($this->dbi->numRows(/** @scrutinizer ignore-type */ $res_all) > 20) {
Loading history...
3589
                $initials = $this->getHtmlForInitials($array_initials);
3590
            }
3591
3592
            /**
3593
            * Display the user overview
3594
            * (if less than 50 users, display them immediately)
3595
            */
3596
            if (isset($_GET['initial'])
3597
                || isset($_GET['showall'])
3598
                || $this->dbi->numRows($res) < 50
3599
            ) {
3600
                $usersOverview = $this->getUsersOverview(
3601
                    $res,
3602
                    $db_rights,
3603
                    $pmaThemeImage,
3604
                    $text_dir
3605
                );
3606
            }
3607
3608
            $response = Response::getInstance();
3609
            if (! $response->isAjax()
3610
                || ! empty($_REQUEST['ajax_page_request'])
3611
            ) {
3612
                if ($GLOBALS['is_reload_priv']) {
3613
                    $flushnote = new Message(
3614
                        __(
3615
                            'Note: phpMyAdmin gets the users’ privileges directly '
3616
                            . 'from MySQL’s privilege tables. The content of these '
3617
                            . 'tables may differ from the privileges the server uses, '
3618
                            . 'if they have been changed manually. In this case, '
3619
                            . 'you should %sreload the privileges%s before you continue.'
3620
                        ),
3621
                        Message::NOTICE
3622
                    );
3623
                    $flushnote->addParamHtml(
3624
                        '<a href="' . Url::getFromRoute('/server/privileges', ['flush_privileges' => 1])
3625
                        . '" id="reload_privileges_anchor">'
3626
                    );
3627
                    $flushnote->addParamHtml('</a>');
3628
                } else {
3629
                    $flushnote = new Message(
3630
                        __(
3631
                            'Note: phpMyAdmin gets the users’ privileges directly '
3632
                            . 'from MySQL’s privilege tables. The content of these '
3633
                            . 'tables may differ from the privileges the server uses, '
3634
                            . 'if they have been changed manually. In this case, '
3635
                            . 'the privileges have to be reloaded but currently, you '
3636
                            . 'don\'t have the RELOAD privilege.'
3637
                        )
3638
                        . Util::showMySQLDocu(
3639
                            'privileges-provided',
3640
                            false,
3641
                            null,
3642
                            null,
3643
                            'priv_reload'
3644
                        ),
3645
                        Message::NOTICE
3646
                    );
3647
                }
3648
                $flushNotice = $flushnote->getDisplay();
3649
            }
3650
        }
3651
3652
        return $this->template->render('server/privileges/user_overview', [
3653
            'error_messages' => $errorMessages,
3654
            'empty_user_notice' => $emptyUserNotice ?? '',
3655
            'initials' => $initials ?? '',
3656
            'users_overview' => $usersOverview ?? '',
3657
            'is_createuser' => $is_createuser,
3658
            'flush_notice' => $flushNotice ?? '',
3659
        ]);
3660
    }
3661
3662
    /**
3663
     * Get HTML snippet for display user properties
3664
     *
3665
     * @param boolean      $dbname_is_wildcard whether database name is wildcard or not
3666
     * @param string       $url_dbname         url database name that urlencode() string
3667
     * @param string       $username           username
3668
     * @param string       $hostname           host name
3669
     * @param string|array $dbname             database name
3670
     * @param string       $tablename          table name
3671
     *
3672
     * @return string
3673
     */
3674
    public function getHtmlForUserProperties(
3675
        $dbname_is_wildcard,
3676
        $url_dbname,
3677
        $username,
3678
        $hostname,
3679
        $dbname,
3680
        $tablename
3681
    ) {
3682
        $sql = "SELECT '1' FROM `mysql`.`user`"
3683
            . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'"
3684
            . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';";
3685
3686
        $user_does_not_exists = (bool) ! $this->dbi->fetchValue($sql);
3687
3688
        $loginInformationFields = '';
3689
        if ($user_does_not_exists) {
3690
            $loginInformationFields = $this->getHtmlForLoginInformationFields();
3691
        }
3692
3693
        $_params = [
3694
            'username' => $username,
3695
            'hostname' => $hostname,
3696
        ];
3697
        if (! is_array($dbname) && strlen($dbname) > 0) {
3698
            $_params['dbname'] = $dbname;
3699
            if (strlen($tablename) > 0) {
3700
                $_params['tablename'] = $tablename;
3701
            }
3702
        } else {
3703
            $_params['dbname'] = $dbname;
3704
        }
3705
3706
        $privilegesTable = $this->getHtmlToDisplayPrivilegesTable(
3707
            // If $dbname is an array, pass any one db as all have same privs.
3708
            Core::ifSetOr($dbname, is_array($dbname) ? $dbname[0] : '*', 'length'),
3709
            Core::ifSetOr($tablename, '*', 'length')
3710
        );
3711
3712
        $tableSpecificRights = '';
3713
        if (! is_array($dbname) && strlen($tablename) === 0
3714
            && empty($dbname_is_wildcard)
3715
        ) {
3716
            // no table name was given, display all table specific rights
3717
            // but only if $dbname contains no wildcards
3718
            if (strlen($dbname) === 0) {
3719
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3720
                    $username,
3721
                    $hostname,
3722
                    'database'
3723
                );
3724
            } else {
3725
                // unescape wildcards in dbname at table level
3726
                $unescaped_db = Util::unescapeMysqlWildcards($dbname);
3727
3728
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3729
                    $username,
3730
                    $hostname,
3731
                    'table',
3732
                    $unescaped_db
3733
                );
3734
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
3735
                    $username,
3736
                    $hostname,
3737
                    'routine',
3738
                    $unescaped_db
3739
                );
3740
            }
3741
        }
3742
3743
        // Provide a line with links to the relevant database and table
3744
        $linkToDatabaseAndTable = '';
3745
        if (! is_array($dbname) && strlen($dbname) > 0 && empty($dbname_is_wildcard)) {
3746
            $linkToDatabaseAndTable = $this->getLinkToDbAndTable($url_dbname, $dbname, $tablename);
3747
        }
3748
3749
        $changePassword = '';
3750
        $changeLoginInformation = '';
3751
        if (! is_array($dbname) && strlen($dbname) === 0 && ! $user_does_not_exists) {
3752
            //change login information
3753
            $changePassword = ChangePassword::getHtml(
3754
                'edit_other',
3755
                $username,
3756
                $hostname
3757
            );
3758
            $changeLoginInformation = $this->getChangeLoginInformationHtmlForm($username, $hostname);
3759
        }
3760
3761
        return $this->template->render('server/privileges/user_properties', [
3762
            'user_does_not_exists' => $user_does_not_exists,
3763
            'login_information_fields' => $loginInformationFields,
3764
            'params' => $_params,
3765
            'privileges_table' => $privilegesTable,
3766
            'table_specific_rights' => $tableSpecificRights,
3767
            'link_to_database_and_table' => $linkToDatabaseAndTable,
3768
            'change_password' => $changePassword,
3769
            'change_login_information' => $changeLoginInformation,
3770
            'database' => $dbname,
3771
            'dbname' => $url_dbname,
3772
            'username' => $username,
3773
            'hostname' => $hostname,
3774
            'is_databases' => $dbname_is_wildcard || is_array($dbname) && count($dbname) > 1,
3775
            'table' => $tablename,
3776
            'current_user' => $this->dbi->getCurrentUser(),
3777
        ]);
3778
    }
3779
3780
    /**
3781
     * Get queries for Table privileges to change or copy user
3782
     *
3783
     * @param string $user_host_condition user host condition to
3784
     *                                    select relevant table privileges
3785
     * @param array  $queries             queries array
3786
     * @param string $username            username
3787
     * @param string $hostname            host name
3788
     *
3789
     * @return array
3790
     */
3791
    public function getTablePrivsQueriesForChangeOrCopyUser(
3792
        $user_host_condition,
3793
        array $queries,
3794
        $username,
3795
        $hostname
3796
    ) {
3797
        $res = $this->dbi->query(
3798
            'SELECT `Db`, `Table_name`, `Table_priv` FROM `mysql`.`tables_priv`'
3799
            . $user_host_condition,
3800
            DatabaseInterface::CONNECT_USER,
3801
            DatabaseInterface::QUERY_STORE
3802
        );
3803
        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

3803
        while ($row = $this->dbi->fetchAssoc(/** @scrutinizer ignore-type */ $res)) {
Loading history...
3804
            $res2 = $this->dbi->query(
3805
                'SELECT `Column_name`, `Column_priv`'
3806
                . ' FROM `mysql`.`columns_priv`'
3807
                . ' WHERE `User`'
3808
                . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . "'"
3809
                . ' AND `Host`'
3810
                . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . '\''
3811
                . ' AND `Db`'
3812
                . ' = \'' . $this->dbi->escapeString($row['Db']) . "'"
3813
                . ' AND `Table_name`'
3814
                . ' = \'' . $this->dbi->escapeString($row['Table_name']) . "'"
3815
                . ';',
3816
                DatabaseInterface::CONNECT_USER,
3817
                DatabaseInterface::QUERY_STORE
3818
            );
3819
3820
            $tmp_privs1 = $this->extractPrivInfo($row);
3821
            $tmp_privs2 = [
3822
                'Select' => [],
3823
                'Insert' => [],
3824
                'Update' => [],
3825
                'References' => [],
3826
            ];
3827
3828
            while ($row2 = $this->dbi->fetchAssoc($res2)) {
3829
                $tmp_array = explode(',', $row2['Column_priv']);
3830
                if (in_array('Select', $tmp_array)) {
3831
                    $tmp_privs2['Select'][] = $row2['Column_name'];
3832
                }
3833
                if (in_array('Insert', $tmp_array)) {
3834
                    $tmp_privs2['Insert'][] = $row2['Column_name'];
3835
                }
3836
                if (in_array('Update', $tmp_array)) {
3837
                    $tmp_privs2['Update'][] = $row2['Column_name'];
3838
                }
3839
                if (in_array('References', $tmp_array)) {
3840
                    $tmp_privs2['References'][] = $row2['Column_name'];
3841
                }
3842
            }
3843
            if (count($tmp_privs2['Select']) > 0 && ! in_array('SELECT', $tmp_privs1)) {
3844
                $tmp_privs1[] = 'SELECT (`' . implode('`, `', $tmp_privs2['Select']) . '`)';
3845
            }
3846
            if (count($tmp_privs2['Insert']) > 0 && ! in_array('INSERT', $tmp_privs1)) {
3847
                $tmp_privs1[] = 'INSERT (`' . implode('`, `', $tmp_privs2['Insert']) . '`)';
3848
            }
3849
            if (count($tmp_privs2['Update']) > 0 && ! in_array('UPDATE', $tmp_privs1)) {
3850
                $tmp_privs1[] = 'UPDATE (`' . implode('`, `', $tmp_privs2['Update']) . '`)';
3851
            }
3852
            if (count($tmp_privs2['References']) > 0
3853
                && ! in_array('REFERENCES', $tmp_privs1)
3854
            ) {
3855
                $tmp_privs1[]
3856
                    = 'REFERENCES (`' . implode('`, `', $tmp_privs2['References']) . '`)';
3857
            }
3858
3859
            $queries[] = 'GRANT ' . implode(', ', $tmp_privs1)
3860
                . ' 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

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

3861
                . /** @scrutinizer ignore-type */ Util::backquote($row['Table_name'])
Loading history...
3862
                . ' TO \'' . $this->dbi->escapeString($username)
3863
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\''
3864
                . (in_array('Grant', explode(',', $row['Table_priv']))
3865
                ? ' WITH GRANT OPTION;'
3866
                : ';');
3867
        }
3868
        return $queries;
3869
    }
3870
3871
    /**
3872
     * Get queries for database specific privileges for change or copy user
3873
     *
3874
     * @param array  $queries  queries array with string
3875
     * @param string $username username
3876
     * @param string $hostname host name
3877
     *
3878
     * @return array
3879
     */
3880
    public function getDbSpecificPrivsQueriesForChangeOrCopyUser(
3881
        array $queries,
3882
        $username,
3883
        $hostname
3884
    ) {
3885
        $user_host_condition = ' WHERE `User`'
3886
            . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . "'"
3887
            . ' AND `Host`'
3888
            . ' = \'' . $this->dbi->escapeString($_POST['old_hostname']) . '\';';
3889
3890
        $res = $this->dbi->query(
3891
            'SELECT * FROM `mysql`.`db`' . $user_host_condition
3892
        );
3893
3894
        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

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

3896
                . ' ON ' . /** @scrutinizer ignore-type */ Util::backquote($row['Db']) . '.*'
Loading history...
3897
                . ' TO \'' . $this->dbi->escapeString($username)
3898
                . '\'@\'' . $this->dbi->escapeString($hostname) . '\''
3899
                . ($row['Grant_priv'] == 'Y' ? ' WITH GRANT OPTION;' : ';');
3900
        }
3901
        $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

3901
        $this->dbi->freeResult(/** @scrutinizer ignore-type */ $res);
Loading history...
3902
3903
        $queries = $this->getTablePrivsQueriesForChangeOrCopyUser(
3904
            $user_host_condition,
3905
            $queries,
3906
            $username,
3907
            $hostname
3908
        );
3909
3910
        return $queries;
3911
    }
3912
3913
    /**
3914
     * Prepares queries for adding users and
3915
     * also create database and return query and message
3916
     *
3917
     * @param boolean $_error               whether user create or not
3918
     * @param string  $real_sql_query       SQL query for add a user
3919
     * @param string  $sql_query            SQL query to be displayed
3920
     * @param string  $username             username
3921
     * @param string  $hostname             host name
3922
     * @param string  $dbname               database name
3923
     * @param string  $alter_real_sql_query SQL query for ALTER USER
3924
     * @param string  $alter_sql_query      SQL query for ALTER USER to be displayed
3925
     *
3926
     * @return array, $message
3927
     */
3928
    public function addUserAndCreateDatabase(
3929
        $_error,
3930
        $real_sql_query,
3931
        $sql_query,
3932
        $username,
3933
        $hostname,
3934
        $dbname,
3935
        $alter_real_sql_query,
3936
        $alter_sql_query
3937
    ) {
3938
        if ($_error || (! empty($real_sql_query)
3939
            && ! $this->dbi->tryQuery($real_sql_query))
3940
        ) {
3941
            $_POST['createdb-1'] = $_POST['createdb-2']
3942
                = $_POST['createdb-3'] = null;
3943
            $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

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