diff --git a/ChangeLog b/ChangeLog index f24ef1d08d..c11f004b0e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -78,6 +78,7 @@ phpMyAdmin - ChangeLog - issue #17363 Fix duplicate route parameter after logging in - issue #15670 Fix case where the data is truncated after changing a longtext column's collation - issue #18865 Fix missing text-nowrap for timestamps columns +- issue #19022 Fix case where tables from wrong database is loaded in navigation tree 5.2.1 (2023-02-07) - issue #17522 Fix case where the routes cache file is invalid diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f4daf6ae8c..a55e7cbdf1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6715,6 +6715,16 @@ parameters: count: 1 path: src/DatabaseInterface.php + - + message: "#^Method PhpMyAdmin\\\\DatabaseInterface\\:\\:getCurrentRoles\\(\\) should return array\\\\> but returns array\\\\.$#" + count: 1 + path: src/DatabaseInterface.php + + - + message: "#^Method PhpMyAdmin\\\\DatabaseInterface\\:\\:getCurrentRoles\\(\\) should return array\\\\> but returns mixed\\.$#" + count: 1 + path: src/DatabaseInterface.php + - message: "#^Method PhpMyAdmin\\\\DatabaseInterface\\:\\:getCurrentUser\\(\\) should return string but returns mixed\\.$#" count: 1 @@ -6745,6 +6755,11 @@ parameters: count: 1 path: src/DatabaseInterface.php + - + message: "#^Only booleans are allowed in an if condition, PhpMyAdmin\\\\Dbal\\\\ResultInterface\\|false given\\.$#" + count: 2 + path: src/DatabaseInterface.php + - message: "#^Only booleans are allowed in an if condition, int\\|false given\\.$#" count: 2 @@ -6755,6 +6770,11 @@ parameters: count: 1 path: src/DatabaseInterface.php + - + message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(array\\\\)\\: mixed\\)\\|null, Closure\\(string\\)\\: non\\-empty\\-array\\ given\\.$#" + count: 1 + path: src/DatabaseInterface.php + - message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(mixed\\)\\: mixed\\)\\|null, Closure\\(string\\)\\: non\\-empty\\-string given\\.$#" count: 1 @@ -6805,6 +6825,11 @@ parameters: count: 1 path: src/DatabaseInterface.php + - + message: "#^Variable \\$roleHost on left side of \\?\\? always exists and is not nullable\\.$#" + count: 2 + path: src/DatabaseInterface.php + - message: "#^Method PhpMyAdmin\\\\Dbal\\\\MysqliResult\\:\\:fetchAllAssoc\\(\\) should return array\\\\> but returns array\\.$#" count: 1 @@ -15905,6 +15930,16 @@ parameters: count: 1 path: tests/unit/DatabaseInterfaceTest.php + - + message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertSame\\(\\) with array\\ and array\\\\> will always evaluate to false\\.$#" + count: 1 + path: tests/unit/DatabaseInterfaceTest.php + + - + message: "#^Parameter \\#2 \\$result of method PhpMyAdmin\\\\Tests\\\\Stubs\\\\DbiDummy\\:\\:addResult\\(\\) expects array\\\\>\\|bool, array\\\\>\\|false given\\.$#" + count: 1 + path: tests/unit/DatabaseInterfaceTest.php + - message: "#^Property PhpMyAdmin\\\\Config\\:\\:\\$selectedServer \\(array\\{host\\: string, port\\: string, socket\\: string, ssl\\: bool, ssl_key\\: string\\|null, ssl_cert\\: string\\|null, ssl_ca\\: string\\|null, ssl_ca_path\\: string\\|null, \\.\\.\\.\\}\\) does not accept array\\{host\\: string, port\\: string, socket\\: string, ssl\\: bool, ssl_key\\: string\\|null, ssl_cert\\: string\\|null, ssl_ca\\: string\\|null, ssl_ca_path\\: string\\|null, \\.\\.\\.\\}\\.$#" count: 1 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 6d2cff9517..e81ceec0d6 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -4911,6 +4911,15 @@ + + + + + + + @@ -4919,6 +4928,9 @@ + + + @@ -4953,6 +4965,7 @@ versionString]]> + >]]> @@ -4963,6 +4976,7 @@ + @@ -5029,6 +5043,10 @@ + + + + isUnique()]]> @@ -13357,6 +13375,9 @@ + + + @@ -13369,6 +13390,7 @@ + diff --git a/src/DatabaseInterface.php b/src/DatabaseInterface.php index 7e036f926d..9187a95c18 100644 --- a/src/DatabaseInterface.php +++ b/src/DatabaseInterface.php @@ -51,6 +51,7 @@ use function openlog; use function reset; use function sprintf; use function str_contains; +use function str_replace; use function str_starts_with; use function stripos; use function strnatcasecmp; @@ -110,6 +111,9 @@ class DatabaseInterface implements DbalInterface /** @var array|null */ private array|null $currentUserAndHost = null; + /** @var array>|null Current role and host cache */ + private array|null $currentRoleAndHost = null; + /** * @var int|null lower_case_table_names value cache * @psalm-var 0|1|2|null @@ -1430,6 +1434,38 @@ class DatabaseInterface implements DbalInterface return '@'; } + /** + * gets the current role with host. Role maybe multiple separated by comma + * Support start from MySQL 8.x / MariaDB 10.0.5 + * + * @see https://dev.mysql.com/doc/refman/8.0/en/roles.html + * @see https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_current-role + * @see https://mariadb.com/kb/en/mariadb-1005-release-notes/#newly-implemented-features + * @see https://mariadb.com/kb/en/roles_overview/ + * + * @return array> the current roles i.e. array of role@host + */ + public function getCurrentRoles(): array + { + if (($this->isMariaDB() && $this->getVersion() < 100500) || $this->getVersion() < 80000) { + return []; + } + + if (SessionCache::has('mysql_cur_role')) { + return SessionCache::get('mysql_cur_role'); + } + + $role = $this->fetchValue('SELECT CURRENT_ROLE();'); + if ($role === false || $role === null || $role === 'NONE') { + return []; + } + + $role = array_map('trim', explode(',', str_replace('`', '', $role))); + SessionCache::set('mysql_cur_role', $role); + + return $role; + } + public function isSuperUser(): bool { if (SessionCache::has('is_superuser')) { @@ -1478,6 +1514,21 @@ class DatabaseInterface implements DbalInterface $query = QueryGenerator::getInformationSchemaDataForGranteeRequest($user, $host); $hasGrantPrivilege = (bool) $this->fetchValue($query); + if (! $hasGrantPrivilege) { + foreach ($this->getCurrentRolesAndHost() as [$role, $roleHost]) { + $query = QueryGenerator::getInformationSchemaDataForGranteeRequest($role, $roleHost ?? ''); + $result = $this->tryQuery($query); + + if ($result) { + $hasGrantPrivilege = (bool) $result->numRows(); + } + + if ($hasGrantPrivilege) { + break; + } + } + } + SessionCache::set('is_grantuser', $hasGrantPrivilege); return $hasGrantPrivilege; @@ -1514,6 +1565,21 @@ class DatabaseInterface implements DbalInterface $query = QueryGenerator::getInformationSchemaDataForCreateRequest($user, $host); $hasCreatePrivilege = (bool) $this->fetchValue($query); + if (! $hasCreatePrivilege) { + foreach ($this->getCurrentRolesAndHost() as [$role, $roleHost]) { + $query = QueryGenerator::getInformationSchemaDataForCreateRequest($role, $roleHost ?? ''); + $result = $this->tryQuery($query); + + if ($result) { + $hasCreatePrivilege = (bool) $result->numRows(); + } + + if ($hasCreatePrivilege) { + break; + } + } + } + SessionCache::set('is_createuser', $hasCreatePrivilege); return $hasCreatePrivilege; @@ -1548,6 +1614,24 @@ class DatabaseInterface implements DbalInterface return $this->currentUserAndHost; } + /** + * Get the current role and host. + * + * @return array> array of role and hostname + */ + public function getCurrentRolesAndHost(): array + { + if ($this->currentRoleAndHost === null) { + $roles = $this->getCurrentRoles(); + + $this->currentRoleAndHost = array_map(static function (string $role) { + return explode('@', $role); + }, $roles); + } + + return $this->currentRoleAndHost; + } + /** * Returns value for lower_case_table_names variable * diff --git a/src/Query/Generator.php b/src/Query/Generator.php index 6da7f6cb52..c899892f25 100644 --- a/src/Query/Generator.php +++ b/src/Query/Generator.php @@ -240,13 +240,20 @@ class Generator public static function getInformationSchemaDataForCreateRequest(string $user, string $host): string { + // second part of query is for MariaDB that not show roles inside INFORMATION_SCHEMA db return 'SELECT 1 FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES` ' . "WHERE `PRIVILEGE_TYPE` = 'CREATE USER' AND " - . "'''" . $user . "''@''" . $host . "''' LIKE `GRANTEE` LIMIT 1"; + . "'''" . $user . "''@''" . $host . "''' LIKE `GRANTEE`" + . ' UNION ' + . 'SELECT 1 FROM mysql.user ' + . "WHERE `create_user_priv` = 'Y' COLLATE utf8mb4_general_ci AND " + . "'" . $user . "' LIKE `User` AND '' LIKE `Host`" + . ' LIMIT 1'; } public static function getInformationSchemaDataForGranteeRequest(string $user, string $host): string { + // second part of query is for MariaDB that not show roles inside INFORMATION_SCHEMA db return 'SELECT 1 FROM (' . 'SELECT `GRANTEE`, `IS_GRANTABLE` FROM ' . '`INFORMATION_SCHEMA`.`COLUMN_PRIVILEGES` UNION ' @@ -257,7 +264,12 @@ class Generator . 'SELECT `GRANTEE`, `IS_GRANTABLE` FROM ' . '`INFORMATION_SCHEMA`.`USER_PRIVILEGES`) t ' . "WHERE `IS_GRANTABLE` = 'YES' AND " - . "'''" . $user . "''@''" . $host . "''' LIKE `GRANTEE` LIMIT 1"; + . "'''" . $user . "''@''" . $host . "''' LIKE `GRANTEE` " + . ' UNION ' + . 'SELECT 1 FROM mysql.user ' + . "WHERE `create_user_priv` = 'Y' COLLATE utf8mb4_general_ci AND " + . "'" . $user . "' LIKE `User` AND '' LIKE `Host`" + . ' LIMIT 1'; } public static function getInformationSchemaForeignKeyConstraintsRequest( diff --git a/src/Util.php b/src/Util.php index dde66130c7..f3a22dfcd3 100644 --- a/src/Util.php +++ b/src/Util.php @@ -839,6 +839,7 @@ class Util SessionCache::remove('is_createuser'); SessionCache::remove('is_grantuser'); SessionCache::remove('mysql_cur_user'); + SessionCache::remove('mysql_cur_role'); } /** diff --git a/tests/unit/DatabaseInterfaceTest.php b/tests/unit/DatabaseInterfaceTest.php index 94c8798880..2044bbcbbc 100644 --- a/tests/unit/DatabaseInterfaceTest.php +++ b/tests/unit/DatabaseInterfaceTest.php @@ -101,6 +101,75 @@ class DatabaseInterfaceTest extends AbstractTestCase ]; } + /** + * Tests for DBI::getCurrentRole() method. + * + * @param string[][]|false $value + * @param string[] $string + * @param string[][] $expected + */ + #[DataProvider('currentRolesData')] + public function testGetCurrentRoles( + string $version, + bool $isRoleSupported, + array|false $value, + array $string, + array $expected, + ): void { + $dbiDummy = $this->createDbiDummy(); + $dbi = $this->createDatabaseInterface($dbiDummy); + $dbi->setVersion(['@@version' => $version]); + + SessionCache::remove('mysql_cur_role'); + + if ($isRoleSupported) { + $dbiDummy->addResult('SELECT CURRENT_ROLE();', $value); + } + + self::assertSame($expected, $dbi->getCurrentRolesAndHost()); + + self::assertSame($string, $dbi->getCurrentRoles()); + + $dbiDummy->assertAllQueriesConsumed(); + } + + /** + * Data provider for getCurrentRole() tests. + * + * @return mixed[] + */ + public static function currentRolesData(): array + { + return [ + ['10.4.99-MariaDB', false, false, [], []], + ['5.7.35 - MySQL Community Server (GPL)', false, false, [], []], + [ + '8.0.0 - MySQL Community Server - GPL', + true, + [['`role`@`localhost`']], + ['role@localhost'], + [['role', 'localhost']], + ], + [ + '8.0.0 - MySQL Community Server - GPL', + true, + [['`role`@`localhost`, `role2`@`localhost`']], + ['role@localhost', 'role2@localhost'], + [['role', 'localhost'], ['role2', 'localhost']], + ], + ['8.0.0 - MySQL Community Server - GPL', true, [['@`localhost`']], ['@localhost'], [['', 'localhost']]], + ['10.5.0-MariaDB', true, [['`role`@`localhost`']], ['role@localhost'], [['role', 'localhost']]], + [ + '10.5.0-MariaDB', + true, + [['`role`@`localhost`, `role2`@`localhost`']], + ['role@localhost', 'role2@localhost'], + [['role', 'localhost'], ['role2', 'localhost']], + ], + ['10.5.0-MariaDB', true, [['@`localhost`']], ['@localhost'], [['', 'localhost']]], + ]; + } + /** * Tests for DBI::getSystemDatabase() method. */ diff --git a/tests/unit/Navigation/Nodes/NodeTest.php b/tests/unit/Navigation/Nodes/NodeTest.php index ed52f3ef41..511b5ccf26 100644 --- a/tests/unit/Navigation/Nodes/NodeTest.php +++ b/tests/unit/Navigation/Nodes/NodeTest.php @@ -96,6 +96,20 @@ final class NodeTest extends AbstractTestCase self::assertSame($child, $parent->getChild('child real name', true)); } + public function testGetChildNodeWithEqualNumericName(): void + { + $config = new Config(); + $parent = new Node($config, 'parent'); + $childOne = new Node($config, '0'); + $childTwo = new Node($config, '00'); + $parent->addChild($childOne); + $parent->addChild($childTwo); + self::assertSame($childTwo, $parent->getChild('00')); + self::assertSame($childOne, $parent->getChild('0')); + self::assertSame($childTwo, $parent->getChild('00', true)); + self::assertSame($childOne, $parent->getChild('0', true)); + } + public function testRemoveChildNode(): void { $config = new Config(); diff --git a/tests/unit/Stubs/DbiDummy.php b/tests/unit/Stubs/DbiDummy.php index 78f4c69735..81df7116d5 100644 --- a/tests/unit/Stubs/DbiDummy.php +++ b/tests/unit/Stubs/DbiDummy.php @@ -369,7 +369,9 @@ class DbiDummy implements DbiExtension [ 'query' => 'SELECT 1 FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES`' . " WHERE `PRIVILEGE_TYPE` = 'CREATE USER'" - . " AND '''pma_test''@''localhost''' LIKE `GRANTEE` LIMIT 1", + . " AND '''pma_test''@''localhost''' LIKE `GRANTEE`" + . " UNION SELECT 1 FROM mysql.user WHERE `create_user_priv` = 'Y' COLLATE utf8mb4_general_ci" + . " AND 'pma_test' LIKE `User` AND '' LIKE `Host` LIMIT 1", 'result' => [['1']], ], [ @@ -382,7 +384,9 @@ class DbiDummy implements DbiExtension . ' UNION SELECT `GRANTEE`, `IS_GRANTABLE`' . ' FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES`) t' . " WHERE `IS_GRANTABLE` = 'YES'" - . " AND '''pma_test''@''localhost''' LIKE `GRANTEE` LIMIT 1", + . " AND '''pma_test''@''localhost''' LIKE `GRANTEE`" + . " UNION SELECT 1 FROM mysql.user WHERE `create_user_priv` = 'Y' COLLATE utf8mb4_general_ci" + . " AND 'pma_test' LIKE `User` AND '' LIKE `Host` LIMIT 1", 'result' => [['1']], ], [