Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ae6a847
refactor: replace direct SQL with WP_Query in redirect_guess_404_perm…
anandrajaram21 Nov 19, 2025
ce90699
perf: optimize wp query and remove superfluous comments
anandrajaram21 Nov 20, 2025
9b52804
chore: change filter function to a static function
anandrajaram21 Nov 21, 2025
4532fae
Merge branch 'trunk' into 64250-redirect-guess-404-permalink
anandrajaram21 Jan 14, 2026
3da7e89
feat: update approach to use wp-query instead of manual searching
anandrajaram21 Jan 14, 2026
c420204
feat: resolve copilot PR review comments
anandrajaram21 Jan 16, 2026
7eed5d7
Use array_first instead of posts[0]
anandrajaram21 Jan 19, 2026
ccc2e39
Add check for post_id to be integer
anandrajaram21 Jan 19, 2026
6021fd9
feat: remove post_name from default search columns
anandrajaram21 Jan 19, 2026
243ac98
doc: update comment to be more descriptive
anandrajaram21 Jan 19, 2026
3e8210f
chore: fix phpcs error in class-wp-query
anandrajaram21 Jan 19, 2026
940add6
feat: update test to use an existing but unsupported search column
anandrajaram21 Jan 22, 2026
ff23ab9
feat: include suppress filters in wp query args and update filter docs
anandrajaram21 Feb 10, 2026
07df29a
feat: change 'true' to true
anandrajaram21 Feb 10, 2026
5bcf66f
Merge branch 'trunk' into 64250-redirect-guess-404-permalink
anandrajaram21 Feb 10, 2026
cf4de84
Apply suggestion from @mukeshpanchal27
anandrajaram21 Feb 11, 2026
eb2f5cd
Add `search_position` parameter to `WP_Query` for more refined search…
spacedmonkey Feb 18, 2026
9b17942
Merge branch 'trunk' into 64250-redirect-guess-404-permalink-v2
jonnynews Feb 18, 2026
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
49 changes: 34 additions & 15 deletions src/wp-includes/canonical.php
Original file line number Diff line number Diff line change
Expand Up @@ -919,13 +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 +968,24 @@ 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',
'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['search_position'] = 'start';
}

// If any of post_type, year, monthnum, or day are set, use them to refine the query.
Expand All @@ -985,31 +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;
}

// Build date_query array from individual year, month, and day query vars for WP_Query compatibility.
$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 );
}

$query = new WP_Query( $query_args );

// 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 ) ) . "')" );
if ( empty( $query->posts ) ) {
return false;
}

if ( ! $post_id ) {
$post_id = array_first( $query->posts );
if ( ! is_int( $post_id ) ) {
return false;
}

Expand Down
50 changes: 40 additions & 10 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @since 7.0.0 Introduced the `$starts_with` parameter.
* @since 7.0.0 Introduced the `$search_position` parameter.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

+1

*
* @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 `$search_position`.
* @type bool $search_position Whether to search start, ends or is anywhere within keyword. Default anywhere.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* @type bool $search_position Whether to search start, ends or is anywhere within keyword. Default anywhere.
* @type string $search_position Whether to search start, ends or is anywhere within keyword. May be 'start', 'end', or the default 'anywhere'.

* Cannot be used together with `$exact`.
* @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['search_position'] ) ) {
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.

  1. Let's allow a dev to pass both exact => true, search_position => anywhere without throwing a notice, as it's the documented default.
  2. Add search_position => '' to fill_query_vars() method.

_doing_it_wrong(
__METHOD__,
__( 'The `exact` and `search_position` query parameters are mutually exclusive and cannot be used together.' ),
'7.0.0'
);
$query_vars['search_position'] = 'anywhere';
}

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

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

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

$default_search_columns = array( 'post_title', 'post_excerpt', 'post_content' );
$search_columns = ! empty( $query_vars['search_columns'] ) ? $query_vars['search_columns'] : $default_search_columns;
$default_search_columns = array( 'post_title', 'post_excerpt', 'post_content' );
$allowed_search_columns = $default_search_columns;
$allowed_search_columns[] = 'post_name';
$search_columns = ! empty( $query_vars['search_columns'] ) ? $query_vars['search_columns'] : $default_search_columns;
if ( ! is_array( $search_columns ) ) {
$search_columns = array( $search_columns );
}

/**
* 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 All @@ -1473,7 +1501,7 @@ protected function parse_search( &$query_vars ) {
$search_columns = (array) apply_filters( 'post_search_columns', $search_columns, $query_vars['s'], $this );

// Use only supported search columns.
$search_columns = array_intersect( $search_columns, $default_search_columns );
$search_columns = array_intersect( $search_columns, $allowed_search_columns );
if ( empty( $search_columns ) ) {
$search_columns = $default_search_columns;
}
Expand All @@ -1500,13 +1528,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 Expand Up @@ -1962,6 +1988,10 @@ public function get_posts() {
}
}

if ( ! isset( $query_vars['search_position'] ) || ! in_array( $query_vars['search_position'], array( 'start', 'end', 'anywhere' ), true ) ) {
$query_vars['search_position'] = 'anywhere';
}

if ( ! isset( $query_vars['ignore_sticky_posts'] ) ) {
$query_vars['ignore_sticky_posts'] = false;
}
Expand Down
4 changes: 2 additions & 2 deletions tests/phpunit/tests/query/searchColumns.php
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.

In this file there are a few tests to ensure the default colums are used for empty/invalid search_columns values. Add assertions to ensure that the slug is not included in those tests.

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_author', $q->request, "SQL request shouldn't contain post_author 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_author' );
return $search_columns;
}

Expand Down
Loading
Loading