REST API: Introduce the Content API endpoints.

REST API endpoints for your WordPress content. These endpoints provide machine-readable external access to your WordPress site with a clear, standards-driven interface, allowing new and innovative apps for interacting with your site. These endpoints support all of the following:
- Posts: Read and write access to all post data, for all types of post-based data, including pages and media.
- Comments: Read and write access to all comment data. This includes pingbacks and trackbacks.
- Terms: Read and write access to all term data.
- Users: Read and write access to all user data. This includes public access to some data for post authors.
- Meta: Read and write access to metadata for posts, comments, terms, and users, on an opt-in basis from plugins.
- Settings: Read and write access to settings, on an opt-in basis from plugins and core. This enables API management of key site content values that are technically stored in options, such as site title and byline.

Love your REST API, WordPress!  The infrastructure says, "Let's do lunch!" but the content API endpoints say, "You're paying!"

Props rmccue, rachelbaker, danielbachhuber, joehoyle, adamsilverstein, afurculita, ahmadawais, airesvsg, alisspers, antisilent, apokalyptik, artoliukkonen, attitude, boonebgorges, bradyvercher, brianhogg, caseypatrickdriscoll, chopinbach, chredd, christianesperar, chrisvanpatten, claudiolabarbera, claudiosmweb, cmmarslender, codebykat, coderkevin, codfish, codonnell822, daggerhart, danielpunkass, davidbhayes, delphinus, desrosj, dimadin, dotancohen, DrewAPicture, Dudo1985, duncanjbrown, eherman24, eivhyl, eliorivero, elyobo, en-alis, ericandrewlewis, ericpedia, evansobkowicz, fjarrett, frozzare, georgestephanis, greatislander, guavaworks, hideokamoto, hkdobrev, hubdotcom, hurtige, iandunn, ircrash, ironpaperweight, iseulde, Japh, jaredcobb, JDGrimes, jdolan, jdoubleu, jeremyfelt, jimt, jjeaton, jmusal, jnylen0, johanmynhardt, johnbillion, jonathanbardo, jorbin, joshkadis, JPry, jshreve, jtsternberg, JustinSainton, kacperszurek, kadamwhite, kalenjohnson, kellbot, kjbenk, kokarn, krogsgard, kuchenundkakao, kuldipem, kwight, lgedeon, lukepettway, mantismamita, markoheijnen, matrixik, mattheu, mauteri, maxcutler, mayukojpn, michael-arestad, miyauchi, mjbanks, modemlooper, mrbobbybryant, NateWr, nathanrice, netweb, NikV, nullvariable, oskosk, oso96_2000, oxymoron, pcfreak30, pento, peterwilsoncc, Pezzab, phh, pippinsplugins, pjgalbraith, pkevan, pollyplummer, pushred, quasel, QWp6t, schlessera, schrapel, Shelob9, shprink, simonlampen, Soean, solal, tapsboy, tfrommen, tharsheblows, thenbrent, tierra, tlovett1, tnegri, tobych, Toddses, toro_unit, traversal, vanillalounge, vishalkakadiya, wanecek, web2style, webbgaraget, websupporter, westonruter, whyisjake, wonderboymusic, wpsmith, xknown, zyphonic.
Fixes #38373.
Built from https://develop.svn.wordpress.org/trunk@38832


git-svn-id: http://core.svn.wordpress.org/trunk@38775 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Rachel Baker 2016-10-20 02:55:32 +00:00
parent f5a3446e4d
commit e4a7c0a397
27 changed files with 10058 additions and 2 deletions

View File

@ -374,7 +374,9 @@ add_action( 'edit_user_created_user', 'wp_send_new_user_notifications', 10, 2 );
// REST API actions.
add_action( 'init', 'rest_api_init' );
add_action( 'rest_api_init', 'rest_api_default_filters', 10, 1 );
add_action( 'rest_api_init', 'rest_api_default_filters', 10, 1 );
add_action( 'rest_api_init', 'register_initial_settings', 10 );
add_action( 'rest_api_init', 'create_initial_rest_routes', 99 );
add_action( 'parse_request', 'rest_api_loaded' );
/**

View File

@ -3429,6 +3429,26 @@ function wp_parse_id_list( $list ) {
return array_unique(array_map('absint', $list));
}
/**
* Clean up an array, comma- or space-separated list of slugs.
*
* @since 4.7.0
*
* @param array|string $list List of slugs.
* @return array Sanitized array of slugs.
*/
function wp_parse_slug_list( $list ) {
if ( ! is_array( $list ) ) {
$list = preg_split( '/[\s,]+/', $list );
}
foreach ( $list as $key => $value ) {
$list[ $key ] = sanitize_title( $value );
}
return array_unique( $list );
}
/**
* Extract a slice of an array, given a list of keys.
*

1337
wp-includes/js/wp-api.js Normal file

File diff suppressed because it is too large Load Diff

1
wp-includes/js/wp-api.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1707,6 +1707,115 @@ function set_site_transient( $transient, $value, $expiration = 0 ) {
return $result;
}
/**
* Register default settings available in WordPress.
*
* The settings registered here are primarily useful for the REST API, so this
* does not encompass all settings available in WordPress.
*
* @since 4.7.0
*/
function register_initial_settings() {
register_setting( 'general', 'blogname', array(
'show_in_rest' => array(
'name' => 'title',
),
'type' => 'string',
'description' => __( 'Site title.' ),
) );
register_setting( 'general', 'blogdescription', array(
'show_in_rest' => array(
'name' => 'description',
),
'type' => 'string',
'description' => __( 'Site description.' ),
) );
register_setting( 'general', 'siteurl', array(
'show_in_rest' => array(
'name' => 'url',
'schema' => array(
'format' => 'uri',
),
),
'type' => 'string',
'description' => __( 'Site URL.' ),
) );
register_setting( 'general', 'admin_email', array(
'show_in_rest' => array(
'name' => 'email',
'schema' => array(
'format' => 'email',
),
),
'type' => 'string',
'description' => __( 'This address is used for admin purposes. If you change this we will send you an email at your new address to confirm it. The new address will not become active until confirmed.' ),
) );
register_setting( 'general', 'timezone_string', array(
'show_in_rest' => array(
'name' => 'timezone',
),
'type' => 'string',
'description' => __( 'A city in the same timezone as you.' ),
) );
register_setting( 'general', 'date_format', array(
'show_in_rest' => true,
'type' => 'string',
'description' => __( 'A date format for all date strings.' ),
) );
register_setting( 'general', 'time_format', array(
'show_in_rest' => true,
'type' => 'string',
'description' => __( 'A time format for all time strings.' ),
) );
register_setting( 'general', 'start_of_week', array(
'show_in_rest' => true,
'type' => 'number',
'description' => __( 'A day number of the week that the week should start on.' ),
) );
register_setting( 'general', 'WPLANG', array(
'show_in_rest' => array(
'name' => 'language',
),
'type' => 'string',
'description' => __( 'WordPress locale code.' ),
'default' => 'en_US',
) );
register_setting( 'writing', 'use_smilies', array(
'show_in_rest' => true,
'type' => 'boolean',
'description' => __( 'Convert emoticons like :-) and :-P to graphics on display.' ),
'default' => true,
) );
register_setting( 'writing', 'default_category', array(
'show_in_rest' => true,
'type' => 'number',
'description' => __( 'Default category.' ),
) );
register_setting( 'writing', 'default_post_format', array(
'show_in_rest' => true,
'type' => 'string',
'description' => __( 'Default post format.' ),
) );
register_setting( 'reading', 'posts_per_page', array(
'show_in_rest' => true,
'type' => 'number',
'description' => __( 'Blog pages show at most.' ),
'default' => 10,
) );
}
/**
* Register a setting and its data.
*

View File

@ -33,6 +33,9 @@ function create_initial_post_types() {
'query_var' => false,
'delete_with_user' => true,
'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'trackbacks', 'custom-fields', 'comments', 'revisions', 'post-formats' ),
'show_in_rest' => true,
'rest_base' => 'posts',
'rest_controller_class' => 'WP_REST_Posts_Controller',
) );
register_post_type( 'page', array(
@ -51,6 +54,9 @@ function create_initial_post_types() {
'query_var' => false,
'delete_with_user' => true,
'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'page-attributes', 'custom-fields', 'comments', 'revisions' ),
'show_in_rest' => true,
'rest_base' => 'pages',
'rest_controller_class' => 'WP_REST_Posts_Controller',
) );
register_post_type( 'attachment', array(
@ -76,6 +82,9 @@ function create_initial_post_types() {
'show_in_nav_menus' => false,
'delete_with_user' => true,
'supports' => array( 'title', 'author', 'comments' ),
'show_in_rest' => true,
'rest_base' => 'media',
'rest_controller_class' => 'WP_REST_Attachments_Controller',
) );
add_post_type_support( 'attachment:audio', 'thumbnail' );
add_post_type_support( 'attachment:video', 'thumbnail' );

View File

@ -70,6 +70,48 @@ function register_rest_route( $namespace, $route, $args = array(), $override = f
return true;
}
/**
* Registers a new field on an existing WordPress object type.
*
* @since 4.7.0
*
* @global array $wp_rest_additional_fields Holds registered fields, organized
* by object type.
*
* @param string|array $object_type Object(s) the field is being registered
* to, "post"|"term"|"comment" etc.
* @param string $attribute The attribute name.
* @param array $args {
* Optional. An array of arguments used to handle the registered field.
*
* @type string|array|null $get_callback Optional. The callback function used to retrieve the field
* value. Default is 'null', the field will not be returned in
* the response.
* @type string|array|null $update_callback Optional. The callback function used to set and update the
* field value. Default is 'null', the value cannot be set or
* updated.
* @type string|array|null $schema Optional. The callback function used to create the schema for
* this field. Default is 'null', no schema entry will be returned.
* }
*/
function register_rest_field( $object_type, $attribute, $args = array() ) {
$defaults = array(
'get_callback' => null,
'update_callback' => null,
'schema' => null,
);
$args = wp_parse_args( $args, $defaults );
global $wp_rest_additional_fields;
$object_types = (array) $object_type;
foreach ( $object_types as $object_type ) {
$wp_rest_additional_fields[ $object_type ][ $attribute ] = $args;
}
}
/**
* Registers rewrite rules for the API.
*
@ -124,6 +166,71 @@ function rest_api_default_filters() {
add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 );
}
/**
* Registers default REST API routes.
*
* @since 4.7.0
*/
function create_initial_rest_routes() {
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
$class = ! empty( $post_type->rest_controller_class ) ? $post_type->rest_controller_class : 'WP_REST_Posts_Controller';
if ( ! class_exists( $class ) ) {
continue;
}
$controller = new $class( $post_type->name );
if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
continue;
}
$controller->register_routes();
if ( post_type_supports( $post_type->name, 'revisions' ) ) {
$revisions_controller = new WP_REST_Revisions_Controller( $post_type->name );
$revisions_controller->register_routes();
}
}
// Post types.
$controller = new WP_REST_Post_Types_Controller;
$controller->register_routes();
// Post statuses.
$controller = new WP_REST_Post_Statuses_Controller;
$controller->register_routes();
// Taxonomies.
$controller = new WP_REST_Taxonomies_Controller;
$controller->register_routes();
// Terms.
foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
$class = ! empty( $taxonomy->rest_controller_class ) ? $taxonomy->rest_controller_class : 'WP_REST_Terms_Controller';
if ( ! class_exists( $class ) ) {
continue;
}
$controller = new $class( $taxonomy->name );
if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
continue;
}
$controller->register_routes();
}
// Users.
$controller = new WP_REST_Users_Controller;
$controller->register_routes();
// Comments.
$controller = new WP_REST_Comments_Controller;
$controller->register_routes();
// Settings.
$controller = new WP_REST_Settings_Controller;
$controller->register_routes();
}
/**
* Loads the REST API.
*
@ -683,3 +790,296 @@ function rest_get_date_with_gmt( $date, $force_utc = false ) {
return array( $local, $utc );
}
/**
* Returns a contextual HTTP error code for authorization failure.
*
* @since 4.7.0
*
* @return integer 401 if the user is not logged in, 403 if the user is logged in.
*/
function rest_authorization_required_code() {
return is_user_logged_in() ? 403 : 401;
}
/**
* Validate a request argument based on details registered to the route.
*
* @since 4.7.0
*
* @param mixed $value
* @param WP_REST_Request $request
* @param string $param
* @return WP_Error|boolean
*/
function rest_validate_request_arg( $value, $request, $param ) {
$attributes = $request->get_attributes();
if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
return true;
}
$args = $attributes['args'][ $param ];
if ( ! empty( $args['enum'] ) ) {
if ( ! in_array( $value, $args['enum'], true ) ) {
return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: list of valid values */ __( '%1$s is not one of %2$s.' ), $param, implode( ', ', $args['enum'] ) ) );
}
}
if ( 'integer' === $args['type'] && ! is_numeric( $value ) ) {
return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'integer' ) );
}
if ( 'boolean' === $args['type'] && ! rest_is_boolean( $value ) ) {
return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $value, 'boolean' ) );
}
if ( 'string' === $args['type'] && ! is_string( $value ) ) {
return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: type name */ __( '%1$s is not of type %2$s.' ), $param, 'string' ) );
}
if ( isset( $args['format'] ) ) {
switch ( $args['format'] ) {
case 'date-time' :
if ( ! rest_parse_date( $value ) ) {
return new WP_Error( 'rest_invalid_date', __( 'The date you provided is invalid.' ) );
}
break;
case 'email' :
if ( ! is_email( $value ) ) {
return new WP_Error( 'rest_invalid_email', __( 'The email address you provided is invalid.' ) );
}
break;
case 'ipv4' :
if ( ! rest_is_ip_address( $value ) ) {
return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not a valid IP address.' ), $value ) );
}
break;
}
}
if ( in_array( $args['type'], array( 'numeric', 'integer' ), true ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) {
if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) {
if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) {
return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (exclusive)' ), $param, $args['minimum'] ) );
} elseif ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) {
return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d (inclusive)' ), $param, $args['minimum'] ) );
}
} elseif ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) {
if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) {
return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (exclusive)' ), $param, $args['maximum'] ) );
} elseif ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) {
return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d (inclusive)' ), $param, $args['maximum'] ) );
}
} elseif ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) {
if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) {
return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: minimum number, 3: maximum number */ __( '%1$s must be between %2$d (exclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
}
} elseif ( empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
if ( $value >= $args['maximum'] || $value < $args['minimum'] ) {
return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: minimum number, 3: maximum number */ __( '%1$s must be between %2$d (inclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
}
} elseif ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
if ( $value > $args['maximum'] || $value <= $args['minimum'] ) {
return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: minimum number, 3: maximum number */ __( '%1$s must be between %2$d (exclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
}
} elseif ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
if ( $value > $args['maximum'] || $value < $args['minimum'] ) {
return new WP_Error( 'rest_invalid_param', sprintf( /* translators: 1: parameter, 2: minimum number, 3: maximum number */ __( '%1$s must be between %2$d (inclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
}
}
}
}
return true;
}
/**
* Sanitize a request argument based on details registered to the route.
*
* @since 4.7.0
*
* @param mixed $value
* @param WP_REST_Request $request
* @param string $param
* @return mixed
*/
function rest_sanitize_request_arg( $value, $request, $param ) {
$attributes = $request->get_attributes();
if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
return $value;
}
$args = $attributes['args'][ $param ];
if ( 'integer' === $args['type'] ) {
return (int) $value;
}
if ( 'boolean' === $args['type'] ) {
return rest_sanitize_boolean( $value );
}
if ( isset( $args['format'] ) ) {
switch ( $args['format'] ) {
case 'date-time' :
return sanitize_text_field( $value );
case 'email' :
/*
* sanitize_email() validates, which would be unexpected
*/
return sanitize_text_field( $value );
case 'uri' :
return esc_url_raw( $value );
case 'ipv4' :
return sanitize_text_field( $value );
}
}
return $value;
}
/**
* Parse a request argument based on details registered to the route.
*
* Runs a validation check and sanitizes the value, primarily to be used via
* the `sanitize_callback` arguments in the endpoint args registration.
*
* @since 4.7.0
*
* @param mixed $value
* @param WP_REST_Request $request
* @param string $param
* @return mixed
*/
function rest_parse_request_arg( $value, $request, $param ) {
$is_valid = rest_validate_request_arg( $value, $request, $param );
if ( is_wp_error( $is_valid ) ) {
return $is_valid;
}
$value = rest_sanitize_request_arg( $value, $request, $param );
return $value;
}
/**
* Determines if a IPv4 address is valid.
*
* Does not handle IPv6 addresses.
*
* @since 4.7.0
*
* @param string $ipv4 IP 32-bit address.
* @return string|false The valid IPv4 address, otherwise false.
*/
function rest_is_ip_address( $ipv4 ) {
$pattern = '/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/';
if ( ! preg_match( $pattern, $ipv4 ) ) {
return false;
}
return $ipv4;
}
/**
* Changes a boolean-like value into the proper boolean value.
*
* @since 4.7.0
*
* @param bool|string|int $value The value being evaluated.
* @return boolean Returns the proper associated boolean value.
*/
function rest_sanitize_boolean( $value ) {
// String values are translated to `true`; make sure 'false' is false.
if ( is_string( $value ) ) {
$value = strtolower( $value );
if ( in_array( $value, array( 'false', '0' ), true ) ) {
$value = false;
}
}
// Everything else will map nicely to boolean.
return (boolean) $value;
}
/**
* Determines if a given value is boolean-like.
*
* @since 4.7.0
*
* @param bool|string $maybe_bool The value being evaluated.
* @return boolean True if a boolean, otherwise false.
*/
function rest_is_boolean( $maybe_bool ) {
if ( is_bool( $maybe_bool ) ) {
return true;
}
if ( is_string( $maybe_bool ) ) {
$maybe_bool = strtolower( $maybe_bool );
$valid_boolean_values = array(
'false',
'true',
'0',
'1',
);
return in_array( $maybe_bool, $valid_boolean_values, true );
}
if ( is_int( $maybe_bool ) ) {
return in_array( $maybe_bool, array( 0, 1 ), true );
}
return false;
}
/**
* Retrieves the avatar urls in various sizes based on a given email address.
*
* @since 4.7.0
*
* @see get_avatar_url()
*
* @param string $email Email address.
* @return array $urls Gravatar url for each size.
*/
function rest_get_avatar_urls( $email ) {
$avatar_sizes = rest_get_avatar_sizes();
$urls = array();
foreach ( $avatar_sizes as $size ) {
$urls[ $size ] = get_avatar_url( $email, array( 'size' => $size ) );
}
return $urls;
}
/**
* Retrieves the pixel sizes for avatars.
*
* @since 4.7.0
*
* @return array List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`.
*/
function rest_get_avatar_sizes() {
/**
* Filter the REST avatar sizes.
*
* Use this filter to adjust the array of sizes returned by the
* `rest_get_avatar_sizes` function.
*
* @since 4.4.0
*
* @param array $sizes An array of int values that are the pixel sizes for avatars.
* Default `[ 24, 48, 96 ]`.
*/
return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) );
}

View File

@ -0,0 +1,607 @@
<?php
class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
/**
* Determine the allowed query_vars for a get_items() response and
* prepare for WP_Query.
*
* @param array $prepared_args Optional. Array of prepared arguments.
* @param WP_REST_Request $request Optional. Request to prepare items for.
* @return array Array of query arguments.
*/
protected function prepare_items_query( $prepared_args = array(), $request = null ) {
$query_args = parent::prepare_items_query( $prepared_args, $request );
if ( empty( $query_args['post_status'] ) || ! in_array( $query_args['post_status'], array( 'inherit', 'private', 'trash' ), true ) ) {
$query_args['post_status'] = 'inherit';
}
$media_types = $this->get_media_types();
if ( ! empty( $request['media_type'] ) && isset( $media_types[ $request['media_type'] ] ) ) {
$query_args['post_mime_type'] = $media_types[ $request['media_type'] ];
}
if ( ! empty( $request['mime_type'] ) ) {
$parts = explode( '/', $request['mime_type'] );
if ( isset( $media_types[ $parts[0] ] ) && in_array( $request['mime_type'], $media_types[ $parts[0] ], true ) ) {
$query_args['post_mime_type'] = $request['mime_type'];
}
}
return $query_args;
}
/**
* Check if a given request has access to create an attachment.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|true Boolean true if the attachment may be created, or a WP_Error if not.
*/
public function create_item_permissions_check( $request ) {
$ret = parent::create_item_permissions_check( $request );
if ( ! $ret || is_wp_error( $ret ) ) {
return $ret;
}
if ( ! current_user_can( 'upload_files' ) ) {
return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to upload media on this site.' ), array( 'status' => 400 ) );
}
// Attaching media to a post requires ability to edit said post.
if ( ! empty( $request['post'] ) ) {
$parent = $this->get_post( (int) $request['post'] );
$post_parent_type = get_post_type_object( $parent->post_type );
if ( ! current_user_can( $post_parent_type->cap->edit_post, $request['post'] ) ) {
return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to upload media to this resource.' ), array( 'status' => rest_authorization_required_code() ) );
}
}
return true;
}
/**
* Create a single attachment.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure.
*/
public function create_item( $request ) {
if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) );
}
// Get the file via $_FILES or raw data
$files = $request->get_file_params();
$headers = $request->get_headers();
if ( ! empty( $files ) ) {
$file = $this->upload_from_file( $files, $headers );
} else {
$file = $this->upload_from_data( $request->get_body(), $headers );
}
if ( is_wp_error( $file ) ) {
return $file;
}
$name = basename( $file['file'] );
$name_parts = pathinfo( $name );
$name = trim( substr( $name, 0, -(1 + strlen( $name_parts['extension'] ) ) ) );
$url = $file['url'];
$type = $file['type'];
$file = $file['file'];
// use image exif/iptc data for title and caption defaults if possible
// @codingStandardsIgnoreStart
$image_meta = @wp_read_image_metadata( $file );
// @codingStandardsIgnoreEnd
if ( ! empty( $image_meta ) ) {
if ( empty( $request['title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
$request['title'] = $image_meta['title'];
}
if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) {
$request['caption'] = $image_meta['caption'];
}
}
$attachment = $this->prepare_item_for_database( $request );
$attachment->file = $file;
$attachment->post_mime_type = $type;
$attachment->guid = $url;
if ( empty( $attachment->post_title ) ) {
$attachment->post_title = preg_replace( '/\.[^.]+$/', '', basename( $file ) );
}
$id = wp_insert_post( $attachment, true );
if ( is_wp_error( $id ) ) {
if ( 'db_update_error' === $id->get_error_code() ) {
$id->add_data( array( 'status' => 500 ) );
} else {
$id->add_data( array( 'status' => 400 ) );
}
return $id;
}
$attachment = $this->get_post( $id );
// Include admin functions to get access to wp_generate_attachment_metadata().
require_once ABSPATH . 'wp-admin/includes/admin.php';
wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) );
if ( isset( $request['alt_text'] ) ) {
update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
}
$fields_update = $this->update_additional_fields_for_object( $attachment, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $attachment, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $id ) ) );
/**
* Fires after a single attachment is created or updated via the REST API.
*
* @param object $attachment Inserted attachment.
* @param WP_REST_Request $request The request sent to the API.
* @param boolean $creating True when creating an attachment, false when updating.
*/
do_action( 'rest_insert_attachment', $attachment, $request, true );
return $response;
}
/**
* Update a single post.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure.
*/
public function update_item( $request ) {
if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), array( 'status' => 400 ) );
}
$response = parent::update_item( $request );
if ( is_wp_error( $response ) ) {
return $response;
}
$response = rest_ensure_response( $response );
$data = $response->get_data();
if ( isset( $request['alt_text'] ) ) {
update_post_meta( $data['id'], '_wp_attachment_image_alt', $request['alt_text'] );
}
$attachment = $this->get_post( $request['id'] );
$fields_update = $this->update_additional_fields_for_object( $attachment, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $attachment, $request );
$response = rest_ensure_response( $response );
/* This action is documented in lib/endpoints/class-wp-rest-attachments-controller.php */
do_action( 'rest_insert_attachment', $data, $request, false );
return $response;
}
/**
* Prepare a single attachment for create or update.
*
* @param WP_REST_Request $request Request object.
* @return WP_Error|stdClass $prepared_attachment Post object.
*/
protected function prepare_item_for_database( $request ) {
$prepared_attachment = parent::prepare_item_for_database( $request );
if ( isset( $request['caption'] ) ) {
$prepared_attachment->post_excerpt = $request['caption'];
}
if ( isset( $request['description'] ) ) {
$prepared_attachment->post_content = $request['description'];
}
if ( isset( $request['post'] ) ) {
$prepared_attachment->post_parent = (int) $request['post'];
}
return $prepared_attachment;
}
/**
* Prepare a single attachment output for response.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $post, $request ) {
$response = parent::prepare_item_for_response( $post, $request );
$data = $response->get_data();
$data['alt_text'] = get_post_meta( $post->ID, '_wp_attachment_image_alt', true );
$data['caption'] = $post->post_excerpt;
$data['description'] = $post->post_content;
$data['media_type'] = wp_attachment_is_image( $post->ID ) ? 'image' : 'file';
$data['mime_type'] = $post->post_mime_type;
$data['media_details'] = wp_get_attachment_metadata( $post->ID );
$data['post'] = ! empty( $post->post_parent ) ? (int) $post->post_parent : null;
$data['source_url'] = wp_get_attachment_url( $post->ID );
// Ensure empty details is an empty object.
if ( empty( $data['media_details'] ) ) {
$data['media_details'] = new stdClass;
} elseif ( ! empty( $data['media_details']['sizes'] ) ) {
foreach ( $data['media_details']['sizes'] as $size => &$size_data ) {
if ( isset( $size_data['mime-type'] ) ) {
$size_data['mime_type'] = $size_data['mime-type'];
unset( $size_data['mime-type'] );
}
// Use the same method image_downsize() does.
$image_src = wp_get_attachment_image_src( $post->ID, $size );
if ( ! $image_src ) {
continue;
}
$size_data['source_url'] = $image_src[0];
}
$full_src = wp_get_attachment_image_src( $post->ID, 'full' );
if ( ! empty( $full_src ) ) {
$data['media_details']['sizes']['full'] = array(
'file' => wp_basename( $full_src[0] ),
'width' => $full_src[1],
'height' => $full_src[2],
'mime_type' => $post->post_mime_type,
'source_url' => $full_src[0],
);
}
} else {
$data['media_details']['sizes'] = new stdClass;
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $post ) );
/**
* Filter an attachment returned from the API.
*
* Allows modification of the attachment right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post The original attachment post.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_attachment', $response, $post, $request );
}
/**
* Get the Attachment's schema, conforming to JSON Schema.
*
* @return array Item schema as an array.
*/
public function get_item_schema() {
$schema = parent::get_item_schema();
$schema['properties']['alt_text'] = array(
'description' => __( 'Alternative text to display when resource is not displayed.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
);
$schema['properties']['caption'] = array(
'description' => __( 'The caption for the resource.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'wp_filter_post_kses',
),
);
$schema['properties']['description'] = array(
'description' => __( 'The description for the resource.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'wp_filter_post_kses',
),
);
$schema['properties']['media_type'] = array(
'description' => __( 'Type of resource.' ),
'type' => 'string',
'enum' => array( 'image', 'file' ),
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
);
$schema['properties']['mime_type'] = array(
'description' => __( 'MIME type of resource.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
);
$schema['properties']['media_details'] = array(
'description' => __( 'Details about the resource file, specific to its type.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
);
$schema['properties']['post'] = array(
'description' => __( 'The id for the associated post of the resource.' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
);
$schema['properties']['source_url'] = array(
'description' => __( 'URL to the original resource file.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
);
return $schema;
}
/**
* Handle an upload via raw POST data.
*
* @param array $data Supplied file data.
* @param array $headers HTTP headers from the request.
* @return array|WP_Error Data from {@see wp_handle_sideload()}.
*/
protected function upload_from_data( $data, $headers ) {
if ( empty( $data ) ) {
return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) );
}
if ( empty( $headers['content_type'] ) ) {
return new WP_Error( 'rest_upload_no_content_type', __( 'No Content-Type supplied.' ), array( 'status' => 400 ) );
}
if ( empty( $headers['content_disposition'] ) ) {
return new WP_Error( 'rest_upload_no_content_disposition', __( 'No Content-Disposition supplied.' ), array( 'status' => 400 ) );
}
$filename = self::get_filename_from_disposition( $headers['content_disposition'] );
if ( empty( $filename ) ) {
return new WP_Error( 'rest_upload_invalid_disposition', __( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as `attachment; filename="image.png"` or similar.' ), array( 'status' => 400 ) );
}
if ( ! empty( $headers['content_md5'] ) ) {
$content_md5 = array_shift( $headers['content_md5'] );
$expected = trim( $content_md5 );
$actual = md5( $data );
if ( $expected !== $actual ) {
return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) );
}
}
// Get the content-type.
$type = array_shift( $headers['content_type'] );
/** Include admin functions to get access to wp_tempnam() and wp_handle_sideload() */
require_once ABSPATH . 'wp-admin/includes/admin.php';
// Save the file.
$tmpfname = wp_tempnam( $filename );
$fp = fopen( $tmpfname, 'w+' );
if ( ! $fp ) {
return new WP_Error( 'rest_upload_file_error', __( 'Could not open file handle.' ), array( 'status' => 500 ) );
}
fwrite( $fp, $data );
fclose( $fp );
// Now, sideload it in.
$file_data = array(
'error' => null,
'tmp_name' => $tmpfname,
'name' => $filename,
'type' => $type,
);
$overrides = array(
'test_form' => false,
);
$sideloaded = wp_handle_sideload( $file_data, $overrides );
if ( isset( $sideloaded['error'] ) ) {
// @codingStandardsIgnoreStart
@unlink( $tmpfname );
// @codingStandardsIgnoreEnd
return new WP_Error( 'rest_upload_sideload_error', $sideloaded['error'], array( 'status' => 500 ) );
}
return $sideloaded;
}
/**
* Parse filename from a Content-Disposition header value.
*
* As per RFC6266:
*
* content-disposition = "Content-Disposition" ":"
* disposition-type *( ";" disposition-parm )
*
* disposition-type = "inline" | "attachment" | disp-ext-type
* ; case-insensitive
* disp-ext-type = token
*
* disposition-parm = filename-parm | disp-ext-parm
*
* filename-parm = "filename" "=" value
* | "filename*" "=" ext-value
*
* disp-ext-parm = token "=" value
* | ext-token "=" ext-value
* ext-token = <the characters in token, followed by "*">
*
* @see http://tools.ietf.org/html/rfc2388
* @see http://tools.ietf.org/html/rfc6266
*
* @param string[] $disposition_header List of Content-Disposition header values.
* @return string|null Filename if available, or null if not found.
*/
public static function get_filename_from_disposition( $disposition_header ) {
// Get the filename.
$filename = null;
foreach ( $disposition_header as $value ) {
$value = trim( $value );
if ( strpos( $value, ';' ) === false ) {
continue;
}
list( $type, $attr_parts ) = explode( ';', $value, 2 );
$attr_parts = explode( ';', $attr_parts );
$attributes = array();
foreach ( $attr_parts as $part ) {
if ( strpos( $part, '=' ) === false ) {
continue;
}
list( $key, $value ) = explode( '=', $part, 2 );
$attributes[ trim( $key ) ] = trim( $value );
}
if ( empty( $attributes['filename'] ) ) {
continue;
}
$filename = trim( $attributes['filename'] );
// Unquote quoted filename, but after trimming.
if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, -1, 1 ) === '"' ) {
$filename = substr( $filename, 1, -1 );
}
}
return $filename;
}
/**
* Get the query params for collections of attachments.
*
* @return array Query parameters for the attachment collection as an array.
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['status']['default'] = 'inherit';
$params['status']['enum'] = array( 'inherit', 'private', 'trash' );
$media_types = $this->get_media_types();
$params['media_type'] = array(
'default' => null,
'description' => __( 'Limit result set to attachments of a particular media type.' ),
'type' => 'string',
'enum' => array_keys( $media_types ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['mime_type'] = array(
'default' => null,
'description' => __( 'Limit result set to attachments of a particular MIME type.' ),
'type' => 'string',
);
return $params;
}
/**
* Validate whether the user can query private statuses
*
* @param mixed $value Status value.
* @param WP_REST_Request $request Request object.
* @param string $parameter Additional parameter to pass to validation.
* @return WP_Error|boolean Boolean true if the user may query, WP_Error if not.
*/
public function validate_user_can_query_private_statuses( $value, $request, $parameter ) {
if ( 'inherit' === $value ) {
return true;
}
return parent::validate_user_can_query_private_statuses( $value, $request, $parameter );
}
/**
* Handle an upload via multipart/form-data ($_FILES).
*
* @param array $files Data from $_FILES.
* @param array $headers HTTP headers from the request.
* @return array|WP_Error Data from {@see wp_handle_upload()}.
*/
protected function upload_from_file( $files, $headers ) {
if ( empty( $files ) ) {
return new WP_Error( 'rest_upload_no_data', __( 'No data supplied.' ), array( 'status' => 400 ) );
}
// Verify hash, if given.
if ( ! empty( $headers['content_md5'] ) ) {
$content_md5 = array_shift( $headers['content_md5'] );
$expected = trim( $content_md5 );
$actual = md5_file( $files['file']['tmp_name'] );
if ( $expected !== $actual ) {
return new WP_Error( 'rest_upload_hash_mismatch', __( 'Content hash did not match expected.' ), array( 'status' => 412 ) );
}
}
// Pass off to WP to handle the actual upload.
$overrides = array(
'test_form' => false,
);
// Bypasses is_uploaded_file() when running unit tests.
if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) {
$overrides['action'] = 'wp_handle_mock_upload';
}
// Include admin functions to get access to wp_handle_upload().
require_once ABSPATH . 'wp-admin/includes/admin.php';
$file = wp_handle_upload( $files['file'], $overrides );
if ( isset( $file['error'] ) ) {
return new WP_Error( 'rest_upload_unknown_error', $file['error'], array( 'status' => 500 ) );
}
return $file;
}
/**
* Get the supported media types.
*
* Media types are considered the MIME type category.
*
* @return array
*/
protected function get_media_types() {
$media_types = array();
foreach ( get_allowed_mime_types() as $mime_type ) {
$parts = explode( '/', $mime_type );
if ( ! isset( $media_types[ $parts[0] ] ) ) {
$media_types[ $parts[0] ] = array();
}
$media_types[ $parts[0] ][] = $mime_type;
}
return $media_types;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,533 @@
<?php
abstract class WP_REST_Controller {
/**
* The namespace of this controller's route.
*
* @var string
*/
protected $namespace;
/**
* The base of this controller's route.
*
* @var string
*/
protected $rest_base;
/**
* Register the routes for the objects of the controller.
*/
public function register_routes() {
_doing_it_wrong( 'WP_REST_Controller::register_routes', __( 'The register_routes() method must be overridden' ), 'WPAPI-2.0' );
}
/**
* Check if a given request has access to get items.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Get a collection of items.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Check if a given request has access to get a specific item.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Get one item from the collection.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Check if a given request has access to create items.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|boolean
*/
public function create_item_permissions_check( $request ) {
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Create one item from the collection.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Check if a given request has access to update a specific item.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Update one item from the collection.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Check if a given request has access to delete a specific item.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|boolean
*/
public function delete_item_permissions_check( $request ) {
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Delete one item from the collection.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|WP_REST_Response
*/
public function delete_item( $request ) {
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Prepare the item for create or update operation.
*
* @param WP_REST_Request $request Request object.
* @return WP_Error|object $prepared_item
*/
protected function prepare_item_for_database( $request ) {
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Prepare the item for the REST response.
*
* @param mixed $item WordPress representation of the item.
* @param WP_REST_Request $request Request object.
* @return WP_Error|WP_REST_Response $response
*/
public function prepare_item_for_response( $item, $request ) {
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Prepare a response for inserting into a collection.
*
* @param WP_REST_Response $response Response object.
* @return array Response data, ready for insertion into collection data.
*/
public function prepare_response_for_collection( $response ) {
if ( ! ( $response instanceof WP_REST_Response ) ) {
return $response;
}
$data = (array) $response->get_data();
$server = rest_get_server();
if ( method_exists( $server, 'get_compact_response_links' ) ) {
$links = call_user_func( array( $server, 'get_compact_response_links' ), $response );
} else {
$links = call_user_func( array( $server, 'get_response_links' ), $response );
}
if ( ! empty( $links ) ) {
$data['_links'] = $links;
}
return $data;
}
/**
* Filter a response based on the context defined in the schema.
*
* @param array $data
* @param string $context
* @return array
*/
public function filter_response_by_context( $data, $context ) {
$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;
}
/**
* Get the item's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
return $this->add_additional_fields_schema( array() );
}
/**
* Get the item's schema for display / public consumption purposes.
*
* @return array
*/
public function get_public_item_schema() {
$schema = $this->get_item_schema();
foreach ( $schema['properties'] as &$property ) {
unset( $property['arg_options'] );
}
return $schema;
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param(),
'page' => array(
'description' => __( 'Current page of the collection.' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
),
'per_page' => array(
'description' => __( 'Maximum number of items to be returned in result set.' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
),
'search' => array(
'description' => __( 'Limit results to those matching a string.' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
),
);
}
/**
* Get the magical context param.
*
* Ensures consistent description between endpoints, and populates enum from schema.
*
* @param array $args
* @return array
*/
public function get_context_param( $args = array() ) {
$param_details = array(
'description' => __( 'Scope under which the request is made; determines fields present in response.' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$schema = $this->get_item_schema();
if ( empty( $schema['properties'] ) ) {
return array_merge( $param_details, $args );
}
$contexts = array();
foreach ( $schema['properties'] as $attributes ) {
if ( ! empty( $attributes['context'] ) ) {
$contexts = array_merge( $contexts, $attributes['context'] );
}
}
if ( ! empty( $contexts ) ) {
$param_details['enum'] = array_unique( $contexts );
rsort( $param_details['enum'] );
}
return array_merge( $param_details, $args );
}
/**
* Add the values from additional fields to a data object.
*
* @param array $object
* @param WP_REST_Request $request
* @return array modified object with additional fields.
*/
protected function add_additional_fields_to_object( $object, $request ) {
$additional_fields = $this->get_additional_fields();
foreach ( $additional_fields as $field_name => $field_options ) {
if ( ! $field_options['get_callback'] ) {
continue;
}
$object[ $field_name ] = call_user_func( $field_options['get_callback'], $object, $field_name, $request, $this->get_object_type() );
}
return $object;
}
/**
* Update the values of additional fields added to a data object.
*
* @param array $object
* @param WP_REST_Request $request
* @return bool|WP_Error True on success, WP_Error object if a field cannot be updated.
*/
protected function update_additional_fields_for_object( $object, $request ) {
$additional_fields = $this->get_additional_fields();
foreach ( $additional_fields as $field_name => $field_options ) {
if ( ! $field_options['update_callback'] ) {
continue;
}
// Don't run the update callbacks if the data wasn't passed in the request.
if ( ! isset( $request[ $field_name ] ) ) {
continue;
}
$result = call_user_func( $field_options['update_callback'], $request[ $field_name ], $object, $field_name, $request, $this->get_object_type() );
if ( is_wp_error( $result ) ) {
return $result;
}
}
return true;
}
/**
* Add the schema from additional fields to an schema array.
*
* The type of object is inferred from the passed schema.
*
* @param array $schema Schema array.
* @return array Modified Schema array.
*/
protected function add_additional_fields_schema( $schema ) {
if ( empty( $schema['title'] ) ) {
return $schema;
}
/**
* Can't use $this->get_object_type otherwise we cause an inf loop.
*/
$object_type = $schema['title'];
$additional_fields = $this->get_additional_fields( $object_type );
foreach ( $additional_fields as $field_name => $field_options ) {
if ( ! $field_options['schema'] ) {
continue;
}
$schema['properties'][ $field_name ] = $field_options['schema'];
}
return $schema;
}
/**
* Get all the registered additional fields for a given object-type.
*
* @param string $object_type
* @return array
*/
protected function get_additional_fields( $object_type = null ) {
if ( ! $object_type ) {
$object_type = $this->get_object_type();
}
if ( ! $object_type ) {
return array();
}
global $wp_rest_additional_fields;
if ( ! $wp_rest_additional_fields || ! isset( $wp_rest_additional_fields[ $object_type ] ) ) {
return array();
}
return $wp_rest_additional_fields[ $object_type ];
}
/**
* Get the object type this controller is responsible for managing.
*
* @return string
*/
protected function get_object_type() {
$schema = $this->get_item_schema();
if ( ! $schema || ! isset( $schema['title'] ) ) {
return null;
}
return $schema['title'];
}
/**
* Get an array of endpoint arguments from the item schema for the controller.
*
* @param string $method HTTP method of the request. The arguments
* for `CREATABLE` requests are checked for required
* values and may fall-back to a given default, this
* is not done on `EDITABLE` requests. Default is
* WP_REST_Server::CREATABLE.
* @return array $endpoint_args
*/
public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
$schema = $this->get_item_schema();
$schema_properties = ! empty( $schema['properties'] ) ? $schema['properties'] : array();
$endpoint_args = array();
foreach ( $schema_properties as $field_id => $params ) {
// Arguments specified as `readonly` are not allowed to be set.
if ( ! empty( $params['readonly'] ) ) {
continue;
}
$endpoint_args[ $field_id ] = array(
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'rest_sanitize_request_arg',
);
if ( isset( $params['description'] ) ) {
$endpoint_args[ $field_id ]['description'] = $params['description'];
}
if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) {
$endpoint_args[ $field_id ]['default'] = $params['default'];
}
if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) {
$endpoint_args[ $field_id ]['required'] = true;
}
foreach ( array( 'type', 'format', 'enum' ) as $schema_prop ) {
if ( isset( $params[ $schema_prop ] ) ) {
$endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
}
}
// Merge in any options provided by the schema property.
if ( isset( $params['arg_options'] ) ) {
// Only use required / default from arg_options on CREATABLE endpoints.
if ( WP_REST_Server::CREATABLE !== $method ) {
$params['arg_options'] = array_diff_key( $params['arg_options'], array( 'required' => '', 'default' => '' ) );
}
$endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] );
}
}
return $endpoint_args;
}
/**
* Retrieves post data given a post ID or post object.
*
* This is a subset of the functionality of the `get_post()` function, with
* the additional functionality of having `the_post` action done on the
* resultant post object. This is done so that plugins may manipulate the
* post that is used in the REST API.
*
* @see get_post()
* @global WP_Query $wp_query
*
* @param int|WP_Post $post Post ID or post object. Defaults to global $post.
* @return WP_Post|null A `WP_Post` object when successful.
*/
public function get_post( $post ) {
$post_obj = get_post( $post );
/**
* Filter the post.
*
* Allows plugins to filter the post object as returned by `\WP_REST_Controller::get_post()`.
*
* @param WP_Post|null $post_obj The post object as returned by `get_post()`.
* @param int|WP_Post $post The original value used to obtain the post object.
*/
$post = apply_filters( 'rest_the_post', $post_obj, $post );
return $post;
}
/**
* Sanitize the slug value.
*
* @internal We can't use {@see sanitize_title} directly, as the second
* parameter is the fallback title, which would end up being set to the
* request object.
* @see https://github.com/WP-API/WP-API/issues/1585
*
* @todo Remove this in favour of https://core.trac.wordpress.org/ticket/34659
*
* @param string $slug Slug value passed in request.
* @return string Sanitized value for the slug.
*/
public function sanitize_slug( $slug ) {
return sanitize_title( $slug );
}
}

View File

@ -0,0 +1,244 @@
<?php
class WP_REST_Post_Statuses_Controller extends WP_REST_Controller {
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'statuses';
}
/**
* Register the routes for the objects of the controller.
*/
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(),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<status>[\w-]+)', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Check whether a given request has permission to read post statuses.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( 'edit' === $request['context'] ) {
$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
foreach ( $types as $type ) {
if ( current_user_can( $type->cap->edit_posts ) ) {
return true;
}
}
return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all post statuses, depending on user context
*
* @param WP_REST_Request $request
* @return array|WP_Error
*/
public function get_items( $request ) {
$data = array();
$statuses = get_post_stati( array( 'internal' => false ), 'object' );
$statuses['trash'] = get_post_status_object( 'trash' );
foreach ( $statuses as $slug => $obj ) {
$ret = $this->check_read_permission( $obj );
if ( ! $ret ) {
continue;
}
$status = $this->prepare_item_for_response( $obj, $request );
$data[ $obj->name ] = $this->prepare_response_for_collection( $status );
}
return rest_ensure_response( $data );
}
/**
* Check if a given request has access to read a post status.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$status = get_post_status_object( $request['status'] );
if ( empty( $status ) ) {
return new WP_Error( 'rest_status_invalid', __( 'Invalid resource.' ), array( 'status' => 404 ) );
}
$check = $this->check_read_permission( $status );
if ( ! $check ) {
return new WP_Error( 'rest_cannot_read_status', __( 'Cannot view resource.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check whether a given post status should be visible
*
* @param object $status
* @return boolean
*/
protected function check_read_permission( $status ) {
if ( true === $status->public ) {
return true;
}
if ( false === $status->internal || 'trash' === $status->name ) {
$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
foreach ( $types as $type ) {
if ( current_user_can( $type->cap->edit_posts ) ) {
return true;
}
}
}
return false;
}
/**
* Get a specific post status
*
* @param WP_REST_Request $request
* @return array|WP_Error
*/
public function get_item( $request ) {
$obj = get_post_status_object( $request['status'] );
if ( empty( $obj ) ) {
return new WP_Error( 'rest_status_invalid', __( 'Invalid resource.' ), array( 'status' => 404 ) );
}
$data = $this->prepare_item_for_response( $obj, $request );
return rest_ensure_response( $data );
}
/**
* Prepare a post status object for serialization
*
* @param stdClass $status Post status data
* @param WP_REST_Request $request
* @return WP_REST_Response Post status data
*/
public function prepare_item_for_response( $status, $request ) {
$data = array(
'name' => $status->label,
'private' => (bool) $status->private,
'protected' => (bool) $status->protected,
'public' => (bool) $status->public,
'queryable' => (bool) $status->publicly_queryable,
'show_in_list' => (bool) $status->show_in_admin_all_list,
'slug' => $status->name,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( 'publish' === $status->name ) {
$response->add_link( 'archives', rest_url( 'wp/v2/posts' ) );
} else {
$response->add_link( 'archives', add_query_arg( 'status', $status->name, rest_url( 'wp/v2/posts' ) ) );
}
/**
* Filter a status returned from the API.
*
* Allows modification of the status data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $status The original status object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_status', $response, $status, $request );
}
/**
* Get the Post status' schema, conforming to JSON Schema
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'status',
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'The title for the resource.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'private' => array(
'description' => __( 'Whether posts with this resource should be private.' ),
'type' => 'boolean',
'context' => array( 'edit' ),
'readonly' => true,
),
'protected' => array(
'description' => __( 'Whether posts with this resource should be protected.' ),
'type' => 'boolean',
'context' => array( 'edit' ),
'readonly' => true,
),
'public' => array(
'description' => __( 'Whether posts of this resource should be shown in the front end of the site.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'queryable' => array(
'description' => __( 'Whether posts with this resource should be publicly-queryable.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'show_in_list' => array(
'description' => __( 'Whether to include posts in the edit listing for their post type.' ),
'type' => 'boolean',
'context' => array( 'edit' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}

View File

@ -0,0 +1,202 @@
<?php
class WP_REST_Post_Types_Controller extends WP_REST_Controller {
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'types';
}
/**
* Register the routes for the objects of the controller.
*/
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(),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<type>[\w-]+)', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Check whether a given request has permission to read types.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( 'edit' === $request['context'] ) {
foreach ( get_post_types( array(), 'object' ) as $post_type ) {
if ( ! empty( $post_type->show_in_rest ) && current_user_can( $post_type->cap->edit_posts ) ) {
return true;
}
}
return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all public post types
*
* @param WP_REST_Request $request
* @return array|WP_Error
*/
public function get_items( $request ) {
$data = array();
foreach ( get_post_types( array(), 'object' ) as $obj ) {
if ( empty( $obj->show_in_rest ) || ( 'edit' === $request['context'] && ! current_user_can( $obj->cap->edit_posts ) ) ) {
continue;
}
$post_type = $this->prepare_item_for_response( $obj, $request );
$data[ $obj->name ] = $this->prepare_response_for_collection( $post_type );
}
return rest_ensure_response( $data );
}
/**
* Get a specific post type
*
* @param WP_REST_Request $request
* @return array|WP_Error
*/
public function get_item( $request ) {
$obj = get_post_type_object( $request['type'] );
if ( empty( $obj ) ) {
return new WP_Error( 'rest_type_invalid', __( 'Invalid resource.' ), array( 'status' => 404 ) );
}
if ( empty( $obj->show_in_rest ) ) {
return new WP_Error( 'rest_cannot_read_type', __( 'Cannot view resource.' ), array( 'status' => rest_authorization_required_code() ) );
}
if ( 'edit' === $request['context'] && ! current_user_can( $obj->cap->edit_posts ) ) {
return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to manage this resource.' ), array( 'status' => rest_authorization_required_code() ) );
}
$data = $this->prepare_item_for_response( $obj, $request );
return rest_ensure_response( $data );
}
/**
* Prepare a post type object for serialization
*
* @param stdClass $post_type Post type data
* @param WP_REST_Request $request
* @return WP_REST_Response $response
*/
public function prepare_item_for_response( $post_type, $request ) {
$data = array(
'capabilities' => $post_type->cap,
'description' => $post_type->description,
'hierarchical' => $post_type->hierarchical,
'labels' => $post_type->labels,
'name' => $post_type->label,
'slug' => $post_type->name,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
$response->add_links( array(
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
'https://api.w.org/items' => array(
'href' => rest_url( sprintf( 'wp/v2/%s', $base ) ),
),
) );
/**
* Filter a post type returned from the API.
*
* Allows modification of the post type data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $item The original post type object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_post_type', $response, $post_type, $request );
}
/**
* Get the Post type's schema, conforming to JSON Schema
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'type',
'type' => 'object',
'properties' => array(
'capabilities' => array(
'description' => __( 'All capabilities used by the resource.' ),
'type' => 'array',
'context' => array( 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'A human-readable description of the resource.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'hierarchical' => array(
'description' => __( 'Whether or not the resource should have children.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'labels' => array(
'description' => __( 'Human-readable labels for the resource for various contexts.' ),
'type' => 'object',
'context' => array( 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'The title for the resource.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,428 @@
<?php
class WP_REST_Revisions_Controller extends WP_REST_Controller {
private $parent_post_type;
private $parent_controller;
private $parent_base;
public function __construct( $parent_post_type ) {
$this->parent_post_type = $parent_post_type;
$this->parent_controller = new WP_REST_Posts_Controller( $parent_post_type );
$this->namespace = 'wp/v2';
$this->rest_base = 'revisions';
$post_type_object = get_post_type_object( $parent_post_type );
$this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
}
/**
* Register routes for revisions based on post types supporting revisions
*
* @access public
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $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(),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
));
}
/**
* Check if a given request has access to get revisions
*
* @access public
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
$parent = $this->get_post( $request['parent'] );
if ( ! $parent ) {
return true;
}
$parent_post_type_obj = get_post_type_object( $parent->post_type );
if ( ! current_user_can( $parent_post_type_obj->cap->edit_post, $parent->ID ) ) {
return new WP_Error( 'rest_cannot_read', __( 'Sorry, you cannot view revisions of this post.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get a collection of revisions
*
* @access public
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$parent = $this->get_post( $request['parent'] );
if ( ! $request['parent'] || ! $parent || $this->parent_post_type !== $parent->post_type ) {
return new WP_Error( 'rest_post_invalid_parent', __( 'Invalid post parent id.' ), array( 'status' => 404 ) );
}
$revisions = wp_get_post_revisions( $request['parent'] );
$response = array();
foreach ( $revisions as $revision ) {
$data = $this->prepare_item_for_response( $revision, $request );
$response[] = $this->prepare_response_for_collection( $data );
}
return rest_ensure_response( $response );
}
/**
* Check if a given request has access to get a specific revision
*
* @access public
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
return $this->get_items_permissions_check( $request );
}
/**
* Get one revision from the collection
*
* @access public
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|array
*/
public function get_item( $request ) {
$parent = $this->get_post( $request['parent'] );
if ( ! $request['parent'] || ! $parent || $this->parent_post_type !== $parent->post_type ) {
return new WP_Error( 'rest_post_invalid_parent', __( 'Invalid post parent id.' ), array( 'status' => 404 ) );
}
$revision = $this->get_post( $request['id'] );
if ( ! $revision || 'revision' !== $revision->post_type ) {
return new WP_Error( 'rest_post_invalid_id', __( 'Invalid revision id.' ), array( 'status' => 404 ) );
}
$response = $this->prepare_item_for_response( $revision, $request );
return rest_ensure_response( $response );
}
/**
* Check if a given request has access to delete a revision
*
* @access public
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function delete_item_permissions_check( $request ) {
$response = $this->get_items_permissions_check( $request );
if ( ! $response || is_wp_error( $response ) ) {
return $response;
}
$post = $this->get_post( $request['id'] );
if ( ! $post ) {
return new WP_Error( 'rest_post_invalid_id', __( 'Invalid revision id.' ), array( 'status' => 404 ) );
}
$post_type = get_post_type_object( 'revision' );
return current_user_can( $post_type->cap->delete_post, $post->ID );
}
/**
* Delete a single revision
*
* @access public
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function delete_item( $request ) {
$result = wp_delete_post( $request['id'], true );
/**
* Fires after a revision is deleted via the REST API.
*
* @param (mixed) $result The revision object (if it was deleted or moved to the trash successfully)
* or false (failure). If the revision was moved to to the trash, $result represents
* its new state; if it was deleted, $result represents its state before deletion.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'rest_delete_revision', $result, $request );
if ( $result ) {
return true;
} else {
return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
}
}
/**
* Prepare the revision for the REST response
*
* @access public
*
* @param WP_Post $post Post revision object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response
*/
public function prepare_item_for_response( $post, $request ) {
$schema = $this->get_item_schema();
$data = array();
if ( ! empty( $schema['properties']['author'] ) ) {
$data['author'] = $post->post_author;
}
if ( ! empty( $schema['properties']['date'] ) ) {
$data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
}
if ( ! empty( $schema['properties']['date_gmt'] ) ) {
$data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
}
if ( ! empty( $schema['properties']['id'] ) ) {
$data['id'] = $post->ID;
}
if ( ! empty( $schema['properties']['modified'] ) ) {
$data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
}
if ( ! empty( $schema['properties']['modified_gmt'] ) ) {
$data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
}
if ( ! empty( $schema['properties']['parent'] ) ) {
$data['parent'] = (int) $post->post_parent;
}
if ( ! empty( $schema['properties']['slug'] ) ) {
$data['slug'] = $post->post_name;
}
if ( ! empty( $schema['properties']['guid'] ) ) {
$data['guid'] = array(
/** This filter is documented in wp-includes/post-template.php */
'rendered' => apply_filters( 'get_the_guid', $post->guid ),
'raw' => $post->guid,
);
}
if ( ! empty( $schema['properties']['title'] ) ) {
$data['title'] = array(
'raw' => $post->post_title,
'rendered' => get_the_title( $post->ID ),
);
}
if ( ! empty( $schema['properties']['content'] ) ) {
$data['content'] = array(
'raw' => $post->post_content,
/** This filter is documented in wp-includes/post-template.php */
'rendered' => apply_filters( 'the_content', $post->post_content ),
);
}
if ( ! empty( $schema['properties']['excerpt'] ) ) {
$data['excerpt'] = array(
'raw' => $post->post_excerpt,
'rendered' => $this->prepare_excerpt_response( $post->post_excerpt, $post ),
);
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( ! empty( $data['parent'] ) ) {
$response->add_link( 'parent', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->parent_base, $data['parent'] ) ) );
}
/**
* Filter a revision returned from the API.
*
* Allows modification of the revision right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post The original revision object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_revision', $response, $post, $request );
}
/**
* Check the post_date_gmt or modified_gmt and prepare any post or
* modified date for single post output.
*
* @access protected
*
* @param string $date_gmt GMT publication time.
* @param string|null $date Optional, default is null. Local publication time.
* @return string|null ISO8601/RFC3339 formatted datetime.
*/
protected function prepare_date_response( $date_gmt, $date = null ) {
if ( '0000-00-00 00:00:00' === $date_gmt ) {
return null;
}
if ( isset( $date ) ) {
return mysql_to_rfc3339( $date );
}
return mysql_to_rfc3339( $date_gmt );
}
/**
* Get the revision's schema, conforming to JSON Schema
*
* @access public
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => "{$this->parent_post_type}-revision",
'type' => 'object',
/*
* Base properties for every Revision
*/
'properties' => array(
'author' => array(
'description' => __( 'The id for the author of the object.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
'date' => array(
'description' => __( 'The date the object was published.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit', 'embed' ),
),
'date_gmt' => array(
'description' => __( 'The date the object was published, as GMT.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'guid' => array(
'description' => __( 'GUID for the object, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'id' => array(
'description' => __( 'Unique identifier for the object.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
'modified' => array(
'description' => __( 'The date the object was last modified.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'modified_gmt' => array(
'description' => __( 'The date the object was last modified, as GMT.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'parent' => array(
'description' => __( 'The id for the parent of the object.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
),
);
$parent_schema = $this->parent_controller->get_item_schema();
if ( ! empty( $parent_schema['properties']['title'] ) ) {
$schema['properties']['title'] = $parent_schema['properties']['title'];
}
if ( ! empty( $parent_schema['properties']['content'] ) ) {
$schema['properties']['content'] = $parent_schema['properties']['content'];
}
if ( ! empty( $parent_schema['properties']['excerpt'] ) ) {
$schema['properties']['excerpt'] = $parent_schema['properties']['excerpt'];
}
if ( ! empty( $parent_schema['properties']['guid'] ) ) {
$schema['properties']['guid'] = $parent_schema['properties']['guid'];
}
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections
*
* @access public
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
/**
* Check the post excerpt and prepare it for single post output.
*
* @access protected
*
* @param string $excerpt The post excerpt.
* @param WP_Post $post Post revision object.
* @return string|null $excerpt
*/
protected function prepare_excerpt_response( $excerpt, $post ) {
/** This filter is documented in wp-includes/post-template.php */
$excerpt = apply_filters( 'the_excerpt', $excerpt, $post );
if ( empty( $excerpt ) ) {
return '';
}
return $excerpt;
}
}

View File

@ -0,0 +1,220 @@
<?php
/**
* Manage a WordPress site's settings.
*/
class WP_REST_Settings_Controller extends WP_REST_Controller {
protected $rest_base = 'settings';
protected $namespace = 'wp/v2';
/**
* Register the routes for the objects of the controller.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'args' => array(),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Check if a given request has access to read and manage settings.
*
* @param WP_REST_Request $request Full details about the request.
* @return boolean
*/
public function get_item_permissions_check( $request ) {
return current_user_can( 'manage_options' );
}
/**
* Get the settings.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|array
*/
public function get_item( $request ) {
$options = $this->get_registered_options();
$response = array();
foreach ( $options as $name => $args ) {
/**
* Filters the value of a setting recognized by the REST API.
*
* Allow hijacking the setting value and overriding the built-in behavior by returning a
* non-null value. The returned value will be presented as the setting value instead.
*
* @since 4.7.0
*
* @param mixed $result Value to use for the requested setting. Can be a scalar
* matching the registered schema for the setting, or null to
* follow the default `get_option` behavior.
* @param string $name Setting name (as shown in REST API responses).
* @param array $args Arguments passed to `register_setting()` for this setting.
*/
$response[ $name ] = apply_filters( 'rest_pre_get_setting', null, $name, $args );
if ( is_null( $response[ $name ] ) ) {
// Default to a null value as "null" in the response means "not set".
$response[ $name ] = get_option( $args['option_name'], $args['schema']['default'] );
}
// Because get_option() is lossy, we have to
// cast values to the type they are registered with.
$response[ $name ] = $this->prepare_value( $response[ $name ], $args['schema'] );
}
return $response;
}
/**
* Prepare a value for output based off a schema array.
*
* @param mixed $value
* @param array $schema
* @return mixed
*/
protected function prepare_value( $value, $schema ) {
switch ( $schema['type'] ) {
case 'string':
return (string) $value;
case 'number':
return (float) $value;
case 'boolean':
return (bool) $value;
default:
return null;
}
}
/**
* Update settings for the settings object.
*
* @param WP_REST_Request $request Full detail about the request.
* @return WP_Error|array
*/
public function update_item( $request ) {
$options = $this->get_registered_options();
$params = $request->get_params();
foreach ( $options as $name => $args ) {
if ( ! array_key_exists( $name, $params ) ) {
continue;
}
/**
* Filters whether to preempt a setting value update.
*
* Allow hijacking the setting update logic and overriding the built-in behavior by
* returning true.
*
* @since 4.7.0
*
* @param boolean $result Whether to override the default behavior for updating the
* value of a setting.
* @param string $name Setting name (as shown in REST API responses).
* @param mixed $value Updated setting value.
* @param array $args Arguments passed to `register_setting()` for this setting.
*/
$updated = apply_filters( 'rest_pre_update_setting', false, $name, $request[ $name ], $args );
if ( $updated ) {
continue;
}
// A null value means reset the option, which is essentially deleting it
// from the database and then relying on the default value.
if ( is_null( $request[ $name ] ) ) {
delete_option( $args['option_name'] );
} else {
update_option( $args['option_name'], $request[ $name ] );
}
}
return $this->get_item( $request );
}
/**
* Get all the registered options for the Settings API
*
* @return array
*/
protected function get_registered_options() {
$rest_options = array();
foreach ( get_registered_settings() as $name => $args ) {
if ( empty( $args['show_in_rest'] ) ) {
continue;
}
$rest_args = array();
if ( is_array( $args['show_in_rest'] ) ) {
$rest_args = $args['show_in_rest'];
}
$defaults = array(
'name' => ! empty( $rest_args['name'] ) ? $rest_args['name'] : $name,
'schema' => array(),
);
$rest_args = array_merge( $defaults, $rest_args );
$default_schema = array(
'type' => empty( $args['type'] ) ? null : $args['type'],
'description' => empty( $args['description'] ) ? '' : $args['description'],
'default' => isset( $args['default'] ) ? $args['default'] : null,
);
$rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );
$rest_args['option_name'] = $name;
// Skip over settings that don't have a defined type in the schema.
if ( empty( $rest_args['schema']['type'] ) ) {
continue;
}
// Whitelist the supported types for settings, as we don't want invalid types
// to be updated with arbitrary values that we can't do decent sanitizing for.
if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'string', 'boolean' ), true ) ) {
continue;
}
$rest_options[ $rest_args['name'] ] = $rest_args;
}
return $rest_options;
}
/**
* Get the site setting schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$options = $this->get_registered_options();
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'settings',
'type' => 'object',
'properties' => array(),
);
foreach ( $options as $option_name => $option ) {
$schema['properties'][ $option_name ] = $option['schema'];
}
return $this->add_additional_fields_schema( $schema );
}
}

View File

@ -0,0 +1,261 @@
<?php
class WP_REST_Taxonomies_Controller extends WP_REST_Controller {
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'taxonomies';
}
/**
* Register the routes for the objects of the controller.
*/
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(),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<taxonomy>[\w-]+)', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Check whether a given request has permission to read taxonomies.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( 'edit' === $request['context'] ) {
if ( ! empty( $request['type'] ) ) {
$taxonomies = get_object_taxonomies( $request['type'], 'objects' );
} else {
$taxonomies = get_taxonomies( '', 'objects' );
}
foreach ( $taxonomies as $taxonomy ) {
if ( ! empty( $taxonomy->show_in_rest ) && current_user_can( $taxonomy->cap->manage_terms ) ) {
return true;
}
}
return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all public taxonomies
*
* @param WP_REST_Request $request
* @return array
*/
public function get_items( $request ) {
// Retrieve the list of registered collection query parameters.
$registered = $this->get_collection_params();
if ( isset( $registered['type'] ) && ! empty( $request['type'] ) ) {
$taxonomies = get_object_taxonomies( $request['type'], 'objects' );
} else {
$taxonomies = get_taxonomies( '', 'objects' );
}
$data = array();
foreach ( $taxonomies as $tax_type => $value ) {
if ( empty( $value->show_in_rest ) || ( 'edit' === $request['context'] && ! current_user_can( $value->cap->manage_terms ) ) ) {
continue;
}
$tax = $this->prepare_item_for_response( $value, $request );
$tax = $this->prepare_response_for_collection( $tax );
$data[ $tax_type ] = $tax;
}
if ( empty( $data ) ) {
// Response should still be returned as a JSON object when it is empty.
$data = (object) $data;
}
return rest_ensure_response( $data );
}
/**
* Check if a given request has access a taxonomy
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$tax_obj = get_taxonomy( $request['taxonomy'] );
if ( $tax_obj ) {
if ( empty( $tax_obj->show_in_rest ) ) {
return false;
}
if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->manage_terms ) ) {
return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to manage this resource.' ), array( 'status' => rest_authorization_required_code() ) );
}
}
return true;
}
/**
* Get a specific taxonomy
*
* @param WP_REST_Request $request
* @return array|WP_Error
*/
public function get_item( $request ) {
$tax_obj = get_taxonomy( $request['taxonomy'] );
if ( empty( $tax_obj ) ) {
return new WP_Error( 'rest_taxonomy_invalid', __( 'Invalid resource.' ), array( 'status' => 404 ) );
}
$data = $this->prepare_item_for_response( $tax_obj, $request );
return rest_ensure_response( $data );
}
/**
* Prepare a taxonomy object for serialization
*
* @param stdClass $taxonomy Taxonomy data
* @param WP_REST_Request $request
* @return WP_REST_Response $response
*/
public function prepare_item_for_response( $taxonomy, $request ) {
$data = array(
'name' => $taxonomy->label,
'slug' => $taxonomy->name,
'capabilities' => $taxonomy->cap,
'description' => $taxonomy->description,
'labels' => $taxonomy->labels,
'types' => $taxonomy->object_type,
'show_cloud' => $taxonomy->show_tagcloud,
'hierarchical' => $taxonomy->hierarchical,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
$response->add_links( array(
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
'https://api.w.org/items' => array(
'href' => rest_url( sprintf( 'wp/v2/%s', $base ) ),
),
) );
/**
* Filter a taxonomy returned from the API.
*
* Allows modification of the taxonomy data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $item The original taxonomy object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_taxonomy', $response, $taxonomy, $request );
}
/**
* Get the taxonomy's schema, conforming to JSON Schema
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'taxonomy',
'type' => 'object',
'properties' => array(
'capabilities' => array(
'description' => __( 'All capabilities used by the resource.' ),
'type' => 'array',
'context' => array( 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'A human-readable description of the resource.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'hierarchical' => array(
'description' => __( 'Whether or not the resource should have children.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'labels' => array(
'description' => __( 'Human-readable labels for the resource for various contexts.' ),
'type' => 'object',
'context' => array( 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'The title for the resource.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'show_cloud' => array(
'description' => __( 'Whether or not the term cloud should be displayed.' ),
'type' => 'boolean',
'context' => array( 'edit' ),
'readonly' => true,
),
'types' => array(
'description' => __( 'Types associated with resource.' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections
*
* @return array
*/
public function get_collection_params() {
$new_params = array();
$new_params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$new_params['type'] = array(
'description' => __( 'Limit results to resources associated with a specific post type.' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
return $new_params;
}
}

View File

@ -0,0 +1,907 @@
<?php
/**
* Access terms associated with a taxonomy.
*/
class WP_REST_Terms_Controller extends WP_REST_Controller {
/**
* Taxonomy key.
*
* @access protected
* @var string
*/
protected $taxonomy;
/**
* Instance of a term meta fields object.
*
* @access protected
* @var WP_REST_Term_Meta_Fields
*/
protected $meta;
/**
* Column to have the terms be sorted by.
*
* @access protected
* @var string
*/
protected $sort_column;
/**
* Number of terms that were found.
*
* @access protected
* @var int
*/
protected $total_terms;
/**
* Constructor.
*
* @param string $taxonomy Taxonomy key.
*/
public function __construct( $taxonomy ) {
$this->taxonomy = $taxonomy;
$this->namespace = 'wp/v2';
$tax_obj = get_taxonomy( $taxonomy );
$this->rest_base = ! empty( $tax_obj->rest_base ) ? $tax_obj->rest_base : $tax_obj->name;
$this->meta = new WP_REST_Term_Meta_Fields( $taxonomy );
}
/**
* Registers the routes for the objects of the controller.
*/
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(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
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 ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'description' => __( 'Required to be true, as resource does not support trashing.' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Checks if a request has access to read terms in the specified taxonomy.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
$tax_obj = get_taxonomy( $this->taxonomy );
if ( ! $tax_obj || ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
return false;
}
if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->edit_terms ) ) {
return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Gets terms associated with a taxonomy.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error
*/
public function get_items( $request ) {
// Retrieve the list of registered collection query parameters.
$registered = $this->get_collection_params();
// This array defines mappings between public API query parameters whose
// values are accepted as-passed, and their internal WP_Query parameter
// name equivalents (some are the same). Only values which are also
// present in $registered will be set.
$parameter_mappings = array(
'exclude' => 'exclude',
'include' => 'include',
'order' => 'order',
'orderby' => 'orderby',
'post' => 'post',
'hide_empty' => 'hide_empty',
'per_page' => 'number',
'search' => 'search',
'slug' => 'slug',
);
$prepared_args = array();
// For each known parameter which is both registered and present in the request,
// set the parameter's value on the query $prepared_args.
foreach ( $parameter_mappings as $api_param => $wp_param ) {
if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
$prepared_args[ $wp_param ] = $request[ $api_param ];
}
}
if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) {
$prepared_args['offset'] = $request['offset'];
} else {
$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
}
$taxonomy_obj = get_taxonomy( $this->taxonomy );
if ( $taxonomy_obj->hierarchical && isset( $registered['parent'], $request['parent'] ) ) {
if ( 0 === $request['parent'] ) {
// Only query top-level terms.
$prepared_args['parent'] = 0;
} else {
if ( $request['parent'] ) {
$prepared_args['parent'] = $request['parent'];
}
}
}
/**
* Filters the query arguments before passing them to get_terms().
*
* Enables adding extra arguments or setting defaults for a terms
* collection request.
*
* @see https://developer.wordpress.org/reference/functions/get_terms/
*
* @param array $prepared_args Array of arguments to be
* passed to get_terms().
* @param WP_REST_Request $request The current request.
*/
$prepared_args = apply_filters( "rest_{$this->taxonomy}_query", $prepared_args, $request );
if ( ! empty( $prepared_args['post'] ) ) {
$query_result = $this->get_terms_for_post( $prepared_args );
$total_terms = $this->total_terms;
} else {
$query_result = get_terms( $this->taxonomy, $prepared_args );
$count_args = $prepared_args;
unset( $count_args['number'], $count_args['offset'] );
$total_terms = wp_count_terms( $this->taxonomy, $count_args );
// wp_count_terms can return a falsy value when the term has no children
if ( ! $total_terms ) {
$total_terms = 0;
}
}
$response = array();
foreach ( $query_result as $term ) {
$data = $this->prepare_item_for_response( $term, $request );
$response[] = $this->prepare_response_for_collection( $data );
}
$response = rest_ensure_response( $response );
// Store pagination values for headers.
$per_page = (int) $prepared_args['number'];
$page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
$response->header( 'X-WP-Total', (int) $total_terms );
$max_pages = ceil( $total_terms / $per_page );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
$base = add_query_arg( $request->get_query_params(), rest_url( $this->namespace . '/' . $this->rest_base ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Gets the terms attached to a post.
*
* This is an alternative to get_terms() that uses get_the_terms()
* instead, which hits the object cache. There are a few things not
* supported, notably `include`, `exclude`. In `self::get_items()` these
* are instead treated as a full query.
*
* @param array $prepared_args Arguments for get_terms().
* @return array List of term objects. (Total count in `$this->total_terms`)
*/
protected function get_terms_for_post( $prepared_args ) {
$query_result = get_the_terms( $prepared_args['post'], $this->taxonomy );
if ( empty( $query_result ) ) {
$this->total_terms = 0;
return array();
}
/*
* get_items() verifies that we don't have `include` set, and default
* ordering is by `name`.
*/
if ( ! in_array( $prepared_args['orderby'], array( 'name', 'none', 'include' ), true ) ) {
switch ( $prepared_args['orderby'] ) {
case 'id':
$this->sort_column = 'term_id';
break;
case 'slug':
case 'term_group':
case 'description':
case 'count':
$this->sort_column = $prepared_args['orderby'];
break;
}
usort( $query_result, array( $this, 'compare_terms' ) );
}
if ( strtolower( $prepared_args['order'] ) !== 'asc' ) {
$query_result = array_reverse( $query_result );
}
// Pagination.
$this->total_terms = count( $query_result );
$query_result = array_slice( $query_result, $prepared_args['offset'], $prepared_args['number'] );
return $query_result;
}
/**
* Comparison function for sorting terms by a column.
*
* Uses `$this->sort_column` to determine field to sort by.
*
* @access protected
*
* @param stdClass $left Term object.
* @param stdClass $right Term object.
* @return int <0 if left is higher "priority" than right, 0 if equal, >0 if right is higher "priority" than left.
*/
protected function compare_terms( $left, $right ) {
$col = $this->sort_column;
$left_val = $left->$col;
$right_val = $right->$col;
if ( is_int( $left_val ) && is_int( $right_val ) ) {
return $left_val - $right_val;
}
return strcmp( $left_val, $right_val );
}
/**
* Checks if a request has access to read the specified term.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$tax_obj = get_taxonomy( $this->taxonomy );
if ( ! $tax_obj || ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
return false;
}
if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->edit_terms ) ) {
return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Gets a single term from a taxonomy.
*
* @param WP_REST_Request $request Full details about the request
* @return WP_REST_Request|WP_Error
*/
public function get_item( $request ) {
$term = get_term( (int) $request['id'], $this->taxonomy );
if ( ! $term || $term->taxonomy !== $this->taxonomy ) {
return new WP_Error( 'rest_term_invalid', __( "Resource doesn't exist." ), array( 'status' => 404 ) );
}
if ( is_wp_error( $term ) ) {
return $term;
}
$response = $this->prepare_item_for_response( $term, $request );
return rest_ensure_response( $response );
}
/**
* Checks if a request has access to create a term.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function create_item_permissions_check( $request ) {
if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
return false;
}
$taxonomy_obj = get_taxonomy( $this->taxonomy );
if ( ! current_user_can( $taxonomy_obj->cap->manage_terms ) ) {
return new WP_Error( 'rest_cannot_create', __( 'Sorry, you cannot create new resource.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Creates a single term in a taxonomy.
*
* @param WP_REST_Request $request Full details about the request
* @return WP_REST_Request|WP_Error
*/
public function create_item( $request ) {
if ( isset( $request['parent'] ) ) {
if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
}
$parent = get_term( (int) $request['parent'], $this->taxonomy );
if ( ! $parent ) {
return new WP_Error( 'rest_term_invalid', __( "Parent resource doesn't exist." ), array( 'status' => 400 ) );
}
}
$prepared_term = $this->prepare_item_for_database( $request );
$term = wp_insert_term( $prepared_term->name, $this->taxonomy, $prepared_term );
if ( is_wp_error( $term ) ) {
/*
* If we're going to inform the client that the term already exists,
* give them the identifier for future use.
*/
if ( $term_id = $term->get_error_data( 'term_exists' ) ) {
$existing_term = get_term( $term_id, $this->taxonomy );
$term->add_data( $existing_term->term_id, 'term_exists' );
}
return $term;
}
$term = get_term( $term['term_id'], $this->taxonomy );
/**
* Fires after a single term is created or updated via the REST API.
*
* @param WP_Term $term Inserted Term object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating term, false when updating.
*/
do_action( "rest_insert_{$this->taxonomy}", $term, $request, true );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], (int) $request['id'] );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$fields_update = $this->update_additional_fields_for_object( $term, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'view' );
$response = $this->prepare_item_for_response( $term, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) );
return $response;
}
/**
* Checks if a request has access to update the specified term.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
return false;
}
$term = get_term( (int) $request['id'], $this->taxonomy );
if ( ! $term ) {
return new WP_Error( 'rest_term_invalid', __( "Resource doesn't exist." ), array( 'status' => 404 ) );
}
$taxonomy_obj = get_taxonomy( $this->taxonomy );
if ( ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) {
return new WP_Error( 'rest_cannot_update', __( 'Sorry, you cannot update resource.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Updates a single term from a taxonomy.
*
* @param WP_REST_Request $request Full details about the request
* @return WP_REST_Request|WP_Error
*/
public function update_item( $request ) {
if ( isset( $request['parent'] ) ) {
if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
}
$parent = get_term( (int) $request['parent'], $this->taxonomy );
if ( ! $parent ) {
return new WP_Error( 'rest_term_invalid', __( "Parent resource doesn't exist." ), array( 'status' => 400 ) );
}
}
$prepared_term = $this->prepare_item_for_database( $request );
$term = get_term( (int) $request['id'], $this->taxonomy );
// Only update the term if we haz something to update.
if ( ! empty( $prepared_term ) ) {
$update = wp_update_term( $term->term_id, $term->taxonomy, (array) $prepared_term );
if ( is_wp_error( $update ) ) {
return $update;
}
}
$term = get_term( (int) $request['id'], $this->taxonomy );
/* This action is documented in lib/endpoints/class-wp-rest-terms-controller.php */
do_action( "rest_insert_{$this->taxonomy}", $term, $request, false );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], (int) $request['id'] );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$fields_update = $this->update_additional_fields_for_object( $term, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'view' );
$response = $this->prepare_item_for_response( $term, $request );
return rest_ensure_response( $response );
}
/**
* Checks if a request has access to delete the specified term.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function delete_item_permissions_check( $request ) {
if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
return false;
}
$term = get_term( (int) $request['id'], $this->taxonomy );
if ( ! $term ) {
return new WP_Error( 'rest_term_invalid', __( "Resource doesn't exist." ), array( 'status' => 404 ) );
}
$taxonomy_obj = get_taxonomy( $this->taxonomy );
if ( ! current_user_can( $taxonomy_obj->cap->delete_terms ) ) {
return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you cannot delete resource.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Deletes a single term from a taxonomy.
*
* @param WP_REST_Request $request Full details about the request
* @return WP_REST_Response|WP_Error
*/
public function delete_item( $request ) {
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for this resource type.
if ( ! $force ) {
return new WP_Error( 'rest_trash_not_supported', __( 'Resource does not support trashing.' ), array( 'status' => 501 ) );
}
$term = get_term( (int) $request['id'], $this->taxonomy );
$request->set_param( 'context', 'view' );
$response = $this->prepare_item_for_response( $term, $request );
$retval = wp_delete_term( $term->term_id, $term->taxonomy );
if ( ! $retval ) {
return new WP_Error( 'rest_cannot_delete', __( 'The resource cannot be deleted.' ), array( 'status' => 500 ) );
}
/**
* Fires after a single term is deleted via the REST API.
*
* @param WP_Term $term The deleted term.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( "rest_delete_{$this->taxonomy}", $term, $response, $request );
return $response;
}
/**
* Prepares a single term for create or update.
*
* @param WP_REST_Request $request Request object.
* @return object $prepared_term Term object.
*/
public function prepare_item_for_database( $request ) {
$prepared_term = new stdClass;
$schema = $this->get_item_schema();
if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
$prepared_term->name = $request['name'];
}
if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) {
$prepared_term->slug = $request['slug'];
}
if ( isset( $request['taxonomy'] ) && ! empty( $schema['properties']['taxonomy'] ) ) {
$prepared_term->taxonomy = $request['taxonomy'];
}
if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) {
$prepared_term->description = $request['description'];
}
if ( isset( $request['parent'] ) && ! empty( $schema['properties']['parent'] ) ) {
$parent_term_id = 0;
$parent_term = get_term( (int) $request['parent'], $this->taxonomy );
if ( $parent_term ) {
$parent_term_id = $parent_term->term_id;
}
$prepared_term->parent = $parent_term_id;
}
/**
* Filters term data before inserting term via the REST API.
*
* @param object $prepared_term Term object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "rest_pre_insert_{$this->taxonomy}", $prepared_term, $request );
}
/**
* Prepares a single term output for response.
*
* @param obj $item Term object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response
*/
public function prepare_item_for_response( $item, $request ) {
$schema = $this->get_item_schema();
$data = array();
if ( ! empty( $schema['properties']['id'] ) ) {
$data['id'] = (int) $item->term_id;
}
if ( ! empty( $schema['properties']['count'] ) ) {
$data['count'] = (int) $item->count;
}
if ( ! empty( $schema['properties']['description'] ) ) {
$data['description'] = $item->description;
}
if ( ! empty( $schema['properties']['link'] ) ) {
$data['link'] = get_term_link( $item );
}
if ( ! empty( $schema['properties']['name'] ) ) {
$data['name'] = $item->name;
}
if ( ! empty( $schema['properties']['slug'] ) ) {
$data['slug'] = $item->slug;
}
if ( ! empty( $schema['properties']['taxonomy'] ) ) {
$data['taxonomy'] = $item->taxonomy;
}
if ( ! empty( $schema['properties']['parent'] ) ) {
$data['parent'] = (int) $item->parent;
}
if ( ! empty( $schema['properties']['meta'] ) ) {
$data['meta'] = $this->meta->get_value( $item->term_id, $request );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item ) );
/**
* Filters a term item returned from the API.
*
* Allows modification of the term data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $item The original term object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( "rest_prepare_{$this->taxonomy}", $response, $item, $request );
}
/**
* Prepares links for the request.
*
* @param object $term Term object.
* @return array Links for the given term.
*/
protected function prepare_links( $term ) {
$base = $this->namespace . '/' . $this->rest_base;
$links = array(
'self' => array(
'href' => rest_url( trailingslashit( $base ) . $term->term_id ),
),
'collection' => array(
'href' => rest_url( $base ),
),
'about' => array(
'href' => rest_url( sprintf( 'wp/v2/taxonomies/%s', $this->taxonomy ) ),
),
);
if ( $term->parent ) {
$parent_term = get_term( (int) $term->parent, $term->taxonomy );
if ( $parent_term ) {
$links['up'] = array(
'href' => rest_url( trailingslashit( $base ) . $parent_term->term_id ),
'embeddable' => true,
);
}
}
$taxonomy_obj = get_taxonomy( $term->taxonomy );
if ( empty( $taxonomy_obj->object_type ) ) {
return $links;
}
$post_type_links = array();
foreach ( $taxonomy_obj->object_type as $type ) {
$post_type_object = get_post_type_object( $type );
if ( empty( $post_type_object->show_in_rest ) ) {
continue;
}
$rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
$post_type_links[] = array(
'href' => add_query_arg( $this->rest_base, $term->term_id, rest_url( sprintf( 'wp/v2/%s', $rest_base ) ) ),
);
}
if ( ! empty( $post_type_links ) ) {
$links['https://api.w.org/post_type'] = $post_type_links;
}
return $links;
}
/**
* Gets the term's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'post_tag' === $this->taxonomy ? 'tag' : $this->taxonomy,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.' ),
'type' => 'integer',
'context' => array( 'view', 'embed', 'edit' ),
'readonly' => true,
),
'count' => array(
'description' => __( 'Number of published posts for the resource.' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'HTML description of the resource.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'wp_filter_post_kses',
),
),
'link' => array(
'description' => __( 'URL to the resource.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'embed', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'HTML title for the resource.' ),
'type' => 'string',
'context' => array( 'view', 'embed', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
'required' => true,
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource unique to its type.' ),
'type' => 'string',
'context' => array( 'view', 'embed', 'edit' ),
'arg_options' => array(
'sanitize_callback' => array( $this, 'sanitize_slug' ),
),
),
'taxonomy' => array(
'description' => __( 'Type attribution for the resource.' ),
'type' => 'string',
'enum' => array_keys( get_taxonomies() ),
'context' => array( 'view', 'embed', 'edit' ),
'readonly' => true,
),
),
);
$taxonomy = get_taxonomy( $this->taxonomy );
if ( $taxonomy->hierarchical ) {
$schema['properties']['parent'] = array(
'description' => __( 'The id for the parent of the resource.' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
);
}
$schema['properties']['meta'] = $this->meta->get_field_schema();
return $this->add_additional_fields_schema( $schema );
}
/**
* Gets the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$taxonomy = get_taxonomy( $this->taxonomy );
$query_params['context']['default'] = 'view';
$query_params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific ids.' ),
'type' => 'array',
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$query_params['include'] = array(
'description' => __( 'Limit result set to specific ids.' ),
'type' => 'array',
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
if ( ! $taxonomy->hierarchical ) {
$query_params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
}
$query_params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'default' => 'asc',
'enum' => array(
'asc',
'desc',
),
'validate_callback' => 'rest_validate_request_arg',
);
$query_params['orderby'] = array(
'description' => __( 'Sort collection by resource attribute.' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'default' => 'name',
'enum' => array(
'id',
'include',
'name',
'slug',
'term_group',
'description',
'count',
),
'validate_callback' => 'rest_validate_request_arg',
);
$query_params['hide_empty'] = array(
'description' => __( 'Whether to hide resources not assigned to any posts.' ),
'type' => 'boolean',
'default' => false,
'validate_callback' => 'rest_validate_request_arg',
);
if ( $taxonomy->hierarchical ) {
$query_params['parent'] = array(
'description' => __( 'Limit result set to resources assigned to a specific parent.' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
}
$query_params['post'] = array(
'description' => __( 'Limit result set to resources assigned to a specific post.' ),
'type' => 'integer',
'default' => null,
'validate_callback' => 'rest_validate_request_arg',
);
$query_params['slug'] = array(
'description' => __( 'Limit result set to resources with a specific slug.' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
return $query_params;
}
/**
* Checks that the taxonomy is valid.
*
* @param string $taxonomy Taxonomy to check.
* @return WP_Error|boolean
*/
protected function check_is_taxonomy_allowed( $taxonomy ) {
$taxonomy_obj = get_taxonomy( $taxonomy );
if ( $taxonomy_obj && ! empty( $taxonomy_obj->show_in_rest ) ) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,990 @@
<?php
/**
* Access users
*/
class WP_REST_Users_Controller extends WP_REST_Controller {
/**
* Instance of a user meta fields object.
*
* @access protected
* @var WP_REST_User_Meta_Fields
*/
protected $meta;
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'users';
$this->meta = new WP_REST_User_Meta_Fields();
}
/**
* Register the routes for the objects of the controller.
*/
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(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
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 ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'description' => __( 'Required to be true, as resource does not support trashing.' ),
),
'reassign' => array(),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/me', array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_current_item' ),
'args' => array(
'context' => array(),
),
'schema' => array( $this, 'get_public_item_schema' ),
));
}
/**
* Permissions check for getting all users.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
// Check if roles is specified in GET request and if user can list users.
if ( ! empty( $request['roles'] ) && ! current_user_can( 'list_users' ) ) {
return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot filter by role.' ), array( 'status' => rest_authorization_required_code() ) );
}
if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
}
if ( in_array( $request['orderby'], array( 'email', 'registered_date' ), true ) && ! current_user_can( 'list_users' ) ) {
return new WP_Error( 'rest_forbidden_orderby', __( 'Sorry, you cannot order by this parameter.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all users
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
// Retrieve the list of registered collection query parameters.
$registered = $this->get_collection_params();
// This array defines mappings between public API query parameters whose
// values are accepted as-passed, and their internal WP_Query parameter
// name equivalents (some are the same). Only values which are also
// present in $registered will be set.
$parameter_mappings = array(
'exclude' => 'exclude',
'include' => 'include',
'order' => 'order',
'per_page' => 'number',
'search' => 'search',
'roles' => 'role__in',
);
$prepared_args = array();
// For each known parameter which is both registered and present in the request,
// set the parameter's value on the query $prepared_args.
foreach ( $parameter_mappings as $api_param => $wp_param ) {
if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
$prepared_args[ $wp_param ] = $request[ $api_param ];
}
}
if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) {
$prepared_args['offset'] = $request['offset'];
} else {
$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
}
if ( isset( $registered['orderby'] ) ) {
$orderby_possibles = array(
'id' => 'ID',
'include' => 'include',
'name' => 'display_name',
'registered_date' => 'registered',
'slug' => 'user_nicename',
'email' => 'user_email',
'url' => 'user_url',
);
$prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
}
if ( ! current_user_can( 'list_users' ) ) {
$prepared_args['has_published_posts'] = true;
}
if ( ! empty( $prepared_args['search'] ) ) {
$prepared_args['search'] = '*' . $prepared_args['search'] . '*';
}
if ( isset( $registered['slug'] ) && ! empty( $request['slug'] ) ) {
$prepared_args['search'] = $request['slug'];
$prepared_args['search_columns'] = array( 'user_nicename' );
}
/**
* Filter arguments, before passing to WP_User_Query, when querying users via the REST API.
*
* @see https://developer.wordpress.org/reference/classes/wp_user_query/
*
* @param array $prepared_args Array of arguments for WP_User_Query.
* @param WP_REST_Request $request The current request.
*/
$prepared_args = apply_filters( 'rest_user_query', $prepared_args, $request );
$query = new WP_User_Query( $prepared_args );
$users = array();
foreach ( $query->results as $user ) {
$data = $this->prepare_item_for_response( $user, $request );
$users[] = $this->prepare_response_for_collection( $data );
}
$response = rest_ensure_response( $users );
// Store pagination values for headers then unset for count query.
$per_page = (int) $prepared_args['number'];
$page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
$prepared_args['fields'] = 'ID';
$total_users = $query->get_total();
if ( $total_users < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count
unset( $prepared_args['number'], $prepared_args['offset'] );
$count_query = new WP_User_Query( $prepared_args );
$total_users = $count_query->get_total();
}
$response->header( 'X-WP-Total', (int) $total_users );
$max_pages = ceil( $total_users / $per_page );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Check if a given request has access to read a user
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$id = (int) $request['id'];
$user = get_userdata( $id );
$types = get_post_types( array( 'show_in_rest' => true ), 'names' );
if ( empty( $id ) || empty( $user->ID ) ) {
return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
}
if ( get_current_user_id() === $id ) {
return true;
}
if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
} elseif ( ! count_user_posts( $id, $types ) && ! current_user_can( 'edit_user', $id ) && ! current_user_can( 'list_users' ) ) {
return new WP_Error( 'rest_user_cannot_view', __( 'Sorry, you cannot view this resource.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get a single user
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$id = (int) $request['id'];
$user = get_userdata( $id );
if ( empty( $id ) || empty( $user->ID ) ) {
return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
}
$user = $this->prepare_item_for_response( $user, $request );
$response = rest_ensure_response( $user );
return $response;
}
/**
* Get the current user
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_current_item( $request ) {
$current_user_id = get_current_user_id();
if ( empty( $current_user_id ) ) {
return new WP_Error( 'rest_not_logged_in', __( 'You are not currently logged in.' ), array( 'status' => 401 ) );
}
$user = wp_get_current_user();
$response = $this->prepare_item_for_response( $user, $request );
$response = rest_ensure_response( $response );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $current_user_id ) ) );
$response->set_status( 302 );
return $response;
}
/**
* Check if a given request has access create users
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function create_item_permissions_check( $request ) {
if ( ! current_user_can( 'create_users' ) ) {
return new WP_Error( 'rest_cannot_create_user', __( 'Sorry, you are not allowed to create resource.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Create a single user
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
return new WP_Error( 'rest_user_exists', __( 'Cannot create existing resource.' ), array( 'status' => 400 ) );
}
$schema = $this->get_item_schema();
if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) {
$check_permission = $this->check_role_update( $request['id'], $request['roles'] );
if ( is_wp_error( $check_permission ) ) {
return $check_permission;
}
}
$user = $this->prepare_item_for_database( $request );
if ( is_multisite() ) {
$ret = wpmu_validate_user_signup( $user->user_login, $user->user_email );
if ( is_wp_error( $ret['errors'] ) && ! empty( $ret['errors']->errors ) ) {
return $ret['errors'];
}
}
if ( is_multisite() ) {
$user_id = wpmu_create_user( $user->user_login, $user->user_pass, $user->user_email );
if ( ! $user_id ) {
return new WP_Error( 'rest_user_create', __( 'Error creating new resource.' ), array( 'status' => 500 ) );
}
$user->ID = $user_id;
$user_id = wp_update_user( $user );
if ( is_wp_error( $user_id ) ) {
return $user_id;
}
} else {
$user_id = wp_insert_user( $user );
if ( is_wp_error( $user_id ) ) {
return $user_id;
}
}
$user = get_user_by( 'id', $user_id );
if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) {
array_map( array( $user, 'add_role' ), $request['roles'] );
}
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $user_id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$fields_update = $this->update_additional_fields_for_object( $user, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
/**
* Fires after a user is created or updated via the REST API.
*
* @param WP_User $user Data used to create the user.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating user, false when updating user.
*/
do_action( 'rest_insert_user', $user, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $user, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user_id ) ) );
return $response;
}
/**
* Check if a given request has access update a user
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
$id = (int) $request['id'];
if ( ! current_user_can( 'edit_user', $id ) ) {
return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit resource.' ), array( 'status' => rest_authorization_required_code() ) );
}
if ( ! empty( $request['roles'] ) && ! current_user_can( 'edit_users' ) ) {
return new WP_Error( 'rest_cannot_edit_roles', __( 'Sorry, you are not allowed to edit roles of this resource.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Update a single user
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
$id = (int) $request['id'];
$user = get_userdata( $id );
if ( ! $user ) {
return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
}
if ( email_exists( $request['email'] ) && $request['email'] !== $user->user_email ) {
return new WP_Error( 'rest_user_invalid_email', __( 'Email address is invalid.' ), array( 'status' => 400 ) );
}
if ( ! empty( $request['username'] ) && $request['username'] !== $user->user_login ) {
return new WP_Error( 'rest_user_invalid_argument', __( "Username isn't editable." ), array( 'status' => 400 ) );
}
if ( ! empty( $request['slug'] ) && $request['slug'] !== $user->user_nicename && get_user_by( 'slug', $request['slug'] ) ) {
return new WP_Error( 'rest_user_invalid_slug', __( 'Slug is invalid.' ), array( 'status' => 400 ) );
}
if ( ! empty( $request['roles'] ) ) {
$check_permission = $this->check_role_update( $id, $request['roles'] );
if ( is_wp_error( $check_permission ) ) {
return $check_permission;
}
}
$user = $this->prepare_item_for_database( $request );
// Ensure we're operating on the same user we already checked
$user->ID = $id;
$user_id = wp_update_user( $user );
if ( is_wp_error( $user_id ) ) {
return $user_id;
}
$user = get_user_by( 'id', $id );
if ( ! empty( $request['roles'] ) ) {
array_map( array( $user, 'add_role' ), $request['roles'] );
}
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$fields_update = $this->update_additional_fields_for_object( $user, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
/* This action is documented in lib/endpoints/class-wp-rest-users-controller.php */
do_action( 'rest_insert_user', $user, $request, false );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $user, $request );
$response = rest_ensure_response( $response );
return $response;
}
/**
* Check if a given request has access delete a user
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function delete_item_permissions_check( $request ) {
$id = (int) $request['id'];
if ( ! current_user_can( 'delete_user', $id ) ) {
return new WP_Error( 'rest_user_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Delete a single user
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function delete_item( $request ) {
$id = (int) $request['id'];
$reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null;
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for this type, error out
if ( ! $force ) {
return new WP_Error( 'rest_trash_not_supported', __( 'Users do not support trashing.' ), array( 'status' => 501 ) );
}
$user = get_userdata( $id );
if ( ! $user ) {
return new WP_Error( 'rest_user_invalid_id', __( 'Invalid resource id.' ), array( 'status' => 404 ) );
}
if ( ! empty( $reassign ) ) {
if ( $reassign === $id || ! get_userdata( $reassign ) ) {
return new WP_Error( 'rest_user_invalid_reassign', __( 'Invalid resource id for reassignment.' ), array( 'status' => 400 ) );
}
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $user, $request );
/** Include admin user functions to get access to wp_delete_user() */
require_once ABSPATH . 'wp-admin/includes/user.php';
$result = wp_delete_user( $id, $reassign );
if ( ! $result ) {
return new WP_Error( 'rest_cannot_delete', __( 'The resource cannot be deleted.' ), array( 'status' => 500 ) );
}
/**
* Fires after a user is deleted via the REST API.
*
* @param WP_User $user The user data.
* @param WP_REST_Response $response The response returned from the API.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'rest_delete_user', $user, $response, $request );
return $response;
}
/**
* Prepare a single user output for response
*
* @param object $user User object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $user, $request ) {
$data = array();
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['id'] ) ) {
$data['id'] = $user->ID;
}
if ( ! empty( $schema['properties']['username'] ) ) {
$data['username'] = $user->user_login;
}
if ( ! empty( $schema['properties']['name'] ) ) {
$data['name'] = $user->display_name;
}
if ( ! empty( $schema['properties']['first_name'] ) ) {
$data['first_name'] = $user->first_name;
}
if ( ! empty( $schema['properties']['last_name'] ) ) {
$data['last_name'] = $user->last_name;
}
if ( ! empty( $schema['properties']['email'] ) ) {
$data['email'] = $user->user_email;
}
if ( ! empty( $schema['properties']['url'] ) ) {
$data['url'] = $user->user_url;
}
if ( ! empty( $schema['properties']['description'] ) ) {
$data['description'] = $user->description;
}
if ( ! empty( $schema['properties']['link'] ) ) {
$data['link'] = get_author_posts_url( $user->ID, $user->user_nicename );
}
if ( ! empty( $schema['properties']['nickname'] ) ) {
$data['nickname'] = $user->nickname;
}
if ( ! empty( $schema['properties']['slug'] ) ) {
$data['slug'] = $user->user_nicename;
}
if ( ! empty( $schema['properties']['roles'] ) ) {
// Defensively call array_values() to ensure an array is returned.
$data['roles'] = array_values( $user->roles );
}
if ( ! empty( $schema['properties']['registered_date'] ) ) {
$data['registered_date'] = date( 'c', strtotime( $user->user_registered ) );
}
if ( ! empty( $schema['properties']['capabilities'] ) ) {
$data['capabilities'] = (object) $user->allcaps;
}
if ( ! empty( $schema['properties']['extra_capabilities'] ) ) {
$data['extra_capabilities'] = (object) $user->caps;
}
if ( ! empty( $schema['properties']['avatar_urls'] ) ) {
$data['avatar_urls'] = rest_get_avatar_urls( $user->user_email );
}
if ( ! empty( $schema['properties']['meta'] ) ) {
$data['meta'] = $this->meta->get_value( $user->ID, $request );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'embed';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $user ) );
/**
* Filter user data returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param object $user User object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'rest_prepare_user', $response, $user, $request );
}
/**
* Prepare links for the request.
*
* @param WP_Post $user User object.
* @return array Links for the given user.
*/
protected function prepare_links( $user ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user->ID ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Prepare a single user for create or update
*
* @param WP_REST_Request $request Request object.
* @return object $prepared_user User object.
*/
protected function prepare_item_for_database( $request ) {
$prepared_user = new stdClass;
$schema = $this->get_item_schema();
// required arguments.
if ( isset( $request['email'] ) && ! empty( $schema['properties']['email'] ) ) {
$prepared_user->user_email = $request['email'];
}
if ( isset( $request['username'] ) && ! empty( $schema['properties']['username'] ) ) {
$prepared_user->user_login = $request['username'];
}
if ( isset( $request['password'] ) && ! empty( $schema['properties']['password'] ) ) {
$prepared_user->user_pass = $request['password'];
}
// optional arguments.
if ( isset( $request['id'] ) ) {
$prepared_user->ID = absint( $request['id'] );
}
if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
$prepared_user->display_name = $request['name'];
}
if ( isset( $request['first_name'] ) && ! empty( $schema['properties']['first_name'] ) ) {
$prepared_user->first_name = $request['first_name'];
}
if ( isset( $request['last_name'] ) && ! empty( $schema['properties']['last_name'] ) ) {
$prepared_user->last_name = $request['last_name'];
}
if ( isset( $request['nickname'] ) && ! empty( $schema['properties']['nickname'] ) ) {
$prepared_user->nickname = $request['nickname'];
}
if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) {
$prepared_user->user_nicename = $request['slug'];
}
if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) {
$prepared_user->description = $request['description'];
}
if ( isset( $request['url'] ) && ! empty( $schema['properties']['url'] ) ) {
$prepared_user->user_url = $request['url'];
}
// setting roles will be handled outside of this function.
if ( isset( $request['roles'] ) ) {
$prepared_user->role = false;
}
/**
* Filter user data before inserting user via the REST API.
*
* @param object $prepared_user User object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'rest_pre_insert_user', $prepared_user, $request );
}
/**
* Determine if the current user is allowed to make the desired roles change.
*
* @param integer $user_id User ID.
* @param array $roles New user roles.
* @return WP_Error|boolean
*/
protected function check_role_update( $user_id, $roles ) {
global $wp_roles;
foreach ( $roles as $role ) {
if ( ! isset( $wp_roles->role_objects[ $role ] ) ) {
return new WP_Error( 'rest_user_invalid_role', sprintf( __( 'The role %s does not exist.' ), $role ), array( 'status' => 400 ) );
}
$potential_role = $wp_roles->role_objects[ $role ];
// Don't let anyone with 'edit_users' (admins) edit their own role to something without it.
// Multisite super admins can freely edit their blog roles -- they possess all caps.
if ( ! ( is_multisite() && current_user_can( 'manage_sites' ) ) && get_current_user_id() === $user_id && ! $potential_role->has_cap( 'edit_users' ) ) {
return new WP_Error( 'rest_user_invalid_role', __( 'You cannot give resource that role.' ), array( 'status' => rest_authorization_required_code() ) );
}
// The new role must be editable by the logged-in user.
/** Include admin functions to get access to get_editable_roles() */
require_once ABSPATH . 'wp-admin/includes/admin.php';
$editable_roles = get_editable_roles();
if ( empty( $editable_roles[ $role ] ) ) {
return new WP_Error( 'rest_user_invalid_role', __( 'You cannot give resource that role.' ), array( 'status' => 403 ) );
}
}
return true;
}
/**
* Get the User's schema, conforming to JSON Schema
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'user',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.' ),
'type' => 'integer',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'username' => array(
'description' => __( 'Login name for the resource.' ),
'type' => 'string',
'context' => array( 'edit' ),
'required' => true,
'arg_options' => array(
'sanitize_callback' => 'sanitize_user',
),
),
'name' => array(
'description' => __( 'Display name for the resource.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'first_name' => array(
'description' => __( 'First name for the resource.' ),
'type' => 'string',
'context' => array( 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'last_name' => array(
'description' => __( 'Last name for the resource.' ),
'type' => 'string',
'context' => array( 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'email' => array(
'description' => __( 'The email address for the resource.' ),
'type' => 'string',
'format' => 'email',
'context' => array( 'edit' ),
'required' => true,
),
'url' => array(
'description' => __( 'URL of the resource.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'embed', 'view', 'edit' ),
),
'description' => array(
'description' => __( 'Description of the resource.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'wp_filter_post_kses',
),
),
'link' => array(
'description' => __( 'Author URL to the resource.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'nickname' => array(
'description' => __( 'The nickname for the resource.' ),
'type' => 'string',
'context' => array( 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => array( $this, 'sanitize_slug' ),
),
),
'registered_date' => array(
'description' => __( 'Registration date for the resource.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'edit' ),
'readonly' => true,
),
'roles' => array(
'description' => __( 'Roles assigned to the resource.' ),
'type' => 'array',
'context' => array( 'edit' ),
),
'password' => array(
'description' => __( 'Password for the resource (never included).' ),
'type' => 'string',
'context' => array(), // Password is never displayed
'required' => true,
),
'capabilities' => array(
'description' => __( 'All capabilities assigned to the resource.' ),
'type' => 'object',
'context' => array( 'edit' ),
'readonly' => true,
),
'extra_capabilities' => array(
'description' => __( 'Any extra capabilities assigned to the resource.' ),
'type' => 'object',
'context' => array( 'edit' ),
'readonly' => true,
),
),
);
if ( get_option( 'show_avatars' ) ) {
$avatar_properties = array();
$avatar_sizes = rest_get_avatar_sizes();
foreach ( $avatar_sizes as $size ) {
$avatar_properties[ $size ] = array(
'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'embed', 'view', 'edit' ),
);
}
$schema['properties']['avatar_urls'] = array(
'description' => __( 'Avatar URLs for the resource.' ),
'type' => 'object',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
'properties' => $avatar_properties,
);
}
$schema['properties']['meta'] = $this->meta->get_field_schema();
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections
*
* @return array
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$query_params['context']['default'] = 'view';
$query_params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific ids.' ),
'type' => 'array',
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$query_params['include'] = array(
'description' => __( 'Limit result set to specific ids.' ),
'type' => 'array',
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$query_params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$query_params['order'] = array(
'default' => 'asc',
'description' => __( 'Order sort attribute ascending or descending.' ),
'enum' => array( 'asc', 'desc' ),
'sanitize_callback' => 'sanitize_key',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$query_params['orderby'] = array(
'default' => 'name',
'description' => __( 'Sort collection by object attribute.' ),
'enum' => array(
'id',
'include',
'name',
'registered_date',
'slug',
'email',
'url',
),
'sanitize_callback' => 'sanitize_key',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$query_params['slug'] = array(
'description' => __( 'Limit result set to resources with a specific slug.' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$query_params['roles'] = array(
'description' => __( 'Limit result set to resources matching at least one specific role provided. Accepts csv list or single role.' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
);
return $query_params;
}
}

View File

@ -0,0 +1,21 @@
<?php
class WP_REST_Comment_Meta_Fields extends WP_REST_Meta_Fields {
/**
* Get the object type for meta.
*
* @return string
*/
protected function get_meta_type() {
return 'comment';
}
/**
* Get the type for `register_rest_field`.
*
* @return string
*/
public function get_rest_field_type() {
return 'comment';
}
}

View File

@ -0,0 +1,365 @@
<?php
/**
* Manage meta values for an object.
*/
abstract class WP_REST_Meta_Fields {
/**
* Get the object type for meta.
*
* @return string One of 'post', 'comment', 'term', 'user', or anything
* else supported by `_get_meta_table()`.
*/
abstract protected function get_meta_type();
/**
* Get the object type for `register_rest_field`.
*
* @return string Custom post type, 'taxonomy', 'comment', or `user`.
*/
abstract protected function get_rest_field_type();
/**
* Register the meta field.
*/
public function register_field() {
register_rest_field( $this->get_rest_field_type(), 'meta', array(
'get_callback' => array( $this, 'get_value' ),
'update_callback' => array( $this, 'update_value' ),
'schema' => $this->get_field_schema(),
));
}
/**
* Get the `meta` field value.
*
* @param int $object_id Object ID to fetch meta for.
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|object
*/
public function get_value( $object_id, $request ) {
$fields = $this->get_registered_fields();
$response = array();
foreach ( $fields as $name => $args ) {
$all_values = get_metadata( $this->get_meta_type(), $object_id, $name, false );
if ( $args['single'] ) {
if ( empty( $all_values ) ) {
$value = $args['schema']['default'];
} else {
$value = $all_values[0];
}
$value = $this->prepare_value_for_response( $value, $request, $args );
} else {
$value = array();
foreach ( $all_values as $row ) {
$value[] = $this->prepare_value_for_response( $row, $request, $args );
}
}
$response[ $name ] = $value;
}
return (object) $response;
}
/**
* Prepare value for response.
*
* This is required because some native types cannot be stored correctly in
* the database, such as booleans. We need to cast back to the relevant
* type before passing back to JSON.
*
* @param mixed $value Value to prepare.
* @param WP_REST_Request $request Current request object.
* @param array $args Options for the field.
* @return mixed Prepared value.
*/
protected function prepare_value_for_response( $value, $request, $args ) {
if ( ! empty( $args['prepare_callback'] ) ) {
$value = call_user_func( $args['prepare_callback'], $value, $request, $args );
}
return $value;
}
/**
* Update meta values.
*
* @param WP_REST_Request $request Full details about the request.
* @param int $object_id Object ID to fetch meta for.
* @return WP_Error|null Error if one occurs, null on success.
*/
public function update_value( $request, $object_id ) {
$fields = $this->get_registered_fields();
foreach ( $fields as $name => $args ) {
if ( ! array_key_exists( $name, $request ) ) {
continue;
}
// A null value means reset the field, which is essentially deleting it
// from the database and then relying on the default value.
if ( is_null( $request[ $name ] ) ) {
$result = $this->delete_meta_value( $object_id, $name );
} elseif ( $args['single'] ) {
$result = $this->update_meta_value( $object_id, $name, $request[ $name ] );
} else {
$result = $this->update_multi_meta_value( $object_id, $name, $request[ $name ] );
}
if ( is_wp_error( $result ) ) {
return $result;
}
}
return null;
}
/**
* Delete meta value for an object.
*
* @param int $object_id Object ID the field belongs to.
* @param string $name Key for the field.
* @return bool|WP_Error True if meta field is deleted, error otherwise.
*/
protected function delete_meta_value( $object_id, $name ) {
if ( ! current_user_can( 'delete_post_meta', $object_id, $name ) ) {
return new WP_Error(
'rest_cannot_delete',
sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
array( 'key' => $name, 'status' => rest_authorization_required_code() )
);
}
if ( ! delete_metadata( $this->get_meta_type(), $object_id, wp_slash( $name ) ) ) {
return new WP_Error(
'rest_meta_database_error',
__( 'Could not delete meta value from database.' ),
array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR )
);
}
return true;
}
/**
* Update multiple meta values for an object.
*
* Alters the list of values in the database to match the list of provided values.
*
* @param int $object_id Object ID to update.
* @param string $name Key for the custom field.
* @param array $values List of values to update to.
* @return bool|WP_Error True if meta fields are updated, error otherwise.
*/
protected function update_multi_meta_value( $object_id, $name, $values ) {
if ( ! current_user_can( 'edit_post_meta', $object_id, $name ) ) {
return new WP_Error(
'rest_cannot_update',
sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
array( 'key' => $name, 'status' => rest_authorization_required_code() )
);
}
$current = get_metadata( $this->get_meta_type(), $object_id, $name, false );
$to_remove = $current;
$to_add = $values;
foreach ( $to_add as $add_key => $value ) {
$remove_keys = array_keys( $to_remove, $value, true );
if ( empty( $remove_keys ) ) {
continue;
}
if ( count( $remove_keys ) > 1 ) {
// To remove, we need to remove first, then add, so don't touch.
continue;
}
$remove_key = $remove_keys[0];
unset( $to_remove[ $remove_key ] );
unset( $to_add[ $add_key ] );
}
// `delete_metadata` removes _all_ instances of the value, so only call
// once.
$to_remove = array_unique( $to_remove );
foreach ( $to_remove as $value ) {
if ( ! delete_metadata( $this->get_meta_type(), $object_id, wp_slash( $name ), wp_slash( $value ) ) ) {
return new WP_Error(
'rest_meta_database_error',
__( 'Could not update meta value in database.' ),
array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR )
);
}
}
foreach ( $to_add as $value ) {
if ( ! add_metadata( $this->get_meta_type(), $object_id, wp_slash( $name ), wp_slash( $value ) ) ) {
return new WP_Error(
'rest_meta_database_error',
__( 'Could not update meta value in database.' ),
array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR )
);
}
}
return true;
}
/**
* Update meta value for an object.
*
* @param int $object_id Object ID to update.
* @param string $name Key for the custom field.
* @param mixed $value Updated value.
* @return bool|WP_Error True if meta field is updated, error otherwise.
*/
protected function update_meta_value( $object_id, $name, $value ) {
if ( ! current_user_can( 'edit_post_meta', $object_id, $name ) ) {
return new WP_Error(
'rest_cannot_update',
sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
array( 'key' => $name, 'status' => rest_authorization_required_code() )
);
}
$meta_type = $this->get_meta_type();
$meta_key = wp_slash( $name );
$meta_value = wp_slash( $value );
// Do the exact same check for a duplicate value as in update_metadata() to avoid update_metadata() returning false.
$old_value = get_metadata( $meta_type, $object_id, $meta_key );
if ( 1 === count( $old_value ) ) {
if ( $old_value[0] === $meta_value ) {
return true;
}
}
if ( ! update_metadata( $meta_type, $object_id, $meta_key, $meta_value ) ) {
return new WP_Error(
'rest_meta_database_error',
__( 'Could not update meta value in database.' ),
array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR )
);
}
return true;
}
/**
* Get all the registered meta fields.
*
* @return array
*/
protected function get_registered_fields() {
$registered = array();
foreach ( get_registered_meta_keys( $this->get_meta_type() ) as $name => $args ) {
if ( empty( $args['show_in_rest'] ) ) {
continue;
}
$rest_args = array();
if ( is_array( $args['show_in_rest'] ) ) {
$rest_args = $args['show_in_rest'];
}
$default_args = array(
'name' => $name,
'single' => $args['single'],
'schema' => array(),
'prepare_callback' => array( $this, 'prepare_value' ),
);
$default_schema = array(
'type' => null,
'description' => empty( $args['description'] ) ? '' : $args['description'],
'default' => isset( $args['default'] ) ? $args['default'] : null,
);
$rest_args = array_merge( $default_args, $rest_args );
$rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );
if ( empty( $rest_args['schema']['type'] ) ) {
// Skip over meta fields that don't have a defined type.
if ( empty( $args['type'] ) ) {
continue;
}
if ( $rest_args['single'] ) {
$rest_args['schema']['type'] = $args['type'];
} else {
$rest_args['schema']['type'] = 'array';
$rest_args['schema']['items'] = array(
'type' => $args['type'],
);
}
}
$registered[ $rest_args['name'] ] = $rest_args;
} // End foreach().
return $registered;
}
/**
* Get the object's `meta` schema, conforming to JSON Schema.
*
* @return array
*/
public function get_field_schema() {
$fields = $this->get_registered_fields();
$schema = array(
'description' => __( 'Meta fields.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(),
);
foreach ( $fields as $key => $args ) {
$schema['properties'][ $key ] = $args['schema'];
}
return $schema;
}
/**
* Prepare a meta value for output.
*
* Default preparation for meta fields. Override by passing the
* `prepare_callback` in your `show_in_rest` options.
*
* @param mixed $value Meta value from the database.
* @param WP_REST_Request $request Request object.
* @param array $args REST-specific options for the meta key.
* @return mixed Value prepared for output.
*/
public static function prepare_value( $value, $request, $args ) {
$type = $args['schema']['type'];
// For multi-value fields, check the item type instead.
if ( 'array' === $type && ! empty( $args['schema']['items']['type'] ) ) {
$type = $args['schema']['items']['type'];
}
switch ( $type ) {
case 'string':
$value = (string) $value;
break;
case 'number':
$value = (float) $value;
break;
case 'boolean':
$value = (bool) $value;
break;
}
// Don't allow objects to be output.
if ( is_object( $value ) && ! ( $value instanceof JsonSerializable ) ) {
return null;
}
return $value;
}
}

View File

@ -0,0 +1,37 @@
<?php
class WP_REST_Post_Meta_Fields extends WP_REST_Meta_Fields {
/**
* Post type to register fields for.
*
* @var string
*/
protected $post_type;
/**
* Constructor.
*
* @param string $post_type Post type to register fields for.
*/
public function __construct( $post_type ) {
$this->post_type = $post_type;
}
/**
* Get the object type for meta.
*
* @return string
*/
protected function get_meta_type() {
return 'post';
}
/**
* Get the type for `register_rest_field`.
*
* @return string Custom post type slug.
*/
public function get_rest_field_type() {
return $this->post_type;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Manage meta values for terms.
*/
class WP_REST_Term_Meta_Fields extends WP_REST_Meta_Fields {
/**
* Taxonomy to register fields for.
*
* @var string
*/
protected $taxonomy;
/**
* Constructor.
*
* @param string $taxonomy Taxonomy to register fields for.
*/
public function __construct( $taxonomy ) {
$this->taxonomy = $taxonomy;
}
/**
* Get the object type for meta.
*
* @return string
*/
protected function get_meta_type() {
return 'term';
}
/**
* Get the type for `register_rest_field`.
*
* @return string
*/
public function get_rest_field_type() {
return 'post_tag' === $this->taxonomy ? 'tag' : $this->taxonomy;
}
}

View File

@ -0,0 +1,21 @@
<?php
class WP_REST_User_Meta_Fields extends WP_REST_Meta_Fields {
/**
* Get the object type for meta.
*
* @return string
*/
protected function get_meta_type() {
return 'user';
}
/**
* Get the type for `register_rest_field`.
*
* @return string
*/
public function get_rest_field_type() {
return 'user';
}
}

View File

@ -499,6 +499,13 @@ function wp_default_scripts( &$scripts ) {
$scripts->add( 'media-audiovideo', "/wp-includes/js/media-audiovideo$suffix.js", array( 'media-editor' ), false, 1 );
$scripts->add( 'mce-view', "/wp-includes/js/mce-view$suffix.js", array( 'shortcode', 'jquery', 'media-views', 'media-audiovideo' ), false, 1 );
$scripts->add( 'wp-api', "/wp-includes/js/wp-api$suffix.js", array( 'jquery', 'backbone', 'underscore' ), false, 1 );
did_action( 'init' ) && $scripts->localize( 'wp-api', 'wpApiSettings', array(
'root' => esc_url_raw( get_rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'versionString' => 'wp/v2/',
) );
if ( is_admin() ) {
$scripts->add( 'admin-tags', "/wp-admin/js/tags$suffix.js", array( 'jquery', 'wp-ajax-response' ), false, 1 );
did_action( 'init' ) && $scripts->localize( 'admin-tags', 'tagsl10n', array(

View File

@ -67,6 +67,9 @@ function create_initial_taxonomies() {
'delete_terms' => 'delete_categories',
'assign_terms' => 'assign_categories',
),
'show_in_rest' => true,
'rest_base' => 'categories',
'rest_controller_class' => 'WP_REST_Terms_Controller',
) );
register_taxonomy( 'post_tag', 'post', array(
@ -83,6 +86,9 @@ function create_initial_taxonomies() {
'delete_terms' => 'delete_post_tags',
'assign_terms' => 'assign_post_tags',
),
'show_in_rest' => true,
'rest_base' => 'tags',
'rest_controller_class' => 'WP_REST_Terms_Controller',
) );
register_taxonomy( 'nav_menu', 'nav_menu_item', array(

View File

@ -4,7 +4,7 @@
*
* @global string $wp_version
*/
$wp_version = '4.7-alpha-38831';
$wp_version = '4.7-alpha-38832';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.

View File

@ -218,6 +218,22 @@ require( ABSPATH . WPINC . '/rest-api.php' );
require( ABSPATH . WPINC . '/rest-api/class-wp-rest-server.php' );
require( ABSPATH . WPINC . '/rest-api/class-wp-rest-response.php' );
require( ABSPATH . WPINC . '/rest-api/class-wp-rest-request.php' );
require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-controller.php' );
require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-posts-controller.php' );
require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-attachments-controller.php' );
require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-post-types-controller.php' );
require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-post-statuses-controller.php' );
require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-revisions-controller.php' );
require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-taxonomies-controller.php' );
require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-terms-controller.php' );
require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-users-controller.php' );
require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-comments-controller.php' );
require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-settings-controller.php' );
require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php' );
require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php' );
require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php' );
require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-term-meta-fields.php' );
require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-user-meta-fields.php' );
$GLOBALS['wp_embed'] = new WP_Embed();