WordPress/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php
spacedmonkey 120b1ab117 REST API: Ensure that the parent link, uses the rest_get_route_for_post function.
In [51962] the `rest_get_route_for_post` function was implemented in all places where link to a post's REST API endpoint is needed. However in this commit, the up link, which links to the parent post of the current object, did not use this function.   

Props Spacedmonkey, SergeyBiryukov.
Fixes #53656.



Built from https://develop.svn.wordpress.org/trunk@52363


git-svn-id: http://core.svn.wordpress.org/trunk@51955 1a063a9b-81f0-0310-95a4-ce76da25c4cd
2021-12-13 15:20:03 +00:00

3114 lines
93 KiB
PHP

<?php
/**
* REST API: WP_REST_Posts_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class to access posts via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Controller
*/
class WP_REST_Posts_Controller extends WP_REST_Controller {
/**
* Post type.
*
* @since 4.7.0
* @var string
*/
protected $post_type;
/**
* Instance of a post meta fields object.
*
* @since 4.7.0
* @var WP_REST_Post_Meta_Fields
*/
protected $meta;
/**
* Passwordless post access permitted.
*
* @since 5.7.1
* @var int[]
*/
protected $password_check_passed = array();
/**
* Whether the controller supports batching.
*
* @since 5.9.0
* @var array
*/
protected $allow_batch = array( 'v1' => true );
/**
* Constructor.
*
* @since 4.7.0
*
* @param string $post_type Post type.
*/
public function __construct( $post_type ) {
$this->post_type = $post_type;
$obj = get_post_type_object( $post_type );
$this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
$this->namespace = ! empty( $obj->rest_namespace ) ? $obj->rest_namespace : 'wp/v2';
$this->meta = new WP_REST_Post_Meta_Fields( $this->post_type );
}
/**
* Registers the routes for posts.
*
* @since 4.7.0
*
* @see register_rest_route()
*/
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 ),
),
'allow_batch' => $this->allow_batch,
'schema' => array( $this, 'get_public_item_schema' ),
)
);
$schema = $this->get_item_schema();
$get_item_args = array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
if ( isset( $schema['properties']['password'] ) ) {
$get_item_args['password'] = array(
'description' => __( 'The password for the post if it is password protected.' ),
'type' => 'string',
);
}
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the post.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => $get_item_args,
),
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(
'type' => 'boolean',
'default' => false,
'description' => __( 'Whether to bypass Trash and force deletion.' ),
),
),
),
'allow_batch' => $this->allow_batch,
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to read posts.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
$post_type = get_post_type_object( $this->post_type );
if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit posts in this post type.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Override the result of the post password check for REST requested posts.
*
* Allow users to read the content of password protected posts if they have
* previously passed a permission check or if they have the `edit_post` capability
* for the post being checked.
*
* @since 5.7.1
*
* @param bool $required Whether the post requires a password check.
* @param WP_Post $post The post been password checked.
* @return bool Result of password check taking in to account REST API considerations.
*/
public function check_password_required( $required, $post ) {
if ( ! $required ) {
return $required;
}
$post = get_post( $post );
if ( ! $post ) {
return $required;
}
if ( ! empty( $this->password_check_passed[ $post->ID ] ) ) {
// Password previously checked and approved.
return false;
}
return ! current_user_can( 'edit_post', $post->ID );
}
/**
* Retrieves a collection of posts.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
// Ensure a search string is set in case the orderby is set to 'relevance'.
if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] && empty( $request['search'] ) ) {
return new WP_Error(
'rest_no_search_term_defined',
__( 'You need to define a search term to order by relevance.' ),
array( 'status' => 400 )
);
}
// Ensure an include parameter is set in case the orderby is set to 'include'.
if ( ! empty( $request['orderby'] ) && 'include' === $request['orderby'] && empty( $request['include'] ) ) {
return new WP_Error(
'rest_orderby_include_missing_include',
__( 'You need to define an include parameter to order by include.' ),
array( 'status' => 400 )
);
}
// Retrieve the list of registered collection query parameters.
$registered = $this->get_collection_params();
$args = array();
/*
* 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(
'author' => 'author__in',
'author_exclude' => 'author__not_in',
'exclude' => 'post__not_in',
'include' => 'post__in',
'menu_order' => 'menu_order',
'offset' => 'offset',
'order' => 'order',
'orderby' => 'orderby',
'page' => 'paged',
'parent' => 'post_parent__in',
'parent_exclude' => 'post_parent__not_in',
'search' => 's',
'slug' => 'post_name__in',
'status' => 'post_status',
);
/*
* For each known parameter which is both registered and present in the request,
* set the parameter's value on the query $args.
*/
foreach ( $parameter_mappings as $api_param => $wp_param ) {
if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
$args[ $wp_param ] = $request[ $api_param ];
}
}
// Check for & assign any parameters which require special handling or setting.
$args['date_query'] = array();
if ( isset( $registered['before'], $request['before'] ) ) {
$args['date_query'][] = array(
'before' => $request['before'],
'column' => 'post_date',
);
}
if ( isset( $registered['modified_before'], $request['modified_before'] ) ) {
$args['date_query'][] = array(
'before' => $request['modified_before'],
'column' => 'post_modified',
);
}
if ( isset( $registered['after'], $request['after'] ) ) {
$args['date_query'][] = array(
'after' => $request['after'],
'column' => 'post_date',
);
}
if ( isset( $registered['modified_after'], $request['modified_after'] ) ) {
$args['date_query'][] = array(
'after' => $request['modified_after'],
'column' => 'post_modified',
);
}
// Ensure our per_page parameter overrides any provided posts_per_page filter.
if ( isset( $registered['per_page'] ) ) {
$args['posts_per_page'] = $request['per_page'];
}
if ( isset( $registered['sticky'], $request['sticky'] ) ) {
$sticky_posts = get_option( 'sticky_posts', array() );
if ( ! is_array( $sticky_posts ) ) {
$sticky_posts = array();
}
if ( $request['sticky'] ) {
/*
* As post__in will be used to only get sticky posts,
* we have to support the case where post__in was already
* specified.
*/
$args['post__in'] = $args['post__in'] ? array_intersect( $sticky_posts, $args['post__in'] ) : $sticky_posts;
/*
* If we intersected, but there are no post IDs in common,
* WP_Query won't return "no posts" for post__in = array()
* so we have to fake it a bit.
*/
if ( ! $args['post__in'] ) {
$args['post__in'] = array( 0 );
}
} elseif ( $sticky_posts ) {
/*
* As post___not_in will be used to only get posts that
* are not sticky, we have to support the case where post__not_in
* was already specified.
*/
$args['post__not_in'] = array_merge( $args['post__not_in'], $sticky_posts );
}
}
$args = $this->prepare_tax_query( $args, $request );
// Force the post_type argument, since it's not a user input variable.
$args['post_type'] = $this->post_type;
/**
* Filters WP_Query arguments when querying posts via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_post_query`
* - `rest_page_query`
* - `rest_attachment_query`
*
* Enables adding extra arguments or setting defaults for a post collection request.
*
* @since 4.7.0
* @since 5.7.0 Moved after the `tax_query` query arg is generated.
*
* @link https://developer.wordpress.org/reference/classes/wp_query/
*
* @param array $args Array of arguments for WP_Query.
* @param WP_REST_Request $request The REST API request.
*/
$args = apply_filters( "rest_{$this->post_type}_query", $args, $request );
$query_args = $this->prepare_items_query( $args, $request );
$posts_query = new WP_Query();
$query_result = $posts_query->query( $query_args );
// Allow access to all password protected posts if the context is edit.
if ( 'edit' === $request['context'] ) {
add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
}
$posts = array();
foreach ( $query_result as $post ) {
if ( ! $this->check_read_permission( $post ) ) {
continue;
}
$data = $this->prepare_item_for_response( $post, $request );
$posts[] = $this->prepare_response_for_collection( $data );
}
// Reset filter.
if ( 'edit' === $request['context'] ) {
remove_filter( 'post_password_required', array( $this, 'check_password_required' ) );
}
$page = (int) $query_args['paged'];
$total_posts = $posts_query->found_posts;
if ( $total_posts < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
unset( $query_args['paged'] );
$count_query = new WP_Query();
$count_query->query( $query_args );
$total_posts = $count_query->found_posts;
}
$max_pages = ceil( $total_posts / (int) $posts_query->query_vars['posts_per_page'] );
if ( $page > $max_pages && $total_posts > 0 ) {
return new WP_Error(
'rest_post_invalid_page_number',
__( 'The page number requested is larger than the number of pages available.' ),
array( 'status' => 400 )
);
}
$response = rest_ensure_response( $posts );
$response->header( 'X-WP-Total', (int) $total_posts );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
$request_params = $request->get_query_params();
$base = add_query_arg( urlencode_deep( $request_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;
}
/**
* Get the post, if the ID is valid.
*
* @since 4.7.2
*
* @param int $id Supplied ID.
* @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
*/
protected function get_post( $id ) {
$error = new WP_Error(
'rest_post_invalid_id',
__( 'Invalid post ID.' ),
array( 'status' => 404 )
);
if ( (int) $id <= 0 ) {
return $error;
}
$post = get_post( (int) $id );
if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
return $error;
}
return $post;
}
/**
* Checks if a given request has access to read a post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit this post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( $post && ! empty( $request['password'] ) ) {
// Check post password, and return error if invalid.
if ( ! hash_equals( $post->post_password, $request['password'] ) ) {
return new WP_Error(
'rest_post_incorrect_password',
__( 'Incorrect post password.' ),
array( 'status' => 403 )
);
}
}
// Allow access to all password protected posts if the context is edit.
if ( 'edit' === $request['context'] ) {
add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
}
if ( $post ) {
return $this->check_read_permission( $post );
}
return true;
}
/**
* Checks if the user can access password-protected content.
*
* This method determines whether we need to override the regular password
* check in core with a filter.
*
* @since 4.7.0
*
* @param WP_Post $post Post to check against.
* @param WP_REST_Request $request Request data to check.
* @return bool True if the user can access password-protected content, otherwise false.
*/
public function can_access_password_content( $post, $request ) {
if ( empty( $post->post_password ) ) {
// No filter required.
return false;
}
/*
* Users always gets access to password protected content in the edit
* context if they have the `edit_post` meta capability.
*/
if (
'edit' === $request['context'] &&
current_user_can( 'edit_post', $post->ID )
) {
return true;
}
// No password, no auth.
if ( empty( $request['password'] ) ) {
return false;
}
// Double-check the request password.
return hash_equals( $post->post_password, $request['password'] );
}
/**
* Retrieves a single post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
$data = $this->prepare_item_for_response( $post, $request );
$response = rest_ensure_response( $data );
if ( is_post_type_viewable( get_post_type_object( $post->post_type ) ) ) {
$response->link_header( 'alternate', get_permalink( $post->ID ), array( 'type' => 'text/html' ) );
}
return $response;
}
/**
* Checks if a given request has access to create a post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
*/
public function create_item_permissions_check( $request ) {
if ( ! empty( $request['id'] ) ) {
return new WP_Error(
'rest_post_exists',
__( 'Cannot create existing post.' ),
array( 'status' => 400 )
);
}
$post_type = get_post_type_object( $this->post_type );
if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
return new WP_Error(
'rest_cannot_edit_others',
__( 'Sorry, you are not allowed to create posts as this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
return new WP_Error(
'rest_cannot_assign_sticky',
__( 'Sorry, you are not allowed to make posts sticky.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! current_user_can( $post_type->cap->create_posts ) ) {
return new WP_Error(
'rest_cannot_create',
__( 'Sorry, you are not allowed to create posts as this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! $this->check_assign_terms_permission( $request ) ) {
return new WP_Error(
'rest_cannot_assign_term',
__( 'Sorry, you are not allowed to assign the provided terms.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Creates a single post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
return new WP_Error(
'rest_post_exists',
__( 'Cannot create existing post.' ),
array( 'status' => 400 )
);
}
$prepared_post = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared_post ) ) {
return $prepared_post;
}
$prepared_post->post_type = $this->post_type;
$post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true, false );
if ( is_wp_error( $post_id ) ) {
if ( 'db_insert_error' === $post_id->get_error_code() ) {
$post_id->add_data( array( 'status' => 500 ) );
} else {
$post_id->add_data( array( 'status' => 400 ) );
}
return $post_id;
}
$post = get_post( $post_id );
/**
* Fires after a single post is created or updated via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_insert_post`
* - `rest_insert_page`
* - `rest_insert_attachment`
*
* @since 4.7.0
*
* @param WP_Post $post Inserted or updated post object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a post, false when updating.
*/
do_action( "rest_insert_{$this->post_type}", $post, $request, true );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['sticky'] ) ) {
if ( ! empty( $request['sticky'] ) ) {
stick_post( $post_id );
} else {
unstick_post( $post_id );
}
}
if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
$this->handle_featured_media( $request['featured_media'], $post_id );
}
if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
set_post_format( $post, $request['format'] );
}
if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
$this->handle_template( $request['template'], $post_id, true );
}
$terms_update = $this->handle_terms( $post_id, $request );
if ( is_wp_error( $terms_update ) ) {
return $terms_update;
}
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $post_id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$post = get_post( $post_id );
$fields_update = $this->update_additional_fields_for_object( $post, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
/**
* Fires after a single post is completely created or updated via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_after_insert_post`
* - `rest_after_insert_page`
* - `rest_after_insert_attachment`
*
* @since 5.0.0
*
* @param WP_Post $post Inserted or updated post object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a post, false when updating.
*/
do_action( "rest_after_insert_{$this->post_type}", $post, $request, true );
wp_after_insert_post( $post, false, null );
$response = $this->prepare_item_for_response( $post, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) );
return $response;
}
/**
* Checks if a given request has access to update a post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
$post_type = get_post_type_object( $this->post_type );
if ( $post && ! $this->check_update_permission( $post ) ) {
return new WP_Error(
'rest_cannot_edit',
__( 'Sorry, you are not allowed to edit this post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
return new WP_Error(
'rest_cannot_edit_others',
__( 'Sorry, you are not allowed to update posts as this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
return new WP_Error(
'rest_cannot_assign_sticky',
__( 'Sorry, you are not allowed to make posts sticky.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! $this->check_assign_terms_permission( $request ) ) {
return new WP_Error(
'rest_cannot_assign_term',
__( 'Sorry, you are not allowed to assign the provided terms.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Updates a single post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
$valid_check = $this->get_post( $request['id'] );
if ( is_wp_error( $valid_check ) ) {
return $valid_check;
}
$post_before = get_post( $request['id'] );
$post = $this->prepare_item_for_database( $request );
if ( is_wp_error( $post ) ) {
return $post;
}
// Convert the post object to an array, otherwise wp_update_post() will expect non-escaped input.
$post_id = wp_update_post( wp_slash( (array) $post ), true, false );
if ( is_wp_error( $post_id ) ) {
if ( 'db_update_error' === $post_id->get_error_code() ) {
$post_id->add_data( array( 'status' => 500 ) );
} else {
$post_id->add_data( array( 'status' => 400 ) );
}
return $post_id;
}
$post = get_post( $post_id );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
do_action( "rest_insert_{$this->post_type}", $post, $request, false );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
set_post_format( $post, $request['format'] );
}
if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
$this->handle_featured_media( $request['featured_media'], $post_id );
}
if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) {
if ( ! empty( $request['sticky'] ) ) {
stick_post( $post_id );
} else {
unstick_post( $post_id );
}
}
if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
$this->handle_template( $request['template'], $post->ID );
}
$terms_update = $this->handle_terms( $post->ID, $request );
if ( is_wp_error( $terms_update ) ) {
return $terms_update;
}
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $post->ID );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$post = get_post( $post_id );
$fields_update = $this->update_additional_fields_for_object( $post, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
// Filter is fired in WP_REST_Attachments_Controller subclass.
if ( 'attachment' === $this->post_type ) {
$response = $this->prepare_item_for_response( $post, $request );
return rest_ensure_response( $response );
}
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
do_action( "rest_after_insert_{$this->post_type}", $post, $request, false );
wp_after_insert_post( $post, true, $post_before );
$response = $this->prepare_item_for_response( $post, $request );
return rest_ensure_response( $response );
}
/**
* Checks if a given request has access to delete a post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
*/
public function delete_item_permissions_check( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
if ( $post && ! $this->check_delete_permission( $post ) ) {
return new WP_Error(
'rest_cannot_delete',
__( 'Sorry, you are not allowed to delete this post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Deletes a single post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
$id = $post->ID;
$force = (bool) $request['force'];
$supports_trash = ( EMPTY_TRASH_DAYS > 0 );
if ( 'attachment' === $post->post_type ) {
$supports_trash = $supports_trash && MEDIA_TRASH;
}
/**
* Filters whether a post is trashable.
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_post_trashable`
* - `rest_page_trashable`
* - `rest_attachment_trashable`
*
* Pass false to disable Trash support for the post.
*
* @since 4.7.0
*
* @param bool $supports_trash Whether the post type support trashing.
* @param WP_Post $post The Post object being considered for trashing support.
*/
$supports_trash = apply_filters( "rest_{$this->post_type}_trashable", $supports_trash, $post );
if ( ! $this->check_delete_permission( $post ) ) {
return new WP_Error(
'rest_user_cannot_delete_post',
__( 'Sorry, you are not allowed to delete this post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
$request->set_param( 'context', 'edit' );
// If we're forcing, then delete permanently.
if ( $force ) {
$previous = $this->prepare_item_for_response( $post, $request );
$result = wp_delete_post( $id, true );
$response = new WP_REST_Response();
$response->set_data(
array(
'deleted' => true,
'previous' => $previous->get_data(),
)
);
} else {
// If we don't support trashing for this type, error out.
if ( ! $supports_trash ) {
return new WP_Error(
'rest_trash_not_supported',
/* translators: %s: force=true */
sprintf( __( "The post does not support trashing. Set '%s' to delete." ), 'force=true' ),
array( 'status' => 501 )
);
}
// Otherwise, only trash if we haven't already.
if ( 'trash' === $post->post_status ) {
return new WP_Error(
'rest_already_trashed',
__( 'The post has already been deleted.' ),
array( 'status' => 410 )
);
}
// (Note that internally this falls through to `wp_delete_post()`
// if the Trash is disabled.)
$result = wp_trash_post( $id );
$post = get_post( $id );
$response = $this->prepare_item_for_response( $post, $request );
}
if ( ! $result ) {
return new WP_Error(
'rest_cannot_delete',
__( 'The post cannot be deleted.' ),
array( 'status' => 500 )
);
}
/**
* Fires immediately after a single post is deleted or trashed via the REST API.
*
* They dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_delete_post`
* - `rest_delete_page`
* - `rest_delete_attachment`
*
* @since 4.7.0
*
* @param WP_Post $post The deleted or trashed post.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( "rest_delete_{$this->post_type}", $post, $response, $request );
return $response;
}
/**
* Determines the allowed query_vars for a get_items() response and prepares
* them for WP_Query.
*
* @since 4.7.0
*
* @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
* @param WP_REST_Request $request Optional. Full details about the request.
* @return array Items query arguments.
*/
protected function prepare_items_query( $prepared_args = array(), $request = null ) {
$query_args = array();
foreach ( $prepared_args as $key => $value ) {
/**
* Filters the query_vars used in get_items() for the constructed query.
*
* The dynamic portion of the hook name, `$key`, refers to the query_var key.
*
* @since 4.7.0
*
* @param string $value The query_var value.
*/
$query_args[ $key ] = apply_filters( "rest_query_var-{$key}", $value ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
}
if ( 'post' !== $this->post_type || ! isset( $query_args['ignore_sticky_posts'] ) ) {
$query_args['ignore_sticky_posts'] = true;
}
// Map to proper WP_Query orderby param.
if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) {
$orderby_mappings = array(
'id' => 'ID',
'include' => 'post__in',
'slug' => 'post_name',
'include_slugs' => 'post_name__in',
);
if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
$query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
}
}
return $query_args;
}
/**
* Checks the post_date_gmt or modified_gmt and prepare any post or
* modified date for single post output.
*
* @since 4.7.0
*
* @param string $date_gmt GMT publication time.
* @param string|null $date Optional. Local publication time. Default null.
* @return string|null ISO8601/RFC3339 formatted datetime.
*/
protected function prepare_date_response( $date_gmt, $date = null ) {
// Use the date if passed.
if ( isset( $date ) ) {
return mysql_to_rfc3339( $date );
}
// Return null if $date_gmt is empty/zeros.
if ( '0000-00-00 00:00:00' === $date_gmt ) {
return null;
}
// Return the formatted datetime.
return mysql_to_rfc3339( $date_gmt );
}
/**
* Prepares a single post for create or update.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Request object.
* @return stdClass|WP_Error Post object or WP_Error.
*/
protected function prepare_item_for_database( $request ) {
$prepared_post = new stdClass();
$current_status = '';
// Post ID.
if ( isset( $request['id'] ) ) {
$existing_post = $this->get_post( $request['id'] );
if ( is_wp_error( $existing_post ) ) {
return $existing_post;
}
$prepared_post->ID = $existing_post->ID;
$current_status = $existing_post->post_status;
}
$schema = $this->get_item_schema();
// Post title.
if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
if ( is_string( $request['title'] ) ) {
$prepared_post->post_title = $request['title'];
} elseif ( ! empty( $request['title']['raw'] ) ) {
$prepared_post->post_title = $request['title']['raw'];
}
}
// Post content.
if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) {
if ( is_string( $request['content'] ) ) {
$prepared_post->post_content = $request['content'];
} elseif ( isset( $request['content']['raw'] ) ) {
$prepared_post->post_content = $request['content']['raw'];
}
}
// Post excerpt.
if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) {
if ( is_string( $request['excerpt'] ) ) {
$prepared_post->post_excerpt = $request['excerpt'];
} elseif ( isset( $request['excerpt']['raw'] ) ) {
$prepared_post->post_excerpt = $request['excerpt']['raw'];
}
}
// Post type.
if ( empty( $request['id'] ) ) {
// Creating new post, use default type for the controller.
$prepared_post->post_type = $this->post_type;
} else {
// Updating a post, use previous type.
$prepared_post->post_type = get_post_type( $request['id'] );
}
$post_type = get_post_type_object( $prepared_post->post_type );
// Post status.
if (
! empty( $schema['properties']['status'] ) &&
isset( $request['status'] ) &&
( ! $current_status || $current_status !== $request['status'] )
) {
$status = $this->handle_status_param( $request['status'], $post_type );
if ( is_wp_error( $status ) ) {
return $status;
}
$prepared_post->post_status = $status;
}
// Post date.
if ( ! empty( $schema['properties']['date'] ) && ! empty( $request['date'] ) ) {
$current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date : false;
$date_data = rest_get_date_with_gmt( $request['date'] );
if ( ! empty( $date_data ) && $current_date !== $date_data[0] ) {
list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
$prepared_post->edit_date = true;
}
} elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) {
$current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date_gmt : false;
$date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
if ( ! empty( $date_data ) && $current_date !== $date_data[1] ) {
list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
$prepared_post->edit_date = true;
}
}
// Sending a null date or date_gmt value resets date and date_gmt to their
// default values (`0000-00-00 00:00:00`).
if (
( ! empty( $schema['properties']['date_gmt'] ) && $request->has_param( 'date_gmt' ) && null === $request['date_gmt'] ) ||
( ! empty( $schema['properties']['date'] ) && $request->has_param( 'date' ) && null === $request['date'] )
) {
$prepared_post->post_date_gmt = null;
$prepared_post->post_date = null;
}
// Post slug.
if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) {
$prepared_post->post_name = $request['slug'];
}
// Author.
if ( ! empty( $schema['properties']['author'] ) && ! empty( $request['author'] ) ) {
$post_author = (int) $request['author'];
if ( get_current_user_id() !== $post_author ) {
$user_obj = get_userdata( $post_author );
if ( ! $user_obj ) {
return new WP_Error(
'rest_invalid_author',
__( 'Invalid author ID.' ),
array( 'status' => 400 )
);
}
}
$prepared_post->post_author = $post_author;
}
// Post password.
if ( ! empty( $schema['properties']['password'] ) && isset( $request['password'] ) ) {
$prepared_post->post_password = $request['password'];
if ( '' !== $request['password'] ) {
if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
return new WP_Error(
'rest_invalid_field',
__( 'A post can not be sticky and have a password.' ),
array( 'status' => 400 )
);
}
if ( ! empty( $prepared_post->ID ) && is_sticky( $prepared_post->ID ) ) {
return new WP_Error(
'rest_invalid_field',
__( 'A sticky post can not be password protected.' ),
array( 'status' => 400 )
);
}
}
}
if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
if ( ! empty( $prepared_post->ID ) && post_password_required( $prepared_post->ID ) ) {
return new WP_Error(
'rest_invalid_field',
__( 'A password protected post can not be set to sticky.' ),
array( 'status' => 400 )
);
}
}
// Parent.
if ( ! empty( $schema['properties']['parent'] ) && isset( $request['parent'] ) ) {
if ( 0 === (int) $request['parent'] ) {
$prepared_post->post_parent = 0;
} else {
$parent = get_post( (int) $request['parent'] );
if ( empty( $parent ) ) {
return new WP_Error(
'rest_post_invalid_id',
__( 'Invalid post parent ID.' ),
array( 'status' => 400 )
);
}
$prepared_post->post_parent = (int) $parent->ID;
}
}
// Menu order.
if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) {
$prepared_post->menu_order = (int) $request['menu_order'];
}
// Comment status.
if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) {
$prepared_post->comment_status = $request['comment_status'];
}
// Ping status.
if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) {
$prepared_post->ping_status = $request['ping_status'];
}
if ( ! empty( $schema['properties']['template'] ) ) {
// Force template to null so that it can be handled exclusively by the REST controller.
$prepared_post->page_template = null;
}
/**
* Filters a post before it is inserted via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_pre_insert_post`
* - `rest_pre_insert_page`
* - `rest_pre_insert_attachment`
*
* @since 4.7.0
*
* @param stdClass $prepared_post An object representing a single post prepared
* for inserting or updating the database.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request );
}
/**
* Checks whether the status is valid for the given post.
*
* Allows for sending an update request with the current status, even if that status would not be acceptable.
*
* @since 5.6.0
*
* @param string $status The provided status.
* @param WP_REST_Request $request The request object.
* @param string $param The parameter name.
* @return true|WP_Error True if the status is valid, or WP_Error if not.
*/
public function check_status( $status, $request, $param ) {
if ( $request['id'] ) {
$post = $this->get_post( $request['id'] );
if ( ! is_wp_error( $post ) && $post->post_status === $status ) {
return true;
}
}
$args = $request->get_attributes()['args'][ $param ];
return rest_validate_value_from_schema( $status, $args, $param );
}
/**
* Determines validity and normalizes the given status parameter.
*
* @since 4.7.0
*
* @param string $post_status Post status.
* @param WP_Post_Type $post_type Post type.
* @return string|WP_Error Post status or WP_Error if lacking the proper permission.
*/
protected function handle_status_param( $post_status, $post_type ) {
switch ( $post_status ) {
case 'draft':
case 'pending':
break;
case 'private':
if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
return new WP_Error(
'rest_cannot_publish',
__( 'Sorry, you are not allowed to create private posts in this post type.' ),
array( 'status' => rest_authorization_required_code() )
);
}
break;
case 'publish':
case 'future':
if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
return new WP_Error(
'rest_cannot_publish',
__( 'Sorry, you are not allowed to publish posts in this post type.' ),
array( 'status' => rest_authorization_required_code() )
);
}
break;
default:
if ( ! get_post_status_object( $post_status ) ) {
$post_status = 'draft';
}
break;
}
return $post_status;
}
/**
* Determines the featured media based on a request param.
*
* @since 4.7.0
*
* @param int $featured_media Featured Media ID.
* @param int $post_id Post ID.
* @return bool|WP_Error Whether the post thumbnail was successfully deleted, otherwise WP_Error.
*/
protected function handle_featured_media( $featured_media, $post_id ) {
$featured_media = (int) $featured_media;
if ( $featured_media ) {
$result = set_post_thumbnail( $post_id, $featured_media );
if ( $result ) {
return true;
} else {
return new WP_Error(
'rest_invalid_featured_media',
__( 'Invalid featured media ID.' ),
array( 'status' => 400 )
);
}
} else {
return delete_post_thumbnail( $post_id );
}
}
/**
* Check whether the template is valid for the given post.
*
* @since 4.9.0
*
* @param string $template Page template filename.
* @param WP_REST_Request $request Request.
* @return bool|WP_Error True if template is still valid or if the same as existing value, or false if template not supported.
*/
public function check_template( $template, $request ) {
if ( ! $template ) {
return true;
}
if ( $request['id'] ) {
$post = get_post( $request['id'] );
$current_template = get_page_template_slug( $request['id'] );
} else {
$post = null;
$current_template = '';
}
// Always allow for updating a post to the same template, even if that template is no longer supported.
if ( $template === $current_template ) {
return true;
}
// If this is a create request, get_post() will return null and wp theme will fallback to the passed post type.
$allowed_templates = wp_get_theme()->get_page_templates( $post, $this->post_type );
if ( isset( $allowed_templates[ $template ] ) ) {
return true;
}
return new WP_Error(
'rest_invalid_param',
/* translators: 1: Parameter, 2: List of valid values. */
sprintf( __( '%1$s is not one of %2$s.' ), 'template', implode( ', ', array_keys( $allowed_templates ) ) )
);
}
/**
* Sets the template for a post.
*
* @since 4.7.0
* @since 4.9.0 Added the `$validate` parameter.
*
* @param string $template Page template filename.
* @param int $post_id Post ID.
* @param bool $validate Whether to validate that the template selected is valid.
*/
public function handle_template( $template, $post_id, $validate = false ) {
if ( $validate && ! array_key_exists( $template, wp_get_theme()->get_page_templates( get_post( $post_id ) ) ) ) {
$template = '';
}
update_post_meta( $post_id, '_wp_page_template', $template );
}
/**
* Updates the post's terms from a REST request.
*
* @since 4.7.0
*
* @param int $post_id The post ID to update the terms form.
* @param WP_REST_Request $request The request object with post and terms data.
* @return null|WP_Error WP_Error on an error assigning any of the terms, otherwise null.
*/
protected function handle_terms( $post_id, $request ) {
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
if ( ! isset( $request[ $base ] ) ) {
continue;
}
$result = wp_set_object_terms( $post_id, $request[ $base ], $taxonomy->name );
if ( is_wp_error( $result ) ) {
return $result;
}
}
}
/**
* Checks whether current user can assign all terms sent with the current request.
*
* @since 4.7.0
*
* @param WP_REST_Request $request The request object with post and terms data.
* @return bool Whether the current user can assign the provided terms.
*/
protected function check_assign_terms_permission( $request ) {
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
if ( ! isset( $request[ $base ] ) ) {
continue;
}
foreach ( (array) $request[ $base ] as $term_id ) {
// Invalid terms will be rejected later.
if ( ! get_term( $term_id, $taxonomy->name ) ) {
continue;
}
if ( ! current_user_can( 'assign_term', (int) $term_id ) ) {
return false;
}
}
}
return true;
}
/**
* Checks if a given post type can be viewed or managed.
*
* @since 4.7.0
*
* @param WP_Post_Type|string $post_type Post type name or object.
* @return bool Whether the post type is allowed in REST.
*/
protected function check_is_post_type_allowed( $post_type ) {
if ( ! is_object( $post_type ) ) {
$post_type = get_post_type_object( $post_type );
}
if ( ! empty( $post_type ) && ! empty( $post_type->show_in_rest ) ) {
return true;
}
return false;
}
/**
* Checks if a post can be read.
*
* Correctly handles posts with the inherit status.
*
* @since 4.7.0
*
* @param WP_Post $post Post object.
* @return bool Whether the post can be read.
*/
public function check_read_permission( $post ) {
$post_type = get_post_type_object( $post->post_type );
if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
return false;
}
// Is the post readable?
if ( 'publish' === $post->post_status || current_user_can( 'read_post', $post->ID ) ) {
return true;
}
$post_status_obj = get_post_status_object( $post->post_status );
if ( $post_status_obj && $post_status_obj->public ) {
return true;
}
// Can we read the parent if we're inheriting?
if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) {
$parent = get_post( $post->post_parent );
if ( $parent ) {
return $this->check_read_permission( $parent );
}
}
/*
* If there isn't a parent, but the status is set to inherit, assume
* it's published (as per get_post_status()).
*/
if ( 'inherit' === $post->post_status ) {
return true;
}
return false;
}
/**
* Checks if a post can be edited.
*
* @since 4.7.0
*
* @param WP_Post $post Post object.
* @return bool Whether the post can be edited.
*/
protected function check_update_permission( $post ) {
$post_type = get_post_type_object( $post->post_type );
if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
return false;
}
return current_user_can( 'edit_post', $post->ID );
}
/**
* Checks if a post can be created.
*
* @since 4.7.0
*
* @param WP_Post $post Post object.
* @return bool Whether the post can be created.
*/
protected function check_create_permission( $post ) {
$post_type = get_post_type_object( $post->post_type );
if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
return false;
}
return current_user_can( $post_type->cap->create_posts );
}
/**
* Checks if a post can be deleted.
*
* @since 4.7.0
*
* @param WP_Post $post Post object.
* @return bool Whether the post can be deleted.
*/
protected function check_delete_permission( $post ) {
$post_type = get_post_type_object( $post->post_type );
if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
return false;
}
return current_user_can( 'delete_post', $post->ID );
}
/**
* Prepares a single post output for response.
*
* @since 4.7.0
* @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param WP_Post $item Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$post = $item;
$GLOBALS['post'] = $post;
setup_postdata( $post );
$fields = $this->get_fields_for_response( $request );
// Base fields for every post.
$data = array();
if ( rest_is_field_included( 'id', $fields ) ) {
$data['id'] = $post->ID;
}
if ( rest_is_field_included( 'date', $fields ) ) {
$data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
}
if ( rest_is_field_included( 'date_gmt', $fields ) ) {
/*
* For drafts, `post_date_gmt` may not be set, indicating that the date
* of the draft should be updated each time it is saved (see #38883).
* In this case, shim the value based on the `post_date` field
* with the site's timezone offset applied.
*/
if ( '0000-00-00 00:00:00' === $post->post_date_gmt ) {
$post_date_gmt = get_gmt_from_date( $post->post_date );
} else {
$post_date_gmt = $post->post_date_gmt;
}
$data['date_gmt'] = $this->prepare_date_response( $post_date_gmt );
}
if ( rest_is_field_included( 'guid', $fields ) ) {
$data['guid'] = array(
/** This filter is documented in wp-includes/post-template.php */
'rendered' => apply_filters( 'get_the_guid', $post->guid, $post->ID ),
'raw' => $post->guid,
);
}
if ( rest_is_field_included( 'modified', $fields ) ) {
$data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
}
if ( rest_is_field_included( 'modified_gmt', $fields ) ) {
/*
* For drafts, `post_modified_gmt` may not be set (see `post_date_gmt` comments
* above). In this case, shim the value based on the `post_modified` field
* with the site's timezone offset applied.
*/
if ( '0000-00-00 00:00:00' === $post->post_modified_gmt ) {
$post_modified_gmt = gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) - ( get_option( 'gmt_offset' ) * 3600 ) );
} else {
$post_modified_gmt = $post->post_modified_gmt;
}
$data['modified_gmt'] = $this->prepare_date_response( $post_modified_gmt );
}
if ( rest_is_field_included( 'password', $fields ) ) {
$data['password'] = $post->post_password;
}
if ( rest_is_field_included( 'slug', $fields ) ) {
$data['slug'] = $post->post_name;
}
if ( rest_is_field_included( 'status', $fields ) ) {
$data['status'] = $post->post_status;
}
if ( rest_is_field_included( 'type', $fields ) ) {
$data['type'] = $post->post_type;
}
if ( rest_is_field_included( 'link', $fields ) ) {
$data['link'] = get_permalink( $post->ID );
}
if ( rest_is_field_included( 'title', $fields ) ) {
$data['title'] = array();
}
if ( rest_is_field_included( 'title.raw', $fields ) ) {
$data['title']['raw'] = $post->post_title;
}
if ( rest_is_field_included( 'title.rendered', $fields ) ) {
add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
$data['title']['rendered'] = get_the_title( $post->ID );
remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
}
$has_password_filter = false;
if ( $this->can_access_password_content( $post, $request ) ) {
$this->password_check_passed[ $post->ID ] = true;
// Allow access to the post, permissions already checked before.
add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
$has_password_filter = true;
}
if ( rest_is_field_included( 'content', $fields ) ) {
$data['content'] = array();
}
if ( rest_is_field_included( 'content.raw', $fields ) ) {
$data['content']['raw'] = $post->post_content;
}
if ( rest_is_field_included( 'content.rendered', $fields ) ) {
/** This filter is documented in wp-includes/post-template.php */
$data['content']['rendered'] = post_password_required( $post ) ? '' : apply_filters( 'the_content', $post->post_content );
}
if ( rest_is_field_included( 'content.protected', $fields ) ) {
$data['content']['protected'] = (bool) $post->post_password;
}
if ( rest_is_field_included( 'content.block_version', $fields ) ) {
$data['content']['block_version'] = block_version( $post->post_content );
}
if ( rest_is_field_included( 'excerpt', $fields ) ) {
/** This filter is documented in wp-includes/post-template.php */
$excerpt = apply_filters( 'get_the_excerpt', $post->post_excerpt, $post );
/** This filter is documented in wp-includes/post-template.php */
$excerpt = apply_filters( 'the_excerpt', $excerpt );
$data['excerpt'] = array(
'raw' => $post->post_excerpt,
'rendered' => post_password_required( $post ) ? '' : $excerpt,
'protected' => (bool) $post->post_password,
);
}
if ( $has_password_filter ) {
// Reset filter.
remove_filter( 'post_password_required', array( $this, 'check_password_required' ) );
}
if ( rest_is_field_included( 'author', $fields ) ) {
$data['author'] = (int) $post->post_author;
}
if ( rest_is_field_included( 'featured_media', $fields ) ) {
$data['featured_media'] = (int) get_post_thumbnail_id( $post->ID );
}
if ( rest_is_field_included( 'parent', $fields ) ) {
$data['parent'] = (int) $post->post_parent;
}
if ( rest_is_field_included( 'menu_order', $fields ) ) {
$data['menu_order'] = (int) $post->menu_order;
}
if ( rest_is_field_included( 'comment_status', $fields ) ) {
$data['comment_status'] = $post->comment_status;
}
if ( rest_is_field_included( 'ping_status', $fields ) ) {
$data['ping_status'] = $post->ping_status;
}
if ( rest_is_field_included( 'sticky', $fields ) ) {
$data['sticky'] = is_sticky( $post->ID );
}
if ( rest_is_field_included( 'template', $fields ) ) {
$template = get_page_template_slug( $post->ID );
if ( $template ) {
$data['template'] = $template;
} else {
$data['template'] = '';
}
}
if ( rest_is_field_included( 'format', $fields ) ) {
$data['format'] = get_post_format( $post->ID );
// Fill in blank post format.
if ( empty( $data['format'] ) ) {
$data['format'] = 'standard';
}
}
if ( rest_is_field_included( 'meta', $fields ) ) {
$data['meta'] = $this->meta->get_value( $post->ID, $request );
}
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
if ( rest_is_field_included( $base, $fields ) ) {
$terms = get_the_terms( $post, $taxonomy->name );
$data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
}
}
$post_type_obj = get_post_type_object( $post->post_type );
if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
$permalink_template_requested = rest_is_field_included( 'permalink_template', $fields );
$generated_slug_requested = rest_is_field_included( 'generated_slug', $fields );
if ( $permalink_template_requested || $generated_slug_requested ) {
if ( ! function_exists( 'get_sample_permalink' ) ) {
require_once ABSPATH . 'wp-admin/includes/post.php';
}
$sample_permalink = get_sample_permalink( $post->ID, $post->post_title, '' );
if ( $permalink_template_requested ) {
$data['permalink_template'] = $sample_permalink[0];
}
if ( $generated_slug_requested ) {
$data['generated_slug'] = $sample_permalink[1];
}
}
}
$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 );
$links = $this->prepare_links( $post );
$response->add_links( $links );
if ( ! empty( $links['self']['href'] ) ) {
$actions = $this->get_available_actions( $post, $request );
$self = $links['self']['href'];
foreach ( $actions as $rel ) {
$response->add_link( $rel, $self );
}
}
/**
* Filters the post data for a REST API response.
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_prepare_post`
* - `rest_prepare_page`
* - `rest_prepare_attachment`
*
* @since 4.7.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
}
/**
* Overwrites the default protected title format.
*
* By default, WordPress will show password protected posts with a title of
* "Protected: %s", as the REST API communicates the protected status of a post
* in a machine readable format, we remove the "Protected: " prefix.
*
* @since 4.7.0
*
* @return string Protected title format.
*/
public function protected_title_format() {
return '%s';
}
/**
* Prepares links for the request.
*
* @since 4.7.0
*
* @param WP_Post $post Post object.
* @return array Links for the given post.
*/
protected function prepare_links( $post ) {
$base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
// Entity meta.
$links = array(
'self' => array(
'href' => rest_url( trailingslashit( $base ) . $post->ID ),
),
'collection' => array(
'href' => rest_url( $base ),
),
'about' => array(
'href' => rest_url( 'wp/v2/types/' . $this->post_type ),
),
);
if ( ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'author' ) )
&& ! empty( $post->post_author ) ) {
$links['author'] = array(
'href' => rest_url( 'wp/v2/users/' . $post->post_author ),
'embeddable' => true,
);
}
if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'comments' ) ) {
$replies_url = rest_url( 'wp/v2/comments' );
$replies_url = add_query_arg( 'post', $post->ID, $replies_url );
$links['replies'] = array(
'href' => $replies_url,
'embeddable' => true,
);
}
if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'revisions' ) ) {
$revisions = wp_get_post_revisions( $post->ID, array( 'fields' => 'ids' ) );
$revisions_count = count( $revisions );
$links['version-history'] = array(
'href' => rest_url( trailingslashit( $base ) . $post->ID . '/revisions' ),
'count' => $revisions_count,
);
if ( $revisions_count > 0 ) {
$last_revision = array_shift( $revisions );
$links['predecessor-version'] = array(
'href' => rest_url( trailingslashit( $base ) . $post->ID . '/revisions/' . $last_revision ),
'id' => $last_revision,
);
}
}
$post_type_obj = get_post_type_object( $post->post_type );
if ( $post_type_obj->hierarchical && ! empty( $post->post_parent ) ) {
$links['up'] = array(
'href' => rest_url( rest_get_route_for_post( $post->post_parent ) ),
'embeddable' => true,
);
}
// If we have a featured media, add that.
$featured_media = get_post_thumbnail_id( $post->ID );
if ( $featured_media ) {
$image_url = rest_url( rest_get_route_for_post( $featured_media ) );
$links['https://api.w.org/featuredmedia'] = array(
'href' => $image_url,
'embeddable' => true,
);
}
if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ), true ) ) {
$attachments_url = rest_url( rest_get_route_for_post_type_items( 'attachment' ) );
$attachments_url = add_query_arg( 'parent', $post->ID, $attachments_url );
$links['https://api.w.org/attachment'] = array(
'href' => $attachments_url,
);
}
$taxonomies = get_object_taxonomies( $post->post_type );
if ( ! empty( $taxonomies ) ) {
$links['https://api.w.org/term'] = array();
foreach ( $taxonomies as $tax ) {
$taxonomy_route = rest_get_route_for_taxonomy_items( $tax );
// Skip taxonomies that are not public.
if ( empty( $taxonomy_route ) ) {
continue;
}
$terms_url = add_query_arg(
'post',
$post->ID,
rest_url( $taxonomy_route )
);
$links['https://api.w.org/term'][] = array(
'href' => $terms_url,
'taxonomy' => $tax,
'embeddable' => true,
);
}
}
return $links;
}
/**
* Get the link relations available for the post and current user.
*
* @since 4.9.8
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return array List of link relations.
*/
protected function get_available_actions( $post, $request ) {
if ( 'edit' !== $request['context'] ) {
return array();
}
$rels = array();
$post_type = get_post_type_object( $post->post_type );
if ( 'attachment' !== $this->post_type && current_user_can( $post_type->cap->publish_posts ) ) {
$rels[] = 'https://api.w.org/action-publish';
}
if ( current_user_can( 'unfiltered_html' ) ) {
$rels[] = 'https://api.w.org/action-unfiltered-html';
}
if ( 'post' === $post_type->name ) {
if ( current_user_can( $post_type->cap->edit_others_posts ) && current_user_can( $post_type->cap->publish_posts ) ) {
$rels[] = 'https://api.w.org/action-sticky';
}
}
if ( post_type_supports( $post_type->name, 'author' ) ) {
if ( current_user_can( $post_type->cap->edit_others_posts ) ) {
$rels[] = 'https://api.w.org/action-assign-author';
}
}
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $tax ) {
$tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name;
$create_cap = is_taxonomy_hierarchical( $tax->name ) ? $tax->cap->edit_terms : $tax->cap->assign_terms;
if ( current_user_can( $create_cap ) ) {
$rels[] = 'https://api.w.org/action-create-' . $tax_base;
}
if ( current_user_can( $tax->cap->assign_terms ) ) {
$rels[] = 'https://api.w.org/action-assign-' . $tax_base;
}
}
return $rels;
}
/**
* Retrieves the post's schema, conforming to JSON Schema.
*
* @since 4.7.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
// Base properties for every Post.
'properties' => array(
'date' => array(
'description' => __( "The date the post was published, in the site's timezone." ),
'type' => array( 'string', 'null' ),
'format' => 'date-time',
'context' => array( 'view', 'edit', 'embed' ),
),
'date_gmt' => array(
'description' => __( 'The date the post was published, as GMT.' ),
'type' => array( 'string', 'null' ),
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'guid' => array(
'description' => __( 'The globally unique identifier for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => array(
'raw' => array(
'description' => __( 'GUID for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
),
'rendered' => array(
'description' => __( 'GUID for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
'id' => array(
'description' => __( 'Unique identifier for the post.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'link' => array(
'description' => __( 'URL to the post.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'modified' => array(
'description' => __( "The date the post was last modified, in the site's timezone." ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'modified_gmt' => array(
'description' => __( 'The date the post was last modified, as GMT.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the post unique to its type.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => array( $this, 'sanitize_slug' ),
),
),
'status' => array(
'description' => __( 'A named status for the post.' ),
'type' => 'string',
'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'validate_callback' => array( $this, 'check_status' ),
),
),
'type' => array(
'description' => __( 'Type of post.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'password' => array(
'description' => __( 'A password to protect access to the content and excerpt.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
),
);
$post_type_obj = get_post_type_object( $this->post_type );
if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
$schema['properties']['permalink_template'] = array(
'description' => __( 'Permalink template for the post.' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
);
$schema['properties']['generated_slug'] = array(
'description' => __( 'Slug automatically generated from the post title.' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
);
}
if ( $post_type_obj->hierarchical ) {
$schema['properties']['parent'] = array(
'description' => __( 'The ID for the parent of the post.' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
);
}
$post_type_attributes = array(
'title',
'editor',
'author',
'excerpt',
'thumbnail',
'comments',
'revisions',
'page-attributes',
'post-formats',
'custom-fields',
);
$fixed_schemas = array(
'post' => array(
'title',
'editor',
'author',
'excerpt',
'thumbnail',
'comments',
'revisions',
'post-formats',
'custom-fields',
),
'page' => array(
'title',
'editor',
'author',
'excerpt',
'thumbnail',
'comments',
'revisions',
'page-attributes',
'custom-fields',
),
'attachment' => array(
'title',
'author',
'comments',
'revisions',
'custom-fields',
),
);
foreach ( $post_type_attributes as $attribute ) {
if ( isset( $fixed_schemas[ $this->post_type ] ) && ! in_array( $attribute, $fixed_schemas[ $this->post_type ], true ) ) {
continue;
} elseif ( ! isset( $fixed_schemas[ $this->post_type ] ) && ! post_type_supports( $this->post_type, $attribute ) ) {
continue;
}
switch ( $attribute ) {
case 'title':
$schema['properties']['title'] = array(
'description' => __( 'The title for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
),
'properties' => array(
'raw' => array(
'description' => __( 'Title for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
'description' => __( 'HTML title for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
break;
case 'editor':
$schema['properties']['content'] = array(
'description' => __( 'The content for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
),
'properties' => array(
'raw' => array(
'description' => __( 'Content for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
'description' => __( 'HTML content for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'block_version' => array(
'description' => __( 'Version of the content block format used by the post.' ),
'type' => 'integer',
'context' => array( 'edit' ),
'readonly' => true,
),
'protected' => array(
'description' => __( 'Whether the content is protected with a password.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
break;
case 'author':
$schema['properties']['author'] = array(
'description' => __( 'The ID for the author of the post.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
);
break;
case 'excerpt':
$schema['properties']['excerpt'] = array(
'description' => __( 'The excerpt for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
),
'properties' => array(
'raw' => array(
'description' => __( 'Excerpt for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
'description' => __( 'HTML excerpt for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'protected' => array(
'description' => __( 'Whether the excerpt is protected with a password.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
break;
case 'thumbnail':
$schema['properties']['featured_media'] = array(
'description' => __( 'The ID of the featured media for the post.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
);
break;
case 'comments':
$schema['properties']['comment_status'] = array(
'description' => __( 'Whether or not comments are open on the post.' ),
'type' => 'string',
'enum' => array( 'open', 'closed' ),
'context' => array( 'view', 'edit' ),
);
$schema['properties']['ping_status'] = array(
'description' => __( 'Whether or not the post can be pinged.' ),
'type' => 'string',
'enum' => array( 'open', 'closed' ),
'context' => array( 'view', 'edit' ),
);
break;
case 'page-attributes':
$schema['properties']['menu_order'] = array(
'description' => __( 'The order of the post in relation to other posts.' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
);
break;
case 'post-formats':
// Get the native post formats and remove the array keys.
$formats = array_values( get_post_format_slugs() );
$schema['properties']['format'] = array(
'description' => __( 'The format for the post.' ),
'type' => 'string',
'enum' => $formats,
'context' => array( 'view', 'edit' ),
);
break;
case 'custom-fields':
$schema['properties']['meta'] = $this->meta->get_field_schema();
break;
}
}
if ( 'post' === $this->post_type ) {
$schema['properties']['sticky'] = array(
'description' => __( 'Whether or not the post should be treated as sticky.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
);
}
$schema['properties']['template'] = array(
'description' => __( 'The theme file to use to display the post.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'validate_callback' => array( $this, 'check_template' ),
),
);
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
if ( array_key_exists( $base, $schema['properties'] ) ) {
$taxonomy_field_name_with_conflict = ! empty( $taxonomy->rest_base ) ? 'rest_base' : 'name';
_doing_it_wrong(
'register_taxonomy',
sprintf(
/* translators: 1: The taxonomy name, 2: The property name, either 'rest_base' or 'name', 3: The conflicting value. */
__( 'The "%1$s" taxonomy "%2$s" property (%3$s) conflicts with an existing property on the REST API Posts Controller. Specify a custom "rest_base" when registering the taxonomy to avoid this error.' ),
$taxonomy->name,
$taxonomy_field_name_with_conflict,
$base
),
'5.4.0'
);
}
$schema['properties'][ $base ] = array(
/* translators: %s: Taxonomy name. */
'description' => sprintf( __( 'The terms assigned to the post in the %s taxonomy.' ), $taxonomy->name ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
);
}
$schema_links = $this->get_schema_links();
if ( $schema_links ) {
$schema['links'] = $schema_links;
}
// Take a snapshot of which fields are in the schema pre-filtering.
$schema_fields = array_keys( $schema['properties'] );
/**
* Filters the post's schema.
*
* The dynamic portion of the filter, `$this->post_type`, refers to the
* post type slug for the controller.
*
* Possible hook names include:
*
* - `rest_post_item_schema`
* - `rest_page_item_schema`
* - `rest_attachment_item_schema`
*
* @since 5.4.0
*
* @param array $schema Item schema data.
*/
$schema = apply_filters( "rest_{$this->post_type}_item_schema", $schema );
// Emit a _doing_it_wrong warning if user tries to add new properties using this filter.
$new_fields = array_diff( array_keys( $schema['properties'] ), $schema_fields );
if ( count( $new_fields ) > 0 ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: %s: register_rest_field */
__( 'Please use %s to add new schema properties.' ),
'register_rest_field'
),
'5.4.0'
);
}
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieve Link Description Objects that should be added to the Schema for the posts collection.
*
* @since 4.9.8
*
* @return array
*/
protected function get_schema_links() {
$href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" );
$links = array();
if ( 'attachment' !== $this->post_type ) {
$links[] = array(
'rel' => 'https://api.w.org/action-publish',
'title' => __( 'The current user can publish this post.' ),
'href' => $href,
'targetSchema' => array(
'type' => 'object',
'properties' => array(
'status' => array(
'type' => 'string',
'enum' => array( 'publish', 'future' ),
),
),
),
);
}
$links[] = array(
'rel' => 'https://api.w.org/action-unfiltered-html',
'title' => __( 'The current user can post unfiltered HTML markup and JavaScript.' ),
'href' => $href,
'targetSchema' => array(
'type' => 'object',
'properties' => array(
'content' => array(
'raw' => array(
'type' => 'string',
),
),
),
),
);
if ( 'post' === $this->post_type ) {
$links[] = array(
'rel' => 'https://api.w.org/action-sticky',
'title' => __( 'The current user can sticky this post.' ),
'href' => $href,
'targetSchema' => array(
'type' => 'object',
'properties' => array(
'sticky' => array(
'type' => 'boolean',
),
),
),
);
}
if ( post_type_supports( $this->post_type, 'author' ) ) {
$links[] = array(
'rel' => 'https://api.w.org/action-assign-author',
'title' => __( 'The current user can change the author on this post.' ),
'href' => $href,
'targetSchema' => array(
'type' => 'object',
'properties' => array(
'author' => array(
'type' => 'integer',
),
),
),
);
}
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $tax ) {
$tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name;
/* translators: %s: Taxonomy name. */
$assign_title = sprintf( __( 'The current user can assign terms in the %s taxonomy.' ), $tax->name );
/* translators: %s: Taxonomy name. */
$create_title = sprintf( __( 'The current user can create terms in the %s taxonomy.' ), $tax->name );
$links[] = array(
'rel' => 'https://api.w.org/action-assign-' . $tax_base,
'title' => $assign_title,
'href' => $href,
'targetSchema' => array(
'type' => 'object',
'properties' => array(
$tax_base => array(
'type' => 'array',
'items' => array(
'type' => 'integer',
),
),
),
),
);
$links[] = array(
'rel' => 'https://api.w.org/action-create-' . $tax_base,
'title' => $create_title,
'href' => $href,
'targetSchema' => array(
'type' => 'object',
'properties' => array(
$tax_base => array(
'type' => 'array',
'items' => array(
'type' => 'integer',
),
),
),
),
);
}
return $links;
}
/**
* Retrieves the query params for the posts collection.
*
* @since 4.7.0
* @since 5.4.0 The `tax_relation` query parameter was added.
* @since 5.7.0 The `modified_after` and `modified_before` query parameters were added.
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$query_params['context']['default'] = 'view';
$query_params['after'] = array(
'description' => __( 'Limit response to posts published after a given ISO8601 compliant date.' ),
'type' => 'string',
'format' => 'date-time',
);
$query_params['modified_after'] = array(
'description' => __( 'Limit response to posts modified after a given ISO8601 compliant date.' ),
'type' => 'string',
'format' => 'date-time',
);
if ( post_type_supports( $this->post_type, 'author' ) ) {
$query_params['author'] = array(
'description' => __( 'Limit result set to posts assigned to specific authors.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['author_exclude'] = array(
'description' => __( 'Ensure result set excludes posts assigned to specific authors.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
}
$query_params['before'] = array(
'description' => __( 'Limit response to posts published before a given ISO8601 compliant date.' ),
'type' => 'string',
'format' => 'date-time',
);
$query_params['modified_before'] = array(
'description' => __( 'Limit response to posts modified before a given ISO8601 compliant date.' ),
'type' => 'string',
'format' => 'date-time',
);
$query_params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['include'] = array(
'description' => __( 'Limit result set to specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
$query_params['menu_order'] = array(
'description' => __( 'Limit result set to posts with a specific menu_order value.' ),
'type' => 'integer',
);
}
$query_params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.' ),
'type' => 'integer',
);
$query_params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
);
$query_params['orderby'] = array(
'description' => __( 'Sort collection by post attribute.' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'author',
'date',
'id',
'include',
'modified',
'parent',
'relevance',
'slug',
'include_slugs',
'title',
),
);
if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
$query_params['orderby']['enum'][] = 'menu_order';
}
$post_type = get_post_type_object( $this->post_type );
if ( $post_type->hierarchical || 'attachment' === $this->post_type ) {
$query_params['parent'] = array(
'description' => __( 'Limit result set to items with particular parent IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['parent_exclude'] = array(
'description' => __( 'Limit result set to all items except those of a particular parent ID.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
}
$query_params['slug'] = array(
'description' => __( 'Limit result set to posts with one or more specific slugs.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'sanitize_callback' => 'wp_parse_slug_list',
);
$query_params['status'] = array(
'default' => 'publish',
'description' => __( 'Limit result set to posts assigned one or more statuses.' ),
'type' => 'array',
'items' => array(
'enum' => array_merge( array_keys( get_post_stati() ), array( 'any' ) ),
'type' => 'string',
),
'sanitize_callback' => array( $this, 'sanitize_post_statuses' ),
);
$query_params = $this->prepare_taxonomy_limit_schema( $query_params );
if ( 'post' === $this->post_type ) {
$query_params['sticky'] = array(
'description' => __( 'Limit result set to items that are sticky.' ),
'type' => 'boolean',
);
}
/**
* Filters collection parameters for the posts controller.
*
* The dynamic part of the filter `$this->post_type` refers to the post
* type slug for the controller.
*
* This filter registers the collection parameter, but does not map the
* collection parameter to an internal WP_Query parameter. Use the
* `rest_{$this->post_type}_query` filter to set WP_Query parameters.
*
* @since 4.7.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
* @param WP_Post_Type $post_type Post type object.
*/
return apply_filters( "rest_{$this->post_type}_collection_params", $query_params, $post_type );
}
/**
* Sanitizes and validates the list of post statuses, including whether the
* user can query private statuses.
*
* @since 4.7.0
*
* @param string|array $statuses One or more post statuses.
* @param WP_REST_Request $request Full details about the request.
* @param string $parameter Additional parameter to pass to validation.
* @return array|WP_Error A list of valid statuses, otherwise WP_Error object.
*/
public function sanitize_post_statuses( $statuses, $request, $parameter ) {
$statuses = wp_parse_slug_list( $statuses );
// The default status is different in WP_REST_Attachments_Controller.
$attributes = $request->get_attributes();
$default_status = $attributes['args']['status']['default'];
foreach ( $statuses as $status ) {
if ( $status === $default_status ) {
continue;
}
$post_type_obj = get_post_type_object( $this->post_type );
if ( current_user_can( $post_type_obj->cap->edit_posts ) || 'private' === $status && current_user_can( $post_type_obj->cap->read_private_posts ) ) {
$result = rest_validate_request_arg( $status, $request, $parameter );
if ( is_wp_error( $result ) ) {
return $result;
}
} else {
return new WP_Error(
'rest_forbidden_status',
__( 'Status is forbidden.' ),
array( 'status' => rest_authorization_required_code() )
);
}
}
return $statuses;
}
/**
* Prepares the 'tax_query' for a collection of posts.
*
* @since 5.7.0
*
* @param array $args WP_Query arguments.
* @param WP_REST_Request $request Full details about the request.
* @return array Updated query arguments.
*/
private function prepare_tax_query( array $args, WP_REST_Request $request ) {
$relation = $request['tax_relation'];
if ( $relation ) {
$args['tax_query'] = array( 'relation' => $relation );
}
$taxonomies = wp_list_filter(
get_object_taxonomies( $this->post_type, 'objects' ),
array( 'show_in_rest' => true )
);
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
$tax_include = $request[ $base ];
$tax_exclude = $request[ $base . '_exclude' ];
if ( $tax_include ) {
$terms = array();
$include_children = false;
$operator = 'IN';
if ( rest_is_array( $tax_include ) ) {
$terms = $tax_include;
} elseif ( rest_is_object( $tax_include ) ) {
$terms = empty( $tax_include['terms'] ) ? array() : $tax_include['terms'];
$include_children = ! empty( $tax_include['include_children'] );
if ( isset( $tax_include['operator'] ) && 'AND' === $tax_include['operator'] ) {
$operator = 'AND';
}
}
if ( $terms ) {
$args['tax_query'][] = array(
'taxonomy' => $taxonomy->name,
'field' => 'term_id',
'terms' => $terms,
'include_children' => $include_children,
'operator' => $operator,
);
}
}
if ( $tax_exclude ) {
$terms = array();
$include_children = false;
if ( rest_is_array( $tax_exclude ) ) {
$terms = $tax_exclude;
} elseif ( rest_is_object( $tax_exclude ) ) {
$terms = empty( $tax_exclude['terms'] ) ? array() : $tax_exclude['terms'];
$include_children = ! empty( $tax_exclude['include_children'] );
}
if ( $terms ) {
$args['tax_query'][] = array(
'taxonomy' => $taxonomy->name,
'field' => 'term_id',
'terms' => $terms,
'include_children' => $include_children,
'operator' => 'NOT IN',
);
}
}
}
return $args;
}
/**
* Prepares the collection schema for including and excluding items by terms.
*
* @since 5.7.0
*
* @param array $query_params Collection schema.
* @return array Updated schema.
*/
private function prepare_taxonomy_limit_schema( array $query_params ) {
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
if ( ! $taxonomies ) {
return $query_params;
}
$query_params['tax_relation'] = array(
'description' => __( 'Limit result set based on relationship between multiple taxonomies.' ),
'type' => 'string',
'enum' => array( 'AND', 'OR' ),
);
$limit_schema = array(
'type' => array( 'object', 'array' ),
'oneOf' => array(
array(
'title' => __( 'Term ID List' ),
'description' => __( 'Match terms with the listed IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
),
array(
'title' => __( 'Term ID Taxonomy Query' ),
'description' => __( 'Perform an advanced term query.' ),
'type' => 'object',
'properties' => array(
'terms' => array(
'description' => __( 'Term IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
),
'include_children' => array(
'description' => __( 'Whether to include child terms in the terms limiting the result set.' ),
'type' => 'boolean',
'default' => false,
),
),
'additionalProperties' => false,
),
),
);
$include_schema = array_merge(
array(
/* translators: %s: Taxonomy name. */
'description' => __( 'Limit result set to items with specific terms assigned in the %s taxonomy.' ),
),
$limit_schema
);
// 'operator' is supported only for 'include' queries.
$include_schema['oneOf'][1]['properties']['operator'] = array(
'description' => __( 'Whether items must be assigned all or any of the specified terms.' ),
'type' => 'string',
'enum' => array( 'AND', 'OR' ),
'default' => 'OR',
);
$exclude_schema = array_merge(
array(
/* translators: %s: Taxonomy name. */
'description' => __( 'Limit result set to items except those with specific terms assigned in the %s taxonomy.' ),
),
$limit_schema
);
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
$base_exclude = $base . '_exclude';
$query_params[ $base ] = $include_schema;
$query_params[ $base ]['description'] = sprintf( $query_params[ $base ]['description'], $base );
$query_params[ $base_exclude ] = $exclude_schema;
$query_params[ $base_exclude ]['description'] = sprintf( $query_params[ $base_exclude ]['description'], $base );
if ( ! $taxonomy->hierarchical ) {
unset( $query_params[ $base ]['oneOf'][1]['properties']['include_children'] );
unset( $query_params[ $base_exclude ]['oneOf'][1]['properties']['include_children'] );
}
}
return $query_params;
}
}