--- a/wp/wp-includes/class-wp-query.php Fri Sep 05 18:40:08 2025 +0200
+++ b/wp/wp-includes/class-wp-query.php Fri Sep 05 18:52:52 2025 +0200
@@ -442,6 +442,7 @@
* via pre_get_posts hooks.
*
* @since 3.1.1
+ * @var bool
*/
private $query_vars_changed = true;
@@ -474,6 +475,17 @@
private $compat_methods = array( 'init_query_flags', 'parse_tax_query' );
/**
+ * The cache key generated by the query.
+ *
+ * The cache key is generated by the method ::generate_cache_key() after the
+ * query has been normalized.
+ *
+ * @since 6.8.0
+ * @var string
+ */
+ private $query_cache_key = '';
+
+ /**
* Resets query flags to false.
*
* The query flags are what page info WordPress was able to figure out.
@@ -1100,7 +1112,8 @@
if ( ! empty( $qv['post_type'] ) ) {
if ( is_array( $qv['post_type'] ) ) {
- $qv['post_type'] = array_map( 'sanitize_key', $qv['post_type'] );
+ $qv['post_type'] = array_map( 'sanitize_key', array_unique( $qv['post_type'] ) );
+ sort( $qv['post_type'] );
} else {
$qv['post_type'] = sanitize_key( $qv['post_type'] );
}
@@ -1108,7 +1121,8 @@
if ( ! empty( $qv['post_status'] ) ) {
if ( is_array( $qv['post_status'] ) ) {
- $qv['post_status'] = array_map( 'sanitize_key', $qv['post_status'] );
+ $qv['post_status'] = array_map( 'sanitize_key', array_unique( $qv['post_status'] ) );
+ sort( $qv['post_status'] );
} else {
$qv['post_status'] = preg_replace( '|[^a-z0-9_,-]|', '', $qv['post_status'] );
}
@@ -1181,9 +1195,12 @@
$term = $q[ $t->query_var ];
- if ( is_array( $term ) ) {
- $term = implode( ',', $term );
+ if ( ! is_array( $term ) ) {
+ $term = explode( ',', $term );
+ $term = array_map( 'trim', $term );
}
+ sort( $term );
+ $term = implode( ',', $term );
if ( str_contains( $term, '+' ) ) {
$terms = preg_split( '/[+]+/', $term );
@@ -1219,7 +1236,8 @@
$cat_array = preg_split( '/[,\s]+/', urldecode( $q['cat'] ) );
$cat_array = array_map( 'intval', $cat_array );
- $q['cat'] = implode( ',', $cat_array );
+ sort( $cat_array );
+ $q['cat'] = implode( ',', $cat_array );
foreach ( $cat_array as $cat ) {
if ( $cat > 0 ) {
@@ -1261,7 +1279,8 @@
if ( ! empty( $q['category__in'] ) ) {
$q['category__in'] = array_map( 'absint', array_unique( (array) $q['category__in'] ) );
- $tax_query[] = array(
+ sort( $q['category__in'] );
+ $tax_query[] = array(
'taxonomy' => 'category',
'terms' => $q['category__in'],
'field' => 'term_id',
@@ -1271,7 +1290,8 @@
if ( ! empty( $q['category__not_in'] ) ) {
$q['category__not_in'] = array_map( 'absint', array_unique( (array) $q['category__not_in'] ) );
- $tax_query[] = array(
+ sort( $q['category__not_in'] );
+ $tax_query[] = array(
'taxonomy' => 'category',
'terms' => $q['category__not_in'],
'operator' => 'NOT IN',
@@ -1281,7 +1301,8 @@
if ( ! empty( $q['category__and'] ) ) {
$q['category__and'] = array_map( 'absint', array_unique( (array) $q['category__and'] ) );
- $tax_query[] = array(
+ sort( $q['category__and'] );
+ $tax_query[] = array(
'taxonomy' => 'category',
'terms' => $q['category__and'],
'field' => 'term_id',
@@ -1299,10 +1320,12 @@
if ( '' !== $q['tag'] && ! $this->is_singular && $this->query_vars_changed ) {
if ( str_contains( $q['tag'], ',' ) ) {
+ // @todo Handle normalizing `tag` query string.
$tags = preg_split( '/[,\r\n\t ]+/', $q['tag'] );
foreach ( (array) $tags as $tag ) {
$tag = sanitize_term_field( 'slug', $tag, 0, 'post_tag', 'db' );
$q['tag_slug__in'][] = $tag;
+ sort( $q['tag_slug__in'] );
}
} elseif ( preg_match( '/[+\r\n\t ]+/', $q['tag'] ) || ! empty( $q['cat'] ) ) {
$tags = preg_split( '/[+\r\n\t ]+/', $q['tag'] );
@@ -1313,6 +1336,7 @@
} else {
$q['tag'] = sanitize_term_field( 'slug', $q['tag'], 0, 'post_tag', 'db' );
$q['tag_slug__in'][] = $q['tag'];
+ sort( $q['tag_slug__in'] );
}
}
@@ -1326,7 +1350,8 @@
if ( ! empty( $q['tag__in'] ) ) {
$q['tag__in'] = array_map( 'absint', array_unique( (array) $q['tag__in'] ) );
- $tax_query[] = array(
+ sort( $q['tag__in'] );
+ $tax_query[] = array(
'taxonomy' => 'post_tag',
'terms' => $q['tag__in'],
);
@@ -1334,7 +1359,8 @@
if ( ! empty( $q['tag__not_in'] ) ) {
$q['tag__not_in'] = array_map( 'absint', array_unique( (array) $q['tag__not_in'] ) );
- $tax_query[] = array(
+ sort( $q['tag__not_in'] );
+ $tax_query[] = array(
'taxonomy' => 'post_tag',
'terms' => $q['tag__not_in'],
'operator' => 'NOT IN',
@@ -1343,7 +1369,8 @@
if ( ! empty( $q['tag__and'] ) ) {
$q['tag__and'] = array_map( 'absint', array_unique( (array) $q['tag__and'] ) );
- $tax_query[] = array(
+ sort( $q['tag__and'] );
+ $tax_query[] = array(
'taxonomy' => 'post_tag',
'terms' => $q['tag__and'],
'operator' => 'AND',
@@ -1352,7 +1379,8 @@
if ( ! empty( $q['tag_slug__in'] ) ) {
$q['tag_slug__in'] = array_map( 'sanitize_title_for_query', array_unique( (array) $q['tag_slug__in'] ) );
- $tax_query[] = array(
+ sort( $q['tag_slug__in'] );
+ $tax_query[] = array(
'taxonomy' => 'post_tag',
'terms' => $q['tag_slug__in'],
'field' => 'slug',
@@ -1361,7 +1389,8 @@
if ( ! empty( $q['tag_slug__and'] ) ) {
$q['tag_slug__and'] = array_map( 'sanitize_title_for_query', array_unique( (array) $q['tag_slug__and'] ) );
- $tax_query[] = array(
+ sort( $q['tag_slug__and'] );
+ $tax_query[] = array(
'taxonomy' => 'post_tag',
'terms' => $q['tag_slug__and'],
'field' => 'slug',
@@ -1902,7 +1931,7 @@
// Set a flag if a 'pre_get_posts' hook changed the query vars.
$hash = md5( serialize( $this->query_vars ) );
- if ( $hash != $this->query_vars_hash ) {
+ if ( $hash !== $this->query_vars_hash ) {
$this->query_vars_changed = true;
$this->query_vars_hash = $hash;
}
@@ -2002,10 +2031,11 @@
}
$q['nopaging'] = false;
}
+
$q['posts_per_page'] = (int) $q['posts_per_page'];
if ( $q['posts_per_page'] < -1 ) {
$q['posts_per_page'] = abs( $q['posts_per_page'] );
- } elseif ( 0 == $q['posts_per_page'] ) {
+ } elseif ( 0 === $q['posts_per_page'] ) {
$q['posts_per_page'] = 1;
}
@@ -2037,6 +2067,15 @@
case 'id=>parent':
$fields = "{$wpdb->posts}.ID, {$wpdb->posts}.post_parent";
break;
+ case '':
+ /*
+ * Set the default to 'all'.
+ *
+ * This is used in `WP_Query::the_post` to determine if the
+ * entire post object has been queried.
+ */
+ $q['fields'] = 'all';
+ // Falls through.
default:
$fields = "{$wpdb->posts}.*";
}
@@ -2185,8 +2224,11 @@
$where .= " AND {$wpdb->posts}.post_name = '" . $q['attachment'] . "'";
} elseif ( is_array( $q['post_name__in'] ) && ! empty( $q['post_name__in'] ) ) {
$q['post_name__in'] = array_map( 'sanitize_title_for_query', $q['post_name__in'] );
- $post_name__in = "'" . implode( "','", $q['post_name__in'] ) . "'";
- $where .= " AND {$wpdb->posts}.post_name IN ($post_name__in)";
+ // Duplicate array before sorting to allow for the orderby clause.
+ $post_name__in_for_where = array_unique( $q['post_name__in'] );
+ sort( $post_name__in_for_where );
+ $post_name__in = "'" . implode( "','", $post_name__in_for_where ) . "'";
+ $where .= " AND {$wpdb->posts}.post_name IN ($post_name__in)";
}
// If an attachment is requested by number, let it supersede any post number.
@@ -2198,9 +2240,14 @@
if ( $q['p'] ) {
$where .= " AND {$wpdb->posts}.ID = " . $q['p'];
} elseif ( $q['post__in'] ) {
- $post__in = implode( ',', array_map( 'absint', $q['post__in'] ) );
+ // Duplicate array before sorting to allow for the orderby clause.
+ $post__in_for_where = $q['post__in'];
+ $post__in_for_where = array_unique( array_map( 'absint', $post__in_for_where ) );
+ sort( $post__in_for_where );
+ $post__in = implode( ',', array_map( 'absint', $post__in_for_where ) );
$where .= " AND {$wpdb->posts}.ID IN ($post__in)";
} elseif ( $q['post__not_in'] ) {
+ sort( $q['post__not_in'] );
$post__not_in = implode( ',', array_map( 'absint', $q['post__not_in'] ) );
$where .= " AND {$wpdb->posts}.ID NOT IN ($post__not_in)";
}
@@ -2208,9 +2255,14 @@
if ( is_numeric( $q['post_parent'] ) ) {
$where .= $wpdb->prepare( " AND {$wpdb->posts}.post_parent = %d ", $q['post_parent'] );
} elseif ( $q['post_parent__in'] ) {
- $post_parent__in = implode( ',', array_map( 'absint', $q['post_parent__in'] ) );
+ // Duplicate array before sorting to allow for the orderby clause.
+ $post_parent__in_for_where = $q['post_parent__in'];
+ $post_parent__in_for_where = array_unique( array_map( 'absint', $post_parent__in_for_where ) );
+ sort( $post_parent__in_for_where );
+ $post_parent__in = implode( ',', array_map( 'absint', $post_parent__in_for_where ) );
$where .= " AND {$wpdb->posts}.post_parent IN ($post_parent__in)";
} elseif ( $q['post_parent__not_in'] ) {
+ sort( $q['post_parent__not_in'] );
$post_parent__not_in = implode( ',', array_map( 'absint', $q['post_parent__not_in'] ) );
$where .= " AND {$wpdb->posts}.post_parent NOT IN ($post_parent__not_in)";
}
@@ -2340,6 +2392,7 @@
if ( ! empty( $q['author'] ) && '0' != $q['author'] ) {
$q['author'] = addslashes_gpc( '' . urldecode( $q['author'] ) );
$authors = array_unique( array_map( 'intval', preg_split( '/[,\s]+/', $q['author'] ) ) );
+ sort( $authors );
foreach ( $authors as $author ) {
$key = $author > 0 ? 'author__in' : 'author__not_in';
$q[ $key ][] = abs( $author );
@@ -2348,9 +2401,17 @@
}
if ( ! empty( $q['author__not_in'] ) ) {
- $author__not_in = implode( ',', array_map( 'absint', array_unique( (array) $q['author__not_in'] ) ) );
+ if ( is_array( $q['author__not_in'] ) ) {
+ $q['author__not_in'] = array_unique( array_map( 'absint', $q['author__not_in'] ) );
+ sort( $q['author__not_in'] );
+ }
+ $author__not_in = implode( ',', (array) $q['author__not_in'] );
$where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";
} elseif ( ! empty( $q['author__in'] ) ) {
+ if ( is_array( $q['author__in'] ) ) {
+ $q['author__in'] = array_unique( array_map( 'absint', $q['author__in'] ) );
+ sort( $q['author__in'] );
+ }
$author__in = implode( ',', array_map( 'absint', array_unique( (array) $q['author__in'] ) ) );
$where .= " AND {$wpdb->posts}.post_author IN ($author__in) ";
}
@@ -2587,6 +2648,7 @@
if ( ! is_array( $q_status ) ) {
$q_status = explode( ',', $q_status );
}
+ sort( $q_status );
$r_status = array();
$p_status = array();
$e_status = array();
@@ -3294,7 +3356,7 @@
return $post_parents;
}
- $is_unfiltered_query = $old_request == $this->request && "{$wpdb->posts}.*" === $fields;
+ $is_unfiltered_query = $old_request === $this->request && "{$wpdb->posts}.*" === $fields;
if ( null === $this->posts ) {
$split_the_query = (
@@ -3686,21 +3748,37 @@
global $post;
if ( ! $this->in_the_loop ) {
- // Only prime the post cache for queries limited to the ID field.
- $post_ids = array_filter( $this->posts, 'is_numeric' );
- // Exclude any falsey values, such as 0.
- $post_ids = array_filter( $post_ids );
- if ( $post_ids ) {
+ if ( 'all' === $this->query_vars['fields'] ) {
+ // Full post objects queried.
+ $post_objects = $this->posts;
+ } else {
+ if ( 'ids' === $this->query_vars['fields'] ) {
+ // Post IDs queried.
+ $post_ids = $this->posts;
+ } else {
+ // Only partial objects queried, need to prime the cache for the loop.
+ $post_ids = array_reduce(
+ $this->posts,
+ function ( $carry, $post ) {
+ if ( isset( $post->ID ) ) {
+ $carry[] = $post->ID;
+ }
+
+ return $carry;
+ },
+ array()
+ );
+ }
_prime_post_caches( $post_ids, $this->query_vars['update_post_term_cache'], $this->query_vars['update_post_meta_cache'] );
+ $post_objects = array_map( 'get_post', $post_ids );
}
- $post_objects = array_map( 'get_post', $this->posts );
update_post_author_caches( $post_objects );
}
$this->in_the_loop = true;
$this->before_loop = false;
- if ( -1 == $this->current_post ) { // Loop has just started.
+ if ( -1 === $this->current_post ) { // Loop has just started.
/**
* Fires once the loop is started.
*
@@ -3712,6 +3790,24 @@
}
$post = $this->next_post();
+
+ // Ensure a full post object is available.
+ if ( 'all' !== $this->query_vars['fields'] ) {
+ if ( 'ids' === $this->query_vars['fields'] ) {
+ // Post IDs queried.
+ $post = get_post( $post );
+ } elseif ( isset( $post->ID ) ) {
+ /*
+ * Partial objecct queried.
+ *
+ * The post object was queried with a partial set of
+ * fields, populate the entire object for the loop.
+ */
+ $post = get_post( $post->ID );
+ }
+ }
+
+ // Set up the global post object for the loop.
$this->setup_postdata( $post );
}
@@ -3727,7 +3823,7 @@
public function have_posts() {
if ( $this->current_post + 1 < $this->post_count ) {
return true;
- } elseif ( $this->current_post + 1 == $this->post_count && $this->post_count > 0 ) {
+ } elseif ( $this->current_post + 1 === $this->post_count && $this->post_count > 0 ) {
/**
* Fires once the loop has ended.
*
@@ -3736,6 +3832,7 @@
* @param WP_Query $query The WP_Query instance (passed by reference).
*/
do_action_ref_array( 'loop_end', array( &$this ) );
+
// Do some cleaning up after the loop.
$this->rewind_posts();
} elseif ( 0 === $this->post_count ) {
@@ -3794,7 +3891,7 @@
$comment = $this->next_comment();
- if ( 0 == $this->current_comment ) {
+ if ( 0 === $this->current_comment ) {
/**
* Fires once the comment loop is started.
*
@@ -3816,7 +3913,7 @@
public function have_comments() {
if ( $this->current_comment + 1 < $this->comment_count ) {
return true;
- } elseif ( $this->current_comment + 1 == $this->comment_count ) {
+ } elseif ( $this->current_comment + 1 === $this->comment_count ) {
$this->rewind_comments();
}
@@ -4011,6 +4108,8 @@
if ( in_array( $name, $this->compat_fields, true ) ) {
return isset( $this->$name );
}
+
+ return false;
}
/**
@@ -4479,9 +4578,10 @@
if ( ! strpos( $pagepath, '/' ) ) {
continue;
}
+
$pagepath_obj = get_page_by_path( $pagepath );
- if ( $pagepath_obj && ( $pagepath_obj->ID == $page_obj->ID ) ) {
+ if ( $pagepath_obj && ( $pagepath_obj->ID === $page_obj->ID ) ) {
return true;
}
}
@@ -4589,9 +4689,10 @@
if ( ! strpos( $postpath, '/' ) ) {
continue;
}
+
$postpath_obj = get_page_by_path( $postpath, OBJECT, $post_obj->post_type );
- if ( $postpath_obj && ( $postpath_obj->ID == $post_obj->ID ) ) {
+ if ( $postpath_obj && ( $postpath_obj->ID === $post_obj->ID ) ) {
return true;
}
}
@@ -4899,6 +5000,33 @@
// Sort post types to ensure same cache key generation.
sort( $args['post_type'] );
+ /*
+ * Sort arrays that can be used for ordering prior to cache key generation.
+ *
+ * These arrays are sorted in the query generator for the purposes of the
+ * WHERE clause but the arguments are not modified as they can be used for
+ * the orderby clase.
+ *
+ * Their use in the orderby clause will generate a different SQL query so
+ * they can be sorted for the cache key generation.
+ */
+ $sortable_arrays_with_int_values = array(
+ 'post__in',
+ 'post_parent__in',
+ );
+ foreach ( $sortable_arrays_with_int_values as $key ) {
+ if ( isset( $args[ $key ] ) && is_array( $args[ $key ] ) ) {
+ $args[ $key ] = array_unique( array_map( 'absint', $args[ $key ] ) );
+ sort( $args[ $key ] );
+ }
+ }
+
+ // Sort and unique the 'post_name__in' for cache key generation.
+ if ( isset( $args['post_name__in'] ) && is_array( $args['post_name__in'] ) ) {
+ $args['post_name__in'] = array_unique( $args['post_name__in'] );
+ sort( $args['post_name__in'] );
+ }
+
if ( isset( $args['post_status'] ) ) {
$args['post_status'] = (array) $args['post_status'];
// Sort post status to ensure same cache key generation.
@@ -4939,7 +5067,8 @@
$last_changed .= wp_cache_get_last_changed( 'terms' );
}
- return "wp_query:$key:$last_changed";
+ $this->query_cache_key = "wp_query:$key:$last_changed";
+ return $this->query_cache_key;
}
/**