Privileges::getHtmlForUserOverview()   B
last analyzed

Complexity

Conditions 10
Paths 28

Size

Total Lines 78
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 13.1764

Importance

Changes 0
Metric Value
eloc 55
c 0
b 0
f 0
dl 0
loc 78
ccs 41
cts 60
cp 0.6833
rs 7.1151
cc 10
nc 28
nop 3
crap 13.1764

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
6
declare(strict_types=1);
7
8
namespace PhpMyAdmin\Server;
9
10
use PhpMyAdmin\Config;
11
use PhpMyAdmin\ConfigStorage\Features\ConfigurableMenusFeature;
12
use PhpMyAdmin\ConfigStorage\Relation;
13
use PhpMyAdmin\ConfigStorage\RelationCleanup;
14
use PhpMyAdmin\Current;
15
use PhpMyAdmin\Database\Routines;
16
use PhpMyAdmin\DatabaseInterface;
17
use PhpMyAdmin\Dbal\Connection;
18
use PhpMyAdmin\Dbal\ConnectionType;
19
use PhpMyAdmin\Dbal\ResultInterface;
20
use PhpMyAdmin\Html\Generator;
21
use PhpMyAdmin\Html\MySQLDocumentation;
22
use PhpMyAdmin\Identifiers\DatabaseName;
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\Identifiers\DatabaseName was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
use PhpMyAdmin\Identifiers\TableName;
0 ignored issues
show
Bug introduced by
The type PhpMyAdmin\Identifiers\TableName was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
use PhpMyAdmin\Message;
25
use PhpMyAdmin\Query\Compatibility;
26
use PhpMyAdmin\ResponseRenderer;
27
use PhpMyAdmin\Template;
28
use PhpMyAdmin\Url;
29
use PhpMyAdmin\UserPrivileges;
30
use PhpMyAdmin\Util;
31
32
use function __;
33
use function array_fill_keys;
34
use function array_filter;
35
use function array_intersect;
36
use function array_keys;
37
use function array_map;
38
use function array_merge;
39
use function array_unique;
40
use function count;
41
use function explode;
42
use function htmlspecialchars;
43
use function implode;
44
use function in_array;
45
use function is_array;
46
use function is_string;
47
use function json_decode;
48
use function ksort;
49
use function max;
50
use function mb_strpos;
51
use function mb_strrpos;
52
use function mb_strtolower;
53
use function mb_strtoupper;
54
use function mb_substr;
55
use function preg_match;
56
use function preg_replace;
57
use function range;
58
use function sprintf;
59
use function str_contains;
60
use function str_replace;
61
use function str_starts_with;
62
use function strnatcasecmp;
63
use function strtr;
64
use function trim;
65
use function uksort;
66
67
/**
68
 * Privileges class
69
 */
70
class Privileges
71
{
72 224
    public function __construct(
73
        public Template $template,
74
        public DatabaseInterface $dbi,
75
        public Relation $relation,
76
        private RelationCleanup $relationCleanup,
77
        private Plugins $plugins,
78
    ) {
79 224
    }
80
81
    /**
82
     * Escapes wildcard in a database+table specification
83
     * before using it in a GRANT statement.
84
     *
85
     * Escaping a wildcard character in a GRANT is only accepted at the global
86
     * or database level, not at table level; this is why I remove
87
     * the escaping character. Internally, in mysql.tables_priv.Db there are
88
     * no escaping (for example test_db) but in mysql.db you'll see test\_db
89
     * for a db-specific privilege.
90
     *
91
     * @param string $dbname    Database name
92
     * @param string $tablename Table name
93
     *
94
     * @return string the escaped (if necessary) database.table
95
     */
96 20
    public function wildcardEscapeForGrant(string $dbname, string $tablename): string
97
    {
98 20
        if ($dbname === '') {
99 12
            return '*.*';
100
        }
101
102 12
        if ($tablename === '') {
103 4
            return Util::backquote($dbname) . '.*';
104
        }
105
106 12
        return Util::backquote(
107 12
            $this->unescapeGrantWildcards($dbname),
108 12
        ) . '.' . Util::backquote($tablename);
109
    }
110
111
    /**
112
     * Add slashes before "_" and "%" characters for using them in MySQL
113
     * database, table and field names.
114
     * Note: This function does not escape backslashes!
115
     *
116
     * @param string $name the string to escape
117
     *
118
     * @return string the escaped string
119
     */
120 36
    public function escapeGrantWildcards(string $name): string
121
    {
122 36
        return strtr($name, ['_' => '\\_', '%' => '\\%']);
123
    }
124
125
    /**
126
     * removes slashes before "_" and "%" characters
127
     * Note: This function does not unescape backslashes!
128
     *
129
     * @param string $name the string to escape
130
     *
131
     * @return string the escaped string
132
     */
133 60
    public function unescapeGrantWildcards(string $name): string
134
    {
135 60
        return strtr($name, ['\\_' => '_', '\\%' => '%']);
136
    }
137
138
    /**
139
     * Generates a condition on the user name
140
     *
141
     * @param string|null $initial the user's initial
142
     *
143
     * @return string   the generated condition
144
     */
145 12
    public function rangeOfUsers(string|null $initial = null): string
146
    {
147 12
        if ($initial === null) {
148 8
            return '';
149
        }
150
151 8
        if ($initial === '') {
152 4
            return "WHERE `User` = ''";
153
        }
154
155 8
        $like = $this->dbi->escapeMysqlWildcards($initial) . '%';
156
157
        // strtolower() is used because the User field
158
        // might be BINARY, so LIKE would be case sensitive
159 8
        return 'WHERE `User` LIKE '
160 8
            . $this->dbi->quoteString($like)
161 8
            . ' OR `User` LIKE '
162 8
            . $this->dbi->quoteString(mb_strtolower($like));
163
    }
164
165
    /**
166
     * Parses privileges into an array, it modifies the array
167
     *
168
     * @param mixed[] $row Results row from
169
     */
170 12
    public function fillInTablePrivileges(array &$row): void
171
    {
172 12
        $row1 = $this->dbi->fetchSingleRow('SHOW COLUMNS FROM `mysql`.`tables_priv` LIKE \'Table_priv\';');
173
        // note: in MySQL 5.0.3 we get "Create View', 'Show view';
174
        // the View for Create is spelled with uppercase V
175
        // the view for Show is spelled with lowercase v
176
        // and there is a space between the words
177
178 12
        $avGrants = explode(
179 12
            '\',\'',
180 12
            mb_substr(
181 12
                $row1['Type'],
182 12
                mb_strpos($row1['Type'], '(') + 2,
183 12
                mb_strpos($row1['Type'], ')')
184 12
                - mb_strpos($row1['Type'], '(') - 3,
185 12
            ),
186 12
        );
187
188 12
        $usersGrants = explode(',', $row['Table_priv']);
189
190 12
        foreach ($avGrants as $currentGrant) {
191 12
            $row[$currentGrant . '_priv'] = in_array($currentGrant, $usersGrants, true) ? 'Y' : 'N';
192
        }
193
194 12
        unset($row['Table_priv']);
195
    }
196
197
    /**
198
     * Extracts the privilege information of a priv table row
199
     *
200
     * @param mixed[]|null $row        the row
201
     * @param bool         $enableHTML add <dfn> tag with tooltips
202
     * @param bool         $tablePrivs whether row contains table privileges
203
     *
204
     * @return string[]
205
     *
206
     * @global resource $user_link the database connection
207
     */
208 48
    public function extractPrivInfo(array|null $row = null, bool $enableHTML = false, bool $tablePrivs = false): array
209
    {
210 48
        $grants = $tablePrivs ? $this->getTableGrantsArray() : $this->getGrantsArray();
211
212 48
        if ($row !== null && isset($row['Table_priv'])) {
213
            $this->fillInTablePrivileges($row);
214
        }
215
216 48
        $privs = [];
217 48
        $allPrivileges = true;
218 48
        foreach ($grants as $currentGrant) {
219
            if (
220 48
                ($row === null || ! isset($row[$currentGrant[0]]))
221 48
                && ($row !== null || ! isset($GLOBALS[$currentGrant[0]]))
222
            ) {
223 48
                continue;
224
            }
225
226
            if (
227 4
                ($row !== null && $row[$currentGrant[0]] === 'Y')
228 4
                || ($row === null
229 4
                && ($GLOBALS[$currentGrant[0]] === 'Y'
230 4
                || (is_array($GLOBALS[$currentGrant[0]])
231 4
                && count($GLOBALS[$currentGrant[0]]) == $_REQUEST['column_count']
232 4
                && empty($GLOBALS[$currentGrant[0] . '_none']))))
233
            ) {
234 4
                if ($enableHTML) {
235 4
                    $privs[] = '<dfn title="' . $currentGrant[2] . '">'
236 4
                    . $currentGrant[1] . '</dfn>';
237
                } else {
238 2
                    $privs[] = $currentGrant[1];
239
                }
240
            } elseif (
241 4
                ! empty($GLOBALS[$currentGrant[0]])
242 4
                && is_array($GLOBALS[$currentGrant[0]])
243 4
                && empty($GLOBALS[$currentGrant[0] . '_none'])
244
            ) {
245
                // Required for proper escaping of ` (backtick) in a column name
246
                $grantCols = array_map(
247
                    Util::backquote(...),
248
                    $GLOBALS[$currentGrant[0]],
249
                );
250
251
                if ($enableHTML) {
252
                    $privs[] = '<dfn title="' . $currentGrant[2] . '">'
253
                        . $currentGrant[1] . '</dfn>'
254
                        . ' (' . implode(', ', $grantCols) . ')';
255
                } else {
256
                    $privs[] = $currentGrant[1]
257
                        . ' (' . implode(', ', $grantCols) . ')';
258
                }
259
            } else {
260 4
                $allPrivileges = false;
261
            }
262
        }
263
264 48
        if ($privs === []) {
265 48
            if ($enableHTML) {
266 8
                $privs[] = '<dfn title="' . __('No privileges.') . '">USAGE</dfn>';
267
            } else {
268 44
                $privs[] = 'USAGE';
269
            }
270 4
        } elseif ($allPrivileges && (! isset($_POST['grant_count']) || count($privs) == $_POST['grant_count'])) {
271 4
            if ($enableHTML) {
272 4
                $privs = ['<dfn title="' . __('Includes all privileges except GRANT.') . '">ALL PRIVILEGES</dfn>'];
273
            } else {
274
                $privs = ['ALL PRIVILEGES'];
275
            }
276
        }
277
278 48
        return $privs;
279
    }
280
281
    /**
282
     * Returns an array of table grants and their descriptions
283
     *
284
     * @return string[][] array of table grants
285
     */
286 4
    public function getTableGrantsArray(): array
287
    {
288 4
        return [
289 4
            ['Delete', 'DELETE', __('Allows deleting data.')],
290 4
            ['Create', 'CREATE', __('Allows creating new tables.')],
291 4
            ['Drop', 'DROP', __('Allows dropping tables.')],
292 4
            ['Index', 'INDEX', __('Allows creating and dropping indexes.')],
293 4
            ['Alter', 'ALTER', __('Allows altering the structure of existing tables.')],
294 4
            ['Create View', 'CREATE_VIEW', __('Allows creating new views.')],
295 4
            ['Show view', 'SHOW_VIEW', __('Allows performing SHOW CREATE VIEW queries.')],
296 4
            ['Trigger', 'TRIGGER', __('Allows creating and dropping triggers.')],
297 4
        ];
298
    }
299
300
    /**
301
     * Get the grants array which contains all the privilege types
302
     * and relevant grant messages
303
     *
304
     * @return string[][]
305
     */
306 52
    public function getGrantsArray(): array
307
    {
308 52
        return [
309 52
            ['Select_priv', 'SELECT', __('Allows reading data.')],
310 52
            ['Insert_priv', 'INSERT', __('Allows inserting and replacing data.')],
311 52
            ['Update_priv', 'UPDATE', __('Allows changing data.')],
312 52
            ['Delete_priv', 'DELETE', __('Allows deleting data.')],
313 52
            ['Create_priv', 'CREATE', __('Allows creating new databases and tables.')],
314 52
            ['Drop_priv', 'DROP', __('Allows dropping databases and tables.')],
315 52
            ['Reload_priv', 'RELOAD', __('Allows reloading server settings and flushing the server\'s caches.')],
316 52
            ['Shutdown_priv', 'SHUTDOWN', __('Allows shutting down the server.')],
317 52
            ['Process_priv', 'PROCESS', __('Allows viewing processes of all users.')],
318 52
            ['File_priv', 'FILE', __('Allows importing data from and exporting data into files.')],
319 52
            ['References_priv', 'REFERENCES', __('Has no effect in this MySQL version.')],
320 52
            ['Index_priv', 'INDEX', __('Allows creating and dropping indexes.')],
321 52
            ['Alter_priv', 'ALTER', __('Allows altering the structure of existing tables.')],
322 52
            ['Show_db_priv', 'SHOW DATABASES', __('Gives access to the complete list of databases.')],
323 52
            [
324 52
                'Super_priv',
325 52
                'SUPER',
326 52
                __(
327 52
                    'Allows connecting, even if maximum number of connections '
328 52
                    . 'is reached; required for most administrative operations '
329 52
                    . 'like setting global variables or killing threads of other users.',
330 52
                ),
331 52
            ],
332 52
            ['Create_tmp_table_priv', 'CREATE TEMPORARY TABLES', __('Allows creating temporary tables.')],
333 52
            ['Lock_tables_priv', 'LOCK TABLES', __('Allows locking tables for the current thread.')],
334 52
            ['Repl_slave_priv', 'REPLICATION SLAVE', __('Needed for the replication replicas.')],
335 52
            [
336 52
                'Repl_client_priv',
337 52
                'REPLICATION CLIENT',
338 52
                __('Allows the user to ask where the replicas / primaries are.'),
339 52
            ],
340 52
            ['Create_view_priv', 'CREATE VIEW', __('Allows creating new views.')],
341 52
            ['Event_priv', 'EVENT', __('Allows to set up events for the event scheduler.')],
342 52
            ['Trigger_priv', 'TRIGGER', __('Allows creating and dropping triggers.')],
343
            // for table privs:
344 52
            ['Create View_priv', 'CREATE VIEW', __('Allows creating new views.')],
345 52
            ['Show_view_priv', 'SHOW VIEW', __('Allows performing SHOW CREATE VIEW queries.')],
346
            // for table privs:
347 52
            ['Show view_priv', 'SHOW VIEW', __('Allows performing SHOW CREATE VIEW queries.')],
348 52
            [
349 52
                'Delete_history_priv',
350 52
                'DELETE HISTORY',
351
                // phpcs:ignore Generic.Files.LineLength.TooLong
352
                /* l10n: https://mariadb.com/kb/en/library/grant/#table-privileges "Remove historical rows from a table using the DELETE HISTORY statement" */
353 52
                __('Allows deleting historical rows.'),
354 52
            ],
355 52
            [
356
                // This was finally removed in the following MariaDB versions
357
                // @see https://jira.mariadb.org/browse/MDEV-20382
358 52
                'Delete versioning rows_priv',
359 52
                'DELETE HISTORY',
360
                // phpcs:ignore Generic.Files.LineLength.TooLong
361
                /* l10n: https://mariadb.com/kb/en/library/grant/#table-privileges "Remove historical rows from a table using the DELETE HISTORY statement" */
362 52
                __('Allows deleting historical rows.'),
363 52
            ],
364 52
            ['Create_routine_priv', 'CREATE ROUTINE', __('Allows creating stored routines.')],
365 52
            ['Alter_routine_priv', 'ALTER ROUTINE', __('Allows altering and dropping stored routines.')],
366 52
            ['Create_user_priv', 'CREATE USER', __('Allows creating, dropping and renaming user accounts.')],
367 52
            ['Execute_priv', 'EXECUTE', __('Allows executing stored routines.')],
368 52
        ];
369
    }
370
371
    /**
372
     * Get sql query for display privileges table
373
     *
374
     * @param string $db       the database
375
     * @param string $table    the table
376
     * @param string $username username for database connection
377
     * @param string $hostname hostname for database connection
378
     *
379
     * @return string sql query
380
     */
381 16
    public function getSqlQueryForDisplayPrivTable(
382
        string $db,
383
        string $table,
384
        string $username,
385
        string $hostname,
386
    ): string {
387 16
        if ($db === '*') {
388 8
            return 'SELECT * FROM `mysql`.`user`'
389 8
                . $this->getUserHostCondition($username, $hostname) . ';';
390
        }
391
392 12
        if ($table === '*') {
393 4
            return 'SELECT * FROM `mysql`.`db`'
394 4
                . $this->getUserHostCondition($username, $hostname)
395 4
                . ' AND `Db` = ' . $this->dbi->quoteString($db);
396
        }
397
398 12
        return 'SELECT `Table_priv`'
399 12
            . ' FROM `mysql`.`tables_priv`'
400 12
            . $this->getUserHostCondition($username, $hostname)
401 12
            . ' AND `Db` = ' . $this->dbi->quoteString($this->unescapeGrantWildcards($db))
402 12
            . ' AND `Table_name` = ' . $this->dbi->quoteString($table) . ';';
403
    }
404
405
    /**
406
     * Sets the user group from request values
407
     *
408
     * @param string $username  username
409
     * @param string $userGroup user group to set
410
     */
411 8
    public function setUserGroup(string $username, string $userGroup): void
412
    {
413 8
        $configurableMenusFeature = $this->relation->getRelationParameters()->configurableMenusFeature;
414 8
        if ($configurableMenusFeature === null) {
415 8
            return;
416
        }
417
418
        $userTable = Util::backquote($configurableMenusFeature->database)
419
            . '.' . Util::backquote($configurableMenusFeature->users);
420
421
        $sqlQuery = 'SELECT `usergroup` FROM ' . $userTable
422
            . ' WHERE `username` = ' . $this->dbi->quoteString($username, ConnectionType::ControlUser);
423
        $oldUserGroup = $this->dbi->fetchValue($sqlQuery, 0, ConnectionType::ControlUser);
424
425
        if ($oldUserGroup === false) {
426
            $updQuery = 'INSERT INTO ' . $userTable . '(`username`, `usergroup`)'
427
                . ' VALUES (' . $this->dbi->quoteString($username, ConnectionType::ControlUser) . ', '
428
                . $this->dbi->quoteString($userGroup, ConnectionType::ControlUser) . ')';
429
        } elseif ($userGroup === '') {
430
            $updQuery = 'DELETE FROM ' . $userTable
431
                . ' WHERE `username`=' . $this->dbi->quoteString($username, ConnectionType::ControlUser);
432
        } elseif ($oldUserGroup != $userGroup) {
433
            $updQuery = 'UPDATE ' . $userTable
434
                . ' SET `usergroup`=' . $this->dbi->quoteString($userGroup, ConnectionType::ControlUser)
435
                . ' WHERE `username`=' . $this->dbi->quoteString($username, ConnectionType::ControlUser);
436
        } else {
437
            return;
438
        }
439
440
        $this->dbi->queryAsControlUser($updQuery);
441
    }
442
443
    /**
444
     * Displays the privileges form table
445
     *
446
     * @param string $db     the database
447
     * @param string $table  the table
448
     * @param bool   $submit whether to display the submit button or not
449
     *
450
     * @return string html snippet
451
     *
452
     * @global array     $cfg         the phpMyAdmin configuration
453
     * @global resource  $user_link   the database connection
454
     */
455 20
    public function getHtmlToDisplayPrivilegesTable(
456
        string $db = '*',
457
        string $table = '*',
458
        bool $submit = true,
459
    ): string {
460 20
        if ($db === '*') {
461 8
            $table = '*';
462
        }
463
464 20
        $username = '';
465 20
        $hostname = '';
466 20
        $row = [];
467 20
        if (isset($GLOBALS['username'])) {
468 12
            $username = $GLOBALS['username'];
469 12
            $hostname = $GLOBALS['hostname'];
470 12
            $sqlQuery = $this->getSqlQueryForDisplayPrivTable($db, $table, $username, $hostname);
471 12
            $row = $this->dbi->fetchSingleRow($sqlQuery);
472
        }
473
474 20
        if ($row === null || $row === []) {
475 12
            if ($table === '*' && $this->dbi->isSuperUser()) {
476
                $row = [];
477
                $sqlQuery = 'SHOW COLUMNS FROM `mysql`.' . ($db === '*' ? '`user`' : '`db`') . ';';
478
479
                $res = $this->dbi->query($sqlQuery);
480
                while ($row1 = $res->fetchRow()) {
481
                    if (str_starts_with($row1[0], 'max_')) {
0 ignored issues
show
Bug introduced by
It seems like $row1[0] can also be of type null; however, parameter $haystack of str_starts_with() 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

481
                    if (str_starts_with(/** @scrutinizer ignore-type */ $row1[0], 'max_')) {
Loading history...
482
                        $row[$row1[0]] = 0;
483
                    } elseif (str_starts_with($row1[0], 'x509_') || str_starts_with($row1[0], 'ssl_')) {
484
                        $row[$row1[0]] = '';
485
                    } else {
486
                        $row[$row1[0]] = 'N';
487
                    }
488
                }
489 12
            } elseif ($table === '*') {
490 4
                $row = [];
491
            } else {
492 8
                $row = ['Table_priv' => ''];
493
            }
494
        }
495
496 20
        $columns = [];
497 20
        if (isset($row['Table_priv'])) {
498 12
            $this->fillInTablePrivileges($row);
499
500
            // get columns
501 12
            $res = $this->dbi->tryQuery(
502 12
                'SHOW COLUMNS FROM '
503 12
                . Util::backquote(
504 12
                    $this->unescapeGrantWildcards($db),
505 12
                )
506 12
                . '.' . Util::backquote($table) . ';',
507 12
            );
508 12
            if ($res) {
509 12
                while ($row1 = $res->fetchRow()) {
510 8
                    $columns[$row1[0]] = [
511 8
                        'Select' => false,
512 8
                        'Insert' => false,
513 8
                        'Update' => false,
514 8
                        'References' => false,
515 8
                    ];
516
                }
517
            }
518
        }
519
520 20
        if ($columns !== []) {
521 8
            $res = $this->dbi->query(
522 8
                'SELECT `Column_name`, `Column_priv`'
523 8
                . ' FROM `mysql`.`columns_priv`'
524 8
                . $this->getUserHostCondition($username, $hostname)
525 8
                . ' AND `Db` = ' . $this->dbi->quoteString($this->unescapeGrantWildcards($db))
526 8
                . ' AND `Table_name` = ' . $this->dbi->quoteString($table) . ';',
527 8
            );
528
529 8
            while ($row1 = $res->fetchRow()) {
530 4
                $row1[1] = explode(',', $row1[1]);
0 ignored issues
show
Bug introduced by
It seems like $row1[1] can also be of type null; however, parameter $string of explode() 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

530
                $row1[1] = explode(',', /** @scrutinizer ignore-type */ $row1[1]);
Loading history...
531 4
                foreach ($row1[1] as $current) {
532 4
                    $columns[$row1[0]][$current] = true;
533
                }
534
            }
535
        }
536
537 20
        return $this->template->render('server/privileges/privileges_table', [
538 20
            'is_global' => $db === '*',
539 20
            'is_database' => $table === '*',
540 20
            'row' => $row,
541 20
            'columns' => $columns,
542 20
            'has_submit' => $submit,
543 20
            'supports_references_privilege' => Compatibility::supportsReferencesPrivilege($this->dbi),
544 20
            'is_mariadb' => $this->dbi->isMariaDB(),
545 20
        ]);
546
    }
547
548
    /**
549
     * Get the HTML snippet for routine specific privileges
550
     *
551
     * @param string $username  username for database connection
552
     * @param string $hostname  hostname for database connection
553
     * @param string $db        the database
554
     * @param string $routine   the routine
555
     * @param string $urlDbname url encoded db name
556
     */
557
    public function getHtmlForRoutineSpecificPrivileges(
558
        string $username,
559
        string $hostname,
560
        string $db,
561
        string $routine,
562
        string $urlDbname,
563
    ): string {
564
        $privileges = $this->getRoutinePrivileges($username, $hostname, $db, $routine);
565
566
        return $this->template->render('server/privileges/edit_routine_privileges', [
567
            'username' => $username,
568
            'hostname' => $hostname,
569
            'database' => $db,
570
            'routine' => $routine,
571
            'privileges' => $privileges,
572
            'dbname' => $urlDbname,
573
            'current_user' => $this->dbi->getCurrentUser(),
574
        ]);
575
    }
576
577
    /**
578
     * Displays the fields used by the "new user" form as well as the
579
     * "change login information / copy user" form.
580
     *
581
     * @param string|null $user User name
582
     * @param string|null $host Host name
583
     *
584
     * @return string  a HTML snippet
585
     */
586 8
    public function getHtmlForLoginInformationFields(
587
        string|null $user = null,
588
        string|null $host = null,
589
    ): string {
590 8
        $GLOBALS['pred_username'] ??= null;
591 8
        $GLOBALS['pred_hostname'] ??= null;
592 8
        $GLOBALS['username'] ??= null;
593 8
        $GLOBALS['hostname'] ??= null;
594 8
        $GLOBALS['new_username'] ??= null;
595
596 8
        [$usernameLength, $hostnameLength] = $this->getUsernameAndHostnameLength();
597
598 8
        if (isset($GLOBALS['username']) && $GLOBALS['username'] === '') {
599
            $GLOBALS['pred_username'] = 'any';
600
        }
601
602 8
        $currentUser = (string) $this->dbi->fetchValue('SELECT USER();');
603 8
        $thisHost = null;
604 8
        if ($currentUser !== '') {
605
            $thisHost = str_replace(
606
                '\'',
607
                '',
608
                mb_substr(
609
                    $currentUser,
610
                    mb_strrpos($currentUser, '@') + 1,
611
                ),
612
            );
613
        }
614
615 8
        if (! isset($GLOBALS['pred_hostname']) && isset($GLOBALS['hostname'])) {
616
            $GLOBALS['pred_hostname'] = match (mb_strtolower($GLOBALS['hostname'])) {
617
                'localhost', '127.0.0.1' => 'localhost',
618
                '%' => 'any',
619
                default => 'userdefined',
620
            };
621
        }
622
623 8
        $serverVersion = $this->dbi->getVersion();
624 8
        if ($user !== null && $host !== null) {
625
            $authPlugin = $this->getCurrentAuthenticationPlugin($user, $host);
626
        } else {
627 8
            $authPlugin = $this->getDefaultAuthenticationPlugin();
628
        }
629
630 8
        $isNew = (Compatibility::isMySqlOrPerconaDb() && $serverVersion >= 50507)
631 8
            || (Compatibility::isMariaDb() && $serverVersion >= 50200);
632
633 8
        $activeAuthPlugins = ['mysql_native_password' => __('Native MySQL authentication')];
634 8
        if ($isNew) {
635
            $activeAuthPlugins = $this->plugins->getAuthentication();
636
            if (isset($activeAuthPlugins['mysql_old_password'])) {
637
                unset($activeAuthPlugins['mysql_old_password']);
638
            }
639
        }
640
641 8
        return $this->template->render('server/privileges/login_information_fields', [
642 8
            'pred_username' => $GLOBALS['pred_username'] ?? null,
643 8
            'pred_hostname' => $GLOBALS['pred_hostname'] ?? null,
644 8
            'username_length' => $usernameLength,
645 8
            'hostname_length' => $hostnameLength,
646 8
            'username' => $GLOBALS['username'] ?? null,
647 8
            'new_username' => $GLOBALS['new_username'] ?? null,
648 8
            'hostname' => $GLOBALS['hostname'] ?? null,
649 8
            'this_host' => $thisHost,
650 8
            'is_change' => $user !== null && $host !== null,
651 8
            'auth_plugin' => $authPlugin,
652 8
            'active_auth_plugins' => $activeAuthPlugins,
653 8
            'is_new' => $isNew,
654 8
        ]);
655
    }
656
657
    /**
658
     * Get username and hostname length
659
     *
660
     * @return mixed[] username length and hostname length
661
     */
662 8
    public function getUsernameAndHostnameLength(): array
663
    {
664
        /* Fallback values */
665 8
        $usernameLength = 16;
666 8
        $hostnameLength = 41;
667
668
        /* Try to get real lengths from the database */
669 8
        $fieldsInfo = $this->dbi->fetchResult(
670 8
            'SELECT COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH '
671 8
            . 'FROM information_schema.columns '
672 8
            . "WHERE table_schema = 'mysql' AND table_name = 'user' "
673 8
            . "AND COLUMN_NAME IN ('User', 'Host')",
674 8
        );
675 8
        foreach ($fieldsInfo as $val) {
676 8
            if ($val['COLUMN_NAME'] === 'User') {
677 8
                $usernameLength = $val['CHARACTER_MAXIMUM_LENGTH'];
678 8
            } elseif ($val['COLUMN_NAME'] === 'Host') {
679 8
                $hostnameLength = $val['CHARACTER_MAXIMUM_LENGTH'];
680
            }
681
        }
682
683 8
        return [$usernameLength, $hostnameLength];
684
    }
685
686
    /**
687
     * Get current authentication plugin in use for a user
688
     *
689
     * @param string $username User name
690
     * @param string $hostname Host name
691
     *
692
     * @return string authentication plugin in use
693
     */
694 4
    public function getCurrentAuthenticationPlugin(
695
        string $username,
696
        string $hostname,
697
    ): string {
698 4
        $plugin = $this->dbi->fetchValue(
699 4
            'SELECT `plugin` FROM `mysql`.`user`' . $this->getUserHostCondition($username, $hostname) . ' LIMIT 1',
700 4
        );
701
702
        // Table 'mysql'.'user' may not exist for some previous
703
        // versions of MySQL - in that case consider fallback value
704 4
        return is_string($plugin) ? $plugin : 'mysql_native_password';
705
    }
706
707
    /**
708
     * Get the default authentication plugin
709
     *
710
     * @return string|null authentication plugin
711
     */
712 8
    public function getDefaultAuthenticationPlugin(): string|null
713
    {
714 8
        if ($this->dbi->getVersion() >= 50702) {
715
            $plugin = $this->dbi->fetchValue('SELECT @@default_authentication_plugin');
716
717
            return is_string($plugin) ? $plugin : null;
718
        }
719
720
        /* Fallback (standard) value */
721 8
        return 'mysql_native_password';
722
    }
723
724
    /**
725
     * Returns all the grants for a certain user on a certain host
726
     * Used in the export privileges for all users section
727
     *
728
     * @param string $user User name
729
     * @param string $host Host name
730
     *
731
     * @return string containing all the grants text
732
     */
733 4
    public function getGrants(string $user, string $host): string
734
    {
735 4
        $grants = $this->dbi->fetchResult(
736 4
            'SHOW GRANTS FOR '
737 4
            . $this->dbi->quoteString($user) . '@'
738 4
            . $this->dbi->quoteString($host),
739 4
        );
740 4
        $response = '';
741 4
        foreach ($grants as $oneGrant) {
742 4
            $response .= $oneGrant . ";\n\n";
743
        }
744
745 4
        return $response;
746
    }
747
748
    /**
749
     * Update password and get message for password updating
750
     *
751
     * @param string $errorUrl error url
752
     * @param string $username username
753
     * @param string $hostname hostname
754
     *
755
     * @return Message success or error message after updating password
756
     */
757 4
    public function updatePassword(string $errorUrl, string $username, string $hostname): Message
758
    {
759
        // similar logic in /user-password
760 4
        $message = null;
761
762 4
        if (isset($_POST['pma_pw'], $_POST['pma_pw2']) && empty($_POST['nopass'])) {
763
            if ($_POST['pma_pw'] != $_POST['pma_pw2']) {
764
                $message = Message::error(__('The passwords aren\'t the same!'));
765
            } elseif (empty($_POST['pma_pw']) || empty($_POST['pma_pw2'])) {
766
                $message = Message::error(__('The password is empty!'));
767
            }
768
        }
769
770
        // here $nopass could be == 1
771 4
        if ($message === null) {
772 4
            $hashingFunction = 'PASSWORD';
773 4
            $serverVersion = $this->dbi->getVersion();
774 4
            $authenticationPlugin = $_POST['authentication_plugin'] ?? $this->getCurrentAuthenticationPlugin(
775 4
                $username,
776 4
                $hostname,
777 4
            );
778
779
            // Use 'ALTER USER ...' syntax for MySQL 5.7.6+
780 4
            if (Compatibility::isMySqlOrPerconaDb() && $serverVersion >= 50706) {
781 4
                if ($authenticationPlugin !== 'mysql_old_password') {
782 4
                    $queryPrefix = 'ALTER USER '
783 4
                        . $this->dbi->quoteString($username)
784 4
                        . '@' . $this->dbi->quoteString($hostname)
785 4
                        . ' IDENTIFIED WITH '
786 4
                        . $authenticationPlugin
787 4
                        . ' BY ';
788
                } else {
789
                    $queryPrefix = 'ALTER USER '
790
                        . $this->dbi->quoteString($username)
791
                        . '@' . $this->dbi->quoteString($hostname)
792
                        . ' IDENTIFIED BY ';
793
                }
794
795
                // in $sql_query which will be displayed, hide the password
796 4
                $sqlQuery = $queryPrefix . "'*'";
797
798 4
                $localQuery = $queryPrefix . $this->dbi->quoteString($_POST['pma_pw']);
799
            } elseif (Compatibility::isMariaDb() && $serverVersion >= 10000) {
800
                // MariaDB uses "SET PASSWORD" syntax to change user password.
801
                // On Galera cluster only DDL queries are replicated, since
802
                // users are stored in MyISAM storage engine.
803
                $sqlQuery = $localQuery = 'SET PASSWORD FOR '
804
                    . $this->dbi->quoteString($username)
805
                    . '@' . $this->dbi->quoteString($hostname)
806
                    . ' = PASSWORD (' . $this->dbi->quoteString($_POST['pma_pw']) . ')';
807
            } elseif (Compatibility::isMariaDb() && $serverVersion >= 50200 && $this->dbi->isSuperUser()) {
808
                // Use 'UPDATE `mysql`.`user` ...' Syntax for MariaDB 5.2+
809
                if ($authenticationPlugin === 'mysql_native_password') {
810
                    // Set the hashing method used by PASSWORD()
811
                    // to be 'mysql_native_password' type
812
                    $this->dbi->tryQuery('SET old_passwords = 0;');
813
                } elseif ($authenticationPlugin === 'sha256_password') {
814
                    // Set the hashing method used by PASSWORD()
815
                    // to be 'sha256_password' type
816
                    $this->dbi->tryQuery('SET `old_passwords` = 2;');
817
                }
818
819
                $hashedPassword = $this->getHashedPassword($_POST['pma_pw']);
820
821
                $sqlQuery = 'SET PASSWORD FOR '
822
                    . $this->dbi->quoteString($username)
823
                    . '@' . $this->dbi->quoteString($hostname) . ' = '
824
                    . ($_POST['pma_pw'] == ''
825
                        ? '\'\''
826
                        : $hashingFunction . '(\''
827
                        . preg_replace('@.@s', '*', $_POST['pma_pw']) . '\')');
828
829
                $localQuery = 'UPDATE `mysql`.`user` SET '
830
                    . " `authentication_string` = '" . $hashedPassword
831
                    . "', `Password` = '', "
832
                    . " `plugin` = '" . $authenticationPlugin . "'"
833
                    . $this->getUserHostCondition($username, $hostname) . ';';
834
            } else {
835
                // USE 'SET PASSWORD ...' syntax for rest of the versions
836
                // Backup the old value, to be reset later
837
                $row = $this->dbi->fetchSingleRow('SELECT @@old_passwords;');
838
                $origValue = $row['@@old_passwords'];
839
                $updatePluginQuery = 'UPDATE `mysql`.`user` SET'
840
                    . " `plugin` = '" . $authenticationPlugin . "'"
841
                    . $this->getUserHostCondition($username, $hostname) . ';';
842
843
                // Update the plugin for the user
844
                if (! $this->dbi->tryQuery($updatePluginQuery)) {
845
                    Generator::mysqlDie(
846
                        $this->dbi->getError(),
847
                        $updatePluginQuery,
848
                        false,
849
                        $errorUrl,
850
                    );
851
                }
852
853
                $this->dbi->tryQuery('FLUSH PRIVILEGES;');
854
855
                if ($authenticationPlugin === 'mysql_native_password') {
856
                    // Set the hashing method used by PASSWORD()
857
                    // to be 'mysql_native_password' type
858
                    $this->dbi->tryQuery('SET old_passwords = 0;');
859
                } elseif ($authenticationPlugin === 'sha256_password') {
860
                    // Set the hashing method used by PASSWORD()
861
                    // to be 'sha256_password' type
862
                    $this->dbi->tryQuery('SET `old_passwords` = 2;');
863
                }
864
865
                $sqlQuery = 'SET PASSWORD FOR '
866
                    . $this->dbi->quoteString($username)
867
                    . '@' . $this->dbi->quoteString($hostname) . ' = '
868
                    . ($_POST['pma_pw'] == ''
869
                        ? '\'\''
870
                        : $hashingFunction . '(\''
871
                        . preg_replace('@.@s', '*', $_POST['pma_pw']) . '\')');
872
873
                $localQuery = 'SET PASSWORD FOR '
874
                    . $this->dbi->quoteString($username)
875
                    . '@' . $this->dbi->quoteString($hostname) . ' = '
876
                    . ($_POST['pma_pw'] == '' ? '\'\'' : $hashingFunction
877
                    . '(' . $this->dbi->quoteString($_POST['pma_pw']) . ')');
878
            }
879
880 4
            if (! $this->dbi->tryQuery($localQuery)) {
881
                Generator::mysqlDie(
882
                    $this->dbi->getError(),
883
                    $sqlQuery,
884
                    false,
885
                    $errorUrl,
886
                );
887
            }
888
889
            // Flush privileges after successful password change
890 4
            $this->dbi->tryQuery('FLUSH PRIVILEGES;');
891
892 4
            $message = Message::success(
893 4
                __('The password for %s was changed successfully.'),
894 4
            );
895 4
            $message->addParam('\'' . $username . '\'@\'' . $hostname . '\'');
896 4
            if (isset($origValue)) {
897
                $this->dbi->tryQuery('SET `old_passwords` = ' . $origValue . ';');
898
            }
899
        }
900
901 4
        return $message;
902
    }
903
904
    /**
905
     * Revokes privileges and get message and SQL query for privileges revokes
906
     *
907
     * @param string $dbname    database name
908
     * @param string $tablename table name
909
     * @param string $username  username
910
     * @param string $hostname  host name
911
     * @param string $itemType  item type
912
     *
913
     * @return array{Message, string} ($message, $sql_query)
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{Message, string} at position 2 could not be parsed: Expected ':' at position 2, but found 'Message'.
Loading history...
914
     */
915 4
    public function getMessageAndSqlQueryForPrivilegesRevoke(
916
        string $dbname,
917
        string $tablename,
918
        string $username,
919
        string $hostname,
920
        string $itemType,
921
    ): array {
922 4
        $dbAndTable = $this->wildcardEscapeForGrant($dbname, $tablename);
923
924 4
        $sqlQuery0 = 'REVOKE ALL PRIVILEGES ON ' . $itemType . ' ' . $dbAndTable
925 4
            . ' FROM '
926 4
            . $this->dbi->quoteString($username) . '@'
927 4
            . $this->dbi->quoteString($hostname) . ';';
928
929 4
        $sqlQuery1 = 'REVOKE GRANT OPTION ON ' . $itemType . ' ' . $dbAndTable
930 4
            . ' FROM ' . $this->dbi->quoteString($username) . '@'
931 4
            . $this->dbi->quoteString($hostname) . ';';
932
933 4
        $this->dbi->query($sqlQuery0);
934 4
        if (! $this->dbi->tryQuery($sqlQuery1)) {
935
            // this one may fail, too...
936
            $sqlQuery1 = '';
937
        }
938
939 4
        $sqlQuery = $sqlQuery0 . ' ' . $sqlQuery1;
940 4
        $message = Message::success(
941 4
            __('You have revoked the privileges for %s.'),
942 4
        );
943 4
        $message->addParam('\'' . $username . '\'@\'' . $hostname . '\'');
944
945 4
        return [$message, $sqlQuery];
946
    }
947
948
    /**
949
     * Get REQUIRE clause
950
     *
951
     * @return string REQUIRE clause
952
     */
953 36
    public function getRequireClause(): string
954
    {
955
        /** @var string|null $sslType */
956 36
        $sslType = $_POST['ssl_type'] ?? $GLOBALS['ssl_type'] ?? null;
957
        /** @var string|null $sslCipher */
958 36
        $sslCipher = $_POST['ssl_cipher'] ?? $GLOBALS['ssl_cipher'] ?? null;
959
        /** @var string|null $x509Issuer */
960 36
        $x509Issuer = $_POST['x509_issuer'] ?? $GLOBALS['x509_issuer'] ?? null;
961
        /** @var string|null $x509Subject */
962 36
        $x509Subject = $_POST['x509_subject'] ?? $GLOBALS['x509_subject'] ?? null;
963
964 36
        if ($sslType === 'SPECIFIED') {
965
            $require = [];
966
            if (is_string($sslCipher) && $sslCipher !== '') {
967
                $require[] = 'CIPHER ' . $this->dbi->quoteString($sslCipher);
968
            }
969
970
            if (is_string($x509Issuer) && $x509Issuer !== '') {
971
                $require[] = 'ISSUER ' . $this->dbi->quoteString($x509Issuer);
972
            }
973
974
            if (is_string($x509Subject) && $x509Subject !== '') {
975
                $require[] = 'SUBJECT ' . $this->dbi->quoteString($x509Subject);
976
            }
977
978
            if ($require !== []) {
979
                $requireClause = ' REQUIRE ' . implode(' AND ', $require);
980
            } else {
981
                $requireClause = ' REQUIRE NONE';
982
            }
983 36
        } elseif ($sslType === 'X509') {
984
            $requireClause = ' REQUIRE X509';
985 36
        } elseif ($sslType === 'ANY') {
986
            $requireClause = ' REQUIRE SSL';
987
        } else {
988 36
            $requireClause = ' REQUIRE NONE';
989
        }
990
991 36
        return $requireClause;
992
    }
993
994
    /**
995
     * Get a WITH clause for 'update privileges' and 'add user'
996
     */
997 40
    public function getWithClauseForAddUserAndUpdatePrivs(): string
998
    {
999 40
        $sqlQuery = '';
1000
        if (
1001 40
            isset($_POST['Grant_priv']) && $_POST['Grant_priv'] === 'Y'
1002 40
            && ! (Compatibility::isMySqlOrPerconaDb() && $this->dbi->getVersion() >= 80011)
1003
        ) {
1004 8
            $sqlQuery .= ' GRANT OPTION';
1005
        }
1006
1007 40
        if (isset($_POST['max_questions'])) {
1008 12
            $maxQuestions = (int) $_POST['max_questions'];
1009 12
            $maxQuestions = max(0, $maxQuestions);
1010 12
            $sqlQuery .= ' MAX_QUERIES_PER_HOUR ' . $maxQuestions;
1011
        }
1012
1013 40
        if (isset($_POST['max_connections'])) {
1014 12
            $maxConnections = (int) $_POST['max_connections'];
1015 12
            $maxConnections = max(0, $maxConnections);
1016 12
            $sqlQuery .= ' MAX_CONNECTIONS_PER_HOUR ' . $maxConnections;
1017
        }
1018
1019 40
        if (isset($_POST['max_updates'])) {
1020 12
            $maxUpdates = (int) $_POST['max_updates'];
1021 12
            $maxUpdates = max(0, $maxUpdates);
1022 12
            $sqlQuery .= ' MAX_UPDATES_PER_HOUR ' . $maxUpdates;
1023
        }
1024
1025 40
        if (isset($_POST['max_user_connections'])) {
1026 12
            $maxUserConnections = (int) $_POST['max_user_connections'];
1027 12
            $maxUserConnections = max(0, $maxUserConnections);
1028 12
            $sqlQuery .= ' MAX_USER_CONNECTIONS ' . $maxUserConnections;
1029
        }
1030
1031 40
        return $sqlQuery !== '' ? ' WITH' . $sqlQuery : '';
1032
    }
1033
1034
    /**
1035
     * Get HTML for addUsersForm, This function call if isset($_GET['adduser'])
1036
     *
1037
     * @param string $dbname database name
1038
     *
1039
     * @return string HTML for addUserForm
1040
     */
1041 4
    public function getHtmlForAddUser(string $dbname): string
1042
    {
1043 4
        $isGrantUser = $this->dbi->isGrantUser();
1044 4
        $loginInformationFieldsNew = $this->getHtmlForLoginInformationFields();
1045 4
        $privilegesTable = '';
1046 4
        if ($isGrantUser) {
1047 4
            $privilegesTable = $this->getHtmlToDisplayPrivilegesTable('*', '*', false);
1048
        }
1049
1050 4
        return $this->template->render('server/privileges/add_user', [
1051 4
            'database' => $dbname,
1052 4
            'login_information_fields_new' => $loginInformationFieldsNew,
1053 4
            'is_grant_user' => $isGrantUser,
1054 4
            'privileges_table' => $privilegesTable,
1055 4
        ]);
1056
    }
1057
1058
    /** @return mixed[] */
1059
    public function getAllPrivileges(DatabaseName $db, TableName|null $table = null): array
1060
    {
1061
        $databasePrivileges = $this->getGlobalAndDatabasePrivileges($db);
1062
        $tablePrivileges = [];
1063
        if ($table !== null) {
1064
            $tablePrivileges = $this->getTablePrivileges($db, $table);
1065
        }
1066
1067
        $routinePrivileges = $this->getRoutinesPrivileges($db);
1068
        $allPrivileges = array_merge($databasePrivileges, $tablePrivileges, $routinePrivileges);
1069
1070
        $privileges = [];
1071
        foreach ($allPrivileges as $privilege) {
1072
            $userHost = $privilege['User'] . '@' . $privilege['Host'];
1073
            $privileges[$userHost] ??= [];
1074
            $privileges[$userHost]['user'] = (string) $privilege['User'];
1075
            $privileges[$userHost]['host'] = (string) $privilege['Host'];
1076
            $privileges[$userHost]['privileges'] ??= [];
1077
            $privileges[$userHost]['privileges'][] = $this->getSpecificPrivilege($privilege);
1078
        }
1079
1080
        return $privileges;
1081
    }
1082
1083
    /**
1084
     * @param mixed[] $row Array with user privileges
1085
     *
1086
     * @return mixed[]
1087
     */
1088
    private function getSpecificPrivilege(array $row): array
1089
    {
1090
        $privilege = ['type' => $row['Type'], 'database' => $row['Db']];
1091
        if ($row['Type'] === 'r') {
1092
            $privilege['routine'] = $row['Routine_name'];
1093
            $privilege['has_grant'] = str_contains($row['Proc_priv'], 'Grant');
1094
            $privilege['privileges'] = explode(',', $row['Proc_priv']);
1095
        } elseif ($row['Type'] === 't') {
1096
            $privilege['table'] = $row['Table_name'];
1097
            $privilege['has_grant'] = str_contains($row['Table_priv'], 'Grant');
1098
            $tablePrivs = explode(',', $row['Table_priv']);
1099
            $specificPrivileges = [];
1100
            $grantsArr = $this->getTableGrantsArray();
1101
            foreach ($grantsArr as $grant) {
1102
                $specificPrivileges[$grant[0]] = 'N';
1103
                foreach ($tablePrivs as $tablePriv) {
1104
                    if ($grant[0] !== $tablePriv) {
1105
                        continue;
1106
                    }
1107
1108
                    $specificPrivileges[$grant[0]] = 'Y';
1109
                }
1110
            }
1111
1112
            $privilege['privileges'] = $this->extractPrivInfo($specificPrivileges, true, true);
1113
        } else {
1114
            $privilege['has_grant'] = $row['Grant_priv'] === 'Y';
1115
            $privilege['privileges'] = $this->extractPrivInfo($row, true);
1116
        }
1117
1118
        return $privilege;
1119
    }
1120
1121
    /** @return array<int, array<string|null>> */
1122
    private function getGlobalAndDatabasePrivileges(DatabaseName $db): array
1123
    {
1124
        $listOfPrivileges = '`Select_priv`,
1125
            `Insert_priv`,
1126
            `Update_priv`,
1127
            `Delete_priv`,
1128
            `Create_priv`,
1129
            `Drop_priv`,
1130
            `Grant_priv`,
1131
            `Index_priv`,
1132
            `Alter_priv`,
1133
            `References_priv`,
1134
            `Create_tmp_table_priv`,
1135
            `Lock_tables_priv`,
1136
            `Create_view_priv`,
1137
            `Show_view_priv`,
1138
            `Create_routine_priv`,
1139
            `Alter_routine_priv`,
1140
            `Execute_priv`,
1141
            `Event_priv`,
1142
            `Trigger_priv`,';
1143
1144
        $listOfComparedPrivileges = 'BINARY `Select_priv` = \'N\' AND
1145
            BINARY `Insert_priv` = \'N\' AND
1146
            BINARY `Update_priv` = \'N\' AND
1147
            BINARY `Delete_priv` = \'N\' AND
1148
            BINARY `Create_priv` = \'N\' AND
1149
            BINARY `Drop_priv` = \'N\' AND
1150
            BINARY `Grant_priv` = \'N\' AND
1151
            BINARY `References_priv` = \'N\' AND
1152
            BINARY `Create_tmp_table_priv` = \'N\' AND
1153
            BINARY `Lock_tables_priv` = \'N\' AND
1154
            BINARY `Create_view_priv` = \'N\' AND
1155
            BINARY `Show_view_priv` = \'N\' AND
1156
            BINARY `Create_routine_priv` = \'N\' AND
1157
            BINARY `Alter_routine_priv` = \'N\' AND
1158
            BINARY `Execute_priv` = \'N\' AND
1159
            BINARY `Event_priv` = \'N\' AND
1160
            BINARY `Trigger_priv` = \'N\'';
1161
1162
        $query = '
1163
            (
1164
                SELECT `User`, `Host`, ' . $listOfPrivileges . ' \'*\' AS `Db`, \'g\' AS `Type`
1165
                FROM `mysql`.`user`
1166
                WHERE NOT (' . $listOfComparedPrivileges . ')
1167
            )
1168
            UNION
1169
            (
1170
                SELECT `User`, `Host`, ' . $listOfPrivileges . ' `Db`, \'d\' AS `Type`
1171
                FROM `mysql`.`db`
1172
                WHERE ' . $this->dbi->quoteString($db->getName()) . ' LIKE `Db` AND NOT ('
1173
                . $listOfComparedPrivileges . ')
1174
            )
1175
            ORDER BY `User` ASC, `Host` ASC, `Db` ASC;
1176
        ';
1177
1178
        return $this->dbi->query($query)->fetchAllAssoc();
1179
    }
1180
1181
    /** @return array<int, array<string|null>> */
1182
    private function getTablePrivileges(DatabaseName $db, TableName $table): array
1183
    {
1184
        $query = '
1185
            SELECT `User`, `Host`, `Db`, \'t\' AS `Type`, `Table_name`, `Table_priv`
1186
            FROM `mysql`.`tables_priv`
1187
            WHERE
1188
                ? LIKE `Db` AND
1189
                ? LIKE `Table_name` AND
1190
                NOT (`Table_priv` = \'\' AND Column_priv = \'\')
1191
            ORDER BY `User` ASC, `Host` ASC, `Db` ASC, `Table_priv` ASC;
1192
        ';
1193
        $statement = $this->dbi->prepare($query);
1194
        if ($statement === null || ! $statement->execute([$db->getName(), $table->getName()])) {
1195
            return [];
1196
        }
1197
1198
        return $statement->getResult()->fetchAllAssoc();
1199
    }
1200
1201
    /** @return array<int, array<string|null>> */
1202
    private function getRoutinesPrivileges(DatabaseName $db): array
1203
    {
1204
        $query = '
1205
            SELECT *, \'r\' AS `Type`
1206
            FROM `mysql`.`procs_priv`
1207
            WHERE Db = ' . $this->dbi->quoteString($db->getName()) . ';';
1208
1209
        return $this->dbi->query($query)->fetchAllAssoc();
1210
    }
1211
1212
    /**
1213
     * Get HTML error for View Users form
1214
     * For non superusers such as grant/create users
1215
     */
1216 4
    private function getHtmlForViewUsersError(): string
1217
    {
1218 4
        return Message::error(
1219 4
            __('Not enough privilege to view users.'),
1220 4
        )->getDisplay();
1221
    }
1222
1223
    /**
1224
     * Returns edit, revoke or export link for a user.
1225
     *
1226
     * @param string $linktype    The link type (edit | revoke | export)
1227
     * @param string $username    User name
1228
     * @param string $hostname    Host name
1229
     * @param string $dbname      Database name
1230
     * @param string $tablename   Table name
1231
     * @param string $routinename Routine name
1232
     * @param string $initial     Initial value
1233
     *
1234
     * @return string HTML code with link
1235
     */
1236 8
    public function getUserLink(
1237
        string $linktype,
1238
        string $username,
1239
        string $hostname,
1240
        string $dbname = '',
1241
        string $tablename = '',
1242
        string $routinename = '',
1243
        string $initial = '',
1244
    ): string {
1245 8
        $linkClass = '';
1246 8
        if ($linktype === 'edit') {
1247 8
            $linkClass = 'edit_user_anchor';
1248 8
        } elseif ($linktype === 'export') {
1249 8
            $linkClass = 'export_user_anchor ajax';
1250
        }
1251
1252 8
        $params = ['username' => $username, 'hostname' => $hostname];
1253
        switch ($linktype) {
1254 8
            case 'edit':
1255 8
                $params['dbname'] = $dbname;
1256 8
                $params['tablename'] = $tablename;
1257 8
                $params['routinename'] = $routinename;
1258 8
                break;
1259 8
            case 'revoke':
1260 8
                $params['dbname'] = $dbname;
1261 8
                $params['tablename'] = $tablename;
1262 8
                $params['routinename'] = $routinename;
1263 8
                $params['revokeall'] = 1;
1264 8
                break;
1265 8
            case 'export':
1266 8
                $params['initial'] = $initial;
1267 8
                $params['export'] = 1;
1268 8
                break;
1269
        }
1270
1271 8
        $action = [];
1272
        switch ($linktype) {
1273 8
            case 'edit':
1274 8
                $action['icon'] = 'b_usredit';
1275 8
                $action['text'] = __('Edit privileges');
1276 8
                break;
1277 8
            case 'revoke':
1278 8
                $action['icon'] = 'b_usrdrop';
1279 8
                $action['text'] = __('Revoke');
1280 8
                break;
1281 8
            case 'export':
1282 8
                $action['icon'] = 'b_tblexport';
1283 8
                $action['text'] = __('Export');
1284 8
                break;
1285
        }
1286
1287 8
        return $this->template->render('server/privileges/get_user_link', [
1288 8
            'link_class' => $linkClass,
1289 8
            'is_revoke' => $linktype === 'revoke',
1290 8
            'url_params' => $params,
1291 8
            'action' => $action,
1292 8
        ]);
1293
    }
1294
1295
    /**
1296
     * Returns number of defined user groups
1297
     */
1298 4
    public function getUserGroupCount(ConfigurableMenusFeature $configurableMenusFeature): int
1299
    {
1300 4
        $userGroupTable = Util::backquote($configurableMenusFeature->database)
1301 4
            . '.' . Util::backquote($configurableMenusFeature->userGroups);
1302 4
        $sqlQuery = 'SELECT COUNT(*) FROM ' . $userGroupTable;
1303
1304 4
        return (int) $this->dbi->fetchValue($sqlQuery, 0, ConnectionType::ControlUser);
1305
    }
1306
1307
    /**
1308
     * Returns name of user group that user is part of
1309
     *
1310
     * @param string $username User name
1311
     *
1312
     * @return string|null usergroup if found or null if not found
1313
     */
1314 4
    public function getUserGroupForUser(string $username): string|null
1315
    {
1316 4
        $configurableMenusFeature = $this->relation->getRelationParameters()->configurableMenusFeature;
1317 4
        if ($configurableMenusFeature === null) {
1318
            return null;
1319
        }
1320
1321 4
        $userTable = Util::backquote($configurableMenusFeature->database)
1322 4
            . '.' . Util::backquote($configurableMenusFeature->users);
1323 4
        $sqlQuery = 'SELECT `usergroup` FROM ' . $userTable
1324 4
            . ' WHERE `username` = \'' . $username . '\''
1325 4
            . ' LIMIT 1';
1326
1327 4
        $usergroup = $this->dbi->fetchValue($sqlQuery, 0, ConnectionType::ControlUser);
1328
1329 4
        if ($usergroup === false) {
1330
            return null;
1331
        }
1332
1333 4
        return $usergroup;
1334
    }
1335
1336
    /**
1337
     * This function return the extra data array for the ajax behavior
1338
     *
1339
     * @param string $password password
1340
     * @param string $sqlQuery sql query
1341
     * @param string $hostname hostname
1342
     * @param string $username username
1343
     *
1344
     * @return (string|bool)[]
1345
     */
1346 4
    public function getExtraDataForAjaxBehavior(
1347
        string $password,
1348
        string $sqlQuery,
1349
        string $hostname,
1350
        string $username,
1351
    ): array {
1352 4
        if (isset($GLOBALS['dbname'])) {
1353
            //if (preg_match('/\\\\(?:_|%)/i', $dbname)) {
1354 4
            if (preg_match('/(?<!\\\\)(?:_|%)/', $GLOBALS['dbname'])) {
1355 4
                $dbnameIsWildcard = true;
1356
            } else {
1357
                $dbnameIsWildcard = false;
1358
            }
1359
        }
1360
1361 4
        $configurableMenusFeature = $this->relation->getRelationParameters()->configurableMenusFeature;
1362
1363 4
        $userGroupCount = 0;
1364 4
        if ($configurableMenusFeature !== null) {
1365
            $userGroupCount = $this->getUserGroupCount($configurableMenusFeature);
1366
        }
1367
1368 4
        $extraData = [];
1369 4
        if ($sqlQuery !== '') {
1370 4
            $extraData['sql_query'] = Generator::getMessage('', $sqlQuery);
1371
        }
1372
1373 4
        if (isset($_POST['change_copy'])) {
1374 4
            $user = [
1375 4
                'name' => $username,
1376 4
                'host' => $hostname,
1377 4
                'has_password' => $password !== '' || isset($_POST['pma_pw']),
1378 4
                'privileges' => implode(', ', $this->extractPrivInfo(null, true)),
1379 4
                'has_group' => $configurableMenusFeature !== null,
1380 4
                'has_group_edit' => $configurableMenusFeature !== null && $userGroupCount > 0,
1381 4
                'has_grant' => isset($_POST['Grant_priv']) && $_POST['Grant_priv'] === 'Y',
1382 4
            ];
1383 4
            $extraData['new_user_string'] = $this->template->render('server/privileges/new_user_ajax', [
1384 4
                'user' => $user,
1385 4
                'is_grantuser' => $this->dbi->isGrantUser(),
1386 4
                'initial' => $_GET['initial'] ?? '',
1387 4
            ]);
1388
1389
            /**
1390
             * Generate the string for this alphabet's initial, to update the user
1391
             * pagination
1392
             */
1393 4
            $newUserInitial = mb_strtoupper(
1394 4
                mb_substr($username, 0, 1),
1395 4
            );
1396 4
            $newUserInitialString = '<a href="';
1397 4
            $newUserInitialString .= Url::getFromRoute('/server/privileges', ['initial' => $newUserInitial]);
1398 4
            $newUserInitialString .= '">' . $newUserInitial . '</a>';
1399 4
            $extraData['new_user_initial'] = $newUserInitial;
1400 4
            $extraData['new_user_initial_string'] = $newUserInitialString;
1401
        }
1402
1403 4
        if (isset($_POST['update_privs'])) {
1404 4
            $extraData['db_specific_privs'] = false;
1405 4
            $extraData['db_wildcard_privs'] = false;
1406 4
            if (isset($dbnameIsWildcard)) {
1407 4
                $extraData['db_specific_privs'] = ! $dbnameIsWildcard;
1408 4
                $extraData['db_wildcard_privs'] = $dbnameIsWildcard;
1409
            }
1410
1411 4
            $newPrivileges = implode(', ', $this->extractPrivInfo(null, true));
1412
1413 4
            $extraData['new_privileges'] = $newPrivileges;
1414
        }
1415
1416 4
        if (isset($_GET['validate_username'])) {
1417 4
            $sqlQuery = 'SELECT * FROM `mysql`.`user` WHERE `User` = '
1418 4
                . $this->dbi->quoteString($_GET['username']) . ';';
1419 4
            $res = $this->dbi->query($sqlQuery);
1420 4
            $extraData['user_exists'] = $res->fetchRow() !== [];
1421
        }
1422
1423 4
        return $extraData;
1424
    }
1425
1426
    /**
1427
     * no db name given, so we want all privs for the given user
1428
     * db name was given, so we want all user specific rights for this db
1429
     * So this function returns user rights as an array
1430
     *
1431
     * @param string $username username
1432
     * @param string $hostname host name
1433
     * @param string $type     database or table
1434
     * @param string $dbname   database name
1435
     *
1436
     * @return mixed[] database rights
1437
     */
1438 4
    public function getUserSpecificRights(string $username, string $hostname, string $type, string $dbname = ''): array
1439
    {
1440 4
        $userHostCondition = $this->getUserHostCondition($username, $hostname);
1441
1442 4
        if ($type === 'database') {
1443 4
            $tablesToSearchForUsers = ['tables_priv', 'columns_priv', 'procs_priv'];
1444 4
            $dbOrTableName = 'Db';
1445 4
        } elseif ($type === 'table') {
1446 4
            $userHostCondition .= ' AND `Db` LIKE ' . $this->dbi->quoteString($dbname);
1447 4
            $tablesToSearchForUsers = ['columns_priv'];
1448 4
            $dbOrTableName = 'Table_name';
1449
        } else { // routine
1450
            $userHostCondition .= ' AND `Db` LIKE ' . $this->dbi->quoteString($dbname);
1451
            $tablesToSearchForUsers = ['procs_priv'];
1452
            $dbOrTableName = 'Routine_name';
1453
        }
1454
1455
        // we also want privileges for this user not in table `db` but in other table
1456 4
        $tables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`;');
1457
1458 4
        $dbRightsSqls = [];
1459 4
        foreach ($tablesToSearchForUsers as $tableSearchIn) {
1460 4
            if (! in_array($tableSearchIn, $tables, true)) {
1461 4
                continue;
1462
            }
1463
1464 4
            $dbRightsSqls[] = 'SELECT DISTINCT `' . $dbOrTableName
1465 4
                . '` FROM `mysql`.' . Util::backquote($tableSearchIn)
1466 4
                . $userHostCondition;
1467
        }
1468
1469 4
        $userDefaults = [$dbOrTableName => '', 'Grant_priv' => 'N', 'privs' => ['USAGE'], 'Column_priv' => true];
1470
1471
        // for the rights
1472 4
        $dbRights = [];
1473
1474 4
        $dbRightsSql = '(' . implode(') UNION (', $dbRightsSqls) . ')'
1475 4
            . ' ORDER BY `' . $dbOrTableName . '` ASC';
1476
1477 4
        $dbRightsResult = $this->dbi->query($dbRightsSql);
1478
1479 4
        while ($dbRightsRow = $dbRightsResult->fetchAssoc()) {
1480
            $dbRightsRow = array_merge($userDefaults, $dbRightsRow);
1481
            if ($type === 'database') {
1482
                // only Db names in the table `mysql`.`db` uses wildcards
1483
                // as we are in the db specific rights display we want
1484
                // all db names escaped, also from other sources
1485
                $dbRightsRow['Db'] = $this->escapeGrantWildcards($dbRightsRow['Db']);
1486
            }
1487
1488
            $dbRights[$dbRightsRow[$dbOrTableName]] = $dbRightsRow;
1489
        }
1490
1491 4
        $sqlQuery = match ($type) {
1492 4
            'database' => 'SELECT * FROM `mysql`.`db`'
1493 4
                . $userHostCondition . ' ORDER BY `Db` ASC',
1494 4
            'table' => 'SELECT `Table_name`,'
1495 4
                . ' `Table_priv`,'
1496 4
                . ' IF(`Column_priv` = _latin1 \'\', 0, 1)'
1497 4
                . ' AS \'Column_priv\''
1498 4
                . ' FROM `mysql`.`tables_priv`'
1499 4
                . $userHostCondition
1500 4
                . ' ORDER BY `Table_name` ASC;',
1501 4
            default => 'SELECT `Routine_name`, `Proc_priv`'
1502 4
                . ' FROM `mysql`.`procs_priv`'
1503 4
                . $userHostCondition
1504 4
                . ' ORDER BY `Routine_name`',
1505 4
        };
1506
1507 4
        $result = $this->dbi->query($sqlQuery);
1508
1509 4
        while ($row = $result->fetchAssoc()) {
1510
            if (isset($dbRights[$row[$dbOrTableName]])) {
1511
                $dbRights[$row[$dbOrTableName]] = array_merge($dbRights[$row[$dbOrTableName]], $row);
1512
            } else {
1513
                $dbRights[$row[$dbOrTableName]] = $row;
1514
            }
1515
1516
            if ($type !== 'database') {
1517
                continue;
1518
            }
1519
1520
            // there are db specific rights for this user
1521
            // so we can drop this db rights
1522
            $dbRights[$row['Db']]['can_delete'] = true;
1523
        }
1524
1525 4
        return $dbRights;
1526
    }
1527
1528
    /**
1529
     * Parses Proc_priv data
1530
     *
1531
     * @param string $privs Proc_priv
1532
     *
1533
     * @return array<string, string>
1534
     */
1535
    public function parseProcPriv(string $privs): array
1536
    {
1537
        $result = ['Alter_routine_priv' => 'N', 'Execute_priv' => 'N', 'Grant_priv' => 'N'];
1538
        foreach (explode(',', $privs) as $priv) {
1539
            if ($priv === 'Alter Routine') {
1540
                $result['Alter_routine_priv'] = 'Y';
1541
            } else {
1542
                $result[$priv . '_priv'] = 'Y';
1543
            }
1544
        }
1545
1546
        return $result;
1547
    }
1548
1549
    /**
1550
     * Get a HTML table for display user's table specific or database specific rights
1551
     *
1552
     * @param string $username username
1553
     * @param string $hostname host name
1554
     * @param string $type     database, table or routine
1555
     * @param string $dbname   database name
1556
     */
1557 4
    public function getHtmlForAllTableSpecificRights(
1558
        string $username,
1559
        string $hostname,
1560
        string $type,
1561
        string $dbname = '',
1562
    ): string {
1563 4
        $uiData = [
1564 4
            'database' => [
1565 4
                'form_id' => 'database_specific_priv',
1566 4
                'sub_menu_label' => __('Database'),
1567 4
                'legend' => __('Database-specific privileges'),
1568 4
                'type_label' => __('Database'),
1569 4
            ],
1570 4
            'table' => [
1571 4
                'form_id' => 'table_specific_priv',
1572 4
                'sub_menu_label' => __('Table'),
1573 4
                'legend' => __('Table-specific privileges'),
1574 4
                'type_label' => __('Table'),
1575 4
            ],
1576 4
            'routine' => [
1577 4
                'form_id' => 'routine_specific_priv',
1578 4
                'sub_menu_label' => __('Routine'),
1579 4
                'legend' => __('Routine-specific privileges'),
1580 4
                'type_label' => __('Routine'),
1581 4
            ],
1582 4
        ];
1583
1584
        /**
1585
         * no db name given, so we want all privs for the given user
1586
         * db name was given, so we want all user specific rights for this db
1587
         */
1588 4
        $dbRights = $this->getUserSpecificRights($username, $hostname, $type, $dbname);
1589 4
        ksort($dbRights);
1590
1591 4
        $foundRows = [];
1592 4
        $privileges = [];
1593 4
        foreach ($dbRights as $row) {
1594
            $onePrivilege = [];
1595
1596
            $paramTableName = '';
1597
            $paramRoutineName = '';
1598
1599
            if ($type === 'database') {
1600
                $name = $row['Db'];
1601
                $onePrivilege['grant'] = $row['Grant_priv'] === 'Y';
1602
                $onePrivilege['table_privs'] = ! empty($row['Table_priv'])
1603
                    || ! empty($row['Column_priv']);
1604
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
1605
1606
                $paramDbName = $row['Db'];
1607
            } elseif ($type === 'table') {
1608
                $name = $row['Table_name'];
1609
                $onePrivilege['grant'] = in_array(
1610
                    'Grant',
1611
                    explode(',', $row['Table_priv']),
1612
                    true,
1613
                );
1614
                $onePrivilege['column_privs'] = ! empty($row['Column_priv']);
1615
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
1616
1617
                $paramDbName = $this->escapeGrantWildcards($dbname);
1618
                $paramTableName = $row['Table_name'];
1619
            } else { // routine
1620
                $name = $row['Routine_name'];
1621
                $onePrivilege['grant'] = in_array(
1622
                    'Grant',
1623
                    explode(',', $row['Proc_priv']),
1624
                    true,
1625
                );
1626
1627
                $privs = $this->parseProcPriv($row['Proc_priv']);
1628
                $onePrivilege['privileges'] = implode(
1629
                    ',',
1630
                    $this->extractPrivInfo($privs, true),
1631
                );
1632
1633
                $paramDbName = $this->escapeGrantWildcards($dbname);
1634
                $paramRoutineName = $row['Routine_name'];
1635
            }
1636
1637
            $foundRows[] = $name;
1638
            $onePrivilege['name'] = $name;
1639
1640
            $onePrivilege['edit_link'] = '';
1641
            if ($this->dbi->isGrantUser()) {
1642
                $onePrivilege['edit_link'] = $this->getUserLink(
1643
                    'edit',
1644
                    $username,
1645
                    $hostname,
1646
                    $paramDbName,
1647
                    $paramTableName,
1648
                    $paramRoutineName,
1649
                );
1650
            }
1651
1652
            $onePrivilege['revoke_link'] = '';
1653
            if ($type !== 'database' || ! empty($row['can_delete'])) {
1654
                $onePrivilege['revoke_link'] = $this->getUserLink(
1655
                    'revoke',
1656
                    $username,
1657
                    $hostname,
1658
                    $paramDbName,
1659
                    $paramTableName,
1660
                    $paramRoutineName,
1661
                );
1662
            }
1663
1664
            $privileges[] = $onePrivilege;
1665
        }
1666
1667 4
        $data = $uiData[$type];
1668 4
        $data['privileges'] = $privileges;
1669 4
        $data['username'] = $username;
1670 4
        $data['hostname'] = $hostname;
1671 4
        $data['database'] = $dbname;
1672 4
        $data['escaped_database'] = $this->escapeGrantWildcards($dbname);
1673 4
        $data['type'] = $type;
1674
1675 4
        if ($type === 'database') {
1676 4
            $predDbArray = $this->dbi->getDatabaseList();
1677 4
            $databasesToSkip = ['information_schema', 'performance_schema'];
1678
1679 4
            $databases = [];
1680 4
            $escapedDatabases = [];
1681 4
            foreach ($predDbArray as $currentDb) {
1682 4
                if (in_array($currentDb, $databasesToSkip, true)) {
1683
                    continue;
1684
                }
1685
1686 4
                $currentDbEscaped = $this->escapeGrantWildcards($currentDb);
1687
                // cannot use array_diff() once, outside of the loop,
1688
                // because the list of databases has special characters
1689
                // already escaped in $foundRows,
1690
                // contrary to the output of SHOW DATABASES
1691 4
                if (in_array($currentDbEscaped, $foundRows, true)) {
1692
                    continue;
1693
                }
1694
1695 4
                $databases[] = $currentDb;
1696 4
                $escapedDatabases[] = $currentDbEscaped;
1697
            }
1698
1699 4
            $data['databases'] = $databases;
1700 4
            $data['escaped_databases'] = $escapedDatabases;
1701 4
        } elseif ($type === 'table') {
1702 4
            $result = $this->dbi->tryQuery('SHOW TABLES FROM ' . Util::backquote($dbname));
1703
1704 4
            $tables = [];
1705 4
            if ($result) {
1706 4
                while ($row = $result->fetchRow()) {
1707 4
                    if (in_array($row[0], $foundRows, true)) {
1708
                        continue;
1709
                    }
1710
1711 4
                    $tables[] = $row[0];
1712
                }
1713
            }
1714
1715 4
            $data['tables'] = $tables;
1716
        } else { // routine
1717
            $routineData = Routines::getDetails($this->dbi, $dbname);
1718
1719
            $routines = [];
1720
            foreach ($routineData as $routine) {
1721
                if (in_array($routine->name, $foundRows, true)) {
1722
                    continue;
1723
                }
1724
1725
                $routines[] = $routine->name;
1726
            }
1727
1728
            $data['routines'] = $routines;
1729
        }
1730
1731 4
        return $this->template->render('server/privileges/privileges_summary', $data);
1732
    }
1733
1734
    /**
1735
     * Get HTML for display the users overview
1736
     * (if less than 50 users, display them immediately)
1737
     *
1738
     * @param ResultInterface $result   ran sql query
1739
     * @param mixed[]         $dbRights user's database rights array
1740
     * @param string          $textDir  text directory
1741
     *
1742
     * @return string HTML snippet
1743
     */
1744 8
    public function getUsersOverview(ResultInterface $result, array $dbRights, string $textDir): string
1745
    {
1746 8
        $configurableMenusFeature = $this->relation->getRelationParameters()->configurableMenusFeature;
1747
1748 8
        while ($row = $result->fetchAssoc()) {
1749 4
            $row['privs'] = $this->extractPrivInfo($row, true);
1750 4
            $dbRights[$row['User']][$row['Host']] = $row;
1751
        }
1752
1753 8
        unset($result);
1754
1755 8
        $userGroupCount = 0;
1756 8
        if ($configurableMenusFeature !== null) {
1757 4
            $sqlQuery = 'SELECT * FROM ' . Util::backquote($configurableMenusFeature->database)
1758 4
                . '.' . Util::backquote($configurableMenusFeature->users);
1759 4
            $result = $this->dbi->tryQueryAsControlUser($sqlQuery);
1760 4
            $groupAssignment = [];
1761 4
            if ($result) {
1762 4
                while ($row = $result->fetchAssoc()) {
1763
                    $groupAssignment[$row['username']] = $row['usergroup'];
1764
                }
1765
            }
1766
1767 4
            unset($result);
1768
1769 4
            $userGroupCount = $this->getUserGroupCount($configurableMenusFeature);
1770
        }
1771
1772 8
        $hosts = [];
1773 8
        $hasAccountLocking = Compatibility::hasAccountLocking($this->dbi->isMariaDB(), $this->dbi->getVersion());
1774 8
        foreach ($dbRights as $user) {
1775 4
            ksort($user);
1776 4
            foreach ($user as $host) {
1777 4
                $res = $this->getUserPrivileges((string) $host['User'], (string) $host['Host'], $hasAccountLocking);
1778
1779 4
                $hasPassword = false;
1780
                if (
1781 4
                    (isset($res['authentication_string'])
1782
                    && $res['authentication_string'] !== '')
1783 4
                    || (isset($res['Password'])
1784 4
                    && $res['Password'] !== '')
1785
                ) {
1786
                    $hasPassword = true;
1787
                }
1788
1789 4
                $hosts[] = [
1790 4
                    'user' => $host['User'],
1791 4
                    'host' => $host['Host'],
1792 4
                    'has_password' => $hasPassword,
1793 4
                    'has_select_priv' => isset($host['Select_priv']),
1794 4
                    'privileges' => $host['privs'],
1795 4
                    'group' => $groupAssignment[$host['User']] ?? '',
1796 4
                    'has_grant' => $host['Grant_priv'] === 'Y',
1797 4
                    'is_account_locked' => isset($res['account_locked']) && $res['account_locked'] === 'Y',
1798 4
                ];
1799
            }
1800
        }
1801
1802 8
        return $this->template->render('server/privileges/users_overview', [
1803 8
            'menus_work' => $configurableMenusFeature !== null,
1804 8
            'user_group_count' => $userGroupCount,
1805 8
            'text_dir' => $textDir,
1806 8
            'initial' => $_GET['initial'] ?? '',
1807 8
            'hosts' => $hosts,
1808 8
            'is_grantuser' => $this->dbi->isGrantUser(),
1809 8
            'is_createuser' => $this->dbi->isCreateUser(),
1810 8
            'has_account_locking' => $hasAccountLocking,
1811 8
        ]);
1812
    }
1813
1814
    /**
1815
     * Displays the initials if there are many privileges
1816
     */
1817 8
    public function getHtmlForInitials(): string
1818
    {
1819 8
        $usersCount = $this->dbi->fetchValue('SELECT COUNT(*) FROM `mysql`.`user`');
1820 8
        if ($usersCount === false || $usersCount <= 20) {
1821 4
            return '';
1822
        }
1823
1824 4
        $result = $this->dbi->tryQuery('SELECT DISTINCT UPPER(LEFT(`User`, 1)) FROM `user`');
1825 4
        if ($result === false) {
1826
            return '';
1827
        }
1828
1829 4
        $initials = $result->fetchAllColumn();
1830
        // Display the initials, which can be any characters, not
1831
        // just letters. For letters A-Z, we add the non-used letters
1832
        // as greyed out.
1833 4
        $initialsMap = array_fill_keys($initials, true) + array_fill_keys(range('A', 'Z'), false);
1834 4
        uksort($initialsMap, strnatcasecmp(...));
0 ignored issues
show
Bug introduced by
The type strnatcasecmp was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1835
1836 4
        return $this->template->render('server/privileges/initials_row', [
1837 4
            'array_initials' => $initialsMap,
1838 4
            'selected_initial' => $_GET['initial'] ?? null,
1839 4
        ]);
1840
    }
1841
1842
    /**
1843
     * Get the database rights array for Display user overview
1844
     *
1845
     * @return (string|string[])[][][]    database rights array
1846
     */
1847 8
    public function getDbRightsForUserOverview(string|null $initial): array
1848
    {
1849
        // we also want users not in table `user` but in other table
1850 8
        $mysqlTables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`');
1851 8
        $userTables = ['user', 'db', 'tables_priv', 'columns_priv', 'procs_priv'];
1852 8
        $whereUser = $this->rangeOfUsers($initial);
1853 8
        $sqls = [];
1854 8
        foreach (array_intersect($userTables, $mysqlTables) as $table) {
1855 8
            $sqls[] = '(SELECT DISTINCT `User`, `Host` FROM `mysql`.`' . $table . '` ' . $whereUser . ')';
1856
        }
1857
1858 8
        $sql = implode(' UNION ', $sqls) . ' ORDER BY `User` ASC, `Host` ASC';
1859 8
        $result = $this->dbi->query($sql);
1860
1861 8
        $userDefaults = ['User' => '', 'Host' => '%', 'Password' => '?', 'Grant_priv' => 'N', 'privs' => ['USAGE']];
1862 8
        $dbRights = [];
1863 8
        while ($row = $result->fetchAssoc()) {
1864
            /** @psalm-var array{User: string, Host: string} $row */
1865 8
            $dbRights[$row['User']][$row['Host']] = array_merge($userDefaults, $row);
1866
        }
1867
1868 8
        ksort($dbRights);
1869
1870 8
        return $dbRights;
1871
    }
1872
1873
    /**
1874
     * Delete user and get message and sql query for delete user in privileges
1875
     *
1876
     * @param mixed[] $queries queries
1877
     *
1878
     * @return array{string, Message} Message
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{string, Message} at position 2 could not be parsed: Expected ':' at position 2, but found 'string'.
Loading history...
1879
     */
1880 4
    public function deleteUser(array $queries): array
1881
    {
1882 4
        $sqlQuery = '';
1883 4
        if ($queries === []) {
1884 4
            $message = Message::error(__('No users selected for deleting!'));
1885
        } else {
1886 4
            if ($_POST['mode'] == 3) {
1887 4
                $queries[] = '# ' . __('Reloading the privileges') . ' …';
1888 4
                $queries[] = 'FLUSH PRIVILEGES;';
1889
            }
1890
1891 4
            $dropUserError = '';
1892 4
            foreach ($queries as $sqlQuery) {
1893 4
                if ($sqlQuery[0] === '#') {
1894 4
                    continue;
1895
                }
1896
1897 4
                if ($this->dbi->tryQuery($sqlQuery)) {
1898 4
                    continue;
1899
                }
1900
1901 4
                $dropUserError .= $this->dbi->getError() . "\n";
1902
            }
1903
1904
            // tracking sets this, causing the deleted db to be shown in navi
1905 4
            Current::$database = '';
1906
1907 4
            $sqlQuery = implode("\n", $queries);
1908 4
            if ($dropUserError !== '') {
1909 4
                $message = Message::rawError($dropUserError);
1910
            } else {
1911 4
                $message = Message::success(
1912 4
                    __('The selected users have been deleted successfully.'),
1913 4
                );
1914
            }
1915
        }
1916
1917 4
        return [$sqlQuery, $message];
1918
    }
1919
1920
    /**
1921
     * Update the privileges and return the success or error message
1922
     *
1923
     * @return array{string, Message} success message or error message for update
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{string, Message} at position 2 could not be parsed: Expected ':' at position 2, but found 'string'.
Loading history...
1924
     */
1925 12
    public function updatePrivileges(
1926
        string $username,
1927
        string $hostname,
1928
        string $tablename,
1929
        string $dbname,
1930
        string $itemType,
1931
    ): array {
1932 12
        $dbAndTable = $this->wildcardEscapeForGrant($dbname, $tablename);
1933
1934 12
        $sqlQuery0 = 'REVOKE ALL PRIVILEGES ON ' . $itemType . ' ' . $dbAndTable
1935 12
            . ' FROM ' . $this->dbi->quoteString($username)
1936 12
            . '@' . $this->dbi->quoteString($hostname) . ';';
1937
1938 12
        if (! isset($_POST['Grant_priv']) || $_POST['Grant_priv'] !== 'Y') {
1939
            $sqlQuery1 = 'REVOKE GRANT OPTION ON ' . $itemType . ' ' . $dbAndTable
1940
                . ' FROM ' . $this->dbi->quoteString($username) . '@'
1941
                . $this->dbi->quoteString($hostname) . ';';
1942
        } else {
1943 12
            $sqlQuery1 = '';
1944
        }
1945
1946 12
        $grantBackQuery = null;
1947 12
        $alterUserQuery = null;
1948
1949
        // Should not do a GRANT USAGE for a table-specific privilege, it
1950
        // causes problems later (cannot revoke it)
1951 12
        if (! ($tablename !== '' && implode('', $this->extractPrivInfo()) === 'USAGE')) {
1952 8
            [$grantBackQuery, $alterUserQuery] = $this->generateQueriesForUpdatePrivileges(
1953 8
                $itemType,
1954 8
                $dbAndTable,
1955 8
                $username,
1956 8
                $hostname,
1957 8
                $dbname,
1958 8
            );
1959
        }
1960
1961 12
        if (! $this->dbi->tryQuery($sqlQuery0)) {
1962
            // This might fail when the executing user does not have
1963
            // ALL PRIVILEGES themselves.
1964
            // See https://github.com/phpmyadmin/phpmyadmin/issues/9673
1965 8
            $sqlQuery0 = '';
1966
        }
1967
1968 12
        if ($sqlQuery1 !== '' && ! $this->dbi->tryQuery($sqlQuery1)) {
1969
            // this one may fail, too...
1970
            $sqlQuery1 = '';
1971
        }
1972
1973 12
        if ($grantBackQuery !== null) {
1974 8
            $this->dbi->query($grantBackQuery);
1975
        } else {
1976 4
            $grantBackQuery = '';
1977
        }
1978
1979 12
        if ($alterUserQuery !== null) {
1980 4
            $this->dbi->query($alterUserQuery);
1981
        } else {
1982 8
            $alterUserQuery = '';
1983
        }
1984
1985 12
        $sqlQuery = $sqlQuery0 . ' ' . $sqlQuery1 . ' ' . $grantBackQuery . ' ' . $alterUserQuery;
1986 12
        $message = Message::success(__('You have updated the privileges for %s.'));
1987 12
        $message->addParam('\'' . $username . '\'@\'' . $hostname . '\'');
1988
1989 12
        return [$sqlQuery, $message];
1990
    }
1991
1992
    /**
1993
     * Generate the query for the GRANTS and requirements + limits
1994
     *
1995
     * @return array<int,string|null>
1996
     */
1997 8
    private function generateQueriesForUpdatePrivileges(
1998
        string $itemType,
1999
        string $dbAndTable,
2000
        string $username,
2001
        string $hostname,
2002
        string $dbname,
2003
    ): array {
2004 8
        $alterUserQuery = null;
2005
2006 8
        $grantBackQuery = 'GRANT ' . implode(', ', $this->extractPrivInfo())
2007 8
            . ' ON ' . $itemType . ' ' . $dbAndTable
2008 8
            . ' TO ' . $this->dbi->quoteString($username) . '@'
2009 8
            . $this->dbi->quoteString($hostname);
2010
2011 8
        $isMySqlOrPercona = Compatibility::isMySqlOrPerconaDb();
2012 8
        $needsToUseAlter = $isMySqlOrPercona && $this->dbi->getVersion() >= 80011;
2013
2014 8
        if ($needsToUseAlter) {
2015 4
            $alterUserQuery = 'ALTER USER ' . $this->dbi->quoteString($username) . '@'
2016 4
            . $this->dbi->quoteString($hostname) . ' ';
2017
        }
2018
2019 8
        if ($dbname === '') {
2020
            // add REQUIRE clause
2021 8
            if ($needsToUseAlter) {
2022 4
                $alterUserQuery .= $this->getRequireClause();
2023
            } else {
2024 4
                $grantBackQuery .= $this->getRequireClause();
2025
            }
2026
        }
2027
2028
        if (
2029 8
            (isset($_POST['Grant_priv']) && $_POST['Grant_priv'] === 'Y')
2030 8
            || ($dbname === ''
2031 8
            && (isset($_POST['max_questions']) || isset($_POST['max_connections'])
2032 8
            || isset($_POST['max_updates'])
2033 8
            || isset($_POST['max_user_connections'])))
2034
        ) {
2035 8
            if ($needsToUseAlter) {
2036 4
                $alterUserQuery .= $this->getWithClauseForAddUserAndUpdatePrivs();
2037
            } else {
2038 4
                $grantBackQuery .= $this->getWithClauseForAddUserAndUpdatePrivs();
2039
            }
2040
        }
2041
2042 8
        $grantBackQuery .= ';';
2043
2044 8
        if ($needsToUseAlter) {
2045 4
            $alterUserQuery .= ';';
2046
        }
2047
2048 8
        return [$grantBackQuery, $alterUserQuery];
2049
    }
2050
2051
    /**
2052
     * Get List of information: Changes / copies a user
2053
     */
2054 4
    public function getDataForChangeOrCopyUser(string $oldUsername, string $oldHostname): string|null
2055
    {
2056 4
        if (isset($_POST['change_copy'])) {
2057 4
            $userHostCondition = $this->getUserHostCondition($oldUsername, $oldHostname);
2058 4
            $row = $this->dbi->fetchSingleRow('SELECT * FROM `mysql`.`user` ' . $userHostCondition . ';');
2059 4
            if ($row === null || $row === []) {
2060
                $response = ResponseRenderer::getInstance();
2061
                $response->addHTML(
2062
                    Message::notice(__('No user found.'))->getDisplay(),
2063
                );
2064
                unset($_POST['change_copy']);
2065
            } else {
2066 4
                foreach ($row as $key => $value) {
2067 4
                    $GLOBALS[$key] = $value;
2068
                }
2069
2070 4
                $serverVersion = $this->dbi->getVersion();
2071
                // Recent MySQL versions have the field "Password" in mysql.user,
2072
                // so the previous extract creates $row['Password'] but this script
2073
                // uses $password
2074 4
                if (! isset($row['password']) && isset($row['Password'])) {
2075 4
                    $row['password'] = $row['Password'];
2076
                }
2077
2078
                if (
2079 4
                    Compatibility::isMySqlOrPerconaDb()
2080 4
                    && $serverVersion >= 50606
2081 4
                    && $serverVersion < 50706
2082 4
                    && ((isset($row['authentication_string'])
2083 4
                    && empty($row['password']))
2084 4
                    || (isset($row['plugin'])
2085 4
                    && $row['plugin'] === 'sha256_password'))
2086
                ) {
2087
                    $row['password'] = $row['authentication_string'];
2088
                }
2089
2090
                if (
2091 4
                    Compatibility::isMariaDb()
2092 4
                    && $serverVersion >= 50500
2093 4
                    && isset($row['authentication_string'])
2094 4
                    && empty($row['password'])
2095
                ) {
2096
                    $row['password'] = $row['authentication_string'];
2097
                }
2098
2099
                // Always use 'authentication_string' column
2100
                // for MySQL 5.7.6+ since it does not have
2101
                // the 'password' column at all
2102
                if (
2103 4
                    Compatibility::isMySqlOrPerconaDb()
2104 4
                    && $serverVersion >= 50706
2105 4
                    && isset($row['authentication_string'])
2106
                ) {
2107 4
                    $row['password'] = $row['authentication_string'];
2108
                }
2109
2110 4
                return $row['password'];
2111
            }
2112
        }
2113
2114 4
        return null;
2115
    }
2116
2117
    /**
2118
     * Update Data for information: Deletes users
2119
     *
2120
     * @param mixed[] $queries queries array
2121
     *
2122
     * @return mixed[]
2123
     */
2124 4
    public function getDataForDeleteUsers(array $queries): array
2125
    {
2126 4
        if (isset($_POST['change_copy'])) {
2127 4
            $selectedUsr = [$_POST['old_username'] . '&amp;#27;' . $_POST['old_hostname']];
2128
        } else {
2129
            // null happens when no user was selected
2130
            $selectedUsr = $_POST['selected_usr'] ?? null;
2131
            $queries = [];
2132
        }
2133
2134
        // this happens, was seen in https://reports.phpmyadmin.net/reports/view/17146
2135 4
        if (! is_array($selectedUsr)) {
2136
            return [];
2137
        }
2138
2139 4
        foreach ($selectedUsr as $eachUser) {
2140 4
            [$thisUser, $thisHost] = explode('&amp;#27;', $eachUser);
2141 4
            $queries[] = '# '
2142 4
                . sprintf(
2143 4
                    __('Deleting %s'),
2144 4
                    '\'' . $thisUser . '\'@\'' . $thisHost . '\'',
2145 4
                )
2146 4
                . ' ...';
2147 4
            $queries[] = 'DROP USER '
2148 4
                . $this->dbi->quoteString($thisUser)
2149 4
                . '@' . $this->dbi->quoteString($thisHost) . ';';
2150 4
            $this->relationCleanup->user($thisUser);
2151
2152 4
            if (! isset($_POST['drop_users_db'])) {
2153 4
                continue;
2154
            }
2155
2156
            $queries[] = 'DROP DATABASE IF EXISTS '
2157
                . Util::backquote($thisUser) . ';';
2158
            $GLOBALS['reload'] = true;
2159
        }
2160
2161 4
        return $queries;
2162
    }
2163
2164
    /**
2165
     * update Message For Reload
2166
     */
2167
    public function updateMessageForReload(): Message|null
2168
    {
2169
        $message = null;
2170
        if (isset($_GET['flush_privileges'])) {
2171
            $sqlQuery = 'FLUSH PRIVILEGES;';
2172
            $this->dbi->query($sqlQuery);
2173
            $message = Message::success(
2174
                __('The privileges were reloaded successfully.'),
2175
            );
2176
        }
2177
2178
        if (isset($_GET['validate_username'])) {
2179
            return Message::success();
2180
        }
2181
2182
        return $message;
2183
    }
2184
2185
    /**
2186
     * update Data For Queries from queries_for_display
2187
     *
2188
     * @param mixed[]      $queries           queries array
2189
     * @param mixed[]|null $queriesForDisplay queries array for display
2190
     *
2191
     * @return mixed[]
2192
     */
2193
    public function getDataForQueries(array $queries, array|null $queriesForDisplay): array
2194
    {
2195
        $tmpCount = 0;
2196
        foreach ($queries as $sqlQuery) {
2197
            if ($sqlQuery[0] !== '#') {
2198
                $this->dbi->query($sqlQuery);
2199
            }
2200
2201
            // when there is a query containing a hidden password, take it
2202
            // instead of the real query sent
2203
            if (isset($queriesForDisplay[$tmpCount])) {
2204
                $queries[$tmpCount] = $queriesForDisplay[$tmpCount];
2205
            }
2206
2207
            $tmpCount++;
2208
        }
2209
2210
        return $queries;
2211
    }
2212
2213
    /**
2214
     * update Data for information: Adds a user
2215
     *
2216
     * @param string|mixed[]|null $dbname     db name
2217
     * @param string              $username   user name
2218
     * @param string              $hostname   host name
2219
     * @param string|null         $password   password
2220
     * @param bool                $isMenuwork is_menuwork set?
2221
     *
2222
     * @return array{Message|null, string[], string[]|null, string|null, bool}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{Message|null, stri...ull, string|null, bool} at position 2 could not be parsed: Expected ':' at position 2, but found 'Message'.
Loading history...
2223
     */
2224 8
    public function addUser(
2225
        string|array|null $dbname,
2226
        string $username,
2227
        string $hostname,
2228
        string|null $password,
2229
        bool $isMenuwork,
2230
    ): array {
2231 8
        $message = null;
2232 8
        $queries = [];
2233 8
        $queriesForDisplay = null;
2234 8
        $sqlQuery = null;
2235
2236 8
        if (! isset($_POST['adduser_submit']) && ! isset($_POST['change_copy'])) {
2237
            return [
2238
                $message,
2239
                $queries,
2240
                $queriesForDisplay,
2241
                $sqlQuery,
2242
                false, // Add user error
2243
            ];
2244
        }
2245
2246 8
        $sqlQuery = '';
2247
        // Some reports were sent to the error reporting server with phpMyAdmin 5.1.0
2248
        // pred_username was reported to be not defined
2249 8
        $predUsername = $_POST['pred_username'] ?? '';
2250 8
        if ($predUsername === 'any') {
2251 8
            $username = '';
2252
        }
2253
2254 8
        switch ($_POST['pred_hostname']) {
2255 8
            case 'any':
2256
                $hostname = '%';
2257
                break;
2258 8
            case 'localhost':
2259 8
                $hostname = 'localhost';
2260 8
                break;
2261
            case 'hosttable':
2262
                $hostname = '';
2263
                break;
2264
            case 'thishost':
2265
                $currentUserName = $this->dbi->fetchValue('SELECT USER()');
2266
                if (is_string($currentUserName)) {
2267
                    $hostname = mb_substr($currentUserName, mb_strrpos($currentUserName, '@') + 1);
2268
                    unset($currentUserName);
2269
                }
2270
2271
                break;
2272
        }
2273
2274 8
        if ($this->userExists($username, $hostname)) {
2275
            $message = Message::error(__('The user %s already exists!'));
2276
            $message->addParam('[em]\'' . $username . '\'@\'' . $hostname . '\'[/em]');
2277
            $_GET['adduser'] = true;
2278
2279
            return [
2280
                $message,
2281
                $queries,
2282
                $queriesForDisplay,
2283
                $sqlQuery,
2284
                true, // Add user error
2285
            ];
2286
        }
2287
2288 8
        [
2289 8
            $createUserReal,
2290 8
            $createUserShow,
2291 8
            $realSqlQuery,
2292 8
            $sqlQuery,
2293 8
            $passwordSetReal,
2294 8
            $passwordSetShow,
2295 8
            $alterRealSqlQuery,
2296 8
            $alterSqlQuery,
2297 8
        ] = $this->getSqlQueriesForDisplayAndAddUser($username, $hostname, $password ?? '');
2298
2299 8
        if (empty($_POST['change_copy'])) {
2300 8
            $error = false;
2301
2302 8
            if (! $this->dbi->tryQuery($createUserReal)) {
2303
                $error = true;
2304
            }
2305
2306 8
            if (isset($_POST['authentication_plugin']) && $passwordSetReal !== '') {
2307 4
                $this->setProperPasswordHashing($_POST['authentication_plugin']);
2308 4
                if ($this->dbi->tryQuery($passwordSetReal)) {
2309 4
                    $sqlQuery .= $passwordSetShow;
2310
                }
2311
            }
2312
2313 8
            $sqlQuery = $createUserShow . $sqlQuery;
2314
2315 8
            [$sqlQuery, $message] = $this->addUserAndCreateDatabase(
2316 8
                $error,
2317 8
                $realSqlQuery,
2318 8
                $sqlQuery,
2319 8
                $username,
2320 8
                $hostname,
2321 8
                is_string($dbname) ? $dbname : '',
0 ignored issues
show
introduced by
The condition is_string($dbname) is always false.
Loading history...
2322 8
                $alterRealSqlQuery,
2323 8
                $alterSqlQuery,
2324 8
                isset($_POST['createdb-1']),
2325 8
                isset($_POST['createdb-2']),
2326 8
                isset($_POST['createdb-3']),
2327 8
            );
2328 8
            if (! empty($_POST['userGroup']) && $isMenuwork) {
2329 8
                $this->setUserGroup($GLOBALS['username'], $_POST['userGroup']);
2330
            }
2331
2332 8
            return [
2333 8
                $message,
2334 8
                $queries,
2335 8
                $queriesForDisplay,
2336 8
                $sqlQuery,
2337 8
                $error, // Add user error if the query fails
2338 8
            ];
2339
        }
2340
2341
        // Copy the user group while copying a user
2342
        $oldUserGroup = $_POST['old_usergroup'] ?? '';
2343
        $this->setUserGroup($_POST['username'], $oldUserGroup);
2344
2345
        $queries[] = $createUserReal;
2346
        $queries[] = $realSqlQuery;
2347
2348
        if (isset($_POST['authentication_plugin']) && $passwordSetReal !== '') {
2349
            $this->setProperPasswordHashing($_POST['authentication_plugin']);
2350
2351
            $queries[] = $passwordSetReal;
2352
        }
2353
2354
        // we put the query containing the hidden password in
2355
        // $queries_for_display, at the same position occupied
2356
        // by the real query in $queries
2357
        $tmpCount = count($queries);
2358
        $queriesForDisplay[$tmpCount - 2] = $createUserShow;
2359
2360
        if ($passwordSetReal !== '') {
2361
            $queriesForDisplay[$tmpCount - 3] = $createUserShow;
2362
            $queriesForDisplay[$tmpCount - 2] = $sqlQuery;
2363
            $queriesForDisplay[$tmpCount - 1] = $passwordSetShow;
2364
        } else {
2365
            $queriesForDisplay[$tmpCount - 1] = $sqlQuery;
2366
        }
2367
2368
        return [
2369
            $message,
2370
            $queries,
2371
            $queriesForDisplay,
2372
            $sqlQuery,
2373
            false, // Add user error
2374
        ];
2375
    }
2376
2377
    /**
2378
     * Sets proper value of `old_passwords` according to
2379
     * the authentication plugin selected
2380
     *
2381
     * @param string $authPlugin authentication plugin selected
2382
     */
2383 16
    public function setProperPasswordHashing(string $authPlugin): void
2384
    {
2385
        // Set the hashing method used by PASSWORD()
2386
        // to be of type depending upon $authentication_plugin
2387 16
        if ($authPlugin === 'sha256_password') {
2388
            $this->dbi->tryQuery('SET `old_passwords` = 2;');
2389 16
        } elseif ($authPlugin === 'mysql_old_password') {
2390
            $this->dbi->tryQuery('SET `old_passwords` = 1;');
2391
        } else {
2392 16
            $this->dbi->tryQuery('SET `old_passwords` = 0;');
2393
        }
2394
    }
2395
2396
    /**
2397
     * Update DB information: DB, Table, isWildcard
2398
     *
2399
     * @return mixed[]
2400
     * @psalm-return array{?string, ?string, array|string|null, ?string, ?string, bool}
2401
     */
2402 4
    public function getDataForDBInfo(): array
2403
    {
2404 4
        $username = null;
2405 4
        $hostname = null;
2406 4
        $dbname = null;
2407 4
        $tablename = null;
2408 4
        $routinename = null;
2409
2410 4
        if (isset($_REQUEST['username'])) {
2411 4
            $username = (string) $_REQUEST['username'];
2412
        }
2413
2414 4
        if (isset($_REQUEST['hostname'])) {
2415 4
            $hostname = (string) $_REQUEST['hostname'];
2416
        }
2417
2418
        /**
2419
         * Checks if a dropdown box has been used for selecting a database / table
2420
         */
2421
        if (
2422 4
            isset($_POST['pred_tablename'])
2423 4
            && is_string($_POST['pred_tablename'])
2424 4
            && $_POST['pred_tablename'] !== ''
2425
        ) {
2426 4
            $tablename = $_POST['pred_tablename'];
2427
        } elseif (
2428 4
            isset($_REQUEST['tablename'])
2429 4
            && is_string($_REQUEST['tablename'])
2430 4
            && $_REQUEST['tablename'] !== ''
2431
        ) {
2432 4
            $tablename = $_REQUEST['tablename'];
2433
        }
2434
2435
        if (
2436 4
            isset($_POST['pred_routinename'])
2437 4
            && is_string($_POST['pred_routinename'])
2438 4
            && $_POST['pred_routinename'] !== ''
2439
        ) {
2440
            $routinename = $_POST['pred_routinename'];
2441
        } elseif (
2442 4
            isset($_REQUEST['routinename'])
2443 4
            && is_string($_REQUEST['routinename'])
2444 4
            && $_REQUEST['routinename'] !== ''
2445
        ) {
2446
            $routinename = $_REQUEST['routinename'];
2447
        }
2448
2449
        // Accept only array of non-empty strings
2450
        if (
2451 4
            isset($_POST['pred_dbname'])
2452 4
            && is_array($_POST['pred_dbname'])
2453 4
            && $_POST['pred_dbname'] === array_filter($_POST['pred_dbname'])
2454
        ) {
2455 4
            $dbname = $_POST['pred_dbname'];
2456
            // If dbname contains only one database.
2457 4
            if (count($dbname) === 1) {
2458 4
                $dbname = (string) $dbname[0];
2459
            }
2460
        }
2461
2462 4
        if ($dbname === null && isset($_REQUEST['dbname'])) {
2463 4
            if (is_array($_REQUEST['dbname'])) {
2464
                // Accept only array of non-empty strings
2465 4
                if ($_REQUEST['dbname'] === array_filter($_REQUEST['dbname'])) {
2466 4
                    $dbname = $_REQUEST['dbname'];
2467
                }
2468
            } elseif (
2469 4
                is_string($_REQUEST['dbname'])
2470 4
                && $_REQUEST['dbname'] !== ''
2471
            ) {
2472 4
                $dbname = $_REQUEST['dbname'];
2473
            }
2474
        }
2475
2476
        // check if given $dbname is a wildcard or not
2477 4
        $databaseNameIsWildcard = is_string($dbname) && preg_match('/(?<!\\\\)(?:_|%)/', $dbname);
2478
2479 4
        return [$username, $hostname, $dbname, $tablename, $routinename, $databaseNameIsWildcard];
2480
    }
2481
2482
    /**
2483
     * Get title and textarea for export user definition in Privileges
2484
     *
2485
     * @param string        $username      username
2486
     * @param string        $hostname      host name
2487
     * @param string[]|null $selectedUsers
2488
     */
2489 4
    public function getExportUserDefinitionTextarea(
2490
        string $username,
2491
        string $hostname,
2492
        array|null $selectedUsers,
2493
    ): string {
2494 4
        $export = '<textarea class="export" cols="60" rows="15">';
2495
2496 4
        if ($selectedUsers !== null) {
0 ignored issues
show
introduced by
The condition $selectedUsers !== null is always true.
Loading history...
2497
            //For removing duplicate entries of users
2498
            $selectedUsers = array_unique($selectedUsers);
2499
2500
            foreach ($selectedUsers as $exportUser) {
2501
                $exportUsername = mb_substr(
2502
                    $exportUser,
2503
                    0,
2504
                    (int) mb_strpos($exportUser, '&'),
2505
                );
2506
                $exportHostname = mb_substr(
2507
                    $exportUser,
2508
                    mb_strrpos($exportUser, ';') + 1,
2509
                );
2510
                $export .= '# '
2511
                    . sprintf(
2512
                        __('Privileges for %s'),
2513
                        '`' . htmlspecialchars($exportUsername)
2514
                        . '`@`' . htmlspecialchars($exportHostname) . '`',
2515
                    )
2516
                    . "\n\n";
2517
                $export .= $this->getGrants($exportUsername, $exportHostname) . "\n";
2518
            }
2519
        } else {
2520
            // export privileges for a single user
2521 4
            $export .= $this->getGrants($username, $hostname);
2522
        }
2523
2524
        // remove trailing whitespace
2525 4
        $export = trim($export);
2526
2527 4
        return $export . '</textarea>';
2528
    }
2529
2530
    /**
2531
     * Get HTML for display Add userfieldset
2532
     *
2533
     * @return string html output
2534
     */
2535 4
    private function getAddUserHtmlFieldset(): string
2536
    {
2537 4
        if (! $this->dbi->isCreateUser()) {
2538
            return '';
2539
        }
2540
2541 4
        return $this->template->render('server/privileges/add_user_fieldset', ['url_params' => ['adduser' => 1]]);
2542
    }
2543
2544 4
    private function checkStructureOfPrivilegeTable(): string
2545
    {
2546
        // the query failed! This may have two reasons:
2547
        // - the user does not have enough privileges
2548
        // - the privilege tables use a structure of an earlier version.
2549
        // so let's try a more simple query
2550 4
        if (! $this->dbi->tryQuery('SELECT 1 FROM `mysql`.`user`')) {
2551 4
            return $this->getHtmlForViewUsersError() . $this->getAddUserHtmlFieldset();
2552
        }
2553
2554
        // This message is hardcoded because I will replace it by
2555
        // a automatic repair feature soon.
2556 4
        $raw = 'Your privilege table structure seems to be older than'
2557 4
            . ' this MySQL version!<br>'
2558 4
            . 'Please run the <code>mysql_upgrade</code> command'
2559 4
            . ' that should be included in your MySQL server distribution'
2560 4
            . ' to solve this problem!';
2561
2562 4
        return Message::rawError($raw)->getDisplay();
2563
    }
2564
2565
    /**
2566
     * Get HTML snippet for display user overview page
2567
     *
2568
     * @param string $textDir text directory
2569
     */
2570 4
    public function getHtmlForUserOverview(
2571
        UserPrivileges $userPrivileges,
2572
        string $textDir,
2573
        string|null $initial,
2574
    ): string {
2575 4
        $serverVersion = $this->dbi->getVersion();
2576 4
        $passwordColumn = Compatibility::isMySqlOrPerconaDb() && $serverVersion >= 50706
2577 4
            ? 'authentication_string'
2578
            : 'Password';
2579
2580
        // $sql is for the initial-filtered
2581 4
        $sql = 'SELECT *, IF(`' . $passwordColumn . "` = _latin1 '', 'N', 'Y') AS `Password`" .
2582 4
            ' FROM `mysql`.`user` ' . $this->rangeOfUsers($initial) . ' ORDER BY `User` ASC, `Host` ASC';
2583
2584 4
        $res = $this->dbi->tryQuery($sql);
2585 4
        if ($res === false) {
2586 4
            $errorMessages = $this->checkStructureOfPrivilegeTable();
2587
        } else {
2588 4
            $dbRights = $this->getDbRightsForUserOverview($initial);
2589 4
            $emptyUserNotice = $this->getEmptyUserNotice($dbRights);
2590 4
            $initialsHtml = $this->getHtmlForInitials();
2591
2592
            // Display the user overview (if less than 50 users, display them immediately)
2593 4
            if (isset($_GET['initial']) || isset($_GET['showall']) || $res->numRows() < 50) {
2594 4
                $usersOverview = $this->getUsersOverview($res, $dbRights, $textDir) .
2595 4
                    $this->template->render('export_modal');
2596
            }
2597
2598 4
            $response = ResponseRenderer::getInstance();
2599 4
            if (! $response->isAjax() || ! empty($_REQUEST['ajax_page_request'])) {
2600 4
                if ($userPrivileges->isReload) {
2601 4
                    $flushnote = new Message(
2602 4
                        __(
2603 4
                            'Note: phpMyAdmin gets the users’ privileges directly '
2604 4
                            . 'from MySQL’s privilege tables. The content of these '
2605 4
                            . 'tables may differ from the privileges the server uses, '
2606 4
                            . 'if they have been changed manually. In this case, '
2607 4
                            . 'you should %sreload the privileges%s before you continue.',
2608 4
                        ),
2609 4
                        Message::NOTICE,
2610 4
                    );
2611 4
                    $flushnote->addParamHtml(
2612 4
                        '<a href="' . Url::getFromRoute('/server/privileges', ['flush_privileges' => 1])
2613 4
                        . '" id="reload_privileges_anchor">',
2614 4
                    );
2615 4
                    $flushnote->addParamHtml('</a>');
2616
                } else {
2617
                    $flushnote = new Message(
2618
                        __(
2619
                            'Note: phpMyAdmin gets the users’ privileges directly '
2620
                            . 'from MySQL’s privilege tables. The content of these '
2621
                            . 'tables may differ from the privileges the server uses, '
2622
                            . 'if they have been changed manually. In this case, '
2623
                            . 'the privileges have to be reloaded but currently, you '
2624
                            . 'don\'t have the RELOAD privilege.',
2625
                        )
2626
                        . MySQLDocumentation::show(
2627
                            'privileges-provided',
2628
                            false,
2629
                            null,
2630
                            null,
2631
                            'priv_reload',
2632
                        ),
2633
                        Message::NOTICE,
2634
                    );
2635
                }
2636
2637 4
                $flushNotice = $flushnote->getDisplay();
2638
            }
2639
        }
2640
2641 4
        return $this->template->render('server/privileges/user_overview', [
2642 4
            'error_messages' => $errorMessages ?? '',
2643 4
            'empty_user_notice' => $emptyUserNotice ?? '',
2644 4
            'initials' => $initialsHtml ?? '',
2645 4
            'users_overview' => $usersOverview ?? '',
2646 4
            'is_createuser' => $this->dbi->isCreateUser(),
2647 4
            'flush_notice' => $flushNotice ?? '',
2648 4
        ]);
2649
    }
2650
2651
    /**
2652
     * Get HTML snippet for display user properties
2653
     *
2654
     * @param bool           $dbnameIsWildcard whether database name is wildcard or not
2655
     * @param string         $urlDbname        url database name that urlencode() string
2656
     * @param string         $username         username
2657
     * @param string         $hostname         host name
2658
     * @param string|mixed[] $dbname           database name
2659
     * @param string         $tablename        table name
2660
     * @psalm-param non-empty-string $route
2661
     */
2662 8
    public function getHtmlForUserProperties(
2663
        bool $dbnameIsWildcard,
2664
        string $urlDbname,
2665
        string $username,
2666
        string $hostname,
2667
        string|array $dbname,
2668
        string $tablename,
2669
        string $route,
2670
    ): string {
2671 8
        $userDoesNotExists = ! $this->userExists($username, $hostname);
2672
2673 8
        $loginInformationFields = '';
2674 8
        if ($userDoesNotExists) {
2675
            $loginInformationFields = $this->getHtmlForLoginInformationFields();
2676
        }
2677
2678 8
        $params = ['username' => $username, 'hostname' => $hostname];
2679 8
        $params['dbname'] = $dbname;
2680 8
        if (! is_array($dbname) && $dbname !== '' && $tablename !== '') {
0 ignored issues
show
introduced by
The condition is_array($dbname) is always true.
Loading history...
2681 8
            $params['tablename'] = $tablename;
2682
        }
2683
2684 8
        $privilegesTable = $this->getHtmlToDisplayPrivilegesTable(
2685
            // If $dbname is an array, pass any one db as all have same privs.
2686 8
            is_string($dbname) && $dbname !== ''
2687 8
                ? $dbname
2688 8
                : (is_array($dbname) ? (string) $dbname[0] : '*'),
2689 8
            $tablename !== ''
2690 8
                ? $tablename
2691 8
                : '*',
2692 8
        );
2693
2694 8
        $tableSpecificRights = '';
2695 8
        if (! is_array($dbname) && $tablename === '' && $dbnameIsWildcard === false) {
2696
            // no table name was given, display all table specific rights
2697
            // but only if $dbname contains no wildcards
2698
            if ($dbname === '') {
2699
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights($username, $hostname, 'database');
2700
            } else {
2701
                // unescape wildcards in dbname at table level
2702
                $unescapedDb = $this->unescapeGrantWildcards($dbname);
2703
2704
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
2705
                    $username,
2706
                    $hostname,
2707
                    'table',
2708
                    $unescapedDb,
2709
                );
2710
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
2711
                    $username,
2712
                    $hostname,
2713
                    'routine',
2714
                    $unescapedDb,
2715
                );
2716
            }
2717
        }
2718
2719 8
        $config = Config::getInstance();
0 ignored issues
show
Deprecated Code introduced by
The function PhpMyAdmin\Config::getInstance() has been deprecated: Use dependency injection instead. ( Ignorable by Annotation )

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

2719
        $config = /** @scrutinizer ignore-deprecated */ Config::getInstance();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
2720 8
        $databaseUrl = Util::getScriptNameForOption($config->settings['DefaultTabDatabase'], 'database');
2721 8
        $databaseUrlTitle = Util::getTitleForTarget($config->settings['DefaultTabDatabase']);
2722 8
        $tableUrl = Util::getScriptNameForOption($config->settings['DefaultTabTable'], 'table');
2723 8
        $tableUrlTitle = Util::getTitleForTarget($config->settings['DefaultTabTable']);
2724
2725 8
        $changePassword = '';
2726 8
        $userGroup = '';
2727 8
        $changeLoginInfoFields = '';
2728 8
        if ($dbname === '' && ! $userDoesNotExists) {
2729
            //change login information
2730
            $changePassword = $this->getFormForChangePassword($username, $hostname, true, $route);
2731
            $userGroup = $this->getUserGroupForUser($username);
2732
            $changeLoginInfoFields = $this->getHtmlForLoginInformationFields($username, $hostname);
2733
        }
2734
2735 8
        return $this->template->render('server/privileges/user_properties', [
2736 8
            'user_does_not_exists' => $userDoesNotExists,
2737 8
            'login_information_fields' => $loginInformationFields,
2738 8
            'params' => $params,
2739 8
            'privileges_table' => $privilegesTable,
2740 8
            'table_specific_rights' => $tableSpecificRights,
2741 8
            'change_password' => $changePassword,
2742 8
            'database' => $dbname,
2743 8
            'dbname' => $urlDbname,
2744 8
            'username' => $username,
2745 8
            'hostname' => $hostname,
2746 8
            'is_databases' => $dbnameIsWildcard || is_array($dbname) && count($dbname) > 1,
2747 8
            'is_wildcard' => $dbnameIsWildcard,
2748 8
            'table' => $tablename,
2749 8
            'current_user' => $this->dbi->getCurrentUser(),
2750 8
            'user_group' => $userGroup,
2751 8
            'change_login_info_fields' => $changeLoginInfoFields,
2752 8
            'database_url' => $databaseUrl,
2753 8
            'database_url_title' => $databaseUrlTitle,
2754 8
            'table_url' => $tableUrl,
2755 8
            'table_url_title' => $tableUrlTitle,
2756 8
        ]);
2757
    }
2758
2759
    /**
2760
     * Get queries for Table privileges to change or copy user
2761
     *
2762
     * @param string  $userHostCondition user host condition to select relevant table privileges
2763
     * @param mixed[] $queries           queries array
2764
     * @param string  $username          username
2765
     * @param string  $hostname          host name
2766
     *
2767
     * @return mixed[]
2768
     */
2769
    public function getTablePrivsQueriesForChangeOrCopyUser(
2770
        string $userHostCondition,
2771
        array $queries,
2772
        string $username,
2773
        string $hostname,
2774
    ): array {
2775
        $res = $this->dbi->query(
2776
            'SELECT `Db`, `Table_name`, `Table_priv` FROM `mysql`.`tables_priv`' . $userHostCondition . ';',
2777
        );
2778
        while ($row = $res->fetchAssoc()) {
2779
            $res2 = $this->dbi->query(
2780
                'SELECT `Column_name`, `Column_priv`'
2781
                . ' FROM `mysql`.`columns_priv`'
2782
                . $userHostCondition
2783
                . ' AND `Db` = ' . $this->dbi->quoteString($row['Db'])
0 ignored issues
show
Bug introduced by
It seems like $row['Db'] can also be of type null; however, parameter $str of PhpMyAdmin\DatabaseInterface::quoteString() 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

2783
                . ' AND `Db` = ' . $this->dbi->quoteString(/** @scrutinizer ignore-type */ $row['Db'])
Loading history...
2784
                . ' AND `Table_name` = ' . $this->dbi->quoteString($row['Table_name'])
2785
                . ';',
2786
            );
2787
2788
            $tmpPrivs1 = $this->extractPrivInfo($row);
2789
            $tmpPrivs2 = ['Select' => [], 'Insert' => [], 'Update' => [], 'References' => []];
2790
2791
            while ($row2 = $res2->fetchAssoc()) {
2792
                $tmpArray = explode(',', $row2['Column_priv']);
0 ignored issues
show
Bug introduced by
It seems like $row2['Column_priv'] can also be of type null; however, parameter $string of explode() 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

2792
                $tmpArray = explode(',', /** @scrutinizer ignore-type */ $row2['Column_priv']);
Loading history...
2793
                if (in_array('Select', $tmpArray, true)) {
2794
                    $tmpPrivs2['Select'][] = $row2['Column_name'];
2795
                }
2796
2797
                if (in_array('Insert', $tmpArray, true)) {
2798
                    $tmpPrivs2['Insert'][] = $row2['Column_name'];
2799
                }
2800
2801
                if (in_array('Update', $tmpArray, true)) {
2802
                    $tmpPrivs2['Update'][] = $row2['Column_name'];
2803
                }
2804
2805
                if (! in_array('References', $tmpArray, true)) {
2806
                    continue;
2807
                }
2808
2809
                $tmpPrivs2['References'][] = $row2['Column_name'];
2810
            }
2811
2812
            if ($tmpPrivs2['Select'] !== [] && ! in_array('SELECT', $tmpPrivs1, true)) {
2813
                $tmpPrivs1[] = 'SELECT (`' . implode('`, `', $tmpPrivs2['Select']) . '`)';
2814
            }
2815
2816
            if ($tmpPrivs2['Insert'] !== [] && ! in_array('INSERT', $tmpPrivs1, true)) {
2817
                $tmpPrivs1[] = 'INSERT (`' . implode('`, `', $tmpPrivs2['Insert']) . '`)';
2818
            }
2819
2820
            if ($tmpPrivs2['Update'] !== [] && ! in_array('UPDATE', $tmpPrivs1, true)) {
2821
                $tmpPrivs1[] = 'UPDATE (`' . implode('`, `', $tmpPrivs2['Update']) . '`)';
2822
            }
2823
2824
            if ($tmpPrivs2['References'] !== [] && ! in_array('REFERENCES', $tmpPrivs1, true)) {
2825
                $tmpPrivs1[] = 'REFERENCES (`' . implode('`, `', $tmpPrivs2['References']) . '`)';
2826
            }
2827
2828
            $queries[] = 'GRANT ' . implode(', ', $tmpPrivs1)
2829
                . ' ON ' . Util::backquote($row['Db']) . '.'
2830
                . Util::backquote($row['Table_name'])
2831
                . ' TO ' . $this->dbi->quoteString($username)
2832
                . '@' . $this->dbi->quoteString($hostname)
2833
                . (in_array('Grant', explode(',', $row['Table_priv']), true)
2834
                ? ' WITH GRANT OPTION;'
2835
                : ';');
2836
        }
2837
2838
        return $queries;
2839
    }
2840
2841
    /**
2842
     * Get queries for database specific privileges for change or copy user
2843
     *
2844
     * @param mixed[] $queries  queries array with string
2845
     * @param string  $username username
2846
     * @param string  $hostname host name
2847
     *
2848
     * @return mixed[]
2849
     */
2850
    public function getDbSpecificPrivsQueriesForChangeOrCopyUser(
2851
        array $queries,
2852
        string $username,
2853
        string $hostname,
2854
        string $oldUsername,
2855
        string $oldHostname,
2856
    ): array {
2857
        $userHostCondition = $this->getUserHostCondition($oldUsername, $oldHostname);
2858
2859
        $res = $this->dbi->query('SELECT * FROM `mysql`.`db`' . $userHostCondition . ';');
2860
2861
        foreach ($res as $row) {
2862
            $queries[] = 'GRANT ' . implode(', ', $this->extractPrivInfo($row))
2863
                . ' ON ' . Util::backquote($row['Db']) . '.*'
2864
                . ' TO ' . $this->dbi->quoteString($username) . '@' . $this->dbi->quoteString($hostname)
2865
                . ($row['Grant_priv'] === 'Y' ? ' WITH GRANT OPTION;' : ';');
2866
        }
2867
2868
        return $this->getTablePrivsQueriesForChangeOrCopyUser($userHostCondition, $queries, $username, $hostname);
2869
    }
2870
2871
    /**
2872
     * Prepares queries for adding users and
2873
     * also create database and return query and message
2874
     *
2875
     * @param bool   $error             whether user create or not
2876
     * @param string $realSqlQuery      SQL query for add a user
2877
     * @param string $sqlQuery          SQL query to be displayed
2878
     * @param string $username          username
2879
     * @param string $hostname          host name
2880
     * @param string $dbname            database name
2881
     * @param string $alterRealSqlQuery SQL query for ALTER USER
2882
     * @param string $alterSqlQuery     SQL query for ALTER USER to be displayed
2883
     *
2884
     * @return array{string, Message}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{string, Message} at position 2 could not be parsed: Expected ':' at position 2, but found 'string'.
Loading history...
2885
     */
2886 12
    public function addUserAndCreateDatabase(
2887
        bool $error,
2888
        string $realSqlQuery,
2889
        string $sqlQuery,
2890
        string $username,
2891
        string $hostname,
2892
        string $dbname,
2893
        string $alterRealSqlQuery,
2894
        string $alterSqlQuery,
2895
        bool $createDb1,
2896
        bool $createDb2,
2897
        bool $createDb3,
2898
    ): array {
2899 12
        if ($error || ($realSqlQuery !== '' && ! $this->dbi->tryQuery($realSqlQuery))) {
2900
            $createDb1 = $createDb2 = $createDb3 = false;
2901
            $message = Message::rawError($this->dbi->getError());
2902 12
        } elseif ($alterRealSqlQuery !== '' && ! $this->dbi->tryQuery($alterRealSqlQuery)) {
2903
            $createDb1 = $createDb2 = $createDb3 = false;
2904
            $message = Message::rawError($this->dbi->getError());
2905
        } else {
2906 12
            $sqlQuery .= $alterSqlQuery;
2907 12
            $message = Message::success(__('You have added a new user.'));
2908
        }
2909
2910 12
        if ($createDb1) {
2911
            // Create database with same name and grant all privileges
2912
            $query = 'CREATE DATABASE IF NOT EXISTS ' . Util::backquote($username) . ';';
2913
            $sqlQuery .= $query;
2914
            if (! $this->dbi->tryQuery($query)) {
2915
                $message = Message::rawError($this->dbi->getError());
2916
            }
2917
2918
            /**
2919
             * Reload the navigation
2920
             */
2921
            $GLOBALS['reload'] = true;
2922
            Current::$database = $username;
2923
2924
            $query = 'GRANT ALL PRIVILEGES ON '
2925
                . Util::backquote(
2926
                    $this->escapeGrantWildcards($username),
2927
                ) . '.* TO '
2928
                . $this->dbi->quoteString($username) . '@' . $this->dbi->quoteString($hostname) . ';';
2929
            $sqlQuery .= $query;
2930
            if (! $this->dbi->tryQuery($query)) {
2931
                $message = Message::rawError($this->dbi->getError());
2932
            }
2933
        }
2934
2935 12
        if ($createDb2) {
2936
            // Grant all privileges on wildcard name (username\_%)
2937
            $query = 'GRANT ALL PRIVILEGES ON '
2938
                . Util::backquote(
2939
                    $this->escapeGrantWildcards($username) . '\_%',
2940
                ) . '.* TO '
2941
                . $this->dbi->quoteString($username) . '@' . $this->dbi->quoteString($hostname) . ';';
2942
            $sqlQuery .= $query;
2943
            if (! $this->dbi->tryQuery($query)) {
2944
                $message = Message::rawError($this->dbi->getError());
2945
            }
2946
        }
2947
2948 12
        if ($createDb3) {
2949
            // Grant all privileges on the specified database to the new user
2950 8
            $query = 'GRANT ALL PRIVILEGES ON '
2951 8
            . Util::backquote($dbname) . '.* TO '
2952 8
            . $this->dbi->quoteString($username) . '@' . $this->dbi->quoteString($hostname) . ';';
2953 8
            $sqlQuery .= $query;
2954 8
            if (! $this->dbi->tryQuery($query)) {
2955
                $message = Message::rawError($this->dbi->getError());
2956
            }
2957
        }
2958
2959 12
        return [$sqlQuery, $message];
2960
    }
2961
2962
    /**
2963
     * Get the hashed string for password
2964
     *
2965
     * @param string $password password
2966
     */
2967 4
    public function getHashedPassword(string $password): string
2968
    {
2969 4
        return (string) $this->dbi->fetchValue('SELECT PASSWORD(' . $this->dbi->quoteString($password) . ');');
2970
    }
2971
2972
    /**
2973
     * Check if MariaDB's 'simple_password_check'
2974
     * OR 'cracklib_password_check' is ACTIVE
2975
     */
2976 28
    private function checkIfMariaDBPwdCheckPluginActive(): bool
2977
    {
2978 28
        $serverVersion = $this->dbi->getVersion();
2979 28
        if (! (Compatibility::isMariaDb() && $serverVersion >= 100002)) {
2980 28
            return false;
2981
        }
2982
2983
        $result = $this->dbi->tryQuery('SHOW PLUGINS SONAME LIKE \'%_password_check%\'');
2984
2985
        /* Plugins are not working, for example directory does not exists */
2986
        if ($result === false) {
2987
            return false;
2988
        }
2989
2990
        while ($row = $result->fetchAssoc()) {
2991
            if ($row['Status'] === 'ACTIVE') {
2992
                return true;
2993
            }
2994
        }
2995
2996
        return false;
2997
    }
2998
2999
    /**
3000
     * Get SQL queries for Display and Add user
3001
     *
3002
     * @param string $username username
3003
     * @param string $hostname host name
3004
     * @param string $password password
3005
     *
3006
     * @return array{string, string, string, string, string, string, string, string}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{string, string, st...string, string, string} at position 2 could not be parsed: Expected ':' at position 2, but found 'string'.
Loading history...
3007
     */
3008 28
    public function getSqlQueriesForDisplayAndAddUser(string $username, string $hostname, string $password): array
3009
    {
3010 28
        $slashedUsername = $this->dbi->quoteString($username);
3011 28
        $slashedHostname = $this->dbi->quoteString($hostname);
3012 28
        $slashedPassword = $this->dbi->quoteString($password);
3013 28
        $serverVersion = $this->dbi->getVersion();
3014
3015 28
        $createUserStmt = sprintf('CREATE USER %s@%s', $slashedUsername, $slashedHostname);
3016 28
        $isMariaDBPwdPluginActive = $this->checkIfMariaDBPwdCheckPluginActive();
3017
3018
        // See https://github.com/phpmyadmin/phpmyadmin/pull/11560#issuecomment-147158219
3019
        // for details regarding details of syntax usage for various versions
3020
3021
        // 'IDENTIFIED WITH auth_plugin'
3022
        // is supported by MySQL 5.5.7+
3023 28
        if (Compatibility::isMySqlOrPerconaDb() && $serverVersion >= 50507 && isset($_POST['authentication_plugin'])) {
3024 12
            $createUserStmt .= ' IDENTIFIED WITH '
3025 12
                . $_POST['authentication_plugin'];
3026
        }
3027
3028
        // 'IDENTIFIED VIA auth_plugin'
3029
        // is supported by MariaDB 5.2+
3030
        if (
3031 28
            Compatibility::isMariaDb()
3032 28
            && $serverVersion >= 50200
3033 28
            && isset($_POST['authentication_plugin'])
3034 28
            && ! $isMariaDBPwdPluginActive
3035
        ) {
3036
            $createUserStmt .= ' IDENTIFIED VIA '
3037
                . $_POST['authentication_plugin'];
3038
        }
3039
3040 28
        $createUserReal = $createUserStmt;
3041 28
        $createUserShow = $createUserStmt;
3042
3043 28
        $passwordSetStmt = 'SET PASSWORD FOR %s@%s = %s';
3044 28
        $passwordSetShow = sprintf($passwordSetStmt, $slashedUsername, $slashedHostname, '\'***\'');
3045
3046 28
        $sqlQueryStmt = sprintf(
3047 28
            'GRANT %s ON *.* TO %s@%s',
3048 28
            implode(', ', $this->extractPrivInfo()),
3049 28
            $slashedUsername,
3050 28
            $slashedHostname,
3051 28
        );
3052 28
        $realSqlQuery = $sqlQuery = $sqlQueryStmt;
3053
3054
        // Set the proper hashing method
3055 28
        if (isset($_POST['authentication_plugin'])) {
3056 16
            $this->setProperPasswordHashing($_POST['authentication_plugin']);
3057
        }
3058
3059
        // Use 'CREATE USER ... WITH ... AS ..' syntax for
3060
        // newer MySQL versions
3061
        // and 'CREATE USER ... VIA .. USING ..' syntax for
3062
        // newer MariaDB versions
3063
        if (
3064 28
            (Compatibility::isMySqlOrPerconaDb() && $serverVersion >= 50706)
3065 28
            || (Compatibility::isMariaDb() && $serverVersion >= 50200)
3066
        ) {
3067 20
            $passwordSetReal = '';
3068
3069
            // Required for binding '%' with '%s'
3070 20
            $createUserStmt = str_replace('%', '%%', $createUserStmt);
3071
3072
            // MariaDB uses 'USING' whereas MySQL uses 'AS'
3073
            // but MariaDB with validation plugin needs cleartext password
3074 20
            if (Compatibility::isMariaDb() && ! $isMariaDBPwdPluginActive) {
3075
                $createUserStmt .= ' USING %s';
3076 20
            } elseif (Compatibility::isMariaDb()) {
3077
                $createUserStmt .= ' IDENTIFIED BY %s';
3078 20
            } elseif (Compatibility::isMySqlOrPerconaDb() && $serverVersion >= 80011) {
3079 12
                if (! str_contains($createUserStmt, 'IDENTIFIED')) {
3080
                    // Maybe the authentication_plugin was not posted and then a part is missing
3081 8
                    $createUserStmt .= ' IDENTIFIED BY %s';
3082
                } else {
3083 8
                    $createUserStmt .= ' BY %s';
3084
                }
3085
            } else {
3086 8
                $createUserStmt .= ' AS %s';
3087
            }
3088
3089 20
            if ($_POST['pred_password'] === 'keep') {
3090 16
                $createUserReal = sprintf($createUserStmt, $slashedPassword);
3091 4
            } elseif ($_POST['pred_password'] === 'none') {
3092
                $createUserReal = sprintf($createUserStmt, "''");
3093
            } else {
3094
                if (
3095 4
                    ! ((Compatibility::isMariaDb() && $isMariaDBPwdPluginActive)
3096 4
                    || Compatibility::isMySqlOrPerconaDb() && $serverVersion >= 80011)
3097
                ) {
3098
                    $hashedPassword = $this->getHashedPassword($_POST['pma_pw']);
3099
                } else {
3100
                    // MariaDB with validation plugin needs cleartext password
3101 4
                    $hashedPassword = $_POST['pma_pw'];
3102
                }
3103
3104 4
                $createUserReal = sprintf($createUserStmt, $this->dbi->quoteString($hashedPassword));
3105
            }
3106
3107 20
            $createUserShow = sprintf($createUserStmt, '\'***\'');
3108 8
        } elseif ($_POST['pred_password'] === 'keep') {
3109
            // Use 'SET PASSWORD' syntax for pre-5.7.6 MySQL versions
3110
            // and pre-5.2.0 MariaDB versions
3111 4
            $passwordSetReal = sprintf($passwordSetStmt, $slashedUsername, $slashedHostname, $slashedPassword);
3112 4
        } elseif ($_POST['pred_password'] === 'none') {
3113
            $passwordSetReal = sprintf($passwordSetStmt, $slashedUsername, $slashedHostname, "''");
3114
        } else {
3115 4
            $hashedPassword = $this->getHashedPassword($_POST['pma_pw']);
3116 4
            $passwordSetReal = sprintf(
3117 4
                $passwordSetStmt,
3118 4
                $slashedUsername,
3119 4
                $slashedHostname,
3120 4
                $this->dbi->quoteString($hashedPassword),
3121 4
            );
3122
        }
3123
3124 28
        $alterRealSqlQuery = '';
3125 28
        $alterSqlQuery = '';
3126 28
        if (Compatibility::isMySqlOrPerconaDb() && $serverVersion >= 80011) {
3127 12
            $sqlQueryStmt = '';
3128 12
            if (isset($_POST['Grant_priv']) && $_POST['Grant_priv'] === 'Y') {
3129
                $sqlQueryStmt = ' WITH GRANT OPTION';
3130
            }
3131
3132 12
            $realSqlQuery .= $sqlQueryStmt;
3133 12
            $sqlQuery .= $sqlQueryStmt;
3134
3135 12
            $alterSqlQueryStmt = sprintf('ALTER USER %s@%s', $slashedUsername, $slashedHostname);
3136 12
            $alterRealSqlQuery = $alterSqlQueryStmt;
3137 12
            $alterSqlQuery = $alterSqlQueryStmt;
3138
        }
3139
3140
        // add REQUIRE clause
3141 28
        $requireClause = $this->getRequireClause();
3142 28
        $withClause = $this->getWithClauseForAddUserAndUpdatePrivs();
3143
3144 28
        if (Compatibility::isMySqlOrPerconaDb() && $serverVersion >= 80011) {
3145 12
            $alterRealSqlQuery .= $requireClause;
3146 12
            $alterSqlQuery .= $requireClause;
3147 12
            $alterRealSqlQuery .= $withClause;
3148 12
            $alterSqlQuery .= $withClause;
3149
        } else {
3150 16
            $realSqlQuery .= $requireClause;
3151 16
            $sqlQuery .= $requireClause;
3152 16
            $realSqlQuery .= $withClause;
3153 16
            $sqlQuery .= $withClause;
3154
        }
3155
3156 28
        if ($alterRealSqlQuery !== '') {
3157 12
            $alterRealSqlQuery .= ';';
3158 12
            $alterSqlQuery .= ';';
3159
        }
3160
3161 28
        $createUserReal .= ';';
3162 28
        $createUserShow .= ';';
3163 28
        $realSqlQuery .= ';';
3164 28
        $sqlQuery .= ';';
3165
        // No Global GRANT_OPTION privilege
3166 28
        if (! $this->dbi->isGrantUser()) {
3167
            $realSqlQuery = '';
3168
            $sqlQuery = '';
3169
        }
3170
3171
        // Use 'SET PASSWORD' for pre-5.7.6 MySQL versions
3172
        // and pre-5.2.0 MariaDB
3173
        if (
3174 28
            (Compatibility::isMySqlOrPerconaDb()
3175 28
            && $serverVersion >= 50706)
3176 28
            || (Compatibility::isMariaDb()
3177 28
            && $serverVersion >= 50200)
3178
        ) {
3179 20
            $passwordSetReal = '';
3180 20
            $passwordSetShow = '';
3181
        } else {
3182 8
            if ($passwordSetReal !== '') {
3183 8
                $passwordSetReal .= ';';
3184
            }
3185
3186 8
            $passwordSetShow .= ';';
3187
        }
3188
3189 28
        return [
3190 28
            $createUserReal,
3191 28
            $createUserShow,
3192 28
            $realSqlQuery,
3193 28
            $sqlQuery,
3194 28
            $passwordSetReal,
3195 28
            $passwordSetShow,
3196 28
            $alterRealSqlQuery,
3197 28
            $alterSqlQuery,
3198 28
        ];
3199
    }
3200
3201
    /**
3202
     * Returns the type ('PROCEDURE' or 'FUNCTION') of the routine
3203
     *
3204
     * @param string $dbname      database
3205
     * @param string $routineName routine
3206
     *
3207
     * @return string type
3208
     */
3209
    public function getRoutineType(string $dbname, string $routineName): string
3210
    {
3211
        $routineData = Routines::getDetails($this->dbi, $dbname);
3212
        $routineName = mb_strtolower($routineName);
3213
3214
        foreach ($routineData as $routine) {
3215
            if (mb_strtolower($routine->name) === $routineName) {
3216
                return $routine->type;
3217
            }
3218
        }
3219
3220
        return '';
3221
    }
3222
3223
    /**
3224
     * @param string $username User name
3225
     * @param string $hostname Host name
3226
     * @param string $database Database name
3227
     * @param string $routine  Routine name
3228
     *
3229
     * @return array<string, string>
3230
     */
3231
    private function getRoutinePrivileges(
3232
        string $username,
3233
        string $hostname,
3234
        string $database,
3235
        string $routine,
3236
    ): array {
3237
        $sql = 'SELECT `Proc_priv`'
3238
            . ' FROM `mysql`.`procs_priv`'
3239
            . $this->getUserHostCondition($username, $hostname)
3240
            . ' AND `Db` = ' . $this->dbi->quoteString($this->unescapeGrantWildcards($database))
3241
            . ' AND `Routine_name` LIKE ' . $this->dbi->quoteString($routine) . ';';
3242
        $privileges = (string) $this->dbi->fetchValue($sql);
3243
3244
        return $this->parseProcPriv($privileges);
3245
    }
3246
3247
    /** @psalm-param non-empty-string $route */
3248 4
    public function getFormForChangePassword(
3249
        string $username,
3250
        string $hostname,
3251
        bool $editOthers,
3252
        string $route,
3253
    ): string {
3254 4
        $isPrivileges = $route === '/server/privileges';
3255
3256 4
        $serverVersion = $this->dbi->getVersion();
3257 4
        $origAuthPlugin = $this->getCurrentAuthenticationPlugin($username, $hostname);
3258
3259 4
        $isNew = (Compatibility::isMySqlOrPerconaDb() && $serverVersion >= 50507)
3260 4
            || (Compatibility::isMariaDb() && $serverVersion >= 50200);
3261 4
        $hasMoreAuthPlugins = (Compatibility::isMySqlOrPerconaDb() && $serverVersion >= 50706)
3262 4
            || ($this->dbi->isSuperUser() && $editOthers);
3263
3264 4
        $activeAuthPlugins = ['mysql_native_password' => __('Native MySQL authentication')];
3265
3266 4
        if ($isNew && $hasMoreAuthPlugins) {
3267 4
            $activeAuthPlugins = $this->plugins->getAuthentication();
3268 4
            if (isset($activeAuthPlugins['mysql_old_password'])) {
3269 4
                unset($activeAuthPlugins['mysql_old_password']);
3270
            }
3271
        }
3272
3273 4
        return $this->template->render('server/privileges/change_password', [
3274 4
            'username' => $username,
3275 4
            'hostname' => $hostname,
3276 4
            'is_privileges' => $isPrivileges,
3277 4
            'is_new' => $isNew,
3278 4
            'has_more_auth_plugins' => $hasMoreAuthPlugins,
3279 4
            'active_auth_plugins' => $activeAuthPlugins,
3280 4
            'orig_auth_plugin' => $origAuthPlugin,
3281 4
        ]);
3282
    }
3283
3284
    /**
3285
     * @see https://dev.mysql.com/doc/refman/en/account-locking.html
3286
     * @see https://mariadb.com/kb/en/account-locking/
3287
     *
3288
     * @return array<string, string|null>|null
3289
     */
3290 8
    private function getUserPrivileges(string $user, string $host, bool $hasAccountLocking): array|null
3291
    {
3292 8
        $query = 'SELECT * FROM `mysql`.`user` WHERE `User` = ? AND `Host` = ?;';
3293 8
        $statement = $this->dbi->prepare($query);
3294 8
        if ($statement === null || ! $statement->execute([$user, $host])) {
3295 4
            return null;
3296
        }
3297
3298 4
        $result = $statement->getResult();
3299
        /** @var array<string, string|null>|null $userPrivileges */
3300 4
        $userPrivileges = $result->fetchAssoc();
3301 4
        if ($userPrivileges === []) {
3302
            return null;
3303
        }
3304
3305 4
        if (! $hasAccountLocking || ! $this->dbi->isMariaDB()) {
3306
            return $userPrivileges;
3307
        }
3308
3309 4
        $userPrivileges['account_locked'] = 'N';
3310
3311 4
        $query = 'SELECT * FROM `mysql`.`global_priv` WHERE `User` = ? AND `Host` = ?;';
3312 4
        $statement = $this->dbi->prepare($query);
3313 4
        if ($statement === null || ! $statement->execute([$user, $host])) {
3314
            return $userPrivileges;
3315
        }
3316
3317 4
        $result = $statement->getResult();
3318
        /** @var array<string, string|null>|null $globalPrivileges */
3319 4
        $globalPrivileges = $result->fetchAssoc();
3320 4
        if ($globalPrivileges === []) {
3321
            return $userPrivileges;
3322
        }
3323
3324 4
        $privileges = json_decode($globalPrivileges['Priv'] ?? '[]', true);
3325 4
        if (! is_array($privileges)) {
3326
            return $userPrivileges;
3327
        }
3328
3329 4
        if (isset($privileges['account_locked']) && $privileges['account_locked']) {
3330 4
            $userPrivileges['account_locked'] = 'Y';
3331
        }
3332
3333 4
        return $userPrivileges;
3334
    }
3335
3336 40
    private function getUserHostCondition(string $username, string $hostname): string
3337
    {
3338 40
        return ' WHERE `User` = ' . $this->dbi->quoteString($username)
3339 40
            . ' AND `Host` = ' . $this->dbi->quoteString($hostname);
3340
    }
3341
3342 16
    private function userExists(string $username, string $hostname): bool
3343
    {
3344 16
        $sql = "SELECT '1' FROM `mysql`.`user`" . $this->getUserHostCondition($username, $hostname) . ';';
3345
3346 16
        return (bool) $this->dbi->fetchValue($sql);
3347
    }
3348
3349
    /** @param array<array<array<mixed>>> $dbRights */
3350 4
    private function getEmptyUserNotice(array $dbRights): string
3351
    {
3352 4
        foreach ($dbRights as $user => $userRights) {
3353 4
            foreach (array_keys($userRights) as $host) {
3354 4
                if ($user === '' && $host === 'localhost') {
3355
                    return Message::notice(
3356
                        __(
3357
                            'A user account allowing any user from localhost to '
3358
                            . 'connect is present. This will prevent other users '
3359
                            . 'from connecting if the host part of their account '
3360
                            . 'allows a connection from any (%) host.',
3361
                        )
3362
                        . MySQLDocumentation::show('problems-connecting'),
3363
                    )->getDisplay();
3364
                }
3365
            }
3366
        }
3367
3368 4
        return '';
3369
    }
3370
}
3371