Skip to content

Commit 7ec8c2f

Browse files
REST API: Serve response data that is not an array as returned.
`rest_filter_response_fields()` leaves the response unchanged when its data is not an array, and keeps list items that are not arrays. `WP_REST_Server::response_to_data()` returns scalar and object data as it is instead of adding links to it, and leaves list items that are not arrays alone when embedding. Routes that return such data no longer need the `response_processing` option to avoid type errors. See #66267.
1 parent 6684a86 commit 7ec8c2f

4 files changed

Lines changed: 74 additions & 14 deletions

File tree

‎src/wp-includes/rest-api.php‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -950,7 +950,8 @@ function _rest_array_intersect_key_recursive( $array1, $array2 ) {
950950
* Filters the REST API response to include only an allow-listed set of response object fields.
951951
*
952952
* @since 4.8.0
953-
* @since 7.2.0 The response is left unchanged when the route turns off the `fields` response processing.
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.
954955
*
955956
* @param WP_REST_Response $response Current response being served.
956957
* @param WP_REST_Server $server ResponseHandler instance (usually WP_REST_Server).
@@ -972,6 +973,11 @@ function rest_filter_response_fields( $response, $server, $request ) {
972973

973974
$data = $response->get_data();
974975

976+
// Only arrays have fields to filter.
977+
if ( ! is_array( $data ) ) {
978+
return $response;
979+
}
980+
975981
$fields = wp_parse_list( $request['_fields'] );
976982

977983
if ( 0 === count( $fields ) ) {
@@ -1002,7 +1008,7 @@ function rest_filter_response_fields( $response, $server, $request ) {
10021008
if ( wp_is_numeric_array( $data ) ) {
10031009
$new_data = array();
10041010
foreach ( $data as $item ) {
1005-
$new_data[] = _rest_array_intersect_key_recursive( $item, $fields_as_keyed );
1011+
$new_data[] = is_array( $item ) ? _rest_array_intersect_key_recursive( $item, $fields_as_keyed ) : $item;
10061012
}
10071013
} else {
10081014
$new_data = _rest_array_intersect_key_recursive( $data, $fields_as_keyed );

‎src/wp-includes/rest-api/class-wp-rest-server.php‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -578,8 +578,9 @@ public function serve_request( $path = null ) {
578578
* embedded data (if any) and before echoing the response data.
579579
*
580580
* @since 4.8.1
581+
* @since 7.2.0 The `$result` parameter can be scalar or object data.
581582
*
582-
* @param array $result Response data to send to the client.
583+
* @param mixed $result Response data to send to the client.
583584
* @param WP_REST_Server $server Server instance.
584585
* @param WP_REST_Request $request Request used to generate the response.
585586
*/
@@ -624,22 +625,27 @@ public function serve_request( $path = null ) {
624625
* @since 4.4.0
625626
* @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include.
626627
* @since 7.2.0 Added the `$request` parameter. The links are not added or embedded when the
627-
* route turns off the `links` or `embed` response processing.
628+
* route turns off the `links` or `embed` response processing, scalar and object
629+
* data is returned as it is unless the object implements `ArrayAccess`, and list
630+
* items that are not arrays are left as they are when embedding.
628631
*
629632
* @param WP_REST_Response $response Response object.
630633
* @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links.
631634
* @param WP_REST_Request|null $request Optional. The request that generated the response. It resolves the
632635
* response processing settings when the response has no matched handler.
633636
* Default null.
634-
* @return array {
635-
* Data with sub-requests embedded.
636-
*
637-
* @type array $_links Links.
638-
* @type array $_embedded Embedded objects.
639-
* }
637+
* @return mixed Data with sub-requests embedded: an array with the links under `_links` and the embedded
638+
* resources under `_embedded`, or scalar and object data as it is unless the object
639+
* implements `ArrayAccess`.
640640
*/
641641
public function response_to_data( $response, $embed, $request = null ) {
642-
$data = $response->get_data();
642+
$data = $response->get_data();
643+
644+
// Scalar and object data has no place for links or embedded resources, unless the object acts as an array.
645+
if ( null !== $data && ! is_array( $data ) && ! $data instanceof ArrayAccess ) {
646+
return $data;
647+
}
648+
643649
$processing = $this->get_response_processing( $request, $response );
644650
$links = $processing['links'] ? self::get_compact_response_links( $response ) : array();
645651

@@ -653,7 +659,7 @@ public function response_to_data( $response, $embed, $request = null ) {
653659
// Determine if this is a numeric array.
654660
if ( wp_is_numeric_array( $data ) ) {
655661
foreach ( $data as $key => $item ) {
656-
$data[ $key ] = $this->embed_links( $item, $embed );
662+
$data[ $key ] = is_array( $item ) || $item instanceof ArrayAccess ? $this->embed_links( $item, $embed ) : $item;
657663
}
658664
} else {
659665
$data = $this->embed_links( $data, $embed );
@@ -933,11 +939,12 @@ public function envelope_response( $response, $embed, $request = null ) {
933939
* Filters the enveloped form of a REST API response.
934940
*
935941
* @since 4.4.0
942+
* @since 7.2.0 The `body` can be scalar or object data.
936943
*
937944
* @param array $envelope {
938945
* Envelope data.
939946
*
940-
* @type array $body Response data.
947+
* @type mixed $body Response data.
941948
* @type int $status The 3-digit HTTP status code.
942949
* @type array $headers Map of header name to header value.
943950
* }

‎tests/phpunit/tests/blocks/editor.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -900,7 +900,7 @@ public function test_ensure_preload_data_script_tag_closes() {
900900
$expected = <<<HTML
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"}}} ) );
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"}}} ) );
904904
//# sourceURL=wp-api-fetch-js-after
905905
</script>
906906

‎tests/phpunit/tests/rest-api/rest-server.php‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3494,4 +3494,51 @@ public function test_options_requests_are_processed_by_default(): void {
34943494
$this->assertSame( array( 'methods' ), array_keys( $data ), 'The _fields parameter should filter the route description of a route with the option.' );
34953495
}
34963496

3497+
/**
3498+
* @ticket 66267
3499+
*/
3500+
public function test_array_access_data_gets_links_and_embedded_resources(): void {
3501+
$this->register_response_processing_route( new ArrayObject( array( 'a' => 1 ) ), true );
3502+
3503+
$_GET['_embed'] = '1';
3504+
rest_get_server()->serve_request( '/test-ns/response-processing' );
3505+
$data = json_decode( rest_get_server()->sent_body, true );
3506+
3507+
$this->assertSame( array( 'a', '_links', '_embedded' ), array_keys( $data ), 'An object that acts as an array should get the links and the embedded resources.' );
3508+
}
3509+
3510+
/**
3511+
* Data provider for test_non_array_data_is_served_as_returned_by_default().
3512+
*
3513+
* @return array<string, array{0: mixed, 1: bool, 2: string}>
3514+
*/
3515+
public static function data_non_array_data_is_served_as_returned_by_default(): array {
3516+
return array(
3517+
'string' => array( 'value', true, '"value"' ),
3518+
'integer' => array( 5, true, '5' ),
3519+
'object' => array( (object) array( 'b' => 2 ), true, '{"b":2}' ),
3520+
'empty object' => array( new stdClass(), true, '{}' ),
3521+
'list of scalars' => array( array( 'a', 'b' ), false, '["a","b"]' ),
3522+
'mixed list' => array( array( array( 'b' => 2 ), 'c' ), false, '[[],"c"]' ),
3523+
'list of objects' => array( array( (object) array( 'b' => 2 ) ), false, '[{"b":2}]' ),
3524+
);
3525+
}
3526+
3527+
/**
3528+
* @ticket 66267
3529+
* @dataProvider data_non_array_data_is_served_as_returned_by_default
3530+
*
3531+
* @param mixed $data The response data.
3532+
* @param bool $with_link Whether the response has an embeddable link.
3533+
* @param string $expected The expected response body.
3534+
*/
3535+
public function test_non_array_data_is_served_as_returned_by_default( $data, bool $with_link, string $expected ): void {
3536+
$this->register_response_processing_route( $data, $with_link );
3537+
3538+
$_GET['_fields'] = 'a';
3539+
$_GET['_embed'] = '1';
3540+
rest_get_server()->serve_request( '/test-ns/response-processing' );
3541+
3542+
$this->assertSame( $expected, rest_get_server()->sent_body, 'Data without fields, or items without fields, should be served as returned.' );
3543+
}
34973544
}

0 commit comments

Comments
 (0)