Skip to content

Commit

Permalink
Fix simulating query with -- comment
Browse files Browse the repository at this point in the history
Signed-off-by: Maximilian Krög <maxi_kroeg@web.de>
  • Loading branch information
MoonE committed May 31, 2024
1 parent 6d917b5 commit ed874e8
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 39 deletions.
65 changes: 47 additions & 18 deletions libraries/classes/Controllers/Import/SimulateDmlController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,32 @@
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statements\DeleteStatement;
use PhpMyAdmin\SqlParser\Statements\UpdateStatement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use PhpMyAdmin\SqlParser\Utils\Query;
use PhpMyAdmin\Template;

use function __;
use function array_filter;
use function array_values;
use function count;

final class SimulateDmlController extends AbstractController
{
/** @var SimulateDml */
private $simulateDml;

private $error = '';

Check failure on line 30 in libraries/classes/Controllers/Import/SimulateDmlController.php

View workflow job for this annotation

GitHub Actions / analyse-php (7.2)

Property PhpMyAdmin\Controllers\Import\SimulateDmlController::$error has no type specified.
/**
* @var list<array<mixed>>
* @psalm-var list<array{
* sql_query: string,
* matched_rows: int,
* matched_rows_url: string,
* }>
*/
private $data = [];

public function __construct(
ResponseRenderer $response,
Template $template,
Expand All @@ -34,42 +49,56 @@ public function __construct(

public function __invoke(): void
{
$error = '';
/** @var string $sqlDelimiter */
$sqlDelimiter = $_POST['sql_delimiter'];
$sqlData = [];
$lexer = new Lexer($GLOBALS['sql_query'], false, $sqlDelimiter);
$parser = new Parser($lexer->list);

$parser = $this->createParser($GLOBALS['sql_query'], $sqlDelimiter);
$this->process($parser);

if ($this->error) {
$this->response->addJSON('message', Message::rawError($this->error));
$this->response->addJSON('sql_data', false);

return;
}

$this->response->addJSON('sql_data', $this->data);
}

private function createParser(string $query, string $delimiter): Parser
{
$lexer = new Lexer($query, false, $delimiter);
$list = new TokensList(array_values(array_filter(
$lexer->list->tokens,
static function ($token): bool {
return $token->type !== Token::TYPE_COMMENT;
}
)));

return new Parser($list);
}

private function process(Parser $parser): void
{
foreach ($parser->statements as $statement) {
if (
! $statement instanceof UpdateStatement && ! $statement instanceof DeleteStatement
|| ! empty($statement->join)
|| count(Query::getTables($statement)) > 1
) {
$error = __('Only single-table UPDATE and DELETE queries can be simulated.');
$this->error = __('Only single-table UPDATE and DELETE queries can be simulated.');
break;
}

// Get the matched rows for the query.
$result = $this->simulateDml->getMatchedRows($parser, $statement);
$error = $this->simulateDml->getError();
$this->error = $this->simulateDml->getError();

if ($error !== '') {
if ($this->error !== '') {
break;
}

$sqlData[] = $result;
}

if ($error) {
$message = Message::rawError($error);
$this->response->addJSON('message', $message);
$this->response->addJSON('sql_data', false);

return;
$this->data[] = $result;
}

$this->response->addJSON('sql_data', $sqlData);
}
}
19 changes: 19 additions & 0 deletions test/classes/AbstractTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -309,4 +309,23 @@ protected function callFunction($object, string $className, string $methodName,

return $method->invokeArgs($object, $params);
}

/**
* Get a private or protected property via reflection.
*
* @param object $object The object to inspect, pass null for static objects()
* @param string $className The class name
* @param string $propertyName The method name
* @phpstan-param class-string $className
*
* @return mixed
*/
protected function getProperty(object $object, string $className, string $propertyName)
{
$class = new ReflectionClass($className);
$property = $class->getProperty($propertyName);
$property->setAccessible(true);

return $property->getValue($object);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,24 @@

namespace PhpMyAdmin\Tests\Import;

use PhpMyAdmin\Controllers\Import\SimulateDmlController;
use PhpMyAdmin\Core;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Import\SimulateDml;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statements\DeleteStatement;
use PhpMyAdmin\SqlParser\Statements\UpdateStatement;
use PhpMyAdmin\Template;
use PhpMyAdmin\Tests\AbstractTestCase;
use PhpMyAdmin\Tests\Stubs\ResponseRenderer;
use PhpMyAdmin\Url;

use function count;

/**
* @covers \PhpMyAdmin\Import\SimulateDml
* @covers \PhpMyAdmin\Controllers\Import\SimulateDmlController
*/
class SimulateDmlTest extends AbstractTestCase
class SimulateDmlControllerTest extends AbstractTestCase
{
/**
* @param array<array<mixed>> $expectedPerQuery
Expand All @@ -35,31 +38,47 @@ class SimulateDmlTest extends AbstractTestCase
public function testGetMatchedRows(string $sqlQuery, array $expectedPerQuery): void
{
$GLOBALS['db'] = 'PMA';
$object = new SimulateDml($this->dbi);
$parser = new Parser($sqlQuery);

$this->assertCount(count($expectedPerQuery), $parser->statements);
foreach ($expectedPerQuery as $idx => $expected) {
/** @var DeleteStatement|UpdateStatement $statement */
$statement = $parser->statements[$idx];

foreach ($expectedPerQuery as $expected) {
$this->dummyDbi->addSelectDb('PMA');
$this->dummyDbi->addResult($expected['simulated'], $expected['result'], $expected['columns']);
$simulatedData = $object->getMatchedRows($parser, $statement);
}

$controller = new SimulateDmlController(
new ResponseRenderer(),
new Template(),
new SimulateDml($this->dbi)
);
/** @var Parser $parser */
$parser = $this->callFunction($controller, SimulateDmlController::class, 'createParser', [$sqlQuery, ';']);
$this->assertCount(count($expectedPerQuery), $parser->statements);

$this->callFunction($controller, SimulateDmlController::class, 'process', [$parser]);

$this->assertAllSelectsConsumed();
$this->assertAllQueriesConsumed();

$matchedRowsUrl = Url::getFromRoute('/sql', [
'db' => 'PMA',
'sql_query' => $expected['simulated'],
'sql_signature' => Core::signSqlQuery($expected['simulated']),
]);
/** @var string $error */
$error = $this->getProperty($controller, SimulateDmlController::class, 'error');
$this->assertSame('', $error);

$this->assertAllSelectsConsumed();
$this->assertAllQueriesConsumed();
$this->assertEquals([
/** @var list<array<mixed>> $result */
$result = $this->getProperty($controller, SimulateDmlController::class, 'data');

foreach ($expectedPerQuery as $idx => $expectedData) {
/** @var DeleteStatement|UpdateStatement $statement */
$statement = $parser->statements[$idx];
$expected = [
'sql_query' => Generator::formatSql($statement->build()),
'matched_rows' => count($expected['result']),
'matched_rows_url' => $matchedRowsUrl,
], $simulatedData);
'matched_rows' => count($expectedData['result']),
'matched_rows_url' => Url::getFromRoute('/sql', [
'db' => 'PMA',
'sql_query' => $expectedData['simulated'],
'sql_signature' => Core::signSqlQuery($expectedData['simulated']),
]),
];

$this->assertEquals($expected, $result[$idx]);
}
}

Expand Down Expand Up @@ -246,6 +265,19 @@ public function providerForTestGetMatchedRows(): array
],
],
],
'statement with comment' => [
"UPDATE `t` SET `a` = 20 -- oops\nWHERE 0",
[
[
'simulated' =>
'SELECT *' .
' FROM (SELECT *, 20 AS `a ``new``` FROM `t` WHERE 0) AS `pma_tmp`' .
' WHERE NOT (`a`) <=> (`a ``new```)',
'columns' => ['id', 'a', 'b', 'a `new`'],
'result' => [],
],
],
],
];
}
}

0 comments on commit ed874e8

Please sign in to comment.