Skip to content

Commit 86ea87d

Browse files
committed
Escape all identifier interpolations in the legacy SQLite translator
Apply quote_identifier() (backtick quoting with internal escaping) to all table names, column names, index names, and trigger names that are interpolated into SQL queries, token values, or DDL reconstruction output. Use PDO::quote() for string arguments to pragma functions. Fix two downstream issues caused by the quoting style change: - get_autoincrement_column() regex now accepts both backtick and double-quote delimiters in stored schemas. - CHANGE COLUMN schema parsing now matches both TYPE_SYMBOL (backtick) and TYPE_STRING (double-quote) tokens.
1 parent 535b42a commit 86ea87d

File tree

2 files changed

+71
-47
lines changed

2 files changed

+71
-47
lines changed

tests/WP_SQLite_Translator_Tests.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,14 +1170,14 @@ public function testColumnWithOnUpdate() {
11701170
'name' => '___tmp_table_created_at_on_update__',
11711171
'tbl_name' => '_tmp_table',
11721172
'rootpage' => '0',
1173-
'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",
1173+
'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",
11741174
),
11751175
(object) array(
11761176
'type' => 'trigger',
11771177
'name' => '___tmp_table_updated_at_on_update__',
11781178
'tbl_name' => '_tmp_table',
11791179
'rootpage' => '0',
1180-
'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",
1180+
'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",
11811181
),
11821182
),
11831183
$results

wp-includes/sqlite/class-wp-sqlite-translator.php

Lines changed: 69 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -916,12 +916,12 @@ private function execute_create_table() {
916916
}
917917

918918
if ( count( $table->primary_key ) > 1 ) {
919-
$definitions[] = 'PRIMARY KEY ("' . implode( '", "', $table->primary_key ) . '")';
919+
$definitions[] = 'PRIMARY KEY (' . implode( ', ', array_map( array( $this, 'quote_identifier' ), $table->primary_key ) ) . ')';
920920
}
921921

922922
$create_query = (
923923
$table->create_table .
924-
'"' . $table->name . '" (' . "\n" .
924+
$this->quote_identifier( $table->name ) . ' (' . "\n" .
925925
implode( ",\n", $definitions ) .
926926
')'
927927
);
@@ -940,7 +940,7 @@ private function execute_create_table() {
940940
}
941941
$index_name = $this->generate_index_name( $table->name, $constraint->name );
942942
$this->execute_sqlite_query(
943-
"CREATE $unique INDEX $if_not_exists \"$index_name\" ON \"{$table->name}\" (\"" . implode( '", "', $constraint->columns ) . '")'
943+
'CREATE ' . $unique . 'INDEX ' . $if_not_exists . ' ' . $this->quote_identifier( $index_name ) . ' ON ' . $this->quote_identifier( $table->name ) . ' (' . implode( ', ', array_map( array( $this, 'quote_identifier' ), $constraint->columns ) ) . ')'
944944
);
945945
$this->update_data_type_cache(
946946
$table->name,
@@ -1208,7 +1208,7 @@ private function parse_mysql_create_table_field() {
12081208
* @return string
12091209
*/
12101210
private function make_sqlite_field_definition( $field ) {
1211-
$definition = '"' . $field->name . '" ' . $field->sqlite_data_type;
1211+
$definition = $this->quote_identifier( $field->name ) . ' ' . $field->sqlite_data_type;
12121212
if ( $field->auto_increment ) {
12131213
$definition .= ' PRIMARY KEY AUTOINCREMENT';
12141214
} elseif ( $field->primary_key ) {
@@ -1382,8 +1382,9 @@ private function execute_delete() {
13821382
// @TODO: Actually rewrite the query instead of using a hardcoded workaround.
13831383
if ( str_contains( $updated_query, ' JOIN ' ) ) {
13841384
$table_prefix = isset( $GLOBALS['table_prefix'] ) ? $GLOBALS['table_prefix'] : 'wp_';
1385+
$quoted_table = $this->quote_identifier( $table_prefix . 'options' );
13851386
$this->execute_sqlite_query(
1386-
"DELETE FROM {$table_prefix}options WHERE option_id IN (SELECT MIN(option_id) FROM {$table_prefix}options GROUP BY option_name HAVING COUNT(*) > 1)"
1387+
"DELETE FROM $quoted_table WHERE option_id IN (SELECT MIN(option_id) FROM $quoted_table GROUP BY option_name HAVING COUNT(*) > 1)"
13871388
);
13881389
$this->set_result_from_affected_rows();
13891390
return;
@@ -1417,7 +1418,7 @@ private function execute_delete() {
14171418
// SELECT to fetch the IDs of the rows to delete, then delete them
14181419
// using a separate DELETE query.
14191420

1420-
$this->table_name = $rewriter->skip()->value;
1421+
$this->table_name = $this->normalize_column_name( $rewriter->skip()->value );
14211422
$rewriter->add( new WP_SQLite_Token( 'SELECT', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) );
14221423

14231424
/*
@@ -1433,7 +1434,7 @@ private function execute_delete() {
14331434
for ( $i = $index + 1; $i < $rewriter->max; $i++ ) {
14341435
// Assume the table name is the first token after FROM.
14351436
if ( ! $rewriter->input_tokens[ $i ]->is_semantically_void() ) {
1436-
$this->table_name = $rewriter->input_tokens[ $i ]->value;
1437+
$this->table_name = $this->normalize_column_name( $rewriter->input_tokens[ $i ]->value );
14371438
break;
14381439
}
14391440
}
@@ -1445,7 +1446,7 @@ private function execute_delete() {
14451446
* Now, let's figure out the primary key name.
14461447
* This assumes that all listed table names are the same.
14471448
*/
1448-
$q = $this->execute_sqlite_query( 'SELECT l.name FROM pragma_table_info("' . $this->table_name . '") as l WHERE l.pk = 1;' );
1449+
$q = $this->execute_sqlite_query( 'SELECT l.name FROM pragma_table_info(' . $this->pdo->quote( $this->table_name ) . ') as l WHERE l.pk = 1;' );
14491450
$pk_name = $q->fetch()['name'];
14501451

14511452
/*
@@ -1468,7 +1469,7 @@ private function execute_delete() {
14681469
$rewriter->add_many(
14691470
array(
14701471
new WP_SQLite_Token( '.', WP_SQLite_Token::TYPE_OPERATOR, WP_SQLite_Token::FLAG_OPERATOR_SQL ),
1471-
new WP_SQLite_Token( $pk_name, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ),
1472+
new WP_SQLite_Token( $this->quote_identifier( $pk_name ), WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ),
14721473
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
14731474
new WP_SQLite_Token( 'AS', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ),
14741475
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
@@ -1491,10 +1492,12 @@ private function execute_delete() {
14911492
$ids_to_delete[] = $id['id_1'];
14921493
}
14931494

1495+
$quoted_table = $this->quote_identifier( $this->table_name );
1496+
$quoted_pk = $this->quote_identifier( $pk_name );
14941497
$query = (
14951498
count( $ids_to_delete )
1496-
? "DELETE FROM {$this->table_name} WHERE {$pk_name} IN (" . implode( ',', $ids_to_delete ) . ')'
1497-
: "DELETE FROM {$this->table_name} WHERE 0=1"
1499+
? "DELETE FROM {$quoted_table} WHERE {$quoted_pk} IN (" . implode( ',', $ids_to_delete ) . ')'
1500+
: "DELETE FROM {$quoted_table} WHERE 0=1"
14981501
);
14991502
$this->execute_sqlite_query( $query );
15001503
$this->set_result_from_affected_rows(
@@ -1765,9 +1768,9 @@ private function describe( $table_name ) {
17651768
ELSE 'PRI'
17661769
END
17671770
) as `Key`
1768-
FROM pragma_table_info(\"$table_name\") p
1771+
FROM pragma_table_info(" . $this->pdo->quote( $table_name ) . ") p
17691772
LEFT JOIN " . self::DATA_TYPES_CACHE_TABLE . " d
1770-
ON d.`table` = \"$table_name\"
1773+
ON d.`table` = " . $this->pdo->quote( $table_name ) . "
17711774
AND d.`column_or_index` = p.`name`
17721775
;
17731776
"
@@ -1891,7 +1894,7 @@ private function preface_where_clause_with_a_subquery() {
18911894
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
18921895
new WP_SQLite_Token( 'FROM', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ),
18931896
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
1894-
new WP_SQLite_Token( $this->table_name, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ),
1897+
new WP_SQLite_Token( $this->quote_identifier( $this->table_name ), WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ),
18951898
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
18961899
)
18971900
);
@@ -2955,7 +2958,7 @@ private function translate_on_duplicate_key( $table_name ) {
29552958
$max = count( $conflict_columns );
29562959
$i = 0;
29572960
foreach ( $conflict_columns as $conflict_column ) {
2958-
$this->rewriter->add( new WP_SQLite_Token( '"' . $conflict_column . '"', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ) );
2961+
$this->rewriter->add( new WP_SQLite_Token( $this->quote_identifier( $conflict_column ), WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ) );
29592962
if ( ++$i < $max ) {
29602963
$this->rewriter->add( new WP_SQLite_Token( ',', WP_SQLite_Token::TYPE_OPERATOR ) );
29612964
$this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) );
@@ -2993,12 +2996,12 @@ private function get_primary_keys( $table_name ) {
29932996
* @return array
29942997
*/
29952998
private function get_keys( $table_name, $only_unique = false ) {
2996-
$query = $this->execute_sqlite_query( 'SELECT * FROM pragma_index_list("' . $table_name . '") as l;' );
2999+
$query = $this->execute_sqlite_query( 'SELECT * FROM pragma_index_list(' . $this->pdo->quote( $table_name ) . ') as l;' );
29973000
$indices = $query->fetchAll();
29983001
$results = array();
29993002
foreach ( $indices as $index ) {
30003003
if ( ! $only_unique || '1' === $index['unique'] ) {
3001-
$query = $this->execute_sqlite_query( 'SELECT * FROM pragma_index_info("' . $index['name'] . '") as l;' );
3004+
$query = $this->execute_sqlite_query( 'SELECT * FROM pragma_index_info(' . $this->pdo->quote( $index['name'] ) . ') as l;' );
30023005
$results[] = array(
30033006
'index' => $index,
30043007
'columns' => $query->fetchAll(),
@@ -3049,7 +3052,7 @@ private function execute_alter() {
30493052
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
30503053
new WP_SQLite_Token( 'TABLE', WP_SQLite_Token::TYPE_KEYWORD ),
30513054
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
3052-
new WP_SQLite_Token( $this->table_name, WP_SQLite_Token::TYPE_KEYWORD ),
3055+
new WP_SQLite_Token( $this->quote_identifier( $this->table_name ), WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ),
30533056
)
30543057
);
30553058
$op_type = strtoupper( $this->rewriter->consume()->token ?? '' );
@@ -3153,7 +3156,7 @@ private function execute_alter() {
31533156

31543157
// Drop ON UPDATE trigger by the old column name.
31553158
$on_update_trigger_name = $this->get_column_on_update_current_timestamp_trigger_name( $this->table_name, $from_name );
3156-
$this->execute_sqlite_query( "DROP TRIGGER IF EXISTS \"$on_update_trigger_name\"" );
3159+
$this->execute_sqlite_query( 'DROP TRIGGER IF EXISTS ' . $this->quote_identifier( $on_update_trigger_name ) );
31573160

31583161
/*
31593162
* In SQLite, there is no direct equivalent to the CHANGE COLUMN
@@ -3186,7 +3189,7 @@ private function execute_alter() {
31863189
if ( ! $token ) {
31873190
break;
31883191
}
3189-
if ( WP_SQLite_Token::TYPE_STRING !== $token->type
3192+
if ( ( WP_SQLite_Token::TYPE_STRING !== $token->type && WP_SQLite_Token::TYPE_SYMBOL !== $token->type )
31903193
|| $from_name !== $this->normalize_column_name( $token->value ) ) {
31913194
continue;
31923195
}
@@ -3222,30 +3225,33 @@ private function execute_alter() {
32223225
// Otherwise, just add the new name in place of the old name we dropped.
32233226
$create_table->add(
32243227
new WP_SQLite_Token(
3225-
"`$new_field->name`",
3226-
WP_SQLite_Token::TYPE_KEYWORD
3228+
$this->quote_identifier( $new_field->name ),
3229+
WP_SQLite_Token::TYPE_KEYWORD,
3230+
WP_SQLite_Token::FLAG_KEYWORD_KEY
32273231
)
32283232
);
32293233
}
32303234
}
32313235

32323236
// 3. Copy the data out of the old table
32333237
$cache_table_name = "_tmp__{$this->table_name}_" . rand( 10000000, 99999999 );
3238+
$quoted_cache_table = $this->quote_identifier( $cache_table_name );
3239+
$quoted_table = $this->quote_identifier( $this->table_name );
32343240
$this->execute_sqlite_query(
3235-
"CREATE TABLE `$cache_table_name` as SELECT * FROM `$this->table_name`"
3241+
"CREATE TABLE $quoted_cache_table as SELECT * FROM $quoted_table"
32363242
);
32373243

32383244
// 4. Drop the old table to free up the indexes names
3239-
$this->execute_sqlite_query( "DROP TABLE `$this->table_name`" );
3245+
$this->execute_sqlite_query( "DROP TABLE $quoted_table" );
32403246

32413247
// 5. Create a new table from the updated schema
32423248
$this->execute_sqlite_query( $create_table->get_updated_query() );
32433249

32443250
// 6. Copy the data from step 3 to the new table
3245-
$this->execute_sqlite_query( "INSERT INTO {$this->table_name} SELECT * FROM $cache_table_name" );
3251+
$this->execute_sqlite_query( "INSERT INTO $quoted_table SELECT * FROM $quoted_cache_table" );
32463252

32473253
// 7. Drop the old table copy
3248-
$this->execute_sqlite_query( "DROP TABLE `$cache_table_name`" );
3254+
$this->execute_sqlite_query( "DROP TABLE $quoted_cache_table" );
32493255

32503256
// 8. Restore any indexes that were dropped in step 4
32513257
foreach ( $old_indexes as $row ) {
@@ -3260,8 +3266,8 @@ private function execute_alter() {
32603266
$columns = array();
32613267
foreach ( $row['columns'] as $column ) {
32623268
$columns[] = ( $column['name'] === $from_name )
3263-
? '`' . $new_field->name . '`'
3264-
: '`' . $column['name'] . '`';
3269+
? $this->quote_identifier( $new_field->name )
3270+
: $this->quote_identifier( $column['name'] );
32653271
}
32663272

32673273
$unique = '1' === $row['index']['unique'] ? 'UNIQUE' : '';
@@ -3271,7 +3277,7 @@ private function execute_alter() {
32713277
* a part of the CREATE TABLE statement
32723278
*/
32733279
$this->execute_sqlite_query(
3274-
"CREATE $unique INDEX IF NOT EXISTS `{$row['index']['name']}` ON $this->table_name (" . implode( ', ', $columns ) . ')'
3280+
"CREATE $unique INDEX IF NOT EXISTS " . $this->quote_identifier( $row['index']['name'] ) . " ON $quoted_table (" . implode( ', ', $columns ) . ')'
32753281
);
32763282
}
32773283

@@ -3300,11 +3306,11 @@ private function execute_alter() {
33003306
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
33013307
new WP_SQLite_Token( $sqlite_index_type, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ),
33023308
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
3303-
new WP_SQLite_Token( "\"$sqlite_index_name\"", WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ),
3309+
new WP_SQLite_Token( $this->quote_identifier( $sqlite_index_name ), WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ),
33043310
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
33053311
new WP_SQLite_Token( 'ON', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ),
33063312
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
3307-
new WP_SQLite_Token( "\"$this->table_name\"", WP_SQLite_Token::TYPE_STRING, WP_SQLite_Token::FLAG_STRING_DOUBLE_QUOTES ),
3313+
new WP_SQLite_Token( $this->quote_identifier( $this->table_name ), WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ),
33083314
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
33093315
new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ),
33103316
)
@@ -3332,8 +3338,8 @@ private function execute_alter() {
33323338
}
33333339
// $token is field name.
33343340
if ( ! $token->matches( WP_SQLite_Token::TYPE_OPERATOR ) ) {
3335-
$token->token = '`' . $this->normalize_column_name( $token->token ) . '`';
3336-
$token->value = '`' . $this->normalize_column_name( $token->token ) . '`';
3341+
$token->token = $this->quote_identifier( $this->normalize_column_name( $token->token ) );
3342+
$token->value = $token->token;
33373343
}
33383344

33393345
/*
@@ -3358,7 +3364,7 @@ private function execute_alter() {
33583364
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
33593365
new WP_SQLite_Token( 'INDEX', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ),
33603366
new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ),
3361-
new WP_SQLite_Token( "\"{$this->table_name}__$key_name\"", WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ),
3367+
new WP_SQLite_Token( $this->quote_identifier( $this->table_name . '__' . $key_name ), WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ),
33623368
)
33633369
);
33643370
} else {
@@ -3618,7 +3624,7 @@ private function execute_show() {
36183624
$tables = $this->strip_sqlite_system_tables( $stmt->fetchAll( $this->pdo_fetch_mode ) );
36193625
foreach ( $tables as $table ) {
36203626
$table_name = $table->Name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
3621-
$stmt = $this->execute_sqlite_query( "SELECT COUNT(1) as `Rows` FROM $table_name" );
3627+
$stmt = $this->execute_sqlite_query( 'SELECT COUNT(1) as `Rows` FROM ' . $this->quote_identifier( $table_name ) );
36223628
$rows = $stmt->fetchall( $this->pdo_fetch_mode );
36233629
$table->Rows = $rows[0]->Rows; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
36243630
}
@@ -3710,7 +3716,7 @@ private function generate_create_statement() {
37103716
* @return stdClass[]
37113717
*/
37123718
protected function get_table_columns( $table_name ) {
3713-
return $this->execute_sqlite_query( "PRAGMA table_info(\"$table_name\");" )
3719+
return $this->execute_sqlite_query( 'PRAGMA table_info(' . $this->pdo->quote( $table_name ) . ');' )
37143720
->fetchAll( $this->pdo_fetch_mode );
37153721
}
37163722

@@ -3729,7 +3735,7 @@ protected function get_column_definitions( $table_name, $columns ) {
37293735
$mysql_type = $this->get_cached_mysql_data_type( $table_name, $column->name );
37303736
$is_auto_incr = $auto_increment_column && strtolower( $auto_increment_column ) === strtolower( $column->name );
37313737
$definition = array();
3732-
$definition[] = '`' . $column->name . '`';
3738+
$definition[] = $this->quote_identifier( $column->name );
37333739
$definition[] = $mysql_type ?? $column->name;
37343740

37353741
if ( '1' === $column->notnull ) {
@@ -3787,7 +3793,7 @@ private function get_key_definitions( $table_name, $columns ) {
37873793
// Remove the prefix from the index name if there is any. We use __ as a separator.
37883794
$index_name = explode( '__', $key['index']['name'], 2 )[1] ?? $key['index']['name'];
37893795

3790-
$key_definition[] = sprintf( '`%s`', $index_name );
3796+
$key_definition[] = $this->quote_identifier( $index_name );
37913797

37923798
$cols = array_map(
37933799
function ( $column ) use ( $table_name, $key_length_limit ) {
@@ -3807,9 +3813,9 @@ function ( $column ) use ( $table_name, $key_length_limit ) {
38073813
str_ends_with( $data_type, 'blob' ) ||
38083814
str_starts_with( $data_type, 'var' )
38093815
) {
3810-
return sprintf( '`%s`(%s)', $column['name'], $data_length );
3816+
return $this->quote_identifier( $column['name'] ) . '(' . $data_length . ')';
38113817
}
3812-
return sprintf( '`%s`', $column['name'] );
3818+
return $this->quote_identifier( $column['name'] );
38133819
},
38143820
$key['columns']
38153821
);
@@ -3842,7 +3848,7 @@ function ( $a, $b ) {
38423848

38433849
foreach ( $columns as $column ) {
38443850
if ( '0' !== $column->pk ) {
3845-
$primary_keys[] = sprintf( '`%s`', $column->name );
3851+
$primary_keys[] = $this->quote_identifier( $column->name );
38463852
}
38473853
}
38483854

@@ -3860,7 +3866,7 @@ function ( $a, $b ) {
38603866
*/
38613867
private function get_autoincrement_column( $table_name ) {
38623868
preg_match(
3863-
'/"([^"]+)"\s+integer\s+primary\s+key\s+autoincrement/i',
3869+
'/[`"]([^`"]+)[`"]\s+integer\s+primary\s+key\s+autoincrement/i',
38643870
$this->get_sqlite_create_table( $table_name ),
38653871
$matches
38663872
);
@@ -4050,6 +4056,21 @@ private function normalize_column_name( $column_name ) {
40504056
return trim( $column_name, '`\'"' );
40514057
}
40524058

4059+
/**
4060+
* Quotes an identifier for safe use in SQLite queries.
4061+
*
4062+
* Wraps the identifier in backticks and escapes any internal backticks
4063+
* by doubling them. This ensures identifiers with special characters
4064+
* are properly escaped in the target SQLite query context.
4065+
*
4066+
* @param string $identifier The unquoted identifier.
4067+
*
4068+
* @return string The properly quoted identifier.
4069+
*/
4070+
private function quote_identifier( $identifier ) {
4071+
return '`' . str_replace( '`', '``', $identifier ) . '`';
4072+
}
4073+
40534074
/**
40544075
* Normalizes an index type.
40554076
*
@@ -4487,12 +4508,15 @@ private function add_column_on_update_current_timestamp( $table, $column ) {
44874508
// The trigger wouldn't work for virtual and "WITHOUT ROWID" tables,
44884509
// but currently that can't happen as we're not creating such tables.
44894510
// See: https://www.sqlite.org/rowidtable.html
4511+
$quoted_trigger = $this->quote_identifier( $trigger_name );
4512+
$quoted_table = $this->quote_identifier( $table );
4513+
$quoted_column = $this->quote_identifier( $column );
44904514
$this->execute_sqlite_query(
4491-
"CREATE TRIGGER \"$trigger_name\"
4492-
AFTER UPDATE ON \"$table\"
4515+
"CREATE TRIGGER $quoted_trigger
4516+
AFTER UPDATE ON $quoted_table
44934517
FOR EACH ROW
44944518
BEGIN
4495-
UPDATE \"$table\" SET \"$column\" = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;
4519+
UPDATE $quoted_table SET $quoted_column = CURRENT_TIMESTAMP WHERE rowid = NEW.rowid;
44964520
END"
44974521
);
44984522
}

0 commit comments

Comments
 (0)