Privileges::extractPrivInfo()   F
last analyzed

Complexity

Conditions 25
Paths 240

Size

Total Lines 70
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 46.5741

Importance

Changes 0
Metric Value
eloc 49
c 0
b 0
f 0
dl 0
loc 70
rs 2.8333
ccs 29
cts 43
cp 0.6744
cc 25
nc 240
nop 3
crap 46.5741

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

489
                    if (str_starts_with(/** @scrutinizer ignore-type */ $row1[0], 'max_')) {
Loading history...
490
                        $row[$row1[0]] = 0;
491
                    } elseif (str_starts_with($row1[0], 'x509_') || str_starts_with($row1[0], 'ssl_')) {
492
                        $row[$row1[0]] = '';
493
                    } else {
494
                        $row[$row1[0]] = 'N';
495
                    }
496
                }
497 12
            } elseif ($table === '*') {
498 4
                $row = [];
499
            } else {
500 8
                $row = ['Table_priv' => ''];
501
            }
502
        }
503
504 20
        $columns = [];
505 20
        if (isset($row['Table_priv'])) {
506 12
            $this->fillInTablePrivileges($row);
507
508
            // get columns
509 12
            $res = $this->dbi->tryQuery(
510 12
                'SHOW COLUMNS FROM '
511 12
                . Util::backquote(
512 12
                    $this->unescapeGrantWildcards($db),
513 12
                )
514 12
                . '.' . Util::backquote($table) . ';',
515 12
            );
516 12
            if ($res) {
517 12
                while ($row1 = $res->fetchRow()) {
518 8
                    $columns[$row1[0]] = [
519 8
                        'Select' => false,
520 8
                        'Insert' => false,
521 8
                        'Update' => false,
522 8
                        'References' => false,
523 8
                    ];
524
                }
525
            }
526
        }
527
528 20
        if ($columns !== []) {
529 8
            $res = $this->dbi->query(
530 8
                'SELECT `Column_name`, `Column_priv`'
531 8
                . ' FROM `mysql`.`columns_priv`'
532 8
                . $this->getUserHostCondition($username, $hostname)
533 8
                . ' AND `Db` = ' . $this->dbi->quoteString($this->unescapeGrantWildcards($db))
534 8
                . ' AND `Table_name` = ' . $this->dbi->quoteString($table) . ';',
535 8
            );
536
537 8
            while ($row1 = $res->fetchRow()) {
538 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

538
                $row1[1] = explode(',', /** @scrutinizer ignore-type */ $row1[1]);
Loading history...
539 4
                foreach ($row1[1] as $current) {
540 4
                    $columns[$row1[0]][$current] = true;
541
                }
542
            }
543
        }
544
545 20
        return $this->template->render('server/privileges/privileges_table', [
546 20
            'is_global' => $db === '*',
547 20
            'is_database' => $table === '*',
548 20
            'row' => $row,
549 20
            'columns' => $columns,
550 20
            'has_submit' => $submit,
551 20
            'supports_references_privilege' => Compatibility::supportsReferencesPrivilege($this->dbi),
552 20
            'is_mariadb' => $this->dbi->isMariaDB(),
553 20
        ]);
554
    }
555
556
    /**
557
     * Get the HTML snippet for routine specific privileges
558
     *
559
     * @param string $username  username for database connection
560
     * @param string $hostname  hostname for database connection
561
     * @param string $db        the database
562
     * @param string $routine   the routine
563
     * @param string $urlDbname url encoded db name
564
     */
565
    public function getHtmlForRoutineSpecificPrivileges(
566
        string $username,
567
        string $hostname,
568
        string $db,
569
        string $routine,
570
        string $urlDbname,
571
    ): string {
572
        $privileges = $this->getRoutinePrivileges($username, $hostname, $db, $routine);
573
574
        return $this->template->render('server/privileges/edit_routine_privileges', [
575
            'username' => $username,
576
            'hostname' => $hostname,
577
            'database' => $db,
578
            'routine' => $routine,
579
            'privileges' => $privileges,
580
            'dbname' => $urlDbname,
581
            'current_user' => $this->dbi->getCurrentUser(),
582
        ]);
583
    }
584
585
    /**
586
     * Displays the fields used by the "new user" form as well as the
587
     * "change login information / copy user" form.
588
     *
589
     * @param string|null $user User name
590
     * @param string|null $host Host name
591
     *
592
     * @return string  a HTML snippet
593
     */
594 8
    public function getHtmlForLoginInformationFields(
595
        string|null $user = null,
596
        string|null $host = null,
597
    ): string {
598 8
        [$usernameLength, $hostnameLength] = $this->getUsernameAndHostnameLength();
599
600 8
        $predefinedUsername = $this->username === '' ? 'any' : 'userdefined';
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
        $predefinedHostname = 'any';
616 8
        if ($this->hostname !== null) {
617
            $predefinedHostname = match (mb_strtolower($this->hostname)) {
618
                'localhost', '127.0.0.1' => 'localhost',
619
                '%' => 'any',
620
                default => 'userdefined',
621
            };
622
        }
623
624 8
        $serverVersion = $this->dbi->getVersion();
625 8
        if ($user !== null && $host !== null) {
626
            $authPlugin = $this->getCurrentAuthenticationPlugin($user, $host);
627
        } else {
628 8
            $authPlugin = $this->getDefaultAuthenticationPlugin();
629
        }
630
631 8
        $isNew = (Compatibility::isMySqlOrPerconaDb() && $serverVersion >= 50507)
632 8
            || (Compatibility::isMariaDb() && $serverVersion >= 50200);
633
634 8
        $activeAuthPlugins = ['mysql_native_password' => __('Native MySQL authentication')];
635 8
        if ($isNew) {
636
            $activeAuthPlugins = $this->plugins->getAuthentication();
637
            if (isset($activeAuthPlugins['mysql_old_password'])) {
638
                unset($activeAuthPlugins['mysql_old_password']);
639
            }
640
        }
641
642 8
        return $this->template->render('server/privileges/login_information_fields', [
643 8
            'predefined_username' => $predefinedUsername,
644 8
            'predefined_hostname' => $predefinedHostname,
645 8
            'username_length' => $usernameLength,
646 8
            'hostname_length' => $hostnameLength,
647 8
            'username' => $this->username,
648 8
            'hostname' => $this->hostname,
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 array{int|string|null, int|string|null} username length and hostname length
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{int|string|null, int|string|null} at position 2 could not be parsed: Expected ':' at position 2, but found 'int'.
Loading history...
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->fetchResultSimple(
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
            if ($val['COLUMN_NAME'] === 'User') {
677
                $usernameLength = $val['CHARACTER_MAXIMUM_LENGTH'];
678
            } elseif ($val['COLUMN_NAME'] === 'Host') {
679
                $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->fetchSingleColumn(
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'] ?? $this->sslType;
957
        /** @var string|null $sslCipher */
958 36
        $sslCipher = $_POST['ssl_cipher'] ?? $this->sslCipher;
959
        /** @var string|null $x509Issuer */
960 36
        $x509Issuer = $_POST['x509_issuer'] ?? $this->x509Issuer;
961
        /** @var string|null $x509Subject */
962 36
        $x509Subject = $_POST['x509_subject'] ?? $this->x509Subject;
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
        $result = $this->dbi->executeQuery($query, [$db->getName(), $table->getName()]);
1194
        if ($result === null) {
1195
            return [];
1196
        }
1197
1198
        return $result->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
        string|null $dbname,
1352
    ): array {
1353 4
        if ($dbname !== null) {
1354
            //if (preg_match('/\\\\(?:_|%)/i', $dbname)) {
1355 4
            if (preg_match('/(?<!\\\\)(?:_|%)/', $dbname) === 1) {
1356 4
                $dbnameIsWildcard = true;
1357
            } else {
1358
                $dbnameIsWildcard = false;
1359
            }
1360
        }
1361
1362 4
        $configurableMenusFeature = $this->relation->getRelationParameters()->configurableMenusFeature;
1363
1364 4
        $userGroupCount = 0;
1365 4
        if ($configurableMenusFeature !== null) {
1366
            $userGroupCount = $this->getUserGroupCount($configurableMenusFeature);
1367
        }
1368
1369 4
        $extraData = [];
1370 4
        if ($sqlQuery !== '') {
1371 4
            $extraData['sql_query'] = Generator::getMessage('', $sqlQuery);
1372
        }
1373
1374 4
        if (isset($_POST['change_copy'])) {
1375 4
            $user = [
1376 4
                'name' => $username,
1377 4
                'host' => $hostname,
1378 4
                'has_password' => $password !== '' || isset($_POST['pma_pw']),
1379 4
                'privileges' => implode(', ', $this->extractPrivInfo(null, true)),
1380 4
                'has_group' => $configurableMenusFeature !== null,
1381 4
                'has_group_edit' => $configurableMenusFeature !== null && $userGroupCount > 0,
1382 4
                'has_grant' => isset($_POST['Grant_priv']) && $_POST['Grant_priv'] === 'Y',
1383 4
            ];
1384 4
            $extraData['new_user_string'] = $this->template->render('server/privileges/new_user_ajax', [
1385 4
                'user' => $user,
1386 4
                'is_grantuser' => $this->dbi->isGrantUser(),
1387 4
                'initial' => $_GET['initial'] ?? '',
1388 4
            ]);
1389
1390
            /**
1391
             * Generate the string for this alphabet's initial, to update the user
1392
             * pagination
1393
             */
1394 4
            $newUserInitial = mb_strtoupper(
1395 4
                mb_substr($username, 0, 1),
1396 4
            );
1397 4
            $newUserInitialString = '<a href="';
1398 4
            $newUserInitialString .= Url::getFromRoute('/server/privileges', ['initial' => $newUserInitial]);
1399 4
            $newUserInitialString .= '">' . $newUserInitial . '</a>';
1400 4
            $extraData['new_user_initial'] = $newUserInitial;
1401 4
            $extraData['new_user_initial_string'] = $newUserInitialString;
1402
        }
1403
1404 4
        if (isset($_POST['update_privs'])) {
1405 4
            $extraData['db_specific_privs'] = false;
1406 4
            $extraData['db_wildcard_privs'] = false;
1407 4
            if (isset($dbnameIsWildcard)) {
1408 4
                $extraData['db_specific_privs'] = ! $dbnameIsWildcard;
1409 4
                $extraData['db_wildcard_privs'] = $dbnameIsWildcard;
1410
            }
1411
1412 4
            $newPrivileges = implode(', ', $this->extractPrivInfo(null, true));
1413
1414 4
            $extraData['new_privileges'] = $newPrivileges;
1415
        }
1416
1417 4
        if (isset($_GET['validate_username'])) {
1418 4
            $sqlQuery = 'SELECT * FROM `mysql`.`user` WHERE `User` = '
1419 4
                . $this->dbi->quoteString($_GET['username']) . ';';
1420 4
            $res = $this->dbi->query($sqlQuery);
1421 4
            $extraData['user_exists'] = $res->fetchRow() !== [];
1422
        }
1423
1424 4
        return $extraData;
1425
    }
1426
1427
    /**
1428
     * no db name given, so we want all privs for the given user
1429
     * db name was given, so we want all user specific rights for this db
1430
     * So this function returns user rights as an array
1431
     *
1432
     * @param string $username username
1433
     * @param string $hostname host name
1434
     * @param string $type     database or table
1435
     * @param string $dbname   database name
1436
     *
1437
     * @return mixed[] database rights
1438
     */
1439 4
    public function getUserSpecificRights(string $username, string $hostname, string $type, string $dbname = ''): array
1440
    {
1441 4
        $userHostCondition = $this->getUserHostCondition($username, $hostname);
1442
1443 4
        if ($type === 'database') {
1444 4
            $tablesToSearchForUsers = ['tables_priv', 'columns_priv', 'procs_priv'];
1445 4
            $dbOrTableName = 'Db';
1446 4
        } elseif ($type === 'table') {
1447 4
            $userHostCondition .= ' AND `Db` LIKE ' . $this->dbi->quoteString($dbname);
1448 4
            $tablesToSearchForUsers = ['columns_priv'];
1449 4
            $dbOrTableName = 'Table_name';
1450
        } else { // routine
1451
            $userHostCondition .= ' AND `Db` LIKE ' . $this->dbi->quoteString($dbname);
1452
            $tablesToSearchForUsers = ['procs_priv'];
1453
            $dbOrTableName = 'Routine_name';
1454
        }
1455
1456
        // we also want privileges for this user not in table `db` but in other table
1457 4
        $tables = $this->dbi->fetchSingleColumn('SHOW TABLES FROM `mysql`;');
1458
1459 4
        $dbRightsSqls = [];
1460 4
        foreach ($tablesToSearchForUsers as $tableSearchIn) {
1461 4
            if (! in_array($tableSearchIn, $tables, true)) {
1462 4
                continue;
1463
            }
1464
1465 4
            $dbRightsSqls[] = 'SELECT DISTINCT `' . $dbOrTableName
1466 4
                . '` FROM `mysql`.' . Util::backquote($tableSearchIn)
1467 4
                . $userHostCondition;
1468
        }
1469
1470 4
        $userDefaults = [$dbOrTableName => '', 'Grant_priv' => 'N', 'privs' => ['USAGE'], 'Column_priv' => true];
1471
1472
        // for the rights
1473 4
        $dbRights = [];
1474
1475 4
        $dbRightsSql = '(' . implode(') UNION (', $dbRightsSqls) . ')'
1476 4
            . ' ORDER BY `' . $dbOrTableName . '` ASC';
1477
1478 4
        $dbRightsResult = $this->dbi->query($dbRightsSql);
1479
1480 4
        while ($dbRightsRow = $dbRightsResult->fetchAssoc()) {
1481
            $dbRightsRow = array_merge($userDefaults, $dbRightsRow);
1482
            if ($type === 'database') {
1483
                // only Db names in the table `mysql`.`db` uses wildcards
1484
                // as we are in the db specific rights display we want
1485
                // all db names escaped, also from other sources
1486
                $dbRightsRow['Db'] = $this->escapeGrantWildcards($dbRightsRow['Db']);
1487
            }
1488
1489
            $dbRights[$dbRightsRow[$dbOrTableName]] = $dbRightsRow;
1490
        }
1491
1492 4
        $sqlQuery = match ($type) {
1493 4
            'database' => 'SELECT * FROM `mysql`.`db`'
1494 4
                . $userHostCondition . ' ORDER BY `Db` ASC',
1495 4
            'table' => 'SELECT `Table_name`,'
1496 4
                . ' `Table_priv`,'
1497 4
                . ' IF(`Column_priv` = _latin1 \'\', 0, 1)'
1498 4
                . ' AS \'Column_priv\''
1499 4
                . ' FROM `mysql`.`tables_priv`'
1500 4
                . $userHostCondition
1501 4
                . ' ORDER BY `Table_name` ASC;',
1502
            default => 'SELECT `Routine_name`, `Proc_priv`'
1503
                . ' FROM `mysql`.`procs_priv`'
1504
                . $userHostCondition
1505
                . ' ORDER BY `Routine_name`',
1506 4
        };
1507
1508 4
        $result = $this->dbi->query($sqlQuery);
1509
1510 4
        while ($row = $result->fetchAssoc()) {
1511
            if (isset($dbRights[$row[$dbOrTableName]])) {
1512
                $dbRights[$row[$dbOrTableName]] = array_merge($dbRights[$row[$dbOrTableName]], $row);
1513
            } else {
1514
                $dbRights[$row[$dbOrTableName]] = $row;
1515
            }
1516
1517
            if ($type !== 'database') {
1518
                continue;
1519
            }
1520
1521
            // there are db specific rights for this user
1522
            // so we can drop this db rights
1523
            $dbRights[$row['Db']]['can_delete'] = true;
1524
        }
1525
1526 4
        return $dbRights;
1527
    }
1528
1529
    /**
1530
     * Parses Proc_priv data
1531
     *
1532
     * @param string $privs Proc_priv
1533
     *
1534
     * @return array<string, string>
1535
     */
1536
    public function parseProcPriv(string $privs): array
1537
    {
1538
        $result = ['Alter_routine_priv' => 'N', 'Execute_priv' => 'N', 'Grant_priv' => 'N'];
1539
        foreach (explode(',', $privs) as $priv) {
1540
            if ($priv === 'Alter Routine') {
1541
                $result['Alter_routine_priv'] = 'Y';
1542
            } else {
1543
                $result[$priv . '_priv'] = 'Y';
1544
            }
1545
        }
1546
1547
        return $result;
1548
    }
1549
1550
    /**
1551
     * Get a HTML table for display user's table specific or database specific rights
1552
     *
1553
     * @param string $username username
1554
     * @param string $hostname host name
1555
     * @param string $type     database, table or routine
1556
     * @param string $dbname   database name
1557
     */
1558 4
    public function getHtmlForAllTableSpecificRights(
1559
        string $username,
1560
        string $hostname,
1561
        string $type,
1562
        string $dbname = '',
1563
    ): string {
1564 4
        $uiData = [
1565 4
            'database' => [
1566 4
                'form_id' => 'database_specific_priv',
1567 4
                'sub_menu_label' => __('Database'),
1568 4
                'legend' => __('Database-specific privileges'),
1569 4
                'type_label' => __('Database'),
1570 4
            ],
1571 4
            'table' => [
1572 4
                'form_id' => 'table_specific_priv',
1573 4
                'sub_menu_label' => __('Table'),
1574 4
                'legend' => __('Table-specific privileges'),
1575 4
                'type_label' => __('Table'),
1576 4
            ],
1577 4
            'routine' => [
1578 4
                'form_id' => 'routine_specific_priv',
1579 4
                'sub_menu_label' => __('Routine'),
1580 4
                'legend' => __('Routine-specific privileges'),
1581 4
                'type_label' => __('Routine'),
1582 4
            ],
1583 4
        ];
1584
1585
        /**
1586
         * no db name given, so we want all privs for the given user
1587
         * db name was given, so we want all user specific rights for this db
1588
         */
1589 4
        $dbRights = $this->getUserSpecificRights($username, $hostname, $type, $dbname);
1590 4
        ksort($dbRights);
1591
1592 4
        $foundRows = [];
1593 4
        $privileges = [];
1594 4
        foreach ($dbRights as $row) {
1595
            $onePrivilege = [];
1596
1597
            $paramTableName = '';
1598
            $paramRoutineName = '';
1599
1600
            if ($type === 'database') {
1601
                $name = $row['Db'];
1602
                $onePrivilege['grant'] = $row['Grant_priv'] === 'Y';
1603
                $onePrivilege['table_privs'] = ! empty($row['Table_priv'])
1604
                    || ! empty($row['Column_priv']);
1605
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
1606
1607
                $paramDbName = $row['Db'];
1608
            } elseif ($type === 'table') {
1609
                $name = $row['Table_name'];
1610
                $onePrivilege['grant'] = in_array(
1611
                    'Grant',
1612
                    explode(',', $row['Table_priv']),
1613
                    true,
1614
                );
1615
                $onePrivilege['column_privs'] = ! empty($row['Column_priv']);
1616
                $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true));
1617
1618
                $paramDbName = $this->escapeGrantWildcards($dbname);
1619
                $paramTableName = $row['Table_name'];
1620
            } else { // routine
1621
                $name = $row['Routine_name'];
1622
                $onePrivilege['grant'] = in_array(
1623
                    'Grant',
1624
                    explode(',', $row['Proc_priv']),
1625
                    true,
1626
                );
1627
1628
                $privs = $this->parseProcPriv($row['Proc_priv']);
1629
                $onePrivilege['privileges'] = implode(
1630
                    ',',
1631
                    $this->extractPrivInfo($privs, true),
1632
                );
1633
1634
                $paramDbName = $this->escapeGrantWildcards($dbname);
1635
                $paramRoutineName = $row['Routine_name'];
1636
            }
1637
1638
            $foundRows[] = $name;
1639
            $onePrivilege['name'] = $name;
1640
1641
            $onePrivilege['edit_link'] = '';
1642
            if ($this->dbi->isGrantUser()) {
1643
                $onePrivilege['edit_link'] = $this->getUserLink(
1644
                    'edit',
1645
                    $username,
1646
                    $hostname,
1647
                    $paramDbName,
1648
                    $paramTableName,
1649
                    $paramRoutineName,
1650
                );
1651
            }
1652
1653
            $onePrivilege['revoke_link'] = '';
1654
            if ($type !== 'database' || ! empty($row['can_delete'])) {
1655
                $onePrivilege['revoke_link'] = $this->getUserLink(
1656
                    'revoke',
1657
                    $username,
1658
                    $hostname,
1659
                    $paramDbName,
1660
                    $paramTableName,
1661
                    $paramRoutineName,
1662
                );
1663
            }
1664
1665
            $privileges[] = $onePrivilege;
1666
        }
1667
1668 4
        $data = $uiData[$type];
1669 4
        $data['privileges'] = $privileges;
1670 4
        $data['username'] = $username;
1671 4
        $data['hostname'] = $hostname;
1672 4
        $data['database'] = $dbname;
1673 4
        $data['escaped_database'] = $this->escapeGrantWildcards($dbname);
1674 4
        $data['type'] = $type;
1675
1676 4
        if ($type === 'database') {
1677 4
            $predDbArray = $this->dbi->getDatabaseList();
1678 4
            $databasesToSkip = ['information_schema', 'performance_schema'];
1679
1680 4
            $databases = [];
1681 4
            $escapedDatabases = [];
1682 4
            foreach ($predDbArray as $currentDb) {
1683 4
                if (in_array($currentDb, $databasesToSkip, true)) {
1684
                    continue;
1685
                }
1686
1687 4
                $currentDbEscaped = $this->escapeGrantWildcards($currentDb);
1688
                // cannot use array_diff() once, outside of the loop,
1689
                // because the list of databases has special characters
1690
                // already escaped in $foundRows,
1691
                // contrary to the output of SHOW DATABASES
1692 4
                if (in_array($currentDbEscaped, $foundRows, true)) {
1693
                    continue;
1694
                }
1695
1696 4
                $databases[] = $currentDb;
1697 4
                $escapedDatabases[] = $currentDbEscaped;
1698
            }
1699
1700 4
            $data['databases'] = $databases;
1701 4
            $data['escaped_databases'] = $escapedDatabases;
1702 4
        } elseif ($type === 'table') {
1703 4
            $result = $this->dbi->tryQuery('SHOW TABLES FROM ' . Util::backquote($dbname));
1704
1705 4
            $tables = [];
1706 4
            if ($result) {
1707 4
                while ($row = $result->fetchRow()) {
1708 4
                    if (in_array($row[0], $foundRows, true)) {
1709
                        continue;
1710
                    }
1711
1712 4
                    $tables[] = $row[0];
1713
                }
1714
            }
1715
1716 4
            $data['tables'] = $tables;
1717
        } else { // routine
1718
            $routineData = Routines::getDetails($this->dbi, $dbname);
1719
1720
            $routines = [];
1721
            foreach ($routineData as $routine) {
1722
                if (in_array($routine->name, $foundRows, true)) {
1723
                    continue;
1724
                }
1725
1726
                $routines[] = $routine->name;
1727
            }
1728
1729
            $data['routines'] = $routines;
1730
        }
1731
1732 4
        return $this->template->render('server/privileges/privileges_summary', $data);
1733
    }
1734
1735
    /**
1736
     * Get HTML for display the users overview
1737
     * (if less than 50 users, display them immediately)
1738
     *
1739
     * @param ResultInterface $result   ran sql query
1740
     * @param mixed[]         $dbRights user's database rights array
1741
     *
1742
     * @return string HTML snippet
1743
     */
1744 8
    public function getUsersOverview(ResultInterface $result, array $dbRights): 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
            'initial' => $_GET['initial'] ?? '',
1806 8
            'hosts' => $hosts,
1807 8
            'is_grantuser' => $this->dbi->isGrantUser(),
1808 8
            'is_createuser' => $this->dbi->isCreateUser(),
1809 8
            'has_account_locking' => $hasAccountLocking,
1810 8
        ]);
1811
    }
1812
1813
    /**
1814
     * Displays the initials if there are many privileges
1815
     */
1816 8
    public function getHtmlForInitials(): string
1817
    {
1818 8
        $usersCount = $this->dbi->fetchValue('SELECT COUNT(*) FROM `mysql`.`user`');
1819 8
        if ($usersCount === false || $usersCount <= 20) {
1820 4
            return '';
1821
        }
1822
1823 4
        $result = $this->dbi->tryQuery('SELECT DISTINCT UPPER(LEFT(`User`, 1)) FROM `user`');
1824 4
        if ($result === false) {
1825
            return '';
1826
        }
1827
1828 4
        $initials = $result->fetchAllColumn();
1829
        // Display the initials, which can be any characters, not
1830
        // just letters. For letters A-Z, we add the non-used letters
1831
        // as greyed out.
1832 4
        $initialsMap = array_fill_keys($initials, true) + array_fill_keys(range('A', 'Z'), false);
1833 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...
1834
1835 4
        return $this->template->render('server/privileges/initials_row', [
1836 4
            'array_initials' => $initialsMap,
1837 4
            'selected_initial' => $_GET['initial'] ?? null,
1838 4
        ]);
1839
    }
1840
1841
    /**
1842
     * Get the database rights array for Display user overview
1843
     *
1844
     * @return (string|string[])[][][]    database rights array
1845
     */
1846 8
    public function getDbRightsForUserOverview(string|null $initial): array
1847
    {
1848
        // we also want users not in table `user` but in other table
1849 8
        $mysqlTables = $this->dbi->fetchSingleColumn('SHOW TABLES FROM `mysql`');
1850 8
        $userTables = ['user', 'db', 'tables_priv', 'columns_priv', 'procs_priv'];
1851 8
        $whereUser = $this->rangeOfUsers($initial);
1852 8
        $sqls = [];
1853 8
        foreach (array_intersect($userTables, $mysqlTables) as $table) {
1854 4
            $sqls[] = '(SELECT DISTINCT `User`, `Host` FROM `mysql`.`' . $table . '` ' . $whereUser . ')';
1855
        }
1856
1857 8
        $sql = implode(' UNION ', $sqls) . ' ORDER BY `User` ASC, `Host` ASC';
1858 8
        $result = $this->dbi->query($sql);
1859
1860 8
        $userDefaults = ['User' => '', 'Host' => '%', 'Password' => '?', 'Grant_priv' => 'N', 'privs' => ['USAGE']];
1861 8
        $dbRights = [];
1862 8
        while ($row = $result->fetchAssoc()) {
1863
            /** @psalm-var array{User: string, Host: string} $row */
1864 8
            $dbRights[$row['User']][$row['Host']] = array_merge($userDefaults, $row);
1865
        }
1866
1867 8
        ksort($dbRights);
1868
1869 8
        return $dbRights;
1870
    }
1871
1872
    /**
1873
     * Delete user and get message and sql query for delete user in privileges
1874
     *
1875
     * @param mixed[] $queries queries
1876
     *
1877
     * @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...
1878
     */
1879 4
    public function deleteUser(array $queries): array
1880
    {
1881 4
        $sqlQuery = '';
1882 4
        if ($queries === []) {
1883 4
            $message = Message::error(__('No users selected for deleting!'));
1884
        } else {
1885 4
            if ($_POST['mode'] == 3) {
1886 4
                $queries[] = '# ' . __('Reloading the privileges') . ' …';
1887 4
                $queries[] = 'FLUSH PRIVILEGES;';
1888
            }
1889
1890 4
            $dropUserError = '';
1891 4
            foreach ($queries as $sqlQuery) {
1892 4
                if ($sqlQuery[0] === '#') {
1893 4
                    continue;
1894
                }
1895
1896 4
                if ($this->dbi->tryQuery($sqlQuery)) {
1897 4
                    continue;
1898
                }
1899
1900 4
                $dropUserError .= $this->dbi->getError() . "\n";
1901
            }
1902
1903
            // tracking sets this, causing the deleted db to be shown in navi
1904 4
            Current::$database = '';
1905
1906 4
            $sqlQuery = implode("\n", $queries);
1907 4
            if ($dropUserError !== '') {
1908 4
                $message = Message::rawError($dropUserError);
1909
            } else {
1910 4
                $message = Message::success(
1911 4
                    __('The selected users have been deleted successfully.'),
1912 4
                );
1913
            }
1914
        }
1915
1916 4
        return [$sqlQuery, $message];
1917
    }
1918
1919
    /**
1920
     * Update the privileges and return the success or error message
1921
     *
1922
     * @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...
1923
     */
1924 12
    public function updatePrivileges(
1925
        string $username,
1926
        string $hostname,
1927
        string $tablename,
1928
        string $dbname,
1929
        string $itemType,
1930
    ): array {
1931 12
        $dbAndTable = $this->wildcardEscapeForGrant($dbname, $tablename);
1932
1933 12
        $sqlQuery0 = 'REVOKE ALL PRIVILEGES ON ' . $itemType . ' ' . $dbAndTable
1934 12
            . ' FROM ' . $this->dbi->quoteString($username)
1935 12
            . '@' . $this->dbi->quoteString($hostname) . ';';
1936
1937 12
        if (! isset($_POST['Grant_priv']) || $_POST['Grant_priv'] !== 'Y') {
1938
            $sqlQuery1 = 'REVOKE GRANT OPTION ON ' . $itemType . ' ' . $dbAndTable
1939
                . ' FROM ' . $this->dbi->quoteString($username) . '@'
1940
                . $this->dbi->quoteString($hostname) . ';';
1941
        } else {
1942 12
            $sqlQuery1 = '';
1943
        }
1944
1945 12
        $grantBackQuery = null;
1946 12
        $alterUserQuery = null;
1947
1948
        // Should not do a GRANT USAGE for a table-specific privilege, it
1949
        // causes problems later (cannot revoke it)
1950 12
        if (! ($tablename !== '' && implode('', $this->extractPrivInfo()) === 'USAGE')) {
1951 8
            [$grantBackQuery, $alterUserQuery] = $this->generateQueriesForUpdatePrivileges(
1952 8
                $itemType,
1953 8
                $dbAndTable,
1954 8
                $username,
1955 8
                $hostname,
1956 8
                $dbname,
1957 8
            );
1958
        }
1959
1960 12
        if (! $this->dbi->tryQuery($sqlQuery0)) {
1961
            // This might fail when the executing user does not have
1962
            // ALL PRIVILEGES themselves.
1963
            // See https://github.com/phpmyadmin/phpmyadmin/issues/9673
1964 8
            $sqlQuery0 = '';
1965
        }
1966
1967 12
        if ($sqlQuery1 !== '' && ! $this->dbi->tryQuery($sqlQuery1)) {
1968
            // this one may fail, too...
1969
            $sqlQuery1 = '';
1970
        }
1971
1972 12
        if ($grantBackQuery !== null) {
1973 8
            $this->dbi->query($grantBackQuery);
1974
        } else {
1975 4
            $grantBackQuery = '';
1976
        }
1977
1978 12
        if ($alterUserQuery !== null) {
1979 4
            $this->dbi->query($alterUserQuery);
1980
        } else {
1981 8
            $alterUserQuery = '';
1982
        }
1983
1984 12
        $sqlQuery = $sqlQuery0 . ' ' . $sqlQuery1 . ' ' . $grantBackQuery . ' ' . $alterUserQuery;
1985 12
        $message = Message::success(__('You have updated the privileges for %s.'));
1986 12
        $message->addParam('\'' . $username . '\'@\'' . $hostname . '\'');
1987
1988 12
        return [$sqlQuery, $message];
1989
    }
1990
1991
    /**
1992
     * Generate the query for the GRANTS and requirements + limits
1993
     *
1994
     * @return array<int,string|null>
1995
     */
1996 8
    private function generateQueriesForUpdatePrivileges(
1997
        string $itemType,
1998
        string $dbAndTable,
1999
        string $username,
2000
        string $hostname,
2001
        string $dbname,
2002
    ): array {
2003 8
        $alterUserQuery = null;
2004
2005 8
        $grantBackQuery = 'GRANT ' . implode(', ', $this->extractPrivInfo())
2006 8
            . ' ON ' . $itemType . ' ' . $dbAndTable
2007 8
            . ' TO ' . $this->dbi->quoteString($username) . '@'
2008 8
            . $this->dbi->quoteString($hostname);
2009
2010 8
        $isMySqlOrPercona = Compatibility::isMySqlOrPerconaDb();
2011 8
        $needsToUseAlter = $isMySqlOrPercona && $this->dbi->getVersion() >= 80011;
2012
2013 8
        if ($needsToUseAlter) {
2014 4
            $alterUserQuery = 'ALTER USER ' . $this->dbi->quoteString($username) . '@'
2015 4
            . $this->dbi->quoteString($hostname) . ' ';
2016
        }
2017
2018 8
        if ($dbname === '') {
2019
            // add REQUIRE clause
2020 8
            if ($needsToUseAlter) {
2021 4
                $alterUserQuery .= $this->getRequireClause();
2022
            } else {
2023 4
                $grantBackQuery .= $this->getRequireClause();
2024
            }
2025
        }
2026
2027
        if (
2028 8
            (isset($_POST['Grant_priv']) && $_POST['Grant_priv'] === 'Y')
2029 8
            || ($dbname === ''
2030 8
            && (isset($_POST['max_questions']) || isset($_POST['max_connections'])
2031 8
            || isset($_POST['max_updates'])
2032 8
            || isset($_POST['max_user_connections'])))
2033
        ) {
2034 8
            if ($needsToUseAlter) {
2035 4
                $alterUserQuery .= $this->getWithClauseForAddUserAndUpdatePrivs();
2036
            } else {
2037 4
                $grantBackQuery .= $this->getWithClauseForAddUserAndUpdatePrivs();
2038
            }
2039
        }
2040
2041 8
        $grantBackQuery .= ';';
2042
2043 8
        if ($needsToUseAlter) {
2044 4
            $alterUserQuery .= ';';
2045
        }
2046
2047 8
        return [$grantBackQuery, $alterUserQuery];
2048
    }
2049
2050
    /**
2051
     * Get List of information: Changes / copies a user
2052
     */
2053 4
    public function getDataForChangeOrCopyUser(string $oldUsername, string $oldHostname): string|null
2054
    {
2055 4
        if (isset($_POST['change_copy'])) {
2056 4
            $userHostCondition = $this->getUserHostCondition($oldUsername, $oldHostname);
2057 4
            $row = $this->dbi->fetchSingleRow('SELECT * FROM `mysql`.`user` ' . $userHostCondition . ';');
2058 4
            if ($row === []) {
2059
                $response = ResponseRenderer::getInstance();
2060
                $response->addHTML(
2061
                    Message::notice(__('No user found.'))->getDisplay(),
2062
                );
2063
                unset($_POST['change_copy']);
2064
            } else {
2065 4
                $this->sslType = $row['ssl_type'];
2066 4
                $this->sslCipher = $row['ssl_cipher'];
2067 4
                $this->x509Issuer = $row['x509_issuer'];
2068 4
                $this->x509Subject = $row['x509_subject'];
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
            ResponseRenderer::$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
            $this->dbi->tryQuery('FLUSH PRIVILEGES;');
2172
            $message = Message::success(
2173
                __('The privileges were reloaded successfully.'),
2174
            );
2175
        }
2176
2177
        if (isset($_GET['validate_username'])) {
2178
            return Message::success();
2179
        }
2180
2181
        return $message;
2182
    }
2183
2184
    /**
2185
     * update Data For Queries from queries_for_display
2186
     *
2187
     * @param mixed[]      $queries           queries array
2188
     * @param mixed[]|null $queriesForDisplay queries array for display
2189
     *
2190
     * @return mixed[]
2191
     */
2192
    public function getDataForQueries(array $queries, array|null $queriesForDisplay): array
2193
    {
2194
        $tmpCount = 0;
2195
        foreach ($queries as $sqlQuery) {
2196
            if ($sqlQuery[0] !== '#') {
2197
                $this->dbi->query($sqlQuery);
2198
            }
2199
2200
            // when there is a query containing a hidden password, take it
2201
            // instead of the real query sent
2202
            if (isset($queriesForDisplay[$tmpCount])) {
2203
                $queries[$tmpCount] = $queriesForDisplay[$tmpCount];
2204
            }
2205
2206
            $tmpCount++;
2207
        }
2208
2209
        return $queries;
2210
    }
2211
2212
    /**
2213
     * update Data for information: Adds a user
2214
     *
2215
     * @param string      $dbname     db name
2216
     * @param string      $username   user name
2217
     * @param string      $hostname   host name
2218
     * @param string|null $password   password
2219
     * @param bool        $isMenuwork is_menuwork set?
2220
     *
2221
     * @return array{Message|null, string[], string[]|null, string, bool}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{Message|null, stri...g[]|null, string, bool} at position 2 could not be parsed: Expected ':' at position 2, but found 'Message'.
Loading history...
2222
     */
2223 8
    public function addUser(
2224
        string $dbname,
2225
        string $username,
2226
        string $hostname,
2227
        string|null $password,
2228
        bool $isMenuwork,
2229
    ): array {
2230
        // Some reports were sent to the error reporting server with phpMyAdmin 5.1.0
2231
        // pred_username was reported to be not defined
2232 8
        $predUsername = $_POST['pred_username'] ?? '';
2233 8
        if ($predUsername === 'any') {
2234 8
            $username = '';
2235
        }
2236
2237 8
        if ($this->userExists($username, $hostname)) {
2238
            $message = Message::error(__('The user %s already exists!'));
2239
            $message->addParam('[em]\'' . $username . '\'@\'' . $hostname . '\'[/em]');
2240
            $_GET['adduser'] = true;
2241
2242
            return [
2243
                $message,
2244
                [],
2245
                null,
2246
                '',
2247
                true, // Add user error
2248
            ];
2249
        }
2250
2251 8
        [
2252 8
            $createUserReal,
2253 8
            $createUserShow,
2254 8
            $realSqlQuery,
2255 8
            $sqlQuery,
2256 8
            $passwordSetReal,
2257 8
            $passwordSetShow,
2258 8
            $alterRealSqlQuery,
2259 8
            $alterSqlQuery,
2260 8
        ] = $this->getSqlQueriesForDisplayAndAddUser($username, $hostname, $password ?? '');
2261
2262 8
        if (empty($_POST['change_copy'])) {
2263 8
            $error = false;
2264
2265 8
            if (! $this->dbi->tryQuery($createUserReal)) {
2266
                $error = true;
2267
            }
2268
2269 8
            if (isset($_POST['authentication_plugin']) && $passwordSetReal !== '') {
2270 4
                $this->setProperPasswordHashing($_POST['authentication_plugin']);
2271 4
                if ($this->dbi->tryQuery($passwordSetReal)) {
2272 4
                    $sqlQuery .= $passwordSetShow;
2273
                }
2274
            }
2275
2276 8
            $sqlQuery = $createUserShow . $sqlQuery;
2277
2278 8
            [$sqlQuery, $message] = $this->addUserAndCreateDatabase(
2279 8
                $error,
2280 8
                $realSqlQuery,
2281 8
                $sqlQuery,
2282 8
                $username,
2283 8
                $hostname,
2284 8
                $dbname,
2285 8
                $alterRealSqlQuery,
2286 8
                $alterSqlQuery,
2287 8
                isset($_POST['createdb-1']),
2288 8
                isset($_POST['createdb-2']),
2289 8
                isset($_POST['createdb-3']),
2290 8
            );
2291 8
            if (! empty($_POST['userGroup']) && $isMenuwork) {
2292 8
                $this->setUserGroup($this->username ?? '', $_POST['userGroup']);
2293
            }
2294
2295 8
            return [
2296 8
                $message,
2297 8
                [],
2298 8
                null,
2299 8
                $sqlQuery,
2300 8
                $error, // Add user error if the query fails
2301 8
            ];
2302
        }
2303
2304
        // Copy the user group while copying a user
2305
        $oldUserGroup = $_POST['old_usergroup'] ?? '';
2306
        $this->setUserGroup($_POST['username'], $oldUserGroup);
2307
2308
        $queries = [];
2309
        $queries[] = $createUserReal;
2310
        $queries[] = $realSqlQuery;
2311
2312
        if (isset($_POST['authentication_plugin']) && $passwordSetReal !== '') {
2313
            $this->setProperPasswordHashing($_POST['authentication_plugin']);
2314
2315
            $queries[] = $passwordSetReal;
2316
        }
2317
2318
        // we put the query containing the hidden password in
2319
        // $queries_for_display, at the same position occupied
2320
        // by the real query in $queries
2321
        $tmpCount = count($queries);
2322
        $queriesForDisplay = [];
2323
        $queriesForDisplay[$tmpCount - 2] = $createUserShow;
2324
2325
        if ($passwordSetReal !== '') {
2326
            $queriesForDisplay[$tmpCount - 3] = $createUserShow;
2327
            $queriesForDisplay[$tmpCount - 2] = $sqlQuery;
2328
            $queriesForDisplay[$tmpCount - 1] = $passwordSetShow;
2329
        } else {
2330
            $queriesForDisplay[$tmpCount - 1] = $sqlQuery;
2331
        }
2332
2333
        return [
2334
            null,
2335
            $queries,
2336
            $queriesForDisplay,
2337
            $sqlQuery,
2338
            false, // Add user error
2339
        ];
2340
    }
2341
2342
    /**
2343
     * Sets proper value of `old_passwords` according to
2344
     * the authentication plugin selected
2345
     *
2346
     * @param string $authPlugin authentication plugin selected
2347
     */
2348 16
    public function setProperPasswordHashing(string $authPlugin): void
2349
    {
2350
        // Set the hashing method used by PASSWORD()
2351
        // to be of type depending upon $authentication_plugin
2352 16
        if ($authPlugin === 'sha256_password') {
2353
            $this->dbi->tryQuery('SET `old_passwords` = 2;');
2354 16
        } elseif ($authPlugin === 'mysql_old_password') {
2355
            $this->dbi->tryQuery('SET `old_passwords` = 1;');
2356
        } else {
2357 16
            $this->dbi->tryQuery('SET `old_passwords` = 0;');
2358
        }
2359
    }
2360
2361
    /**
2362
     * Get title and textarea for export user definition in Privileges
2363
     *
2364
     * @param string        $username      username
2365
     * @param string        $hostname      host name
2366
     * @param string[]|null $selectedUsers
2367
     */
2368 4
    public function getExportUserDefinitionTextarea(
2369
        string $username,
2370
        string $hostname,
2371
        array|null $selectedUsers,
2372
    ): string {
2373 4
        $export = '<textarea class="export" cols="60" rows="15">';
2374
2375 4
        if ($selectedUsers !== null) {
0 ignored issues
show
introduced by
The condition $selectedUsers !== null is always true.
Loading history...
2376
            //For removing duplicate entries of users
2377
            $selectedUsers = array_unique($selectedUsers);
2378
2379
            foreach ($selectedUsers as $exportUser) {
2380
                $exportUsername = mb_substr(
2381
                    $exportUser,
2382
                    0,
2383
                    (int) mb_strpos($exportUser, '&'),
2384
                );
2385
                $exportHostname = mb_substr(
2386
                    $exportUser,
2387
                    mb_strrpos($exportUser, ';') + 1,
2388
                );
2389
                $export .= '# '
2390
                    . sprintf(
2391
                        __('Privileges for %s'),
2392
                        '`' . htmlspecialchars($exportUsername)
2393
                        . '`@`' . htmlspecialchars($exportHostname) . '`',
2394
                    )
2395
                    . "\n\n";
2396
                $export .= $this->getGrants($exportUsername, $exportHostname) . "\n";
2397
            }
2398
        } else {
2399
            // export privileges for a single user
2400 4
            $export .= $this->getGrants($username, $hostname);
2401
        }
2402
2403
        // remove trailing whitespace
2404 4
        $export = trim($export);
2405
2406 4
        return $export . '</textarea>';
2407
    }
2408
2409
    /**
2410
     * Get HTML for display Add userfieldset
2411
     *
2412
     * @return string html output
2413
     */
2414 4
    private function getAddUserHtmlFieldset(): string
2415
    {
2416 4
        if (! $this->dbi->isCreateUser()) {
2417
            return '';
2418
        }
2419
2420 4
        return $this->template->render('server/privileges/add_user_fieldset', ['url_params' => ['adduser' => 1]]);
2421
    }
2422
2423 4
    private function checkStructureOfPrivilegeTable(): string
2424
    {
2425
        // the query failed! This may have two reasons:
2426
        // - the user does not have enough privileges
2427
        // - the privilege tables use a structure of an earlier version.
2428
        // so let's try a more simple query
2429 4
        if (! $this->dbi->tryQuery('SELECT 1 FROM `mysql`.`user`')) {
2430 4
            return $this->getHtmlForViewUsersError() . $this->getAddUserHtmlFieldset();
2431
        }
2432
2433
        // This message is hardcoded because I will replace it by
2434
        // a automatic repair feature soon.
2435 4
        $raw = 'Your privilege table structure seems to be older than'
2436 4
            . ' this MySQL version!<br>'
2437 4
            . 'Please run the <code>mysql_upgrade</code> command'
2438 4
            . ' that should be included in your MySQL server distribution'
2439 4
            . ' to solve this problem!';
2440
2441 4
        return Message::rawError($raw)->getDisplay();
2442
    }
2443
2444
    /**
2445
     * Get HTML snippet for display user overview page
2446
     */
2447 4
    public function getHtmlForUserOverview(UserPrivileges $userPrivileges, string|null $initial): string
2448
    {
2449 4
        $serverVersion = $this->dbi->getVersion();
2450 4
        $passwordColumn = Compatibility::isMySqlOrPerconaDb() && $serverVersion >= 50706
2451 4
            ? 'authentication_string'
2452
            : 'Password';
2453
2454
        // $sql is for the initial-filtered
2455 4
        $sql = 'SELECT *, IF(`' . $passwordColumn . "` = _latin1 '', 'N', 'Y') AS `Password`" .
2456 4
            ' FROM `mysql`.`user` ' . $this->rangeOfUsers($initial) . ' ORDER BY `User` ASC, `Host` ASC';
2457
2458 4
        $res = $this->dbi->tryQuery($sql);
2459 4
        if ($res === false) {
2460 4
            $errorMessages = $this->checkStructureOfPrivilegeTable();
2461
        } else {
2462 4
            $dbRights = $this->getDbRightsForUserOverview($initial);
2463 4
            $emptyUserNotice = $this->getEmptyUserNotice($dbRights);
2464 4
            $initialsHtml = $this->getHtmlForInitials();
2465
2466
            // Display the user overview (if less than 50 users, display them immediately)
2467 4
            if (isset($_GET['initial']) || isset($_GET['showall']) || $res->numRows() < 50) {
2468 4
                $usersOverview = $this->getUsersOverview($res, $dbRights) .
2469 4
                    $this->template->render('export_modal');
2470
            }
2471
2472 4
            $response = ResponseRenderer::getInstance();
2473 4
            if (! $response->isAjax() || ! empty($_REQUEST['ajax_page_request'])) {
2474 4
                if ($userPrivileges->isReload) {
2475 4
                    $flushnote = new Message(
2476 4
                        __(
2477 4
                            'Note: phpMyAdmin gets the users’ privileges directly '
2478 4
                            . 'from MySQL’s privilege tables. The content of these '
2479 4
                            . 'tables may differ from the privileges the server uses, '
2480 4
                            . 'if they have been changed manually. In this case, '
2481 4
                            . 'you should %sreload the privileges%s before you continue.',
2482 4
                        ),
2483 4
                        MessageType::Notice,
2484 4
                    );
2485 4
                    $flushnote->addParamHtml(
2486 4
                        '<a href="' . Url::getFromRoute('/server/privileges', ['flush_privileges' => 1])
2487 4
                        . '" id="reload_privileges_anchor">',
2488 4
                    );
2489 4
                    $flushnote->addParamHtml('</a>');
2490
                } else {
2491
                    $flushnote = new Message(
2492
                        __(
2493
                            'Note: phpMyAdmin gets the users’ privileges directly '
2494
                            . 'from MySQL’s privilege tables. The content of these '
2495
                            . 'tables may differ from the privileges the server uses, '
2496
                            . 'if they have been changed manually. In this case, '
2497
                            . 'the privileges have to be reloaded but currently, you '
2498
                            . 'don\'t have the RELOAD privilege.',
2499
                        )
2500
                        . MySQLDocumentation::show(
2501
                            'privileges-provided',
2502
                            false,
2503
                            null,
2504
                            null,
2505
                            'priv_reload',
2506
                        ),
2507
                        MessageType::Notice,
2508
                    );
2509
                }
2510
2511 4
                $flushNotice = $flushnote->getDisplay();
2512
            }
2513
        }
2514
2515 4
        return $this->template->render('server/privileges/user_overview', [
2516 4
            'error_messages' => $errorMessages ?? '',
2517 4
            'empty_user_notice' => $emptyUserNotice ?? '',
2518 4
            'initials' => $initialsHtml ?? '',
2519 4
            'users_overview' => $usersOverview ?? '',
2520 4
            'is_createuser' => $this->dbi->isCreateUser(),
2521 4
            'flush_notice' => $flushNotice ?? '',
2522 4
        ]);
2523
    }
2524
2525
    /**
2526
     * Get HTML snippet for display user properties
2527
     *
2528
     * @param bool           $dbnameIsWildcard whether database name is wildcard or not
2529
     * @param string         $urlDbname        url database name that urlencode() string
2530
     * @param string         $username         username
2531
     * @param string         $hostname         host name
2532
     * @param string|mixed[] $dbname           database name
2533
     * @param string         $tablename        table name
2534
     * @psalm-param non-empty-string $route
2535
     */
2536 8
    public function getHtmlForUserProperties(
2537
        bool $dbnameIsWildcard,
2538
        string $urlDbname,
2539
        string $username,
2540
        string $hostname,
2541
        string|array $dbname,
2542
        string $tablename,
2543
        string $route,
2544
    ): string {
2545 8
        $userDoesNotExists = ! $this->userExists($username, $hostname);
2546
2547 8
        $loginInformationFields = '';
2548 8
        if ($userDoesNotExists) {
2549
            $loginInformationFields = $this->getHtmlForLoginInformationFields();
2550
        }
2551
2552 8
        $params = ['username' => $username, 'hostname' => $hostname];
2553 8
        $params['dbname'] = $dbname;
2554 8
        if (! is_array($dbname) && $dbname !== '' && $tablename !== '') {
0 ignored issues
show
introduced by
The condition is_array($dbname) is always true.
Loading history...
2555 8
            $params['tablename'] = $tablename;
2556
        }
2557
2558 8
        $privilegesTable = $this->getHtmlToDisplayPrivilegesTable(
2559
            // If $dbname is an array, pass any one db as all have same privs.
2560 8
            is_string($dbname) && $dbname !== ''
2561 8
                ? $dbname
2562 8
                : (is_array($dbname) ? (string) $dbname[0] : '*'),
2563 8
            $tablename !== ''
2564 8
                ? $tablename
2565 8
                : '*',
2566 8
        );
2567
2568 8
        $tableSpecificRights = '';
2569 8
        if (! is_array($dbname) && $tablename === '' && $dbnameIsWildcard === false) {
2570
            // no table name was given, display all table specific rights
2571
            // but only if $dbname contains no wildcards
2572
            if ($dbname === '') {
2573
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights($username, $hostname, 'database');
2574
            } else {
2575
                // unescape wildcards in dbname at table level
2576
                $unescapedDb = $this->unescapeGrantWildcards($dbname);
2577
2578
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
2579
                    $username,
2580
                    $hostname,
2581
                    'table',
2582
                    $unescapedDb,
2583
                );
2584
                $tableSpecificRights .= $this->getHtmlForAllTableSpecificRights(
2585
                    $username,
2586
                    $hostname,
2587
                    'routine',
2588
                    $unescapedDb,
2589
                );
2590
            }
2591
        }
2592
2593 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

2593
        $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...
2594 8
        $databaseUrl = Url::getFromRoute($config->settings['DefaultTabDatabase']);
2595 8
        $databaseUrlTitle = Util::getTitleForTarget($config->settings['DefaultTabDatabase']);
2596 8
        $tableUrl = Url::getFromRoute($config->settings['DefaultTabTable']);
2597 8
        $tableUrlTitle = Util::getTitleForTarget($config->settings['DefaultTabTable']);
2598
2599 8
        $changePassword = '';
2600 8
        $userGroup = '';
2601 8
        $changeLoginInfoFields = '';
2602 8
        if ($dbname === '' && ! $userDoesNotExists) {
2603
            //change login information
2604
            $changePassword = $this->getFormForChangePassword($username, $hostname, true, $route);
2605
            $userGroup = $this->getUserGroupForUser($username);
2606
            $changeLoginInfoFields = $this->getHtmlForLoginInformationFields($username, $hostname);
2607
        }
2608
2609 8
        return $this->template->render('server/privileges/user_properties', [
2610 8
            'user_does_not_exists' => $userDoesNotExists,
2611 8
            'login_information_fields' => $loginInformationFields,
2612 8
            'params' => $params,
2613 8
            'privileges_table' => $privilegesTable,
2614 8
            'table_specific_rights' => $tableSpecificRights,
2615 8
            'change_password' => $changePassword,
2616 8
            'database' => $dbname,
2617 8
            'dbname' => $urlDbname,
2618 8
            'username' => $username,
2619 8
            'hostname' => $hostname,
2620 8
            'is_databases' => $dbnameIsWildcard || is_array($dbname) && count($dbname) > 1,
2621 8
            'is_wildcard' => $dbnameIsWildcard,
2622 8
            'table' => $tablename,
2623 8
            'current_user' => $this->dbi->getCurrentUser(),
2624 8
            'user_group' => $userGroup,
2625 8
            'change_login_info_fields' => $changeLoginInfoFields,
2626 8
            'database_url' => $databaseUrl,
2627 8
            'database_url_title' => $databaseUrlTitle,
2628 8
            'table_url' => $tableUrl,
2629 8
            'table_url_title' => $tableUrlTitle,
2630 8
        ]);
2631
    }
2632
2633
    /**
2634
     * Get queries for Table privileges to change or copy user
2635
     *
2636
     * @param string  $userHostCondition user host condition to select relevant table privileges
2637
     * @param mixed[] $queries           queries array
2638
     * @param string  $username          username
2639
     * @param string  $hostname          host name
2640
     *
2641
     * @return mixed[]
2642
     */
2643
    public function getTablePrivsQueriesForChangeOrCopyUser(
2644
        string $userHostCondition,
2645
        array $queries,
2646
        string $username,
2647
        string $hostname,
2648
    ): array {
2649
        $res = $this->dbi->query(
2650
            'SELECT `Db`, `Table_name`, `Table_priv` FROM `mysql`.`tables_priv`' . $userHostCondition . ';',
2651
        );
2652
        while ($row = $res->fetchAssoc()) {
2653
            $res2 = $this->dbi->query(
2654
                'SELECT `Column_name`, `Column_priv`'
2655
                . ' FROM `mysql`.`columns_priv`'
2656
                . $userHostCondition
2657
                . ' 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\Dbal\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

2657
                . ' AND `Db` = ' . $this->dbi->quoteString(/** @scrutinizer ignore-type */ $row['Db'])
Loading history...
2658
                . ' AND `Table_name` = ' . $this->dbi->quoteString($row['Table_name'])
2659
                . ';',
2660
            );
2661
2662
            $tmpPrivs1 = $this->extractPrivInfo($row);
2663
            $tmpPrivs2 = ['Select' => [], 'Insert' => [], 'Update' => [], 'References' => []];
2664
2665
            while ($row2 = $res2->fetchAssoc()) {
2666
                $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

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