From 7a5e12858c53eaac4c5ad25f0c78bf9b66296403 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 21 Oct 2026 20:23:37 +0100 Subject: [PATCH 1/5] 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 5b2ee49d1cdf9f3fc7c91cefcaeff517f966f446 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 22 Oct 2026 15:15:51 +0100 Subject: [PATCH 2/5] 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 | 245 +++++++--- .../endpoints/class-wp-rest-controller.php | 6 +- .../tests/rest-api/rest-controller.php | 37 ++ tests/phpunit/tests/rest-api/rest-server.php | 447 +++++++++++++++--- 5 files changed, 614 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..8bca265eebbe7 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,115 @@ 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: * - * Routes, or individual handlers of a route, can set these options to false: + * - `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`. * - * - `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`. + * 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. * - * A handler option takes precedence over the route option of the same name. + * 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. * * @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. + * @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`. + * } + * + * @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 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 ]; + } + } } - $handler = $response->get_matched_handler(); - if ( is_array( $handler ) && isset( $handler[ $option ] ) ) { - return (bool) $handler[ $option ]; + if ( ! $processing['links'] ) { + $processing['embed'] = false; } - $route = $response->get_matched_route(); - if ( ! is_string( $route ) || '' === $route ) { - return true; + 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. + * + * @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 + ); } - $route_options = $this->get_route_options( $route ); + if ( ! $request instanceof WP_REST_Request ) { + return self::resolve_response_processing( array() ); + } - return ! isset( $route_options[ $option ] ) || (bool) $route_options[ $option ]; + $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] ) ); } /** @@ -1273,6 +1353,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 +1433,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 +1442,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 +1850,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 +1888,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 +2113,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..319d28b9e3d28 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -2902,17 +2902,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 +2948,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 +3043,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 +3053,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'; - 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 _fields parameter should not filter the data.' ); + $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_by_default(): void { - $this->register_response_options_route( + public function test_fields_are_filtered_and_links_embedded_by_default(): void { + $this->register_response_processing_route( array( 'a' => 1, 'b' => 2, ), - false + 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( '{"a":1}', rest_get_server()->sent_body, 'The _fields parameter should 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.' ); } /** - * Data provider for test_allow_links_false_leaves_out_the_links(). + * @ticket 66267 + */ + public function test_response_processing_fields_false_leaves_the_data_unfiltered(): void { + $this->register_response_processing_route( + array( + 'a' => 1, + 'b' => 2, + ), + true, + array( 'response_processing' => array( 'fields' => false ) ) + ); + + $_GET['_fields'] = 'a'; + $_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', '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(). * * @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 +3126,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 +3157,322 @@ 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 ) ) ); - $this->assertSame( $expected, rest_get_server()->sent_body, 'The handler option should take precedence over the route option.' ); + $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_response_options_apply_to_preloaded_requests(): void { - $this->register_response_options_route( + 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"' ), + ); + } + + /** + * @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_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, ) ); - $preload = rest_preload_api_request( array(), '/test-ns/response-options?_fields=a&_embed=1' ); + $request = new WP_REST_Request( 'POST', '/batch/v1' ); + $request->set_body_params( + array( + 'requests' => array( + array( 'path' => '/test-ns/response-processing?_fields=a' ), + ), + ) + ); + + $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_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_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.' ); } } From c93e5defcd2f8f8f70e2c6b672a52c79179fc426 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 22 Oct 2026 15:15:51 +0100 Subject: [PATCH 3/5] 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. 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 | 19 ++++++----- tests/phpunit/tests/rest-api/rest-server.php | 34 +++++++++++++++++++ 3 files changed, 53 insertions(+), 10 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 8bca265eebbe7..13a8c22f28a53 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,25 @@ 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, and data that is + * not an array is returned as it is. * * @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 data that is not an array as it is. */ 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. + if ( ! is_array( $data ) && null !== $data ) { + return $data; + } + $processing = $this->get_response_processing( $request, $response ); $links = $processing['links'] ? self::get_compact_response_links( $response ) : array(); diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index 319d28b9e3d28..17fbfbf9627ef 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -3475,4 +3475,38 @@ public function test_response_processing_is_listed_in_the_index(): void { ); $this->assertArrayNotHasKey( 'response_processing', $data['endpoints'][1], 'An endpoint with every feature on should not list the settings.' ); } + + /** + * 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"]' ), + ); + } + + /** + * @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 257351ae930e66ecadb1bc6a62cdd3fce1afd707 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 21 Oct 2026 20:27:37 +0100 Subject: [PATCH 4/5] Abilities API: Encode empty object results as JSON objects. The run endpoint prepared only associative array results for JSON clients, so an empty array returned for an object output schema, such as the `core/settings-get` result when no setting matches, was encoded as [] and failed output validation in JSON clients. Prepare every result, including root results, lists and object results, so empty objects are encoded as {} while arrays stay arrays. Disable the `_fields` and `_embed` parameters and response links for the run endpoint, so the result follows the ability's output schema whether it is an object, an array, or a scalar value. Follow-up to [64311], [64592]. See #66267. --- ...ss-wp-rest-abilities-v1-run-controller.php | 22 +-- .../wpRestAbilitiesSettingsController.php | 44 +++++ .../wpRestAbilitiesV1RunController.php | 176 ++++++++++++++++-- 3 files changed, 216 insertions(+), 26 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php index 12f6a75083be7..cca2509b4c712 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php @@ -65,6 +65,14 @@ public function register_routes(): void { 'callback' => array( $this, 'execute_ability' ), 'permission_callback' => array( $this, 'check_ability_permissions' ), 'args' => $this->get_run_args(), + + /* + * Serve the ability result as returned, so it follows the ability's output + * schema whether it is an object, an array, or a scalar value. + */ + 'allow_fields' => false, + 'allow_embed' => false, + 'allow_links' => false, ), 'schema' => array( $this, 'get_run_schema' ), ) @@ -96,18 +104,10 @@ public function execute_ability( $request ) { } /* - * Preserve empty objects in nested properties when encoding an associative - * result as JSON. Preparation can convert empty PHP arrays to stdClass. - * - * The REST server's `_fields` and `_embed` processing expects arrays at the - * response root and for numeric collection items. Converting those values - * to objects would cause type errors, so skip root empty arrays and numeric - * collections until that processing supports objects. An empty object result - * therefore still encodes as [], and a list containing one encodes as [[]]. + * Preserve empty objects when encoding the result as JSON. Preparation can + * convert empty PHP arrays to stdClass, also within object results. */ - if ( is_array( $result ) && ! wp_is_numeric_array( $result ) ) { - $result = wp_prepare_json_value_for_client( $result, $ability->get_output_schema() ); - } + $result = wp_prepare_json_value_for_client( $result, $ability->get_output_schema() ); return rest_ensure_response( $result ); } diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesSettingsController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesSettingsController.php index 278041158ef90..3139598656c79 100644 --- a/tests/phpunit/tests/rest-api/wpRestAbilitiesSettingsController.php +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesSettingsController.php @@ -196,6 +196,50 @@ public function test_fields_filter_from_query_string_limits_settings(): void { $this->assertEqualSets( array( 'title', 'posts_per_page' ), array_keys( $data ), 'Only the settings from the comma-separated list should be returned.' ); } + /** + * Data provider for test_no_matching_setting_is_encoded_as_an_empty_object(). + * + * @return array}> + */ + public static function data_global_params(): array { + return array( + 'no global parameter' => array( array() ), + '_fields' => array( array( '_fields' => 'title' ) ), + '_embed' => array( array( '_embed' => '1' ) ), + ); + } + + /** + * An empty result is encoded as a JSON object, which the output schema requires, + * also when the global `_fields` or `_embed` parameter is given. + * + * @ticket 66267 + * @dataProvider data_global_params + * + * @param array $global_params The global parameters sent with the request. + */ + public function test_no_matching_setting_is_encoded_as_an_empty_object( array $global_params ): void { + $request = $this->run_request( + array( + 'group' => 'reading', + 'fields' => 'title', + ) + ); + foreach ( $global_params as $name => $value ) { + $request->set_param( $name, $value ); + } + + $response = $this->server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'Filters that match no setting should be accepted.' ); + + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $this->server, $request ); + $data = $this->server->response_to_data( $response, isset( $global_params['_embed'] ) ); + + $this->assertSame( '{}', wp_json_encode( $data ), 'The empty result should be encoded as an empty object.' ); + } + /** * @ticket 64605 */ diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php index 26792b200a85f..9ac3a39e7ef01 100644 --- a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php @@ -443,6 +443,118 @@ private function register_test_abilities(): void { ) ); + // Ability that returns an object result with an empty object property. + $this->register_test_ability( + 'test/nested-empty-object-in-object-output', + array( + 'label' => 'Nested Empty Object In Object Output', + 'description' => 'Returns an object result with an empty object property.', + 'category' => 'general', + 'output_schema' => array( + 'type' => 'object', + 'properties' => array( + 'settings' => array( 'type' => 'object' ), + ), + ), + 'execute_callback' => static function () { + return (object) array( 'settings' => array() ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + // Ability that returns a JsonSerializable result with an empty object property. + $this->register_test_ability( + 'test/nested-empty-object-in-json-serializable-output', + array( + 'label' => 'Nested Empty Object In JsonSerializable Output', + 'description' => 'Returns a JsonSerializable result with an empty object property.', + 'category' => 'general', + 'output_schema' => array( + 'type' => 'object', + 'properties' => array( + 'settings' => array( 'type' => 'object' ), + ), + ), + 'execute_callback' => static function () { + return new class() implements JsonSerializable { + public function jsonSerialize(): array { + return array( 'settings' => array() ); + } + }; + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + // Ability that returns an empty list. + $this->register_test_ability( + 'test/empty-list-output', + array( + 'label' => 'Empty List Output', + 'description' => 'Returns an empty list.', + 'category' => 'general', + 'output_schema' => array( + 'type' => 'array', + 'items' => array( 'type' => 'object' ), + ), + 'execute_callback' => static function () { + return array(); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + // Ability that returns a list of strings. + $this->register_test_ability( + 'test/string-list-output', + array( + 'label' => 'String List Output', + 'description' => 'Returns a list of strings.', + 'category' => 'general', + 'output_schema' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'execute_callback' => static function () { + return array( 'a', 'b' ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + // Ability that returns a string. + $this->register_test_ability( + 'test/string-output', + array( + 'label' => 'String Output', + 'description' => 'Returns a string.', + 'category' => 'general', + 'output_schema' => array( + 'type' => 'string', + ), + 'execute_callback' => static function () { + return 'value'; + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + // Read-only ability for query params testing. $this->register_test_ability( 'test/query-params', @@ -633,39 +745,73 @@ public function test_output_validation(): void { /** * Data provider for test_output_is_prepared_for_json_clients(). * - * @return array + * Each result is requested without global parameters, with `_fields` and `_embed`, + * and with a link that a `rest_post_dispatch` filter adds, alone and embedded. + * + * @return array */ public static function data_output_is_prepared_for_json_clients(): array { - return array( - 'root empty object stays unchanged' => array( 'test/empty-object-output', '[]' ), - 'nested empty object' => array( 'test/nested-empty-object-output', '{"settings":{}}' ), - 'numeric collection stays unchanged' => array( 'test/empty-object-list-output', '[[]]' ), - ); + $results = array( + 'root empty object' => array( 'test/empty-object-output', '{}' ), + 'nested empty object' => array( 'test/nested-empty-object-output', '{"settings":{}}' ), + 'numeric collection' => array( 'test/empty-object-list-output', '[{}]' ), + 'object result' => array( 'test/nested-empty-object-in-object-output', '{"settings":{}}' ), + 'JsonSerializable result' => array( 'test/nested-empty-object-in-json-serializable-output', '{"settings":{}}' ), + 'root empty list' => array( 'test/empty-list-output', '[]' ), + 'list of strings' => array( 'test/string-list-output', '["a","b"]' ), + 'string' => array( 'test/string-output', '"value"' ), + ); + + $data = array(); + foreach ( $results as $label => $result ) { + foreach ( array( 'none', '_fields', '_embed', 'link', 'embedded link' ) as $global_param ) { + $data[ "{$label}, {$global_param}" ] = array( $result[0], $global_param, $result[1] ); + } + } + + return $data; } /** - * Tests that ability results are prepared for JSON clients without breaking - * the global `_fields` and `_embed` parameters. + * Tests that ability results are prepared for JSON clients, and that the global + * `_fields` and `_embed` parameters and response links leave them unchanged. * * @ticket 66267 * @dataProvider data_output_is_prepared_for_json_clients * * @param string $ability_name The ability name. + * @param string $global_param The global parameter sent with the request, 'link' or + * 'embedded link' for a link added by a filter, or 'none'. * @param string $expected The expected encoded result. */ - public function test_output_is_prepared_for_json_clients( string $ability_name, string $expected ): void { - $request = new WP_REST_Request( 'POST', "/wp-abilities/v1/abilities/{$ability_name}/run" ); - $response = $this->server->dispatch( $request ); + public function test_output_is_prepared_for_json_clients( string $ability_name, string $global_param, string $expected ): void { + $request = new WP_REST_Request( 'POST', "/wp-abilities/v1/abilities/{$ability_name}/run" ); + if ( '_fields' === $global_param ) { + // No result has this field, so filtering would leave none of the result. + $request->set_param( '_fields', 'other' ); + } elseif ( '_embed' === $global_param || 'embedded link' === $global_param ) { + $request->set_param( '_embed', '1' ); + } - $this->assertSame( 200, $response->get_status() ); - $this->assertSame( $expected, wp_json_encode( $response->get_data() ) ); + if ( 'link' === $global_param || 'embedded link' === $global_param ) { + add_filter( + 'rest_post_dispatch', + static function ( $response ) { + $response->add_link( 'alternate', rest_url( '/wp-abilities/v1/abilities' ), array( 'embeddable' => true ) ); + return $response; + } + ); + } + + $response = $this->server->dispatch( $request ); - $request->set_param( '_fields', 'settings' ); + $this->assertSame( 200, $response->get_status(), 'The ability should run successfully.' ); add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); $response = apply_filters( 'rest_post_dispatch', $response, $this->server, $request ); + $data = $this->server->response_to_data( $response, $request->has_param( '_embed' ) ); - $this->assertSame( $expected, wp_json_encode( $this->server->response_to_data( $response, true ) ) ); + $this->assertSame( $expected, wp_json_encode( $data ), 'The result should be encoded as expected.' ); } /** From cd02d31b91dd5ca11d8361410e1a0b628f199abb Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 22 Oct 2026 15:37:50 +0100 Subject: [PATCH 5/5] Abilities API: Register the run endpoint with the `response_processing` route option. Replace the `allow_fields`, `allow_embed` and `allow_links` route handler keys with `response_processing => false`, so `_fields`, `_embed` and response links leave the ability result unchanged, and regenerate the REST API client fixture, where the run endpoint now lists the disabled processing. See #66267. --- .../class-wp-rest-abilities-v1-run-controller.php | 7 +++---- tests/qunit/fixtures/wp-api-generated.js | 5 +++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php index cca2509b4c712..7da5b524504a6 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php @@ -68,11 +68,10 @@ public function register_routes(): void { /* * Serve the ability result as returned, so it follows the ability's output - * schema whether it is an object, an array, or a scalar value. + * schema whether it is an object, an array, or a scalar value: `_fields`, + * `_embed` and response links leave it unchanged. */ - 'allow_fields' => false, - 'allow_embed' => false, - 'allow_links' => false, + 'response_processing' => false, ), 'schema' => array( $this, 'get_run_schema' ), ) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index d3ba47bef2fb6..fcf6ff01d9e44 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -12682,6 +12682,11 @@ mockedApiResponse.Schema = { "PATCH", "DELETE" ], + "response_processing": { + "fields": false, + "embed": false, + "links": false + }, "args": { "name": { "description": "Unique identifier for the ability.",