diff --git a/src/wp-includes/rest-api/class-wp-rest-request.php b/src/wp-includes/rest-api/class-wp-rest-request.php index 6ed6ce667432c..3257194e547d2 100644 --- a/src/wp-includes/rest-api/class-wp-rest-request.php +++ b/src/wp-includes/rest-api/class-wp-rest-request.php @@ -161,6 +161,18 @@ public function get_headers() { return $this->headers; } + /** + * Determines if the request is the given method. + * + * @since 6.8.0 + * + * @param string $method HTTP method. + * @return bool Whether the request is of the given method. + */ + public function is_method( $method ) { + return $this->get_method() === strtoupper( $method ); + } + /** * Canonicalizes the header name. * diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php index 5545625df4609..43fd8d3b293aa 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -305,6 +305,10 @@ public function get_items( $request ) { return $parent; } + if ( $request->is_method( 'HEAD' ) ) { + // Return early as this handler doesn't add any response headers. + return new WP_REST_Response(); + } $response = array(); $parent_id = $parent->ID; $revisions = wp_get_post_revisions( $parent_id, array( 'check_enabled' => false ) ); @@ -448,6 +452,11 @@ public function prepare_item_for_response( $item, $request ) { // Restores the more descriptive, specific name for use within this method. $post = $item; + // 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-autosaves-controller.php */ + return apply_filters( 'rest_prepare_autosave', new WP_REST_Response(), $post, $request ); + } $response = $this->revisions_controller->prepare_item_for_response( $post, $request ); $fields = $this->get_fields_for_response( $request ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-block-pattern-categories-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-block-pattern-categories-controller.php index 5d31891678cf7..86ee4cdc5268c 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-block-pattern-categories-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-block-pattern-categories-controller.php @@ -81,6 +81,11 @@ public function get_items_permissions_check( $request ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { + if ( $request->is_method( 'HEAD' ) ) { + // Return early as this handler doesn't add any response headers. + return new WP_REST_Response(); + } + $response = array(); $categories = WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered(); foreach ( $categories as $category ) { diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php index 263f2a8ef2066..f167d72255901 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php @@ -131,6 +131,11 @@ public function get_items_permissions_check( $request ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { + if ( $request->is_method( 'HEAD' ) ) { + // Return early as this handler doesn't add any response headers. + return new WP_REST_Response(); + } + $data = array(); $block_types = $this->block_registry->get_all_registered(); @@ -250,6 +255,12 @@ public function prepare_item_for_response( $item, $request ) { // Restores the more descriptive, specific name for use within this method. $block_type = $item; + // 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-block-types-controller.php */ + return apply_filters( 'rest_prepare_block_type', new WP_REST_Response(), $block_type, $request ); + } + $fields = $this->get_fields_for_response( $request ); $data = array(); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php index c129ef6e130c4..7ecf669803fa4 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php @@ -262,6 +262,14 @@ public function get_items( $request ) { $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 ); } + $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. + $prepared_args['fields'] = 'ids'; + // Disable priming comment meta for HEAD requests to improve performance. + $prepared_args['update_comment_meta_cache'] = false; + } + /** * Filters WP_Comment_Query arguments when querying comments via the REST API. * @@ -277,15 +285,17 @@ public function get_items( $request ) { $query = new WP_Comment_Query(); $query_result = $query->query( $prepared_args ); - $comments = array(); + if ( ! $is_head_request ) { + $comments = array(); - foreach ( $query_result as $comment ) { - if ( ! $this->check_read_permission( $comment, $request ) ) { - continue; - } + foreach ( $query_result as $comment ) { + if ( ! $this->check_read_permission( $comment, $request ) ) { + continue; + } - $data = $this->prepare_item_for_response( $comment, $request ); - $comments[] = $this->prepare_response_for_collection( $data ); + $data = $this->prepare_item_for_response( $comment, $request ); + $comments[] = $this->prepare_response_for_collection( $data ); + } } $total_comments = (int) $query->found_comments; @@ -303,7 +313,7 @@ public function get_items( $request ) { $max_pages = (int) ceil( $total_comments / $request['per_page'] ); } - $response = rest_ensure_response( $comments ); + $response = $is_head_request ? new WP_REST_Response() : rest_ensure_response( $comments ); $response->header( 'X-WP-Total', $total_comments ); $response->header( 'X-WP-TotalPages', $max_pages ); @@ -1041,6 +1051,12 @@ public function prepare_item_for_response( $item, $request ) { // Restores the more descriptive, specific name for use within this method. $comment = $item; + // 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-comments-controller.php */ + return apply_filters( 'rest_prepare_comment', new WP_REST_Response(), $comment, $request ); + } + $fields = $this->get_fields_for_response( $request ); $data = array(); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php index b409af4fc2977..476f1b750fd1e 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php @@ -89,6 +89,8 @@ public function get_items( $request ) { $collections_page = array_slice( $collections_all, ( $page - 1 ) * $per_page, $per_page ); + $is_head_request = $request->is_method( 'HEAD' ); + $items = array(); foreach ( $collections_page as $collection ) { $item = $this->prepare_item_for_response( $collection, $request ); @@ -97,11 +99,21 @@ public function get_items( $request ) { if ( is_wp_error( $item ) ) { continue; } + + /* + * Skip preparing the response body for HEAD requests. + * Cannot exit earlier due to backward compatibility reasons, + * as validation occurs in the prepare_item_for_response method. + */ + if ( $is_head_request ) { + continue; + } + $item = $this->prepare_response_for_collection( $item ); $items[] = $item; } - $response = rest_ensure_response( $items ); + $response = $is_head_request ? new WP_REST_Response() : rest_ensure_response( $items ); $response->header( 'X-WP-Total', (int) $total_items ); $response->header( 'X-WP-TotalPages', $max_pages ); @@ -175,6 +187,15 @@ public function prepare_item_for_response( $item, $request ) { return $collection_data; } + /** + * Don't prepare the response body for HEAD requests. + * Can't exit at the beginning of the method due to the potential need to return a WP_Error object. + */ + if ( $request->is_method( 'HEAD' ) ) { + /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php */ + return apply_filters( 'rest_prepare_font_collection', new WP_REST_Response(), $item, $request ); + } + foreach ( $data_fields as $field ) { if ( rest_is_field_included( $field, $fields ) ) { $data[ $field ] = $collection_data[ $field ]; @@ -182,6 +203,15 @@ public function prepare_item_for_response( $item, $request ) { } } + /** + * Don't prepare the response body for HEAD requests. + * Can't exit at the beginning of the method due to the potential need to return a WP_Error object. + */ + if ( $request->is_method( 'HEAD' ) ) { + /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php */ + return apply_filters( 'rest_prepare_font_collection', new WP_REST_Response(), $item, $request ); + } + $response = rest_ensure_response( $data ); if ( rest_is_field_included( '_links', $fields ) ) { diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-revisions-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-revisions-controller.php index 4108f9711c8a7..e214ab6d39da8 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-revisions-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-revisions-controller.php @@ -163,6 +163,8 @@ public function get_items( $request ) { return $global_styles_config; } + $is_head_request = $request->is_method( 'HEAD' ); + if ( wp_revisions_enabled( $parent ) ) { $registered = $this->get_collection_params(); $query_args = array( @@ -186,6 +188,14 @@ public function get_items( $request ) { } } + if ( $is_head_request ) { + // Force the 'fields' argument. For HEAD requests, only post IDs are required to calculate pagination. + $query_args['fields'] = 'ids'; + // Disable priming post meta for HEAD requests to improve performance. + $query_args['update_post_term_cache'] = false; + $query_args['update_post_meta_cache'] = false; + } + $revisions_query = new WP_Query(); $revisions = $revisions_query->query( $query_args ); $offset = isset( $query_args['offset'] ) ? (int) $query_args['offset'] : 0; @@ -228,14 +238,18 @@ public function get_items( $request ) { $page = (int) $request['page']; } - $response = array(); + if ( ! $is_head_request ) { + $response = array(); - foreach ( $revisions as $revision ) { - $data = $this->prepare_item_for_response( $revision, $request ); - $response[] = $this->prepare_response_for_collection( $data ); - } + foreach ( $revisions as $revision ) { + $data = $this->prepare_item_for_response( $revision, $request ); + $response[] = $this->prepare_response_for_collection( $data ); + } - $response = rest_ensure_response( $response ); + $response = rest_ensure_response( $response ); + } else { + $response = new WP_REST_Response(); + } $response->header( 'X-WP-Total', (int) $total_revisions ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); @@ -275,6 +289,11 @@ public function get_items( $request ) { * @return WP_REST_Response|WP_Error Response object. */ public function prepare_item_for_response( $post, $request ) { + // Don't prepare the response body for HEAD requests. + if ( $request->is_method( 'HEAD' ) ) { + return new WP_REST_Response(); + } + $parent = $this->get_parent( $request['parent'] ); $global_styles_config = $this->get_decoded_global_styles_json( $post->post_content ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php index e31c47ee61a4b..4dda8b345569e 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-pattern-directory-controller.php @@ -161,6 +161,11 @@ public function get_items( $request ) { return $raw_patterns; } + if ( $request->is_method( 'HEAD' ) ) { + // Return early as this handler doesn't add any response headers. + return new WP_REST_Response(); + } + $response = array(); if ( $raw_patterns ) { diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php index d766d5c61ddf8..6ead90af59d38 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php @@ -109,6 +109,11 @@ public function get_items_permissions_check( $request ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { + if ( $request->is_method( 'HEAD' ) ) { + // Return early as this handler doesn't add any response headers. + return new WP_REST_Response(); + } + $data = array(); $types = get_post_types( array( 'show_in_rest' => true ), 'objects' ); @@ -178,6 +183,12 @@ public function prepare_item_for_response( $item, $request ) { // Restores the more descriptive, specific name for use within this method. $post_type = $item; + // 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-post-types-controller.php */ + return apply_filters( 'rest_prepare_post_type', new WP_REST_Response(), $post_type, $request ); + } + $taxonomies = wp_list_filter( get_object_taxonomies( $post_type->name, 'objects' ), array( 'show_in_rest' => true ) ); $taxonomies = wp_list_pluck( $taxonomies, 'name' ); $base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name; diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index 6f872be3dfd78..1367ee0619e4c 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -402,6 +402,15 @@ static function ( $format ) { // 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. * @@ -434,22 +443,24 @@ static function ( $format ) { 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 ( ! $is_head_request ) { + $posts = array(); - if ( post_type_supports( $this->post_type, 'thumbnail' ) ) { - update_post_thumbnail_cache( $posts_query ); - } + update_post_author_caches( $query_result ); + update_post_parent_caches( $query_result ); - foreach ( $query_result as $post ) { - if ( ! $this->check_read_permission( $post ) ) { - continue; + 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. @@ -479,7 +490,7 @@ static function ( $format ) { ); } - $response = rest_ensure_response( $posts ); + $response = $is_head_request ? new WP_REST_Response() : rest_ensure_response( $posts ); $response->header( 'X-WP-Total', (int) $total_posts ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); @@ -1824,6 +1835,12 @@ public function prepare_item_for_response( $item, $request ) { 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(), $post, $request ); + } + $fields = $this->get_fields_for_response( $request ); // Base fields for every post. diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php index 1dbc611631b9b..2756e3d751af1 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php @@ -253,6 +253,8 @@ public function get_items( $request ) { ); } + $is_head_request = $request->is_method( 'HEAD' ); + if ( wp_revisions_enabled( $parent ) ) { $registered = $this->get_collection_params(); $args = array( @@ -287,6 +289,14 @@ public function get_items( $request ) { $args['orderby'] = 'date ID'; } + 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; + } + /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ $args = apply_filters( 'rest_revision_query', $args, $request ); $query_args = $this->prepare_items_query( $args, $request ); @@ -335,14 +345,18 @@ public function get_items( $request ) { $page = (int) $request['page']; } - $response = array(); + if ( ! $is_head_request ) { + $response = array(); - foreach ( $revisions as $revision ) { - $data = $this->prepare_item_for_response( $revision, $request ); - $response[] = $this->prepare_response_for_collection( $data ); - } + foreach ( $revisions as $revision ) { + $data = $this->prepare_item_for_response( $revision, $request ); + $response[] = $this->prepare_response_for_collection( $data ); + } - $response = rest_ensure_response( $response ); + $response = rest_ensure_response( $response ); + } else { + $response = new WP_REST_Response(); + } $response->header( 'X-WP-Total', (int) $total_revisions ); $response->header( 'X-WP-TotalPages', (int) $max_pages ); @@ -574,6 +588,12 @@ public function prepare_item_for_response( $item, $request ) { 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-revisions-controller.php */ + return apply_filters( 'rest_prepare_revision', new WP_REST_Response(), $post, $request ); + } + $fields = $this->get_fields_for_response( $request ); $data = array(); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php index 55fe1ad63ae7a..345a200c67cb8 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php @@ -142,11 +142,14 @@ public function get_items( $request ) { $ids = $result[ WP_REST_Search_Handler::RESULT_IDS ]; - $results = array(); + $is_head_request = $request->is_method( 'HEAD' ); + if ( ! $is_head_request ) { + $results = array(); - foreach ( $ids as $id ) { - $data = $this->prepare_item_for_response( $id, $request ); - $results[] = $this->prepare_response_for_collection( $data ); + foreach ( $ids as $id ) { + $data = $this->prepare_item_for_response( $id, $request ); + $results[] = $this->prepare_response_for_collection( $data ); + } } $total = (int) $result[ WP_REST_Search_Handler::RESULT_TOTAL ]; @@ -162,7 +165,7 @@ public function get_items( $request ) { ); } - $response = rest_ensure_response( $results ); + $response = $is_head_request ? new WP_REST_Response() : rest_ensure_response( $results ); $response->header( 'X-WP-Total', $total ); $response->header( 'X-WP-TotalPages', $max_pages ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-sidebars-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-sidebars-controller.php index 4ff100723757c..95a24cf53e9b9 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-sidebars-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-sidebars-controller.php @@ -119,6 +119,11 @@ public function get_items_permissions_check( $request ) { * @return WP_REST_Response Response object on success. */ public function get_items( $request ) { + if ( $request->is_method( 'HEAD' ) ) { + // Return early as this handler doesn't add any response headers. + return new WP_REST_Response(); + } + $this->retrieve_widgets(); $data = array(); @@ -321,6 +326,12 @@ public function prepare_item_for_response( $item, $request ) { // Restores the more descriptive, specific name for use within this method. $raw_sidebar = $item; + // 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-sidebars-controller.php */ + return apply_filters( 'rest_prepare_sidebar', new WP_REST_Response(), $raw_sidebar, $request ); + } + $id = $raw_sidebar['id']; $sidebar = array( 'id' => $id ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php index 0b15f3494c4b5..b7492c815061b 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php @@ -113,6 +113,10 @@ public function get_items_permissions_check( $request ) { * @return WP_REST_Response Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { + if ( $request->is_method( 'HEAD' ) ) { + // Return early as this handler doesn't add any response headers. + return new WP_REST_Response(); + } // Retrieve the list of registered collection query parameters. $registered = $this->get_collection_params(); @@ -210,6 +214,12 @@ public function prepare_item_for_response( $item, $request ) { // Restores the more descriptive, specific name for use within this method. $taxonomy = $item; + // 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-taxonomies-controller.php */ + return apply_filters( 'rest_prepare_taxonomy', new WP_REST_Response(), $taxonomy, $request ); + } + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; $fields = $this->get_fields_for_response( $request ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-template-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-template-autosaves-controller.php index c996894a5933c..dbdca575089fc 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-template-autosaves-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-template-autosaves-controller.php @@ -175,6 +175,11 @@ public function prepare_item_for_response( $item, $request ) { $template = _build_block_template_result_from_post( $item ); $response = $this->parent_controller->prepare_item_for_response( $template, $request ); + // Don't prepare the response body for HEAD requests. + if ( $request->is_method( 'HEAD' ) ) { + return $response; + } + $fields = $this->get_fields_for_response( $request ); $data = $response->get_data(); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-template-revisions-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-template-revisions-controller.php index c0bc7663e1f8b..f88b60372b038 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-template-revisions-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-template-revisions-controller.php @@ -200,6 +200,11 @@ public function prepare_item_for_response( $item, $request ) { $template = _build_block_template_result_from_post( $item ); $response = $this->parent_controller->prepare_item_for_response( $template, $request ); + // Don't prepare the response body for HEAD requests. + if ( $request->is_method( 'HEAD' ) ) { + return $response; + } + $fields = $this->get_fields_for_response( $request ); $data = $response->get_data(); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php index 267f40e77fd0f..45e0d59d5b982 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php @@ -269,6 +269,11 @@ public function get_items_permissions_check( $request ) { * @return WP_REST_Response */ public function get_items( $request ) { + if ( $request->is_method( 'HEAD' ) ) { + // Return early as this handler doesn't add any response headers. + return new WP_REST_Response(); + } + $query = array(); if ( isset( $request['wp_id'] ) ) { $query['wp_id'] = $request['wp_id']; @@ -668,6 +673,11 @@ protected function prepare_item_for_database( $request ) { * @return WP_REST_Response Response object. */ public function prepare_item_for_response( $item, $request ) { + // Don't prepare the response body for HEAD requests. + if ( $request->is_method( 'HEAD' ) ) { + return new WP_REST_Response(); + } + /* * Resolve pattern blocks so they don't need to be resolved client-side * in the editor, improving performance. diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php index 455c83a68d605..d927ba5e04c34 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php @@ -312,6 +312,14 @@ public function get_items( $request ) { $prepared_args = array_merge( $prepared_args, $taxonomy_obj->args ); } + $is_head_request = $request->is_method( 'HEAD' ); + if ( $is_head_request ) { + // Force the 'fields' argument. For HEAD requests, only term IDs are required. + $prepared_args['fields'] = 'ids'; + // Disable priming term meta for HEAD requests to improve performance. + $prepared_args['update_term_meta_cache'] = false; + } + /** * Filters get_terms() arguments when querying terms via the REST API. * @@ -354,14 +362,15 @@ public function get_items( $request ) { $total_terms = 0; } - $response = array(); - - foreach ( $query_result as $term ) { - $data = $this->prepare_item_for_response( $term, $request ); - $response[] = $this->prepare_response_for_collection( $data ); + if ( ! $is_head_request ) { + $response = array(); + foreach ( $query_result as $term ) { + $data = $this->prepare_item_for_response( $term, $request ); + $response[] = $this->prepare_response_for_collection( $data ); + } } - $response = rest_ensure_response( $response ); + $response = $is_head_request ? new WP_REST_Response() : rest_ensure_response( $response ); // Store pagination values for headers. $per_page = (int) $prepared_args['number']; @@ -887,6 +896,12 @@ public function prepare_item_for_database( $request ) { */ public function prepare_item_for_response( $item, $request ) { + // 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-terms-controller.php */ + return apply_filters( "rest_prepare_{$this->taxonomy}", new WP_REST_Response(), $item, $request ); + } + $fields = $this->get_fields_for_response( $request ); $data = array(); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php index 84bcea052cf32..39705e92aa12a 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php @@ -333,6 +333,12 @@ public function get_items( $request ) { } $prepared_args['search'] = '*' . $prepared_args['search'] . '*'; } + + $is_head_request = $request->is_method( 'HEAD' ); + if ( $is_head_request ) { + // Force the 'fields' argument. For HEAD requests, only user IDs are required. + $prepared_args['fields'] = 'id'; + } /** * Filters WP_User_Query arguments when querying users via the REST API. * @@ -347,14 +353,16 @@ public function get_items( $request ) { $query = new WP_User_Query( $prepared_args ); - $users = array(); + if ( ! $is_head_request ) { + $users = array(); - foreach ( $query->get_results() as $user ) { - $data = $this->prepare_item_for_response( $user, $request ); - $users[] = $this->prepare_response_for_collection( $data ); + foreach ( $query->get_results() as $user ) { + $data = $this->prepare_item_for_response( $user, $request ); + $users[] = $this->prepare_response_for_collection( $data ); + } } - $response = rest_ensure_response( $users ); + $response = $is_head_request ? new WP_REST_Response() : rest_ensure_response( $users ); // Store pagination values for headers then unset for count query. $per_page = (int) $prepared_args['number']; @@ -999,6 +1007,12 @@ public function prepare_item_for_response( $item, $request ) { // Restores the more descriptive, specific name for use within this method. $user = $item; + // 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-users-controller.php */ + return apply_filters( 'rest_prepare_user', new WP_REST_Response(), $user, $request ); + } + $fields = $this->get_fields_for_response( $request ); $data = array(); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-widget-types-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-widget-types-controller.php index d70e524411938..cd0aef0c87916 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-widget-types-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-widget-types-controller.php @@ -145,6 +145,11 @@ public function get_items_permissions_check( $request ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { + if ( $request->is_method( 'HEAD' ) ) { + // Return early as this handler doesn't add any response headers. + return new WP_REST_Response(); + } + $data = array(); foreach ( $this->get_widgets() as $widget ) { $widget_type = $this->prepare_item_for_response( $widget, $request ); @@ -298,6 +303,12 @@ public function prepare_item_for_response( $item, $request ) { // Restores the more descriptive, specific name for use within this method. $widget_type = $item; + // 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-widget-types-controller.php */ + return apply_filters( 'rest_prepare_widget_type', new WP_REST_Response(), $widget_type, $request ); + } + $fields = $this->get_fields_for_response( $request ); $data = array( 'id' => $widget_type['id'], diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php index f2d17186b62d7..2a154693a1164 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php @@ -136,6 +136,11 @@ public function get_items_permissions_check( $request ) { * @return WP_REST_Response Response object. */ public function get_items( $request ) { + if ( $request->is_method( 'HEAD' ) ) { + // Return early as this handler doesn't add any response headers. + return new WP_REST_Response(); + } + $this->retrieve_widgets(); $prepared = array(); @@ -678,6 +683,12 @@ public function prepare_item_for_response( $item, $request ) { } $widget = $wp_registered_widgets[ $widget_id ]; + // 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-widgets-controller.php */ + return apply_filters( 'rest_prepare_widget', new WP_REST_Response(), $widget, $request ); + } + $parsed_id = wp_parse_widget_id( $widget_id ); $fields = $this->get_fields_for_response( $request ); diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php index f5af774d0dbbd..6f95def922f20 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -78,9 +78,13 @@ public function test_get_items() { } /** + * @dataProvider data_readable_http_methods * @covers WP_REST_Font_Collections_Controller::get_items + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_should_only_return_valid_collections() { + public function test_get_items_should_only_return_valid_collections( $method ) { $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' ); wp_set_current_user( self::$admin_id ); @@ -92,14 +96,23 @@ public function test_get_items_should_only_return_valid_collections() { ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections' ); + $request = new WP_REST_Request( $method, '/wp/v2/font-collections' ); $response = rest_get_server()->dispatch( $request ); $content = $response->get_data(); wp_unregister_font_collection( 'invalid-collection' ); $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); - $this->assertCount( 1, $content, 'The response should only contain valid collections.' ); + if ( 'HEAD' !== $method ) { + $this->assertCount( 1, $content, 'The response should only contain valid collections.' ); + return null; + } + + $this->assertNull( $content, 'The response should be empty.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-WP-Total', $headers, 'The "X-WP-Total" header should be present in the response.' ); + // Includes non-valid collections. + $this->assertSame( 2, $headers['X-WP-Total'], 'The "X-WP-Total" header value should be equal to 1.' ); } /** @@ -127,19 +140,75 @@ public function test_get_item() { } /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $hook_name = 'rest_prepare_font_collection'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( $method, '/wp/v2/font-collections/mock-col-slug' ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods * @covers WP_REST_Font_Collections_Controller::get_item + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_item_invalid_slug() { + public function test_get_item_invalid_slug( $method ) { wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/non-existing-collection' ); + $request = new WP_REST_Request( $method, '/wp/v2/font-collections/non-existing-collection' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_font_collection_not_found', $response, 404 ); } /** + * @dataProvider data_readable_http_methods * @covers WP_REST_Font_Collections_Controller::get_item + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_item_invalid_collection() { + public function test_get_item_invalid_collection( $method ) { $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' ); wp_set_current_user( self::$admin_id ); @@ -152,7 +221,7 @@ public function test_get_item_invalid_collection() { ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/' . $slug ); + $request = new WP_REST_Request( $method, '/wp/v2/font-collections/' . $slug ); $response = rest_get_server()->dispatch( $request ); wp_unregister_font_collection( $slug ); @@ -161,10 +230,14 @@ public function test_get_item_invalid_collection() { } /** + * @dataProvider data_readable_http_methods * @covers WP_REST_Font_Collections_Controller::get_item + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_item_invalid_id_permission() { - $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/mock-col-slug' ); + public function test_get_item_invalid_id_permission( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/font-collections/mock-col-slug' ); wp_set_current_user( 0 ); $response = rest_get_server()->dispatch( $request ); diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index c268b74518561..d452d6d1269fe 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -179,9 +179,35 @@ public function test_get_items() { $this->check_get_autosave_response( $data[0], $this->post_autosave ); } - public function test_get_items_no_permission() { + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_autosaves_data() { + $request = new WP_REST_Request( 'HEAD', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + + $hook_name = 'rest_prepare_autosave'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + + $this->assertNotWPError( $response ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/autosaves' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); wp_set_current_user( self::$contributor_id ); @@ -189,16 +215,40 @@ public function test_get_items_no_permission() { $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); } - public function test_get_items_missing_parent() { + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_missing_parent( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/autosaves' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/autosaves' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } - public function test_get_items_invalid_parent_post_type() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_invalid_parent_post_type( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$page_id . '/autosaves' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$page_id . '/autosaves' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } @@ -230,6 +280,43 @@ public function test_get_item() { $this->assertSame( self::$editor_id, $data['author'] ); } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/autosaves/' . self::$autosave_post_id ); + + $hook_name = 'rest_prepare_autosave'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + public function test_get_item_embed_context() { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves/' . self::$autosave_post_id ); @@ -248,23 +335,41 @@ public function test_get_item_embed_context() { $this->assertSameSets( $fields, array_keys( $data ) ); } - public function test_get_item_no_permission() { - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves/' . self::$autosave_post_id ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_no_permission( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/autosaves/' . self::$autosave_post_id ); wp_set_current_user( self::$contributor_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); } - public function test_get_item_missing_parent() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_missing_parent( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/autosaves/' . self::$autosave_post_id ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/autosaves/' . self::$autosave_post_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } - public function test_get_item_invalid_parent_post_type() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_invalid_parent_post_type( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$page_id . '/autosaves' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$page_id . '/autosaves' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } diff --git a/tests/phpunit/tests/rest-api/rest-block-type-controller.php b/tests/phpunit/tests/rest-api/rest-block-type-controller.php index 6ae8954abf1be..e0b0de91d083b 100644 --- a/tests/phpunit/tests/rest-api/rest-block-type-controller.php +++ b/tests/phpunit/tests/rest-api/rest-block-type-controller.php @@ -602,47 +602,128 @@ public function test_get_item_schema() { } /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $block_name = 'fake/test'; + wp_set_current_user( self::$admin_id ); + + $hook_name = 'rest_prepare_block_type'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $request = new WP_REST_Request( $method, '/wp/v2/block-types/' . $block_name ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_block_type_data() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/block-types' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_readable_http_methods * @ticket 47620 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_wrong_permission() { + public function test_get_items_wrong_permission( $method ) { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/block-types' ); + $request = new WP_REST_Request( $method, '/wp/v2/block-types' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_block_type_cannot_view', $response, 403 ); } /** + * @dataProvider data_readable_http_methods * @ticket 47620 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_item_wrong_permission() { + public function test_get_item_wrong_permission( $method ) { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/block-types/fake/test' ); + $request = new WP_REST_Request( $method, '/wp/v2/block-types/fake/test' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_block_type_cannot_view', $response, 403 ); } /** + * @dataProvider data_readable_http_methods * @ticket 47620 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_no_permission() { + public function test_get_items_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/block-types' ); + $request = new WP_REST_Request( $method, '/wp/v2/block-types' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_block_type_cannot_view', $response, 401 ); } /** + * @dataProvider data_readable_http_methods * @ticket 47620 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_item_no_permission() { + public function test_get_item_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/block-types/fake/test' ); + $request = new WP_REST_Request( $method, '/wp/v2/block-types/fake/test' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_block_type_cannot_view', $response, 401 ); } /** + * @dataProvider data_readable_http_methods * @ticket 47620 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ public function test_prepare_item() { $registry = new WP_Block_Type_Registry(); diff --git a/tests/phpunit/tests/rest-api/rest-categories-controller.php b/tests/phpunit/tests/rest-api/rest-categories-controller.php index 9da866a87bc63..ba921b999b866 100644 --- a/tests/phpunit/tests/rest-api/rest-categories-controller.php +++ b/tests/phpunit/tests/rest-api/rest-categories-controller.php @@ -576,8 +576,14 @@ public function test_get_terms_parent_arg() { $this->assertSame( 'Child', $data[0]['name'] ); } - public function test_get_terms_invalid_parent_arg() { - $request = new WP_REST_Request( 'GET', '/wp/v2/categories' ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_terms_invalid_parent_arg( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/categories' ); $request->set_param( 'parent', 'invalid-parent' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); @@ -609,17 +615,25 @@ public function test_get_terms_invalid_taxonomy() { $this->assertErrorResponse( 'rest_no_route', $response, 404 ); } - public function test_get_terms_pagination_headers() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_terms_pagination_headers( $method ) { $total_categories = self::$total_categories; $total_pages = (int) ceil( $total_categories / 10 ); // Start of the index + Uncategorized default term. - $request = new WP_REST_Request( 'GET', '/wp/v2/categories' ); + $request = new WP_REST_Request( $method, '/wp/v2/categories' ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); $this->assertSame( $total_categories, $headers['X-WP-Total'] ); $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); - $this->assertCount( 10, $response->get_data() ); + if ( 'HEAD' !== $method ) { + $this->assertCount( 10, $response->get_data() ); + } $next_link = add_query_arg( array( 'page' => 2, @@ -662,7 +676,9 @@ public function test_get_terms_pagination_headers() { $headers = $response->get_headers(); $this->assertSame( $total_categories, $headers['X-WP-Total'] ); $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); - $this->assertCount( 1, $response->get_data() ); + if ( 'HEAD' !== $method ) { + $this->assertCount( 1, $response->get_data() ); + } $prev_link = add_query_arg( array( 'page' => $total_pages - 1, @@ -679,7 +695,9 @@ public function test_get_terms_pagination_headers() { $headers = $response->get_headers(); $this->assertSame( $total_categories, $headers['X-WP-Total'] ); $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); - $this->assertCount( 0, $response->get_data() ); + if ( 'HEAD' !== $method ) { + $this->assertCount( 0, $response->get_data() ); + } $prev_link = add_query_arg( array( 'page' => $total_pages, @@ -1236,4 +1254,112 @@ protected function check_get_taxonomy_term_response( $response ) { $category = get_term( 1, 'category' ); $this->check_taxonomy_term( $category, $data, $response->get_links() ); } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_only_fetches_ids_for_head_requests( $method ) { + $is_head_request = 'HEAD' === $method; + $request = new WP_REST_Request( $method, '/wp/v2/categories' ); + + $filter = new MockAction(); + + add_filter( 'terms_pre_query', array( $filter, 'filter' ), 10, 2 ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + if ( $is_head_request ) { + $this->assertEmpty( $response->get_data() ); + } else { + $this->assertNotEmpty( $response->get_data() ); + } + + $args = $filter->get_args(); + $this->assertTrue( isset( $args[0][1] ), 'Query parameters were not captured.' ); + $this->assertInstanceOf( WP_Term_Query::class, $args[0][1], 'Query parameters were not captured.' ); + + /** @var WP_Term_Query $query */ + $query = $args[0][1]; + + if ( $is_head_request ) { + $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' ); + $this->assertSame( 'ids', $query->query_vars['fields'], 'The query must fetch only term IDs.' ); + $this->assertArrayHasKey( 'update_term_meta_cache', $query->query_vars, 'The update_term_meta_cache key is missing in the query vars.' ); + $this->assertFalse( $query->query_vars['update_term_meta_cache'], 'The update_term_meta_cache value should be false for HEAD requests.' ); + } else { + $this->assertTrue( + ! array_key_exists( 'fields', $query->query_vars ) || 'ids' !== $query->query_vars['fields'], + 'The fields parameter should not be forced to "ids" for non-HEAD requests.' + ); + $this->assertArrayHasKey( 'update_term_meta_cache', $query->query_vars, 'The update_term_meta_cache key is missing in the query vars.' ); + $this->assertTrue( $query->query_vars['update_term_meta_cache'], 'The update_term_meta_cache value should be true for HEAD requests.' ); + } + + if ( ! $is_head_request ) { + return; + } + + global $wpdb; + $terms_table = preg_quote( $wpdb->terms, '/' ); + + $pattern = '/SELECT\s+t\.term_id.+FROM\s+' . $terms_table . '\s+AS\s+t\s+INNER\s+JOIN/is'; + + // Assert that the SQL query only fetches the term_id column. + $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $category_id = self::factory()->category->create(); + + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/categories/%d', $category_id ) ); + + $hook_name = 'rest_prepare_category'; + + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-comments-controller.php b/tests/phpunit/tests/rest-api/rest-comments-controller.php index ee42906c61796..3de15cfa76080 100644 --- a/tests/phpunit/tests/rest-api/rest-comments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-comments-controller.php @@ -432,19 +432,43 @@ public function test_get_items_no_post() { $this->assertCount( 2, $comments ); } - public function test_get_items_no_permission_for_no_post() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_no_permission_for_no_post( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request = new WP_REST_Request( $method, '/wp/v2/comments' ); $request->set_param( 'post', 0 ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); } - public function test_get_items_edit_context() { + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_edit_context( $method ) { wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request = new WP_REST_Request( $method, '/wp/v2/comments' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); @@ -593,12 +617,18 @@ public function test_get_items_order_query() { $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } - public function test_get_items_private_post_no_permissions() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_private_post_no_permissions( $method ) { wp_set_current_user( 0 ); $post_id = self::factory()->post->create( array( 'post_status' => 'private' ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request = new WP_REST_Request( $method, '/wp/v2/comments' ); $request->set_param( 'post', $post_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read_post', $response, 401 ); @@ -801,14 +831,20 @@ public function test_get_items_search_query() { $this->assertSame( $id, $data[0]['id'] ); } - public function test_get_comments_pagination_headers() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comments_pagination_headers( $method ) { $total_comments = self::$total_comments; $total_pages = (int) ceil( $total_comments / 10 ); wp_set_current_user( self::$admin_id ); // Start of the index. - $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request = new WP_REST_Request( $method, '/wp/v2/comments' ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); $this->assertSame( $total_comments, $headers['X-WP-Total'] ); @@ -884,8 +920,14 @@ public function test_get_comments_pagination_headers() { $this->assertStringNotContainsString( 'rel="next"', $headers['Link'] ); } - public function test_get_comments_invalid_date() { - $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comments_invalid_date( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/comments' ); $request->set_param( 'after', 'foo' ); $request->set_param( 'before', 'bar' ); $response = rest_get_server()->dispatch( $request ); @@ -997,23 +1039,41 @@ public function test_get_comment_author_avatar_urls() { $this->assertSame( substr( get_avatar_url( $comment->comment_author_email ), 9 ), substr( $data['author_avatar_urls'][96], 9 ) ); } - public function test_get_comment_invalid_id() { - $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_invalid_id( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/comments/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_comment_invalid_id', $response, 404 ); } - public function test_get_comment_invalid_context() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_invalid_context( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', self::$approved_id ) ); + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%s', self::$approved_id ) ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 ); } - public function test_get_comment_invalid_post_id() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_invalid_post_id( $method ) { wp_set_current_user( 0 ); $comment_id = self::factory()->comment->create( @@ -1023,12 +1083,18 @@ public function test_get_comment_invalid_post_id() { ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id ); + $request = new WP_REST_Request( $method, '/wp/v2/comments/' . $comment_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); } - public function test_get_comment_invalid_post_id_as_admin() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_invalid_post_id_as_admin( $method ) { wp_set_current_user( self::$admin_id ); $comment_id = self::factory()->comment->create( @@ -1038,23 +1104,35 @@ public function test_get_comment_invalid_post_id_as_admin() { ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id ); + $request = new WP_REST_Request( $method, '/wp/v2/comments/' . $comment_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); } - public function test_get_comment_not_approved() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_not_approved( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$hold_id ) ); + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%d', self::$hold_id ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); } - public function test_get_comment_not_approved_same_user() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_not_approved_same_user( $method ) { wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$hold_id ) ); + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%d', self::$hold_id ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); } @@ -1098,7 +1176,13 @@ public function test_get_comment_without_children_link() { $this->assertArrayNotHasKey( 'children', $response->get_links() ); } - public function test_get_comment_with_password_without_edit_post_permission() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_with_password_without_edit_post_permission( $method ) { wp_set_current_user( self::$subscriber_id ); $args = array( @@ -1108,15 +1192,19 @@ public function test_get_comment_with_password_without_edit_post_permission() { $password_comment = self::factory()->comment->create( $args ); - $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $password_comment ) ); + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%s', $password_comment ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); } /** + * @dataProvider data_readable_http_methods * @ticket 38692 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_comment_with_password_with_valid_password() { + public function test_get_comment_with_password_with_valid_password( $method ) { wp_set_current_user( self::$subscriber_id ); $args = array( @@ -1126,7 +1214,7 @@ public function test_get_comment_with_password_with_valid_password() { $password_comment = self::factory()->comment->create( $args ); - $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $password_comment ) ); + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%s', $password_comment ) ); $request->set_param( 'password', 'toomanysecrets' ); $response = rest_get_server()->dispatch( $request ); @@ -3365,9 +3453,13 @@ protected function check_comment_data( $data, $context, $links ) { } /** + * @dataProvider data_readable_http_methods * @ticket 42238 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_check_read_post_permission_with_invalid_post_type() { + public function test_check_read_post_permission_with_invalid_post_type( $method ) { register_post_type( 'bug-post', array( @@ -3386,8 +3478,95 @@ public function test_check_read_post_permission_with_invalid_post_type() { $this->setExpectedIncorrectUsage( 'map_meta_cap' ); wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id ); + $request = new WP_REST_Request( $method, '/wp/v2/comments/' . $comment_id ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 403, $response->get_status() ); } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_only_fetches_ids_for_head_requests( $method ) { + $is_head_request = 'HEAD' === $method; + $request = new WP_REST_Request( $method, '/wp/v2/comments' ); + + $filter = new MockAction(); + + add_filter( 'comments_pre_query', array( $filter, 'filter' ), 10, 2 ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + if ( $is_head_request ) { + $this->assertEmpty( $response->get_data() ); + } else { + $this->assertNotEmpty( $response->get_data() ); + } + + $args = $filter->get_args(); + $this->assertTrue( isset( $args[0][1] ), 'Query parameters were not captured.' ); + $this->assertInstanceOf( WP_Comment_Query::class, $args[0][1], 'Query parameters were not captured.' ); + + /** @var WP_Comment_Query $query */ + $query = $args[0][1]; + + if ( $is_head_request ) { + $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' ); + $this->assertSame( 'ids', $query->query_vars['fields'], 'The query must fetch only post IDs.' ); + $this->assertArrayHasKey( 'update_comment_meta_cache', $query->query_vars, 'The update_comment_meta_cache key is missing in the query vars.' ); + $this->assertFalse( $query->query_vars['update_comment_meta_cache'], 'The update_comment_meta_cache value should be false for HEAD requests.' ); + } else { + $this->assertTrue( ! array_key_exists( 'fields', $query->query_vars ) || 'ids' !== $query->query_vars['fields'], 'The fields parameter should not be forced to "ids" for non-HEAD requests.' ); + $this->assertArrayHasKey( 'update_comment_meta_cache', $query->query_vars, 'The update_comment_meta_cache key is missing in the query vars.' ); + $this->assertTrue( $query->query_vars['update_comment_meta_cache'], 'The update_comment_meta_cache value should be true for non-HEAD requests.' ); + return; + } + + global $wpdb; + $comments_table = preg_quote( $wpdb->comments, '/' ); + $pattern = '/^SELECT\s+SQL_CALC_FOUND_ROWS\s+' . $comments_table . '\.comment_ID\s+FROM\s+' . $comments_table . '\s+WHERE/i'; + + // Assert that the SQL query only fetches the ID column. + $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); + + $hook_name = 'rest_prepare_comment'; + + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $headers = $response->get_headers(); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-global-styles-revisions-controller.php b/tests/phpunit/tests/rest-api/rest-global-styles-revisions-controller.php index 5fd97a7a71abf..c57d16da5e1ca 100644 --- a/tests/phpunit/tests/rest-api/rest-global-styles-revisions-controller.php +++ b/tests/phpunit/tests/rest-api/rest-global-styles-revisions-controller.php @@ -245,17 +245,33 @@ public function test_register_routes() { } /** + * @dataProvider data_readable_http_methods * @ticket 58524 + * @ticket 56481 * * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. */ - public function test_get_items_missing_parent() { + public function test_get_items_missing_parent( $method ) { wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + /** * Utility function to check the items in WP_REST_Global_Styles_Controller::get_items * against the expected values. @@ -310,6 +326,19 @@ public function test_get_items() { $this->check_get_revision_response( $data[2], $this->revision_1 ); } + /** + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::prepare_item_for_response + */ + public function test_get_items_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + /** * @ticket 59810 * @@ -327,16 +356,34 @@ public function test_get_item() { } /** + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_item + * @covers WP_REST_Global_Styles_Controller::prepare_item_for_response + */ + public function test_get_item_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions/' . $this->revision_1_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_readable_http_methods * @ticket 59810 + * @ticket 56481 * * @covers WP_REST_Global_Styles_Controller::get_revision + * + * @param string $method The HTTP method to use. */ - public function test_get_item_invalid_revision_id_should_error() { + public function test_get_item_invalid_revision_id_should_error( $method ) { wp_set_current_user( self::$admin_id ); $expected_error = 'rest_post_invalid_id'; $expected_status = 404; - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions/20000001' ); + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions/20000001' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( $expected_error, $response, $expected_status ); @@ -419,14 +466,18 @@ public function test_get_item_schema() { } /** + * @dataProvider data_readable_http_methods * @ticket 58524 * @ticket 60131 + * @ticket 56481 * * @covers WP_REST_Global_Styles_Controller::get_item_permissions_check + * + * @param string $method The HTTP method to use. */ - public function test_get_item_permissions_check() { + public function test_get_item_permissions_check( $method ) { wp_set_current_user( self::$author_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); @@ -435,13 +486,15 @@ public function test_get_item_permissions_check() { /** * Tests the pagination header of the first page. * - * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_pagination_header_of_the_first_page - * + * @dataProvider data_readable_http_methods * @ticket 58524 + * @ticket 56481 * * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. */ - public function test_get_items_pagination_header_of_the_first_page() { + public function test_get_items_pagination_header_of_the_first_page( $method ) { wp_set_current_user( self::$admin_id ); $rest_route = '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions'; @@ -449,7 +502,7 @@ public function test_get_items_pagination_header_of_the_first_page() { $total_pages = (int) ceil( $this->total_revisions / $per_page ); $page = 1; // First page. - $request = new WP_REST_Request( 'GET', $rest_route ); + $request = new WP_REST_Request( $method, $rest_route ); $request->set_query_params( array( 'per_page' => $per_page, @@ -476,11 +529,15 @@ public function test_get_items_pagination_header_of_the_first_page() { * * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_pagination_header_of_the_last_page * + * @dataProvider data_readable_http_methods * @ticket 58524 + * @ticket 56481 * * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. */ - public function test_get_items_pagination_header_of_the_last_page() { + public function test_get_items_pagination_header_of_the_last_page( $method ) { wp_set_current_user( self::$admin_id ); $rest_route = '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions'; @@ -488,7 +545,7 @@ public function test_get_items_pagination_header_of_the_last_page() { $total_pages = (int) ceil( $this->total_revisions / $per_page ); $page = 2; // Last page. - $request = new WP_REST_Request( 'GET', $rest_route ); + $request = new WP_REST_Request( $method, $rest_route ); $request->set_query_params( array( 'per_page' => $per_page, @@ -514,18 +571,22 @@ public function test_get_items_pagination_header_of_the_last_page() { * * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_invalid_per_page_should_error * + * @dataProvider data_readable_http_methods * @ticket 58524 + * @ticket 56481 * * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. */ - public function test_get_items_invalid_per_page_should_error() { + public function test_get_items_invalid_per_page_should_error( $method ) { wp_set_current_user( self::$admin_id ); $per_page = -1; // Invalid number. $expected_error = 'rest_invalid_param'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); $request->set_param( 'per_page', $per_page ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( $expected_error, $response, $expected_status ); @@ -536,11 +597,15 @@ public function test_get_items_invalid_per_page_should_error() { * * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_out_of_bounds_page_should_error * + * @dataProvider data_readable_http_methods * @ticket 58524 + * @ticket 56481 * * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. */ - public function test_get_items_out_of_bounds_page_should_error() { + public function test_get_items_out_of_bounds_page_should_error( $method ) { wp_set_current_user( self::$admin_id ); $per_page = 2; @@ -549,7 +614,7 @@ public function test_get_items_out_of_bounds_page_should_error() { $expected_error = 'rest_revision_invalid_page_number'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); $request->set_query_params( array( 'per_page' => $per_page, @@ -565,11 +630,15 @@ public function test_get_items_out_of_bounds_page_should_error() { * * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_invalid_max_pages_should_error * + * @dataProvider data_readable_http_methods * @ticket 58524 + * @ticket 56481 * * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. */ - public function test_get_items_invalid_max_pages_should_error() { + public function test_get_items_invalid_max_pages_should_error( $method ) { wp_set_current_user( self::$admin_id ); $per_page = 2; @@ -577,7 +646,7 @@ public function test_get_items_invalid_max_pages_should_error() { $expected_error = 'rest_revision_invalid_page_number'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); $request->set_query_params( array( 'per_page' => $per_page, @@ -689,11 +758,15 @@ public function test_get_items_offset_should_take_priority_over_page() { * * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_total_revisions_offset_should_return_empty_data * + * @dataProvider data_readable_http_methods * @ticket 58524 + * @ticket 56481 * * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. */ - public function test_get_items_total_revisions_offset_should_return_empty_data() { + public function test_get_items_total_revisions_offset_should_return_empty_data( $method ) { wp_set_current_user( self::$admin_id ); $per_page = 2; @@ -701,7 +774,7 @@ public function test_get_items_total_revisions_offset_should_return_empty_data() $expected_error = 'rest_revision_invalid_offset_number'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); $request->set_query_params( array( 'offset' => $offset, @@ -717,11 +790,15 @@ public function test_get_items_total_revisions_offset_should_return_empty_data() * * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_out_of_bound_offset_should_error * + * @dataProvider data_readable_http_methods * @ticket 58524 + * @ticket 56481 * * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. */ - public function test_get_items_out_of_bound_offset_should_error() { + public function test_get_items_out_of_bound_offset_should_error( $method ) { wp_set_current_user( self::$admin_id ); $per_page = 2; @@ -729,7 +806,7 @@ public function test_get_items_out_of_bound_offset_should_error() { $expected_error = 'rest_revision_invalid_offset_number'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); $request->set_query_params( array( 'offset' => $offset, @@ -745,11 +822,15 @@ public function test_get_items_out_of_bound_offset_should_error() { * * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_impossible_high_number_offset_should_error * + * @dataProvider data_readable_http_methods * @ticket 58524 + * @ticket 56481 * * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. */ - public function test_get_items_impossible_high_number_offset_should_error() { + public function test_get_items_impossible_high_number_offset_should_error( $method ) { wp_set_current_user( self::$admin_id ); $per_page = 2; @@ -757,7 +838,7 @@ public function test_get_items_impossible_high_number_offset_should_error() { $expected_error = 'rest_revision_invalid_offset_number'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); $request->set_query_params( array( 'offset' => $offset, @@ -773,11 +854,15 @@ public function test_get_items_impossible_high_number_offset_should_error() { * * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_invalid_offset_should_error * + * @dataProvider data_readable_http_methods * @ticket 58524 + * @ticket 56481 * * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. */ - public function test_get_items_invalid_offset_should_error() { + public function test_get_items_invalid_offset_should_error( $method ) { wp_set_current_user( self::$admin_id ); $per_page = 2; @@ -785,7 +870,7 @@ public function test_get_items_invalid_offset_should_error() { $expected_error = 'rest_invalid_param'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); $request->set_query_params( array( 'offset' => $offset, diff --git a/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php b/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php index eda42beebe25f..23a7931511c65 100644 --- a/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php +++ b/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php @@ -144,6 +144,32 @@ public function test_get_items() { $this->assertSame( array( 'call to action', 'hero section' ), $patterns[2]['keywords'] ); } + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_block_patterns_data() { + wp_set_current_user( self::$contributor_id ); + self::mock_successful_response( 'browse-all', true ); + + $request = new WP_REST_Request( 'HEAD', '/wp/v2/pattern-directory/patterns' ); + + $hook_name = 'rest_prepare_block_pattern'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + + $this->assertNotWPError( $response ); + $response = rest_ensure_response( $response ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + /** * @covers WP_REST_Pattern_Directory_Controller::get_items * @@ -219,27 +245,49 @@ public function test_get_items_search() { } /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * * @covers WP_REST_Pattern_Directory_Controller::get_items * * @since 5.8.0 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_wdotorg_unavailable() { + public function test_get_items_wdotorg_unavailable( $method ) { wp_set_current_user( self::$contributor_id ); self::prevent_requests_to_host( 'api.wordpress.org' ); - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $request = new WP_REST_Request( $method, '/wp/v2/pattern-directory/patterns' ); $response = rest_do_request( $request ); $this->assertErrorResponse( 'patterns_api_failed', $response, 500 ); } /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * * @covers WP_REST_Pattern_Directory_Controller::get_items * * @since 5.8.0 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_logged_out() { - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + public function test_get_items_logged_out( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/pattern-directory/patterns' ); $request->set_query_params( array( 'search' => 'button' ) ); $response = rest_do_request( $request ); @@ -283,15 +331,20 @@ public function test_get_items_search_no_results() { } /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * * @covers WP_REST_Pattern_Directory_Controller::get_items * * @since 5.8.0 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_invalid_response_data() { + public function test_get_items_invalid_response_data( $method ) { wp_set_current_user( self::$contributor_id ); self::mock_successful_response( 'invalid-data', true ); - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $request = new WP_REST_Request( $method, '/wp/v2/pattern-directory/patterns' ); $response = rest_do_request( $request ); $this->assertSame( 500, $response->status ); diff --git a/tests/phpunit/tests/rest-api/rest-post-types-controller.php b/tests/phpunit/tests/rest-api/rest-post-types-controller.php index 8967a507063a1..3b61e0d6a9fc3 100644 --- a/tests/phpunit/tests/rest-api/rest-post-types-controller.php +++ b/tests/phpunit/tests/rest-api/rest-post-types-controller.php @@ -44,14 +44,32 @@ public function test_get_items() { $this->assertArrayNotHasKey( 'revision', $data ); } - public function test_get_items_invalid_permission_for_context() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_invalid_permission_for_context( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/types' ); + $request = new WP_REST_Request( $method, '/wp/v2/types' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_view', $response, 401 ); } + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + public function test_get_item() { $request = new WP_REST_Request( 'GET', '/wp/v2/types/post' ); $response = rest_get_server()->dispatch( $request ); @@ -60,6 +78,42 @@ public function test_get_item() { $this->assertSame( array( 'category', 'post_tag' ), $data['taxonomies'] ); } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/types/post' ); + + $hook_name = 'rest_prepare_post_type'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + /** * @ticket 53656 */ @@ -106,8 +160,14 @@ public function test_get_item_page() { $this->assertSame( array(), $data['taxonomies'] ); } - public function test_get_item_invalid_type() { - $request = new WP_REST_Request( 'GET', '/wp/v2/types/invalid' ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_item_invalid_type( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/types/invalid' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_type_invalid', $response, 404 ); } @@ -121,9 +181,15 @@ public function test_get_item_edit_context() { $this->check_post_type_object_response( 'edit', $response ); } - public function test_get_item_invalid_permission_for_context() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_item_invalid_permission_for_context( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/types/post' ); + $request = new WP_REST_Request( $method, '/wp/v2/types/post' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 ); @@ -245,6 +311,22 @@ public function additional_field_get_callback( $response_data ) { return 123; } + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_post_types_data() { + $request = new WP_REST_Request( 'HEAD', '/wp/v2/types' ); + $hook_name = 'rest_prepare_post_type'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + protected function check_post_type_obj( $context, $post_type_obj, $data, $links ) { $this->assertSame( $post_type_obj->label, $data['name'] ); $this->assertSame( $post_type_obj->name, $data['slug'] ); diff --git a/tests/phpunit/tests/rest-api/rest-posts-controller.php b/tests/phpunit/tests/rest-api/rest-posts-controller.php index a1b8de0474359..53b196f5b88e8 100644 --- a/tests/phpunit/tests/rest-api/rest-posts-controller.php +++ b/tests/phpunit/tests/rest-api/rest-posts-controller.php @@ -272,13 +272,45 @@ public function test_get_items() { $this->check_get_posts_response( $response ); } + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_post_data() { + $request = new WP_REST_Request( 'HEAD', '/wp/v2/posts' ); + + $hook_name = 'rest_prepare_post'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + + $this->assertNotWPError( $response ); + $response = rest_ensure_response( $response ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + + $headers = $response->get_headers(); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertArrayHasKey( 'Link', $headers, 'The "Link" header should be present in the response.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + /** * A valid query that returns 0 results should return an empty JSON list. + * In case of a HEAD request, the response should not contain a body. * + * @dataProvider data_readable_http_methods * @link https://github.com/WP-API/WP-API/issues/862 + * @ticket 56481 + * + * @covers WP_REST_Posts_Controller::get_items + * + * @param string $method The HTTP method to use. */ - public function test_get_items_empty_query() { - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + public function test_get_items_empty_query( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_query_params( array( 'author' => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER, @@ -286,85 +318,147 @@ public function test_get_items_empty_query() { ); $response = rest_get_server()->dispatch( $request ); - $this->assertEmpty( $response->get_data() ); - $this->assertSame( 200, $response->get_status() ); + if ( $request->is_method( 'HEAD' ) ) { + $this->assertNull( $response->get_data(), 'Failed asserting that response data is null for HEAD request.' ); + } else { + $this->assertSame( array(), $response->get_data(), 'Failed asserting that response data is an empty array for GET request.' ); + } + + $headers = $response->get_headers(); + $this->assertSame( 0, $headers['X-WP-Total'], 'Failed asserting that X-WP-Total header is 0.' ); + $this->assertSame( 0, $headers['X-WP-TotalPages'], 'Failed asserting that X-WP-TotalPages header is 0.' ); } - public function test_get_items_author_query() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_author_query( $method ) { self::factory()->post->create( array( 'post_author' => self::$editor_id ) ); self::factory()->post->create( array( 'post_author' => self::$author_id ) ); $total_posts = self::$total_posts + 2; // All posts in the database. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'per_page', self::$per_page ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); - $this->assertCount( $total_posts, $response->get_data() ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( $total_posts, $response->get_data() ); + + } else { + $this->assertNull( $response->get_data(), 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts, $headers['X-WP-Total'] ); + } // Limit to editor and author. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'author', array( self::$editor_id, self::$author_id ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertCount( 2, $data ); - $this->assertSameSets( array( self::$editor_id, self::$author_id ), wp_list_pluck( $data, 'author' ) ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( 2, $data ); + $this->assertSameSets( array( self::$editor_id, self::$author_id ), wp_list_pluck( $data, 'author' ) ); + } else { + $this->assertNull( $data, 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( 2, $headers['X-WP-Total'], 'Failed asserting that X-WP-Total header is 2.' ); + } // Limit to editor. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'author', self::$editor_id ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertCount( 1, $data ); - $this->assertSame( self::$editor_id, $data[0]['author'] ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( 1, $data ); + $this->assertSame( self::$editor_id, $data[0]['author'] ); + } else { + $this->assertNull( $data, 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( 1, $headers['X-WP-Total'], 'Failed asserting that X-WP-Total header is 1.' ); + } } - public function test_get_items_author_exclude_query() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_author_exclude_query( $method ) { self::factory()->post->create( array( 'post_author' => self::$editor_id ) ); self::factory()->post->create( array( 'post_author' => self::$author_id ) ); $total_posts = self::$total_posts + 2; // All posts in the database. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'per_page', self::$per_page ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); - $this->assertCount( $total_posts, $response->get_data() ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( $total_posts, $response->get_data() ); + } else { + $this->assertNull( $response->get_data(), 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts, $headers['X-WP-Total'], 'Failed asserting that the number of posts is correct.' ); + } // Exclude editor and author. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'per_page', self::$per_page ); $request->set_param( 'author_exclude', array( self::$editor_id, self::$author_id ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertCount( $total_posts - 2, $data ); - $this->assertNotEquals( self::$editor_id, $data[0]['author'] ); - $this->assertNotEquals( self::$author_id, $data[0]['author'] ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( $total_posts - 2, $data ); + $this->assertNotEquals( self::$editor_id, $data[0]['author'] ); + $this->assertNotEquals( self::$author_id, $data[0]['author'] ); + } else { + $this->assertNull( $response->get_data(), 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts - 2, $headers['X-WP-Total'], 'Failed asserting that the number of posts is correct.' ); + } // Exclude editor. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'per_page', self::$per_page ); $request->set_param( 'author_exclude', self::$editor_id ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertCount( $total_posts - 1, $data ); - $this->assertNotEquals( self::$editor_id, $data[0]['author'] ); - $this->assertNotEquals( self::$editor_id, $data[1]['author'] ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( $total_posts - 1, $data ); + $this->assertNotEquals( self::$editor_id, $data[0]['author'] ); + $this->assertNotEquals( self::$editor_id, $data[1]['author'] ); + } else { + $this->assertNull( $response->get_data(), 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts - 1, $headers['X-WP-Total'], 'Failed asserting that the number of posts is correct.' ); + } // Invalid 'author_exclude' should error. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'author_exclude', 'invalid' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } - public function test_get_items_include_query() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_include_query( $method ) { $id1 = self::factory()->post->create( array( 'post_status' => 'publish', @@ -378,26 +472,40 @@ public function test_get_items_include_query() { ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); // Order defaults to date descending. $request->set_param( 'include', array( $id1, $id2 ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertCount( 2, $data ); - $this->assertSame( $id2, $data[0]['id'] ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( 2, $data ); + $this->assertSame( $id2, $data[0]['id'] ); + } else { + $this->assertNull( $data, 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( 2, $headers['X-WP-Total'], 'Failed asserting that the number of posts is correct.' ); + } + $this->assertPostsOrderedBy( '{posts}.post_date DESC' ); // 'orderby' => 'include'. $request->set_param( 'orderby', 'include' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertCount( 2, $data ); - $this->assertSame( $id1, $data[0]['id'] ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( 2, $data ); + $this->assertSame( $id1, $data[0]['id'] ); + } else { + $this->assertNull( $data, 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( 2, $headers['X-WP-Total'], 'Failed asserting that the number of posts is correct.' ); + } + $this->assertPostsOrderedBy( "FIELD({posts}.ID,$id1,$id2)" ); // Invalid 'include' should error. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'include', 'invalid' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); @@ -1728,12 +1836,18 @@ public function test_get_items_primes_parent_post_caches() { $this->assertSameSets( $parent_ids, $primed[1] ); } - public function test_get_items_pagination_headers() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_pagination_headers( $method ) { $total_posts = self::$total_posts; $total_pages = (int) ceil( $total_posts / 10 ); // Start of the index. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); $this->assertSame( $total_posts, $headers['X-WP-Total'] ); @@ -1751,7 +1865,7 @@ public function test_get_items_pagination_headers() { self::factory()->post->create(); ++$total_posts; ++$total_pages; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'page', 3 ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); @@ -1773,7 +1887,7 @@ public function test_get_items_pagination_headers() { $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); // Last page. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'page', $total_pages ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); @@ -1789,7 +1903,7 @@ public function test_get_items_pagination_headers() { $this->assertStringNotContainsString( 'rel="next"', $headers['Link'] ); // Out of bounds. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'page', 100 ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); @@ -1797,7 +1911,7 @@ public function test_get_items_pagination_headers() { // With query params. $total_pages = (int) ceil( $total_posts / 5 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_query_params( array( 'per_page' => 5, @@ -1826,6 +1940,78 @@ public function test_get_items_pagination_headers() { $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); } + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_only_fetches_ids_for_head_requests( $method ) { + $is_head_request = 'HEAD' === $method; + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); + + $filter = new MockAction(); + + add_filter( 'posts_pre_query', array( $filter, 'filter' ), 10, 2 ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + if ( $is_head_request ) { + $this->assertEmpty( $response->get_data() ); + } else { + $this->assertNotEmpty( $response->get_data() ); + } + + $args = $filter->get_args(); + $this->assertTrue( isset( $args[0][1] ), 'Query parameters were not captured.' ); + $this->assertInstanceOf( WP_Query::class, $args[0][1], 'Query parameters were not captured.' ); + + /** @var WP_Query $query */ + $query = $args[0][1]; + + if ( $is_head_request ) { + $this->assertArrayHasKey( 'fields', $query->query, 'The fields parameter is not set in the query vars.' ); + $this->assertSame( 'ids', $query->query['fields'], 'The query must fetch only post IDs.' ); + $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' ); + $this->assertSame( 'ids', $query->query_vars['fields'], 'The query must fetch only post IDs.' ); + $this->assertArrayHasKey( 'update_post_term_cache', $query->query_vars, 'The "update_post_term_cache" parameter is missing in the query vars.' ); + $this->assertFalse( $query->query_vars['update_post_term_cache'], 'The "update_post_term_cache" parameter must be false for HEAD requests.' ); + $this->assertArrayHasKey( 'update_post_meta_cache', $query->query_vars, 'The "update_post_meta_cache" parameter is missing in the query vars.' ); + $this->assertFalse( $query->query_vars['update_post_meta_cache'], 'The "update_post_meta_cache" parameter must be false for HEAD requests.' ); + } else { + $this->assertTrue( ! array_key_exists( 'fields', $query->query ) || 'ids' !== $query->query['fields'], 'The fields parameter should not be forced to "ids" for non-HEAD requests.' ); + $this->assertTrue( ! array_key_exists( 'fields', $query->query_vars ) || 'ids' !== $query->query_vars['fields'], 'The fields parameter should not be forced to "ids" for non-HEAD requests.' ); + $this->assertArrayHasKey( 'update_post_term_cache', $query->query_vars, 'The "update_post_term_cache" parameter is missing in the query vars.' ); + $this->assertTrue( $query->query_vars['update_post_term_cache'], 'The "update_post_term_cache" parameter must be true for non-HEAD requests.' ); + $this->assertArrayHasKey( 'update_post_meta_cache', $query->query_vars, 'The "update_post_meta_cache" parameter is missing in the query vars.' ); + $this->assertTrue( $query->query_vars['update_post_meta_cache'], 'The "update_post_meta_cache" parameter must be true for non-HEAD requests.' ); + } + + if ( ! $is_head_request ) { + return; + } + + global $wpdb; + $posts_table = preg_quote( $wpdb->posts, '/' ); + $pattern = '/^SELECT\s+SQL_CALC_FOUND_ROWS\s+' . $posts_table . '\.ID\s+FROM\s+' . $posts_table . '\s+WHERE/i'; + + // Assert that the SQL query only fetches the ID column. + $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' ); + } + public function test_get_items_status_draft_permissions() { $draft_id = self::factory()->post->create( array( 'post_status' => 'draft' ) ); @@ -1972,6 +2158,43 @@ public function test_get_item() { $this->check_get_post_response( $response, 'view' ); } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + + $hook_name = 'rest_prepare_' . get_post_type( self::$post_id ); + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'Link', $headers, 'The "Link" header should be present in the response.' ); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + public function test_get_item_links() { $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); $response = rest_get_server()->dispatch( $request ); diff --git a/tests/phpunit/tests/rest-api/rest-request.php b/tests/phpunit/tests/rest-api/rest-request.php index aac7145287109..1067739ed2469 100644 --- a/tests/phpunit/tests/rest-api/rest-request.php +++ b/tests/phpunit/tests/rest-api/rest-request.php @@ -1081,4 +1081,49 @@ public function test_route_level_validate_callback_is_not_executed_if_parameter_ $this->assertWPError( $valid ); $this->assertSame( 'rest_invalid_param', $valid->get_error_code() ); } + + /** + * Tests that WP_REST_Request::is_method() correctly detects the request method, + * regardless of case sensitivity. + * + * @dataProvider data_is_method_should_detect_method_ignoring_case + * @ticket 56481 + * + * @param string $method The expected HTTP method of the request. + * @param string $input_method The HTTP method to check against. + * @param bool $expected The expected result of the is_method() check. + */ + public function test_is_method_should_detect_method_ignoring_case( $method, $input_method, $expected ) { + $request = new WP_REST_Request(); + $request->set_method( $method ); + $result = $request->is_method( $input_method ); + + $this->assertSame( $expected, $result, 'Failed asserting that the WP_REST_Request::is_method() method correctly detects the request method.' ); + } + + /** + * Provides test cases for verifying HTTP method comparison is case-insensitive. + * + * @return array + */ + public function data_is_method_should_detect_method_ignoring_case() { + return array( + // GET. + 'GET same case' => array( 'GET', 'GET', true ), + 'GET different case' => array( 'GET', 'get', true ), + 'GET different case #2' => array( 'GET', 'get', true ), + 'GET different case #3' => array( 'GET', 'gEt', true ), + 'GET wrong method' => array( 'GET', 'POST', false ), + // POST. + 'POST same case' => array( 'POST', 'POST', true ), + 'POST different case' => array( 'POST', 'post', true ), + 'POST different case #2' => array( 'POST', 'pOsT', true ), + 'POST wrong method' => array( 'POST', 'GET', false ), + // HEAD. + 'HEAD same case' => array( 'HEAD', 'HEAD', true ), + 'HEAD different case' => array( 'HEAD', 'head', true ), + 'HEAD different case #2' => array( 'HEAD', 'HeAd', true ), + 'HEAD wrong method' => array( 'HEAD', 'GET', false ), + ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-revisions-controller.php b/tests/phpunit/tests/rest-api/rest-revisions-controller.php index cdb886c4e47e6..8d1c7c540c7e7 100644 --- a/tests/phpunit/tests/rest-api/rest-revisions-controller.php +++ b/tests/phpunit/tests/rest-api/rest-revisions-controller.php @@ -162,9 +162,38 @@ public function test_get_items() { $this->check_get_revision_response( $data[2], $this->revision_1 ); } - public function test_get_items_no_permission() { + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_revisions_data() { + wp_set_current_user( self::$editor_id ); + + $hook_name = 'rest_prepare_revision'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + + add_filter( $hook_name, $callback ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + + $this->assertNotWPError( $response ); + $response = rest_ensure_response( $response ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); @@ -173,16 +202,40 @@ public function test_get_items_no_permission() { $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); } - public function test_get_items_missing_parent() { + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_missing_parent( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } - public function test_get_items_invalid_parent_post_type() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_invalid_parent_post_type( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$page_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$page_id . '/revisions' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } @@ -213,6 +266,43 @@ public function test_get_item() { $this->assertSame( self::$editor_id, $data['author'] ); } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); + + $hook_name = 'rest_prepare_revision'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'GET' === $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + public function test_get_item_embed_context() { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); @@ -231,9 +321,15 @@ public function test_get_item_embed_context() { $this->assertSameSets( $fields, array_keys( $data ) ); } - public function test_get_item_no_permission() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); @@ -242,16 +338,28 @@ public function test_get_item_no_permission() { $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); } - public function test_get_item_missing_parent() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_missing_parent( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions/' . $this->revision_id1 ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions/' . $this->revision_id1 ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } - public function test_get_item_invalid_parent_post_type() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_invalid_parent_post_type( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$page_id . '/revisions/' . $this->revision_id1 ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$page_id . '/revisions/' . $this->revision_id1 ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } @@ -269,11 +377,15 @@ public function test_get_item_valid_parent_id() { } /** + * @dataProvider data_readable_http_methods * @ticket 59875 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_item_invalid_parent_id() { + public function test_get_item_invalid_parent_id( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_2_1_id ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_2_1_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_revision_parent_id_mismatch', $response, 404 ); @@ -510,9 +622,13 @@ public function test_get_item_sets_up_postdata() { /** * Test the pagination header of the first page. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_pagination_header_of_the_first_page() { + public function test_get_items_pagination_header_of_the_first_page( $method ) { wp_set_current_user( self::$editor_id ); $rest_route = '/wp/v2/posts/' . self::$post_id . '/revisions'; @@ -520,7 +636,7 @@ public function test_get_items_pagination_header_of_the_first_page() { $total_pages = (int) ceil( $this->total_revisions / $per_page ); $page = 1; // First page. - $request = new WP_REST_Request( 'GET', $rest_route ); + $request = new WP_REST_Request( $method, $rest_route ); $request->set_query_params( array( 'per_page' => $per_page, @@ -545,9 +661,13 @@ public function test_get_items_pagination_header_of_the_first_page() { /** * Test the pagination header of the last page. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_pagination_header_of_the_last_page() { + public function test_get_items_pagination_header_of_the_last_page( $method ) { wp_set_current_user( self::$editor_id ); $rest_route = '/wp/v2/posts/' . self::$post_id . '/revisions'; @@ -555,7 +675,7 @@ public function test_get_items_pagination_header_of_the_last_page() { $total_pages = (int) ceil( $this->total_revisions / $per_page ); $page = 2; // Last page. - $request = new WP_REST_Request( 'GET', $rest_route ); + $request = new WP_REST_Request( $method, $rest_route ); $request->set_query_params( array( 'per_page' => $per_page, @@ -576,19 +696,24 @@ public function test_get_items_pagination_header_of_the_last_page() { $this->assertStringContainsString( '<' . $prev_link . '>; rel="prev"', $headers['Link'] ); } + /** * Test that invalid 'per_page' query should error. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_invalid_per_page_should_error() { + public function test_get_items_invalid_per_page_should_error( $method ) { wp_set_current_user( self::$editor_id ); $per_page = -1; // Invalid number. $expected_error = 'rest_invalid_param'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $request->set_param( 'per_page', $per_page ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( $expected_error, $response, $expected_status ); @@ -597,9 +722,13 @@ public function test_get_items_invalid_per_page_should_error() { /** * Test that out of bounds 'page' query should error. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_out_of_bounds_page_should_error() { + public function test_get_items_out_of_bounds_page_should_error( $method ) { wp_set_current_user( self::$editor_id ); $per_page = 2; @@ -608,7 +737,7 @@ public function test_get_items_out_of_bounds_page_should_error() { $expected_error = 'rest_revision_invalid_page_number'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $request->set_query_params( array( 'per_page' => $per_page, @@ -622,9 +751,13 @@ public function test_get_items_out_of_bounds_page_should_error() { /** * Test that impossibly high 'page' query should error. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_invalid_max_pages_should_error() { + public function test_get_items_invalid_max_pages_should_error( $method ) { wp_set_current_user( self::$editor_id ); $per_page = 2; @@ -632,7 +765,7 @@ public function test_get_items_invalid_max_pages_should_error() { $expected_error = 'rest_revision_invalid_page_number'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $request->set_query_params( array( 'per_page' => $per_page, @@ -770,9 +903,13 @@ public function test_get_items_total_revisions_offset_should_return_empty_data() /** * Test that out of bound 'offset' query should error. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_out_of_bound_offset_should_error() { + public function test_get_items_out_of_bound_offset_should_error( $method ) { wp_set_current_user( self::$editor_id ); $per_page = 2; @@ -780,7 +917,7 @@ public function test_get_items_out_of_bound_offset_should_error() { $expected_error = 'rest_revision_invalid_offset_number'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $request->set_query_params( array( 'offset' => $offset, @@ -794,9 +931,13 @@ public function test_get_items_out_of_bound_offset_should_error() { /** * Test that impossible high number for 'offset' query should error. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_impossible_high_number_offset_should_error() { + public function test_get_items_impossible_high_number_offset_should_error( $method ) { wp_set_current_user( self::$editor_id ); $per_page = 2; @@ -804,7 +945,7 @@ public function test_get_items_impossible_high_number_offset_should_error() { $expected_error = 'rest_revision_invalid_offset_number'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $request->set_query_params( array( 'offset' => $offset, @@ -818,9 +959,13 @@ public function test_get_items_impossible_high_number_offset_should_error() { /** * Test that invalid 'offset' query should error. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_invalid_offset_should_error() { + public function test_get_items_invalid_offset_should_error( $method ) { wp_set_current_user( self::$editor_id ); $per_page = 2; @@ -828,7 +973,7 @@ public function test_get_items_invalid_offset_should_error() { $expected_error = 'rest_invalid_param'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $request->set_query_params( array( 'offset' => $offset, diff --git a/tests/phpunit/tests/rest-api/rest-search-controller.php b/tests/phpunit/tests/rest-api/rest-search-controller.php index 668cfb4e6bdeb..e4235fd699798 100644 --- a/tests/phpunit/tests/rest-api/rest-search-controller.php +++ b/tests/phpunit/tests/rest-api/rest-search-controller.php @@ -160,6 +160,101 @@ public function test_get_items() { ); } + /** + * Test pagination headers. + * + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_pagination_headers( $method ) { + $total_posts = count( self::$my_title_post_ids ) + count( self::$my_title_page_ids ) + count( self::$my_content_post_ids ); + $per_page = 3; + $total_pages = (int) ceil( $total_posts / $per_page ); + + // Start of the index. + $response = $this->do_request_with_params( + array( + 'per_page' => $per_page, + ), + $method + ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts, $headers['X-WP-Total'] ); + $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); + + $next_link = add_query_arg( + array( + 'per_page' => $per_page, + 'page' => 2, + ), + rest_url( '/wp/v2/search' ) + ); + $this->assertStringNotContainsString( 'rel="prev"', $headers['Link'] ); + $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); + + $response = $this->do_request_with_params( + array( + 'per_page' => $per_page, + 'page' => 3, + ), + $method + ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts, $headers['X-WP-Total'] ); + $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); + $prev_link = add_query_arg( + array( + 'per_page' => $per_page, + 'page' => 2, + ), + rest_url( '/wp/v2/search' ) + ); + $this->assertStringContainsString( '<' . $prev_link . '>; rel="prev"', $headers['Link'] ); + $next_link = add_query_arg( + array( + 'per_page' => $per_page, + 'page' => 4, + ), + rest_url( '/wp/v2/search' ) + ); + $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); + + // Last page. + $response = $this->do_request_with_params( + array( + 'per_page' => $per_page, + 'page' => $total_pages, + ), + $method + ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts, $headers['X-WP-Total'] ); + $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); + $prev_link = add_query_arg( + array( + 'per_page' => $per_page, + 'page' => $total_pages - 1, + ), + rest_url( '/wp/v2/search' ) + ); + $this->assertStringContainsString( '<' . $prev_link . '>; rel="prev"', $headers['Link'] ); + $this->assertStringNotContainsString( 'rel="next"', $headers['Link'] ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + /** * Search through all content with a low limit. */ @@ -239,13 +334,19 @@ public function test_get_items_search_type_post_subtype_page() { /** * Search through an invalid type + * + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_search_type_invalid() { + public function test_get_items_search_type_invalid( $method ) { $response = $this->do_request_with_params( array( 'per_page' => 100, 'type' => 'invalid', - ) + ), + $method ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); @@ -253,6 +354,11 @@ public function test_get_items_search_type_invalid() { /** * Search through posts of an invalid post type. + * + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. */ public function test_get_items_search_type_post_subtype_invalid() { $response = $this->do_request_with_params( @@ -462,13 +568,19 @@ public function test_get_item_schema() { /** * Tests that non-public post types are not allowed. + * + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_non_public_post_type() { + public function test_non_public_post_type( $method ) { $response = $this->do_request_with_params( array( 'type' => 'post', 'subtype' => 'post,nav_menu_item', - ) + ), + $method ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } @@ -632,15 +744,21 @@ public function test_get_items_search_type_term_subtype_category() { /** * Search through posts of an invalid post type. * + * + * @dataProvider data_readable_http_methods * @ticket 51458 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_search_term_subtype_invalid() { + public function test_get_items_search_term_subtype_invalid( $method ) { $response = $this->do_request_with_params( array( 'per_page' => 100, 'type' => 'term', 'subtype' => 'invalid', - ) + ), + $method ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); @@ -890,14 +1008,19 @@ public function test_get_items_search_terms_exclude_ids() { } /** + * @dataProvider data_readable_http_methods * @ticket 60771 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_sanitize_subtypes_validates_type() { + public function test_sanitize_subtypes_validates_type( $method ) { $response = $this->do_request_with_params( array( 'subtype' => 'page', 'type' => array( 'invalid' ), - ) + ), + $method ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); diff --git a/tests/phpunit/tests/rest-api/rest-sidebars-controller.php b/tests/phpunit/tests/rest-api/rest-sidebars-controller.php index 67a49770dbc86..1c6249da9e823 100644 --- a/tests/phpunit/tests/rest-api/rest-sidebars-controller.php +++ b/tests/phpunit/tests/rest-api/rest-sidebars-controller.php @@ -152,11 +152,38 @@ public function test_get_items() { } /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_sidebar_data() { + wp_widgets_init(); + + $request = new WP_REST_Request( 'HEAD', '/wp/v2/sidebars' ); + + $hook_name = 'rest_prepare_sidebar'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + + $this->assertNotWPError( $response ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_no_permission() { + public function test_get_items_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); + $request = new WP_REST_Request( $method, '/wp/v2/sidebars' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } @@ -501,9 +528,68 @@ public function test_get_item() { } /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $hook_name = 'rest_prepare_sidebar'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( $method, '/wp/v2/sidebars/sidebar-1' ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_item_no_permission() { + public function test_get_item_no_permission( $method ) { wp_set_current_user( 0 ); $this->setup_sidebar( 'sidebar-1', @@ -512,7 +598,7 @@ public function test_get_item_no_permission() { ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars/sidebar-1' ); + $request = new WP_REST_Request( $method, '/wp/v2/sidebars/sidebar-1' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } @@ -552,9 +638,13 @@ public function test_get_item_no_permission_public() { } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_item_wrong_permission_author() { + public function test_get_item_wrong_permission_author( $method ) { wp_set_current_user( self::$author_id ); $this->setup_sidebar( 'sidebar-1', @@ -563,7 +653,7 @@ public function test_get_item_wrong_permission_author() { ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars/sidebar-1' ); + $request = new WP_REST_Request( $method, '/wp/v2/sidebars/sidebar-1' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); } diff --git a/tests/phpunit/tests/rest-api/rest-tags-controller.php b/tests/phpunit/tests/rest-api/rest-tags-controller.php index cd593578a9836..bf09be061540e 100644 --- a/tests/phpunit/tests/rest-api/rest-tags-controller.php +++ b/tests/phpunit/tests/rest-api/rest-tags-controller.php @@ -627,12 +627,18 @@ public function test_get_terms_private_taxonomy() { $this->assertErrorResponse( 'rest_no_route', $response, 404 ); } - public function test_get_terms_pagination_headers() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_terms_pagination_headers( $method ) { $total_tags = self::$total_tags; $total_pages = (int) ceil( $total_tags / 10 ); // Start of the index. - $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); + $request = new WP_REST_Request( $method, '/wp/v2/tags' ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); $this->assertSame( $total_tags, $headers['X-WP-Total'] ); @@ -1502,4 +1508,112 @@ protected function check_get_taxonomy_term_response( $response, $id ) { $tag = get_term( $id, 'post_tag' ); $this->check_taxonomy_term( $tag, $data, $response->get_links() ); } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_only_fetches_ids_for_head_requests( $method ) { + $is_head_request = 'HEAD' === $method; + $request = new WP_REST_Request( $method, '/wp/v2/tags' ); + + $filter = new MockAction(); + + add_filter( 'terms_pre_query', array( $filter, 'filter' ), 10, 2 ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + if ( $is_head_request ) { + $this->assertEmpty( $response->get_data() ); + } else { + $this->assertNotEmpty( $response->get_data() ); + } + + $args = $filter->get_args(); + $this->assertTrue( isset( $args[0][1] ), 'Query parameters were not captured.' ); + $this->assertInstanceOf( WP_Term_Query::class, $args[0][1], 'Query parameters were not captured.' ); + + /** @var WP_Term_Query $query */ + $query = $args[0][1]; + + if ( $is_head_request ) { + $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' ); + $this->assertSame( 'ids', $query->query_vars['fields'], 'The query must fetch only term IDs.' ); + $this->assertArrayHasKey( 'update_term_meta_cache', $query->query_vars, 'The update_term_meta_cache key is missing in the query vars.' ); + $this->assertFalse( $query->query_vars['update_term_meta_cache'], 'The update_term_meta_cache value should be false for HEAD requests.' ); + } else { + $this->assertTrue( + ! array_key_exists( 'fields', $query->query_vars ) || 'ids' !== $query->query_vars['fields'], + 'The fields parameter should not be forced to "ids" for non-HEAD requests.' + ); + $this->assertArrayHasKey( 'update_term_meta_cache', $query->query_vars, 'The update_term_meta_cache key is missing in the query vars.' ); + $this->assertTrue( $query->query_vars['update_term_meta_cache'], 'The update_term_meta_cache value should be true for HEAD requests.' ); + } + + if ( ! $is_head_request ) { + return; + } + + global $wpdb; + $terms_table = preg_quote( $wpdb->terms, '/' ); + + $pattern = '/SELECT\s+t\.term_id.+FROM\s+' . $terms_table . '\s+AS\s+t\s+INNER\s+JOIN/is'; + + // Assert that the SQL query only fetches the term_id column. + $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( string $method ) { + $tag_id = self::factory()->tag->create(); + + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/tags/%d', $tag_id ) ); + + $hook_name = 'rest_prepare_post_tag'; + + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php b/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php index 86ad0daf351b9..9ff0450af9674 100644 --- a/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php +++ b/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php @@ -60,6 +60,22 @@ public function test_get_items() { $this->assertSame( 'tags', $data['post_tag']['rest_base'] ); } + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_taxonomy_data() { + $request = new WP_REST_Request( 'HEAD', '/wp/v2/taxonomies' ); + $hook_name = 'rest_prepare_taxonomy'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + public function test_get_items_context_edit() { wp_set_current_user( self::$contributor_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' ); @@ -79,14 +95,33 @@ public function test_get_items_context_edit() { $this->assertSame( 'tags', $data['post_tag']['rest_base'] ); } - public function test_get_items_invalid_permission_for_context() { + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_invalid_permission_for_context( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' ); + $request = new WP_REST_Request( $method, '/wp/v2/taxonomies' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_view', $response, 401 ); } + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + public function test_get_taxonomies_for_type() { $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' ); $request->set_param( 'type', 'post' ); @@ -94,11 +129,20 @@ public function test_get_taxonomies_for_type() { $this->check_taxonomies_for_type_response( 'post', $response ); } - public function test_get_taxonomies_for_invalid_type() { - $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_taxonomies_for_invalid_type( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/taxonomies' ); $request->set_param( 'type', 'wingding' ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); + if ( 'HEAD' === $method ) { + return null; + } $data = $response->get_data(); $this->assertSame( '{}', json_encode( $data ) ); } @@ -109,6 +153,41 @@ public function test_get_item() { $this->check_taxonomy_object_response( 'view', $response ); } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $request = new WP_REST_Request( 'HEAD', '/wp/v2/taxonomies/category' ); + $hook_name = 'rest_prepare_taxonomy'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + public function test_get_item_edit_context() { $editor_id = self::factory()->user->create( array( 'role' => 'editor' ) ); wp_set_current_user( $editor_id ); @@ -118,33 +197,57 @@ public function test_get_item_edit_context() { $this->check_taxonomy_object_response( 'edit', $response ); } - public function test_get_item_invalid_permission_for_context() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_item_invalid_permission_for_context( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/category' ); + $request = new WP_REST_Request( $method, '/wp/v2/taxonomies/category' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 ); } - public function test_get_invalid_taxonomy() { - $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/invalid' ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_invalid_taxonomy( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/taxonomies/invalid' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_taxonomy_invalid', $response, 404 ); } - public function test_get_non_public_taxonomy_not_authenticated() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_non_public_taxonomy_not_authenticated( $method ) { register_taxonomy( 'api-private', 'post', array( 'public' => false ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/api-private' ); + $request = new WP_REST_Request( $method, '/wp/v2/taxonomies/api-private' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_forbidden', $response, 401 ); } - public function test_get_non_public_taxonomy_no_permission() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_non_public_taxonomy_no_permission( $method ) { wp_set_current_user( self::$contributor_id ); register_taxonomy( 'api-private', 'post', array( 'public' => false ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/api-private' ); + $request = new WP_REST_Request( $method, '/wp/v2/taxonomies/api-private' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_forbidden', $response, 403 ); } diff --git a/tests/phpunit/tests/rest-api/rest-users-controller.php b/tests/phpunit/tests/rest-api/rest-users-controller.php index e9576a2c9bf97..2156946fd9480 100644 --- a/tests/phpunit/tests/rest-api/rest-users-controller.php +++ b/tests/phpunit/tests/rest-api/rest-users-controller.php @@ -233,14 +233,29 @@ public function test_get_items() { $this->check_user_data( $userdata, $data, 'view', $data['_links'] ); } - public function test_get_items_with_edit_context() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_with_edit_context( $method ) { wp_set_current_user( self::$user ); - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); - $this->assertSame( 200, $response->get_status() ); + $this->assertSame( + 200, + $response->get_status(), + sprintf( 'Expected HTTP status code 200 but got %s.', $response->get_status() ) + ); + + if ( 'HEAD' === $method ) { + $this->assertNull( $response->get_data(), 'Expected null response data for HEAD request, but received non-null data.' ); + return null; + } $all_data = $response->get_data(); $data = $all_data[0]; @@ -248,9 +263,27 @@ public function test_get_items_with_edit_context() { $this->check_user_data( $userdata, $data, 'edit', $data['_links'] ); } - public function test_get_items_with_edit_context_without_permission() { + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_with_edit_context_without_permission( $method ) { // Test with a user not logged in. - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); @@ -260,7 +293,7 @@ public function test_get_items_with_edit_context_without_permission() { // capability in question: 'list_users'. wp_set_current_user( self::$editor ); - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); @@ -318,14 +351,20 @@ public function test_get_items_unauthenticated_does_not_include_users_without_pu $this->assertNotContains( self::$user, $user_ids ); } - public function test_get_items_pagination_headers() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_pagination_headers( $method ) { $total_users = self::$total_users; $total_pages = (int) ceil( $total_users / 10 ); wp_set_current_user( self::$user ); // Start of the index. - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); $this->assertSame( $total_users, $headers['X-WP-Total'] ); @@ -343,7 +382,7 @@ public function test_get_items_pagination_headers() { self::factory()->user->create(); ++$total_users; ++$total_pages; - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'page', 3 ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); @@ -365,7 +404,7 @@ public function test_get_items_pagination_headers() { $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); // Last page. - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'page', $total_pages ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); @@ -381,7 +420,7 @@ public function test_get_items_pagination_headers() { $this->assertStringNotContainsString( 'rel="next"', $headers['Link'] ); // Out of bounds. - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'page', 100 ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); @@ -410,14 +449,24 @@ public function test_get_items_per_page() { $this->assertCount( 5, $response->get_data() ); } - public function test_get_items_page() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_page( $method ) { wp_set_current_user( self::$user ); - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'per_page', 5 ); $request->set_param( 'page', 2 ); $response = rest_get_server()->dispatch( $request ); - $this->assertCount( 5, $response->get_data() ); + + if ( 'HEAD' !== $method ) { + $this->assertCount( 5, $response->get_data() ); + } + $prev_link = add_query_arg( array( 'per_page' => 5, @@ -1207,17 +1256,26 @@ public function test_get_item_published_author_wrong_context() { $this->assertErrorResponse( 'rest_user_cannot_view', $response, 401 ); } - public function test_get_current_user() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_current_user( $method ) { wp_set_current_user( self::$user ); - $request = new WP_REST_Request( 'GET', '/wp/v2/users/me' ); + $request = new WP_REST_Request( $method, '/wp/v2/users/me' ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); - $this->check_get_user_response( $response, 'view' ); - $headers = $response->get_headers(); $this->assertArrayNotHasKey( 'Location', $headers ); + if ( 'HEAD' === $method ) { + // HEAD responses only contain headers. Bail. + return null; + } + $this->check_get_user_response( $response, 'view' ); $links = $response->get_links(); $this->assertSame( rest_url( 'wp/v2/users/' . self::$user ), $links['self'][0]['href'] ); } @@ -3100,6 +3158,96 @@ public function data_get_default_data() { ); } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + wp_set_current_user( self::$user ); + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/users/%d', self::$user ) ); + + $hook_name = 'rest_prepare_user'; + + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_only_fetches_ids_for_head_requests( $method ) { + $is_head_request = 'HEAD' === $method; + $request = new WP_REST_Request( $method, '/wp/v2/users' ); + + $filter = new MockAction(); + + add_filter( 'pre_user_query', array( $filter, 'filter' ), 10, 2 ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + if ( $is_head_request ) { + $this->assertNull( $response->get_data() ); + } else { + $this->assertNotEmpty( $response->get_data() ); + } + + $args = $filter->get_args(); + $this->assertTrue( isset( $args[0][0] ), 'Query parameters were not captured.' ); + $this->assertInstanceOf( WP_User_Query::class, $args[0][0], 'Query parameters were not captured.' ); + + /** @var WP_User $query */ + $query = $args[0][0]; + + if ( $is_head_request ) { + $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' ); + $this->assertSame( 'id', $query->query_vars['fields'], 'The query must fetch only user IDs.' ); + } else { + $this->assertTrue( + ! array_key_exists( 'fields', $query->query_vars ) || 'id' !== $query->query_vars['fields'], + 'The fields parameter should not be forced to "id" for non-HEAD requests.' + ); + } + + if ( ! $is_head_request ) { + return; + } + + global $wpdb; + $users_table = preg_quote( $wpdb->users, '/' ); + $pattern = '/SELECT SQL_CALC_FOUND_ROWS wptests_users.ID\n\s+FROM\s+' . $users_table . '/is'; + + // Assert that the SQL query only fetches the id column. + $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' ); + } + protected function check_user_data( $user, $data, $context, $links ) { $this->assertSame( $user->ID, $data['id'] ); $this->assertSame( $user->display_name, $data['name'] ); diff --git a/tests/phpunit/tests/rest-api/rest-widget-types-controller.php b/tests/phpunit/tests/rest-api/rest-widget-types-controller.php index cac586447e385..bf7c1a3d7a693 100644 --- a/tests/phpunit/tests/rest-api/rest-widget-types-controller.php +++ b/tests/phpunit/tests/rest-api/rest-widget-types-controller.php @@ -121,6 +121,17 @@ public function test_get_items() { } } + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_widget_types_data() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/widget-types' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + /** * @ticket 53303 */ @@ -181,6 +192,56 @@ public function test_get_item() { $this->check_widget_type_object( $widget_type, $response->get_data(), $response->get_links() ); } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $widget_name = 'calendar'; + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( $method, '/wp/v2/widget-types/' . $widget_name ); + + $hook_name = 'rest_prepare_widget_type'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + /** * @ticket 41683 */ @@ -200,12 +261,16 @@ static function () {} } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_widget_invalid_name() { + public function test_get_widget_invalid_name( $method ) { $widget_type = 'fake'; wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types/' . $widget_type ); + $request = new WP_REST_Request( $method, '/wp/v2/widget-types/' . $widget_type ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_widget_type_invalid', $response, 404 ); @@ -251,41 +316,57 @@ public function test_get_item_schema() { } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_wrong_permission() { + public function test_get_items_wrong_permission( $method ) { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types' ); + $request = new WP_REST_Request( $method, '/wp/v2/widget-types' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_item_wrong_permission() { + public function test_get_item_wrong_permission( $method ) { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types/calendar' ); + $request = new WP_REST_Request( $method, '/wp/v2/widget-types/calendar' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_no_permission() { + public function test_get_items_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types' ); + $request = new WP_REST_Request( $method, '/wp/v2/widget-types' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_item_no_permission() { + public function test_get_item_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types/calendar' ); + $request = new WP_REST_Request( $method, '/wp/v2/widget-types/calendar' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } diff --git a/tests/phpunit/tests/rest-api/rest-widgets-controller.php b/tests/phpunit/tests/rest-api/rest-widgets-controller.php index c0a311f25f0a9..02b63da6d8d27 100644 --- a/tests/phpunit/tests/rest-api/rest-widgets-controller.php +++ b/tests/phpunit/tests/rest-api/rest-widgets-controller.php @@ -228,15 +228,31 @@ public function test_get_items_no_widgets() { } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_no_permission() { + public function test_get_items_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); + $request = new WP_REST_Request( $method, '/wp/v2/widgets' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + /** * @ticket 53915 */ @@ -332,11 +348,15 @@ public function test_get_items_without_show_in_rest_are_removed_from_the_list() } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_wrong_permission_author() { + public function test_get_items_wrong_permission_author( $method ) { wp_set_current_user( self::$author_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); + $request = new WP_REST_Request( $method, '/wp/v2/widgets' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); } @@ -377,8 +397,9 @@ public function test_get_items() { $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $data = $this->remove_links( $data ); + remove_filter( 'pre_http_request', array( $this, 'mocked_rss_response' ) ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); $this->assertSameSets( array( array( @@ -406,6 +427,54 @@ public function test_get_items() { $wp_widget_factory->widgets['WP_Widget_RSS']->widget_options['show_instance_in_rest'] = true; } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_with_head_request_should_not_prepare_widget_data( $method ) { + $block_content = '

Block test

'; + + $this->setup_widget( + 'rss', + 1, + array( + 'title' => 'RSS test', + 'url' => 'https://wordpress.org/news/feed', + ) + ); + $this->setup_widget( + 'block', + 1, + array( + 'content' => $block_content, + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'block-1', 'rss-1', 'testwidget' ) + ); + + $request = new WP_REST_Request( 'HEAD', '/wp/v2/widgets' ); + + $hook_name = 'rest_prepare_post'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + + $this->assertNotWPError( $response ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + public function mocked_rss_response() { $single_value_headers = array( 'Content-Type' => 'application/rss+xml; charset=UTF-8', @@ -528,9 +597,64 @@ public function test_get_item() { } /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1' ) + ); + + $request = new WP_REST_Request( $method, '/wp/v2/widgets/text-1' ); + + $hook_name = 'rest_prepare_widget'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_item_no_permission() { + public function test_get_item_no_permission( $method ) { wp_set_current_user( 0 ); $this->setup_widget( @@ -548,15 +672,19 @@ public function test_get_item_no_permission() { array( 'text-1' ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widgets/text-1' ); + $request = new WP_REST_Request( $method, '/wp/v2/widgets/text-1' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_item_wrong_permission_author() { + public function test_get_item_wrong_permission_author( $method ) { wp_set_current_user( self::$author_id ); $this->setup_widget( 'text', @@ -572,7 +700,7 @@ public function test_get_item_wrong_permission_author() { ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widgets/text-1' ); + $request = new WP_REST_Request( $method, '/wp/v2/widgets/text-1' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); } diff --git a/tests/phpunit/tests/rest-api/wpRestBlockPatternCategoriesController.php b/tests/phpunit/tests/rest-api/wpRestBlockPatternCategoriesController.php index 63c2256b36ba3..58352a0a62009 100644 --- a/tests/phpunit/tests/rest-api/wpRestBlockPatternCategoriesController.php +++ b/tests/phpunit/tests/rest-api/wpRestBlockPatternCategoriesController.php @@ -123,6 +123,17 @@ public function test_get_items() { } } + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_block_pattern_categories_data() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', static::REQUEST_ROUTE ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + /** * Verify capability check for unauthorized request (not logged in). */ diff --git a/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php index e2da442e7d874..d5e9213517317 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php @@ -34,6 +34,11 @@ class Tests_REST_wpRestTemplateAutosavesController extends WP_Test_REST_Controll */ const TEMPLATE_PART_POST_TYPE = 'wp_template_part'; + /** + * @var string + */ + const PARENT_POST_TYPE = 'wp_template'; + /** * Admin user ID. * @@ -292,6 +297,28 @@ public function test_get_items_with_data_provider( $parent_post_property_name, $ ); } + /** + * @ticket 56481 + */ + public function test_get_items_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $autosave_post_id = wp_create_post_autosave( + array( + 'post_content' => 'Autosave content.', + 'post_ID' => self::$template_post->ID, + 'post_type' => self::PARENT_POST_TYPE, + ) + ); + + $request = new WP_REST_Request( + 'HEAD', + '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/autosaves' + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + /** * Data provider for test_get_items_with_data_provider. * @@ -426,6 +453,26 @@ public function test_get_item_with_data_provider( $parent_post_property_name, $r ); } + /** + * @ticket 56481 + */ + public function test_get_item_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + + $autosave_post_id = wp_create_post_autosave( + array( + 'post_content' => 'Autosave content.', + 'post_ID' => self::$template_post->ID, + 'post_type' => self::PARENT_POST_TYPE, + ) + ); + + $request = new WP_REST_Request( 'HEAD', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/autosaves/' . $autosave_post_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + /** * Data provider for test_get_item_with_data_provider. * diff --git a/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php b/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php index 23352879f3cfe..700d31360cc2e 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php @@ -426,18 +426,33 @@ public function data_get_items_with_data_provider() { 'template parts' => array( 'template_part_post', 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), ); } + /** + * @ticket 56481 + */ + public function test_get_items_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( + 'HEAD', + '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions' + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } /** * @dataProvider data_get_items_endpoint_should_return_unauthorized_https_status_code_for_unauthorized_request * @covers WP_REST_Template_Revisions_Controller::get_items_permissions_check * @ticket 56922 + * @ticket 56481 * * @param string $rest_base Base part of the REST API endpoint to test. * @param string $template_id Template ID to use in the test. + * @param string $method HTTP method to use. */ - public function test_get_items_endpoint_should_return_unauthorized_https_status_code_for_unauthorized_request( $rest_base, $template_id ) { + public function test_get_items_endpoint_should_return_unauthorized_https_status_code_for_unauthorized_request( $rest_base, $template_id, $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, WP_Http::UNAUTHORIZED ); } @@ -449,8 +464,10 @@ public function test_get_items_endpoint_should_return_unauthorized_https_status_ */ public function data_get_items_endpoint_should_return_unauthorized_https_status_code_for_unauthorized_request() { return array( - 'templates' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME ), - 'template parts' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), + 'templates, GET request' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME, 'GET' ), + 'templates, HEAD request' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME, 'HEAD' ), + 'template parts, GET request' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, 'GET' ), + 'template parts, HEAD request' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, 'HEAD' ), ); } @@ -458,13 +475,15 @@ public function data_get_items_endpoint_should_return_unauthorized_https_status_ * @dataProvider data_get_items_endpoint_should_return_forbidden_https_status_code_for_users_with_insufficient_permissions * @covers WP_REST_Template_Revisions_Controller::get_items_permissions_check * @ticket 56922 + * @ticket 56481 * * @param string $rest_base Base part of the REST API endpoint to test. * @param string $template_id Template ID to use in the test. + * @param string $method HTTP method to use. */ - public function test_get_items_endpoint_should_return_forbidden_https_status_code_for_users_with_insufficient_permissions( $rest_base, string $template_id ) { + public function test_get_items_endpoint_should_return_forbidden_https_status_code_for_users_with_insufficient_permissions( $rest_base, string $template_id, $method ) { wp_set_current_user( self::$contributor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, WP_Http::FORBIDDEN ); } @@ -476,8 +495,10 @@ public function test_get_items_endpoint_should_return_forbidden_https_status_cod */ public function data_get_items_endpoint_should_return_forbidden_https_status_code_for_users_with_insufficient_permissions() { return array( - 'templates' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME ), - 'template parts' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), + 'templates, GET request' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME, 'GET' ), + 'templates, HEAD request' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME, 'HEAD' ), + 'template parts, GET request' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, 'GET' ), + 'template parts, HEAD request' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, 'HEAD' ), ); } @@ -596,6 +617,19 @@ public function test_get_item_with_data_provider( $parent_post_property_name, $r ); } + /** + * @ticket 56481 + */ + public function test_get_item_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $revisions = wp_get_post_revisions( self::$template_post, array( 'fields' => 'ids' ) ); + $revision_id = array_shift( $revisions ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions/' . $revision_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + /** * Data provider for test_get_item_with_data_provider. * @@ -612,11 +646,13 @@ public function data_get_item_with_data_provider() { * @dataProvider data_get_item_not_found * @covers WP_REST_Template_Revisions_Controller::get_item * @ticket 56922 + * @ticket 56481 * * @param string $parent_post_property_name A class property name that contains the parent post object. * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $method HTTP method to use. */ - public function test_get_item_not_found( $parent_post_property_name, $rest_base ) { + public function test_get_item_not_found( $parent_post_property_name, $rest_base, $method ) { wp_set_current_user( self::$admin_id ); $parent_post = self::$$parent_post_property_name; @@ -624,7 +660,7 @@ public function test_get_item_not_found( $parent_post_property_name, $rest_base $revisions = wp_get_post_revisions( $parent_post, array( 'fields' => 'ids' ) ); $revision_id = array_shift( $revisions ); - $request = new WP_REST_Request( 'GET', '/wp/v2/' . $rest_base . '/invalid//parent/revisions/' . $revision_id ); + $request = new WP_REST_Request( $method, '/wp/v2/' . $rest_base . '/invalid//parent/revisions/' . $revision_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, WP_Http::NOT_FOUND ); } @@ -636,8 +672,10 @@ public function test_get_item_not_found( $parent_post_property_name, $rest_base */ public function data_get_item_not_found() { return array( - 'templates' => array( 'template_post', 'templates' ), - 'template parts' => array( 'template_part_post', 'template-parts' ), + 'templates, GET request' => array( 'template_post', 'templates', 'GET' ), + 'templates, HEAD request' => array( 'template_post', 'templates', 'HEAD' ), + 'template parts, GET request' => array( 'template_part_post', 'template-parts', 'GET' ), + 'template parts, HEAD request' => array( 'template_part_post', 'template-parts', 'HEAD' ), ); } @@ -645,13 +683,15 @@ public function data_get_item_not_found() { * @dataProvider data_get_item_invalid_parent_id * @covers WP_REST_Template_Revisions_Controller::get_item * @ticket 59875 + * @ticket 56481 * * @param string $parent_post_property_name A class property name that contains the parent post object. * @param string $actual_parent_post_property_name A class property name that contains the parent post object. * @param string $rest_base Base part of the REST API endpoint to test. * @param string $template_id Template ID to use in the test. + * @param string $method HTTP method to use. */ - public function test_get_item_invalid_parent_id( $parent_post_property_name, $actual_parent_post_property_name, $rest_base, $template_id ) { + public function test_get_item_invalid_parent_id( $parent_post_property_name, $actual_parent_post_property_name, $rest_base, $template_id, $method ) { wp_set_current_user( self::$admin_id ); $parent_post = self::$$parent_post_property_name; @@ -659,7 +699,7 @@ public function test_get_item_invalid_parent_id( $parent_post_property_name, $ac $revisions = wp_get_post_revisions( $parent_post, array( 'fields' => 'ids' ) ); $revision_id = array_shift( $revisions ); - $request = new WP_REST_Request( 'GET', '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions/' . $revision_id ); + $request = new WP_REST_Request( $method, '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions/' . $revision_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_revision_parent_id_mismatch', $response, 404 ); @@ -675,17 +715,33 @@ public function test_get_item_invalid_parent_id( $parent_post_property_name, $ac */ public function data_get_item_invalid_parent_id() { return array( - 'templates' => array( + 'templates, GET request' => array( 'template_post', 'template_post_2', 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME_2, + 'GET', ), - 'template parts' => array( + 'templates, HEAD request' => array( + 'template_post', + 'template_post_2', + 'templates', + self::TEST_THEME . '//' . self::TEMPLATE_NAME_2, + 'HEAD', + ), + 'template parts, GET request' => array( + 'template_part_post', + 'template_part_post_2', + 'template-parts', + self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME_2, + 'GET', + ), + 'template parts, HEAD request' => array( 'template_part_post', 'template_part_post_2', 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME_2, + 'HEAD', ), ); } diff --git a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php index ad8cf945f971e..8ffa8b4e79023 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php @@ -175,6 +175,19 @@ public function test_get_items() { ); } + /** + * @ticket 56481 + * + * @covers WP_REST_Templates_Controller::get_items + */ + public function test_get_items_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/templates' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + /** * @covers WP_REST_Templates_Controller::get_items */ @@ -267,6 +280,20 @@ public function test_get_item() { ); } + /** + * @ticket 56481 + * + * @covers WP_REST_Templates_Controller::get_item + * @covers WP_REST_Templates_Controller::prepare_item_for_response + */ + public function test_get_item_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/templates/default//my_template' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertNull( $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + /** * @covers WP_REST_Templates_Controller::get_item */ @@ -311,7 +338,6 @@ public function test_get_item_subscriber() { wp_set_current_user( self::$subscriber_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/templates/default//my_template' ); $response = rest_get_server()->dispatch( $request ); - $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_templates', $response, 403 ); }