includes/abstracts/class-llms-rest-controller.php
<?php
/**
* Base REST Controller
*
* @package LifterLMS_REST/Abstracts
*
* @since 1.0.0-beta.1
* @version 1.0.0-beta.27
*/
defined( 'ABSPATH' ) || exit;
/**
* LLMS_REST_Controller class.
*
* @since 1.0.0-beta.1
* @since 1.0.0-beta.3 Fix an issue displaying a last page for lists with 0 possible results & handle error conditions early in responses.
* @since 1.0.0-beta.7 Break `get_items()` method into `prepare_collection_query_args()`, `prepare_args_for_total_count_query()`,
* `prepare_collection_items_for_response()` and `add_header_pagination()` methods so to improve abstraction.
* `prepare_objects_query()` renamed to `prepare_collection_query_args()`.
* @since 1.0.0-beta.12 Added logic to perform a collection search.
* Added `object_inserted()` and `object_completely_inserted()` methods called after an object is
* respectively inserted in the DB and all its additional fields have been updated as well (completely inserted).
* @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`.
*/
abstract class LLMS_REST_Controller extends LLMS_REST_Controller_Stubs {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'llms/v1';
/**
* Schema properties available for ordering the collection.
*
* @var string[]
*/
protected $orderby_properties = array(
'id',
);
/**
* Whether search is allowed.
*
* @var boolean
*/
protected $is_searchable = false;
/**
* LLMS REST resource schema.
*
* @var array
*/
protected $schema;
/**
* Additional rest field names to skip (added via `register_rest_field()`).
*
* @var string[]
*/
protected $disallowed_additional_fields = array();
/**
* Meta field names to skip (added via `register_meta()`).
*
* @var string[]
*/
protected $disallowed_meta_fields = array();
/**
* Create an item.
*
* @since 1.0.0-beta.1
* @since 1.0.0-beta.12 Call `object_inserted` and `object_completely_inserted` after an object is
* respectively inserted in the DB and all its additional fields have been
* updated as well (completely inserted).
* @since 1.0.0-beta.27 Handle custom meta registered via `register_meta()` and custom rest fields registered via `register_rest_field()`.
*
* @param WP_REST_Request $request Request object.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
return llms_rest_bad_request_error( __( 'Cannot create an existing resource.', 'lifterlms' ) );
}
$schema = $this->get_item_schema();
$item = $this->prepare_item_for_database( $request );
// Exclude additional fields registered via `register_rest_field()`.
$item = array_diff_key( $item, $this->get_additional_fields() );
$object = $this->create_object( $item, $request );
if ( is_wp_error( $object ) ) {
return $object;
}
$this->object_inserted( $object, $request, $schema, true );
// Registered via `register_meta()`.
$meta_update = $this->update_meta( $object, $request, $schema );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
// Registered via `register_rest_field()`.
$fields_update = $this->update_additional_fields_for_object( $object, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$this->object_completely_inserted( $object, $request, $schema, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $object, $request );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $this->get_object_id( $object ) ) ) );
return $response;
}
/**
* Called right after a resource is inserted (created/updated).
*
* @since 1.0.0-beta.12
*
* @param object $object Inserted or updated object.
* @param WP_REST_Request $request Request object.
* @param array $schema The item schema.
* @param bool $creating True when creating a post, false when updating.
*/
protected function object_inserted( $object, $request, $schema, $creating ) {
$type = $this->get_object_type();
/**
* Fires after a single llms resource is created or updated via the REST API.
*
* The dynamic portion of the hook name, `$type`, refers to the object type this controller is responsible for managing.
*
* @since 1.0.0-beta.12
*
* @param object $object Inserted or updated object.
* @param WP_REST_Request $request Request object.
* @param array $schema The item schema.
* @param bool $creating True when creating a post, false when updating.
*/
do_action( "llms_rest_insert_{$type}", $object, $request, $schema, $creating );
}
/**
* Called right after a resource is completely inserted (created/updated).
*
* @since 1.0.0-beta.12
*
* @param LLMS_Post $object Inserted or updated object.
* @param WP_REST_Request $request Request object.
* @param array $schema The item schema.
* @param bool $creating True when creating a post, false when updating.
*/
protected function object_completely_inserted( $object, $request, $schema, $creating ) {
$type = $this->get_object_type();
/**
* Fires after a single llms resource is completely created or updated via the REST API.
*
* The dynamic portion of the hook name, `$type`, refers to the object type this controller is responsible for managing.
*
* @since 1.0.0-beta.12
*
* @param object $object Inserted or updated object.
* @param WP_REST_Request $request Request object.
* @param array $schema The item schema.
* @param bool $creating True when creating a post, false when updating.
*/
do_action( "llms_rest_after_insert_{$type}", $object, $request, $schema, $creating );
}
/**
* Delete the item.
*
* @since 1.0.0-beta.1
*
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error
*/
public function delete_item( $request ) {
$object = $this->get_object( $request['id'], false );
// We don't return 404s for items that are not found.
if ( ! is_wp_error( $object ) ) {
// If there was an error deleting the object return the error. If the error is that the object doesn't exist return 204 below!
$del = $this->delete_object( $object, $request );
if ( is_wp_error( $del ) ) {
return $del;
}
}
$response = rest_ensure_response( null );
$response->set_status( 204 );
return $response;
}
/**
* Retrieves the query params for the objects collection.
*
* @since 1.0.0-beta.1
* @since 1.0.0-beta.12 Added `search_columns` collection param for searchable resources.
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$query_params['context']['default'] = 'view';
// We're not currently implementing searching for all of our controllers.
if ( empty( $this->is_searchable ) ) {
unset( $query_params['search'] );
} elseif ( ! empty( $this->search_columns_mapping ) ) {
$search_columns = array_keys( $this->search_columns_mapping );
$query_params['search_columns'] = array(
'description' => __( 'Column names to be searched. Accepts a single column or a comma separated list of columns.', 'lifterlms' ),
'type' => 'array',
'items' => array(
'type' => 'string',
'enum' => $search_columns,
),
'default' => $search_columns,
);
}
// page and per_page params are already specified in WP_Rest_Controller->get_collection_params().
$query_params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'lifterlms' ),
'type' => 'string',
'default' => 'asc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$query_params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'lifterlms' ),
'type' => 'string',
'default' => $this->orderby_properties[0],
'enum' => $this->orderby_properties,
'validate_callback' => 'rest_validate_request_arg',
);
$query_params['include'] = array(
'description' => __( 'Limit results to a list of ids. Accepts a single id or a comma separated list of ids.', 'lifterlms' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'validate_callback' => 'rest_validate_request_arg',
);
$query_params['exclude'] = array(
'description' => __( 'Exclude a list of ids from results. Accepts a single id or a comma separated list of ids.', 'lifterlms' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'validate_callback' => 'rest_validate_request_arg',
);
return $query_params;
}
/**
* Get a single item.
*
* @since 1.0.0-beta.1
* @since 1.0.0-beta.27 Don't call `rest_ensure_response()` twice, already called in `$this->prepare_item_for_response()`.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$object = $this->get_object( (int) $request['id'] );
if ( is_wp_error( $object ) ) {
return $object;
}
$response = $this->prepare_item_for_response( $object, $request );
return $response;
}
/**
* Retrieves all items.
*
* @since 1.0.0-beta.1
* @since 1.0.0-beta.3 Fix an issue displaying a last page for lists with 0 possible results.
* @since 1.0.0-beta.7 Broken into several methods so to improve abstraction.
* @since 1.0.0-beta.12 Return early if `prepare_collection_query_args()` is a `WP_Error`.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$prepared = $this->prepare_collection_query_args( $request );
if ( is_wp_error( $prepared ) ) {
return $prepared;
}
$query = $this->get_objects_query( $prepared, $request );
$pagination = $this->get_pagination_data_from_query( $query, $prepared, $request );
// Out-of-bounds, run the query again on page one to get a proper total count.
if ( $pagination['total_results'] < 1 ) {
$prepared_for_total_count = $this->prepare_args_for_total_count_query( $prepared, $request );
$count_query = $this->get_objects_query( $prepared_for_total_count, $request );
$count_results = $this->get_pagination_data_from_query( $count_query, $prepared_for_total_count, $request );
$pagination['total_results'] = $count_results['total_results'];
}
if ( $pagination['current_page'] > $pagination['total_pages'] && $pagination['total_results'] > 0 ) {
return llms_rest_bad_request_error( __( 'The page number requested is larger than the number of pages available.', 'lifterlms' ) );
}
$objects = $this->get_objects_from_query( $query );
$items = $this->prepare_collection_items_for_response( $objects, $request );
$response = rest_ensure_response( $items );
$response = $this->add_header_pagination( $response, $pagination, $request );
return $response;
}
/**
* Format query arguments to retrieve a collection of objects.
*
* @since 1.0.0-beta.7
* @since 1.0.0-beta.12 Prepare args for search and call collection params to query args map method.
*
* @param WP_REST_Request $request Full details about the request.
* @return array|WP_Error
*/
protected function prepare_collection_query_args( $request ) {
// Prepare all set args.
$registered = $this->get_collection_params();
$prepared = array();
foreach ( $registered as $key => $value ) {
if ( isset( $request[ $key ] ) ) {
$prepared[ $key ] = $request[ $key ];
}
}
$prepared = $this->prepare_collection_query_search_args( $prepared, $request );
if ( is_wp_error( $prepared ) ) {
return $prepared;
}
$prepared = $this->map_params_to_query_args( $prepared, $registered, $request );
return $prepared;
}
/**
* Map schema to query arguments to retrieve a collection of objects.
*
* @since 1.0.0-beta.12
*
* @param array $prepared Array of collection arguments.
* @param array $registered Registered collection params.
* @param WP_REST_Request $request Full details about the request.
* @return array|WP_Error
*/
protected function map_params_to_query_args( $prepared, $registered, $request ) {
return $prepared;
}
/**
* Format search query arguments to retrieve a collection of objects.
*
* @since 1.0.0-beta.12
* @since 1.0.0-beta.21 Return an error if requesting a list ordered by 'relevance' without providing a search string.
*
* @param array $prepared Array of collection arguments.
* @param WP_REST_Request $request Request object.
* @return array|WP_Error
*/
protected function prepare_collection_query_search_args( $prepared, $request ) {
// Search?
if ( ! empty( $prepared['search'] ) ) {
if ( ! empty( $this->search_columns_mapping ) ) {
if ( empty( $prepared['search_columns'] ) ) {
return llms_rest_bad_request_error( __( 'You must provide a valid set of columns to search into.', 'lifterlms' ) );
}
// Filter search columns by context.
$search_columns = array_keys( $this->filter_response_by_context( array_flip( $prepared['search_columns'] ), $request['context'] ) );
// Check if one of more unallowed search columns have been provided as request query params (not merged with defaults).
if ( ! empty( $request->get_query_params()['search_columns'] ) ) {
$forbidden_columns = array_diff( $prepared['search_columns'], $search_columns );
if ( ! empty( $forbidden_columns ) ) {
return llms_rest_authorization_required_error(
sprintf(
// Translators: %1$s comma separated list of search columns.
__( 'You are not allowed to search into the provided column(s): %1$s', 'lifterlms' ),
implode( ',', $forbidden_columns )
)
);
}
}
$prepared['search_columns'] = array();
// Map our search columns into query compatible ones.
foreach ( $search_columns as $search_column ) {
if ( isset( $this->search_columns_mapping[ $search_column ] ) ) {
$prepared['search_columns'][] = $this->search_columns_mapping[ $search_column ];
}
}
if ( empty( $prepared['search_columns'] ) ) {
return llms_rest_bad_request_error( __( 'You must provide a valid set of columns to search into.', 'lifterlms' ) );
}
}
$prepared['search'] = '*' . $prepared['search'] . '*';
} else {
// Ensure a search string is set in case the orderby is set to 'relevance'.
if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] ) {
return llms_rest_bad_request_error(
__( 'You need to define a search term to order by relevance.', 'lifterlms' )
);
}
}
return $prepared;
}
/**
* Prepare query args for total count query.
*
* @since 1.0.0-beta.7
*
* @param array $args Array of query args.
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
protected function prepare_args_for_total_count_query( $args, $request ) {
// Run the query again without pagination to get a proper total count.
unset( $args['paged'], $args['page'] );
return $args;
}
/**
* Prepare collection items for response.
*
* @since 1.0.0-beta.7
*
* @param array $objects Array of objects to be prepared for response.
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
protected function prepare_collection_items_for_response( $objects, $request ) {
$items = array();
foreach ( $objects as $object ) {
$object = $this->get_object( $object, false );
if ( ! $this->check_read_object_permissions( $object ) ) {
continue;
}
$item = $this->prepare_item_for_response( $object, $request );
if ( ! is_wp_error( $item ) ) {
$items[] = $this->prepare_response_for_collection( $item );
}
}
return $items;
}
/**
* Add pagination info and links to the response header.
*
* @since 1.0.0-beta.7
*
* @param WP_REST_Response $response Current response being served.
* @param array $pagination Pagination array.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response
*/
protected function add_header_pagination( $response, $pagination, $request ) {
$response->header( 'X-WP-Total', $pagination['total_results'] );
$response->header( 'X-WP-TotalPages', $pagination['total_pages'] );
$base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( $request->get_route() ) );
// First page link.
if ( 1 !== $pagination['current_page'] ) {
$first_link = add_query_arg( 'page', 1, $base );
$response->link_header( 'first', $first_link );
}
// Previous page link.
if ( $pagination['current_page'] > 1 ) {
$prev_page = $pagination['current_page'] - 1;
if ( $prev_page > $pagination['total_pages'] ) {
$prev_page = $pagination['total_pages'];
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
// Next page link.
if ( $pagination['total_pages'] > $pagination['current_page'] ) {
$next_link = add_query_arg( 'page', $pagination['current_page'] + 1, $base );
$response->link_header( 'next', $next_link );
}
// Last page link.
if ( $pagination['total_pages'] && $pagination['total_pages'] !== $pagination['current_page'] ) {
$last_link = add_query_arg( 'page', $pagination['total_pages'], $base );
$response->link_header( 'last', $last_link );
}
return $response;
}
/**
* Retrieves the query params for retrieving a single resource.
*
* @since 1.0.0-beta.1
*
* @return array
*/
public function get_get_item_params() {
return array(
'context' => $this->get_context_param(
array(
'default' => 'view',
)
),
);
}
/**
* Retrieve arguments for deleting a resource.
*
* @since 1.0.0-beta.1
*
* @return array
*/
public function get_delete_item_args() {
return array();
}
/**
* Map request keys to database keys for insertion.
*
* Array keys are the request fields (as defined in the schema) and
* array values are the database fields.
*
* @since 1.0.0-beta.1
*
* @return array
*/
protected function map_schema_to_database() {
$schema = $this->get_item_schema();
$keys = array_keys( $schema['properties'] );
return array_combine( $keys, $keys );
}
/**
* Get the LLMS REST resource schema, conforming to JSON Schema.
*
* @since 1.0.0-beta.27
*
* @return array
*/
public function get_item_schema() {
if ( isset( $this->schema ) ) {
// Additional fields are not cached in the schema, @see https://core.trac.wordpress.org/ticket/47871#comment:5.
return $this->add_additional_fields_schema( $this->schema );
}
$schema = $this->get_item_schema_base();
// Add custom fields registered via `register_meta()`.
$schema = $this->add_meta_fields_schema( $schema );
// Allow the schema to be filtered.
$schema = $this->filter_item_schema( $schema );
/**
* Set `$this->schema` here so that we can exclude additional fields
* already covered by the base, filtered, schema.
*
* Additional fields are added through the call to add_additional_fields_schema() below,
* which will call {@see LLMS_REST_Controller::get_additional_fields()}, which requires
* `$this->schema` to be set.
*/
$this->schema = $schema;
/**
* Adds the schema from additional fields (registered via `register_rest_field()`) to the schema array.
* Note: WordPress core doesn't cache the additional fields in the schema, see https://core.trac.wordpress.org/ticket/47871#comment:5
*/
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Add custom fields registered via `register_meta()`.
*
* @since 1.0.0-beta.27
*
* @param array $schema The resource item schema.
* @return array
*/
protected function add_meta_fields_schema( $schema ) {
if ( ! empty( $this->meta ) ) {
$schema['properties']['meta'] = $this->meta->get_field_schema();
$schema['properties']['meta']['properties'] = $this->exclude_disallowed_meta_fields(
$schema['properties']['meta']['properties'],
$schema
);
}
return $schema;
}
/**
* Retrieves all of the registered additional fields for a given object-type.
*
* Overrides wp core `get_additional_fields()` to allow excluding fields.
*
* @since 1.0.0-beta.27
*
* @param string $object_type The object type.
* @return array Registered additional fields (if any), empty array if none or if the object type could
* not be inferred.
*/
protected function get_additional_fields( $object_type = null ) {
// We require the `$this->schema['properties']` to be set in order to exclude additional fields already covered by our schema definition.
if ( ! isset( $this->schema['properties'] ) ) {
return parent::get_additional_fields( $object_type );
}
$additional_fields = parent::get_additional_fields( $object_type );
if ( ! empty( $additional_fields ) ) {
/**
* Filters the disallowed additional fields.
*
* The dynamic portion of the hook name, `$object_type`, refers the object type this controller is responsible for managing.
*
* @since 1.0.0-beta.27
*
* @param string[] $disallowed_additional_fields Additional rest field names to skip (added via `register_rest_field()`).
*/
$disallowed_fields = apply_filters( "llms_rest_{$object_type}_disallowed_additional_fields", $this->disallowed_additional_fields );
/**
* Exclude:
* - disallowed fields defined in the instance's property `disallowed_additional_fields`.
* - additional fields already covered in the schema.
*
* This is meant to run only once, because otherwise we have no way
* to determine whether the property comes from the original schema
* definition, or has been added via `register_rest_field()`.
*/
$additional_fields = array_diff_key(
$additional_fields,
array_flip( array_keys( $this->schema['properties'] ) ),
array_flip( $disallowed_fields )
);
}
return $additional_fields;
}
/**
* Exclude disallowed meta fields.
*
* @since 1.0.0-beta.27
*
* @param array $meta Array of meta fields.
* @param array $schema The resource item schema. Falls back on the defined schema if not supplied.
* @return array
*/
protected function exclude_disallowed_meta_fields( $meta, $schema = array() ) {
$schema = empty( $schema ) ? $this->schema : $schema;
if ( empty( $schema ) || empty( $meta ) ) {
return $meta;
}
$object_type = $this->get_object_type( $schema );
/**
* Filters the disallowed meta fields.
*
* The dynamic portion of the hook name, `$object_type`, refers the object type this controller is responsible for managing.
*
* @since 1.0.0-beta.27
*
* @param string[] $disallowed_meta_fields Meta field names to skip (added via `register_meta()`).
*/
$disallowed_meta_fields = apply_filters( "llms_rest_{$object_type}_disallowed_meta_fields", $this->disallowed_meta_fields );
$meta = array_diff_key(
$meta,
array_flip( $disallowed_meta_fields )
);
return $meta;
}
/**
* Get the LLMS REST resource schema base properties, conforming to JSON Schema.
*
* @since 1.0.0-beta.27
*
* @return array
*/
protected function get_item_schema_base() {
return array();
}
/**
* Filter the item schema.
*
* @since 1.0.0-beta.27
*
* @param array $schema The resource item schema.
* @return array
*/
protected function filter_item_schema( $schema ) {
$object_type = $this->get_object_type( $schema );
$unfiltered_schema = $schema;
/**
* Filters the item schema.
*
* The dynamic portion of the hook name, `$object_type`, refers the object type this controller is responsible for managing.
*
* @since 1.0.0-beta.27
*
* @param array $schema The item schema.
*/
$schema = apply_filters( "llms_rest_{$object_type}_item_schema", $schema );
/**
* Filters whether to allow filtering the item schema to add fields.
*
* The dynamic portion of the hook name, `$object_type`, refers the object type this controller is responsible for managing.
*
* @since 1.0.0-beta.27
*
* @param bool $allow Whether to allow filtering the item schema to add fields.
* @param array $schema The item schema.
*/
if ( ! apply_filters( "llms_rest_allow_filtering_{$object_type}_item_schema_to_add_fields", false, $schema ) ) {
// Emit a _doing_it_wrong warning if user tries to add new properties using this filter.
$new_fields = array_diff_key( $schema['properties'], $unfiltered_schema['properties'] );
if ( count( $new_fields ) > 0 ) {
_doing_it_wrong(
esc_html( "llms_rest_{$object_type}_item_schema" ),
esc_html(
sprintf(
/* translators: %s: register_rest_field */
__( 'Please use %s to add new schema properties.', 'lifterlms' ),
'register_rest_field()'
)
),
'[version]'
);
}
}
return $schema;
}
/**
* Retrieves the resource object-type this controller is responsible for managing.
*
* Overrides wp core `get_object_type()` to allow passing an item schema to retrieve the type from.
*
* @since 1.0.0-beta.27
*
* @param null|array $schema Optional. The item schema. Default `null`.
* @return string
*/
protected function get_object_type( $schema = null ) {
if ( empty( $schema ) ) {
return parent::get_object_type();
}
return $schema['title'];
}
/**
* Prepare request arguments for a database insert/update.
*
* @since 1.0.0-beta.1
*
* @param WP_Rest_Request $request Request object.
* @return array
*/
protected function prepare_item_for_database( $request ) {
$prepared = array();
$map = $this->map_schema_to_database();
$schema = $this->get_item_schema();
foreach ( $map as $req_key => $db_key ) {
if ( ! empty( $request[ $req_key ] ) ) {
$prepared[ $db_key ] = $request[ $req_key ];
}
}
return $prepared;
}
/**
* Prepares a single object for response.
*
* @since 1.0.0-beta.1
* @since 1.0.0-beta.3 Return early with a WP_Error if `$object` is a WP_Error.
* @since 1.0.0-beta.14 Pass the `$request` parameter to `prepare_links()`.
* @since 1.0.0-beta.27 Move big part of the logic to the new method `LLMS_REST_Controller_Stubs::prepare_object_data_for_response()`.
*
* @param obj $object Raw object from database.
* @param WP_REST_Request $request Request object.
* @return WP_Error|WP_REST_Response
*/
public function prepare_item_for_response( $object, $request ) {
if ( is_wp_error( $object ) ) {
return $object;
}
$data = $this->prepare_object_data_for_response( $object, $request );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
// Add links.
$response->add_links( $this->prepare_links( $object, $request ) );
return $response;
}
/**
* Prepare links for the request.
*
* @since 1.0.0-beta.1
* @since 1.0.0-beta.14 Added $request parameter.
*
* @param obj $object Item object.
* @param WP_REST_Request $request Request object.
* @return array
*/
protected function prepare_links( $object, $request ) {
$base = rest_url( sprintf( '/%1$s/%2$s', $this->namespace, $this->rest_base ) );
$links = array(
'self' => array(
'href' => sprintf( '%1$s/%2$d', $base, $this->get_object_id( $object ) ),
),
'collection' => array(
'href' => $base,
),
);
return $links;
}
/**
* Register routes.
*
* @since 1.0.0-beta.1
*
* @return void
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'lifterlms' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => $this->get_get_item_params(),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), // see class-wp-rest-controller.php.
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => $this->get_delete_item_args(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Update item.
*
* @since 1.0.0-beta.1
* @since 1.0.0-beta.12 Call `object_inserted` and `object_completely_inserted` after an object is
* respectively inserted in the DB and all its additional fields have been
* updated as well (completely inserted).
* @since 1.0.0-beta.27 Handle custom meta registered via `register_meta()` and custom rest fields registered via `register_rest_field()`.
*
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object or WP_Error on failure.
*/
public function update_item( $request ) {
$object = $this->get_object( $request['id'] );
if ( is_wp_error( $object ) ) {
return $object;
}
$schema = $this->get_item_schema();
$item = $this->prepare_item_for_database( $request );
// Exclude additional fields registered via `register_rest_field()`.
$item = array_diff_key( $item, $this->get_additional_fields() );
$object = $this->update_object( $item, $request );
if ( is_wp_error( $object ) ) {
return $object;
}
$this->object_inserted( $object, $request, $schema, false );
// Registered via `register_meta()`.
$meta_update = $this->update_meta( $object, $request, $schema );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
// Registered via `register_rest_field()`.
$fields_update = $this->update_additional_fields_for_object( $object, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$this->object_completely_inserted( $object, $request, $schema, false );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $object, $request );
return $response;
}
/**
* Update meta.
*
* @since 1.0.0-beta.27
*
* @param object $object Inserted or updated object.
* @param WP_REST_Request $request Request object.
* @param array $schema The item schema.
* @return void|WP_Error
*/
protected function update_meta( $object, $request, $schema ) {
if ( empty( $schema['properties']['meta'] ) || empty( $request['meta'] ) ) {
return;
}
$request['meta'] = $this->exclude_disallowed_meta_fields( $request['meta'] );
if ( empty( $request['meta'] ) ) {
return;
}
$meta_update = $this->meta->update_value( $request['meta'], $this->get_object_id( $object ) );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
}