diff -r 48c4eec2b7e6 -r 8c2e4d02f4ef wp/wp-includes/class-wp-query.php --- 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; } /**