Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 149 additions & 2 deletions tests/WP_SQLite_Translator_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -1170,14 +1170,14 @@ public function testColumnWithOnUpdate() {
'name' => '___tmp_table_created_at_on_update__',
'tbl_name' => '_tmp_table',
'rootpage' => '0',
'sql' => "CREATE TRIGGER \"___tmp_table_created_at_on_update__\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"created_at\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND",
'sql' => "CREATE TRIGGER `___tmp_table_created_at_on_update__`\n\t\t\tAFTER UPDATE ON `_tmp_table`\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE `_tmp_table` SET `created_at` = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND",
),
(object) array(
'type' => 'trigger',
'name' => '___tmp_table_updated_at_on_update__',
'tbl_name' => '_tmp_table',
'rootpage' => '0',
'sql' => "CREATE TRIGGER \"___tmp_table_updated_at_on_update__\"\n\t\t\tAFTER UPDATE ON \"_tmp_table\"\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE \"_tmp_table\" SET \"updated_at\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND",
'sql' => "CREATE TRIGGER `___tmp_table_updated_at_on_update__`\n\t\t\tAFTER UPDATE ON `_tmp_table`\n\t\t\tFOR EACH ROW\n\t\t\tBEGIN\n\t\t\t UPDATE `_tmp_table` SET `updated_at` = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;\n\t\t\tEND",
),
),
$results
Expand Down Expand Up @@ -3557,4 +3557,151 @@ public function testCreateTableWithDefaultNowFunction() {
$result = $this->assertQuery( 'SELECT * FROM test_now_default WHERE id = 2' );
$this->assertRegExp( '/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/', $result[0]->updated );
}

public function testQuoteIdentifierEscapesBackticks() {
// Create a table with a backtick in the column name using double-quote
// quoting (MySQL syntax). The translator must properly escape the
// backtick when generating SQLite DDL with backtick-quoted identifiers.
$this->assertQuery(
'CREATE TABLE _tmp_backtick_test (
ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
"col`name" varchar(50) NOT NULL
);'
);

$this->assertQuery( "INSERT INTO _tmp_backtick_test (ID, \"col`name\") VALUES (1, 'value1')" );

$result = $this->assertQuery( 'SELECT * FROM _tmp_backtick_test WHERE ID = 1' );
$this->assertCount( 1, $result );
$this->assertEquals( 'value1', $result[0]->{'col`name'} );

// Verify the column appears in DESCRIBE output.
$description = $this->assertQuery( 'DESCRIBE _tmp_backtick_test' );
$column_names = array_map(
function ( $row ) {
return $row->Field;
},
$description
);
$this->assertContains( 'col`name', $column_names );

// Verify SHOW CREATE TABLE produces valid, parseable output.
$create = $this->assertQuery( 'SHOW CREATE TABLE _tmp_backtick_test' );
$create_sql = $create[0]->{'Create Table'};
$this->assertStringContainsString( '`col``name`', $create_sql );

// Verify autoincrement detection works with backtick-quoted identifiers.
$this->assertStringContainsString( 'AUTO_INCREMENT', $create_sql );
}

public function testDoubleQuotedStringsAreParameterized() {
$this->assertQuery( 'INSERT INTO _options (option_name, option_value) VALUES ("dq_name", "dq_value")' );

// The double-quoted strings should be bound as parameters, not inlined.
$insert_query = null;
foreach ( $this->engine->executed_sqlite_queries as $q ) {
if ( stripos( $q['sql'], 'INSERT' ) !== false && stripos( $q['sql'], '_options' ) !== false ) {
$insert_query = $q;
break;
}
}
$this->assertNotNull( $insert_query );
$this->assertNotEmpty( $insert_query['params'], 'Double-quoted strings should be bound as parameters' );
$this->assertStringNotContainsString( 'dq_name', $insert_query['sql'], 'Value should not appear in SQL' );
$this->assertStringNotContainsString( 'dq_value', $insert_query['sql'], 'Value should not appear in SQL' );
$this->assertContains( 'dq_name', $insert_query['params'] );
$this->assertContains( 'dq_value', $insert_query['params'] );

// Verify the data was inserted correctly.
$result = $this->assertQuery( 'SELECT * FROM _options WHERE option_name = "dq_name"' );
$this->assertCount( 1, $result );
$this->assertEquals( 'dq_value', $result[0]->option_value );
}

public function testDoubleQuotedStringWithBackslashEscapeDoesNotCauseInjection() {
// In MySQL, \" inside double-quoted strings is an escaped double quote.
// The MySQL lexer produces a single token: "admin\" OR 1=1--"
// with value: admin" OR 1=1--
//
// Without parameterization, passing the raw token to SQLite would be:
// "admin\" OR 1=1--" (SQLite sees "admin\" as identifier + SQL)
//
// With parameterization, the value is safely bound as a parameter.
$this->assertQuery(
'INSERT INTO _options (option_name, option_value) VALUES ("safe_key", "admin\" OR 1=1--")'
);

// Verify the injection payload is not present in the SQL sent to SQLite.
$insert_query = null;
foreach ( $this->engine->executed_sqlite_queries as $q ) {
if ( stripos( $q['sql'], 'INSERT' ) !== false && stripos( $q['sql'], '_options' ) !== false ) {
$insert_query = $q;
break;
}
}
$this->assertNotNull( $insert_query );
$this->assertStringNotContainsString( 'OR 1=1', $insert_query['sql'], 'Injection payload should not appear in SQL' );
$this->assertNotEmpty( $insert_query['params'], 'Values should be bound as parameters' );

$result = $this->assertQuery( 'SELECT * FROM _options WHERE option_name = "safe_key"' );
$this->assertCount( 1, $result );
$this->assertEquals( 'admin" OR 1=1--', $result[0]->option_value );
}

public function testDateFormatWithSingleQuotesInFormat() {
$this->assertQuery(
'CREATE TABLE _tmp_dates (
ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
created_at DATETIME NOT NULL
);'
);
$this->assertQuery( "INSERT INTO _tmp_dates (created_at) VALUES ('2024-01-15 10:30:00')" );

// DATE_FORMAT with a format that produces a value — verify it works.
$result = $this->assertQuery(
"SELECT DATE_FORMAT(created_at, '%Y-%m-%d') as formatted FROM _tmp_dates"
);
$this->assertCount( 1, $result );
$this->assertEquals( '2024-01-15', $result[0]->formatted );
}

public function testIntervalExpression() {
$this->assertQuery(
'CREATE TABLE _tmp_dates (
ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
created_at DATETIME NOT NULL
);'
);
$this->assertQuery( 'INSERT INTO _tmp_dates (created_at) VALUES (\'2024-01-15 10:30:00\')' );

$result = $this->assertQuery(
'SELECT DATE_ADD(created_at, INTERVAL 1 DAY) as future_date FROM _tmp_dates'
);
$this->assertCount( 1, $result );
$this->assertEquals( '2024-01-16 10:30:00', $result[0]->future_date );

$result = $this->assertQuery(
'SELECT DATE_SUB(created_at, INTERVAL 1 DAY) as past_date FROM _tmp_dates'
);
$this->assertCount( 1, $result );
$this->assertEquals( '2024-01-14 10:30:00', $result[0]->past_date );
}

public function testLikeBinaryWithSingleQuoteInPattern() {
$this->assertQuery(
"CREATE TABLE _tmp_table (
ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name varchar(50) NOT NULL default ''
);"
);

$this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('it''s a test')" );
$this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('no quote here')" );

$result = $this->assertQuery(
"SELECT * FROM _tmp_table WHERE name LIKE BINARY 'it''s%'"
);
$this->assertCount( 1, $result );
$this->assertEquals( "it's a test", $result[0]->name );
}
}
Loading
Loading