diff --git a/libraries/classes/DatabaseInterface.php b/libraries/classes/DatabaseInterface.php index 8bc1695003..3dceec9c9b 100644 --- a/libraries/classes/DatabaseInterface.php +++ b/libraries/classes/DatabaseInterface.php @@ -12,6 +12,7 @@ use PhpMyAdmin\Dbal\DbalInterface; use PhpMyAdmin\Dbal\DbiExtension; use PhpMyAdmin\Dbal\DbiMysqli; use PhpMyAdmin\Dbal\ResultInterface; +use PhpMyAdmin\Dbal\Statement; use PhpMyAdmin\Dbal\Warning; use PhpMyAdmin\Html\Generator; use PhpMyAdmin\Query\Cache; @@ -2092,10 +2093,8 @@ class DatabaseInterface implements DbalInterface * * @param string $query The query, as a string. * @psalm-param ConnectionType $connectionType - * - * @return object|false A statement object or false. */ - public function prepare(string $query, int $connectionType = Connection::TYPE_USER) + public function prepare(string $query, int $connectionType = Connection::TYPE_USER): ?Statement { return $this->extension->prepare($this->connections[$connectionType], $query); } diff --git a/libraries/classes/Dbal/DbalInterface.php b/libraries/classes/Dbal/DbalInterface.php index 29b6dcd0f8..04808a6509 100644 --- a/libraries/classes/Dbal/DbalInterface.php +++ b/libraries/classes/Dbal/DbalInterface.php @@ -679,8 +679,6 @@ interface DbalInterface * * @param string $query The query, as a string. * @psalm-param ConnectionType $connectionType - * - * @return object|false A statement object or false. */ - public function prepare(string $query, int $connectionType = Connection::TYPE_USER); + public function prepare(string $query, int $connectionType = Connection::TYPE_USER): ?Statement; } diff --git a/libraries/classes/Dbal/DbiExtension.php b/libraries/classes/Dbal/DbiExtension.php index eee0373ca3..76028e8e97 100644 --- a/libraries/classes/Dbal/DbiExtension.php +++ b/libraries/classes/Dbal/DbiExtension.php @@ -109,10 +109,8 @@ interface DbiExtension * Prepare an SQL statement for execution. * * @param string $query The query, as a string. - * - * @return object|false A statement object or false. */ - public function prepare(Connection $connection, string $query); + public function prepare(Connection $connection, string $query): ?Statement; /** * Returns the number of warnings from the last query. diff --git a/libraries/classes/Dbal/DbiMysqli.php b/libraries/classes/Dbal/DbiMysqli.php index 6386fd825e..ce9eabc6f1 100644 --- a/libraries/classes/Dbal/DbiMysqli.php +++ b/libraries/classes/Dbal/DbiMysqli.php @@ -9,7 +9,6 @@ namespace PhpMyAdmin\Dbal; use mysqli; use mysqli_sql_exception; -use mysqli_stmt; use PhpMyAdmin\Config\Settings\Server; use PhpMyAdmin\DatabaseInterface; use PhpMyAdmin\Query\Utilities; @@ -346,15 +345,17 @@ class DbiMysqli implements DbiExtension * Prepare an SQL statement for execution. * * @param string $query The query, as a string. - * - * @return mysqli_stmt|false A statement object or false. */ - public function prepare(Connection $connection, string $query) + public function prepare(Connection $connection, string $query): ?Statement { /** @var mysqli $mysqli */ $mysqli = $connection->connection; + $statement = $mysqli->prepare($query); + if ($statement === false) { + return null; + } - return $mysqli->prepare($query); + return new MysqliStatement($statement); } /** diff --git a/libraries/classes/Dbal/MysqliStatement.php b/libraries/classes/Dbal/MysqliStatement.php new file mode 100644 index 0000000000..fc4ae59550 --- /dev/null +++ b/libraries/classes/Dbal/MysqliStatement.php @@ -0,0 +1,59 @@ +statement = $statement; + } + + /** + * Executes a prepared statement. + * + * @param list $params + */ + public function execute(array $params): bool + { + $paramCount = $this->statement->param_count; + if (count($params) !== $paramCount) { + return false; + } + + if (PHP_VERSION_ID >= 80100) { + /** + * @psalm-suppress TooManyArguments + * @phpstan-ignore-next-line + */ + return $this->statement->execute($params); + } + + $types = str_repeat('s', $paramCount); + if (! $this->statement->bind_param($types, ...$params)) { + return false; + } + + return $this->statement->execute(); + } + + /** + * Gets a result set from a prepared statement. + */ + public function getResult(): ResultInterface + { + return new MysqliResult($this->statement->get_result()); + } +} diff --git a/libraries/classes/Dbal/Statement.php b/libraries/classes/Dbal/Statement.php new file mode 100644 index 0000000000..721206bba5 --- /dev/null +++ b/libraries/classes/Dbal/Statement.php @@ -0,0 +1,20 @@ + $params + */ + public function execute(array $params): bool; + + /** + * Gets a result set from a prepared statement. + */ + public function getResult(): ResultInterface; +} diff --git a/libraries/classes/Server/Privileges.php b/libraries/classes/Server/Privileges.php index c82f4233f7..a237f6fea4 100644 --- a/libraries/classes/Server/Privileges.php +++ b/libraries/classes/Server/Privileges.php @@ -7,7 +7,6 @@ declare(strict_types=1); namespace PhpMyAdmin\Server; -use mysqli_stmt; use PhpMyAdmin\ConfigStorage\Features\ConfigurableMenusFeature; use PhpMyAdmin\ConfigStorage\Relation; use PhpMyAdmin\ConfigStorage\RelationCleanup; @@ -15,7 +14,6 @@ use PhpMyAdmin\Database\Routines; use PhpMyAdmin\DatabaseInterface; use PhpMyAdmin\Dbal\Connection; use PhpMyAdmin\Dbal\DatabaseName; -use PhpMyAdmin\Dbal\MysqliResult; use PhpMyAdmin\Dbal\ResultInterface; use PhpMyAdmin\Dbal\TableName; use PhpMyAdmin\Html\Generator; @@ -1425,13 +1423,12 @@ class Privileges NOT (`Table_priv` = \'\' AND Column_priv = \'\') ORDER BY `User` ASC, `Host` ASC, `Db` ASC, `Table_priv` ASC; '; - /** @var mysqli_stmt|false $statement */ $statement = $this->dbi->prepare($query); - if ($statement === false || ! $statement->bind_param('ss', $db, $table) || ! $statement->execute()) { + if ($statement === null || ! $statement->execute([$db->getName(), $table->getName()])) { return []; } - $result = new MysqliResult($statement->get_result()); + $result = $statement->getResult(); return $result->fetchAllAssoc(); } @@ -3769,13 +3766,12 @@ class Privileges private function getUserPrivileges(string $user, string $host, bool $hasAccountLocking): ?array { $query = 'SELECT * FROM `mysql`.`user` WHERE `User` = ? AND `Host` = ?;'; - /** @var mysqli_stmt|false $statement */ $statement = $this->dbi->prepare($query); - if ($statement === false || ! $statement->bind_param('ss', $user, $host) || ! $statement->execute()) { + if ($statement === null || ! $statement->execute([$user, $host])) { return null; } - $result = new MysqliResult($statement->get_result()); + $result = $statement->getResult(); /** @var array|null $userPrivileges */ $userPrivileges = $result->fetchAssoc(); if ($userPrivileges === []) { @@ -3789,13 +3785,12 @@ class Privileges $userPrivileges['account_locked'] = 'N'; $query = 'SELECT * FROM `mysql`.`global_priv` WHERE `User` = ? AND `Host` = ?;'; - /** @var mysqli_stmt|false $statement */ $statement = $this->dbi->prepare($query); - if ($statement === false || ! $statement->bind_param('ss', $user, $host) || ! $statement->execute()) { + if ($statement === null || ! $statement->execute([$user, $host])) { return $userPrivileges; } - $result = new MysqliResult($statement->get_result()); + $result = $statement->getResult(); /** @var array|null $globalPrivileges */ $globalPrivileges = $result->fetchAssoc(); if ($globalPrivileges === []) { diff --git a/test/classes/DatabaseInterfaceTest.php b/test/classes/DatabaseInterfaceTest.php index 3df0785b37..2af5fc3b62 100644 --- a/test/classes/DatabaseInterfaceTest.php +++ b/test/classes/DatabaseInterfaceTest.php @@ -4,12 +4,12 @@ declare(strict_types=1); namespace PhpMyAdmin\Tests; -use mysqli_stmt; use PhpMyAdmin\ConfigStorage\Relation; use PhpMyAdmin\DatabaseInterface; use PhpMyAdmin\Dbal\Connection; use PhpMyAdmin\Dbal\DbiExtension; use PhpMyAdmin\Dbal\ResultInterface; +use PhpMyAdmin\Dbal\Statement; use PhpMyAdmin\LanguageManager; use PhpMyAdmin\Query\Utilities; use PhpMyAdmin\SqlParser\Context; @@ -826,7 +826,7 @@ class DatabaseInterfaceTest extends AbstractTestCase public function testPrepare(): void { $query = 'SELECT * FROM `mysql`.`user` WHERE `User` = ? AND `Host` = ?;'; - $stmtStub = $this->createStub(mysqli_stmt::class); + $stmtStub = $this->createStub(Statement::class); $dummyDbi = $this->createMock(DbiExtension::class); $dummyDbi->expects($this->once())->method('prepare') ->with($this->isType('object'), $this->equalTo($query)) diff --git a/test/classes/Dbal/MysqliStatementTest.php b/test/classes/Dbal/MysqliStatementTest.php new file mode 100644 index 0000000000..0b1bf0a2fa --- /dev/null +++ b/test/classes/Dbal/MysqliStatementTest.php @@ -0,0 +1,26 @@ +createMock(mysqli_stmt::class); + $mysqliStmt->expects($this->once())->method('get_result')->willReturn(false); + $statement = new MysqliStatement($mysqliStmt); + $result = $statement->getResult(); + $this->assertInstanceOf(MysqliResult::class, $result); + } +} diff --git a/test/classes/Server/PrivilegesTest.php b/test/classes/Server/PrivilegesTest.php index b1a059cad2..ecba0dd867 100644 --- a/test/classes/Server/PrivilegesTest.php +++ b/test/classes/Server/PrivilegesTest.php @@ -4,12 +4,12 @@ declare(strict_types=1); namespace PhpMyAdmin\Tests\Server; -use mysqli_result; -use mysqli_stmt; use PhpMyAdmin\ConfigStorage\Relation; use PhpMyAdmin\ConfigStorage\RelationCleanup; use PhpMyAdmin\ConfigStorage\RelationParameters; use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Dbal\ResultInterface; +use PhpMyAdmin\Dbal\Statement; use PhpMyAdmin\Html\Generator; use PhpMyAdmin\Message; use PhpMyAdmin\Server\Plugins; @@ -1906,13 +1906,10 @@ class PrivilegesTest extends AbstractTestCase public function testGetUserPrivileges(): void { - $mysqliResultStub = $this->createMock(mysqli_result::class); - $mysqliStmtStub = $this->createMock(mysqli_stmt::class); - $mysqliStmtStub->expects($this->exactly(2))->method('bind_param')->willReturn(true); + $mysqliResultStub = $this->createMock(ResultInterface::class); + $mysqliStmtStub = $this->createMock(Statement::class); $mysqliStmtStub->expects($this->exactly(2))->method('execute')->willReturn(true); - $mysqliStmtStub->expects($this->exactly(2)) - ->method('get_result') - ->willReturn($mysqliResultStub); + $mysqliStmtStub->expects($this->exactly(2))->method('getResult')->willReturn($mysqliResultStub); $dbi = $this->createMock(DatabaseInterface::class); $dbi->expects($this->once())->method('isMariaDB')->willReturn(true); @@ -1924,7 +1921,7 @@ class PrivilegesTest extends AbstractTestCase ) ->willReturn($mysqliStmtStub); $mysqliResultStub->expects($this->exactly(2)) - ->method('fetch_assoc') + ->method('fetchAssoc') ->willReturnOnConsecutiveCalls( ['Host' => 'test.host', 'User' => 'test.user'], ['Host' => 'test.host', 'User' => 'test.user', 'Priv' => '{"account_locked":true}'] diff --git a/test/classes/Stubs/DbiDummy.php b/test/classes/Stubs/DbiDummy.php index c018734609..73b62cbe2e 100644 --- a/test/classes/Stubs/DbiDummy.php +++ b/test/classes/Stubs/DbiDummy.php @@ -16,6 +16,7 @@ use PhpMyAdmin\Dbal\Connection; use PhpMyAdmin\Dbal\DatabaseName; use PhpMyAdmin\Dbal\DbiExtension; use PhpMyAdmin\Dbal\ResultInterface; +use PhpMyAdmin\Dbal\Statement; use PhpMyAdmin\FieldMetadata; use PHPUnit\Framework\Assert; use stdClass; @@ -495,14 +496,9 @@ class DbiDummy implements DbiExtension $this->dummyQueries = []; } - /** - * @param string $query query - * - * @return object|false - */ - public function prepare(Connection $connection, string $query) + public function prepare(Connection $connection, string $query): ?Statement { - return false; + return null; } /**