Make WordPress Core

Changeset 48947


Ignore:
Timestamp:
09/05/2020 09:50:31 PM (4 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.