From 88ec90d6dfa31154f50a0246265f30adec09f67a Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Sat, 5 Sep 2020 18:07:46 +0000 Subject: [PATCH] REST API: Support a route-level validation callback. Most request data is validated on a per-parameter basis. Often, however, additional validation is needed that operates on the entire request object. Currently, this is done in the route callback and often in the `prepare_item_for_database` method specifically. #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 patch introduces support for calling a `validate_callback` after all parameter validation has succeeded. That allows moving more validation outside of the route callback and into `WP_REST_Request` which will improve "pre-validate" support. Props TimothyBlynJacobs, zieladam. Fixes #51255. See #50244. git-svn-id: https://develop.svn.wordpress.org/trunk@48945 602fd350-edb4-49c9-b593-d223f7449a82 --- .../rest-api/class-wp-rest-request.php | 24 ++++--- tests/phpunit/tests/rest-api/rest-request.php | 68 +++++++++++++++++++ 2 files changed, 84 insertions(+), 8 deletions(-) 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 73548d4f38..937b5684cd 100644 --- a/src/wp-includes/rest-api/class-wp-rest-request.php +++ b/src/wp-includes/rest-api/class-wp-rest-request.php @@ -858,13 +858,9 @@ class WP_REST_Request implements ArrayAccess { $attributes = $this->get_attributes(); $required = array(); - // No arguments set, skip validation. - if ( empty( $attributes['args'] ) ) { - return true; - } - - foreach ( $attributes['args'] as $key => $arg ) { + $args = empty( $attributes['args'] ) ? array() : $attributes['args']; + foreach ( $args as $key => $arg ) { $param = $this->get_param( $key ); if ( isset( $arg['required'] ) && true === $arg['required'] && null === $param ) { $required[] = $key; @@ -890,7 +886,7 @@ class WP_REST_Request implements ArrayAccess { */ $invalid_params = array(); - foreach ( $attributes['args'] as $key => $arg ) { + foreach ( $args as $key => $arg ) { $param = $this->get_param( $key ); @@ -919,8 +915,20 @@ class WP_REST_Request implements ArrayAccess { ); } - return true; + if ( isset( $attributes['validate_callback'] ) ) { + $valid_check = call_user_func( $attributes['validate_callback'], $this ); + if ( is_wp_error( $valid_check ) ) { + return $valid_check; + } + + if ( false === $valid_check ) { + // A WP_Error instance is preferred, but false is supported for parity with the per-arg validate_callback. + return new WP_Error( 'rest_invalid_params', __( 'Invalid parameters.' ), array( 'status' => 400 ) ); + } + } + + return true; } /** diff --git a/tests/phpunit/tests/rest-api/rest-request.php b/tests/phpunit/tests/rest-api/rest-request.php index 7ad78d81aa..d2f5021ce2 100644 --- a/tests/phpunit/tests/rest-api/rest-request.php +++ b/tests/phpunit/tests/rest-api/rest-request.php @@ -779,4 +779,72 @@ class Tests_REST_Request extends WP_UnitTestCase { $this->assertTrue( $request->has_param( 'param' ) ); $this->assertSame( 'value', $request->get_param( 'param' ) ); } + + /** + * @ticket 51255 + */ + public function test_route_level_validate_callback() { + $request = new WP_REST_Request(); + $request->set_query_params( array( 'test' => 'value' ) ); + + $error = new WP_Error( 'error_code', __( 'Error Message' ), array( 'status' => 400 ) ); + $callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) ); + $callback->expects( self::once() )->method( '__invoke' )->with( self::identicalTo( $request ) )->willReturn( $error ); + $request->set_attributes( + array( + 'args' => array( + 'test' => array( + 'validate_callback' => '__return_true', + ), + ), + 'validate_callback' => $callback, + ) + ); + + $this->assertSame( $error, $request->has_valid_params() ); + } + + /** + * @ticket 51255 + */ + public function test_route_level_validate_callback_no_parameter_callbacks() { + $request = new WP_REST_Request(); + $request->set_query_params( array( 'test' => 'value' ) ); + + $error = new WP_Error( 'error_code', __( 'Error Message' ), array( 'status' => 400 ) ); + $callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) ); + $callback->expects( self::once() )->method( '__invoke' )->with( self::identicalTo( $request ) )->willReturn( $error ); + $request->set_attributes( + array( + 'validate_callback' => $callback, + ) + ); + + $this->assertSame( $error, $request->has_valid_params() ); + } + + /** + * @ticket 51255 + */ + public function test_route_level_validate_callback_is_not_executed_if_parameter_validation_fails() { + $request = new WP_REST_Request(); + $request->set_query_params( array( 'test' => 'value' ) ); + + $callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) ); + $callback->expects( self::never() )->method( '__invoke' ); + $request->set_attributes( + array( + 'validate_callback' => $callback, + 'args' => array( + 'test' => array( + 'validate_callback' => '__return_false', + ), + ), + ) + ); + + $valid = $request->has_valid_params(); + $this->assertWPError( $valid ); + $this->assertEquals( 'rest_invalid_param', $valid->get_error_code() ); + } }