Make WordPress Core

Changeset 64689


Ignore:
Timestamp:
10/23/2026 03:46:22 PM (21 hours ago)
Author:
jorgefilipecosta
Message:

REST API: Let routes turn off _fields, _embed and response links.

Add a data_processing option to REST routes and route handlers, so a route can serve its response data as returned. The option is false to turn off every feature, or an array with any of the fields, embed and links keys. Every feature is on by default, the handler setting applies over the route setting feature by feature, and turning off links also turns off embed.
The settings are resolved once, when the request is matched, and stored on the request attributes, the matched handler and the response, where rest_filter_response_fields(), WP_REST_Controller::get_fields_for_response() and WP_REST_Server::response_to_data() read them. They apply to served, enveloped, batched and preloaded requests, and to responses returned by the rest_pre_dispatch filter. The index lists the settings of endpoints that turn a feature off.
Response data that is not an array is now served as returned even without the option: _fields leaves scalars and objects alone and keeps list items that are not arrays, links are not added to them unless the object implements ArrayAccess, and rest_preload_api_request() no longer casts preloaded data to an array.
This lets endpoints that own their output schema, such as the abilities run endpoint, return objects, lists and scalar values unchanged.

Props jorgefilipecosta, gziolo.
See #66267.
Fixes #66372.

Location:
trunk
Files:
7 edited

Legend:

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

    r64602 r64689  
    951951 *
    952952 * @since 4.8.0
     953 * @since 7.2.0 The response is left unchanged when the route turns off the `fields` response
     954 *              processing or when the response data is not an array.
    953955 *
    954956 * @param WP_REST_Response $response Current response being served.
    … …  
    962964        }
    963965
     966        $processing = $response->get_data_processing();
     967        if ( ! $processing['fields'] ) {
     968                return $response;
     969        }
     970
    964971        $data = $response->get_data();
     972
     973        // Only arrays have fields to filter.
     974        if ( ! is_array( $data ) ) {
     975                return $response;
     976        }
    965977
    966978        $fields = wp_parse_list( $request['_fields'] );
    … …  
    9941006                $new_data = array();
    9951007                foreach ( $data as $item ) {
    996                         $new_data[] = _rest_array_intersect_key_recursive( $item, $fields_as_keyed );
     1008                        $new_data[] = is_array( $item ) ? _rest_array_intersect_key_recursive( $item, $fields_as_keyed ) : $item;
    9971009                }
    9981010        } else {
    … …  
    30493061                $response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $server, $request );
    30503062                $embed    = $request->has_param( '_embed' ) ? rest_parse_embed_param( $request['_embed'] ) : false;
    3051                 $data     = (array) $server->response_to_data( $response, $embed );
     3063                $data     = $server->response_to_data( $response, $embed );
    30523064
    30533065                if ( 'OPTIONS' === $method ) {
  • trunk/src/wp-includes/rest-api/class-wp-rest-response.php

    r64527 r64689  
    1616 *
    1717 * @phpstan-import-type Route_Handler from WP_REST_Server
     18 * @phpstan-import-type Data_Processing from WP_REST_Server
    1819 *
    1920 * @phpstan-type Target_Hints array{
    … …  
    7172
    7273        /**
     74         * Whether each data processing feature is on for the response.
     75         *
     76         * @since 7.2.0
     77         * @var array<string, bool> Whether the `fields`, `embed` and `links` features are on. Every feature is on by default.
     78         * @phpstan-var Data_Processing
     79         */
     80        protected $data_processing = array(
     81                'fields' => true,
     82                'embed'  => true,
     83                'links'  => true,
     84        );
     85
     86        /**
    7387         * Adds a link to the response.
    7488         *
    … …  
    248262        public function set_matched_handler( $handler ) {
    249263                $this->matched_handler = $handler;
     264        }
     265
     266        /**
     267         * Retrieves whether each data processing feature is on for the response.
     268         *
     269         * The server sets the settings of the matched route when it dispatches a request, for responses
     270         * returned by the `rest_pre_dispatch` filter too. Every feature is on until then.
     271         *
     272         * @since 7.2.0
     273         *
     274         * @see WP_REST_Server::resolve_data_processing()
     275         *
     276         * @return array<string, bool> {
     277         *     Whether each data processing feature is on.
     278         *
     279         *     @type bool $fields Whether the `_fields` parameter filters the response data.
     280         *     @type bool $embed  Whether the `_embed` parameter embeds linked resources in the response data.
     281         *     @type bool $links  Whether the response links are added to the response data as `_links`.
     282         * }
     283         *
     284         * @phpstan-return Data_Processing
     285         */
     286        public function get_data_processing() {
     287                return $this->data_processing;
     288        }
     289
     290        /**
     291         * Sets whether each data processing feature is on for the response.
     292         *
     293         * The setting takes the form of the `data_processing` route option: `false` turns off every
     294         * feature, and an array turns off, or on, the features it names. Embedding needs the links, so
     295         * `embed` is off whenever `links` is off.
     296         *
     297         * @since 7.2.0
     298         *
     299         * @see WP_REST_Server::resolve_data_processing()
     300         *
     301         * @param bool|array<string, bool> $processing Whether every feature is on, or whether the `fields`, `embed`
     302         *                                             and `links` features are on. A feature not named stays on.
     303         *
     304         * @phpstan-param bool|array{fields?: bool, embed?: bool, links?: bool} $processing
     305         */
     306        public function set_data_processing( $processing ) {
     307                $this->data_processing = WP_REST_Server::resolve_data_processing( array( 'data_processing' => $processing ) );
    250308        }
    251309
  • trunk/src/wp-includes/rest-api/class-wp-rest-server.php

    r64588 r64689  
    4141 *     show_in_index: bool,
    4242 *     allow_batch?: array{v1?: bool}|false,
     43 *     data_processing?: bool|array{fields?: bool, embed?: bool, links?: bool},
    4344 *     ...
    4445 * }
     46 * @phpstan-type Data_Processing array{fields: bool, embed: bool, links: bool}
    4547 */
    4648#[AllowDynamicProperties]
    … …  
    623625         * @since 4.4.0
    624626         * @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include.
     627         * @since 7.2.0 The links are not added or embedded when the response turns off the `links` or
     628         *              `embed` data processing, scalar and object data is returned as it is unless
     629         *              the object implements `ArrayAccess`, and list items that are not arrays are left
     630         *              as they are when embedding.
     631         *
     632         * @see WP_REST_Response::get_data_processing()
    625633         *
    626634         * @param WP_REST_Response $response Response object.
    627635         * @param bool|string[]    $embed    Whether to embed all links, a filtered list of link relations, or no links.
    628          * @return array {
    629          *     Data with sub-requests embedded.
    630          *
    631          *     @type array $_links    Links.
    632          *     @type array $_embedded Embedded objects.
    633          * }
     636         * @return mixed Data with sub-requests embedded: an array with the links under `_links` and the embedded
     637         *               resources under `_embedded`, or scalar and object data as it is unless the object
     638         *               implements `ArrayAccess`.
    634639         */
    635640        public function response_to_data( $response, $embed ) {
    636                 $data  = $response->get_data();
    637                 $links = self::get_compact_response_links( $response );
     641                $data = $response->get_data();
     642
     643                // Scalar and object data has no place for links or embedded resources, unless the object acts as an array.
     644                if ( null !== $data && ! is_array( $data ) && ! $data instanceof ArrayAccess ) {
     645                        return $data;
     646                }
     647
     648                $processing = $response->get_data_processing();
     649                $links      = $processing['links'] ? self::get_compact_response_links( $response ) : array();
    638650
    639651                if ( ! empty( $links ) ) {
    … …  
    642654                }
    643655
    644                 if ( $embed ) {
     656                if ( $embed && $processing['embed'] ) {
    645657                        $this->embed_cache = array();
    646658                        // Determine if this is a numeric array.
    647659                        if ( wp_is_numeric_array( $data ) ) {
    648660                                foreach ( $data as $key => $item ) {
    649                                         $data[ $key ] = $this->embed_links( $item, $embed );
     661                                        $data[ $key ] = is_array( $item ) || $item instanceof ArrayAccess ? $this->embed_links( $item, $embed ) : $item;
    650662                                }
    651663                        } else {
    … …  
    11331145
    11341146        /**
     1147         * Resolves the data processing settings of a route handler.
     1148         *
     1149         * A route, or one of its handlers, sets the `data_processing` option to serve its data as returned.
     1150         * The option is `false` to turn off every feature, or an array with any of these keys:
     1151         *
     1152         * - `fields`: Whether the `_fields` parameter filters the response data.
     1153         * - `embed`:  Whether the `_embed` parameter embeds linked resources in the response data.
     1154         * - `links`:  Whether the response links are added to the response data as `_links`.
     1155         *
     1156         * Each feature is resolved on its own: the handler setting applies, then the route setting, and the
     1157         * feature stays on when neither sets it. Embedding needs the links, so `embed` is off whenever `links`
     1158         * is off.
     1159         *
     1160         * The option covers these three features only. Authentication, permission checks, status codes, headers
     1161         * and JSON encoding apply as usual, and keys of the data itself, such as its own `_links`, stay as returned.
     1162         *
     1163         * The server resolves the settings when it matches a request, and sets them on the request attributes,
     1164         * on the matched handler and on the response, for a response returned by the `rest_pre_dispatch` filter too.
     1165         *
     1166         * @since 7.2.0
     1167         *
     1168         * @see WP_REST_Response::get_data_processing()
     1169         *
     1170         * @param array      $handler       The route handler.
     1171         * @param array|null $route_options Optional. The options of the handler's route. Default null.
     1172         * @return array {
     1173         *     Whether each data processing feature is on.
     1174         *
     1175         *     @type bool $fields Whether the `_fields` parameter filters the response data.
     1176         *     @type bool $embed  Whether the `_embed` parameter embeds linked resources in the response data.
     1177         *     @type bool $links  Whether the response links are added to the response data as `_links`.
     1178         * }
     1179         *
     1180         * @phpstan-return Data_Processing
     1181         */
     1182        public static function resolve_data_processing( $handler, $route_options = null ) {
     1183                $processing = array(
     1184                        'fields' => true,
     1185                        'embed'  => true,
     1186                        'links'  => true,
     1187                );
     1188
     1189                $settings = array(
     1190                        $route_options['data_processing'] ?? null,
     1191                        $handler['data_processing'] ?? null,
     1192                );
     1193
     1194                foreach ( $settings as $setting ) {
     1195                        if ( is_bool( $setting ) ) {
     1196                                $processing = array_fill_keys( array_keys( $processing ), $setting );
     1197                                continue;
     1198                        }
     1199
     1200                        if ( ! is_array( $setting ) ) {
     1201                                continue;
     1202                        }
     1203
     1204                        foreach ( array_keys( $processing ) as $feature ) {
     1205                                if ( isset( $setting[ $feature ] ) ) {
     1206                                        $processing[ $feature ] = (bool) $setting[ $feature ];
     1207                                }
     1208                        }
     1209                }
     1210
     1211                if ( ! $processing['links'] ) {
     1212                        $processing['embed'] = false;
     1213                }
     1214
     1215                return $processing;
     1216        }
     1217
     1218        /**
    11351219         * Matches the request to a callback and call it.
    11361220         *
    … …  
    11661250                        if ( is_wp_error( $result ) ) {
    11671251                                $result = $this->error_to_response( $result );
     1252                        }
     1253
     1254                        // The response matched no handler, so it follows the settings of the handler the request would match.
     1255                        $match = $this->find_handler_for_request( $request );
     1256                        if ( null !== $match ) {
     1257                                $result->set_data_processing( $match[1]['data_processing'] ?? true );
    11681258                        }
    11691259
    … …  
    12331323         */
    12341324        protected function match_request_to_handler( $request ) {
     1325                $match = $this->find_handler_for_request( $request );
     1326
     1327                if ( null === $match ) {
     1328                        return new WP_Error(
     1329                                'rest_no_route',
     1330                                __( 'No route was found matching the URL and request method.' ),
     1331                                array( 'status' => 404 )
     1332                        );
     1333                }
     1334
     1335                list( $route, $handler, $args ) = $match;
     1336
     1337                if ( ! is_callable( $handler['callback'] ) ) {
     1338                        return array( $route, $handler );
     1339                }
     1340
     1341                $request->set_url_params( $args );
     1342                $request->set_attributes( $handler );
     1343
     1344                $defaults = array();
     1345
     1346                foreach ( $handler['args'] as $arg => $options ) {
     1347                        if ( isset( $options['default'] ) ) {
     1348                                $defaults[ $arg ] = $options['default'];
     1349                        }
     1350                }
     1351
     1352                $request->set_default_params( $defaults );
     1353
     1354                return array( $route, $handler );
     1355        }
     1356
     1357        /**
     1358         * Finds the route and the handler that a request matches, without changing the request.
     1359         *
     1360         * The handler carries its resolved data processing settings under `data_processing`.
     1361         *
     1362         * @access private
     1363         * @since 7.2.0
     1364         *
     1365         * @param WP_REST_Request $request The request object.
     1366         * @return array|null The route, the handler with its resolved data processing settings, and the URL
     1367         *                    parameters, or null when no handler matches.
     1368         *
     1369         * @phpstan-return array{non-empty-string, Route_Handler, array<string, string>}|null
     1370         */
     1371        protected function find_handler_for_request( $request ) {
    12351372                $method = $request->get_method();
    12361373                $path   = $request->get_route();
    … …  
    12661403
    12671404                        foreach ( $handlers as $handler ) {
    1268                                 $callback = $handler['callback'];
    1269 
    12701405                                // Fallback to GET method if no HEAD method is registered.
    12711406                                $checked_method = $method;
    … …  
    12771412                                }
    12781413
    1279                                 if ( ! is_callable( $callback ) ) {
    1280                                         return array( $route, $handler );
    1281                                 }
    1282 
    1283                                 $request->set_url_params( $args );
    1284                                 $request->set_attributes( $handler );
    1285 
    1286                                 $defaults = array();
    1287 
    1288                                 foreach ( $handler['args'] as $arg => $options ) {
    1289                                         if ( isset( $options['default'] ) ) {
    1290                                                 $defaults[ $arg ] = $options['default'];
    1291                                         }
    1292                                 }
    1293 
    1294                                 $request->set_default_params( $defaults );
    1295 
    1296                                 return array( $route, $handler );
    1297                         }
    1298                 }
    1299 
    1300                 return new WP_Error(
    1301                         'rest_no_route',
    1302                         __( 'No route was found matching the URL and request method.' ),
    1303                         array( 'status' => 404 )
    1304                 );
     1414                                // Resolve the data processing settings once, for the request attributes, the matched handler and the response.
     1415                                $handler['data_processing'] = self::resolve_data_processing( $handler, $this->get_route_options( $route ) );
     1416
     1417                                return array( $route, $handler, $args );
     1418                        }
     1419                }
     1420
     1421                return null;
    13051422        }
    13061423
    … …  
    14151532                $response->set_matched_route( $route );
    14161533                $response->set_matched_handler( $handler );
     1534                $response->set_data_processing( $handler['data_processing'] ?? true );
    14171535
    14181536                return $response;
    … …  
    17061824                );
    17071825
    1708                 $allow_batch = false;
    1709 
    1710                 if ( isset( $this->route_options[ $route ] ) ) {
    1711                         $options = $this->route_options[ $route ];
     1826                $allow_batch   = false;
     1827                $route_options = $this->get_route_options( $route );
     1828
     1829                if ( null !== $route_options ) {
     1830                        $options = $route_options;
    17121831
    17131832                        if ( isset( $options['namespace'] ) ) {
    … …  
    17411860                        if ( $callback_batch ) {
    17421861                                $endpoint_data['allow_batch'] = $callback_batch;
     1862                        }
     1863
     1864                        $data_processing = self::resolve_data_processing( $callback, $route_options );
     1865
     1866                        if ( in_array( false, $data_processing, true ) ) {
     1867                                $endpoint_data['data_processing'] = $data_processing;
    17431868                        }
    17441869
    … …  
    19562081
    19572082                                        $result = $this->respond_to_request( $single_request, $route, $handler, $error );
     2083                                }
     2084                        } elseif ( ! is_wp_error( $matches[ $i ] ) ) {
     2085                                // The response matched no handler, so it follows the settings of the handler the request matched.
     2086                                $result = rest_ensure_response( $result );
     2087                                if ( $result instanceof WP_REST_Response ) {
     2088                                        $result->set_data_processing( $matches[ $i ][1]['data_processing'] ?? true );
    19582089                                }
    19592090                        }
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php

    r61572 r64689  
    566566         *
    567567         * @since 4.9.6
     568         * @since 7.2.0 The `_fields` argument is ignored when the matched route turns off
     569         *              the `fields` data processing.
    568570         *
    569571         * @param WP_REST_Request $request Full details about the request.
    … …  
    610612                $fields = array_unique( $fields );
    611613
    612                 if ( ! isset( $request['_fields'] ) ) {
     614                $processing = WP_REST_Server::resolve_data_processing( $request->get_attributes() );
     615
     616                if ( ! isset( $request['_fields'] ) || ! $processing['fields'] ) {
    613617                        return $fields;
    614618                }
  • trunk/tests/phpunit/tests/blocks/editor.php

    r64385 r64689  
    901901<script src="{$baseurl}/wp-includes/js/dist/api-fetch.min.js?ver=test" id="wp-api-fetch-js"></script>
    902902<script id="wp-api-fetch-js-after">
    903 wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( {"/test/v0/test-62797":{"body":["Unclosed comment and a script open tag \\u003C!--\\u003Cscript\\u003E"],"headers":{"Allow":"GET"}}} ) );
     903wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( {"/test/v0/test-62797":{"body":"Unclosed comment and a script open tag \\u003C!--\\u003Cscript\\u003E","headers":{"Allow":"GET"}}} ) );
    904904//# sourceURL=wp-api-fetch-js-after
    905905</script>
  • trunk/tests/phpunit/tests/rest-api/rest-controller.php

    r56548 r64689  
    442442        }
    443443
     444        /**
     445         * Data provider for test_get_fields_for_response_respects_data_processing().
     446         *
     447         * @return array<string, array{0: mixed, 1: bool}>
     448         */
     449        public static function data_get_fields_for_response_respects_data_processing(): array {
     450                return array(
     451                        'false'        => array( false, false ),
     452                        'fields false' => array( array( 'fields' => false ), false ),
     453                        'links false'  => array( array( 'links' => false ), true ),
     454                        'true'         => array( true, true ),
     455                );
     456        }
     457
     458        /**
     459         * @ticket 66372
     460         * @dataProvider data_get_fields_for_response_respects_data_processing
     461         *
     462         * @param mixed $data_processing The `data_processing` setting of the matched handler.
     463         * @param bool  $filtered            Whether the `_fields` parameter should filter the fields.
     464         */
     465        public function test_get_fields_for_response_respects_data_processing( $data_processing, bool $filtered ): void {
     466                $controller = new WP_REST_Test_Controller();
     467                $request    = new WP_REST_Request( 'GET', '/wp/v2/testroute' );
     468                $request->set_attributes( array( 'data_processing' => $data_processing ) );
     469                $request->set_param( '_fields', 'somestring' );
     470
     471                $fields = $controller->get_fields_for_response( $request );
     472
     473                if ( $filtered ) {
     474                        $this->assertSame( array( 'somestring' ), $fields, 'The _fields parameter should filter the fields.' );
     475                } else {
     476                        $this->assertContains( 'someinteger', $fields, 'The _fields parameter should be ignored when the fields processing is off.' );
     477                        $this->assertContains( '_links', $fields, 'The _fields parameter should be ignored when the fields processing is off.' );
     478                }
     479        }
     480
    444481        public function test_get_fields_for_response_filters_by_context() {
    445482                $controller = new WP_REST_Test_Controller();
  • trunk/tests/phpunit/tests/rest-api/rest-server.php

    r64644 r64689  
    16781678
    16791679                $server->serve_request( '/test/status' );
     1680
     1681                $_SERVER['REQUEST_METHOD'] = 'GET';
     1682                unset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
    16801683
    16811684                $this->assertSame( $status, $server->status, 'The response status is not the one returned by the callback.' );
    … …  
    29012904                $this->assertSame( array( 'GET', 'PUT' ), $link['targetHints']['allow'] );
    29022905        }
     2906
     2907        /**
     2908         * Registers a route for the data processing tests, and a route that its link embeds.
     2909         *
     2910         * @param mixed                $data            The response data.
     2911         * @param bool                 $with_link       Whether the response has an embeddable link.
     2912         * @param array<string, mixed> $handler_options Options for the route handler.
     2913         * @param array<string, mixed> $route_options   Options for the route.
     2914         */
     2915        private function register_data_processing_route( $data, bool $with_link, array $handler_options = array(), array $route_options = array() ): void {
     2916                rest_get_server()->register_route(
     2917                        'test-ns',
     2918                        '/test-ns/data-processing',
     2919                        array_merge(
     2920                                array(
     2921                                        array_merge(
     2922                                                array(
     2923                                                        'methods'             => 'GET',
     2924                                                        'callback'            => static function () use ( $data, $with_link ) {
     2925                                                                $response = new WP_REST_Response( $data );
     2926                                                                if ( $with_link ) {
     2927                                                                        $response->add_link( 'alternate', rest_url( '/test-ns/embedded' ), array( 'embeddable' => true ) );
     2928                                                                }
     2929                                                                return $response;
     2930                                                        },
     2931                                                        'permission_callback' => '__return_true',
     2932                                                ),
     2933                                                $handler_options
     2934                                        ),
     2935                                ),
     2936                                $route_options
     2937                        )
     2938                );
     2939
     2940                rest_get_server()->register_route(
     2941                        'test-ns',
     2942                        '/test-ns/embedded',
     2943                        array(
     2944                                'methods'             => 'GET',
     2945                                'callback'            => static function () {
     2946                                        return array( 'hello' => 'world' );
     2947                                },
     2948                                'permission_callback' => '__return_true',
     2949                        )
     2950                );
     2951        }
     2952
     2953        /**
     2954         * Data provider for test_resolve_data_processing().
     2955         *
     2956         * @return array<string, array{0: array<string, mixed>, 1: array<string, mixed>|null, 2: array<string, bool>}>
     2957         */
     2958        public static function data_resolve_data_processing(): array {
     2959                $all_on  = array(
     2960                        'fields' => true,
     2961                        'embed'  => true,
     2962                        'links'  => true,
     2963                );
     2964                $all_off = array(
     2965                        'fields' => false,
     2966                        'embed'  => false,
     2967                        'links'  => false,
     2968                );
     2969
     2970                return array(
     2971                        'no setting'                          => array( array(), null, $all_on ),
     2972                        'empty route options'                 => array( array(), array(), $all_on ),
     2973                        'handler false'                       => array( array( 'data_processing' => false ), null, $all_off ),
     2974                        'route false'                         => array( array(), array( 'data_processing' => false ), $all_off ),
     2975                        'route false and handler true'        => array( array( 'data_processing' => true ), array( 'data_processing' => false ), $all_on ),
     2976                        'route false and handler fields'      => array(
     2977                                array( 'data_processing' => array( 'fields' => true ) ),
     2978                                array( 'data_processing' => false ),
     2979                                array(
     2980                                        'fields' => true,
     2981                                        'embed'  => false,
     2982                                        'links'  => false,
     2983                                ),
     2984                        ),
     2985                        'handler fields false'                => array(
     2986                                array( 'data_processing' => array( 'fields' => false ) ),
     2987                                null,
     2988                                array(
     2989                                        'fields' => false,
     2990                                        'embed'  => true,
     2991                                        'links'  => true,
     2992                                ),
     2993                        ),
     2994                        'handler links false turns off embed' => array(
     2995                                array( 'data_processing' => array( 'links' => false ) ),
     2996                                null,
     2997                                array(
     2998                                        'fields' => true,
     2999                                        'embed'  => false,
     3000                                        'links'  => false,
     3001                                ),
     3002                        ),
     3003                        'handler embed true needs the links'  => array(
     3004                                array( 'data_processing' => array( 'embed' => true ) ),
     3005                                array( 'data_processing' => array( 'links' => false ) ),
     3006                                array(
     3007                                        'fields' => true,
     3008                                        'embed'  => false,
     3009                                        'links'  => false,
     3010                                ),
     3011                        ),
     3012                        'route fields false and handler true' => array(
     3013                                array( 'data_processing' => array( 'fields' => true ) ),
     3014                                array( 'data_processing' => array( 'fields' => false ) ),
     3015                                $all_on,
     3016                        ),
     3017                        'unknown keys are ignored'            => array( array( 'data_processing' => array( 'envelope' => false ) ), null, $all_on ),
     3018                        'other types are ignored'             => array( array( 'data_processing' => 'no' ), array( 'data_processing' => 0 ), $all_on ),
     3019                );
     3020        }
     3021
     3022        /**
     3023         * @ticket 66372
     3024         * @dataProvider data_resolve_data_processing
     3025         *
     3026         * @param array<string, mixed>      $handler       The route handler.
     3027         * @param array<string, mixed>|null $route_options The route options.
     3028         * @param array<string, bool>       $expected      The expected settings.
     3029         */
     3030        public function test_resolve_data_processing( array $handler, ?array $route_options, array $expected ): void {
     3031                $this->assertSame( $expected, WP_REST_Server::resolve_data_processing( $handler, $route_options ), 'The settings should be resolved feature by feature.' );
     3032        }
     3033
     3034        /**
     3035         * Data provider for test_data_processing_false_serves_the_data_as_returned().
     3036         *
     3037         * @return array<string, array{0: mixed, 1: string}>
     3038         */
     3039        public static function data_data_processing_false_serves_the_data_as_returned(): array {
     3040                return array(
     3041                        'associative array' => array(
     3042                                array(
     3043                                        'a' => 1,
     3044                                        'b' => 2,
     3045                                ),
     3046                                '{"a":1,"b":2}',
     3047                        ),
     3048                        'list'              => array( array( array( 'b' => 2 ), 'c' ), '[{"b":2},"c"]' ),
     3049                        'empty array'       => array( array(), '[]' ),
     3050                        'empty object'      => array( new stdClass(), '{}' ),
     3051                        'object'            => array( (object) array( 'b' => 2 ), '{"b":2}' ),
     3052                        'string'            => array( 'value', '"value"' ),
     3053                        'integer'           => array( 5, '5' ),
     3054                );
     3055        }
     3056
     3057        /**
     3058         * @ticket 66372
     3059         * @dataProvider data_data_processing_false_serves_the_data_as_returned
     3060         *
     3061         * @param mixed  $data     The response data.
     3062         * @param string $expected The expected response body.
     3063         */
     3064        public function test_data_processing_false_serves_the_data_as_returned( $data, string $expected ): void {
     3065                $this->register_data_processing_route( $data, true, array( 'data_processing' => false ) );
     3066
     3067                $_GET['_fields'] = 'a';
     3068                $_GET['_embed']  = '1';
     3069                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3070
     3071                $this->assertSame( $expected, rest_get_server()->sent_body, 'The data should be served as returned, without links or filtering.' );
     3072        }
     3073
     3074        /**
     3075         * @ticket 66372
     3076         */
     3077        public function test_fields_are_filtered_and_links_embedded_by_default(): void {
     3078                $this->register_data_processing_route(
     3079                        array(
     3080                                'a' => 1,
     3081                                'b' => 2,
     3082                        ),
     3083                        true
     3084                );
     3085
     3086                $_GET['_fields'] = 'a';
     3087                $_GET['_embed']  = '1';
     3088                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3089                $data = json_decode( rest_get_server()->sent_body, true );
     3090
     3091                $this->assertSame( array( 'a', '_links', '_embedded' ), array_keys( $data ), 'The _fields parameter should filter the data, and the links should be added.' );
     3092                $this->assertSame( array( array( 'hello' => 'world' ) ), $data['_embedded']['alternate'], 'The link should be embedded.' );
     3093        }
     3094
     3095        /**
     3096         * @ticket 66372
     3097         */
     3098        public function test_data_processing_fields_false_leaves_the_data_unfiltered(): void {
     3099                $this->register_data_processing_route(
     3100                        array(
     3101                                'a' => 1,
     3102                                'b' => 2,
     3103                        ),
     3104                        true,
     3105                        array( 'data_processing' => array( 'fields' => false ) )
     3106                );
     3107
     3108                $_GET['_fields'] = 'a';
     3109                $_GET['_embed']  = '1';
     3110                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3111                $data = json_decode( rest_get_server()->sent_body, true );
     3112
     3113                $this->assertSame( array( 'a', 'b', '_links', '_embedded' ), array_keys( $data ), 'Only the _fields parameter should be ignored.' );
     3114        }
     3115
     3116        /**
     3117         * Data provider for test_data_processing_links_false_leaves_out_the_links().
     3118         *
     3119         * @return array<string, array{0: mixed, 1: string}>
     3120         */
     3121        public static function data_data_processing_links_false_leaves_out_the_links(): array {
     3122                return array(
     3123                        'associative array' => array( array( 'a' => 1 ), '{"a":1}' ),
     3124                        'list'              => array( array( 'a', 'b' ), '["a","b"]' ),
     3125                        'empty object'      => array( new stdClass(), '{}' ),
     3126                        'string'            => array( 'value', '"value"' ),
     3127                );
     3128        }
     3129
     3130        /**
     3131         * @ticket 66372
     3132         * @dataProvider data_data_processing_links_false_leaves_out_the_links
     3133         *
     3134         * @param mixed  $data     The response data.
     3135         * @param string $expected The expected response body.
     3136         */
     3137        public function test_data_processing_links_false_leaves_out_the_links( $data, string $expected ): void {
     3138                $this->register_data_processing_route( $data, true, array( 'data_processing' => array( 'links' => false ) ) );
     3139
     3140                $_GET['_embed'] = '1';
     3141                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3142
     3143                $this->assertSame( $expected, rest_get_server()->sent_body, 'The response links should not be added or embedded.' );
     3144        }
     3145
     3146        /**
     3147         * @ticket 66372
     3148         */
     3149        public function test_data_processing_embed_false_keeps_the_links_without_embedding(): void {
     3150                $this->register_data_processing_route( array( 'a' => 1 ), true, array( 'data_processing' => array( 'embed' => false ) ) );
     3151
     3152                $_GET['_embed'] = '1';
     3153                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3154                $data = json_decode( rest_get_server()->sent_body, true );
     3155
     3156                $this->assertArrayHasKey( '_links', $data, 'The response links should be added.' );
     3157                $this->assertArrayNotHasKey( '_embedded', $data, 'The links should not be embedded.' );
     3158        }
     3159
     3160        /**
     3161         * @ticket 66372
     3162         */
     3163        public function test_data_processing_false_keeps_the_own_links_of_the_data(): void {
     3164                $own_links = array( 'custom' => array( array( 'href' => 'https://example.org/' ) ) );
     3165                $this->register_data_processing_route(
     3166                        array(
     3167                                'a'      => 1,
     3168                                '_links' => $own_links,
     3169                        ),
     3170                        true,
     3171                        array( 'data_processing' => false )
     3172                );
     3173
     3174                $_GET['_embed'] = '1';
     3175                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3176                $data = json_decode( rest_get_server()->sent_body, true );
     3177
     3178                $this->assertSame( $own_links, $data['_links'], 'The _links key of the data should be served as returned.' );
     3179                $this->assertArrayNotHasKey( '_embedded', $data, 'The links of the data should not be embedded.' );
     3180        }
     3181
     3182        /**
     3183         * @ticket 66372
     3184         */
     3185        public function test_data_processing_applies_to_enveloped_responses(): void {
     3186                $this->register_data_processing_route(
     3187                        array(
     3188                                'a' => 1,
     3189                                'b' => 2,
     3190                        ),
     3191                        true,
     3192                        array( 'data_processing' => false )
     3193                );
     3194
     3195                $_GET['_fields']   = 'a';
     3196                $_GET['_embed']    = '1';
     3197                $_GET['_envelope'] = '1';
     3198                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3199                $data = json_decode( rest_get_server()->sent_body, true );
     3200
     3201                $this->assertSame( 200, $data['status'], 'The enveloped response should succeed.' );
     3202                $this->assertSame(
     3203                        array(
     3204                                'a' => 1,
     3205                                'b' => 2,
     3206                        ),
     3207                        $data['body'],
     3208                        'The envelope should wrap the data as returned.'
     3209                );
     3210        }
     3211
     3212        /**
     3213         * Data provider for test_handler_setting_takes_precedence_over_route_setting().
     3214         *
     3215         * @return array<string, array{0: array<string, mixed>, 1: array<string, mixed>, 2: string[]}>
     3216         */
     3217        public static function data_handler_setting_takes_precedence_over_route_setting(): array {
     3218                return array(
     3219                        'route false'                              => array( array(), array( 'data_processing' => false ), array( 'a', 'b' ) ),
     3220                        'route false and handler true'             => array( array( 'data_processing' => true ), array( 'data_processing' => false ), array( 'a', '_links' ) ),
     3221                        'route false and handler fields'           => array( array( 'data_processing' => array( 'fields' => true ) ), array( 'data_processing' => false ), array( 'a' ) ),
     3222                        'route fields false and handler links off' => array( array( 'data_processing' => array( 'links' => false ) ), array( 'data_processing' => array( 'fields' => false ) ), array( 'a', 'b' ) ),
     3223                        'route links false and handler fields off' => array( array( 'data_processing' => array( 'fields' => false ) ), array( 'data_processing' => array( 'links' => false ) ), array( 'a', 'b' ) ),
     3224                );
     3225        }
     3226
     3227        /**
     3228         * @ticket 66372
     3229         * @dataProvider data_handler_setting_takes_precedence_over_route_setting
     3230         *
     3231         * @param array<string, mixed> $handler_options Options for the route handler.
     3232         * @param array<string, mixed> $route_options   Options for the route.
     3233         * @param string[]             $expected_keys   The expected keys of the response body.
     3234         */
     3235        public function test_handler_setting_takes_precedence_over_route_setting( array $handler_options, array $route_options, array $expected_keys ): void {
     3236                $this->register_data_processing_route(
     3237                        array(
     3238                                'a' => 1,
     3239                                'b' => 2,
     3240                        ),
     3241                        true,
     3242                        $handler_options,
     3243                        $route_options
     3244                );
     3245
     3246                $_GET['_fields'] = 'a';
     3247                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3248                $data = json_decode( rest_get_server()->sent_body, true );
     3249
     3250                $this->assertSame( $expected_keys, array_keys( $data ), 'The handler setting should take precedence over the route setting, feature by feature.' );
     3251        }
     3252
     3253        /**
     3254         * @ticket 66372
     3255         */
     3256        public function test_data_processing_is_set_on_the_request_attributes_the_matched_handler_and_the_response(): void {
     3257                $this->register_data_processing_route( array( 'a' => 1 ), false, array( 'data_processing' => array( 'links' => false ) ) );
     3258
     3259                $request  = new WP_REST_Request( 'GET', '/test-ns/data-processing' );
     3260                $response = rest_get_server()->dispatch( $request );
     3261                $expected = array(
     3262                        'fields' => true,
     3263                        'embed'  => false,
     3264                        'links'  => false,
     3265                );
     3266
     3267                $this->assertSame( $expected, $request->get_attributes()['data_processing'], 'The request attributes should carry the resolved settings.' );
     3268                $this->assertSame( $expected, $response->get_matched_handler()['data_processing'], 'The matched handler should carry the resolved settings.' );
     3269                $this->assertSame( $expected, $response->get_data_processing(), 'The response should carry the resolved settings.' );
     3270        }
     3271
     3272        /**
     3273         * @ticket 66372
     3274         */
     3275        public function test_routes_without_the_option_have_every_feature_on(): void {
     3276                $this->register_data_processing_route( array( 'a' => 1 ), false );
     3277
     3278                $request  = new WP_REST_Request( 'GET', '/test-ns/data-processing' );
     3279                $response = rest_get_server()->dispatch( $request );
     3280                $expected = array(
     3281                        'fields' => true,
     3282                        'embed'  => true,
     3283                        'links'  => true,
     3284                );
     3285
     3286                $this->assertSame( $expected, $request->get_attributes()['data_processing'], 'The request attributes should have every feature on.' );
     3287                $this->assertSame( $expected, $response->get_matched_handler()['data_processing'], 'The matched handler should have every feature on.' );
     3288                $this->assertSame( $expected, $response->get_data_processing(), 'The response should have every feature on.' );
     3289        }
     3290
     3291        /**
     3292         * Data provider for test_data_processing_applies_to_hijacked_responses().
     3293         *
     3294         * @return array<string, array{0: mixed, 1: string}>
     3295         */
     3296        public static function data_data_processing_applies_to_hijacked_responses(): array {
     3297                return array(
     3298                        'associative array' => array(
     3299                                array(
     3300                                        'a' => 1,
     3301                                        'b' => 2,
     3302                                ),
     3303                                '{"a":1,"b":2}',
     3304                        ),
     3305                        'string'            => array( 'value', '"value"' ),
     3306                );
     3307        }
     3308
     3309        /**
     3310         * @ticket 66372
     3311         * @dataProvider data_data_processing_applies_to_hijacked_responses
     3312         *
     3313         * @param mixed  $data     The data returned by the `rest_pre_dispatch` filter.
     3314         * @param string $expected The expected response body.
     3315         */
     3316        public function test_data_processing_applies_to_hijacked_responses( $data, string $expected ): void {
     3317                $this->register_data_processing_route( array( 'unexpected' => true ), true, array( 'data_processing' => false ) );
     3318
     3319                add_filter(
     3320                        'rest_pre_dispatch',
     3321                        static function () use ( $data ) {
     3322                                return $data;
     3323                        }
     3324                );
     3325
     3326                $_GET['_fields'] = 'a';
     3327                $_GET['_embed']  = '1';
     3328                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3329
     3330                $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.' );
     3331        }
     3332
     3333        /**
     3334         * @ticket 66372
     3335         */
     3336        public function test_hijacked_responses_are_processed_by_default(): void {
     3337                $this->register_data_processing_route( array( 'unexpected' => true ), true );
     3338
     3339                add_filter(
     3340                        'rest_pre_dispatch',
     3341                        static function () {
     3342                                return array(
     3343                                        'a' => 1,
     3344                                        'b' => 2,
     3345                                );
     3346                        }
     3347                );
     3348
     3349                $_GET['_fields'] = 'a';
     3350                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3351
     3352                $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.' );
     3353        }
     3354
     3355        /**
     3356         * @ticket 66372
     3357         */
     3358        public function test_hijacked_responses_carry_the_settings_of_their_route(): void {
     3359                $this->register_data_processing_route( array( 'unexpected' => true ), true, array( 'data_processing' => array( 'links' => false ) ) );
     3360
     3361                add_filter(
     3362                        'rest_pre_dispatch',
     3363                        static function () {
     3364                                return array( 'a' => 1 );
     3365                        }
     3366                );
     3367
     3368                $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/test-ns/data-processing' ) );
     3369
     3370                $this->assertSame(
     3371                        array(
     3372                                'fields' => true,
     3373                                'embed'  => false,
     3374                                'links'  => false,
     3375                        ),
     3376                        $response->get_data_processing(),
     3377                        'A response returned by rest_pre_dispatch should carry the settings of the handler the request would match.'
     3378                );
     3379                $this->assertSame( '', $response->get_matched_route(), 'A response returned by rest_pre_dispatch should still have no matched route.' );
     3380                $this->assertNull( $response->get_matched_handler(), 'A response returned by rest_pre_dispatch should still have no matched handler.' );
     3381        }
     3382
     3383        /**
     3384         * @ticket 66372
     3385         */
     3386        public function test_hijacked_responses_for_unknown_routes_have_every_feature_on(): void {
     3387                add_filter(
     3388                        'rest_pre_dispatch',
     3389                        static function () {
     3390                                return array( 'a' => 1 );
     3391                        }
     3392                );
     3393
     3394                $response = rest_get_server()->dispatch( new WP_REST_Request( 'GET', '/test-ns/no-such-route' ) );
     3395
     3396                $this->assertSame(
     3397                        array(
     3398                                'fields' => true,
     3399                                'embed'  => true,
     3400                                'links'  => true,
     3401                        ),
     3402                        $response->get_data_processing(),
     3403                        'A response returned by rest_pre_dispatch for an unknown route should have every feature on.'
     3404                );
     3405        }
     3406
     3407        /**
     3408         * @ticket 66372
     3409         */
     3410        public function test_response_setting_applies_when_set_before_the_fields_filter(): void {
     3411                $this->register_data_processing_route(
     3412                        array(
     3413                                'a' => 1,
     3414                                'b' => 2,
     3415                        ),
     3416                        true
     3417                );
     3418
     3419                // Before rest_filter_response_fields(), which runs on the same filter at priority 10.
     3420                add_filter(
     3421                        'rest_post_dispatch',
     3422                        static function ( $response ) {
     3423                                $response->set_data_processing( false );
     3424                                return $response;
     3425                        },
     3426                        9
     3427                );
     3428
     3429                $_GET['_fields'] = 'a';
     3430                $_GET['_embed']  = '1';
     3431                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3432
     3433                $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.' );
     3434        }
     3435
     3436        /**
     3437         * Data provider for test_response_set_data_processing().
     3438         *
     3439         * @return array<string, array{0: mixed, 1: array<string, bool>}>
     3440         */
     3441        public static function data_response_set_data_processing(): array {
     3442                $all_on = array(
     3443                        'fields' => true,
     3444                        'embed'  => true,
     3445                        'links'  => true,
     3446                );
     3447
     3448                return array(
     3449                        'true'        => array( true, $all_on ),
     3450                        'false'       => array(
     3451                                false,
     3452                                array(
     3453                                        'fields' => false,
     3454                                        'embed'  => false,
     3455                                        'links'  => false,
     3456                                ),
     3457                        ),
     3458                        'empty array' => array( array(), $all_on ),
     3459                        'fields off'  => array(
     3460                                array( 'fields' => false ),
     3461                                array(
     3462                                        'fields' => false,
     3463                                        'embed'  => true,
     3464                                        'links'  => true,
     3465                                ),
     3466                        ),
     3467                        'links off'   => array(
     3468                                array( 'links' => false ),
     3469                                array(
     3470                                        'fields' => true,
     3471                                        'embed'  => false,
     3472                                        'links'  => false,
     3473                                ),
     3474                        ),
     3475                );
     3476        }
     3477
     3478        /**
     3479         * @ticket 66372
     3480         * @dataProvider data_response_set_data_processing
     3481         *
     3482         * @param mixed               $processing The setting, in the form of the `data_processing` option.
     3483         * @param array<string, bool> $expected   The expected settings of the response.
     3484         */
     3485        public function test_response_set_data_processing( $processing, array $expected ): void {
     3486                $response = new WP_REST_Response();
     3487
     3488                $this->assertSame(
     3489                        array(
     3490                                'fields' => true,
     3491                                'embed'  => true,
     3492                                'links'  => true,
     3493                        ),
     3494                        $response->get_data_processing(),
     3495                        'A new response should have every feature on.'
     3496                );
     3497
     3498                $response->set_data_processing( $processing );
     3499
     3500                $this->assertSame( $expected, $response->get_data_processing(), 'The setting should be resolved like the route option.' );
     3501        }
     3502
     3503        /**
     3504         * Data provider for test_data_processing_applies_to_preloaded_requests().
     3505         *
     3506         * @return array<string, array{0: mixed, 1: string}>
     3507         */
     3508        public static function data_data_processing_applies_to_preloaded_requests(): array {
     3509                return array(
     3510                        'associative array' => array(
     3511                                array(
     3512                                        'a' => 1,
     3513                                        'b' => 2,
     3514                                ),
     3515                                '{"a":1,"b":2}',
     3516                        ),
     3517                        'empty object'      => array( new stdClass(), '{}' ),
     3518                        'string'            => array( 'value', '"value"' ),
     3519                );
     3520        }
     3521
     3522        /**
     3523         * @ticket 66372
     3524         * @dataProvider data_data_processing_applies_to_preloaded_requests
     3525         *
     3526         * @param mixed  $data     The response data.
     3527         * @param string $expected The expected JSON encoding of the preloaded body.
     3528         */
     3529        public function test_data_processing_applies_to_preloaded_requests( $data, string $expected ): void {
     3530                $this->register_data_processing_route( $data, true, array( 'data_processing' => false ) );
     3531
     3532                $preload = rest_preload_api_request( array(), '/test-ns/data-processing?_fields=a&_embed=1' );
     3533
     3534                $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.' );
     3535        }
     3536
     3537        /**
     3538         * @ticket 66372
     3539         */
     3540        public function test_data_processing_applies_to_batch_requests(): void {
     3541                $this->register_data_processing_route(
     3542                        array(
     3543                                'a' => 1,
     3544                                'b' => 2,
     3545                        ),
     3546                        true,
     3547                        array(
     3548                                'methods'         => 'POST',
     3549                                'allow_batch'     => array( 'v1' => true ),
     3550                                'data_processing' => false,
     3551                        )
     3552                );
     3553
     3554                $request = new WP_REST_Request( 'POST', '/batch/v1' );
     3555                $request->set_body_params(
     3556                        array(
     3557                                'requests' => array(
     3558                                        array( 'path' => '/test-ns/data-processing?_fields=a' ),
     3559                                ),
     3560                        )
     3561                );
     3562
     3563                $response = rest_do_request( $request );
     3564
     3565                $this->assertSame( 207, $response->get_status(), 'The batch request should succeed.' );
     3566                $this->assertSame(
     3567                        array(
     3568                                'a' => 1,
     3569                                'b' => 2,
     3570                        ),
     3571                        $response->get_data()['responses'][0]['body'],
     3572                        'The batched response body should be the data as returned.'
     3573                );
     3574        }
     3575
     3576        /**
     3577         * @ticket 66372
     3578         */
     3579        public function test_data_processing_is_listed_in_the_index(): void {
     3580                rest_get_server()->register_route(
     3581                        'test-ns',
     3582                        '/test-ns/data-processing',
     3583                        array(
     3584                                array(
     3585                                        'methods'             => 'GET',
     3586                                        'callback'            => '__return_null',
     3587                                        'permission_callback' => '__return_true',
     3588                                ),
     3589                                array(
     3590                                        'methods'             => 'POST',
     3591                                        'callback'            => '__return_null',
     3592                                        'permission_callback' => '__return_true',
     3593                                        'data_processing'     => array( 'fields' => true ),
     3594                                ),
     3595                                'data_processing' => array( 'fields' => false ),
     3596                        )
     3597                );
     3598
     3599                $request  = new WP_REST_Request( 'OPTIONS', '/test-ns/data-processing' );
     3600                $response = rest_get_server()->dispatch( $request );
     3601                $data     = $response->get_data();
     3602
     3603                $this->assertSame(
     3604                        array(
     3605                                'fields' => false,
     3606                                'embed'  => true,
     3607                                'links'  => true,
     3608                        ),
     3609                        $data['endpoints'][0]['data_processing'],
     3610                        'An endpoint with a feature turned off should list the settings.'
     3611                );
     3612                $this->assertArrayNotHasKey( 'data_processing', $data['endpoints'][1], 'An endpoint with every feature on should not list the settings.' );
     3613        }
     3614
     3615        /**
     3616         * @ticket 66372
     3617         */
     3618        public function test_options_requests_are_processed_by_default(): void {
     3619                $this->register_data_processing_route( array( 'a' => 1 ), true, array( 'data_processing' => false ) );
     3620
     3621                $_SERVER['REQUEST_METHOD'] = 'OPTIONS';
     3622                $_GET['_fields']           = 'methods';
     3623                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3624                $_SERVER['REQUEST_METHOD'] = 'GET';
     3625                $data                      = json_decode( rest_get_server()->sent_body, true );
     3626
     3627                $this->assertSame( array( 'methods' ), array_keys( $data ), 'The _fields parameter should filter the route description of a route with the option.' );
     3628        }
     3629
     3630        /**
     3631         * @ticket 66372
     3632         */
     3633        public function test_array_access_data_gets_links_and_embedded_resources(): void {
     3634                $this->register_data_processing_route( new ArrayObject( array( 'a' => 1 ) ), true );
     3635
     3636                $_GET['_embed'] = '1';
     3637                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3638                $data = json_decode( rest_get_server()->sent_body, true );
     3639
     3640                $this->assertSame( array( 'a', '_links', '_embedded' ), array_keys( $data ), 'An object that acts as an array should get the links and the embedded resources.' );
     3641        }
     3642
     3643        /**
     3644         * Data provider for test_non_array_data_is_served_as_returned_by_default().
     3645         *
     3646         * @return array<string, array{0: mixed, 1: bool, 2: string}>
     3647         */
     3648        public static function data_non_array_data_is_served_as_returned_by_default(): array {
     3649                return array(
     3650                        'string'          => array( 'value', true, '"value"' ),
     3651                        'integer'         => array( 5, true, '5' ),
     3652                        'object'          => array( (object) array( 'b' => 2 ), true, '{"b":2}' ),
     3653                        'empty object'    => array( new stdClass(), true, '{}' ),
     3654                        'list of scalars' => array( array( 'a', 'b' ), false, '["a","b"]' ),
     3655                        'mixed list'      => array( array( array( 'b' => 2 ), 'c' ), false, '[[],"c"]' ),
     3656                        'list of objects' => array( array( (object) array( 'b' => 2 ) ), false, '[{"b":2}]' ),
     3657                );
     3658        }
     3659
     3660        /**
     3661         * @ticket 66372
     3662         * @dataProvider data_non_array_data_is_served_as_returned_by_default
     3663         *
     3664         * @param mixed  $data      The response data.
     3665         * @param bool   $with_link Whether the response has an embeddable link.
     3666         * @param string $expected  The expected response body.
     3667         */
     3668        public function test_non_array_data_is_served_as_returned_by_default( $data, bool $with_link, string $expected ): void {
     3669                $this->register_data_processing_route( $data, $with_link );
     3670
     3671                $_GET['_fields'] = 'a';
     3672                $_GET['_embed']  = '1';
     3673                rest_get_server()->serve_request( '/test-ns/data-processing' );
     3674
     3675                $this->assertSame( $expected, rest_get_server()->sent_body, 'Data without fields, or items without fields, should be served as returned.' );
     3676        }
    29033677}
Note: See TracChangeset for help on using the changeset viewer.