REST API: Refactor `WP_REST_Server::dispatch()` to make internal logic reusable.

#50244 aims to introduce batch processing in the REST API. An important feature is the ability to enforce that all requests have valid data before executing the route callbacks in "pre-validate" mode.

This necessitates splitting `WP_REST_Server::dispatch()` into two methods so the batch controller can determine the request handler to perform pre-validation and then respond to the requests.

The two new methods, `match_request_to_handler` and `respond_to_request`, have a public visibility, but are marked as `@access private`. This is to allow for iteration on the batch controller to happen in the Gutenberg repository. Developers should not rely upon these methods, their visibility may change in the future.

See #50244.
Props andraganescu, zieladam, TimothyBlynJacobs.


git-svn-id: https://develop.svn.wordpress.org/trunk@48947 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Timothy Jacobs 2020-09-05 21:50:31 +00:00
parent 1c1a7b8365
commit d803f6bf82
2 changed files with 264 additions and 124 deletions

View File

@ -911,6 +911,48 @@ class WP_REST_Server {
return $result;
}
$error = null;
$matched = $this->match_request_to_handler( $request );
if ( is_wp_error( $matched ) ) {
return $this->error_to_response( $matched );
}
list( $route, $handler ) = $matched;
if ( ! is_callable( $handler['callback'] ) ) {
$error = new WP_Error(
'rest_invalid_handler',
__( 'The handler for the route is invalid' ),
array( 'status' => 500 )
);
}
if ( ! is_wp_error( $error ) ) {
$check_required = $request->has_valid_params();
if ( is_wp_error( $check_required ) ) {
$error = $check_required;
} else {
$check_sanitized = $request->sanitize_params();
if ( is_wp_error( $check_sanitized ) ) {
$error = $check_sanitized;
}
}
}
return $this->respond_to_request( $request, $route, $handler, $error );
}
/**
* Matches a request object to it's handler.
*
* @access private
* @since 5.6.0
*
* @param WP_REST_Request $request The request object.
* @return array|WP_Error The route and request handler on success or a WP_Error instance if no handler was found.
*/
public function match_request_to_handler( $request ) {
$method = $request->get_method();
$path = $request->get_route();
@ -957,17 +999,9 @@ class WP_REST_Server {
}
if ( ! is_callable( $callback ) ) {
$response = new WP_Error(
'rest_invalid_handler',
__( 'The handler for the route is invalid' ),
array( 'status' => 500 )
);
return array( $route, $handler );
}
if ( ! is_wp_error( $response ) ) {
// Remove the redundant preg_match argument.
unset( $args[0] );
$request->set_url_params( $args );
$request->set_attributes( $handler );
@ -981,17 +1015,31 @@ class WP_REST_Server {
$request->set_default_params( $defaults );
$check_required = $request->has_valid_params();
if ( is_wp_error( $check_required ) ) {
$response = $check_required;
} else {
$check_sanitized = $request->sanitize_params();
if ( is_wp_error( $check_sanitized ) ) {
$response = $check_sanitized;
}
return array( $route, $handler );
}
}
return new WP_Error(
'rest_no_route',
__( 'No route was found matching the URL and request method' ),
array( 'status' => 404 )
);
}
/**
* Dispatches the request to the callback handler.
*
* @access private
* @since 5.6.0
*
* @param WP_REST_Request $request The request object.
* @param array $handler The matched route handler.
* @param string $route The matched route regex.
* @param WP_Error|null $response The current error object if any.
*
* @return WP_REST_Response
*/
public function respond_to_request( $request, $route, $handler, $response ) {
/**
* Filters the response before executing any REST API callbacks.
*
@ -1010,9 +1058,8 @@ class WP_REST_Server {
*/
$response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );
if ( ! is_wp_error( $response ) ) {
// Check permission specified on the route.
if ( ! empty( $handler['permission_callback'] ) ) {
if ( ! is_wp_error( $response ) && ! empty( $handler['permission_callback'] ) ) {
$permission = call_user_func( $handler['permission_callback'], $request );
if ( is_wp_error( $permission ) ) {
@ -1025,7 +1072,6 @@ class WP_REST_Server {
);
}
}
}
if ( ! is_wp_error( $response ) ) {
/**
@ -1047,7 +1093,7 @@ class WP_REST_Server {
if ( null !== $dispatch_result ) {
$response = $dispatch_result;
} else {
$response = call_user_func( $callback, $request );
$response = call_user_func( $handler['callback'], $request );
}
}
@ -1084,16 +1130,6 @@ class WP_REST_Server {
return $response;
}
}
return $this->error_to_response(
new WP_Error(
'rest_no_route',
__( 'No route was found matching the URL and request method' ),
array( 'status' => 404 )
)
);
}
/**
* Returns if an error occurred during most recent JSON encode/decode.

View File

@ -1513,6 +1513,110 @@ class Tests_REST_Server extends WP_Test_REST_TestCase {
$this->assertSame( 204, $response->get_status(), '/test-ns/v1/test' );
}
/**
* @ticket 50244
*/
public function test_no_route() {
$mock_hook = new MockAction();
add_filter( 'rest_request_after_callbacks', array( $mock_hook, 'filter' ) );
$response = rest_do_request( '/test-ns/v1/test' );
$this->assertErrorResponse( 'rest_no_route', $response, 404 );
// Verify that the no route error was not filtered.
$this->assertCount( 0, $mock_hook->get_events() );
}
/**
* @ticket 50244
*/
public function test_invalid_handler() {
register_rest_route(
'test-ns/v1',
'/test',
array(
'callback' => 'invalid_callback',
'permission_callback' => '__return_true',
)
);
$mock_hook = new MockAction();
add_filter( 'rest_request_after_callbacks', array( $mock_hook, 'filter' ) );
$response = rest_do_request( '/test-ns/v1/test' );
$this->assertErrorResponse( 'rest_invalid_handler', $response, 500 );
// Verify that the invalid handler error was filtered.
$events = $mock_hook->get_events();
$this->assertCount( 1, $events );
$this->assertWPError( $events[0]['args'][0] );
$this->assertEquals( 'rest_invalid_handler', $events[0]['args'][0]->get_error_code() );
}
/**
* @ticket 50244
*/
public function test_callbacks_are_not_executed_if_request_validation_fails() {
$callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) );
$callback->expects( self::never() )->method( '__invoke' );
$permission_callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) );
$permission_callback->expects( self::never() )->method( '__invoke' );
register_rest_route(
'test-ns/v1',
'/test',
array(
'callback' => $callback,
'permission_callback' => $permission_callback,
'args' => array(
'test' => array(
'validate_callback' => '__return_false',
),
),
)
);
$request = new WP_REST_Request( 'GET', '/test-ns/v1/test' );
$request->set_query_params( array( 'test' => 'world' ) );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
}
/**
* @ticket 50244
*/
public function test_filters_are_executed_if_request_validation_fails() {
register_rest_route(
'test-ns/v1',
'/test',
array(
'callback' => '__return_empty_array',
'permission_callback' => '__return_true',
'args' => array(
'test' => array(
'validate_callback' => '__return_false',
),
),
)
);
$mock_hook = new MockAction();
add_filter( 'rest_request_after_callbacks', array( $mock_hook, 'filter' ) );
$request = new WP_REST_Request( 'GET', '/test-ns/v1/test' );
$request->set_query_params( array( 'test' => 'world' ) );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
// Verify that the invalid param error was filtered.
$events = $mock_hook->get_events();
$this->assertCount( 1, $events );
$this->assertWPError( $events[0]['args'][0] );
$this->assertEquals( 'rest_invalid_param', $events[0]['args'][0]->get_error_code() );
}
public function _validate_as_integer_123( $value, $request, $key ) {
if ( ! is_int( $value ) ) {
return new WP_Error( 'some-error', 'This is not valid!' );