diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 2d6ecde631..58cbcacbfb 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -1658,3 +1658,63 @@ function rest_parse_embed_param( $embed ) { return $rels; } + +/** + * Filters the response to remove any fields not available in the given context. + * + * @since 5.5.0 + * + * @param array|object $data The response data to modify. + * @param array $schema The schema for the endpoint used to filter the response. + * @param string $context The requested context. + * @return array|object The filtered response data. + */ +function rest_filter_response_by_context( $data, $schema, $context ) { + if ( ! is_array( $data ) && ! is_object( $data ) ) { + return $data; + } + + if ( isset( $schema['type'] ) ) { + $type = $schema['type']; + } elseif ( isset( $schema['properties'] ) ) { + $type = 'object'; // Back compat if a developer accidentally omitted the type. + } else { + return $data; + } + + foreach ( $data as $key => $value ) { + $check = array(); + + if ( 'array' === $type || ( is_array( $type ) && in_array( 'array', $type, true ) ) ) { + $check = isset( $schema['items'] ) ? $schema['items'] : array(); + } elseif ( 'object' === $type || ( is_array( $type ) && in_array( 'object', $type, true ) ) ) { + if ( isset( $schema['properties'][ $key ] ) ) { + $check = $schema['properties'][ $key ]; + } elseif ( isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) { + $check = $schema['additionalProperties']; + } + } + + if ( ! isset( $check['context'] ) ) { + continue; + } + + if ( ! in_array( $context, $check['context'], true ) ) { + if ( is_object( $data ) ) { + unset( $data->$key ); + } else { + unset( $data[ $key ] ); + } + } elseif ( is_array( $value ) || is_object( $value ) ) { + $new_value = rest_filter_response_by_context( $value, $check, $context ); + + if ( is_object( $data ) ) { + $data->$key = $new_value; + } else { + $data[ $key ] = $new_value; + } + } + } + + return $data; +} diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php index b9c05eb894..da5254a327 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php @@ -288,7 +288,7 @@ abstract class WP_REST_Controller { * * @since 4.7.0 * - * @param array $data Response data to fiter. + * @param array $data Response data to filter. * @param string $context Context defined in the schema. * @return array Filtered response. */ @@ -296,32 +296,7 @@ abstract class WP_REST_Controller { $schema = $this->get_item_schema(); - foreach ( $data as $key => $value ) { - if ( empty( $schema['properties'][ $key ] ) || empty( $schema['properties'][ $key ]['context'] ) ) { - continue; - } - - if ( ! in_array( $context, $schema['properties'][ $key ]['context'], true ) ) { - unset( $data[ $key ] ); - continue; - } - - if ( 'object' === $schema['properties'][ $key ]['type'] && ! empty( $schema['properties'][ $key ]['properties'] ) ) { - foreach ( $schema['properties'][ $key ]['properties'] as $attribute => $details ) { - if ( empty( $details['context'] ) ) { - continue; - } - - if ( ! in_array( $context, $details['context'], true ) ) { - if ( isset( $data[ $key ][ $attribute ] ) ) { - unset( $data[ $key ][ $attribute ] ); - } - } - } - } - } - - return $data; + return rest_filter_response_by_context( $data, $schema, $context ); } /** diff --git a/tests/phpunit/tests/rest-api.php b/tests/phpunit/tests/rest-api.php index 97b79c8c88..bc05e7ed38 100644 --- a/tests/phpunit/tests/rest-api.php +++ b/tests/phpunit/tests/rest-api.php @@ -975,4 +975,324 @@ class Tests_REST_API extends WP_UnitTestCase { array( array( 'https://api.w.org/term', 'https://api.w.org/attachment' ), array( 'https://api.w.org/term', 'https://api.w.org/attachment' ) ), ); } + + /** + * @ticket 48819 + * + * @dataProvider _dp_rest_filter_response_by_context + */ + public function test_rest_filter_response_by_context( $schema, $data, $expected ) { + $this->assertEquals( $expected, rest_filter_response_by_context( $data, $schema, 'view' ) ); + } + + public function _dp_rest_filter_response_by_context() { + return array( + 'default' => array( + array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => array( + 'first' => array( + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'second' => array( + 'type' => 'string', + 'context' => array( 'edit' ), + ), + ), + ), + array( + 'first' => 'a', + 'second' => 'b', + ), + array( 'first' => 'a' ), + ), + 'keeps missing context' => array( + array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => array( + 'first' => array( + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'second' => array( + 'type' => 'string', + ), + ), + ), + array( + 'first' => 'a', + 'second' => 'b', + ), + array( + 'first' => 'a', + 'second' => 'b', + ), + ), + 'removes empty context' => array( + array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => array( + 'first' => array( + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'second' => array( + 'type' => 'string', + 'context' => array(), + ), + ), + ), + array( + 'first' => 'a', + 'second' => 'b', + ), + array( 'first' => 'a' ), + ), + 'nested properties' => array( + array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => array( + 'parent' => array( + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'child' => array( + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'hidden' => array( + 'type' => 'string', + 'context' => array( 'edit' ), + ), + ), + ), + ), + ), + array( + 'parent' => array( + 'child' => 'hi', + 'hidden' => 'there', + ), + ), + array( 'parent' => array( 'child' => 'hi' ) ), + ), + 'grand child properties' => array( + array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => array( + 'parent' => array( + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'child' => array( + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'grand' => array( + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'hidden' => array( + 'type' => 'string', + 'context' => array( 'edit' ), + ), + ), + ), + ), + ), + ), + ), + array( + 'parent' => array( + 'child' => array( + 'grand' => 'hi', + ), + ), + ), + array( 'parent' => array( 'child' => array( 'grand' => 'hi' ) ) ), + ), + 'array' => array( + array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => array( + 'arr' => array( + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'visible' => array( + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'hidden' => array( + 'type' => 'string', + 'context' => array( 'edit' ), + ), + ), + ), + ), + ), + ), + array( + 'arr' => array( + array( + 'visible' => 'hi', + 'hidden' => 'there', + ), + ), + ), + array( 'arr' => array( array( 'visible' => 'hi' ) ) ), + ), + 'additional properties' => array( + array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => array( + 'additional' => array( + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'a' => array( + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'b' => array( + 'type' => 'string', + 'context' => array( 'edit' ), + ), + ), + 'additionalProperties' => array( + 'type' => 'string', + 'context' => array( 'edit' ), + ), + ), + ), + ), + array( + 'additional' => array( + 'a' => '1', + 'b' => '2', + 'c' => '3', + ), + ), + array( 'additional' => array( 'a' => '1' ) ), + ), + 'multiple types object' => array( + array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => array( + 'multi' => array( + 'type' => array( 'object', 'string' ), + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'a' => array( + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'b' => array( + 'type' => 'string', + 'context' => array( 'edit' ), + ), + ), + ), + ), + ), + array( + 'multi' => array( + 'a' => '1', + 'b' => '2', + ), + ), + array( 'multi' => array( 'a' => '1' ) ), + ), + 'multiple types array' => array( + array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => array( + 'multi' => array( + 'type' => array( 'array', 'string' ), + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'visible' => array( + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'hidden' => array( + 'type' => 'string', + 'context' => array( 'edit' ), + ), + ), + ), + ), + ), + ), + array( + 'multi' => array( + array( + 'visible' => '1', + 'hidden' => '2', + ), + ), + ), + array( 'multi' => array( array( 'visible' => '1' ) ) ), + ), + 'grand child properties does not traverses missing context' => array( + array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => array( + 'parent' => array( + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'child' => array( + 'type' => 'object', + 'properties' => array( + 'grand' => array( + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'hidden' => array( + 'type' => 'string', + 'context' => array( 'edit' ), + ), + ), + ), + ), + ), + ), + ), + array( + 'parent' => array( + 'child' => array( + 'grand' => 'hi', + 'hidden' => 'there', + ), + ), + ), + array( + 'parent' => array( + 'child' => array( + 'grand' => 'hi', + 'hidden' => 'there', + ), + ), + ), + ), + ); + } }