edit pages
This commit is contained in:
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Get the REST API schema for a given field.
|
||||
*
|
||||
* @param array $field
|
||||
* @return array
|
||||
*/
|
||||
function acf_get_field_rest_schema( array $field ) {
|
||||
$type = acf_get_field_type( $field['type'] );
|
||||
$schema = array();
|
||||
|
||||
if ( ! is_object( $type ) || ! method_exists( $type, 'get_rest_schema' ) ) {
|
||||
return $schema;
|
||||
}
|
||||
|
||||
$schema = $type->get_rest_schema( $field );
|
||||
|
||||
/**
|
||||
* Filter the REST API schema for a given field.
|
||||
*
|
||||
* @param array $schema The field schema array.
|
||||
* @param array $field The field array.
|
||||
*/
|
||||
return (array) apply_filters( 'acf/rest/get_field_schema', $schema, $field );
|
||||
}
|
||||
|
||||
acf_add_filter_variations( 'acf/rest/get_field_schema', array( 'type', 'name', 'key' ), 1 );
|
||||
|
||||
/**
|
||||
* Get the REST API field links for a given field. The links are appended to the REST response under the _links property
|
||||
* and provide API resource links to related objects. If a link is marked as 'embeddable', WordPress can load the resource
|
||||
* in the main request under the _embedded property when the request contains the _embed URL parameter.
|
||||
*
|
||||
* @see \acf_field::get_rest_links()
|
||||
* @see https://developer.wordpress.org/rest-api/using-the-rest-api/linking-and-embedding/
|
||||
*
|
||||
* @param string|int $post_id
|
||||
* @param array $field
|
||||
* @return array
|
||||
*/
|
||||
function acf_get_field_rest_links( $post_id, array $field ) {
|
||||
$value = acf_get_value( $post_id, $field );
|
||||
$type = acf_get_field_type( $field['type'] );
|
||||
$links = $type->get_rest_links( $value, $post_id, $field );
|
||||
|
||||
/**
|
||||
* Filter the REST API links for a given field.
|
||||
*
|
||||
* @param array $links
|
||||
* @param string|int $post_id
|
||||
* @param array $field
|
||||
* @param mixed $value
|
||||
*/
|
||||
return (array) apply_filters( 'acf/rest/get_field_links', $links, $post_id, $field, $value );
|
||||
}
|
||||
|
||||
acf_add_filter_variations( 'acf/rest/get_field_links', array( 'type', 'name', 'key' ), 2 );
|
||||
|
||||
/**
|
||||
* Format a given field's value for output in the REST API.
|
||||
*
|
||||
* @param $value
|
||||
* @param $post_id
|
||||
* @param $field
|
||||
* @param string $format 'light' for normal REST API formatting or 'standard' to apply ACF's normal field formatting.
|
||||
* @return mixed
|
||||
*/
|
||||
function acf_format_value_for_rest( $value, $post_id, $field, $format = 'light' ) {
|
||||
if ( $format === 'standard' ) {
|
||||
$value_formatted = acf_format_value( $value, $post_id, $field );
|
||||
} else {
|
||||
$type = acf_get_field_type( $field['type'] );
|
||||
$value_formatted = $type->format_value_for_rest( $value, $post_id, $field );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the formatted value for a given field.
|
||||
*
|
||||
* @param mixed $value_formatted The formatted value.
|
||||
* @param string|int $post_id The post ID of the current object.
|
||||
* @param array $field The field array.
|
||||
* @param mixed $value The raw/unformatted value.
|
||||
* @param string $format The format applied to the field value.
|
||||
*/
|
||||
return apply_filters( 'acf/rest/format_value_for_rest', $value_formatted, $post_id, $field, $value, $format );
|
||||
}
|
||||
|
||||
acf_add_filter_variations( 'acf/rest/format_value_for_rest', array( 'type', 'name', 'key' ), 2 );
|
@ -0,0 +1,555 @@
|
||||
<?php
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// If class is already defined, return.
|
||||
if ( class_exists( 'ACF_Rest_Api' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
class ACF_Rest_Api {
|
||||
|
||||
/** @var ACF_Rest_Request */
|
||||
private $request;
|
||||
|
||||
/** @var ACF_Rest_Embed_Links */
|
||||
private $embed_links;
|
||||
|
||||
public function __construct() {
|
||||
add_filter( 'rest_pre_dispatch', array( $this, 'initialize' ), 10, 3 );
|
||||
add_action( 'rest_api_init', array( $this, 'register_field' ) );
|
||||
}
|
||||
|
||||
public function initialize( $response, $handler, $request ) {
|
||||
if ( ! acf_get_setting( 'rest_api_enabled' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse request and set the object for local access.
|
||||
$this->request = new ACF_Rest_Request();
|
||||
$this->request->parse_request( $request );
|
||||
|
||||
// Register the 'acf' REST property.
|
||||
$this->register_field();
|
||||
|
||||
// If embed links are enabled in ACF's global settings, init the handler and set for local access.
|
||||
if ( acf_get_setting( 'rest_api_embed_links' ) ) {
|
||||
$this->embed_links = new ACF_Rest_Embed_Links();
|
||||
$this->embed_links->initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register our custom property as a REST field.
|
||||
*/
|
||||
public function register_field() {
|
||||
if ( ! acf_get_setting( 'rest_api_enabled' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->request instanceof ACF_Rest_Request ) {
|
||||
$this->request = new ACF_Rest_Request();
|
||||
$this->request->parse_request( null );
|
||||
}
|
||||
|
||||
$base = $this->request->object_sub_type;
|
||||
|
||||
// If the object sub type ($post_type, $taxonomy, 'user') cannot be determined from the current request,
|
||||
// we don't know what endpoint to register the field against. Bail if that is the case.
|
||||
if ( ! $base ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $this->request->child_object_type ) {
|
||||
$base = $this->request->child_object_type;
|
||||
}
|
||||
|
||||
// If we've already registered this route, no need to do it again.
|
||||
if ( acf_did( 'acf/register_rest_field' ) ) {
|
||||
global $wp_rest_additional_fields;
|
||||
|
||||
if ( isset( $wp_rest_additional_fields[ $base ], $wp_rest_additional_fields[ $base ]['acf'] ) ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
register_rest_field(
|
||||
$base,
|
||||
'acf',
|
||||
array(
|
||||
'schema' => $this->get_schema(),
|
||||
'get_callback' => array( $this, 'load_fields' ),
|
||||
'update_callback' => array( $this, 'update_fields' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically generate the schema for the current request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_schema() {
|
||||
$schema = array(
|
||||
'description' => 'ACF field data',
|
||||
'type' => 'object',
|
||||
'properties' => array(),
|
||||
'arg_options' => array(
|
||||
'validate_callback' => array( $this, 'validate_rest_arg' ),
|
||||
),
|
||||
);
|
||||
|
||||
// If we don't have an object type, we can't determine the schema for the current request.
|
||||
$object_type = $this->request->object_type;
|
||||
if ( ! $object_type ) {
|
||||
return $schema;
|
||||
}
|
||||
|
||||
$object_id = $this->request->get_url_param( 'id' );
|
||||
$child_id = $this->request->get_url_param( 'child_id' );
|
||||
$object_sub_type = $this->request->object_sub_type;
|
||||
|
||||
if ( $child_id ) {
|
||||
$object_id = $child_id;
|
||||
}
|
||||
|
||||
if ( ! $object_id ) {
|
||||
$field_groups = $this->get_field_groups_by_object_type( $object_type );
|
||||
} else {
|
||||
$field_groups = $this->get_field_groups_by_id( $object_id, $object_type, $object_sub_type );
|
||||
}
|
||||
|
||||
if ( empty( $field_groups ) ) {
|
||||
return $schema;
|
||||
}
|
||||
|
||||
foreach ( $field_groups as $field_group ) {
|
||||
foreach ( $this->get_fields( $field_group, $object_id ) as $field ) {
|
||||
$schema['properties'][ $field['name'] ] = acf_get_field_rest_schema( $field );
|
||||
}
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the request args. Mostly a wrapper for `rest_validate_request_arg()`, but also
|
||||
* fires off a filter, so we can add some custom validation for specific fields.
|
||||
*
|
||||
* This will likely no longer be needed once WordPress implements something like `validate_callback`
|
||||
* and `sanitize_callback` for nested schema properties, see:
|
||||
* https://core.trac.wordpress.org/ticket/49960
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param \WP_REST_Request $request
|
||||
* @param string $param
|
||||
*
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function validate_rest_arg( $value, $request, $param ) {
|
||||
// Validate all fields with default WordPress validation first.
|
||||
$valid = rest_validate_request_arg( $value, $request, $param );
|
||||
|
||||
if ( true !== $valid ) {
|
||||
return $valid;
|
||||
}
|
||||
|
||||
foreach ( $value as $field_name => $field_value ) {
|
||||
$field = acf_get_field( $field_name );
|
||||
|
||||
if ( ! $field ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters whether a value passed via REST is valid.
|
||||
*
|
||||
* @since 5.11
|
||||
*
|
||||
* @param bool $valid True if the value is valid, false or WP_Error if not.
|
||||
* @param mixed $value The value to check.
|
||||
* @param array $field An array of information about the field.
|
||||
*/
|
||||
$valid = apply_filters( 'acf/validate_rest_value/type=' . $field['type'], true, $field_value, $field );
|
||||
|
||||
if ( true !== $valid ) {
|
||||
return $valid;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load field values into the requested object. This method is not a part of any public API and is only public as
|
||||
* it is required by WordPress.
|
||||
*
|
||||
* @param array $object An array representation of the post, term, or user object.
|
||||
* @param string $field_name
|
||||
* @param WP_REST_Request $request
|
||||
* @param string $object_sub_type Note that this isn't the same as $this->object_type. This variable is
|
||||
* more specific and can be a post type or taxonomy.
|
||||
* @return array
|
||||
*/
|
||||
public function load_fields( $object, $field_name, $request, $object_sub_type ) {
|
||||
// The fields loaded for display on the REST API in the form of {$field_name}=>{$field_value} pairs.
|
||||
$fields = array();
|
||||
|
||||
// Determine the object ID from the given object.
|
||||
$object_id = acf_get_object_id( $object );
|
||||
|
||||
// Use this object type parsed from the request.
|
||||
$object_type = $this->request->object_type;
|
||||
|
||||
// Object ID and type are essential to determining which fields to load. Return if we don't have both.
|
||||
if ( ! $object_id or ! $object_type ) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$object_sub_type = str_replace( '-revision', '', $object_sub_type );
|
||||
|
||||
// Get all field groups for the current object.
|
||||
$field_groups = $this->get_field_groups_by_id( $object_id, $object_type, $object_sub_type );
|
||||
if ( empty( $field_groups ) ) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
// Determine the ACF ID string for the current object.
|
||||
$post_id = $this->make_identifier( $object_id, $object_type );
|
||||
|
||||
// Loop through the fields within all applicable field groups and add the fields to the response.
|
||||
foreach ( $field_groups as $field_group ) {
|
||||
foreach ( $this->get_fields( $field_group, $object_id ) as $field ) {
|
||||
$value = acf_get_value( $post_id, $field );
|
||||
|
||||
if ( $this->embed_links ) {
|
||||
$this->embed_links->prepare_links( $post_id, $field );
|
||||
}
|
||||
|
||||
// Format the field value according to the request params.
|
||||
$format = $request->get_param( 'acf_format' ) ?: acf_get_setting( 'rest_api_format' );
|
||||
$value = acf_format_value_for_rest( $value, $post_id, $field, $format );
|
||||
|
||||
$fields[ $field['name'] ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the store so that REST API values (which may be preloaded
|
||||
* by WP core and have different values than standard values) aren't
|
||||
* saved to the store.
|
||||
*/
|
||||
acf_get_store( 'values' )->reset();
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update any incoming field values for the given object. This method is not a part of any public API and is only
|
||||
* public as it is required by WordPress.
|
||||
*
|
||||
* @param array $data
|
||||
* @param WP_Post|WP_Term|WP_User $object
|
||||
* @param string $property 'acf'
|
||||
* @param WP_REST_Request $request
|
||||
* @param string $object_sub_type This will be the post type, the taxonomy, or 'user'.
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function update_fields( $data, $object, $property, $request, $object_sub_type ) {
|
||||
// If 'acf' data object is empty, don't do anything.
|
||||
if ( empty( $data ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Determine the object context (type & ID). If the context can't be determined from the current request, throw an
|
||||
// error as the fields are not updateable. This handles in line with WordPress' \WP_REST_Request::sanitize_params().
|
||||
$object_id = acf_get_object_id( $object );
|
||||
$object_type = $this->request->object_type;
|
||||
if ( ! $object_id or ! $object_type ) {
|
||||
return new WP_Error(
|
||||
'acf_rest_object_unknown',
|
||||
__( sprintf( 'Unable to determine the %s object ID or type. The %s property cannot be updated.', get_class( $object ), $property ), 'acf' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
// Determine the ACF selector for the current object.
|
||||
$post_id = $this->make_identifier( $object_id, $object_type );
|
||||
|
||||
// Allow unrestricted update of fields by field key when saving via the WordPress admin. Admin mode will
|
||||
// update fields using their field keys to lookup the field. The field lookup is not scoped to field groups
|
||||
// located on the given object so any field can be updated. Given the field keys are not defined in the
|
||||
// schema, core validation/sanitisation are also bypassed.
|
||||
// if ( $this->is_admin_mode( $data ) ) {
|
||||
// Loop through payload and save fields using field keys.
|
||||
// foreach ( $data as $field_key => $value ) {
|
||||
// if ( $field = acf_get_field( $field_key ) ) {
|
||||
// acf_update_value( $value, $post_id, $field );
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// todo - consider/discuss handling this in the request object instead
|
||||
// If the incoming data defines field group keys, extract it from the data. This is used to scope the
|
||||
// field lookup in \ACF_Rest_Api::get_field_groups_by_id();
|
||||
$field_group_scope = acf_extract_var( $data, '_acf_field_group_scope', array() );
|
||||
|
||||
// Get all field groups for the current object.
|
||||
$field_groups = $this->get_field_groups_by_id( $object_id, $object_type, $object_sub_type, $field_group_scope );
|
||||
if ( empty( $field_groups ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Collect all fields from matching field groups.
|
||||
$all_fields = array();
|
||||
foreach ( $field_groups as $field_group ) {
|
||||
if ( $fields = $this->get_fields( $field_group, $object_id ) ) {
|
||||
$all_fields = array_merge( $fields, $all_fields );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $all_fields ) {
|
||||
// todo - consider/discuss handling this in the request object instead.
|
||||
// If the incoming request has a map of field names to keys, extract it for use in the subsequent
|
||||
// field search.
|
||||
$field_key_map = acf_extract_var( $data, '_acf_field_key_map', array() );
|
||||
|
||||
// Loop through the inbound data payload, find the field matching the incoming field name, and
|
||||
// update the field.
|
||||
foreach ( $data as $field_name => $value ) {
|
||||
|
||||
// If the field name has a key explicitly mapped to it, use the field key to find the field.
|
||||
if ( isset( $field_key_map[ $field_name ] ) ) {
|
||||
$field_name = $field_key_map[ $field_name ];
|
||||
}
|
||||
|
||||
if ( $field = acf_search_fields( $field_name, $all_fields ) ) {
|
||||
acf_update_value( $value, $post_id, $field );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// todo - this should check for a flag and validate a nonce to ensure we are in admin mode.
|
||||
// todo - consider/discuss handling this in the request object instead.
|
||||
private function is_admin_mode( $data ) {
|
||||
return isset( $data['_acf_admin_mode'] ) && $data['_acf_admin_mode'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the ACF identifier string for the given object.
|
||||
*
|
||||
* @param int $object_id
|
||||
* @param string $object_type 'user', 'term', or 'post'
|
||||
* @return string
|
||||
*/
|
||||
private function make_identifier( $object_id, $object_type ) {
|
||||
$formats = array(
|
||||
'user' => 'user_%s',
|
||||
'term' => 'term_%s',
|
||||
'comment' => 'comment_%s',
|
||||
);
|
||||
|
||||
return isset( $formats[ $object_type ] )
|
||||
? sprintf( $formats[ $object_type ], $object_id )
|
||||
: $object_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of the location types that a field group is configured to use.
|
||||
*
|
||||
* @param string $object_type 'user', 'term', or 'post'
|
||||
* @param array $field_group The field group to check.
|
||||
* @param array $location_types An array of location types.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function object_type_has_field_group( $object_type, $field_group, $location_types = array() ) {
|
||||
if ( ! isset( $field_group['location'] ) || ! is_array( $field_group['location'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$location_types = empty( $location_types ) ? acf_get_location_types() : $location_types;
|
||||
|
||||
foreach ( $field_group['location'] as $rule_group ) {
|
||||
$match = false;
|
||||
foreach ( $rule_group as $rule ) {
|
||||
$rule = acf_validate_location_rule( $rule );
|
||||
|
||||
if ( ! isset( $location_types[ $rule['param'] ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make sure the main object type matches.
|
||||
$location_type = $location_types[ $rule['param'] ];
|
||||
if ( ! isset( $location_type->object_type ) || $location_type->object_type !== (string) $object_type ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* For posts/pages, we can only be sure that fields will show up if
|
||||
* the field group is configured to show up for all items of the current
|
||||
* post type.
|
||||
*/
|
||||
if ( 'post' === $object_type && 'post_type' === $rule['param'] ) {
|
||||
if ( $rule['operator'] === '==' && $this->request->object_sub_type !== $rule['value'] ) {
|
||||
continue;
|
||||
}
|
||||
if ( $rule['operator'] === '!=' && $this->request->object_sub_type === $rule['value'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$match = true;
|
||||
}
|
||||
|
||||
if ( 'term' === $object_type && 'taxonomy' === $rule['param'] ) {
|
||||
if ( $rule['operator'] === '==' && $this->request->object_sub_type !== $rule['value'] ) {
|
||||
continue;
|
||||
}
|
||||
if ( $rule['operator'] === '!=' && $this->request->object_sub_type === $rule['value'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$match = true;
|
||||
}
|
||||
|
||||
if ( in_array( $object_type, array( 'user', 'comment' ) ) ) {
|
||||
$match = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $match ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all field groups for the provided object type.
|
||||
*
|
||||
* @param string $object_type 'user', 'term', or 'post'
|
||||
*
|
||||
* @return array An array of field groups that display for that location type.
|
||||
*/
|
||||
private function get_field_groups_by_object_type( $object_type ) {
|
||||
$field_groups = acf_get_field_groups();
|
||||
$location_types = acf_get_location_types();
|
||||
$object_type_groups = array();
|
||||
|
||||
foreach ( $field_groups as $field_group ) {
|
||||
if ( empty( $field_group['show_in_rest'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $this->object_type_has_field_group( $object_type, $field_group, $location_types ) ) {
|
||||
$object_type_groups[] = $field_group;
|
||||
}
|
||||
}
|
||||
|
||||
return $object_type_groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all field groups for a given object.
|
||||
*
|
||||
* @param int $object_id
|
||||
* @param string $object_type 'user', 'term', or 'post'
|
||||
* @param string|null $object_sub_type The post type or taxonomy. When an $object_type of 'user' is in play, this can be ignored.
|
||||
* @param array $scope Field group keys to limit the returned set of field groups to. This is used to scope field lookups to specific groups.
|
||||
* @return array An array of matching field groups.
|
||||
*/
|
||||
private function get_field_groups_by_id( $object_id, $object_type, $object_sub_type = null, $scope = array() ) {
|
||||
// When dealing with a term, we need the taxonomy in order to look up the relevant field groups. The taxonomy is expected
|
||||
// in the $object_sub_type variable but when building our schema, this isn't readily available. This block ensures the
|
||||
// taxonomy is set when not passed in.
|
||||
if ( $object_type === 'term' && $object_sub_type === null ) {
|
||||
$term = get_term( $object_id );
|
||||
if ( ! $term instanceof WP_Term ) {
|
||||
return array();
|
||||
}
|
||||
$object_sub_type = $term->taxonomy;
|
||||
}
|
||||
|
||||
switch ( $object_type ) {
|
||||
case 'user':
|
||||
$args = array(
|
||||
'user_id' => $object_id,
|
||||
'rest' => true,
|
||||
);
|
||||
break;
|
||||
case 'term':
|
||||
$args = array( 'taxonomy' => $object_sub_type );
|
||||
break;
|
||||
case 'comment':
|
||||
$comment = get_comment( $object_id );
|
||||
$post_type = get_post_type( $comment->comment_post_ID );
|
||||
$args = array( 'comment' => $post_type );
|
||||
break;
|
||||
case 'post':
|
||||
default:
|
||||
$args = array( 'post_id' => $object_id );
|
||||
$child_rest_base = $this->request->get_url_param( 'child_rest_base' );
|
||||
if ( $child_rest_base && 'post' === $object_type ) {
|
||||
$args['post_type'] = $object_sub_type;
|
||||
}
|
||||
}
|
||||
|
||||
// Only return field groups that are configured to show in REST.
|
||||
return array_filter(
|
||||
acf_get_field_groups( $args ),
|
||||
function ( $group ) use ( $scope ) {
|
||||
if ( $scope and ! in_array( $group['key'], $scope ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $group['show_in_rest'];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all ACF fields for a given field group and allow third party filtering.
|
||||
*
|
||||
* @param array $field_group This could technically be other possible values supported by acf_get_fields() but in this
|
||||
* context, we're only using the field group arrays.
|
||||
* @param null|int $object_id The ID of the object being prepared.
|
||||
* @return array
|
||||
*/
|
||||
private function get_fields( $field_group, $object_id = null ) {
|
||||
// Get all fields for this field group that are rest enabled.
|
||||
$fields = array_filter(
|
||||
acf_get_fields( $field_group ),
|
||||
function ( $field ) {
|
||||
$field_type = acf_get_field_type( $field['type'] );
|
||||
return isset( $field_type->show_in_rest ) && $field_type->show_in_rest;
|
||||
}
|
||||
);
|
||||
|
||||
// Set up context array for use in the filter below.
|
||||
$resource = array(
|
||||
'type' => $this->request->object_type,
|
||||
'sub_type' => $this->request->object_sub_type,
|
||||
'id' => $object_id,
|
||||
);
|
||||
|
||||
$http_method = $this->request->http_method;
|
||||
|
||||
/**
|
||||
* Filter the fields available to the REST API.
|
||||
*
|
||||
* @param array $fields The ACF fields for this field group.
|
||||
* @param array $resource Contextual information about the current resource request.
|
||||
* @param string $http_method The HTTP method of the current request (GET, POST, PUT, PATCH, DELETE, OPTION, HEAD).
|
||||
*/
|
||||
return (array) apply_filters( 'acf/rest/get_fields', $fields, $resource, $http_method );
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// If class is already defined, return.
|
||||
if ( class_exists( 'ACF_Rest_Api' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class ACF_Rest_Embed_Links
|
||||
*
|
||||
* Manage the addition of embed links on supported REST endpoints.
|
||||
*/
|
||||
class ACF_Rest_Embed_Links {
|
||||
|
||||
/** @var array Links to add to the response. These can be flagged as embeddable and expanded when _embed is passed with the request. */
|
||||
private $links = array();
|
||||
|
||||
public function initialize() {
|
||||
$this->hook_link_handlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into all REST-enabled post type, taxonomy, and the user controllers in order to prepare links.
|
||||
*/
|
||||
private function hook_link_handlers() {
|
||||
foreach ( get_post_types( array( 'show_in_rest' => true ) ) as $post_type ) {
|
||||
add_filter( "rest_prepare_{$post_type}", array( $this, 'load_item_links' ), 10, 3 );
|
||||
}
|
||||
|
||||
foreach ( get_taxonomies( array( 'show_in_rest' => true ) ) as $taxonomy ) {
|
||||
add_filter( "rest_prepare_{$taxonomy}", array( $this, 'load_item_links' ), 10, 3 );
|
||||
}
|
||||
|
||||
add_filter( 'rest_prepare_user', array( $this, 'load_item_links' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add links to internal property for subsequent use in \ACF_Rest_Embed_Links::load_item_links().
|
||||
*
|
||||
* @param $post_id
|
||||
* @param array $field
|
||||
*/
|
||||
public function prepare_links( $post_id, array $field ) {
|
||||
$links = acf_get_field_rest_links( $post_id, $field );
|
||||
if ( ! $links ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $links as $link ) {
|
||||
// If required array keys are not provided, skip.
|
||||
if ( empty( $link['rel'] ) or empty( $link['href'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use the 'rel' and 'href' to for a key. The key only prevents against the same object
|
||||
// appearing more than once within the same 'rel' property.
|
||||
$this->links[ $link['rel'] . ':' . $link['href'] ] = $link;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into the rest_prepare_{$type} filters and add links for the object being prepared.
|
||||
*
|
||||
* @param WP_REST_Response $response
|
||||
* @param WP_Post|WP_User|WP_Term $item
|
||||
* @param WP_REST_Request $request
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function load_item_links( $response, $item, $request ) {
|
||||
if ( empty( $this->links ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
while ( $attributes = array_pop( $this->links ) ) {
|
||||
$response->add_link(
|
||||
acf_extract_var( $attributes, 'rel' ),
|
||||
acf_extract_var( $attributes, 'href' ),
|
||||
$attributes
|
||||
);
|
||||
}
|
||||
|
||||
// Reset the links prop.
|
||||
$this->links = array();
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// If class is already defined, return.
|
||||
if ( class_exists( 'ACF_Rest_Request' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class ACF_Rest_Request
|
||||
*
|
||||
* @property-read string $object_sub_type
|
||||
* @property-read string $object_type
|
||||
* @property-read string $http_method
|
||||
*/
|
||||
class ACF_Rest_Request {
|
||||
|
||||
/**
|
||||
* Define which private/protected class properties are allowed read access. Access to these is controlled in
|
||||
* \ACF_Rest_Request::__get();
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $readonly_props = array( 'object_type', 'object_sub_type', 'child_object_type', 'http_method' );
|
||||
|
||||
/** @var string The HTTP request method for the current request. i.e; GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD */
|
||||
private $http_method;
|
||||
|
||||
/** @var string The current route being requested. */
|
||||
private $current_route;
|
||||
|
||||
/** @var array Route URL patterns we support. */
|
||||
private $supported_routes = array();
|
||||
|
||||
/** @var array Parameters matched from the URL. e.g; object IDs. */
|
||||
private $url_params = array();
|
||||
|
||||
/** @var string The underlying object type. e.g; post, term, user, etc. */
|
||||
private $object_type;
|
||||
|
||||
/** @var string The requested object type. */
|
||||
private $object_sub_type;
|
||||
|
||||
/** @var string The object type for a child object. e.g. post-revision, autosaves, etc. */
|
||||
private $child_object_type;
|
||||
|
||||
/**
|
||||
* Determine all required information from the current request.
|
||||
*/
|
||||
public function parse_request( $request ) {
|
||||
$this->set_http_method();
|
||||
$this->set_current_route( $request );
|
||||
$this->build_supported_routes();
|
||||
$this->set_url_params();
|
||||
$this->set_object_types();
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter for accessing read-only properties. Should we ever need to enforce a getter method, we can do so here.
|
||||
*
|
||||
* @param string $name The desired property name.
|
||||
* @return string|null
|
||||
*/
|
||||
public function __get( $name ) {
|
||||
if ( in_array( $name, $this->readonly_props ) ) {
|
||||
return $this->$name;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a URL parameter if found on the request URL.
|
||||
*
|
||||
* @param $param
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function get_url_param( $param ) {
|
||||
return isset( $this->url_params[ $param ] ) ? $this->url_params[ $param ] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the HTTP method of the current request.
|
||||
*/
|
||||
private function set_http_method() {
|
||||
$this->http_method = 'GET';
|
||||
|
||||
if ( ! empty( $_SERVER['REQUEST_METHOD'] ) ) {
|
||||
$this->http_method = strtoupper( sanitize_text_field( $_SERVER['REQUEST_METHOD'] ) );
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Verified elsewhere.
|
||||
// HTTP method override for clients that can't use PUT/PATCH/DELETE. This is identical to WordPress'
|
||||
// handling in \WP_REST_Server::serve_request(). This block of code should always be identical to that
|
||||
// in core.
|
||||
if ( isset( $_GET['_method'] ) ) {
|
||||
$this->http_method = strtoupper( sanitize_text_field( $_GET['_method'] ) );
|
||||
} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
|
||||
$this->http_method = strtoupper( sanitize_text_field( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) );
|
||||
}
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current REST route as determined by WordPress.
|
||||
*/
|
||||
private function set_current_route( $request ) {
|
||||
if ( $request ) {
|
||||
$this->current_route = $request->get_route();
|
||||
} else {
|
||||
$this->current_route = empty( $GLOBALS['wp']->query_vars['rest_route'] ) ? null : $GLOBALS['wp']->query_vars['rest_route'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an array of route match patterns that we handle. These are the same as WordPress' core patterns except
|
||||
* we are also matching the object type here as well.
|
||||
*/
|
||||
private function build_supported_routes() {
|
||||
// Add post type routes for all post types configured to show in REST.
|
||||
/** @var WP_Post_Type $post_type */
|
||||
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
|
||||
$rest_base = acf_get_object_type_rest_base( $post_type );
|
||||
$this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})";
|
||||
$this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})/(?P<id>[\d]+)";
|
||||
|
||||
if ( post_type_supports( $post_type->name, 'revisions' ) ) {
|
||||
$this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})/(?P<id>[\d]+)/(?P<child_rest_base>revisions)";
|
||||
$this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})/(?P<id>[\d]+)/(?P<child_rest_base>revisions)/(?P<child_id>[\d]+)";
|
||||
}
|
||||
|
||||
if ( 'attachment' !== $post_type->name ) {
|
||||
$this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})/(?P<id>[\d]+)/(?P<child_rest_base>autosaves)";
|
||||
$this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})/(?P<id>[\d]+)/(?P<child_rest_base>autosaves)/(?P<child_id>[\d]+)";
|
||||
}
|
||||
}
|
||||
|
||||
// Add taxonomy routes all taxonomies configured to show in REST.
|
||||
/** @var WP_Taxonomy $taxonomy */
|
||||
foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
|
||||
$rest_base = acf_get_object_type_rest_base( $taxonomy );
|
||||
$this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})";
|
||||
$this->supported_routes[] = "/wp/v2/(?P<rest_base>{$rest_base})/(?P<id>[\d]+)";
|
||||
}
|
||||
|
||||
// Add user routes.
|
||||
$this->supported_routes[] = '/wp/v2/(?P<rest_base>users)';
|
||||
$this->supported_routes[] = '/wp/v2/(?P<rest_base>users)/(?P<id>[\d]+)';
|
||||
$this->supported_routes[] = '/wp/v2/(?P<rest_base>users)/me';
|
||||
|
||||
// Add comment routes.
|
||||
$this->supported_routes[] = '/wp/v2/(?P<rest_base>comments)';
|
||||
$this->supported_routes[] = '/wp/v2/(?P<rest_base>comments)/(?P<id>[\d]+)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop through supported routes to find matching pattern. Use matching pattern to determine any URL parameters.
|
||||
*/
|
||||
private function set_url_params() {
|
||||
if ( ! $this->supported_routes || ! is_string( $this->current_route ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine query args passed within the URL.
|
||||
foreach ( $this->supported_routes as $route ) {
|
||||
$match = preg_match( '@^' . $route . '$@i', $this->current_route, $matches );
|
||||
if ( ! $match ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( $matches as $param => $value ) {
|
||||
if ( ! is_int( $param ) ) {
|
||||
$this->url_params[ $param ] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the object type and sub type from the requested route. We need to know both the underlying WordPress
|
||||
* object type as well as post type or taxonomy in order to provide the right context when getting/updating fields.
|
||||
*/
|
||||
private function set_object_types() {
|
||||
$base = $this->get_url_param( 'rest_base' );
|
||||
$child_base = $this->get_url_param( 'child_rest_base' );
|
||||
|
||||
// We need a matched rest base to proceed here. If we haven't matched one while parsing the request, bail.
|
||||
if ( is_null( $base ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the matching object type from the rest base. Start with users as that is simple. From there,
|
||||
// check post types then check taxonomies if a matching post type cannot be found.
|
||||
if ( $base === 'users' ) {
|
||||
$this->object_type = $this->object_sub_type = 'user';
|
||||
} elseif ( $base === 'comments' ) {
|
||||
$this->object_type = $this->object_sub_type = 'comment';
|
||||
} elseif ( $post_type = $this->get_post_type_by_rest_base( $base ) ) {
|
||||
$this->object_type = 'post';
|
||||
$this->object_sub_type = $post_type->name;
|
||||
|
||||
// Autosaves and revisions are mostly handled the same by WP, and share the same schema.
|
||||
if ( in_array( $this->get_url_param( 'child_rest_base' ), array( 'revisions', 'autosaves' ) ) ) {
|
||||
$this->child_object_type = $this->object_sub_type . '-revision';
|
||||
}
|
||||
} elseif ( $taxonomy = $this->get_taxonomy_by_rest_base( $base ) ) {
|
||||
$this->object_type = 'term';
|
||||
$this->object_sub_type = $taxonomy->name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the REST enabled post type object that matches the given REST base.
|
||||
*
|
||||
* @param string $rest_base
|
||||
* @return WP_Post_Type|null
|
||||
*/
|
||||
private function get_post_type_by_rest_base( $rest_base ) {
|
||||
$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
|
||||
|
||||
foreach ( $types as $type ) {
|
||||
if ( acf_get_object_type_rest_base( $type ) === $rest_base ) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the REST enabled taxonomy object that matches the given REST base.
|
||||
*
|
||||
* @param $rest_base
|
||||
* @return WP_Taxonomy|null
|
||||
*/
|
||||
private function get_taxonomy_by_rest_base( $rest_base ) {
|
||||
$taxonomies = get_taxonomies( array( 'show_in_rest' => true ), 'objects' );
|
||||
|
||||
foreach ( $taxonomies as $taxonomy ) {
|
||||
if ( acf_get_object_type_rest_base( $taxonomy ) === $rest_base ) {
|
||||
return $taxonomy;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user