Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: bugfix

Sitemaps: Fix SQL error when wp_posts has a column whose name is a reserved SQL keyword.
46 changes: 28 additions & 18 deletions projects/plugins/jetpack/modules/sitemaps/sitemap-librarian.php
Original file line number Diff line number Diff line change
Expand Up @@ -302,20 +302,19 @@
}
$post_types_list = implode( ',', $post_types );

$columns_list = $this->get_sanitized_post_columns( $wpdb );
$columns = $this->get_sanitized_post_columns( $wpdb );

// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WPCS: db call ok; no-cache ok.
return $wpdb->get_results(
$wpdb->prepare(

Check warning on line 309 in projects/plugins/jetpack/modules/sitemaps/sitemap-librarian.php

View workflow job for this annotation

GitHub Actions / PHP Code Sniffer (changes to excluded files only)

Incorrect number of replacements passed to $wpdb->prepare(). Found 1 replacement parameters, expected 2. (WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lint issue on this line is doubly confused. Best to suppress it.

Suggested change
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Sniff is confused by both the interpolation of `$columns['placeholders']` and the spread.
$wpdb->prepare(

"SELECT $columns_list
"SELECT {$columns['placeholders']}
FROM $wpdb->posts
WHERE post_status='publish'
AND post_type IN ($post_types_list)
AND ID>%d
ORDER BY ID ASC
LIMIT %d;",
$from_id,
$num_posts
...array_merge( $columns['columns'], array( $from_id, $num_posts ) )
)
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
Expand Down Expand Up @@ -445,41 +444,52 @@

$post_types_list = implode( ',', $post_types );

$columns_list = $this->get_sanitized_post_columns( $wpdb );
$columns = $this->get_sanitized_post_columns( $wpdb );

// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.QuotedSimplePlaceholder,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WPCS: db call ok; no-cache ok.
return $wpdb->get_results(
$wpdb->prepare(

Check warning on line 451 in projects/plugins/jetpack/modules/sitemaps/sitemap-librarian.php

View workflow job for this annotation

GitHub Actions / PHP Code Sniffer (changes to excluded files only)

Incorrect number of replacements passed to $wpdb->prepare(). Found 1 replacement parameters, expected 2. (WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Suggested change
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Sniff is confused by both the interpolation of `$columns['placeholders']` and the spread.
$wpdb->prepare(

"SELECT $columns_list
"SELECT {$columns['placeholders']}
Comment on lines +447 to +452
FROM $wpdb->posts
WHERE post_status='publish'
AND post_date >= '%s'
AND post_type IN ($post_types_list)
ORDER BY post_date DESC
LIMIT %d;",
$two_days_ago,
$num_posts
...array_merge( $columns['columns'], array( $two_days_ago, $num_posts ) )
)
);
// phpcs:enable WordPress.DB.PreparedSQLPlaceholders.QuotedSimplePlaceholder,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}

/**
* Returns all columns from the posts table,
* except post_content and post_content_filtered.
* Returns all columns from the posts table, except post_content and post_content_filtered,
* along with a matching list of %i placeholders for safe use in wpdb::prepare().
*
* Using %i identifier placeholders ensures column names are correctly backtick-quoted,
* which avoids SQL errors when a column name happens to be a reserved keyword
* (e.g. `order`, `key`, `group`).
*
* @param object $wpdb The WordPress database object.
* @return string The sanitized post columns.
* @return array {
* @type string $placeholders Comma-separated list of %i placeholders, one per column.
* @type string[] $columns The column names to feed into wpdb::prepare().
* }
*/
private function get_sanitized_post_columns( $wpdb ) {
$columns = array_filter(
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->get_col( "SHOW COLUMNS FROM $wpdb->posts" ),
function ( $column ) {
return $column !== 'post_content' && $column !== 'post_content_filtered';
}
$columns = array_values(
array_filter(
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->get_col( "SHOW COLUMNS FROM $wpdb->posts" ),
function ( $column ) {
return $column !== 'post_content' && $column !== 'post_content_filtered';
}
)
);

return implode( ',', array_map( 'esc_sql', $columns ) );
return array(
'placeholders' => implode( ',', array_fill( 0, count( $columns ), '%i' ) ),
'columns' => $columns,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,54 @@ public function test_sitemap_librarian_delete_contiguously_named_rows() {
$this->assertTrue( $librarian->read_sitemap_data( 'name-2', 'type' ) === null );
$this->assertTrue( $librarian->read_sitemap_data( 'name-3', 'type' ) === null );
}

/**
* Regression test for https://github.com/Automattic/jetpack/issues/48202
*
* If wp_posts has a column whose name is a reserved SQL keyword (e.g. `order`),
* the SELECT used by the sitemap query must still succeed because column names
* are passed as %i identifier placeholders to wpdb::prepare().
*
* @group jetpack-sitemap
*/
#[Group( 'jetpack-sitemap' )]
public function test_query_posts_after_id_handles_reserved_keyword_columns() {
global $wpdb;

// Check whether the column already exists before adding it.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$column_existed = (bool) $wpdb->get_var( "SHOW COLUMNS FROM {$wpdb->posts} LIKE 'order'" );

if ( ! $column_existed ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( "ALTER TABLE {$wpdb->posts} ADD COLUMN `order` INT NULL" );
$this->assertEmpty( $wpdb->last_error, 'ALTER TABLE to add `order` column failed.' );
}

try {
$post_id = self::factory()->post->create(
array(
'post_status' => 'publish',
'post_type' => 'post',
)
);

$librarian = new Jetpack_Sitemap_Librarian();
$results = $librarian->query_posts_after_id( 0, 10 );

// The query must run without raising a SQL error.
$this->assertSame( '', $wpdb->last_error );
$this->assertIsArray( $results );

// And the post we just created should come back.
$ids = wp_list_pluck( $results, 'ID' );
$this->assertContains( (string) $post_id, array_map( 'strval', $ids ) );
} finally {
// Only drop the column if we added it — don't mutate a pre-existing schema.
if ( ! $column_existed ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query( "ALTER TABLE {$wpdb->posts} DROP COLUMN `order`" );
}
}
}
}
Loading