From ae6a847aacd2f0e256b4b3cc2d64f35bb66a49dc Mon Sep 17 00:00:00 2001 From: Anand Rajaram Date: Wed, 19 Nov 2025 21:46:31 +0530 Subject: [PATCH 1/6] refactor: replace direct SQL with WP_Query in redirect_guess_404_permalink() --- src/wp-includes/canonical.php | 59 ++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 9315ba7fb7ff9..4f6c3843b8dc2 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -972,10 +972,35 @@ function redirect_guess_404_permalink() { */ $strict_guess = apply_filters( 'strict_redirect_guess_404_permalink', false ); + // Build WP_Query arguments. + $query_args = array( + 'post_status' => $publicly_viewable_statuses, + 'posts_per_page' => 1, + 'no_found_rows' => true, + 'ignore_sticky_posts' => true, + 'fields' => 'ids', + ); + + // Handle strict vs. loose post_name matching. 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' ) ) . '%' ); + // For loose matching (LIKE), we'll use a posts_where filter. + $post_name_for_filter = get_query_var( 'name' ); + + // Store the filter callback so we can remove it later. + $post_name_where_filter = function ( $where, $query ) use ( $post_name_for_filter, $wpdb ) { + // Only apply to our specific query. + if ( isset( $query->query_vars['redirect_guess_404'] ) && $query->query_vars['redirect_guess_404'] ) { + $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_name LIKE %s", $wpdb->esc_like( $post_name_for_filter ) . '%' ); + } + return $where; + }; + + add_filter( 'posts_where', $post_name_where_filter, 10, 2 ); + + // Mark this query so our filter knows to apply the LIKE clause. + $query_args['redirect_guess_404'] = true; } // If any of post_type, year, monthnum, or day are set, use them to refine the query. @@ -985,34 +1010,46 @@ 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. + $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 ); + } + + // Execute the 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 ) ) . "')" ); + // Clean up the filter if we added it (remove only our specific callback). + if ( ! $strict_guess && isset( $post_name_where_filter ) ) { + remove_filter( 'posts_where', $post_name_where_filter, 10 ); + } - if ( ! $post_id ) { + if ( empty( $query->posts ) ) { return false; } + $post_id = $query->posts[0]; + if ( get_query_var( 'feed' ) ) { return get_post_comments_feed_link( $post_id, get_query_var( 'feed' ) ); } elseif ( get_query_var( 'page' ) > 1 ) { From ce90699085a5573338fcbcc338ef688426b1ee97 Mon Sep 17 00:00:00 2001 From: Anand Rajaram Date: Thu, 20 Nov 2025 14:40:43 +0530 Subject: [PATCH 2/6] perf: optimize wp query and remove superfluous comments --- src/wp-includes/canonical.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 4f6c3843b8dc2..69232861bd904 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -972,13 +972,14 @@ function redirect_guess_404_permalink() { */ $strict_guess = apply_filters( 'strict_redirect_guess_404_permalink', false ); - // Build WP_Query arguments. $query_args = array( - 'post_status' => $publicly_viewable_statuses, - 'posts_per_page' => 1, - 'no_found_rows' => true, - 'ignore_sticky_posts' => true, - 'fields' => 'ids', + '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', ); // Handle strict vs. loose post_name matching. @@ -991,7 +992,7 @@ function redirect_guess_404_permalink() { // Store the filter callback so we can remove it later. $post_name_where_filter = function ( $where, $query ) use ( $post_name_for_filter, $wpdb ) { // Only apply to our specific query. - if ( isset( $query->query_vars['redirect_guess_404'] ) && $query->query_vars['redirect_guess_404'] ) { + if ( isset( $query->query_vars['redirect_guess_404'] ) ) { $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_name LIKE %s", $wpdb->esc_like( $post_name_for_filter ) . '%' ); } return $where; @@ -1036,7 +1037,6 @@ function redirect_guess_404_permalink() { $query_args['date_query'] = array( $date_query ); } - // Execute the query. $query = new WP_Query( $query_args ); // Clean up the filter if we added it (remove only our specific callback). From 9b52804033ee0a8f1c907d2cc5b98dbdc3dd1545 Mon Sep 17 00:00:00 2001 From: Anand Rajaram Date: Fri, 21 Nov 2025 13:24:01 +0530 Subject: [PATCH 3/6] chore: change filter function to a static function --- src/wp-includes/canonical.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 69232861bd904..865e1d15cece9 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -990,7 +990,7 @@ function redirect_guess_404_permalink() { $post_name_for_filter = get_query_var( 'name' ); // Store the filter callback so we can remove it later. - $post_name_where_filter = function ( $where, $query ) use ( $post_name_for_filter, $wpdb ) { + $post_name_where_filter = static function ( $where, $query ) use ( $post_name_for_filter, $wpdb ) { // Only apply to our specific query. if ( isset( $query->query_vars['redirect_guess_404'] ) ) { $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_name LIKE %s", $wpdb->esc_like( $post_name_for_filter ) . '%' ); From 4c87073adeed8643d01d8a5934d7c8c5c00cf889 Mon Sep 17 00:00:00 2001 From: Jonny Harrus Date: Wed, 3 Dec 2025 09:48:44 +0000 Subject: [PATCH 4/6] Tweak WP_Query --- src/wp-includes/canonical.php | 18 ++---------------- src/wp-includes/class-wp-query.php | 10 ++++++---- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 865e1d15cece9..5172291fe27b3 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -986,22 +986,8 @@ function redirect_guess_404_permalink() { if ( $strict_guess ) { $query_args['name'] = get_query_var( 'name' ); } else { - // For loose matching (LIKE), we'll use a posts_where filter. - $post_name_for_filter = get_query_var( 'name' ); - - // Store the filter callback so we can remove it later. - $post_name_where_filter = static function ( $where, $query ) use ( $post_name_for_filter, $wpdb ) { - // Only apply to our specific query. - if ( isset( $query->query_vars['redirect_guess_404'] ) ) { - $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_name LIKE %s", $wpdb->esc_like( $post_name_for_filter ) . '%' ); - } - return $where; - }; - - add_filter( 'posts_where', $post_name_where_filter, 10, 2 ); - - // Mark this query so our filter knows to apply the LIKE clause. - $query_args['redirect_guess_404'] = true; + $query_args['s'] = get_query_var( 's' ); + $query_args['search_columns'] = array( 'post_name' ); } // If any of post_type, year, monthnum, or day are set, use them to refine the query. diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index ba7395f959af0..af7ed8a41ace0 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -774,7 +774,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. @@ -1452,8 +1452,10 @@ protected function parse_search( &$query_vars ) { $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 ); } @@ -1473,7 +1475,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; } From 714d744728cd37cb6f5eca1804dfab76ab7313b0 Mon Sep 17 00:00:00 2001 From: Jonny Harrus Date: Fri, 5 Dec 2025 23:12:43 +0000 Subject: [PATCH 5/6] Tweak `WP_Query` to add `starts_with` parameter and refine 404 redirect logic --- src/wp-includes/canonical.php | 13 +++---------- src/wp-includes/class-wp-query.php | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 5172291fe27b3..dcde0c8c944eb 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -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. * @@ -980,14 +976,16 @@ function redirect_guess_404_permalink() { 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'fields' => 'ids', + 'orderby' => 'none', ); // Handle strict vs. loose post_name matching. if ( $strict_guess ) { $query_args['name'] = get_query_var( 'name' ); } else { - $query_args['s'] = get_query_var( 's' ); + $query_args['s'] = get_query_var( 'name' ); $query_args['search_columns'] = array( 'post_name' ); + $query_args['starts_with'] = true; } // If any of post_type, year, monthnum, or day are set, use them to refine the query. @@ -1025,11 +1023,6 @@ function redirect_guess_404_permalink() { $query = new WP_Query( $query_args ); - // Clean up the filter if we added it (remove only our specific callback). - if ( ! $strict_guess && isset( $post_name_where_filter ) ) { - remove_filter( 'posts_where', $post_name_where_filter, 10 ); - } - if ( empty( $query->posts ) ) { return false; } diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index af7ed8a41ace0..00a90125cdfb4 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -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. * * @param string|array $query { * Optional. Array or string of Query parameters. @@ -686,6 +687,7 @@ 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. + * @type bool $starts_with Whether to search start with keyword. Default false. * @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[]`). @@ -1448,7 +1450,15 @@ 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(); @@ -1502,12 +1512,12 @@ protected function parse_search( &$query_vars ) { $andor_op = 'OR'; } - if ( $n && ! $exclude ) { + if ( $end && ! $exclude ) { $like = '%' . $wpdb->esc_like( $term ) . '%'; $query_vars['search_orderby_title'][] = $wpdb->prepare( "{$wpdb->posts}.post_title LIKE %s", $like ); } - $like = $n . $wpdb->esc_like( $term ) . $n; + $like = $start . $wpdb->esc_like( $term ) . $end; $search_columns_parts = array(); foreach ( $search_columns as $search_column ) { From ff81f8864ce2277b9c2c3596315af4e55942960b Mon Sep 17 00:00:00 2001 From: Jonny Harrus Date: Fri, 5 Dec 2025 23:18:48 +0000 Subject: [PATCH 6/6] Refine `WP_Query` search logic by adjusting `$like` variable handling --- src/wp-includes/class-wp-query.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 00a90125cdfb4..46c5b4e11d6d8 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -1512,13 +1512,11 @@ protected function parse_search( &$query_vars ) { $andor_op = 'OR'; } + $like = $start . $wpdb->esc_like( $term ) . $end; if ( $end && ! $exclude ) { - $like = '%' . $wpdb->esc_like( $term ) . '%'; $query_vars['search_orderby_title'][] = $wpdb->prepare( "{$wpdb->posts}.post_title LIKE %s", $like ); } - $like = $start . $wpdb->esc_like( $term ) . $end; - $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 );