Changeset 64689
- Timestamp:
- 10/23/2026 03:46:22 PM (21 hours ago)
- Location:
- trunk
- Files:
-
- 7 edited
-
src/wp-includes/rest-api.php (modified) (4 diffs)
-
src/wp-includes/rest-api/class-wp-rest-response.php (modified) (3 diffs)
-
src/wp-includes/rest-api/class-wp-rest-server.php (modified) (12 diffs)
-
src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php (modified) (2 diffs)
-
tests/phpunit/tests/blocks/editor.php (modified) (1 diff)
-
tests/phpunit/tests/rest-api/rest-controller.php (modified) (1 diff)
-
tests/phpunit/tests/rest-api/rest-server.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/rest-api.php
r64602 r64689 951 951 * 952 952 * @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. 953 955 * 954 956 * @param WP_REST_Response $response Current response being served. … … 962 964 } 963 965 966 $processing = $response->get_data_processing(); 967 if ( ! $processing['fields'] ) { 968 return $response; 969 } 970 964 971 $data = $response->get_data(); 972 973 // Only arrays have fields to filter. 974 if ( ! is_array( $data ) ) { 975 return $response; 976 } 965 977 966 978 $fields = wp_parse_list( $request['_fields'] ); … … 994 1006 $new_data = array(); 995 1007 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; 997 1009 } 998 1010 } else { … … 3049 3061 $response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $server, $request ); 3050 3062 $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 ); 3052 3064 3053 3065 if ( 'OPTIONS' === $method ) { -
trunk/src/wp-includes/rest-api/class-wp-rest-response.php
r64527 r64689 16 16 * 17 17 * @phpstan-import-type Route_Handler from WP_REST_Server 18 * @phpstan-import-type Data_Processing from WP_REST_Server 18 19 * 19 20 * @phpstan-type Target_Hints array{ … … 71 72 72 73 /** 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 /** 73 87 * Adds a link to the response. 74 88 * … … 248 262 public function set_matched_handler( $handler ) { 249 263 $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 ) ); 250 308 } 251 309 -
trunk/src/wp-includes/rest-api/class-wp-rest-server.php
r64588 r64689 41 41 * show_in_index: bool, 42 42 * allow_batch?: array{v1?: bool}|false, 43 * data_processing?: bool|array{fields?: bool, embed?: bool, links?: bool}, 43 44 * ... 44 45 * } 46 * @phpstan-type Data_Processing array{fields: bool, embed: bool, links: bool} 45 47 */ 46 48 #[AllowDynamicProperties] … … 623 625 * @since 4.4.0 624 626 * @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() 625 633 * 626 634 * @param WP_REST_Response $response Response object. 627 635 * @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`. 634 639 */ 635 640 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(); 638 650 639 651 if ( ! empty( $links ) ) { … … 642 654 } 643 655 644 if ( $embed ) {656 if ( $embed && $processing['embed'] ) { 645 657 $this->embed_cache = array(); 646 658 // Determine if this is a numeric array. 647 659 if ( wp_is_numeric_array( $data ) ) { 648 660 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; 650 662 } 651 663 } else { … … 1133 1145 1134 1146 /** 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 /** 1135 1219 * Matches the request to a callback and call it. 1136 1220 * … … 1166 1250 if ( is_wp_error( $result ) ) { 1167 1251 $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 ); 1168 1258 } 1169 1259 … … 1233 1323 */ 1234 1324 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 ) { 1235 1372 $method = $request->get_method(); 1236 1373 $path = $request->get_route(); … … 1266 1403 1267 1404 foreach ( $handlers as $handler ) { 1268 $callback = $handler['callback'];1269 1270 1405 // Fallback to GET method if no HEAD method is registered. 1271 1406 $checked_method = $method; … … 1277 1412 } 1278 1413 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; 1305 1422 } 1306 1423 … … 1415 1532 $response->set_matched_route( $route ); 1416 1533 $response->set_matched_handler( $handler ); 1534 $response->set_data_processing( $handler['data_processing'] ?? true ); 1417 1535 1418 1536 return $response; … … 1706 1824 ); 1707 1825 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; 1712 1831 1713 1832 if ( isset( $options['namespace'] ) ) { … … 1741 1860 if ( $callback_batch ) { 1742 1861 $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; 1743 1868 } 1744 1869 … … 1956 2081 1957 2082 $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 ); 1958 2089 } 1959 2090 } -
trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php
r61572 r64689 566 566 * 567 567 * @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. 568 570 * 569 571 * @param WP_REST_Request $request Full details about the request. … … 610 612 $fields = array_unique( $fields ); 611 613 612 if ( ! isset( $request['_fields'] ) ) { 614 $processing = WP_REST_Server::resolve_data_processing( $request->get_attributes() ); 615 616 if ( ! isset( $request['_fields'] ) || ! $processing['fields'] ) { 613 617 return $fields; 614 618 } -
trunk/tests/phpunit/tests/blocks/editor.php
r64385 r64689 901 901 <script src="{$baseurl}/wp-includes/js/dist/api-fetch.min.js?ver=test" id="wp-api-fetch-js"></script> 902 902 <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"}}} ) );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"}}} ) ); 904 904 //# sourceURL=wp-api-fetch-js-after 905 905 </script> -
trunk/tests/phpunit/tests/rest-api/rest-controller.php
r56548 r64689 442 442 } 443 443 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 444 481 public function test_get_fields_for_response_filters_by_context() { 445 482 $controller = new WP_REST_Test_Controller(); -
trunk/tests/phpunit/tests/rest-api/rest-server.php
r64644 r64689 1678 1678 1679 1679 $server->serve_request( '/test/status' ); 1680 1681 $_SERVER['REQUEST_METHOD'] = 'GET'; 1682 unset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ); 1680 1683 1681 1684 $this->assertSame( $status, $server->status, 'The response status is not the one returned by the callback.' ); … … 2901 2904 $this->assertSame( array( 'GET', 'PUT' ), $link['targetHints']['allow'] ); 2902 2905 } 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 } 2903 3677 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)