From 7a5e12858c53eaac4c5ad25f0c78bf9b66296403 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 21 Oct 2026 20:23:37 +0100 Subject: [PATCH 1/7] REST API: Allow routes to disable `_fields`, `_embed` and response links. Add the `allow_fields`, `allow_embed` and `allow_links` route and handler options. When an option is false, the `_fields` parameter does not filter the response data, the `_embed` parameter does not embed linked resources, or the response links are not added to the response data as `_links`. A handler option takes precedence over the route option, as with `allow_batch`. This lets a route serve its response data as returned, for example data that follows a schema of its own or that is not an array. See #66267. --- src/wp-includes/rest-api.php | 5 + .../rest-api/class-wp-rest-server.php | 45 +++- tests/phpunit/tests/rest-api/rest-server.php | 250 ++++++++++++++++++ 3 files changed, 298 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 9998e14da7361..b24fd381b29bf 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -950,6 +950,7 @@ function _rest_array_intersect_key_recursive( $array1, $array2 ) { * Filters the REST API response to include only an allow-listed set of response object fields. * * @since 4.8.0 + * @since 7.2.0 The response is left unchanged when the route disables `allow_fields`. * * @param WP_REST_Response $response Current response being served. * @param WP_REST_Server $server ResponseHandler instance (usually WP_REST_Server). @@ -961,6 +962,10 @@ function rest_filter_response_fields( $response, $server, $request ) { return $response; } + if ( $server instanceof WP_REST_Server && ! $server->is_response_option_enabled( $response, 'allow_fields' ) ) { + return $response; + } + $data = $response->get_data(); $fields = wp_parse_list( $request['_fields'] ); diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index 179edb04c8b68..58713a9933f52 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -40,6 +40,9 @@ * accept_raw: bool, * show_in_index: bool, * allow_batch?: array{v1?: bool}|false, + * allow_fields?: bool, + * allow_embed?: bool, + * allow_links?: bool, * ... * } */ @@ -622,6 +625,7 @@ public function serve_request( $path = null ) { * * @since 4.4.0 * @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include. + * @since 7.2.0 Links are not added or embedded when the route disables `allow_links` or `allow_embed`. * * @param WP_REST_Response $response Response object. * @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links. @@ -634,14 +638,14 @@ public function serve_request( $path = null ) { */ public function response_to_data( $response, $embed ) { $data = $response->get_data(); - $links = self::get_compact_response_links( $response ); + $links = $this->is_response_option_enabled( $response, 'allow_links' ) ? self::get_compact_response_links( $response ) : array(); if ( ! empty( $links ) ) { // Convert links to part of the data. $data['_links'] = $links; } - if ( $embed ) { + if ( $embed && $this->is_response_option_enabled( $response, 'allow_embed' ) ) { $this->embed_cache = array(); // Determine if this is a numeric array. if ( wp_is_numeric_array( $data ) ) { @@ -1131,6 +1135,43 @@ public function get_route_options( $route ) { return $this->route_options[ $route ]; } + /** + * Determines whether a response option is enabled for the route that generated a response. + * + * Routes, or individual handlers of a route, can set these options to false: + * + * - `allow_fields`: Whether the `_fields` parameter filters the response data. + * - `allow_embed`: Whether the `_embed` parameter embeds linked resources in the response data. + * - `allow_links`: Whether the response links are added to the response data as `_links`. + * + * A handler option takes precedence over the route option of the same name. + * + * @since 7.2.0 + * + * @param WP_HTTP_Response $response Response object. + * @param string $option Option name. Accepts 'allow_fields', 'allow_embed', or 'allow_links'. + * @return bool Whether the option is enabled. True when neither the handler nor the route sets it. + */ + public function is_response_option_enabled( $response, $option ) { + if ( ! $response instanceof WP_REST_Response ) { + return true; + } + + $handler = $response->get_matched_handler(); + if ( is_array( $handler ) && isset( $handler[ $option ] ) ) { + return (bool) $handler[ $option ]; + } + + $route = $response->get_matched_route(); + if ( ! is_string( $route ) || '' === $route ) { + return true; + } + + $route_options = $this->get_route_options( $route ); + + return ! isset( $route_options[ $option ] ) || (bool) $route_options[ $option ]; + } + /** * Matches the request to a callback and call it. * diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index c4d6a883d67b1..3dbacc81b9035 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -2900,4 +2900,254 @@ public function test_prefers_developer_defined_target_hints() { $this->assertArrayHasKey( 'allow', $link['targetHints'] ); $this->assertSame( array( 'GET', 'PUT' ), $link['targetHints']['allow'] ); } + + /** + * Registers a route for the response option tests, and a route that its link embeds. + * + * @param mixed $data The response data. + * @param bool $with_link Whether the response has an embeddable link. + * @param array $handler_options Options for the route handler. + * @param array $route_options Options for the route. + */ + private function register_response_options_route( $data, bool $with_link, array $handler_options = array(), array $route_options = array() ): void { + rest_get_server()->register_route( + 'test-ns', + '/test-ns/response-options', + array_merge( + array( + array_merge( + array( + 'methods' => 'GET', + 'callback' => static function () use ( $data, $with_link ) { + $response = new WP_REST_Response( $data ); + if ( $with_link ) { + $response->add_link( 'alternate', rest_url( '/test-ns/embedded' ), array( 'embeddable' => true ) ); + } + return $response; + }, + 'permission_callback' => '__return_true', + ), + $handler_options + ), + ), + $route_options + ) + ); + + rest_get_server()->register_route( + 'test-ns', + '/test-ns/embedded', + array( + 'methods' => 'GET', + 'callback' => static function () { + return array( 'hello' => 'world' ); + }, + 'permission_callback' => '__return_true', + ) + ); + } + + /** + * Data provider for test_allow_fields_false_leaves_the_data_unfiltered(). + * + * @return array + */ + public static function data_allow_fields_false_leaves_the_data_unfiltered(): array { + return array( + 'associative array' => array( + array( + 'a' => 1, + 'b' => 2, + ), + '{"a":1,"b":2}', + ), + 'list' => array( array( array( 'b' => 2 ), 'c' ), '[{"b":2},"c"]' ), + 'empty object' => array( new stdClass(), '{}' ), + 'object' => array( (object) array( 'b' => 2 ), '{"b":2}' ), + 'string' => array( 'value', '"value"' ), + 'integer' => array( 5, '5' ), + ); + } + + /** + * @ticket 66267 + * @dataProvider data_allow_fields_false_leaves_the_data_unfiltered + * + * @param mixed $data The response data. + * @param string $expected The expected response body. + */ + public function test_allow_fields_false_leaves_the_data_unfiltered( $data, string $expected ): void { + $this->register_response_options_route( $data, false, array( 'allow_fields' => false ) ); + + $_GET['_fields'] = 'a'; + rest_get_server()->serve_request( '/test-ns/response-options' ); + + $this->assertSame( $expected, rest_get_server()->sent_body, 'The _fields parameter should not filter the data.' ); + } + + /** + * @ticket 66267 + */ + public function test_fields_are_filtered_by_default(): void { + $this->register_response_options_route( + array( + 'a' => 1, + 'b' => 2, + ), + false + ); + + $_GET['_fields'] = 'a'; + rest_get_server()->serve_request( '/test-ns/response-options' ); + + $this->assertSame( '{"a":1}', rest_get_server()->sent_body, 'The _fields parameter should filter the data.' ); + } + + /** + * Data provider for test_allow_links_false_leaves_out_the_links(). + * + * @return array + */ + public static function data_allow_links_false_leaves_out_the_links(): array { + return array( + 'associative array' => array( array( 'a' => 1 ), '{"a":1}' ), + 'list' => array( array( 'a', 'b' ), '["a","b"]' ), + 'empty object' => array( new stdClass(), '{}' ), + 'string' => array( 'value', '"value"' ), + ); + } + + /** + * @ticket 66267 + * @dataProvider data_allow_links_false_leaves_out_the_links + * + * @param mixed $data The response data. + * @param string $expected The expected response body. + */ + public function test_allow_links_false_leaves_out_the_links( $data, string $expected ): void { + $this->register_response_options_route( $data, true, array( 'allow_links' => false ) ); + + rest_get_server()->serve_request( '/test-ns/response-options' ); + + $this->assertSame( $expected, rest_get_server()->sent_body, 'The response links should not be added.' ); + } + + /** + * @ticket 66267 + */ + public function test_allow_embed_false_keeps_the_links_without_embedding(): void { + $this->register_response_options_route( array( 'a' => 1 ), true, array( 'allow_embed' => false ) ); + + $_GET['_embed'] = '1'; + rest_get_server()->serve_request( '/test-ns/response-options' ); + $data = json_decode( rest_get_server()->sent_body, true ); + + $this->assertArrayHasKey( '_links', $data, 'The response links should be added.' ); + $this->assertArrayNotHasKey( '_embedded', $data, 'The links should not be embedded.' ); + } + + /** + * @ticket 66267 + */ + public function test_links_are_added_and_embedded_by_default(): void { + $this->register_response_options_route( array( 'a' => 1 ), true ); + + $_GET['_embed'] = '1'; + rest_get_server()->serve_request( '/test-ns/response-options' ); + $data = json_decode( rest_get_server()->sent_body, true ); + + $this->assertArrayHasKey( '_links', $data, 'The response links should be added.' ); + $this->assertSame( array( array( 'hello' => 'world' ) ), $data['_embedded']['alternate'], 'The link should be embedded.' ); + } + + /** + * @ticket 66267 + */ + public function test_allow_embed_false_applies_to_enveloped_responses(): void { + $this->register_response_options_route( array( 'a' => 1 ), true, array( 'allow_embed' => false ) ); + + $_GET['_embed'] = '1'; + $_GET['_envelope'] = '1'; + rest_get_server()->serve_request( '/test-ns/response-options' ); + $data = json_decode( rest_get_server()->sent_body, true ); + + $this->assertSame( 200, $data['status'], 'The enveloped response should succeed.' ); + $this->assertArrayHasKey( '_links', $data['body'], 'The response links should be added to the body.' ); + $this->assertArrayNotHasKey( '_embedded', $data['body'], 'The links should not be embedded in the body.' ); + } + + /** + * Data provider for test_handler_option_takes_precedence_over_route_option(). + * + * @return array, 1: array, 2: string}> + */ + public static function data_handler_option_takes_precedence_over_route_option(): array { + return array( + 'route disables' => array( array(), array( 'allow_fields' => false ), '{"a":1,"b":2}' ), + 'route disables and handler enables' => array( array( 'allow_fields' => true ), array( 'allow_fields' => false ), '{"a":1}' ), + 'route enables and handler disables' => array( array( 'allow_fields' => false ), array( 'allow_fields' => true ), '{"a":1,"b":2}' ), + 'handler disables another option' => array( array( 'allow_links' => false ), array(), '{"a":1}' ), + ); + } + + /** + * @ticket 66267 + * @dataProvider data_handler_option_takes_precedence_over_route_option + * + * @param array $handler_options Options for the route handler. + * @param array $route_options Options for the route. + * @param string $expected The expected response body. + */ + public function test_handler_option_takes_precedence_over_route_option( array $handler_options, array $route_options, string $expected ): void { + $this->register_response_options_route( + array( + 'a' => 1, + 'b' => 2, + ), + false, + $handler_options, + $route_options + ); + + $_GET['_fields'] = 'a'; + rest_get_server()->serve_request( '/test-ns/response-options' ); + + $this->assertSame( $expected, rest_get_server()->sent_body, 'The handler option should take precedence over the route option.' ); + } + + /** + * @ticket 66267 + */ + public function test_response_options_apply_to_preloaded_requests(): void { + $this->register_response_options_route( + array( + 'a' => 1, + 'b' => 2, + ), + true, + array( + 'allow_fields' => false, + 'allow_links' => false, + ) + ); + + $preload = rest_preload_api_request( array(), '/test-ns/response-options?_fields=a&_embed=1' ); + + $this->assertSame( + array( + 'a' => 1, + 'b' => 2, + ), + $preload['/test-ns/response-options?_fields=a&_embed=1']['body'], + 'The preloaded data should not be filtered or have links.' + ); + } + + /** + * @ticket 66267 + */ + public function test_is_response_option_enabled_without_a_matched_route(): void { + $this->assertTrue( rest_get_server()->is_response_option_enabled( new WP_REST_Response(), 'allow_fields' ), 'A response without a matched route should enable the option.' ); + $this->assertTrue( rest_get_server()->is_response_option_enabled( new WP_HTTP_Response(), 'allow_links' ), 'A response that is not a REST response should enable the option.' ); + } } From 6684a867440cc582a32e632edd429c22b80dc94a Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 22 Oct 2026 15:15:51 +0100 Subject: [PATCH 2/7] REST API: Let routes turn off `_fields`, `_embed` and response links. Replace the `allow_fields`, `allow_embed` and `allow_links` options with one `response_processing` route and handler option. It is `false` to serve the response data as returned, or an array with `fields`, `embed` and `links` keys to turn off single features: the `_fields` parameter filtering the data, the `_embed` parameter embedding linked resources, and the response links being added to the data as `_links`. Each feature resolves on its own, the handler setting over the route setting, and `embed` is off whenever `links` is off. The settings are resolved once, when the request is matched, and are read from the request attributes and the matched handler by the server, by `rest_filter_response_fields()` and by `WP_REST_Controller::get_fields_for_response()`. A response returned by the `rest_pre_dispatch` filter follows the settings of the route it was returned for. The index lists the settings of an endpoint that turns off a feature, and `rest_preload_api_request()` no longer casts the data to an array. See #66267. --- src/wp-includes/rest-api.php | 12 +- .../rest-api/class-wp-rest-server.php | 252 +++++++--- .../endpoints/class-wp-rest-controller.php | 6 +- .../tests/rest-api/rest-controller.php | 37 ++ tests/phpunit/tests/rest-api/rest-server.php | 466 +++++++++++++++--- 5 files changed, 640 insertions(+), 133 deletions(-) diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index b24fd381b29bf..af645e55c9d1b 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -950,7 +950,7 @@ function _rest_array_intersect_key_recursive( $array1, $array2 ) { * Filters the REST API response to include only an allow-listed set of response object fields. * * @since 4.8.0 - * @since 7.2.0 The response is left unchanged when the route disables `allow_fields`. + * @since 7.2.0 The response is left unchanged when the route turns off the `fields` response processing. * * @param WP_REST_Response $response Current response being served. * @param WP_REST_Server $server ResponseHandler instance (usually WP_REST_Server). @@ -962,8 +962,12 @@ function rest_filter_response_fields( $response, $server, $request ) { return $response; } - if ( $server instanceof WP_REST_Server && ! $server->is_response_option_enabled( $response, 'allow_fields' ) ) { - return $response; + if ( $server instanceof WP_REST_Server ) { + $processing = $server->get_response_processing( $request, $response ); + + if ( ! $processing['fields'] ) { + return $response; + } } $data = $response->get_data(); @@ -3053,7 +3057,7 @@ function rest_preload_api_request( $memo, $path ) { /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ $response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $server, $request ); $embed = $request->has_param( '_embed' ) ? rest_parse_embed_param( $request['_embed'] ) : false; - $data = (array) $server->response_to_data( $response, $embed ); + $data = $server->response_to_data( $response, $embed, $request ); if ( 'OPTIONS' === $method ) { $memo[ $method ][ $path ] = array( diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index 58713a9933f52..a423466396b44 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -40,9 +40,7 @@ * accept_raw: bool, * show_in_index: bool, * allow_batch?: array{v1?: bool}|false, - * allow_fields?: bool, - * allow_embed?: bool, - * allow_links?: bool, + * response_processing?: bool|array{fields?: bool, embed?: bool, links?: bool}, * ... * } */ @@ -515,7 +513,7 @@ public function serve_request( $path = null ) { // Wrap the response in an envelope if asked for. if ( isset( $_GET['_envelope'] ) ) { $embed = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false; - $result = $this->envelope_response( $result, $embed ); + $result = $this->envelope_response( $result, $embed, $request ); } // Send extra data from response objects. @@ -571,7 +569,7 @@ public function serve_request( $path = null ) { // Embed links inside the request. $embed = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false; - $result = $this->response_to_data( $result, $embed ); + $result = $this->response_to_data( $result, $embed, $request ); /** * Filters the REST API response. @@ -625,10 +623,14 @@ public function serve_request( $path = null ) { * * @since 4.4.0 * @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include. - * @since 7.2.0 Links are not added or embedded when the route disables `allow_links` or `allow_embed`. - * - * @param WP_REST_Response $response Response object. - * @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links. + * @since 7.2.0 Added the `$request` parameter. The links are not added or embedded when the + * route turns off the `links` or `embed` response processing. + * + * @param WP_REST_Response $response Response object. + * @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links. + * @param WP_REST_Request|null $request Optional. The request that generated the response. It resolves the + * response processing settings when the response has no matched handler. + * Default null. * @return array { * Data with sub-requests embedded. * @@ -636,16 +638,17 @@ public function serve_request( $path = null ) { * @type array $_embedded Embedded objects. * } */ - public function response_to_data( $response, $embed ) { - $data = $response->get_data(); - $links = $this->is_response_option_enabled( $response, 'allow_links' ) ? self::get_compact_response_links( $response ) : array(); + public function response_to_data( $response, $embed, $request = null ) { + $data = $response->get_data(); + $processing = $this->get_response_processing( $request, $response ); + $links = $processing['links'] ? self::get_compact_response_links( $response ) : array(); if ( ! empty( $links ) ) { // Convert links to part of the data. $data['_links'] = $links; } - if ( $embed && $this->is_response_option_enabled( $response, 'allow_embed' ) ) { + if ( $embed && $processing['embed'] ) { $this->embed_cache = array(); // Determine if this is a numeric array. if ( wp_is_numeric_array( $data ) ) { @@ -882,7 +885,7 @@ protected function embed_links( $data, $embed = true ) { /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ $response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request ); - $this->embed_cache[ $item['href'] ] = $this->response_to_data( $response, false ); + $this->embed_cache[ $item['href'] ] = $this->response_to_data( $response, false, $request ); } $embeds[] = $this->embed_cache[ $item['href'] ]; @@ -912,14 +915,16 @@ protected function embed_links( $data, $embed = true ) { * * @since 4.4.0 * @since 6.0.0 The `$embed` parameter can now contain a list of link relations to include. + * @since 7.2.0 Added the `$request` parameter. * - * @param WP_REST_Response $response Response object. - * @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links. + * @param WP_REST_Response $response Response object. + * @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links. + * @param WP_REST_Request|null $request Optional. The request that generated the response. Default null. * @return WP_REST_Response New response with wrapped data */ - public function envelope_response( $response, $embed ) { + public function envelope_response( $response, $embed, $request = null ) { $envelope = array( - 'body' => $this->response_to_data( $response, $embed ), + 'body' => $this->response_to_data( $response, $embed, $request ), 'status' => $response->get_status(), 'headers' => $response->get_headers(), ); @@ -1136,40 +1141,122 @@ public function get_route_options( $route ) { } /** - * Determines whether a response option is enabled for the route that generated a response. + * Resolves the response processing settings of a route handler. + * + * A route, or one of its handlers, sets the `response_processing` option to serve its data as returned. + * The option is `false` to turn off every feature, or an array with any of these keys: + * + * - `fields`: Whether the `_fields` parameter filters the response data. + * - `embed`: Whether the `_embed` parameter embeds linked resources in the response data. + * - `links`: Whether the response links are added to the response data as `_links`. + * + * Each feature is resolved on its own: the handler setting applies, then the route setting, and the + * feature stays on when neither sets it. Embedding needs the links, so `embed` is off whenever `links` + * is off. + * + * The option covers these three features only. Authentication, permission checks, status codes, headers + * and JSON encoding apply as usual, and keys of the data itself, such as its own `_links`, stay as returned. * - * Routes, or individual handlers of a route, can set these options to false: + * @since 7.2.0 + * + * @param array $handler The route handler. + * @param array|null $route_options Optional. The options of the handler's route. Default null. + * @return array { + * Whether each response processing feature is on. + * + * @type bool $fields Whether the `_fields` parameter filters the response data. + * @type bool $embed Whether the `_embed` parameter embeds linked resources in the response data. + * @type bool $links Whether the response links are added to the response data as `_links`. + * } * - * - `allow_fields`: Whether the `_fields` parameter filters the response data. - * - `allow_embed`: Whether the `_embed` parameter embeds linked resources in the response data. - * - `allow_links`: Whether the response links are added to the response data as `_links`. + * @phpstan-return array{fields: bool, embed: bool, links: bool} + */ + public static function resolve_response_processing( $handler, $route_options = null ) { + $processing = array( + 'fields' => true, + 'embed' => true, + 'links' => true, + ); + + $settings = array( + $route_options['response_processing'] ?? null, + $handler['response_processing'] ?? null, + ); + + foreach ( $settings as $setting ) { + if ( is_bool( $setting ) ) { + $processing = array_fill_keys( array_keys( $processing ), $setting ); + continue; + } + + if ( ! is_array( $setting ) ) { + continue; + } + + foreach ( array_keys( $processing ) as $feature ) { + if ( isset( $setting[ $feature ] ) ) { + $processing[ $feature ] = (bool) $setting[ $feature ]; + } + } + } + + if ( ! $processing['links'] ) { + $processing['embed'] = false; + } + + return $processing; + } + + /** + * Retrieves the response processing settings for a request. * - * A handler option takes precedence over the route option of the same name. + * The settings are those of the handler the request matched. A response returned by the + * `rest_pre_dispatch` filter matches no handler, so the request is matched to a handler here + * without changing the request. An OPTIONS request is served the route description, so every + * feature is on for it. * * @since 7.2.0 * - * @param WP_HTTP_Response $response Response object. - * @param string $option Option name. Accepts 'allow_fields', 'allow_embed', or 'allow_links'. - * @return bool Whether the option is enabled. True when neither the handler nor the route sets it. + * @see WP_REST_Server::resolve_response_processing() + * + * @param WP_REST_Request|null $request The request, or null when only the response is known. + * @param WP_REST_Response|null $response Optional. The response generated for the request. Default null. + * @return array Whether each feature is on, keyed by `fields`, `embed` and `links`. Every feature is on + * when no handler matches. + * + * @phpstan-return array{fields: bool, embed: bool, links: bool} */ - public function is_response_option_enabled( $response, $option ) { - if ( ! $response instanceof WP_REST_Response ) { - return true; + public function get_response_processing( $request, $response = null ) { + if ( $response instanceof WP_REST_Response && is_array( $response->get_matched_handler() ) ) { + $route = $response->get_matched_route(); + + return self::resolve_response_processing( + $response->get_matched_handler(), + is_string( $route ) ? $this->get_route_options( $route ) : null + ); } - $handler = $response->get_matched_handler(); - if ( is_array( $handler ) && isset( $handler[ $option ] ) ) { - return (bool) $handler[ $option ]; + if ( ! $request instanceof WP_REST_Request ) { + return self::resolve_response_processing( array() ); } - $route = $response->get_matched_route(); - if ( ! is_string( $route ) || '' === $route ) { - return true; + // An OPTIONS request is served the route description, which rest_handle_options_request() + // builds with the raw handlers of the route as the request attributes. + if ( 'OPTIONS' === $request->get_method() ) { + return self::resolve_response_processing( array() ); } - $route_options = $this->get_route_options( $route ); + $attributes = $request->get_attributes(); + if ( isset( $attributes['response_processing'] ) ) { + return self::resolve_response_processing( $attributes ); + } + + $match = $this->find_handler_for_request( $request ); + if ( null === $match ) { + return self::resolve_response_processing( array() ); + } - return ! isset( $route_options[ $option ] ) || (bool) $route_options[ $option ]; + return self::resolve_response_processing( $match[1], $this->get_route_options( $match[0] ) ); } /** @@ -1273,6 +1360,53 @@ public function is_dispatching() { * @phpstan-return array{non-empty-string, Route_Handler}|WP_Error */ protected function match_request_to_handler( $request ) { + $match = $this->find_handler_for_request( $request ); + + if ( null === $match ) { + return new WP_Error( + 'rest_no_route', + __( 'No route was found matching the URL and request method.' ), + array( 'status' => 404 ) + ); + } + + list( $route, $handler, $args ) = $match; + + // Resolve the response processing settings once, for the request attributes and the matched handler. + $handler['response_processing'] = self::resolve_response_processing( $handler, $this->get_route_options( $route ) ); + + if ( ! is_callable( $handler['callback'] ) ) { + return array( $route, $handler ); + } + + $request->set_url_params( $args ); + $request->set_attributes( $handler ); + + $defaults = array(); + + foreach ( $handler['args'] as $arg => $options ) { + if ( isset( $options['default'] ) ) { + $defaults[ $arg ] = $options['default']; + } + } + + $request->set_default_params( $defaults ); + + return array( $route, $handler ); + } + + /** + * Finds the route and the handler that a request matches, without changing the request. + * + * @access private + * @since 7.2.0 + * + * @param WP_REST_Request $request The request object. + * @return array|null The route, the handler and the URL parameters, or null when no handler matches. + * + * @phpstan-return array{non-empty-string, Route_Handler, array}|null + */ + protected function find_handler_for_request( $request ) { $method = $request->get_method(); $path = $request->get_route(); @@ -1306,8 +1440,6 @@ protected function match_request_to_handler( $request ) { } foreach ( $handlers as $handler ) { - $callback = $handler['callback']; - // Fallback to GET method if no HEAD method is registered. $checked_method = $method; if ( 'HEAD' === $method && empty( $handler['methods']['HEAD'] ) ) { @@ -1317,32 +1449,11 @@ protected function match_request_to_handler( $request ) { continue; } - if ( ! is_callable( $callback ) ) { - return array( $route, $handler ); - } - - $request->set_url_params( $args ); - $request->set_attributes( $handler ); - - $defaults = array(); - - foreach ( $handler['args'] as $arg => $options ) { - if ( isset( $options['default'] ) ) { - $defaults[ $arg ] = $options['default']; - } - } - - $request->set_default_params( $defaults ); - - return array( $route, $handler ); + return array( $route, $handler, $args ); } } - return new WP_Error( - 'rest_no_route', - __( 'No route was found matching the URL and request method.' ), - array( 'status' => 404 ) - ); + return null; } /** @@ -1746,10 +1857,11 @@ public function get_data_for_route( $route, $callbacks, $context = 'view' ) { 'endpoints' => array(), ); - $allow_batch = false; + $allow_batch = false; + $route_options = $this->get_route_options( $route ); - if ( isset( $this->route_options[ $route ] ) ) { - $options = $this->route_options[ $route ]; + if ( null !== $route_options ) { + $options = $route_options; if ( isset( $options['namespace'] ) ) { $data['namespace'] = $options['namespace']; @@ -1783,6 +1895,12 @@ public function get_data_for_route( $route, $callbacks, $context = 'view' ) { $endpoint_data['allow_batch'] = $callback_batch; } + $response_processing = self::resolve_response_processing( $callback, $route_options ); + + if ( in_array( false, $response_processing, true ) ) { + $endpoint_data['response_processing'] = $response_processing; + } + if ( isset( $callback['args'] ) ) { $endpoint_data['args'] = array(); @@ -2002,7 +2120,7 @@ public function serve_batch_request_v1( WP_REST_Request $batch_request ) { /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $single_request ); - $responses[] = $this->envelope_response( $result, false )->get_data(); + $responses[] = $this->envelope_response( $result, false, $single_request )->get_data(); } return new WP_REST_Response( array( 'responses' => $responses ), WP_Http::MULTI_STATUS ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php index 74337bfd2442c..0b65682863017 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php @@ -565,6 +565,8 @@ protected function get_object_type() { * Included fields are based on item schema and `_fields=` request argument. * * @since 4.9.6 + * @since 7.2.0 The `_fields` argument is ignored when the matched route turns off + * the `fields` response processing. * * @param WP_REST_Request $request Full details about the request. * @return string[] Fields to be included in the response. @@ -609,7 +611,9 @@ public function get_fields_for_response( $request ) { $fields = array_unique( $fields ); - if ( ! isset( $request['_fields'] ) ) { + $processing = WP_REST_Server::resolve_response_processing( $request->get_attributes() ); + + if ( ! isset( $request['_fields'] ) || ! $processing['fields'] ) { return $fields; } $requested_fields = wp_parse_list( $request['_fields'] ); diff --git a/tests/phpunit/tests/rest-api/rest-controller.php b/tests/phpunit/tests/rest-api/rest-controller.php index 33ac4f3c814f7..15b260eb6a631 100644 --- a/tests/phpunit/tests/rest-api/rest-controller.php +++ b/tests/phpunit/tests/rest-api/rest-controller.php @@ -441,6 +441,43 @@ public function test_get_fields_for_response_respects_embed() { $this->assertContains( '_embedded', $controller->get_fields_for_response( $request ) ); } + /** + * Data provider for test_get_fields_for_response_respects_response_processing(). + * + * @return array + */ + public static function data_get_fields_for_response_respects_response_processing(): array { + return array( + 'false' => array( false, false ), + 'fields false' => array( array( 'fields' => false ), false ), + 'links false' => array( array( 'links' => false ), true ), + 'true' => array( true, true ), + ); + } + + /** + * @ticket 66267 + * @dataProvider data_get_fields_for_response_respects_response_processing + * + * @param mixed $response_processing The `response_processing` setting of the matched handler. + * @param bool $filtered Whether the `_fields` parameter should filter the fields. + */ + public function test_get_fields_for_response_respects_response_processing( $response_processing, bool $filtered ): void { + $controller = new WP_REST_Test_Controller(); + $request = new WP_REST_Request( 'GET', '/wp/v2/testroute' ); + $request->set_attributes( array( 'response_processing' => $response_processing ) ); + $request->set_param( '_fields', 'somestring' ); + + $fields = $controller->get_fields_for_response( $request ); + + if ( $filtered ) { + $this->assertSame( array( 'somestring' ), $fields, 'The _fields parameter should filter the fields.' ); + } else { + $this->assertContains( 'someinteger', $fields, 'The _fields parameter should be ignored when the fields processing is off.' ); + $this->assertContains( '_links', $fields, 'The _fields parameter should be ignored when the fields processing is off.' ); + } + } + public function test_get_fields_for_response_filters_by_context() { $controller = new WP_REST_Test_Controller(); diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index 3dbacc81b9035..37a9978a15378 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -1678,6 +1678,9 @@ public function test_nocache_headers_on_method_overridden_requests( int $status, $server->serve_request( '/test/status' ); + $_SERVER['REQUEST_METHOD'] = 'GET'; + unset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ); + $this->assertSame( $status, $server->status, 'The response status is not the one returned by the callback.' ); $headers = $server->sent_headers; @@ -2902,17 +2905,17 @@ public function test_prefers_developer_defined_target_hints() { } /** - * Registers a route for the response option tests, and a route that its link embeds. + * Registers a route for the response processing tests, and a route that its link embeds. * * @param mixed $data The response data. * @param bool $with_link Whether the response has an embeddable link. * @param array $handler_options Options for the route handler. * @param array $route_options Options for the route. */ - private function register_response_options_route( $data, bool $with_link, array $handler_options = array(), array $route_options = array() ): void { + private function register_response_processing_route( $data, bool $with_link, array $handler_options = array(), array $route_options = array() ): void { rest_get_server()->register_route( 'test-ns', - '/test-ns/response-options', + '/test-ns/response-processing', array_merge( array( array_merge( @@ -2948,11 +2951,92 @@ private function register_response_options_route( $data, bool $with_link, array } /** - * Data provider for test_allow_fields_false_leaves_the_data_unfiltered(). + * Data provider for test_resolve_response_processing(). + * + * @return array, 1: array|null, 2: array}> + */ + public static function data_resolve_response_processing(): array { + $all_on = array( + 'fields' => true, + 'embed' => true, + 'links' => true, + ); + $all_off = array( + 'fields' => false, + 'embed' => false, + 'links' => false, + ); + + return array( + 'no setting' => array( array(), null, $all_on ), + 'empty route options' => array( array(), array(), $all_on ), + 'handler false' => array( array( 'response_processing' => false ), null, $all_off ), + 'route false' => array( array(), array( 'response_processing' => false ), $all_off ), + 'route false and handler true' => array( array( 'response_processing' => true ), array( 'response_processing' => false ), $all_on ), + 'route false and handler fields' => array( + array( 'response_processing' => array( 'fields' => true ) ), + array( 'response_processing' => false ), + array( + 'fields' => true, + 'embed' => false, + 'links' => false, + ), + ), + 'handler fields false' => array( + array( 'response_processing' => array( 'fields' => false ) ), + null, + array( + 'fields' => false, + 'embed' => true, + 'links' => true, + ), + ), + 'handler links false turns off embed' => array( + array( 'response_processing' => array( 'links' => false ) ), + null, + array( + 'fields' => true, + 'embed' => false, + 'links' => false, + ), + ), + 'handler embed true needs the links' => array( + array( 'response_processing' => array( 'embed' => true ) ), + array( 'response_processing' => array( 'links' => false ) ), + array( + 'fields' => true, + 'embed' => false, + 'links' => false, + ), + ), + 'route fields false and handler true' => array( + array( 'response_processing' => array( 'fields' => true ) ), + array( 'response_processing' => array( 'fields' => false ) ), + $all_on, + ), + 'unknown keys are ignored' => array( array( 'response_processing' => array( 'envelope' => false ) ), null, $all_on ), + 'other types are ignored' => array( array( 'response_processing' => 'no' ), array( 'response_processing' => 0 ), $all_on ), + ); + } + + /** + * @ticket 66267 + * @dataProvider data_resolve_response_processing + * + * @param array $handler The route handler. + * @param array|null $route_options The route options. + * @param array $expected The expected settings. + */ + public function test_resolve_response_processing( array $handler, ?array $route_options, array $expected ): void { + $this->assertSame( $expected, WP_REST_Server::resolve_response_processing( $handler, $route_options ), 'The settings should be resolved feature by feature.' ); + } + + /** + * Data provider for test_response_processing_false_serves_the_data_as_returned(). * * @return array */ - public static function data_allow_fields_false_leaves_the_data_unfiltered(): array { + public static function data_response_processing_false_serves_the_data_as_returned(): array { return array( 'associative array' => array( array( @@ -2962,6 +3046,7 @@ public static function data_allow_fields_false_leaves_the_data_unfiltered(): arr '{"a":1,"b":2}', ), 'list' => array( array( array( 'b' => 2 ), 'c' ), '[{"b":2},"c"]' ), + 'empty array' => array( array(), '[]' ), 'empty object' => array( new stdClass(), '{}' ), 'object' => array( (object) array( 'b' => 2 ), '{"b":2}' ), 'string' => array( 'value', '"value"' ), @@ -2971,44 +3056,69 @@ public static function data_allow_fields_false_leaves_the_data_unfiltered(): arr /** * @ticket 66267 - * @dataProvider data_allow_fields_false_leaves_the_data_unfiltered + * @dataProvider data_response_processing_false_serves_the_data_as_returned * * @param mixed $data The response data. * @param string $expected The expected response body. */ - public function test_allow_fields_false_leaves_the_data_unfiltered( $data, string $expected ): void { - $this->register_response_options_route( $data, false, array( 'allow_fields' => false ) ); + public function test_response_processing_false_serves_the_data_as_returned( $data, string $expected ): void { + $this->register_response_processing_route( $data, true, array( 'response_processing' => false ) ); + + $_GET['_fields'] = 'a'; + $_GET['_embed'] = '1'; + rest_get_server()->serve_request( '/test-ns/response-processing' ); + + $this->assertSame( $expected, rest_get_server()->sent_body, 'The data should be served as returned, without links or filtering.' ); + } + + /** + * @ticket 66267 + */ + public function test_fields_are_filtered_and_links_embedded_by_default(): void { + $this->register_response_processing_route( + array( + 'a' => 1, + 'b' => 2, + ), + true + ); $_GET['_fields'] = 'a'; - rest_get_server()->serve_request( '/test-ns/response-options' ); + $_GET['_embed'] = '1'; + rest_get_server()->serve_request( '/test-ns/response-processing' ); + $data = json_decode( rest_get_server()->sent_body, true ); - $this->assertSame( $expected, rest_get_server()->sent_body, 'The _fields parameter should not filter the data.' ); + $this->assertSame( array( 'a', '_links', '_embedded' ), array_keys( $data ), 'The _fields parameter should filter the data, and the links should be added.' ); + $this->assertSame( array( array( 'hello' => 'world' ) ), $data['_embedded']['alternate'], 'The link should be embedded.' ); } /** * @ticket 66267 */ - public function test_fields_are_filtered_by_default(): void { - $this->register_response_options_route( + public function test_response_processing_fields_false_leaves_the_data_unfiltered(): void { + $this->register_response_processing_route( array( 'a' => 1, 'b' => 2, ), - false + true, + array( 'response_processing' => array( 'fields' => false ) ) ); $_GET['_fields'] = 'a'; - rest_get_server()->serve_request( '/test-ns/response-options' ); + $_GET['_embed'] = '1'; + rest_get_server()->serve_request( '/test-ns/response-processing' ); + $data = json_decode( rest_get_server()->sent_body, true ); - $this->assertSame( '{"a":1}', rest_get_server()->sent_body, 'The _fields parameter should filter the data.' ); + $this->assertSame( array( 'a', 'b', '_links', '_embedded' ), array_keys( $data ), 'Only the _fields parameter should be ignored.' ); } /** - * Data provider for test_allow_links_false_leaves_out_the_links(). + * Data provider for test_response_processing_links_false_leaves_out_the_links(). * * @return array */ - public static function data_allow_links_false_leaves_out_the_links(): array { + public static function data_response_processing_links_false_leaves_out_the_links(): array { return array( 'associative array' => array( array( 'a' => 1 ), '{"a":1}' ), 'list' => array( array( 'a', 'b' ), '["a","b"]' ), @@ -3019,27 +3129,28 @@ public static function data_allow_links_false_leaves_out_the_links(): array { /** * @ticket 66267 - * @dataProvider data_allow_links_false_leaves_out_the_links + * @dataProvider data_response_processing_links_false_leaves_out_the_links * * @param mixed $data The response data. * @param string $expected The expected response body. */ - public function test_allow_links_false_leaves_out_the_links( $data, string $expected ): void { - $this->register_response_options_route( $data, true, array( 'allow_links' => false ) ); + public function test_response_processing_links_false_leaves_out_the_links( $data, string $expected ): void { + $this->register_response_processing_route( $data, true, array( 'response_processing' => array( 'links' => false ) ) ); - rest_get_server()->serve_request( '/test-ns/response-options' ); + $_GET['_embed'] = '1'; + rest_get_server()->serve_request( '/test-ns/response-processing' ); - $this->assertSame( $expected, rest_get_server()->sent_body, 'The response links should not be added.' ); + $this->assertSame( $expected, rest_get_server()->sent_body, 'The response links should not be added or embedded.' ); } /** * @ticket 66267 */ - public function test_allow_embed_false_keeps_the_links_without_embedding(): void { - $this->register_response_options_route( array( 'a' => 1 ), true, array( 'allow_embed' => false ) ); + public function test_response_processing_embed_false_keeps_the_links_without_embedding(): void { + $this->register_response_processing_route( array( 'a' => 1 ), true, array( 'response_processing' => array( 'embed' => false ) ) ); $_GET['_embed'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-options' ); + rest_get_server()->serve_request( '/test-ns/response-processing' ); $data = json_decode( rest_get_server()->sent_body, true ); $this->assertArrayHasKey( '_links', $data, 'The response links should be added.' ); @@ -3049,105 +3160,338 @@ public function test_allow_embed_false_keeps_the_links_without_embedding(): void /** * @ticket 66267 */ - public function test_links_are_added_and_embedded_by_default(): void { - $this->register_response_options_route( array( 'a' => 1 ), true ); + public function test_response_processing_false_keeps_the_own_links_of_the_data(): void { + $own_links = array( 'custom' => array( array( 'href' => 'https://example.org/' ) ) ); + $this->register_response_processing_route( + array( + 'a' => 1, + '_links' => $own_links, + ), + true, + array( 'response_processing' => false ) + ); $_GET['_embed'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-options' ); + rest_get_server()->serve_request( '/test-ns/response-processing' ); $data = json_decode( rest_get_server()->sent_body, true ); - $this->assertArrayHasKey( '_links', $data, 'The response links should be added.' ); - $this->assertSame( array( array( 'hello' => 'world' ) ), $data['_embedded']['alternate'], 'The link should be embedded.' ); + $this->assertSame( $own_links, $data['_links'], 'The _links key of the data should be served as returned.' ); + $this->assertArrayNotHasKey( '_embedded', $data, 'The links of the data should not be embedded.' ); } /** * @ticket 66267 */ - public function test_allow_embed_false_applies_to_enveloped_responses(): void { - $this->register_response_options_route( array( 'a' => 1 ), true, array( 'allow_embed' => false ) ); + public function test_response_processing_applies_to_enveloped_responses(): void { + $this->register_response_processing_route( + array( + 'a' => 1, + 'b' => 2, + ), + true, + array( 'response_processing' => false ) + ); + $_GET['_fields'] = 'a'; $_GET['_embed'] = '1'; $_GET['_envelope'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-options' ); + rest_get_server()->serve_request( '/test-ns/response-processing' ); $data = json_decode( rest_get_server()->sent_body, true ); $this->assertSame( 200, $data['status'], 'The enveloped response should succeed.' ); - $this->assertArrayHasKey( '_links', $data['body'], 'The response links should be added to the body.' ); - $this->assertArrayNotHasKey( '_embedded', $data['body'], 'The links should not be embedded in the body.' ); + $this->assertSame( + array( + 'a' => 1, + 'b' => 2, + ), + $data['body'], + 'The envelope should wrap the data as returned.' + ); } /** - * Data provider for test_handler_option_takes_precedence_over_route_option(). + * Data provider for test_handler_setting_takes_precedence_over_route_setting(). * - * @return array, 1: array, 2: string}> + * @return array, 1: array, 2: string[]}> */ - public static function data_handler_option_takes_precedence_over_route_option(): array { + public static function data_handler_setting_takes_precedence_over_route_setting(): array { return array( - 'route disables' => array( array(), array( 'allow_fields' => false ), '{"a":1,"b":2}' ), - 'route disables and handler enables' => array( array( 'allow_fields' => true ), array( 'allow_fields' => false ), '{"a":1}' ), - 'route enables and handler disables' => array( array( 'allow_fields' => false ), array( 'allow_fields' => true ), '{"a":1,"b":2}' ), - 'handler disables another option' => array( array( 'allow_links' => false ), array(), '{"a":1}' ), + 'route false' => array( array(), array( 'response_processing' => false ), array( 'a', 'b' ) ), + 'route false and handler true' => array( array( 'response_processing' => true ), array( 'response_processing' => false ), array( 'a', '_links' ) ), + 'route false and handler fields' => array( array( 'response_processing' => array( 'fields' => true ) ), array( 'response_processing' => false ), array( 'a' ) ), + 'route fields false and handler links off' => array( array( 'response_processing' => array( 'links' => false ) ), array( 'response_processing' => array( 'fields' => false ) ), array( 'a', 'b' ) ), + 'route links false and handler fields off' => array( array( 'response_processing' => array( 'fields' => false ) ), array( 'response_processing' => array( 'links' => false ) ), array( 'a', 'b' ) ), ); } /** * @ticket 66267 - * @dataProvider data_handler_option_takes_precedence_over_route_option + * @dataProvider data_handler_setting_takes_precedence_over_route_setting * - * @param array $handler_options Options for the route handler. - * @param array $route_options Options for the route. - * @param string $expected The expected response body. + * @param array $handler_options Options for the route handler. + * @param array $route_options Options for the route. + * @param string[] $expected_keys The expected keys of the response body. */ - public function test_handler_option_takes_precedence_over_route_option( array $handler_options, array $route_options, string $expected ): void { - $this->register_response_options_route( + public function test_handler_setting_takes_precedence_over_route_setting( array $handler_options, array $route_options, array $expected_keys ): void { + $this->register_response_processing_route( array( 'a' => 1, 'b' => 2, ), - false, + true, $handler_options, $route_options ); $_GET['_fields'] = 'a'; - rest_get_server()->serve_request( '/test-ns/response-options' ); + rest_get_server()->serve_request( '/test-ns/response-processing' ); + $data = json_decode( rest_get_server()->sent_body, true ); + + $this->assertSame( $expected_keys, array_keys( $data ), 'The handler setting should take precedence over the route setting, feature by feature.' ); + } + + /** + * @ticket 66267 + */ + public function test_response_processing_is_set_on_the_request_attributes_and_the_matched_handler(): void { + $this->register_response_processing_route( array( 'a' => 1 ), false, array( 'response_processing' => array( 'links' => false ) ) ); + + $request = new WP_REST_Request( 'GET', '/test-ns/response-processing' ); + $response = rest_get_server()->dispatch( $request ); + $expected = array( + 'fields' => true, + 'embed' => false, + 'links' => false, + ); + + $this->assertSame( $expected, $request->get_attributes()['response_processing'], 'The request attributes should carry the resolved settings.' ); + $this->assertSame( $expected, $response->get_matched_handler()['response_processing'], 'The matched handler should carry the resolved settings.' ); + $this->assertSame( $expected, rest_get_server()->get_response_processing( $request, $response ), 'The settings of the response should be the resolved ones.' ); + } + + /** + * @ticket 66267 + */ + public function test_routes_without_the_option_have_every_feature_on(): void { + $this->register_response_processing_route( array( 'a' => 1 ), false ); + + $request = new WP_REST_Request( 'GET', '/test-ns/response-processing' ); + $response = rest_get_server()->dispatch( $request ); + $expected = array( + 'fields' => true, + 'embed' => true, + 'links' => true, + ); + + $this->assertSame( $expected, $request->get_attributes()['response_processing'], 'The request attributes should have every feature on.' ); + $this->assertSame( $expected, $response->get_matched_handler()['response_processing'], 'The matched handler should have every feature on.' ); + $this->assertSame( $expected, rest_get_server()->get_response_processing( $request, $response ), 'The settings of the response should have every feature on.' ); + } + + /** + * Data provider for test_response_processing_applies_to_hijacked_responses(). + * + * @return array + */ + public static function data_response_processing_applies_to_hijacked_responses(): array { + return array( + 'associative array' => array( + array( + 'a' => 1, + 'b' => 2, + ), + '{"a":1,"b":2}', + ), + 'string' => array( 'value', '"value"' ), + ); + } + + /** + * @ticket 66267 + * @dataProvider data_response_processing_applies_to_hijacked_responses + * + * @param mixed $data The data returned by the `rest_pre_dispatch` filter. + * @param string $expected The expected response body. + */ + public function test_response_processing_applies_to_hijacked_responses( $data, string $expected ): void { + $this->register_response_processing_route( array( 'unexpected' => true ), true, array( 'response_processing' => false ) ); + + add_filter( + 'rest_pre_dispatch', + static function () use ( $data ) { + return $data; + } + ); + + $_GET['_fields'] = 'a'; + $_GET['_embed'] = '1'; + rest_get_server()->serve_request( '/test-ns/response-processing' ); + + $this->assertSame( $expected, rest_get_server()->sent_body, 'A response returned by rest_pre_dispatch should follow the settings of the route it was returned for.' ); + } + + /** + * @ticket 66267 + */ + public function test_hijacked_responses_are_processed_by_default(): void { + $this->register_response_processing_route( array( 'unexpected' => true ), true ); + + add_filter( + 'rest_pre_dispatch', + static function () { + return array( + 'a' => 1, + 'b' => 2, + ); + } + ); + + $_GET['_fields'] = 'a'; + rest_get_server()->serve_request( '/test-ns/response-processing' ); + + $this->assertSame( '{"a":1}', rest_get_server()->sent_body, 'A response returned by rest_pre_dispatch for a route without the option should be filtered.' ); + } + + /** + * @ticket 66267 + */ + public function test_get_response_processing_without_a_matched_handler(): void { + $all_on = array( + 'fields' => true, + 'embed' => true, + 'links' => true, + ); + + $this->assertSame( $all_on, rest_get_server()->get_response_processing( new WP_REST_Request( 'GET', '/test-ns/no-such-route' ) ), 'A request that matches no route should enable every feature.' ); + $this->assertSame( $all_on, rest_get_server()->get_response_processing( null, new WP_REST_Response() ), 'A response without a matched handler should enable every feature.' ); + $this->assertSame( $all_on, rest_get_server()->get_response_processing( null ), 'The defaults should apply without a request.' ); + } + + /** + * Data provider for test_response_processing_applies_to_preloaded_requests(). + * + * @return array + */ + public static function data_response_processing_applies_to_preloaded_requests(): array { + return array( + 'associative array' => array( + array( + 'a' => 1, + 'b' => 2, + ), + '{"a":1,"b":2}', + ), + 'empty object' => array( new stdClass(), '{}' ), + 'string' => array( 'value', '"value"' ), + ); + } - $this->assertSame( $expected, rest_get_server()->sent_body, 'The handler option should take precedence over the route option.' ); + /** + * @ticket 66267 + * @dataProvider data_response_processing_applies_to_preloaded_requests + * + * @param mixed $data The response data. + * @param string $expected The expected JSON encoding of the preloaded body. + */ + public function test_response_processing_applies_to_preloaded_requests( $data, string $expected ): void { + $this->register_response_processing_route( $data, true, array( 'response_processing' => false ) ); + + $preload = rest_preload_api_request( array(), '/test-ns/response-processing?_fields=a&_embed=1' ); + + $this->assertSame( $expected, wp_json_encode( $preload['/test-ns/response-processing?_fields=a&_embed=1']['body'] ), 'The preloaded body should be the data as returned.' ); } /** * @ticket 66267 */ - public function test_response_options_apply_to_preloaded_requests(): void { - $this->register_response_options_route( + public function test_response_processing_applies_to_batch_requests(): void { + $this->register_response_processing_route( array( 'a' => 1, 'b' => 2, ), true, array( - 'allow_fields' => false, - 'allow_links' => false, + 'methods' => 'POST', + 'allow_batch' => array( 'v1' => true ), + 'response_processing' => false, + ) + ); + + $request = new WP_REST_Request( 'POST', '/batch/v1' ); + $request->set_body_params( + array( + 'requests' => array( + array( 'path' => '/test-ns/response-processing?_fields=a' ), + ), ) ); - $preload = rest_preload_api_request( array(), '/test-ns/response-options?_fields=a&_embed=1' ); + $response = rest_do_request( $request ); + $this->assertSame( 207, $response->get_status(), 'The batch request should succeed.' ); $this->assertSame( array( 'a' => 1, 'b' => 2, ), - $preload['/test-ns/response-options?_fields=a&_embed=1']['body'], - 'The preloaded data should not be filtered or have links.' + $response->get_data()['responses'][0]['body'], + 'The batched response body should be the data as returned.' + ); + } + + /** + * @ticket 66267 + */ + public function test_response_processing_is_listed_in_the_index(): void { + rest_get_server()->register_route( + 'test-ns', + '/test-ns/response-processing', + array( + array( + 'methods' => 'GET', + 'callback' => '__return_null', + 'permission_callback' => '__return_true', + ), + array( + 'methods' => 'POST', + 'callback' => '__return_null', + 'permission_callback' => '__return_true', + 'response_processing' => array( 'fields' => true ), + ), + 'response_processing' => array( 'fields' => false ), + ) + ); + + $request = new WP_REST_Request( 'OPTIONS', '/test-ns/response-processing' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( + array( + 'fields' => false, + 'embed' => true, + 'links' => true, + ), + $data['endpoints'][0]['response_processing'], + 'An endpoint with a feature turned off should list the settings.' ); + $this->assertArrayNotHasKey( 'response_processing', $data['endpoints'][1], 'An endpoint with every feature on should not list the settings.' ); } /** * @ticket 66267 */ - public function test_is_response_option_enabled_without_a_matched_route(): void { - $this->assertTrue( rest_get_server()->is_response_option_enabled( new WP_REST_Response(), 'allow_fields' ), 'A response without a matched route should enable the option.' ); - $this->assertTrue( rest_get_server()->is_response_option_enabled( new WP_HTTP_Response(), 'allow_links' ), 'A response that is not a REST response should enable the option.' ); + public function test_options_requests_are_processed_by_default(): void { + $this->register_response_processing_route( array( 'a' => 1 ), true, array( 'response_processing' => false ) ); + + $_SERVER['REQUEST_METHOD'] = 'OPTIONS'; + $_GET['_fields'] = 'methods'; + rest_get_server()->serve_request( '/test-ns/response-processing' ); + $_SERVER['REQUEST_METHOD'] = 'GET'; + $data = json_decode( rest_get_server()->sent_body, true ); + + $this->assertSame( array( 'methods' ), array_keys( $data ), 'The _fields parameter should filter the route description of a route with the option.' ); } + } From ba8e2f3aaf2a208388d6faaad59984cfa295972f Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 22 Oct 2026 15:15:51 +0100 Subject: [PATCH 3/7] REST API: Serve response data that is not an array as returned. `rest_filter_response_fields()` leaves the response unchanged when its data is not an array, and keeps list items that are not arrays. `WP_REST_Server::response_to_data()` returns scalar and object data as it is instead of adding links to it, and leaves list items that are not arrays alone when embedding. Routes that return such data no longer need the `response_processing` option to avoid type errors. See #66267. --- src/wp-includes/rest-api.php | 10 +++- .../rest-api/class-wp-rest-server.php | 23 +++++---- tests/phpunit/tests/blocks/editor.php | 2 +- tests/phpunit/tests/rest-api/rest-server.php | 47 +++++++++++++++++++ 4 files changed, 70 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index af645e55c9d1b..4f0d6e9fd8be0 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -950,7 +950,8 @@ function _rest_array_intersect_key_recursive( $array1, $array2 ) { * Filters the REST API response to include only an allow-listed set of response object fields. * * @since 4.8.0 - * @since 7.2.0 The response is left unchanged when the route turns off the `fields` response processing. + * @since 7.2.0 The response is left unchanged when the route turns off the `fields` response + * processing or when the response data is not an array. * * @param WP_REST_Response $response Current response being served. * @param WP_REST_Server $server ResponseHandler instance (usually WP_REST_Server). @@ -972,6 +973,11 @@ function rest_filter_response_fields( $response, $server, $request ) { $data = $response->get_data(); + // Only arrays have fields to filter. + if ( ! is_array( $data ) ) { + return $response; + } + $fields = wp_parse_list( $request['_fields'] ); if ( 0 === count( $fields ) ) { @@ -1002,7 +1008,7 @@ function rest_filter_response_fields( $response, $server, $request ) { if ( wp_is_numeric_array( $data ) ) { $new_data = array(); foreach ( $data as $item ) { - $new_data[] = _rest_array_intersect_key_recursive( $item, $fields_as_keyed ); + $new_data[] = is_array( $item ) ? _rest_array_intersect_key_recursive( $item, $fields_as_keyed ) : $item; } } else { $new_data = _rest_array_intersect_key_recursive( $data, $fields_as_keyed ); diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index a423466396b44..6fa740b03c98a 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -624,22 +624,27 @@ public function serve_request( $path = null ) { * @since 4.4.0 * @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include. * @since 7.2.0 Added the `$request` parameter. The links are not added or embedded when the - * route turns off the `links` or `embed` response processing. + * route turns off the `links` or `embed` response processing, scalar and object + * data is returned as it is unless the object implements `ArrayAccess`, and list + * items that are not arrays are left as they are when embedding. * * @param WP_REST_Response $response Response object. * @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links. * @param WP_REST_Request|null $request Optional. The request that generated the response. It resolves the * response processing settings when the response has no matched handler. * Default null. - * @return array { - * Data with sub-requests embedded. - * - * @type array $_links Links. - * @type array $_embedded Embedded objects. - * } + * @return mixed Data with sub-requests embedded: an array with the links under `_links` and the embedded + * resources under `_embedded`, or scalar and object data as it is unless the object + * implements `ArrayAccess`. */ public function response_to_data( $response, $embed, $request = null ) { - $data = $response->get_data(); + $data = $response->get_data(); + + // Scalar and object data has no place for links or embedded resources, unless the object acts as an array. + if ( null !== $data && ! is_array( $data ) && ! $data instanceof ArrayAccess ) { + return $data; + } + $processing = $this->get_response_processing( $request, $response ); $links = $processing['links'] ? self::get_compact_response_links( $response ) : array(); @@ -653,7 +658,7 @@ public function response_to_data( $response, $embed, $request = null ) { // Determine if this is a numeric array. if ( wp_is_numeric_array( $data ) ) { foreach ( $data as $key => $item ) { - $data[ $key ] = $this->embed_links( $item, $embed ); + $data[ $key ] = is_array( $item ) || $item instanceof ArrayAccess ? $this->embed_links( $item, $embed ) : $item; } } else { $data = $this->embed_links( $data, $embed ); diff --git a/tests/phpunit/tests/blocks/editor.php b/tests/phpunit/tests/blocks/editor.php index 2c363d4103e92..c5755ff434b82 100644 --- a/tests/phpunit/tests/blocks/editor.php +++ b/tests/phpunit/tests/blocks/editor.php @@ -900,7 +900,7 @@ public function test_ensure_preload_data_script_tag_closes() { $expected = << diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index 37a9978a15378..12d2a4162f38f 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -3494,4 +3494,51 @@ public function test_options_requests_are_processed_by_default(): void { $this->assertSame( array( 'methods' ), array_keys( $data ), 'The _fields parameter should filter the route description of a route with the option.' ); } + /** + * @ticket 66267 + */ + public function test_array_access_data_gets_links_and_embedded_resources(): void { + $this->register_response_processing_route( new ArrayObject( array( 'a' => 1 ) ), true ); + + $_GET['_embed'] = '1'; + rest_get_server()->serve_request( '/test-ns/response-processing' ); + $data = json_decode( rest_get_server()->sent_body, true ); + + $this->assertSame( array( 'a', '_links', '_embedded' ), array_keys( $data ), 'An object that acts as an array should get the links and the embedded resources.' ); + } + + /** + * Data provider for test_non_array_data_is_served_as_returned_by_default(). + * + * @return array + */ + public static function data_non_array_data_is_served_as_returned_by_default(): array { + return array( + 'string' => array( 'value', true, '"value"' ), + 'integer' => array( 5, true, '5' ), + 'object' => array( (object) array( 'b' => 2 ), true, '{"b":2}' ), + 'empty object' => array( new stdClass(), true, '{}' ), + 'list of scalars' => array( array( 'a', 'b' ), false, '["a","b"]' ), + 'mixed list' => array( array( array( 'b' => 2 ), 'c' ), false, '[[],"c"]' ), + 'list of objects' => array( array( (object) array( 'b' => 2 ) ), false, '[{"b":2}]' ), + ); + } + + /** + * @ticket 66267 + * @dataProvider data_non_array_data_is_served_as_returned_by_default + * + * @param mixed $data The response data. + * @param bool $with_link Whether the response has an embeddable link. + * @param string $expected The expected response body. + */ + public function test_non_array_data_is_served_as_returned_by_default( $data, bool $with_link, string $expected ): void { + $this->register_response_processing_route( $data, $with_link ); + + $_GET['_fields'] = 'a'; + $_GET['_embed'] = '1'; + rest_get_server()->serve_request( '/test-ns/response-processing' ); + + $this->assertSame( $expected, rest_get_server()->sent_body, 'Data without fields, or items without fields, should be served as returned.' ); + } } From c38d38ccb78b5b34607b72e58d69ade2f3e438ce Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 23 Oct 2026 14:14:36 +0100 Subject: [PATCH 4/7] REST API: Store the response processing settings on the response. Add `WP_REST_Response::get_response_processing()` and `set_response_processing()`. The server sets the resolved settings on the response when it dispatches a request, for responses returned by the `rest_pre_dispatch` filter too, so `WP_REST_Server::response_to_data()`, `envelope_response()` and `rest_filter_response_fields()` read them from the response. The `$request` parameter is removed from `WP_REST_Server::response_to_data()` and `envelope_response()`, and `WP_REST_Server::get_response_processing()` is removed, so no existing signature changes. --- src/wp-includes/rest-api.php | 11 +- .../rest-api/class-wp-rest-response.php | 58 +++++++ .../rest-api/class-wp-rest-server.php | 114 +++++--------- tests/phpunit/tests/rest-api/rest-server.php | 147 +++++++++++++++++- 4 files changed, 241 insertions(+), 89 deletions(-) diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 4f0d6e9fd8be0..cb46f36b14d88 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -963,12 +963,9 @@ function rest_filter_response_fields( $response, $server, $request ) { return $response; } - if ( $server instanceof WP_REST_Server ) { - $processing = $server->get_response_processing( $request, $response ); - - if ( ! $processing['fields'] ) { - return $response; - } + $processing = $response->get_response_processing(); + if ( ! $processing['fields'] ) { + return $response; } $data = $response->get_data(); @@ -3063,7 +3060,7 @@ function rest_preload_api_request( $memo, $path ) { /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ $response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $server, $request ); $embed = $request->has_param( '_embed' ) ? rest_parse_embed_param( $request['_embed'] ) : false; - $data = $server->response_to_data( $response, $embed, $request ); + $data = $server->response_to_data( $response, $embed ); if ( 'OPTIONS' === $method ) { $memo[ $method ][ $path ] = array( diff --git a/src/wp-includes/rest-api/class-wp-rest-response.php b/src/wp-includes/rest-api/class-wp-rest-response.php index 7624e01d615b3..dca6e5bdabe8e 100644 --- a/src/wp-includes/rest-api/class-wp-rest-response.php +++ b/src/wp-includes/rest-api/class-wp-rest-response.php @@ -15,6 +15,7 @@ * @see WP_HTTP_Response * * @phpstan-import-type Route_Handler from WP_REST_Server + * @phpstan-import-type Response_Processing from WP_REST_Server * * @phpstan-type Target_Hints array{ * allow?: list, @@ -69,6 +70,19 @@ class WP_REST_Response extends WP_HTTP_Response { */ protected $matched_handler = null; + /** + * Whether each response processing feature is on for the response. + * + * @since 7.2.0 + * @var array Whether the `fields`, `embed` and `links` features are on. Every feature is on by default. + * @phpstan-var Response_Processing + */ + protected $response_processing = array( + 'fields' => true, + 'embed' => true, + 'links' => true, + ); + /** * Adds a link to the response. * @@ -249,6 +263,50 @@ public function set_matched_handler( $handler ) { $this->matched_handler = $handler; } + /** + * Retrieves whether each response processing feature is on for the response. + * + * The server sets the settings of the matched route when it dispatches a request, for responses + * returned by the `rest_pre_dispatch` filter too. Every feature is on until then. + * + * @since 7.2.0 + * + * @see WP_REST_Server::resolve_response_processing() + * + * @return array { + * Whether each response processing feature is on. + * + * @type bool $fields Whether the `_fields` parameter filters the response data. + * @type bool $embed Whether the `_embed` parameter embeds linked resources in the response data. + * @type bool $links Whether the response links are added to the response data as `_links`. + * } + * + * @phpstan-return Response_Processing + */ + public function get_response_processing() { + return $this->response_processing; + } + + /** + * Sets whether each response processing feature is on for the response. + * + * The setting takes the form of the `response_processing` route option: `false` turns off every + * feature, and an array turns off, or on, the features it names. Embedding needs the links, so + * `embed` is off whenever `links` is off. + * + * @since 7.2.0 + * + * @see WP_REST_Server::resolve_response_processing() + * + * @param bool|array $processing Whether every feature is on, or whether the `fields`, `embed` + * and `links` features are on. A feature not named stays on. + * + * @phpstan-param bool|array{fields?: bool, embed?: bool, links?: bool} $processing + */ + public function set_response_processing( $processing ) { + $this->response_processing = WP_REST_Server::resolve_response_processing( array( 'response_processing' => $processing ) ); + } + /** * Checks if the response is an error, i.e. >= 400 response code. * diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index 6fa740b03c98a..638c574ea0c81 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -43,6 +43,7 @@ * response_processing?: bool|array{fields?: bool, embed?: bool, links?: bool}, * ... * } + * @phpstan-type Response_Processing array{fields: bool, embed: bool, links: bool} */ #[AllowDynamicProperties] class WP_REST_Server { @@ -513,7 +514,7 @@ public function serve_request( $path = null ) { // Wrap the response in an envelope if asked for. if ( isset( $_GET['_envelope'] ) ) { $embed = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false; - $result = $this->envelope_response( $result, $embed, $request ); + $result = $this->envelope_response( $result, $embed ); } // Send extra data from response objects. @@ -569,7 +570,7 @@ public function serve_request( $path = null ) { // Embed links inside the request. $embed = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false; - $result = $this->response_to_data( $result, $embed, $request ); + $result = $this->response_to_data( $result, $embed ); /** * Filters the REST API response. @@ -623,21 +624,20 @@ public function serve_request( $path = null ) { * * @since 4.4.0 * @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include. - * @since 7.2.0 Added the `$request` parameter. The links are not added or embedded when the - * route turns off the `links` or `embed` response processing, scalar and object - * data is returned as it is unless the object implements `ArrayAccess`, and list - * items that are not arrays are left as they are when embedding. - * - * @param WP_REST_Response $response Response object. - * @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links. - * @param WP_REST_Request|null $request Optional. The request that generated the response. It resolves the - * response processing settings when the response has no matched handler. - * Default null. + * @since 7.2.0 The links are not added or embedded when the response turns off the `links` or + * `embed` response processing, scalar and object data is returned as it is unless + * the object implements `ArrayAccess`, and list items that are not arrays are left + * as they are when embedding. + * + * @see WP_REST_Response::get_response_processing() + * + * @param WP_REST_Response $response Response object. + * @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links. * @return mixed Data with sub-requests embedded: an array with the links under `_links` and the embedded * resources under `_embedded`, or scalar and object data as it is unless the object * implements `ArrayAccess`. */ - public function response_to_data( $response, $embed, $request = null ) { + public function response_to_data( $response, $embed ) { $data = $response->get_data(); // Scalar and object data has no place for links or embedded resources, unless the object acts as an array. @@ -645,7 +645,7 @@ public function response_to_data( $response, $embed, $request = null ) { return $data; } - $processing = $this->get_response_processing( $request, $response ); + $processing = $response->get_response_processing(); $links = $processing['links'] ? self::get_compact_response_links( $response ) : array(); if ( ! empty( $links ) ) { @@ -890,7 +890,7 @@ protected function embed_links( $data, $embed = true ) { /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ $response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request ); - $this->embed_cache[ $item['href'] ] = $this->response_to_data( $response, false, $request ); + $this->embed_cache[ $item['href'] ] = $this->response_to_data( $response, false ); } $embeds[] = $this->embed_cache[ $item['href'] ]; @@ -920,16 +920,14 @@ protected function embed_links( $data, $embed = true ) { * * @since 4.4.0 * @since 6.0.0 The `$embed` parameter can now contain a list of link relations to include. - * @since 7.2.0 Added the `$request` parameter. * - * @param WP_REST_Response $response Response object. - * @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links. - * @param WP_REST_Request|null $request Optional. The request that generated the response. Default null. + * @param WP_REST_Response $response Response object. + * @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links. * @return WP_REST_Response New response with wrapped data */ - public function envelope_response( $response, $embed, $request = null ) { + public function envelope_response( $response, $embed ) { $envelope = array( - 'body' => $this->response_to_data( $response, $embed, $request ), + 'body' => $this->response_to_data( $response, $embed ), 'status' => $response->get_status(), 'headers' => $response->get_headers(), ); @@ -1162,8 +1160,13 @@ public function get_route_options( $route ) { * The option covers these three features only. Authentication, permission checks, status codes, headers * and JSON encoding apply as usual, and keys of the data itself, such as its own `_links`, stay as returned. * + * The server resolves the settings when it matches a request, and sets them on the request attributes, + * on the matched handler and on the response, for a response returned by the `rest_pre_dispatch` filter too. + * * @since 7.2.0 * + * @see WP_REST_Response::get_response_processing() + * * @param array $handler The route handler. * @param array|null $route_options Optional. The options of the handler's route. Default null. * @return array { @@ -1174,7 +1177,7 @@ public function get_route_options( $route ) { * @type bool $links Whether the response links are added to the response data as `_links`. * } * - * @phpstan-return array{fields: bool, embed: bool, links: bool} + * @phpstan-return Response_Processing */ public static function resolve_response_processing( $handler, $route_options = null ) { $processing = array( @@ -1212,58 +1215,6 @@ public static function resolve_response_processing( $handler, $route_options = n return $processing; } - /** - * Retrieves the response processing settings for a request. - * - * The settings are those of the handler the request matched. A response returned by the - * `rest_pre_dispatch` filter matches no handler, so the request is matched to a handler here - * without changing the request. An OPTIONS request is served the route description, so every - * feature is on for it. - * - * @since 7.2.0 - * - * @see WP_REST_Server::resolve_response_processing() - * - * @param WP_REST_Request|null $request The request, or null when only the response is known. - * @param WP_REST_Response|null $response Optional. The response generated for the request. Default null. - * @return array Whether each feature is on, keyed by `fields`, `embed` and `links`. Every feature is on - * when no handler matches. - * - * @phpstan-return array{fields: bool, embed: bool, links: bool} - */ - public function get_response_processing( $request, $response = null ) { - if ( $response instanceof WP_REST_Response && is_array( $response->get_matched_handler() ) ) { - $route = $response->get_matched_route(); - - return self::resolve_response_processing( - $response->get_matched_handler(), - is_string( $route ) ? $this->get_route_options( $route ) : null - ); - } - - if ( ! $request instanceof WP_REST_Request ) { - return self::resolve_response_processing( array() ); - } - - // An OPTIONS request is served the route description, which rest_handle_options_request() - // builds with the raw handlers of the route as the request attributes. - if ( 'OPTIONS' === $request->get_method() ) { - return self::resolve_response_processing( array() ); - } - - $attributes = $request->get_attributes(); - if ( isset( $attributes['response_processing'] ) ) { - return self::resolve_response_processing( $attributes ); - } - - $match = $this->find_handler_for_request( $request ); - if ( null === $match ) { - return self::resolve_response_processing( array() ); - } - - return self::resolve_response_processing( $match[1], $this->get_route_options( $match[0] ) ); - } - /** * Matches the request to a callback and call it. * @@ -1300,6 +1251,12 @@ public function dispatch( $request ) { $result = $this->error_to_response( $result ); } + // The response matched no handler, so it follows the settings of the handler the request would match. + $match = $this->find_handler_for_request( $request ); + if ( null !== $match ) { + $result->set_response_processing( self::resolve_response_processing( $match[1], $this->get_route_options( $match[0] ) ) ); + } + array_pop( $this->dispatching_requests ); return $result; } @@ -1571,6 +1528,7 @@ protected function respond_to_request( $request, $route, $handler, $response ) { $response->set_matched_route( $route ); $response->set_matched_handler( $handler ); + $response->set_response_processing( self::resolve_response_processing( $handler, $this->get_route_options( $route ) ) ); return $response; } @@ -2120,12 +2078,18 @@ public function serve_batch_request_v1( WP_REST_Request $batch_request ) { $result = $this->respond_to_request( $single_request, $route, $handler, $error ); } + } elseif ( ! is_wp_error( $matches[ $i ] ) ) { + // The response matched no handler, so it follows the settings of the handler the request matched. + $result = rest_ensure_response( $result ); + if ( $result instanceof WP_REST_Response ) { + $result->set_response_processing( self::resolve_response_processing( $matches[ $i ][1], $this->get_route_options( $matches[ $i ][0] ) ) ); + } } /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $single_request ); - $responses[] = $this->envelope_response( $result, false, $single_request )->get_data(); + $responses[] = $this->envelope_response( $result, false )->get_data(); } return new WP_REST_Response( array( 'responses' => $responses ), WP_Http::MULTI_STATUS ); diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index 12d2a4162f38f..f0601f0857ed3 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -3253,7 +3253,7 @@ public function test_handler_setting_takes_precedence_over_route_setting( array /** * @ticket 66267 */ - public function test_response_processing_is_set_on_the_request_attributes_and_the_matched_handler(): void { + public function test_response_processing_is_set_on_the_request_attributes_the_matched_handler_and_the_response(): void { $this->register_response_processing_route( array( 'a' => 1 ), false, array( 'response_processing' => array( 'links' => false ) ) ); $request = new WP_REST_Request( 'GET', '/test-ns/response-processing' ); @@ -3266,7 +3266,7 @@ public function test_response_processing_is_set_on_the_request_attributes_and_th $this->assertSame( $expected, $request->get_attributes()['response_processing'], 'The request attributes should carry the resolved settings.' ); $this->assertSame( $expected, $response->get_matched_handler()['response_processing'], 'The matched handler should carry the resolved settings.' ); - $this->assertSame( $expected, rest_get_server()->get_response_processing( $request, $response ), 'The settings of the response should be the resolved ones.' ); + $this->assertSame( $expected, $response->get_response_processing(), 'The response should carry the resolved settings.' ); } /** @@ -3285,7 +3285,7 @@ public function test_routes_without_the_option_have_every_feature_on(): void { $this->assertSame( $expected, $request->get_attributes()['response_processing'], 'The request attributes should have every feature on.' ); $this->assertSame( $expected, $response->get_matched_handler()['response_processing'], 'The matched handler should have every feature on.' ); - $this->assertSame( $expected, rest_get_server()->get_response_processing( $request, $response ), 'The settings of the response should have every feature on.' ); + $this->assertSame( $expected, $response->get_response_processing(), 'The response should have every feature on.' ); } /** @@ -3355,16 +3355,149 @@ static function () { /** * @ticket 66267 */ - public function test_get_response_processing_without_a_matched_handler(): void { + public function test_hijacked_responses_carry_the_settings_of_their_route(): void { + $this->register_response_processing_route( array( 'unexpected' => true ), true, array( 'response_processing' => array( 'links' => false ) ) ); + + add_filter( + 'rest_pre_dispatch', + static function () { + return array( 'a' => 1 ); + } + ); + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/test-ns/response-processing' ) ); + + $this->assertSame( + array( + 'fields' => true, + 'embed' => false, + 'links' => false, + ), + $response->get_response_processing(), + 'A response returned by rest_pre_dispatch should carry the settings of the handler the request would match.' + ); + $this->assertSame( '', $response->get_matched_route(), 'A response returned by rest_pre_dispatch should still have no matched route.' ); + $this->assertNull( $response->get_matched_handler(), 'A response returned by rest_pre_dispatch should still have no matched handler.' ); + } + + /** + * @ticket 66267 + */ + public function test_hijacked_responses_for_unknown_routes_have_every_feature_on(): void { + add_filter( + 'rest_pre_dispatch', + static function () { + return array( 'a' => 1 ); + } + ); + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/test-ns/no-such-route' ) ); + + $this->assertSame( + array( + 'fields' => true, + 'embed' => true, + 'links' => true, + ), + $response->get_response_processing(), + 'A response returned by rest_pre_dispatch for an unknown route should have every feature on.' + ); + } + + /** + * @ticket 66267 + */ + public function test_response_setting_applies_when_set_before_the_fields_filter(): void { + $this->register_response_processing_route( + array( + 'a' => 1, + 'b' => 2, + ), + true + ); + + // Before rest_filter_response_fields(), which runs on the same filter at priority 10. + add_filter( + 'rest_post_dispatch', + static function ( $response ) { + $response->set_response_processing( false ); + return $response; + }, + 9 + ); + + $_GET['_fields'] = 'a'; + $_GET['_embed'] = '1'; + rest_get_server()->serve_request( '/test-ns/response-processing' ); + + $this->assertSame( '{"a":1,"b":2}', rest_get_server()->sent_body, 'A setting made on the response after dispatch should apply when the response is served.' ); + } + + /** + * Data provider for test_response_set_response_processing(). + * + * @return array}> + */ + public static function data_response_set_response_processing(): array { $all_on = array( 'fields' => true, 'embed' => true, 'links' => true, ); - $this->assertSame( $all_on, rest_get_server()->get_response_processing( new WP_REST_Request( 'GET', '/test-ns/no-such-route' ) ), 'A request that matches no route should enable every feature.' ); - $this->assertSame( $all_on, rest_get_server()->get_response_processing( null, new WP_REST_Response() ), 'A response without a matched handler should enable every feature.' ); - $this->assertSame( $all_on, rest_get_server()->get_response_processing( null ), 'The defaults should apply without a request.' ); + return array( + 'true' => array( true, $all_on ), + 'false' => array( + false, + array( + 'fields' => false, + 'embed' => false, + 'links' => false, + ), + ), + 'empty array' => array( array(), $all_on ), + 'fields off' => array( + array( 'fields' => false ), + array( + 'fields' => false, + 'embed' => true, + 'links' => true, + ), + ), + 'links off' => array( + array( 'links' => false ), + array( + 'fields' => true, + 'embed' => false, + 'links' => false, + ), + ), + ); + } + + /** + * @ticket 66267 + * @dataProvider data_response_set_response_processing + * + * @param mixed $processing The setting, in the form of the `response_processing` option. + * @param array $expected The expected settings of the response. + */ + public function test_response_set_response_processing( $processing, array $expected ): void { + $response = new WP_REST_Response(); + + $this->assertSame( + array( + 'fields' => true, + 'embed' => true, + 'links' => true, + ), + $response->get_response_processing(), + 'A new response should have every feature on.' + ); + + $response->set_response_processing( $processing ); + + $this->assertSame( $expected, $response->get_response_processing(), 'The setting should be resolved like the route option.' ); } /** From 35d29145acd27dd74bb0131480568b9651e00252 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 23 Oct 2026 14:57:12 +0100 Subject: [PATCH 5/7] REST API: Rename the `response_processing` option to `data_processing`. The settings live on the response now, so the option, `WP_REST_Server::resolve_data_processing()` and the `WP_REST_Response` accessors are named after the response data they apply to. --- src/wp-includes/rest-api.php | 2 +- .../rest-api/class-wp-rest-response.php | 30 +-- .../rest-api/class-wp-rest-server.php | 42 ++-- .../endpoints/class-wp-rest-controller.php | 4 +- .../tests/rest-api/rest-controller.php | 12 +- tests/phpunit/tests/rest-api/rest-server.php | 234 +++++++++--------- 6 files changed, 162 insertions(+), 162 deletions(-) diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index cb46f36b14d88..28332a6562063 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -963,7 +963,7 @@ function rest_filter_response_fields( $response, $server, $request ) { return $response; } - $processing = $response->get_response_processing(); + $processing = $response->get_data_processing(); if ( ! $processing['fields'] ) { return $response; } diff --git a/src/wp-includes/rest-api/class-wp-rest-response.php b/src/wp-includes/rest-api/class-wp-rest-response.php index dca6e5bdabe8e..f86976e94dfa0 100644 --- a/src/wp-includes/rest-api/class-wp-rest-response.php +++ b/src/wp-includes/rest-api/class-wp-rest-response.php @@ -15,7 +15,7 @@ * @see WP_HTTP_Response * * @phpstan-import-type Route_Handler from WP_REST_Server - * @phpstan-import-type Response_Processing from WP_REST_Server + * @phpstan-import-type Data_Processing from WP_REST_Server * * @phpstan-type Target_Hints array{ * allow?: list, @@ -71,13 +71,13 @@ class WP_REST_Response extends WP_HTTP_Response { protected $matched_handler = null; /** - * Whether each response processing feature is on for the response. + * Whether each data processing feature is on for the response. * * @since 7.2.0 * @var array Whether the `fields`, `embed` and `links` features are on. Every feature is on by default. - * @phpstan-var Response_Processing + * @phpstan-var Data_Processing */ - protected $response_processing = array( + protected $data_processing = array( 'fields' => true, 'embed' => true, 'links' => true, @@ -264,47 +264,47 @@ public function set_matched_handler( $handler ) { } /** - * Retrieves whether each response processing feature is on for the response. + * Retrieves whether each data processing feature is on for the response. * * The server sets the settings of the matched route when it dispatches a request, for responses * returned by the `rest_pre_dispatch` filter too. Every feature is on until then. * * @since 7.2.0 * - * @see WP_REST_Server::resolve_response_processing() + * @see WP_REST_Server::resolve_data_processing() * * @return array { - * Whether each response processing feature is on. + * Whether each data processing feature is on. * * @type bool $fields Whether the `_fields` parameter filters the response data. * @type bool $embed Whether the `_embed` parameter embeds linked resources in the response data. * @type bool $links Whether the response links are added to the response data as `_links`. * } * - * @phpstan-return Response_Processing + * @phpstan-return Data_Processing */ - public function get_response_processing() { - return $this->response_processing; + public function get_data_processing() { + return $this->data_processing; } /** - * Sets whether each response processing feature is on for the response. + * Sets whether each data processing feature is on for the response. * - * The setting takes the form of the `response_processing` route option: `false` turns off every + * The setting takes the form of the `data_processing` route option: `false` turns off every * feature, and an array turns off, or on, the features it names. Embedding needs the links, so * `embed` is off whenever `links` is off. * * @since 7.2.0 * - * @see WP_REST_Server::resolve_response_processing() + * @see WP_REST_Server::resolve_data_processing() * * @param bool|array $processing Whether every feature is on, or whether the `fields`, `embed` * and `links` features are on. A feature not named stays on. * * @phpstan-param bool|array{fields?: bool, embed?: bool, links?: bool} $processing */ - public function set_response_processing( $processing ) { - $this->response_processing = WP_REST_Server::resolve_response_processing( array( 'response_processing' => $processing ) ); + public function set_data_processing( $processing ) { + $this->data_processing = WP_REST_Server::resolve_data_processing( array( 'data_processing' => $processing ) ); } /** diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index 638c574ea0c81..d37b3f33cb331 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -40,10 +40,10 @@ * accept_raw: bool, * show_in_index: bool, * allow_batch?: array{v1?: bool}|false, - * response_processing?: bool|array{fields?: bool, embed?: bool, links?: bool}, + * data_processing?: bool|array{fields?: bool, embed?: bool, links?: bool}, * ... * } - * @phpstan-type Response_Processing array{fields: bool, embed: bool, links: bool} + * @phpstan-type Data_Processing array{fields: bool, embed: bool, links: bool} */ #[AllowDynamicProperties] class WP_REST_Server { @@ -625,11 +625,11 @@ public function serve_request( $path = null ) { * @since 4.4.0 * @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include. * @since 7.2.0 The links are not added or embedded when the response turns off the `links` or - * `embed` response processing, scalar and object data is returned as it is unless + * `embed` data processing, scalar and object data is returned as it is unless * the object implements `ArrayAccess`, and list items that are not arrays are left * as they are when embedding. * - * @see WP_REST_Response::get_response_processing() + * @see WP_REST_Response::get_data_processing() * * @param WP_REST_Response $response Response object. * @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links. @@ -645,7 +645,7 @@ public function response_to_data( $response, $embed ) { return $data; } - $processing = $response->get_response_processing(); + $processing = $response->get_data_processing(); $links = $processing['links'] ? self::get_compact_response_links( $response ) : array(); if ( ! empty( $links ) ) { @@ -1144,9 +1144,9 @@ public function get_route_options( $route ) { } /** - * Resolves the response processing settings of a route handler. + * Resolves the data processing settings of a route handler. * - * A route, or one of its handlers, sets the `response_processing` option to serve its data as returned. + * A route, or one of its handlers, sets the `data_processing` option to serve its data as returned. * The option is `false` to turn off every feature, or an array with any of these keys: * * - `fields`: Whether the `_fields` parameter filters the response data. @@ -1165,21 +1165,21 @@ public function get_route_options( $route ) { * * @since 7.2.0 * - * @see WP_REST_Response::get_response_processing() + * @see WP_REST_Response::get_data_processing() * * @param array $handler The route handler. * @param array|null $route_options Optional. The options of the handler's route. Default null. * @return array { - * Whether each response processing feature is on. + * Whether each data processing feature is on. * * @type bool $fields Whether the `_fields` parameter filters the response data. * @type bool $embed Whether the `_embed` parameter embeds linked resources in the response data. * @type bool $links Whether the response links are added to the response data as `_links`. * } * - * @phpstan-return Response_Processing + * @phpstan-return Data_Processing */ - public static function resolve_response_processing( $handler, $route_options = null ) { + public static function resolve_data_processing( $handler, $route_options = null ) { $processing = array( 'fields' => true, 'embed' => true, @@ -1187,8 +1187,8 @@ public static function resolve_response_processing( $handler, $route_options = n ); $settings = array( - $route_options['response_processing'] ?? null, - $handler['response_processing'] ?? null, + $route_options['data_processing'] ?? null, + $handler['data_processing'] ?? null, ); foreach ( $settings as $setting ) { @@ -1254,7 +1254,7 @@ public function dispatch( $request ) { // The response matched no handler, so it follows the settings of the handler the request would match. $match = $this->find_handler_for_request( $request ); if ( null !== $match ) { - $result->set_response_processing( self::resolve_response_processing( $match[1], $this->get_route_options( $match[0] ) ) ); + $result->set_data_processing( self::resolve_data_processing( $match[1], $this->get_route_options( $match[0] ) ) ); } array_pop( $this->dispatching_requests ); @@ -1334,8 +1334,8 @@ protected function match_request_to_handler( $request ) { list( $route, $handler, $args ) = $match; - // Resolve the response processing settings once, for the request attributes and the matched handler. - $handler['response_processing'] = self::resolve_response_processing( $handler, $this->get_route_options( $route ) ); + // Resolve the data processing settings once, for the request attributes and the matched handler. + $handler['data_processing'] = self::resolve_data_processing( $handler, $this->get_route_options( $route ) ); if ( ! is_callable( $handler['callback'] ) ) { return array( $route, $handler ); @@ -1528,7 +1528,7 @@ protected function respond_to_request( $request, $route, $handler, $response ) { $response->set_matched_route( $route ); $response->set_matched_handler( $handler ); - $response->set_response_processing( self::resolve_response_processing( $handler, $this->get_route_options( $route ) ) ); + $response->set_data_processing( self::resolve_data_processing( $handler, $this->get_route_options( $route ) ) ); return $response; } @@ -1858,10 +1858,10 @@ public function get_data_for_route( $route, $callbacks, $context = 'view' ) { $endpoint_data['allow_batch'] = $callback_batch; } - $response_processing = self::resolve_response_processing( $callback, $route_options ); + $data_processing = self::resolve_data_processing( $callback, $route_options ); - if ( in_array( false, $response_processing, true ) ) { - $endpoint_data['response_processing'] = $response_processing; + if ( in_array( false, $data_processing, true ) ) { + $endpoint_data['data_processing'] = $data_processing; } if ( isset( $callback['args'] ) ) { @@ -2082,7 +2082,7 @@ public function serve_batch_request_v1( WP_REST_Request $batch_request ) { // The response matched no handler, so it follows the settings of the handler the request matched. $result = rest_ensure_response( $result ); if ( $result instanceof WP_REST_Response ) { - $result->set_response_processing( self::resolve_response_processing( $matches[ $i ][1], $this->get_route_options( $matches[ $i ][0] ) ) ); + $result->set_data_processing( self::resolve_data_processing( $matches[ $i ][1], $this->get_route_options( $matches[ $i ][0] ) ) ); } } diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php index 0b65682863017..4af4f0ed3c206 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php @@ -566,7 +566,7 @@ protected function get_object_type() { * * @since 4.9.6 * @since 7.2.0 The `_fields` argument is ignored when the matched route turns off - * the `fields` response processing. + * the `fields` data processing. * * @param WP_REST_Request $request Full details about the request. * @return string[] Fields to be included in the response. @@ -611,7 +611,7 @@ public function get_fields_for_response( $request ) { $fields = array_unique( $fields ); - $processing = WP_REST_Server::resolve_response_processing( $request->get_attributes() ); + $processing = WP_REST_Server::resolve_data_processing( $request->get_attributes() ); if ( ! isset( $request['_fields'] ) || ! $processing['fields'] ) { return $fields; diff --git a/tests/phpunit/tests/rest-api/rest-controller.php b/tests/phpunit/tests/rest-api/rest-controller.php index 15b260eb6a631..431fae4350fbe 100644 --- a/tests/phpunit/tests/rest-api/rest-controller.php +++ b/tests/phpunit/tests/rest-api/rest-controller.php @@ -442,11 +442,11 @@ public function test_get_fields_for_response_respects_embed() { } /** - * Data provider for test_get_fields_for_response_respects_response_processing(). + * Data provider for test_get_fields_for_response_respects_data_processing(). * * @return array */ - public static function data_get_fields_for_response_respects_response_processing(): array { + public static function data_get_fields_for_response_respects_data_processing(): array { return array( 'false' => array( false, false ), 'fields false' => array( array( 'fields' => false ), false ), @@ -457,15 +457,15 @@ public static function data_get_fields_for_response_respects_response_processing /** * @ticket 66267 - * @dataProvider data_get_fields_for_response_respects_response_processing + * @dataProvider data_get_fields_for_response_respects_data_processing * - * @param mixed $response_processing The `response_processing` setting of the matched handler. + * @param mixed $data_processing The `data_processing` setting of the matched handler. * @param bool $filtered Whether the `_fields` parameter should filter the fields. */ - public function test_get_fields_for_response_respects_response_processing( $response_processing, bool $filtered ): void { + public function test_get_fields_for_response_respects_data_processing( $data_processing, bool $filtered ): void { $controller = new WP_REST_Test_Controller(); $request = new WP_REST_Request( 'GET', '/wp/v2/testroute' ); - $request->set_attributes( array( 'response_processing' => $response_processing ) ); + $request->set_attributes( array( 'data_processing' => $data_processing ) ); $request->set_param( '_fields', 'somestring' ); $fields = $controller->get_fields_for_response( $request ); diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index f0601f0857ed3..f030a681acc46 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -2905,17 +2905,17 @@ public function test_prefers_developer_defined_target_hints() { } /** - * Registers a route for the response processing tests, and a route that its link embeds. + * Registers a route for the data processing tests, and a route that its link embeds. * * @param mixed $data The response data. * @param bool $with_link Whether the response has an embeddable link. * @param array $handler_options Options for the route handler. * @param array $route_options Options for the route. */ - private function register_response_processing_route( $data, bool $with_link, array $handler_options = array(), array $route_options = array() ): void { + private function register_data_processing_route( $data, bool $with_link, array $handler_options = array(), array $route_options = array() ): void { rest_get_server()->register_route( 'test-ns', - '/test-ns/response-processing', + '/test-ns/data-processing', array_merge( array( array_merge( @@ -2951,11 +2951,11 @@ private function register_response_processing_route( $data, bool $with_link, arr } /** - * Data provider for test_resolve_response_processing(). + * Data provider for test_resolve_data_processing(). * * @return array, 1: array|null, 2: array}> */ - public static function data_resolve_response_processing(): array { + public static function data_resolve_data_processing(): array { $all_on = array( 'fields' => true, 'embed' => true, @@ -2970,12 +2970,12 @@ public static function data_resolve_response_processing(): array { return array( 'no setting' => array( array(), null, $all_on ), 'empty route options' => array( array(), array(), $all_on ), - 'handler false' => array( array( 'response_processing' => false ), null, $all_off ), - 'route false' => array( array(), array( 'response_processing' => false ), $all_off ), - 'route false and handler true' => array( array( 'response_processing' => true ), array( 'response_processing' => false ), $all_on ), + 'handler false' => array( array( 'data_processing' => false ), null, $all_off ), + 'route false' => array( array(), array( 'data_processing' => false ), $all_off ), + 'route false and handler true' => array( array( 'data_processing' => true ), array( 'data_processing' => false ), $all_on ), 'route false and handler fields' => array( - array( 'response_processing' => array( 'fields' => true ) ), - array( 'response_processing' => false ), + array( 'data_processing' => array( 'fields' => true ) ), + array( 'data_processing' => false ), array( 'fields' => true, 'embed' => false, @@ -2983,7 +2983,7 @@ public static function data_resolve_response_processing(): array { ), ), 'handler fields false' => array( - array( 'response_processing' => array( 'fields' => false ) ), + array( 'data_processing' => array( 'fields' => false ) ), null, array( 'fields' => false, @@ -2992,7 +2992,7 @@ public static function data_resolve_response_processing(): array { ), ), 'handler links false turns off embed' => array( - array( 'response_processing' => array( 'links' => false ) ), + array( 'data_processing' => array( 'links' => false ) ), null, array( 'fields' => true, @@ -3001,8 +3001,8 @@ public static function data_resolve_response_processing(): array { ), ), 'handler embed true needs the links' => array( - array( 'response_processing' => array( 'embed' => true ) ), - array( 'response_processing' => array( 'links' => false ) ), + array( 'data_processing' => array( 'embed' => true ) ), + array( 'data_processing' => array( 'links' => false ) ), array( 'fields' => true, 'embed' => false, @@ -3010,33 +3010,33 @@ public static function data_resolve_response_processing(): array { ), ), 'route fields false and handler true' => array( - array( 'response_processing' => array( 'fields' => true ) ), - array( 'response_processing' => array( 'fields' => false ) ), + array( 'data_processing' => array( 'fields' => true ) ), + array( 'data_processing' => array( 'fields' => false ) ), $all_on, ), - 'unknown keys are ignored' => array( array( 'response_processing' => array( 'envelope' => false ) ), null, $all_on ), - 'other types are ignored' => array( array( 'response_processing' => 'no' ), array( 'response_processing' => 0 ), $all_on ), + 'unknown keys are ignored' => array( array( 'data_processing' => array( 'envelope' => false ) ), null, $all_on ), + 'other types are ignored' => array( array( 'data_processing' => 'no' ), array( 'data_processing' => 0 ), $all_on ), ); } /** * @ticket 66267 - * @dataProvider data_resolve_response_processing + * @dataProvider data_resolve_data_processing * * @param array $handler The route handler. * @param array|null $route_options The route options. * @param array $expected The expected settings. */ - public function test_resolve_response_processing( array $handler, ?array $route_options, array $expected ): void { - $this->assertSame( $expected, WP_REST_Server::resolve_response_processing( $handler, $route_options ), 'The settings should be resolved feature by feature.' ); + public function test_resolve_data_processing( array $handler, ?array $route_options, array $expected ): void { + $this->assertSame( $expected, WP_REST_Server::resolve_data_processing( $handler, $route_options ), 'The settings should be resolved feature by feature.' ); } /** - * Data provider for test_response_processing_false_serves_the_data_as_returned(). + * Data provider for test_data_processing_false_serves_the_data_as_returned(). * * @return array */ - public static function data_response_processing_false_serves_the_data_as_returned(): array { + public static function data_data_processing_false_serves_the_data_as_returned(): array { return array( 'associative array' => array( array( @@ -3056,17 +3056,17 @@ public static function data_response_processing_false_serves_the_data_as_returne /** * @ticket 66267 - * @dataProvider data_response_processing_false_serves_the_data_as_returned + * @dataProvider data_data_processing_false_serves_the_data_as_returned * * @param mixed $data The response data. * @param string $expected The expected response body. */ - public function test_response_processing_false_serves_the_data_as_returned( $data, string $expected ): void { - $this->register_response_processing_route( $data, true, array( 'response_processing' => false ) ); + public function test_data_processing_false_serves_the_data_as_returned( $data, string $expected ): void { + $this->register_data_processing_route( $data, true, array( 'data_processing' => false ) ); $_GET['_fields'] = 'a'; $_GET['_embed'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $this->assertSame( $expected, rest_get_server()->sent_body, 'The data should be served as returned, without links or filtering.' ); } @@ -3075,7 +3075,7 @@ public function test_response_processing_false_serves_the_data_as_returned( $dat * @ticket 66267 */ public function test_fields_are_filtered_and_links_embedded_by_default(): void { - $this->register_response_processing_route( + $this->register_data_processing_route( array( 'a' => 1, 'b' => 2, @@ -3085,7 +3085,7 @@ public function test_fields_are_filtered_and_links_embedded_by_default(): void { $_GET['_fields'] = 'a'; $_GET['_embed'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $data = json_decode( rest_get_server()->sent_body, true ); $this->assertSame( array( 'a', '_links', '_embedded' ), array_keys( $data ), 'The _fields parameter should filter the data, and the links should be added.' ); @@ -3095,30 +3095,30 @@ public function test_fields_are_filtered_and_links_embedded_by_default(): void { /** * @ticket 66267 */ - public function test_response_processing_fields_false_leaves_the_data_unfiltered(): void { - $this->register_response_processing_route( + public function test_data_processing_fields_false_leaves_the_data_unfiltered(): void { + $this->register_data_processing_route( array( 'a' => 1, 'b' => 2, ), true, - array( 'response_processing' => array( 'fields' => false ) ) + array( 'data_processing' => array( 'fields' => false ) ) ); $_GET['_fields'] = 'a'; $_GET['_embed'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $data = json_decode( rest_get_server()->sent_body, true ); $this->assertSame( array( 'a', 'b', '_links', '_embedded' ), array_keys( $data ), 'Only the _fields parameter should be ignored.' ); } /** - * Data provider for test_response_processing_links_false_leaves_out_the_links(). + * Data provider for test_data_processing_links_false_leaves_out_the_links(). * * @return array */ - public static function data_response_processing_links_false_leaves_out_the_links(): array { + public static function data_data_processing_links_false_leaves_out_the_links(): array { return array( 'associative array' => array( array( 'a' => 1 ), '{"a":1}' ), 'list' => array( array( 'a', 'b' ), '["a","b"]' ), @@ -3129,16 +3129,16 @@ public static function data_response_processing_links_false_leaves_out_the_links /** * @ticket 66267 - * @dataProvider data_response_processing_links_false_leaves_out_the_links + * @dataProvider data_data_processing_links_false_leaves_out_the_links * * @param mixed $data The response data. * @param string $expected The expected response body. */ - public function test_response_processing_links_false_leaves_out_the_links( $data, string $expected ): void { - $this->register_response_processing_route( $data, true, array( 'response_processing' => array( 'links' => false ) ) ); + public function test_data_processing_links_false_leaves_out_the_links( $data, string $expected ): void { + $this->register_data_processing_route( $data, true, array( 'data_processing' => array( 'links' => false ) ) ); $_GET['_embed'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $this->assertSame( $expected, rest_get_server()->sent_body, 'The response links should not be added or embedded.' ); } @@ -3146,11 +3146,11 @@ public function test_response_processing_links_false_leaves_out_the_links( $data /** * @ticket 66267 */ - public function test_response_processing_embed_false_keeps_the_links_without_embedding(): void { - $this->register_response_processing_route( array( 'a' => 1 ), true, array( 'response_processing' => array( 'embed' => false ) ) ); + public function test_data_processing_embed_false_keeps_the_links_without_embedding(): void { + $this->register_data_processing_route( array( 'a' => 1 ), true, array( 'data_processing' => array( 'embed' => false ) ) ); $_GET['_embed'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $data = json_decode( rest_get_server()->sent_body, true ); $this->assertArrayHasKey( '_links', $data, 'The response links should be added.' ); @@ -3160,19 +3160,19 @@ public function test_response_processing_embed_false_keeps_the_links_without_emb /** * @ticket 66267 */ - public function test_response_processing_false_keeps_the_own_links_of_the_data(): void { + public function test_data_processing_false_keeps_the_own_links_of_the_data(): void { $own_links = array( 'custom' => array( array( 'href' => 'https://example.org/' ) ) ); - $this->register_response_processing_route( + $this->register_data_processing_route( array( 'a' => 1, '_links' => $own_links, ), true, - array( 'response_processing' => false ) + array( 'data_processing' => false ) ); $_GET['_embed'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $data = json_decode( rest_get_server()->sent_body, true ); $this->assertSame( $own_links, $data['_links'], 'The _links key of the data should be served as returned.' ); @@ -3182,20 +3182,20 @@ public function test_response_processing_false_keeps_the_own_links_of_the_data() /** * @ticket 66267 */ - public function test_response_processing_applies_to_enveloped_responses(): void { - $this->register_response_processing_route( + public function test_data_processing_applies_to_enveloped_responses(): void { + $this->register_data_processing_route( array( 'a' => 1, 'b' => 2, ), true, - array( 'response_processing' => false ) + array( 'data_processing' => false ) ); $_GET['_fields'] = 'a'; $_GET['_embed'] = '1'; $_GET['_envelope'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $data = json_decode( rest_get_server()->sent_body, true ); $this->assertSame( 200, $data['status'], 'The enveloped response should succeed.' ); @@ -3216,11 +3216,11 @@ public function test_response_processing_applies_to_enveloped_responses(): void */ public static function data_handler_setting_takes_precedence_over_route_setting(): array { return array( - 'route false' => array( array(), array( 'response_processing' => false ), array( 'a', 'b' ) ), - 'route false and handler true' => array( array( 'response_processing' => true ), array( 'response_processing' => false ), array( 'a', '_links' ) ), - 'route false and handler fields' => array( array( 'response_processing' => array( 'fields' => true ) ), array( 'response_processing' => false ), array( 'a' ) ), - 'route fields false and handler links off' => array( array( 'response_processing' => array( 'links' => false ) ), array( 'response_processing' => array( 'fields' => false ) ), array( 'a', 'b' ) ), - 'route links false and handler fields off' => array( array( 'response_processing' => array( 'fields' => false ) ), array( 'response_processing' => array( 'links' => false ) ), array( 'a', 'b' ) ), + 'route false' => array( array(), array( 'data_processing' => false ), array( 'a', 'b' ) ), + 'route false and handler true' => array( array( 'data_processing' => true ), array( 'data_processing' => false ), array( 'a', '_links' ) ), + 'route false and handler fields' => array( array( 'data_processing' => array( 'fields' => true ) ), array( 'data_processing' => false ), array( 'a' ) ), + 'route fields false and handler links off' => array( array( 'data_processing' => array( 'links' => false ) ), array( 'data_processing' => array( 'fields' => false ) ), array( 'a', 'b' ) ), + 'route links false and handler fields off' => array( array( 'data_processing' => array( 'fields' => false ) ), array( 'data_processing' => array( 'links' => false ) ), array( 'a', 'b' ) ), ); } @@ -3233,7 +3233,7 @@ public static function data_handler_setting_takes_precedence_over_route_setting( * @param string[] $expected_keys The expected keys of the response body. */ public function test_handler_setting_takes_precedence_over_route_setting( array $handler_options, array $route_options, array $expected_keys ): void { - $this->register_response_processing_route( + $this->register_data_processing_route( array( 'a' => 1, 'b' => 2, @@ -3244,7 +3244,7 @@ public function test_handler_setting_takes_precedence_over_route_setting( array ); $_GET['_fields'] = 'a'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $data = json_decode( rest_get_server()->sent_body, true ); $this->assertSame( $expected_keys, array_keys( $data ), 'The handler setting should take precedence over the route setting, feature by feature.' ); @@ -3253,10 +3253,10 @@ public function test_handler_setting_takes_precedence_over_route_setting( array /** * @ticket 66267 */ - public function test_response_processing_is_set_on_the_request_attributes_the_matched_handler_and_the_response(): void { - $this->register_response_processing_route( array( 'a' => 1 ), false, array( 'response_processing' => array( 'links' => false ) ) ); + public function test_data_processing_is_set_on_the_request_attributes_the_matched_handler_and_the_response(): void { + $this->register_data_processing_route( array( 'a' => 1 ), false, array( 'data_processing' => array( 'links' => false ) ) ); - $request = new WP_REST_Request( 'GET', '/test-ns/response-processing' ); + $request = new WP_REST_Request( 'GET', '/test-ns/data-processing' ); $response = rest_get_server()->dispatch( $request ); $expected = array( 'fields' => true, @@ -3264,18 +3264,18 @@ public function test_response_processing_is_set_on_the_request_attributes_the_ma 'links' => false, ); - $this->assertSame( $expected, $request->get_attributes()['response_processing'], 'The request attributes should carry the resolved settings.' ); - $this->assertSame( $expected, $response->get_matched_handler()['response_processing'], 'The matched handler should carry the resolved settings.' ); - $this->assertSame( $expected, $response->get_response_processing(), 'The response should carry the resolved settings.' ); + $this->assertSame( $expected, $request->get_attributes()['data_processing'], 'The request attributes should carry the resolved settings.' ); + $this->assertSame( $expected, $response->get_matched_handler()['data_processing'], 'The matched handler should carry the resolved settings.' ); + $this->assertSame( $expected, $response->get_data_processing(), 'The response should carry the resolved settings.' ); } /** * @ticket 66267 */ public function test_routes_without_the_option_have_every_feature_on(): void { - $this->register_response_processing_route( array( 'a' => 1 ), false ); + $this->register_data_processing_route( array( 'a' => 1 ), false ); - $request = new WP_REST_Request( 'GET', '/test-ns/response-processing' ); + $request = new WP_REST_Request( 'GET', '/test-ns/data-processing' ); $response = rest_get_server()->dispatch( $request ); $expected = array( 'fields' => true, @@ -3283,17 +3283,17 @@ public function test_routes_without_the_option_have_every_feature_on(): void { 'links' => true, ); - $this->assertSame( $expected, $request->get_attributes()['response_processing'], 'The request attributes should have every feature on.' ); - $this->assertSame( $expected, $response->get_matched_handler()['response_processing'], 'The matched handler should have every feature on.' ); - $this->assertSame( $expected, $response->get_response_processing(), 'The response should have every feature on.' ); + $this->assertSame( $expected, $request->get_attributes()['data_processing'], 'The request attributes should have every feature on.' ); + $this->assertSame( $expected, $response->get_matched_handler()['data_processing'], 'The matched handler should have every feature on.' ); + $this->assertSame( $expected, $response->get_data_processing(), 'The response should have every feature on.' ); } /** - * Data provider for test_response_processing_applies_to_hijacked_responses(). + * Data provider for test_data_processing_applies_to_hijacked_responses(). * * @return array */ - public static function data_response_processing_applies_to_hijacked_responses(): array { + public static function data_data_processing_applies_to_hijacked_responses(): array { return array( 'associative array' => array( array( @@ -3308,13 +3308,13 @@ public static function data_response_processing_applies_to_hijacked_responses(): /** * @ticket 66267 - * @dataProvider data_response_processing_applies_to_hijacked_responses + * @dataProvider data_data_processing_applies_to_hijacked_responses * * @param mixed $data The data returned by the `rest_pre_dispatch` filter. * @param string $expected The expected response body. */ - public function test_response_processing_applies_to_hijacked_responses( $data, string $expected ): void { - $this->register_response_processing_route( array( 'unexpected' => true ), true, array( 'response_processing' => false ) ); + public function test_data_processing_applies_to_hijacked_responses( $data, string $expected ): void { + $this->register_data_processing_route( array( 'unexpected' => true ), true, array( 'data_processing' => false ) ); add_filter( 'rest_pre_dispatch', @@ -3325,7 +3325,7 @@ static function () use ( $data ) { $_GET['_fields'] = 'a'; $_GET['_embed'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $this->assertSame( $expected, rest_get_server()->sent_body, 'A response returned by rest_pre_dispatch should follow the settings of the route it was returned for.' ); } @@ -3334,7 +3334,7 @@ static function () use ( $data ) { * @ticket 66267 */ public function test_hijacked_responses_are_processed_by_default(): void { - $this->register_response_processing_route( array( 'unexpected' => true ), true ); + $this->register_data_processing_route( array( 'unexpected' => true ), true ); add_filter( 'rest_pre_dispatch', @@ -3347,7 +3347,7 @@ static function () { ); $_GET['_fields'] = 'a'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $this->assertSame( '{"a":1}', rest_get_server()->sent_body, 'A response returned by rest_pre_dispatch for a route without the option should be filtered.' ); } @@ -3356,7 +3356,7 @@ static function () { * @ticket 66267 */ public function test_hijacked_responses_carry_the_settings_of_their_route(): void { - $this->register_response_processing_route( array( 'unexpected' => true ), true, array( 'response_processing' => array( 'links' => false ) ) ); + $this->register_data_processing_route( array( 'unexpected' => true ), true, array( 'data_processing' => array( 'links' => false ) ) ); add_filter( 'rest_pre_dispatch', @@ -3365,7 +3365,7 @@ static function () { } ); - $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/test-ns/response-processing' ) ); + $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/test-ns/data-processing' ) ); $this->assertSame( array( @@ -3373,7 +3373,7 @@ static function () { 'embed' => false, 'links' => false, ), - $response->get_response_processing(), + $response->get_data_processing(), 'A response returned by rest_pre_dispatch should carry the settings of the handler the request would match.' ); $this->assertSame( '', $response->get_matched_route(), 'A response returned by rest_pre_dispatch should still have no matched route.' ); @@ -3399,7 +3399,7 @@ static function () { 'embed' => true, 'links' => true, ), - $response->get_response_processing(), + $response->get_data_processing(), 'A response returned by rest_pre_dispatch for an unknown route should have every feature on.' ); } @@ -3408,7 +3408,7 @@ static function () { * @ticket 66267 */ public function test_response_setting_applies_when_set_before_the_fields_filter(): void { - $this->register_response_processing_route( + $this->register_data_processing_route( array( 'a' => 1, 'b' => 2, @@ -3420,7 +3420,7 @@ public function test_response_setting_applies_when_set_before_the_fields_filter( add_filter( 'rest_post_dispatch', static function ( $response ) { - $response->set_response_processing( false ); + $response->set_data_processing( false ); return $response; }, 9 @@ -3428,17 +3428,17 @@ static function ( $response ) { $_GET['_fields'] = 'a'; $_GET['_embed'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $this->assertSame( '{"a":1,"b":2}', rest_get_server()->sent_body, 'A setting made on the response after dispatch should apply when the response is served.' ); } /** - * Data provider for test_response_set_response_processing(). + * Data provider for test_response_set_data_processing(). * * @return array}> */ - public static function data_response_set_response_processing(): array { + public static function data_response_set_data_processing(): array { $all_on = array( 'fields' => true, 'embed' => true, @@ -3477,12 +3477,12 @@ public static function data_response_set_response_processing(): array { /** * @ticket 66267 - * @dataProvider data_response_set_response_processing + * @dataProvider data_response_set_data_processing * - * @param mixed $processing The setting, in the form of the `response_processing` option. + * @param mixed $processing The setting, in the form of the `data_processing` option. * @param array $expected The expected settings of the response. */ - public function test_response_set_response_processing( $processing, array $expected ): void { + public function test_response_set_data_processing( $processing, array $expected ): void { $response = new WP_REST_Response(); $this->assertSame( @@ -3491,21 +3491,21 @@ public function test_response_set_response_processing( $processing, array $expec 'embed' => true, 'links' => true, ), - $response->get_response_processing(), + $response->get_data_processing(), 'A new response should have every feature on.' ); - $response->set_response_processing( $processing ); + $response->set_data_processing( $processing ); - $this->assertSame( $expected, $response->get_response_processing(), 'The setting should be resolved like the route option.' ); + $this->assertSame( $expected, $response->get_data_processing(), 'The setting should be resolved like the route option.' ); } /** - * Data provider for test_response_processing_applies_to_preloaded_requests(). + * Data provider for test_data_processing_applies_to_preloaded_requests(). * * @return array */ - public static function data_response_processing_applies_to_preloaded_requests(): array { + public static function data_data_processing_applies_to_preloaded_requests(): array { return array( 'associative array' => array( array( @@ -3521,33 +3521,33 @@ public static function data_response_processing_applies_to_preloaded_requests(): /** * @ticket 66267 - * @dataProvider data_response_processing_applies_to_preloaded_requests + * @dataProvider data_data_processing_applies_to_preloaded_requests * * @param mixed $data The response data. * @param string $expected The expected JSON encoding of the preloaded body. */ - public function test_response_processing_applies_to_preloaded_requests( $data, string $expected ): void { - $this->register_response_processing_route( $data, true, array( 'response_processing' => false ) ); + public function test_data_processing_applies_to_preloaded_requests( $data, string $expected ): void { + $this->register_data_processing_route( $data, true, array( 'data_processing' => false ) ); - $preload = rest_preload_api_request( array(), '/test-ns/response-processing?_fields=a&_embed=1' ); + $preload = rest_preload_api_request( array(), '/test-ns/data-processing?_fields=a&_embed=1' ); - $this->assertSame( $expected, wp_json_encode( $preload['/test-ns/response-processing?_fields=a&_embed=1']['body'] ), 'The preloaded body should be the data as returned.' ); + $this->assertSame( $expected, wp_json_encode( $preload['/test-ns/data-processing?_fields=a&_embed=1']['body'] ), 'The preloaded body should be the data as returned.' ); } /** * @ticket 66267 */ - public function test_response_processing_applies_to_batch_requests(): void { - $this->register_response_processing_route( + public function test_data_processing_applies_to_batch_requests(): void { + $this->register_data_processing_route( array( 'a' => 1, 'b' => 2, ), true, array( - 'methods' => 'POST', - 'allow_batch' => array( 'v1' => true ), - 'response_processing' => false, + 'methods' => 'POST', + 'allow_batch' => array( 'v1' => true ), + 'data_processing' => false, ) ); @@ -3555,7 +3555,7 @@ public function test_response_processing_applies_to_batch_requests(): void { $request->set_body_params( array( 'requests' => array( - array( 'path' => '/test-ns/response-processing?_fields=a' ), + array( 'path' => '/test-ns/data-processing?_fields=a' ), ), ) ); @@ -3576,10 +3576,10 @@ public function test_response_processing_applies_to_batch_requests(): void { /** * @ticket 66267 */ - public function test_response_processing_is_listed_in_the_index(): void { + public function test_data_processing_is_listed_in_the_index(): void { rest_get_server()->register_route( 'test-ns', - '/test-ns/response-processing', + '/test-ns/data-processing', array( array( 'methods' => 'GET', @@ -3590,13 +3590,13 @@ public function test_response_processing_is_listed_in_the_index(): void { 'methods' => 'POST', 'callback' => '__return_null', 'permission_callback' => '__return_true', - 'response_processing' => array( 'fields' => true ), + 'data_processing' => array( 'fields' => true ), ), - 'response_processing' => array( 'fields' => false ), + 'data_processing' => array( 'fields' => false ), ) ); - $request = new WP_REST_Request( 'OPTIONS', '/test-ns/response-processing' ); + $request = new WP_REST_Request( 'OPTIONS', '/test-ns/data-processing' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -3606,21 +3606,21 @@ public function test_response_processing_is_listed_in_the_index(): void { 'embed' => true, 'links' => true, ), - $data['endpoints'][0]['response_processing'], + $data['endpoints'][0]['data_processing'], 'An endpoint with a feature turned off should list the settings.' ); - $this->assertArrayNotHasKey( 'response_processing', $data['endpoints'][1], 'An endpoint with every feature on should not list the settings.' ); + $this->assertArrayNotHasKey( 'data_processing', $data['endpoints'][1], 'An endpoint with every feature on should not list the settings.' ); } /** * @ticket 66267 */ public function test_options_requests_are_processed_by_default(): void { - $this->register_response_processing_route( array( 'a' => 1 ), true, array( 'response_processing' => false ) ); + $this->register_data_processing_route( array( 'a' => 1 ), true, array( 'data_processing' => false ) ); $_SERVER['REQUEST_METHOD'] = 'OPTIONS'; $_GET['_fields'] = 'methods'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $_SERVER['REQUEST_METHOD'] = 'GET'; $data = json_decode( rest_get_server()->sent_body, true ); @@ -3631,10 +3631,10 @@ public function test_options_requests_are_processed_by_default(): void { * @ticket 66267 */ public function test_array_access_data_gets_links_and_embedded_resources(): void { - $this->register_response_processing_route( new ArrayObject( array( 'a' => 1 ) ), true ); + $this->register_data_processing_route( new ArrayObject( array( 'a' => 1 ) ), true ); $_GET['_embed'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $data = json_decode( rest_get_server()->sent_body, true ); $this->assertSame( array( 'a', '_links', '_embedded' ), array_keys( $data ), 'An object that acts as an array should get the links and the embedded resources.' ); @@ -3666,11 +3666,11 @@ public static function data_non_array_data_is_served_as_returned_by_default(): a * @param string $expected The expected response body. */ public function test_non_array_data_is_served_as_returned_by_default( $data, bool $with_link, string $expected ): void { - $this->register_response_processing_route( $data, $with_link ); + $this->register_data_processing_route( $data, $with_link ); $_GET['_fields'] = 'a'; $_GET['_embed'] = '1'; - rest_get_server()->serve_request( '/test-ns/response-processing' ); + rest_get_server()->serve_request( '/test-ns/data-processing' ); $this->assertSame( $expected, rest_get_server()->sent_body, 'Data without fields, or items without fields, should be served as returned.' ); } From 78bb14a8f9102bf25d08c0f32c8d4b20db0eb520 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 23 Oct 2026 15:09:18 +0100 Subject: [PATCH 6/7] REST API: Resolve the data processing settings once, when the handler is found. `WP_REST_Server::find_handler_for_request()` sets the resolved settings on the handler it returns, so the matched handler, the response and the responses returned by `rest_pre_dispatch` read them from there instead of resolving them again. --- .../rest-api/class-wp-rest-server.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index d37b3f33cb331..ed583cc900655 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -1254,7 +1254,7 @@ public function dispatch( $request ) { // The response matched no handler, so it follows the settings of the handler the request would match. $match = $this->find_handler_for_request( $request ); if ( null !== $match ) { - $result->set_data_processing( self::resolve_data_processing( $match[1], $this->get_route_options( $match[0] ) ) ); + $result->set_data_processing( $match[1]['data_processing'] ?? true ); } array_pop( $this->dispatching_requests ); @@ -1334,9 +1334,6 @@ protected function match_request_to_handler( $request ) { list( $route, $handler, $args ) = $match; - // Resolve the data processing settings once, for the request attributes and the matched handler. - $handler['data_processing'] = self::resolve_data_processing( $handler, $this->get_route_options( $route ) ); - if ( ! is_callable( $handler['callback'] ) ) { return array( $route, $handler ); } @@ -1360,11 +1357,14 @@ protected function match_request_to_handler( $request ) { /** * Finds the route and the handler that a request matches, without changing the request. * + * The handler carries its resolved data processing settings under `data_processing`. + * * @access private * @since 7.2.0 * * @param WP_REST_Request $request The request object. - * @return array|null The route, the handler and the URL parameters, or null when no handler matches. + * @return array|null The route, the handler with its resolved data processing settings, and the URL + * parameters, or null when no handler matches. * * @phpstan-return array{non-empty-string, Route_Handler, array}|null */ @@ -1411,6 +1411,9 @@ protected function find_handler_for_request( $request ) { continue; } + // Resolve the data processing settings once, for the request attributes, the matched handler and the response. + $handler['data_processing'] = self::resolve_data_processing( $handler, $this->get_route_options( $route ) ); + return array( $route, $handler, $args ); } } @@ -1528,7 +1531,7 @@ protected function respond_to_request( $request, $route, $handler, $response ) { $response->set_matched_route( $route ); $response->set_matched_handler( $handler ); - $response->set_data_processing( self::resolve_data_processing( $handler, $this->get_route_options( $route ) ) ); + $response->set_data_processing( $handler['data_processing'] ?? true ); return $response; } @@ -2082,7 +2085,7 @@ public function serve_batch_request_v1( WP_REST_Request $batch_request ) { // The response matched no handler, so it follows the settings of the handler the request matched. $result = rest_ensure_response( $result ); if ( $result instanceof WP_REST_Response ) { - $result->set_data_processing( self::resolve_data_processing( $matches[ $i ][1], $this->get_route_options( $matches[ $i ][0] ) ) ); + $result->set_data_processing( $matches[ $i ][1]['data_processing'] ?? true ); } } From aed46297011be887826d4df40f21ec9e03119736 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 23 Oct 2026 16:08:14 +0100 Subject: [PATCH 7/7] REST API: Tag the data processing tests with their own ticket. --- .../tests/rest-api/rest-controller.php | 2 +- tests/phpunit/tests/rest-api/rest-server.php | 46 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-controller.php b/tests/phpunit/tests/rest-api/rest-controller.php index 431fae4350fbe..1c7202dc8825d 100644 --- a/tests/phpunit/tests/rest-api/rest-controller.php +++ b/tests/phpunit/tests/rest-api/rest-controller.php @@ -456,7 +456,7 @@ public static function data_get_fields_for_response_respects_data_processing(): } /** - * @ticket 66267 + * @ticket 66372 * @dataProvider data_get_fields_for_response_respects_data_processing * * @param mixed $data_processing The `data_processing` setting of the matched handler. diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index f030a681acc46..3ff82c7231fd1 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -3020,7 +3020,7 @@ public static function data_resolve_data_processing(): array { } /** - * @ticket 66267 + * @ticket 66372 * @dataProvider data_resolve_data_processing * * @param array $handler The route handler. @@ -3055,7 +3055,7 @@ public static function data_data_processing_false_serves_the_data_as_returned(): } /** - * @ticket 66267 + * @ticket 66372 * @dataProvider data_data_processing_false_serves_the_data_as_returned * * @param mixed $data The response data. @@ -3072,7 +3072,7 @@ public function test_data_processing_false_serves_the_data_as_returned( $data, s } /** - * @ticket 66267 + * @ticket 66372 */ public function test_fields_are_filtered_and_links_embedded_by_default(): void { $this->register_data_processing_route( @@ -3093,7 +3093,7 @@ public function test_fields_are_filtered_and_links_embedded_by_default(): void { } /** - * @ticket 66267 + * @ticket 66372 */ public function test_data_processing_fields_false_leaves_the_data_unfiltered(): void { $this->register_data_processing_route( @@ -3128,7 +3128,7 @@ public static function data_data_processing_links_false_leaves_out_the_links(): } /** - * @ticket 66267 + * @ticket 66372 * @dataProvider data_data_processing_links_false_leaves_out_the_links * * @param mixed $data The response data. @@ -3144,7 +3144,7 @@ public function test_data_processing_links_false_leaves_out_the_links( $data, st } /** - * @ticket 66267 + * @ticket 66372 */ public function test_data_processing_embed_false_keeps_the_links_without_embedding(): void { $this->register_data_processing_route( array( 'a' => 1 ), true, array( 'data_processing' => array( 'embed' => false ) ) ); @@ -3158,7 +3158,7 @@ public function test_data_processing_embed_false_keeps_the_links_without_embeddi } /** - * @ticket 66267 + * @ticket 66372 */ public function test_data_processing_false_keeps_the_own_links_of_the_data(): void { $own_links = array( 'custom' => array( array( 'href' => 'https://example.org/' ) ) ); @@ -3180,7 +3180,7 @@ public function test_data_processing_false_keeps_the_own_links_of_the_data(): vo } /** - * @ticket 66267 + * @ticket 66372 */ public function test_data_processing_applies_to_enveloped_responses(): void { $this->register_data_processing_route( @@ -3225,7 +3225,7 @@ public static function data_handler_setting_takes_precedence_over_route_setting( } /** - * @ticket 66267 + * @ticket 66372 * @dataProvider data_handler_setting_takes_precedence_over_route_setting * * @param array $handler_options Options for the route handler. @@ -3251,7 +3251,7 @@ public function test_handler_setting_takes_precedence_over_route_setting( array } /** - * @ticket 66267 + * @ticket 66372 */ public function test_data_processing_is_set_on_the_request_attributes_the_matched_handler_and_the_response(): void { $this->register_data_processing_route( array( 'a' => 1 ), false, array( 'data_processing' => array( 'links' => false ) ) ); @@ -3270,7 +3270,7 @@ public function test_data_processing_is_set_on_the_request_attributes_the_matche } /** - * @ticket 66267 + * @ticket 66372 */ public function test_routes_without_the_option_have_every_feature_on(): void { $this->register_data_processing_route( array( 'a' => 1 ), false ); @@ -3307,7 +3307,7 @@ public static function data_data_processing_applies_to_hijacked_responses(): arr } /** - * @ticket 66267 + * @ticket 66372 * @dataProvider data_data_processing_applies_to_hijacked_responses * * @param mixed $data The data returned by the `rest_pre_dispatch` filter. @@ -3331,7 +3331,7 @@ static function () use ( $data ) { } /** - * @ticket 66267 + * @ticket 66372 */ public function test_hijacked_responses_are_processed_by_default(): void { $this->register_data_processing_route( array( 'unexpected' => true ), true ); @@ -3353,7 +3353,7 @@ static function () { } /** - * @ticket 66267 + * @ticket 66372 */ public function test_hijacked_responses_carry_the_settings_of_their_route(): void { $this->register_data_processing_route( array( 'unexpected' => true ), true, array( 'data_processing' => array( 'links' => false ) ) ); @@ -3381,7 +3381,7 @@ static function () { } /** - * @ticket 66267 + * @ticket 66372 */ public function test_hijacked_responses_for_unknown_routes_have_every_feature_on(): void { add_filter( @@ -3405,7 +3405,7 @@ static function () { } /** - * @ticket 66267 + * @ticket 66372 */ public function test_response_setting_applies_when_set_before_the_fields_filter(): void { $this->register_data_processing_route( @@ -3476,7 +3476,7 @@ public static function data_response_set_data_processing(): array { } /** - * @ticket 66267 + * @ticket 66372 * @dataProvider data_response_set_data_processing * * @param mixed $processing The setting, in the form of the `data_processing` option. @@ -3520,7 +3520,7 @@ public static function data_data_processing_applies_to_preloaded_requests(): arr } /** - * @ticket 66267 + * @ticket 66372 * @dataProvider data_data_processing_applies_to_preloaded_requests * * @param mixed $data The response data. @@ -3535,7 +3535,7 @@ public function test_data_processing_applies_to_preloaded_requests( $data, strin } /** - * @ticket 66267 + * @ticket 66372 */ public function test_data_processing_applies_to_batch_requests(): void { $this->register_data_processing_route( @@ -3574,7 +3574,7 @@ public function test_data_processing_applies_to_batch_requests(): void { } /** - * @ticket 66267 + * @ticket 66372 */ public function test_data_processing_is_listed_in_the_index(): void { rest_get_server()->register_route( @@ -3613,7 +3613,7 @@ public function test_data_processing_is_listed_in_the_index(): void { } /** - * @ticket 66267 + * @ticket 66372 */ public function test_options_requests_are_processed_by_default(): void { $this->register_data_processing_route( array( 'a' => 1 ), true, array( 'data_processing' => false ) ); @@ -3628,7 +3628,7 @@ public function test_options_requests_are_processed_by_default(): void { } /** - * @ticket 66267 + * @ticket 66372 */ public function test_array_access_data_gets_links_and_embedded_resources(): void { $this->register_data_processing_route( new ArrayObject( array( 'a' => 1 ) ), true ); @@ -3658,7 +3658,7 @@ public static function data_non_array_data_is_served_as_returned_by_default(): a } /** - * @ticket 66267 + * @ticket 66372 * @dataProvider data_non_array_data_is_served_as_returned_by_default * * @param mixed $data The response data.