REST API: Support dot.nested hierarchical properties in _fields query parameter.

Enable clients to opt-in to receipt of one or more specific sub-properties within a response, and not other sub-properties.
Skip potentially expensive filtering and processing for post resources which were explicitly not requested.

Props kadamwhite, TimothyBlynJacobs, dlh.
Fixes #42094.


git-svn-id: https://develop.svn.wordpress.org/trunk@46184 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
K. Adam White
2019-09-19 14:04:51 +00:00
parent 5f1cbd7165
commit b926baa51d
6 changed files with 271 additions and 43 deletions
+68 -3
View File
@@ -696,6 +696,24 @@ function rest_send_allow_header( $response, $server, $request ) {
return $response;
}
/**
* Recursively computes the intersection of arrays using keys for comparison.
*
* @param array $array1 The array with master keys to check.
* @param array $array2 An array to compare keys against.
*
* @return array An associative array containing all the entries of array1 which have keys that are present in all arguments.
*/
function _rest_array_intersect_key_recursive( $array1, $array2 ) {
$array1 = array_intersect_key( $array1, $array2 );
foreach ( $array1 as $key => $value ) {
if ( is_array( $value ) && is_array( $array2[ $key ] ) ) {
$array1[ $key ] = _rest_array_intersect_key_recursive( $value, $array2[ $key ] );
}
}
return $array1;
}
/**
* Filter the API response to include only a white-listed set of response object fields.
*
@@ -723,15 +741,27 @@ function rest_filter_response_fields( $response, $server, $request ) {
// Trim off outside whitespace from the comma delimited list.
$fields = array_map( 'trim', $fields );
$fields_as_keyed = array_combine( $fields, array_fill( 0, count( $fields ), true ) );
// Create nested array of accepted field hierarchy.
$fields_as_keyed = array();
foreach ( $fields as $field ) {
$parts = explode( '.', $field );
$ref = &$fields_as_keyed;
while ( count( $parts ) > 1 ) {
$next = array_shift( $parts );
$ref[ $next ] = array();
$ref = &$ref[ $next ];
}
$last = array_shift( $parts );
$ref[ $last ] = true;
}
if ( wp_is_numeric_array( $data ) ) {
$new_data = array();
foreach ( $data as $item ) {
$new_data[] = array_intersect_key( $item, $fields_as_keyed );
$new_data[] = _rest_array_intersect_key_recursive( $item, $fields_as_keyed );
}
} else {
$new_data = array_intersect_key( $data, $fields_as_keyed );
$new_data = _rest_array_intersect_key_recursive( $data, $fields_as_keyed );
}
$response->set_data( $new_data );
@@ -739,6 +769,41 @@ function rest_filter_response_fields( $response, $server, $request ) {
return $response;
}
/**
* Given an array of fields to include in a response, some of which may be
* `nested.fields`, determine whether the provided field should be included
* in the response body.
*
* If a parent field is passed in, the presence of any nested field within
* that parent will cause the method to return `true`. For example "title"
* will return true if any of `title`, `title.raw` or `title.rendered` is
* provided.
*
* @since 5.3.0
*
* @param string $field A field to test for inclusion in the response body.
* @param array $fields An array of string fields supported by the endpoint.
* @return bool Whether to include the field or not.
*/
function rest_is_field_included( $field, $fields ) {
if ( in_array( $field, $fields, true ) ) {
return true;
}
foreach ( $fields as $accepted_field ) {
// Check to see if $field is the parent of any item in $fields.
// A field "parent" should be accepted if "parent.child" is accepted.
if ( strpos( $accepted_field, "$field." ) === 0 ) {
return true;
}
// Conversely, if "parent" is accepted, all "parent.child" fields should
// also be accepted.
if ( strpos( $field, "$accepted_field." ) === 0 ) {
return true;
}
}
return false;
}
/**
* Adds the REST API URL to the WP RSD endpoint.
*