--- 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',
+ ),
+ );
}
/**