Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't prepare the response body for HEAD requests in WP_REST_Users_Controller #7814

12 changes: 12 additions & 0 deletions src/wp-includes/rest-api/class-wp-rest-request.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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'];
Expand Down Expand Up @@ -480,6 +488,10 @@ public function get_item( $request ) {
return $user;
}

if ( $request->is_method( 'head' ) ) {
return new WP_REST_Response();
}

$user = $this->prepare_item_for_response( $user, $request );
$response = rest_ensure_response( $user );

Expand Down
164 changes: 148 additions & 16 deletions tests/phpunit/tests/rest-api/rest-users-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,24 +233,57 @@ 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;
}

$all_data = $response->get_data();
$data = $all_data[0];
$userdata = get_userdata( $data['id'] );
$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 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 );

Expand All @@ -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 );

Expand Down Expand Up @@ -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'] );
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1207,17 +1256,27 @@ 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;
}
$links = $response->get_links();
$this->assertSame( rest_url( 'wp/v2/users/' . self::$user ), $links['self'][0]['href'] );
}
Expand Down Expand Up @@ -3100,6 +3159,79 @@ public function data_get_default_data() {
);
}

/**
* @ticket 56481
*/
public function test_get_item_with_head_request_should_not_prepare_user_data() {
wp_set_current_user( self::$user );
$request = new WP_REST_Request( 'HEAD', sprintf( '/wp/v2/users/%d', self::$user ) );

$hook_name = 'rest_prepare_user';

$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.' );
}

/**
* @dataProvider data_readable_http_methods
* @ticket 56481
*
* @param string $method HTTP method to use.
*/
public function test_get_items_returns_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'] );
Expand Down
Loading