Make WordPress Core

Changeset 48947


Ignore:
Timestamp:
09/05/2020 09:50:31 PM (6 years ago)
Author:
TimothyBlynJacobs
Message:

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.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/rest-api/class-wp-rest-server.php

    r48576 r48947  
    912912                }
    913913
     914                $error   = null;
     915                $matched = $this->match_request_to_handler( $request );
     916
     917                if ( is_wp_error( $matched ) ) {
     918                        return $this->error_to_response( $matched );
     919                }
     920
     921                list( $route, $handler ) = $matched;
     922
     923                if ( ! is_callable( $handler['callback'] ) ) {
     924                        $error = new WP_Error(
     925                                'rest_invalid_handler',
     926                                __( 'The handler for the route is invalid' ),
     927                                array( 'status' => 500 )
     928                        );
     929                }
     930
     931                if ( ! is_wp_error( $error ) ) {
     932                        $check_required = $request->has_valid_params();
     933                        if ( is_wp_error( $check_required ) ) {
     934                                $error = $check_required;
     935                        } else {
     936                                $check_sanitized = $request->sanitize_params();
     937                                if ( is_wp_error( $check_sanitized ) ) {
     938                                        $error = $check_sanitized;
     939                                }
     940                        }
     941                }
     942
     943                return $this->respond_to_request( $request, $route, $handler, $error );
     944        }
     945
     946        /**
     947         * Matches a request object to it's handler.
     948         *
     949         * @access private
     950         * @since 5.6.0
     951         *
     952         * @param WP_REST_Request $request The request object.
     953         * @return array|WP_Error The route and request handler on success or a WP_Error instance if no handler was found.
     954         */
     955        public function match_request_to_handler( $request ) {
    914956                $method = $request->get_method();
    915957                $path   = $request->get_route();
     
    9581000
    9591001                                if ( ! is_callable( $callback ) ) {
    960                                         $response = new WP_Error(
    961                                                 'rest_invalid_handler',
    962                                                 __( 'The handler for the route is invalid' ),
    963                                                 array( 'status' => 500 )
    964                                         );
    965                                 }
    966 
    967                                 if ( ! is_wp_error( $response ) ) {
    968                                         // Remove the redundant preg_match argument.
    969                                         unset( $args[0] );
    970 
    971                                         $request->set_url_params( $args );
    972                                         $request->set_attributes( $handler );
    973 
    974                                         $defaults = array();
    975 
    976                                         foreach ( $handler['args'] as $arg => $options ) {
    977                                                 if ( isset( $options['default'] ) ) {
    978                                                         $defaults[ $arg ] = $options['default'];
    979                                                 }
     1002                                        return array( $route, $handler );
     1003                                }
     1004
     1005                                $request->set_url_params( $args );
     1006                                $request->set_attributes( $handler );
     1007
     1008                                $defaults = array();
     1009
     1010                                foreach ( $handler['args'] as $arg => $options ) {
     1011                                        if ( isset( $options['default'] ) ) {
     1012                                                $defaults[ $arg ] = $options['default'];
    9801013                                        }
    981 
    982                                         $request->set_default_params( $defaults );
    983 
    984                                         $check_required = $request->has_valid_params();
    985                                         if ( is_wp_error( $check_required ) ) {
    986                                                 $response = $check_required;
    987                                         } else {
    988                                                 $check_sanitized = $request->sanitize_params();
    989                                                 if ( is_wp_error( $check_sanitized ) ) {
    990                                                         $response = $check_sanitized;
    991                                                 }
    992                                         }
    993                                 }
    994 
    995                                 /**
    996                                  * Filters the response before executing any REST API callbacks.
    997                                  *
    998                                  * Allows plugins to perform additional validation after a
    999                                  * request is initialized and matched to a registered route,
    1000                                  * but before it is executed.
    1001                                  *
    1002                                  * Note that this filter will not be called for requests that
    1003                                  * fail to authenticate or match to a registered route.
    1004                                  *
    1005                                  * @since 4.7.0
    1006                                  *
    1007                                  * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. Usually a WP_REST_Response or WP_Error.
    1008                                  * @param array                                            $handler  Route handler used for the request.
    1009                                  * @param WP_REST_Request                                  $request  Request used to generate the response.
    1010                                  */
    1011                                 $response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );
    1012 
    1013                                 if ( ! is_wp_error( $response ) ) {
    1014                                         // Check permission specified on the route.
    1015                                         if ( ! empty( $handler['permission_callback'] ) ) {
    1016                                                 $permission = call_user_func( $handler['permission_callback'], $request );
    1017 
    1018                                                 if ( is_wp_error( $permission ) ) {
    1019                                                         $response = $permission;
    1020                                                 } elseif ( false === $permission || null === $permission ) {
    1021                                                         $response = new WP_Error(
    1022                                                                 'rest_forbidden',
    1023                                                                 __( 'Sorry, you are not allowed to do that.' ),
    1024                                                                 array( 'status' => rest_authorization_required_code() )
    1025                                                         );
    1026                                                 }
    1027                                         }
    1028                                 }
    1029 
    1030                                 if ( ! is_wp_error( $response ) ) {
    1031                                         /**
    1032                                          * Filters the REST dispatch request result.
    1033                                          *
    1034                                          * Allow plugins to override dispatching the request.
    1035                                          *
    1036                                          * @since 4.4.0
    1037                                          * @since 4.5.0 Added `$route` and `$handler` parameters.
    1038                                          *
    1039                                          * @param mixed           $dispatch_result Dispatch result, will be used if not empty.
    1040                                          * @param WP_REST_Request $request         Request used to generate the response.
    1041                                          * @param string          $route           Route matched for the request.
    1042                                          * @param array           $handler         Route handler used for the request.
    1043                                          */
    1044                                         $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
    1045 
    1046                                         // Allow plugins to halt the request via this filter.
    1047                                         if ( null !== $dispatch_result ) {
    1048                                                 $response = $dispatch_result;
    1049                                         } else {
    1050                                                 $response = call_user_func( $callback, $request );
    1051                                         }
    1052                                 }
    1053 
    1054                                 /**
    1055                                  * Filters the response immediately after executing any REST API
    1056                                  * callbacks.
    1057                                  *
    1058                                  * Allows plugins to perform any needed cleanup, for example,
    1059                                  * to undo changes made during the {@see 'rest_request_before_callbacks'}
    1060                                  * filter.
    1061                                  *
    1062                                  * Note that this filter will not be called for requests that
    1063                                  * fail to authenticate or match to a registered route.
    1064                                  *
    1065                                  * Note that an endpoint's `permission_callback` can still be
    1066                                  * called after this filter - see `rest_send_allow_header()`.
    1067                                  *
    1068                                  * @since 4.7.0
    1069                                  *
    1070                                  * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. Usually a WP_REST_Response or WP_Error.
    1071                                  * @param array                                            $handler  Route handler used for the request.
    1072                                  * @param WP_REST_Request                                  $request  Request used to generate the response.
    1073                                  */
    1074                                 $response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request );
    1075 
    1076                                 if ( is_wp_error( $response ) ) {
    1077                                         $response = $this->error_to_response( $response );
    1078                                 } else {
    1079                                         $response = rest_ensure_response( $response );
    1080                                 }
    1081 
    1082                                 $response->set_matched_route( $route );
    1083                                 $response->set_matched_handler( $handler );
    1084 
    1085                                 return $response;
    1086                         }
    1087                 }
    1088 
    1089                 return $this->error_to_response(
    1090                         new WP_Error(
    1091                                 'rest_no_route',
    1092                                 __( 'No route was found matching the URL and request method' ),
    1093                                 array( 'status' => 404 )
    1094                         )
     1014                                }
     1015
     1016                                $request->set_default_params( $defaults );
     1017
     1018                                return array( $route, $handler );
     1019                        }
     1020                }
     1021
     1022                return new WP_Error(
     1023                        'rest_no_route',
     1024                        __( 'No route was found matching the URL and request method' ),
     1025                        array( 'status' => 404 )
    10951026                );
     1027        }
     1028
     1029        /**
     1030         * Dispatches the request to the callback handler.
     1031         *
     1032         * @access private
     1033         * @since 5.6.0
     1034         *
     1035         * @param WP_REST_Request $request  The request object.
     1036         * @param array           $handler  The matched route handler.
     1037         * @param string          $route    The matched route regex.
     1038         * @param WP_Error|null   $response The current error object if any.
     1039         *
     1040         * @return WP_REST_Response
     1041         */
     1042        public function respond_to_request( $request, $route, $handler, $response ) {
     1043                /**
     1044                 * Filters the response before executing any REST API callbacks.
     1045                 *
     1046                 * Allows plugins to perform additional validation after a
     1047                 * request is initialized and matched to a registered route,
     1048                 * but before it is executed.
     1049                 *
     1050                 * Note that this filter will not be called for requests that
     1051                 * fail to authenticate or match to a registered route.
     1052                 *
     1053                 * @since 4.7.0
     1054                 *
     1055                 * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. Usually a WP_REST_Response or WP_Error.
     1056                 * @param array                                            $handler  Route handler used for the request.
     1057                 * @param WP_REST_Request                                  $request  Request used to generate the response.
     1058                 */
     1059                $response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );
     1060
     1061                // Check permission specified on the route.
     1062                if ( ! is_wp_error( $response ) && ! empty( $handler['permission_callback'] ) ) {
     1063                        $permission = call_user_func( $handler['permission_callback'], $request );
     1064
     1065                        if ( is_wp_error( $permission ) ) {
     1066                                $response = $permission;
     1067                        } elseif ( false === $permission || null === $permission ) {
     1068                                $response = new WP_Error(
     1069                                        'rest_forbidden',
     1070                                        __( 'Sorry, you are not allowed to do that.' ),
     1071                                        array( 'status' => rest_authorization_required_code() )
     1072                                );
     1073                        }
     1074                }
     1075
     1076                if ( ! is_wp_error( $response ) ) {
     1077                        /**
     1078                         * Filters the REST dispatch request result.
     1079                         *
     1080                         * Allow plugins to override dispatching the request.
     1081                         *
     1082                         * @since 4.4.0
     1083                         * @since 4.5.0 Added `$route` and `$handler` parameters.
     1084                         *
     1085                         * @param mixed           $dispatch_result Dispatch result, will be used if not empty.
     1086                         * @param WP_REST_Request $request         Request used to generate the response.
     1087                         * @param string          $route           Route matched for the request.
     1088                         * @param array           $handler         Route handler used for the request.
     1089                         */
     1090                        $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
     1091
     1092                        // Allow plugins to halt the request via this filter.
     1093                        if ( null !== $dispatch_result ) {
     1094                                $response = $dispatch_result;
     1095                        } else {
     1096                                $response = call_user_func( $handler['callback'], $request );
     1097                        }
     1098                }
     1099
     1100                /**
     1101                 * Filters the response immediately after executing any REST API
     1102                 * callbacks.
     1103                 *
     1104                 * Allows plugins to perform any needed cleanup, for example,
     1105                 * to undo changes made during the {@see 'rest_request_before_callbacks'}
     1106                 * filter.
     1107                 *
     1108                 * Note that this filter will not be called for requests that
     1109                 * fail to authenticate or match to a registered route.
     1110                 *
     1111                 * Note that an endpoint's `permission_callback` can still be
     1112                 * called after this filter - see `rest_send_allow_header()`.
     1113                 *
     1114                 * @since 4.7.0
     1115                 *
     1116                 * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. Usually a WP_REST_Response or WP_Error.
     1117                 * @param array                                            $handler  Route handler used for the request.
     1118                 * @param WP_REST_Request                                  $request  Request used to generate the response.
     1119                 */
     1120                $response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request );
     1121
     1122                if ( is_wp_error( $response ) ) {
     1123                        $response = $this->error_to_response( $response );
     1124                } else {
     1125                        $response = rest_ensure_response( $response );
     1126                }
     1127
     1128                $response->set_matched_route( $route );
     1129                $response->set_matched_handler( $handler );
     1130
     1131                return $response;
    10961132        }
    10971133
  • trunk/tests/phpunit/tests/rest-api/rest-server.php

    r48939 r48947  
    15141514        }
    15151515
     1516        /**
     1517         * @ticket 50244
     1518         */
     1519        public function test_no_route() {
     1520                $mock_hook = new MockAction();
     1521                add_filter( 'rest_request_after_callbacks', array( $mock_hook, 'filter' ) );
     1522
     1523                $response = rest_do_request( '/test-ns/v1/test' );
     1524                $this->assertErrorResponse( 'rest_no_route', $response, 404 );
     1525
     1526                // Verify that the no route error was not filtered.
     1527                $this->assertCount( 0, $mock_hook->get_events() );
     1528        }
     1529
     1530        /**
     1531         * @ticket 50244
     1532         */
     1533        public function test_invalid_handler() {
     1534                register_rest_route(
     1535                        'test-ns/v1',
     1536                        '/test',
     1537                        array(
     1538                                'callback'            => 'invalid_callback',
     1539                                'permission_callback' => '__return_true',
     1540                        )
     1541                );
     1542
     1543                $mock_hook = new MockAction();
     1544                add_filter( 'rest_request_after_callbacks', array( $mock_hook, 'filter' ) );
     1545
     1546                $response = rest_do_request( '/test-ns/v1/test' );
     1547                $this->assertErrorResponse( 'rest_invalid_handler', $response, 500 );
     1548
     1549                // Verify that the invalid handler error was filtered.
     1550                $events = $mock_hook->get_events();
     1551                $this->assertCount( 1, $events );
     1552                $this->assertWPError( $events[0]['args'][0] );
     1553                $this->assertEquals( 'rest_invalid_handler', $events[0]['args'][0]->get_error_code() );
     1554        }
     1555
     1556        /**
     1557         * @ticket 50244
     1558         */
     1559        public function test_callbacks_are_not_executed_if_request_validation_fails() {
     1560                $callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) );
     1561                $callback->expects( self::never() )->method( '__invoke' );
     1562                $permission_callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) );
     1563                $permission_callback->expects( self::never() )->method( '__invoke' );
     1564
     1565                register_rest_route(
     1566                        'test-ns/v1',
     1567                        '/test',
     1568                        array(
     1569                                'callback'            => $callback,
     1570                                'permission_callback' => $permission_callback,
     1571                                'args'                => array(
     1572                                        'test' => array(
     1573                                                'validate_callback' => '__return_false',
     1574                                        ),
     1575                                ),
     1576                        )
     1577                );
     1578
     1579                $request = new WP_REST_Request( 'GET', '/test-ns/v1/test' );
     1580                $request->set_query_params( array( 'test' => 'world' ) );
     1581                $response = rest_do_request( $request );
     1582
     1583                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1584        }
     1585
     1586        /**
     1587         * @ticket 50244
     1588         */
     1589        public function test_filters_are_executed_if_request_validation_fails() {
     1590                register_rest_route(
     1591                        'test-ns/v1',
     1592                        '/test',
     1593                        array(
     1594                                'callback'            => '__return_empty_array',
     1595                                'permission_callback' => '__return_true',
     1596                                'args'                => array(
     1597                                        'test' => array(
     1598                                                'validate_callback' => '__return_false',
     1599                                        ),
     1600                                ),
     1601                        )
     1602                );
     1603
     1604                $mock_hook = new MockAction();
     1605                add_filter( 'rest_request_after_callbacks', array( $mock_hook, 'filter' ) );
     1606
     1607                $request = new WP_REST_Request( 'GET', '/test-ns/v1/test' );
     1608                $request->set_query_params( array( 'test' => 'world' ) );
     1609                $response = rest_do_request( $request );
     1610
     1611                $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     1612
     1613                // Verify that the invalid param error was filtered.
     1614                $events = $mock_hook->get_events();
     1615                $this->assertCount( 1, $events );
     1616                $this->assertWPError( $events[0]['args'][0] );
     1617                $this->assertEquals( 'rest_invalid_param', $events[0]['args'][0]->get_error_code() );
     1618        }
     1619
    15161620        public function _validate_as_integer_123( $value, $request, $key ) {
    15171621                if ( ! is_int( $value ) ) {
Note: See TracChangeset for help on using the changeset viewer.