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
44 changes: 30 additions & 14 deletions src/wp-includes/canonical.php
Original file line number Diff line number Diff line change
Expand Up @@ -919,12 +919,9 @@ function strip_fragment_from_url( $url ) {
*
* @since 2.3.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @return string|false The correct URL if one is found. False on failure.
*/
function redirect_guess_404_permalink() {
global $wpdb;

/**
* Filters whether to attempt to guess a redirect URL for a 404 request.
Expand Down Expand Up @@ -972,10 +969,23 @@ function redirect_guess_404_permalink() {
*/
$strict_guess = apply_filters( 'strict_redirect_guess_404_permalink', false );

$query_args = array(
'post_status' => $publicly_viewable_statuses,
'posts_per_page' => 1,
'no_found_rows' => true,
'ignore_sticky_posts' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'fields' => 'ids',
'orderby' => 'none',
Copy link
Member

Choose a reason for hiding this comment

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

@spacedmonkey Should this also include suppress_filters`? It wasn't filterable before, so this would prevent accidental filtering.

Suggested change
'orderby' => 'none',
'orderby' => 'none',
'suppress_filters' => true,

);

if ( $strict_guess ) {
$where = $wpdb->prepare( 'post_name = %s', get_query_var( 'name' ) );
$query_args['name'] = get_query_var( 'name' );
} else {
$where = $wpdb->prepare( 'post_name LIKE %s', $wpdb->esc_like( get_query_var( 'name' ) ) . '%' );
$query_args['s'] = get_query_var( 'name' );
$query_args['search_columns'] = array( 'post_name' );
$query_args['starts_with'] = true;
Comment on lines +986 to +988
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

The new starts_with parameter usage in redirect_guess_404_permalink() lacks test coverage. Consider adding tests to verify that the 404 redirect guessing works correctly with this new parameter, particularly for the non-strict guess scenario.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

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

@westonruter @spacedmonkey if this approach seems alright, can I go ahead and write tests, or do I have to make any more changes in the approach before I go ahead

Copy link
Member

Choose a reason for hiding this comment

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

I'll defer to @spacedmonkey since this was his suggestion. But it's looking good for far to me.

Comment on lines +986 to +988
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The refactored code uses WP_Query's search functionality with search_columns set to post_name only, but this relies on the new behavior where post_name is included in the default search columns. If the previous comment about adding post_name to default search columns is addressed by making it opt-in, this code will need to be updated to explicitly ensure post_name searching is supported regardless of the default behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +986 to +988
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The refactored non-strict guess implementation introduces a behavioral change regarding password-protected posts. When using WP_Query with search functionality, the parse_search method automatically excludes password-protected posts for non-logged-in users (see line 1546). The original SQL implementation did not have this restriction and would redirect to password-protected posts. This could break existing functionality where 404 redirects would work for password-protected posts.

Copilot uses AI. Check for mistakes.
}

// If any of post_type, year, monthnum, or day are set, use them to refine the query.
Expand All @@ -985,34 +995,40 @@ function redirect_guess_404_permalink() {
if ( empty( $post_types ) ) {
return false;
}
$where .= " AND post_type IN ('" . join( "', '", esc_sql( get_query_var( 'post_type' ) ) ) . "')";
$query_args['post_type'] = $post_types;
} else {
if ( ! in_array( get_query_var( 'post_type' ), $publicly_viewable_post_types, true ) ) {
return false;
}
$where .= $wpdb->prepare( ' AND post_type = %s', get_query_var( 'post_type' ) );
$query_args['post_type'] = get_query_var( 'post_type' );
}
} else {
$where .= " AND post_type IN ('" . implode( "', '", esc_sql( $publicly_viewable_post_types ) ) . "')";
$query_args['post_type'] = $publicly_viewable_post_types;
}

// Handle date queries.
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The comment says "Handle date queries" but this should be more descriptive about what's happening. Consider updating to "Build date_query array from individual year, month, and day query vars for WP_Query compatibility" to better explain the transformation being performed.

Suggested change
// Handle date queries.
// Build date_query array from individual year, month, and day query vars for WP_Query compatibility.

Copilot uses AI. Check for mistakes.
$date_query = array();
if ( get_query_var( 'year' ) ) {
$where .= $wpdb->prepare( ' AND YEAR(post_date) = %d', get_query_var( 'year' ) );
$date_query['year'] = get_query_var( 'year' );
}
if ( get_query_var( 'monthnum' ) ) {
$where .= $wpdb->prepare( ' AND MONTH(post_date) = %d', get_query_var( 'monthnum' ) );
$date_query['month'] = get_query_var( 'monthnum' );
}
if ( get_query_var( 'day' ) ) {
$where .= $wpdb->prepare( ' AND DAYOFMONTH(post_date) = %d', get_query_var( 'day' ) );
$date_query['day'] = get_query_var( 'day' );
}
if ( ! empty( $date_query ) ) {
$query_args['date_query'] = array( $date_query );
}

// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$post_id = $wpdb->get_var( "SELECT ID FROM $wpdb->posts WHERE $where AND post_status IN ('" . implode( "', '", esc_sql( $publicly_viewable_statuses ) ) . "')" );
$query = new WP_Query( $query_args );

if ( ! $post_id ) {
if ( empty( $query->posts ) ) {
return false;
}

$post_id = $query->posts[0];
Copy link
Member

Choose a reason for hiding this comment

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

This can now use:

Suggested change
$post_id = $query->posts[0];
$post_id = array_first( $query->posts );

This is polyfilled in core as of WP 6.9. The function was introduced in PHP 8.5. This is better since a posts_results filter could try doing array_filter() and the result could be array keys that don't have index 0.


Copy link
Member

Choose a reason for hiding this comment

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

As with the above, to be extra safe:

Suggested change
if ( ! is_int( $post_id ) ) {
return false;
}

if ( get_query_var( 'feed' ) ) {
return get_post_comments_feed_link( $post_id, get_query_var( 'feed' ) );
} elseif ( get_query_var( 'page' ) > 1 ) {
Expand Down
35 changes: 27 additions & 8 deletions src/wp-includes/class-wp-query.php
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ public function fill_query_vars( $query_vars ) {
* @since 5.3.0 Introduced the `$meta_type_key` parameter.
* @since 6.1.0 Introduced the `$update_menu_item_cache` parameter.
* @since 6.2.0 Introduced the `$search_columns` parameter.
* @since 7.0.0 Introduced the `$starts_with` parameter.
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The test file for public query variables (tests/phpunit/tests/query/vars.php) needs to be updated to include starts_with in the expected list of public query vars, similar to how exact is listed. Without this update, the test will fail once starts_with is added to the public query vars.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

To do once implementation is finalized.

*
* @param string|array $query {
* Optional. Array or string of Query parameters.
Expand All @@ -686,6 +687,9 @@ public function fill_query_vars( $query_vars ) {
* See WP_Date_Query::__construct().
* @type int $day Day of the month. Default empty. Accepts numbers 1-31.
* @type bool $exact Whether to search by exact keyword. Default false.
* Cannot be used together with `$starts_with`.
* @type bool $starts_with Whether to search starts with keyword. Default false.
* Cannot be used together with `$exact`.
Comment on lines +690 to +692
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

The new starts_with query parameter lacks test coverage. Tests should be added to verify: 1) that starts_with correctly searches for terms at the beginning of columns, 2) that it cannot be used together with exact (triggering the mutual exclusivity check), and 3) that it works correctly with different search columns including post_name.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

To do once implementation is finalized.

* @type string $fields Post fields to query for. Accepts:
* - '' Returns an array of complete post objects (`WP_Post[]`).
* - 'ids' Returns an array of post IDs (`int[]`).
Expand Down Expand Up @@ -774,7 +778,7 @@ public function fill_query_vars( $query_vars ) {
* character used for exclusion can be modified using the
* the 'wp_query_search_exclusion_prefix' filter.
* @type string[] $search_columns Array of column names to be searched. Accepts 'post_title',
* 'post_excerpt' and 'post_content'. Default empty array.
* 'post_excerpt', 'post_content' and 'post_name'. Default empty array.
* @type int $second Second of the minute. Default empty. Accepts numbers 0-59.
* @type bool $sentence Whether to search by phrase. Default false.
* @type bool $suppress_filters Whether to suppress filters. Default false.
Expand Down Expand Up @@ -813,6 +817,15 @@ public function parse_query( $query = '' ) {
$query_vars = &$this->query_vars;
$this->query_vars_changed = true;

if ( ! empty( $query_vars['exact'] ) && ! empty( $query_vars['starts_with'] ) ) {
_doing_it_wrong(
__METHOD__,
__( 'The `exact` and `starts_with` query parameters are mutually exclusive and cannot be used together.' ),
'7.0.0'
);
$query_vars['starts_with'] = false;
}

if ( ! empty( $query_vars['robots'] ) ) {
$this->is_robots = true;
} elseif ( ! empty( $query_vars['favicon'] ) ) {
Expand Down Expand Up @@ -1448,11 +1461,19 @@ protected function parse_search( &$query_vars ) {
}
}

$n = ! empty( $query_vars['exact'] ) ? '' : '%';
$start = '%';
$end = '%';
if ( ! empty( $query_vars['exact'] ) ) {
$start = '';
$end = '';
} elseif ( ! empty( $query_vars['starts_with'] ) ) {
$start = '';
}

$searchand = '';
$query_vars['search_orderby_title'] = array();

$default_search_columns = array( 'post_title', 'post_excerpt', 'post_content' );
$default_search_columns = array( 'post_title', 'post_excerpt', 'post_content', 'post_name' );
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

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

Adding post_name to the default search columns changes the behavior of all existing searches that don't explicitly specify search_columns. This means that searches will now match against post slugs by default, which could return unexpected results for users. For example, a search for "hello" would now match a post with slug "hello-world" even if the post title, excerpt, or content don't contain "hello". This is a significant behavioral change that could affect backwards compatibility. Consider documenting this breaking change or making this opt-in rather than default behavior.

Copilot uses AI. Check for mistakes.
$search_columns = ! empty( $query_vars['search_columns'] ) ? $query_vars['search_columns'] : $default_search_columns;
if ( ! is_array( $search_columns ) ) {
$search_columns = array( $search_columns );
Expand All @@ -1461,7 +1482,7 @@ protected function parse_search( &$query_vars ) {
/**
* Filters the columns to search in a WP_Query search.
*
* The supported columns are `post_title`, `post_excerpt` and `post_content`.
* The supported columns are `post_title`, `post_excerpt`, `post_content` and `post_name`.
* They are all included by default.
*
* @since 6.2.0
Expand Down Expand Up @@ -1500,13 +1521,11 @@ protected function parse_search( &$query_vars ) {
$andor_op = 'OR';
}

if ( $n && ! $exclude ) {
$like = '%' . $wpdb->esc_like( $term ) . '%';
$like = $start . $wpdb->esc_like( $term ) . $end;
if ( $end && ! $exclude ) {
$query_vars['search_orderby_title'][] = $wpdb->prepare( "{$wpdb->posts}.post_title LIKE %s", $like );
}

$like = $n . $wpdb->esc_like( $term ) . $n;

$search_columns_parts = array();
foreach ( $search_columns as $search_column ) {
$search_columns_parts[ $search_column ] = $wpdb->prepare( "({$wpdb->posts}.$search_column $like_op %s)", $like );
Expand Down
4 changes: 2 additions & 2 deletions tests/phpunit/tests/query/searchColumns.php
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ public function test_search_columns_should_not_be_filterable_with_non_supported_
)
);

$this->assertStringNotContainsString( 'post_name', $q->request, "SQL request shouldn't contain post_name string." );
$this->assertStringNotContainsString( 'post_slug', $q->request, "SQL request shouldn't contain post_slug string." );
$this->assertSameSets( array( self::$pid1, self::$pid2, self::$pid3 ), $q->posts, 'Query results should be equal to the set.' );
}

Expand All @@ -376,7 +376,7 @@ public function test_search_columns_should_not_be_filterable_with_non_supported_
* @return string[] $search_columns Array of column names to be searched.
*/
public function post_non_supported_search_column( $search_columns, $search, $wp_query ) {
$search_columns = array( 'post_name' );
$search_columns = array( 'post_slug' );
return $search_columns;
}

Expand Down
Loading