diff -r 48c4eec2b7e6 -r 8c2e4d02f4ef wp/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php --- a/wp/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php Fri Sep 05 18:40:08 2025 +0200 +++ b/wp/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php Fri Sep 05 18:52:52 2025 +0200 @@ -183,7 +183,7 @@ * * @param bool $required Whether the post requires a password check. * @param WP_Post $post The post been password checked. - * @return bool Result of password check taking in to account REST API considerations. + * @return bool Result of password check taking into account REST API considerations. */ public function check_password_required( $required, $post ) { if ( ! $required ) { @@ -247,6 +247,7 @@ 'author_exclude' => 'author__not_in', 'exclude' => 'post__not_in', 'include' => 'post__in', + 'ignore_sticky' => 'ignore_sticky_posts', 'menu_order' => 'menu_order', 'offset' => 'offset', 'order' => 'order', @@ -337,11 +338,88 @@ } } + /* + * Honor the original REST API `post__in` behavior. Don't prepend sticky posts + * when `post__in` has been specified. + */ + if ( ! empty( $args['post__in'] ) ) { + unset( $args['ignore_sticky_posts'] ); + } + + if ( + isset( $registered['search_semantics'], $request['search_semantics'] ) + && 'exact' === $request['search_semantics'] + ) { + $args['exact'] = true; + } + $args = $this->prepare_tax_query( $args, $request ); + if ( isset( $registered['format'], $request['format'] ) ) { + $formats = $request['format']; + /* + * The relation needs to be set to `OR` since the request can contain + * two separate conditions. The user may be querying for items that have + * either the `standard` format or a specific format. + */ + $formats_query = array( 'relation' => 'OR' ); + + /* + * The default post format, `standard`, is not stored in the database. + * If `standard` is part of the request, the query needs to exclude all post items that + * have a format assigned. + */ + if ( in_array( 'standard', $formats, true ) ) { + $formats_query[] = array( + 'taxonomy' => 'post_format', + 'field' => 'slug', + 'operator' => 'NOT EXISTS', + ); + // Remove the `standard` format, since it cannot be queried. + unset( $formats[ array_search( 'standard', $formats, true ) ] ); + } + + // Add any remaining formats to the formats query. + if ( ! empty( $formats ) ) { + // Add the `post-format-` prefix. + $terms = array_map( + static function ( $format ) { + return "post-format-$format"; + }, + $formats + ); + + $formats_query[] = array( + 'taxonomy' => 'post_format', + 'field' => 'slug', + 'terms' => $terms, + 'operator' => 'IN', + ); + } + + // Enable filtering by both post formats and other taxonomies by combining them with `AND`. + if ( isset( $args['tax_query'] ) ) { + $args['tax_query'][] = array( + 'relation' => 'AND', + $formats_query, + ); + } else { + $args['tax_query'] = $formats_query; + } + } + // Force the post_type argument, since it's not a user input variable. $args['post_type'] = $this->post_type; + $is_head_request = $request->is_method( 'HEAD' ); + if ( $is_head_request ) { + // Force the 'fields' argument. For HEAD requests, only post IDs are required to calculate pagination. + $args['fields'] = 'ids'; + // Disable priming post meta for HEAD requests to improve performance. + $args['update_post_term_cache'] = false; + $args['update_post_meta_cache'] = false; + } + /** * Filters WP_Query arguments when querying posts via the REST API. * @@ -374,22 +452,24 @@ add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 ); } - $posts = array(); - - update_post_author_caches( $query_result ); - update_post_parent_caches( $query_result ); - - if ( post_type_supports( $this->post_type, 'thumbnail' ) ) { - update_post_thumbnail_cache( $posts_query ); - } - - foreach ( $query_result as $post ) { - if ( ! $this->check_read_permission( $post ) ) { - continue; + if ( ! $is_head_request ) { + $posts = array(); + + update_post_author_caches( $query_result ); + update_post_parent_caches( $query_result ); + + if ( post_type_supports( $this->post_type, 'thumbnail' ) ) { + update_post_thumbnail_cache( $posts_query ); } - $data = $this->prepare_item_for_response( $post, $request ); - $posts[] = $this->prepare_response_for_collection( $data ); + foreach ( $query_result as $post ) { + if ( ! $this->check_read_permission( $post ) ) { + continue; + } + + $data = $this->prepare_item_for_response( $post, $request ); + $posts[] = $this->prepare_response_for_collection( $data ); + } } // Reset filter. @@ -397,7 +477,7 @@ remove_filter( 'post_password_required', array( $this, 'check_password_required' ) ); } - $page = (int) $query_args['paged']; + $page = isset( $query_args['paged'] ) ? (int) $query_args['paged'] : 0; $total_posts = $posts_query->found_posts; if ( $total_posts < 1 && $page > 1 ) { @@ -419,7 +499,7 @@ ); } - $response = rest_ensure_response( $posts ); + $response = $is_head_request ? new WP_REST_Response( array() ) : rest_ensure_response( $posts ); $response->header( 'X-WP-Total', (int) $total_posts ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); @@ -497,9 +577,9 @@ ); } - if ( $post && ! empty( $request['password'] ) ) { + if ( $post && ! empty( $request->get_query_params()['password'] ) ) { // Check post password, and return error if invalid. - if ( ! hash_equals( $post->post_password, $request['password'] ) ) { + if ( ! hash_equals( $post->post_password, $request->get_query_params()['password'] ) ) { return new WP_Error( 'rest_post_incorrect_password', __( 'Incorrect post password.' ), @@ -1589,6 +1669,8 @@ return $result; } } + + return null; } /** @@ -1762,6 +1844,12 @@ setup_postdata( $post ); + // Don't prepare the response body for HEAD requests. + if ( $request->is_method( 'HEAD' ) ) { + /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ + return apply_filters( "rest_prepare_{$this->post_type}", new WP_REST_Response( array() ), $post, $request ); + } + $fields = $this->get_fields_for_response( $request ); // Base fields for every post. @@ -1809,7 +1897,7 @@ * with the site's timezone offset applied. */ if ( '0000-00-00 00:00:00' === $post->post_modified_gmt ) { - $post_modified_gmt = gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) - ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ) ); + $post_modified_gmt = gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) - (int) ( (float) get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ) ); } else { $post_modified_gmt = $post->post_modified_gmt; } @@ -1844,10 +1932,12 @@ } if ( rest_is_field_included( 'title.rendered', $fields ) ) { add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); + add_filter( 'private_title_format', array( $this, 'protected_title_format' ) ); $data['title']['rendered'] = get_the_title( $post->ID ); remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); + remove_filter( 'private_title_format', array( $this, 'protected_title_format' ) ); } $has_password_filter = false; @@ -2047,15 +2137,15 @@ } /** - * Overwrites the default protected title format. + * Overwrites the default protected and private title format. * - * By default, WordPress will show password protected posts with a title of - * "Protected: %s", as the REST API communicates the protected status of a post - * in a machine readable format, we remove the "Protected: " prefix. + * By default, WordPress will show password protected or private posts with a title of + * "Protected: %s" or "Private: %s", as the REST API communicates the status of a post + * in a machine-readable format, we remove the prefix. * * @since 4.7.0 * - * @return string Protected title format. + * @return string Title format. */ public function protected_title_format() { return '%s'; @@ -2884,6 +2974,12 @@ ); } + $query_params['search_semantics'] = array( + 'description' => __( 'How to interpret the search input.' ), + 'type' => 'string', + 'enum' => array( 'exact' ), + ); + $query_params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.' ), 'type' => 'integer', @@ -2975,6 +3071,24 @@ 'description' => __( 'Limit result set to items that are sticky.' ), 'type' => 'boolean', ); + + $query_params['ignore_sticky'] = array( + 'description' => __( 'Whether to ignore sticky posts or not.' ), + 'type' => 'boolean', + 'default' => true, + ); + } + + if ( post_type_supports( $this->post_type, 'post-formats' ) ) { + $query_params['format'] = array( + 'description' => __( 'Limit result set to items assigned one or more given formats.' ), + 'type' => 'array', + 'uniqueItems' => true, + 'items' => array( + 'enum' => array_values( get_post_format_slugs() ), + 'type' => 'string', + ), + ); } /**