WordPress 4.7
[autoinstalls/wordpress.git] / wp-includes / rest-api / endpoints / class-wp-rest-posts-controller.php
1 <?php
2 /**
3  * REST API: WP_REST_Posts_Controller class
4  *
5  * @package WordPress
6  * @subpackage REST_API
7  * @since 4.7.0
8  */
9
10 /**
11  * Core class to access posts via the REST API.
12  *
13  * @since 4.7.0
14  *
15  * @see WP_REST_Controller
16  */
17 class WP_REST_Posts_Controller extends WP_REST_Controller {
18
19         /**
20          * Post type.
21          *
22          * @since 4.7.0
23          * @access protected
24          * @var string
25          */
26         protected $post_type;
27
28         /**
29          * Instance of a post meta fields object.
30          *
31          * @since 4.7.0
32          * @access protected
33          * @var WP_REST_Post_Meta_Fields
34          */
35         protected $meta;
36
37         /**
38          * Constructor.
39          *
40          * @since 4.7.0
41          * @access public
42          *
43          * @param string $post_type Post type.
44          */
45         public function __construct( $post_type ) {
46                 $this->post_type = $post_type;
47                 $this->namespace = 'wp/v2';
48                 $obj = get_post_type_object( $post_type );
49                 $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
50
51                 $this->meta = new WP_REST_Post_Meta_Fields( $this->post_type );
52         }
53
54         /**
55          * Registers the routes for the objects of the controller.
56          *
57          * @since 4.7.0
58          * @access public
59          *
60          * @see register_rest_route()
61          */
62         public function register_routes() {
63
64                 register_rest_route( $this->namespace, '/' . $this->rest_base, array(
65                         array(
66                                 'methods'             => WP_REST_Server::READABLE,
67                                 'callback'            => array( $this, 'get_items' ),
68                                 'permission_callback' => array( $this, 'get_items_permissions_check' ),
69                                 'args'                => $this->get_collection_params(),
70                         ),
71                         array(
72                                 'methods'             => WP_REST_Server::CREATABLE,
73                                 'callback'            => array( $this, 'create_item' ),
74                                 'permission_callback' => array( $this, 'create_item_permissions_check' ),
75                                 'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
76                         ),
77                         'schema' => array( $this, 'get_public_item_schema' ),
78                 ) );
79
80                 register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
81                         array(
82                                 'methods'             => WP_REST_Server::READABLE,
83                                 'callback'            => array( $this, 'get_item' ),
84                                 'permission_callback' => array( $this, 'get_item_permissions_check' ),
85                                 'args'                => array(
86                                         'context'  => $this->get_context_param( array( 'default' => 'view' ) ),
87                                         'password' => array(
88                                                 'description' => __( 'The password for the post if it is password protected.' ),
89                                                 'type'        => 'string',
90                                         ),
91                                 ),
92                         ),
93                         array(
94                                 'methods'             => WP_REST_Server::EDITABLE,
95                                 'callback'            => array( $this, 'update_item' ),
96                                 'permission_callback' => array( $this, 'update_item_permissions_check' ),
97                                 'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
98                         ),
99                         array(
100                                 'methods'             => WP_REST_Server::DELETABLE,
101                                 'callback'            => array( $this, 'delete_item' ),
102                                 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
103                                 'args'                => array(
104                                         'force' => array(
105                                                 'type'        => 'boolean',
106                                                 'default'     => false,
107                                                 'description' => __( 'Whether to bypass trash and force deletion.' ),
108                                         ),
109                                 ),
110                         ),
111                         'schema' => array( $this, 'get_public_item_schema' ),
112                 ) );
113         }
114
115         /**
116          * Checks if a given request has access to read posts.
117          *
118          * @since 4.7.0
119          * @access public
120          *
121          * @param  WP_REST_Request $request Full details about the request.
122          * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
123          */
124         public function get_items_permissions_check( $request ) {
125
126                 $post_type = get_post_type_object( $this->post_type );
127
128                 if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) {
129                         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() ) );
130                 }
131
132                 return true;
133         }
134
135         /**
136          * Retrieves a collection of posts.
137          *
138          * @since 4.7.0
139          * @access public
140          *
141          * @param WP_REST_Request $request Full details about the request.
142          * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
143          */
144         public function get_items( $request ) {
145
146                 // Ensure a search string is set in case the orderby is set to 'relevance'.
147                 if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] && empty( $request['search'] ) ) {
148                         return new WP_Error( 'rest_no_search_term_defined', __( 'You need to define a search term to order by relevance.' ), array( 'status' => 400 ) );
149                 }
150
151                 // Ensure an include parameter is set in case the orderby is set to 'include'.
152                 if ( ! empty( $request['orderby'] ) && 'include' === $request['orderby'] && empty( $request['include'] ) ) {
153                         return new WP_Error( 'rest_orderby_include_missing_include', sprintf( __( 'Missing parameter(s): %s' ), 'include' ), array( 'status' => 400 ) );
154                 }
155
156                 // Retrieve the list of registered collection query parameters.
157                 $registered = $this->get_collection_params();
158                 $args = array();
159
160                 /*
161                  * This array defines mappings between public API query parameters whose
162                  * values are accepted as-passed, and their internal WP_Query parameter
163                  * name equivalents (some are the same). Only values which are also
164                  * present in $registered will be set.
165                  */
166                 $parameter_mappings = array(
167                         'author'         => 'author__in',
168                         'author_exclude' => 'author__not_in',
169                         'exclude'        => 'post__not_in',
170                         'include'        => 'post__in',
171                         'menu_order'     => 'menu_order',
172                         'offset'         => 'offset',
173                         'order'          => 'order',
174                         'orderby'        => 'orderby',
175                         'page'           => 'paged',
176                         'parent'         => 'post_parent__in',
177                         'parent_exclude' => 'post_parent__not_in',
178                         'search'         => 's',
179                         'slug'           => 'post_name__in',
180                         'status'         => 'post_status',
181                 );
182
183                 /*
184                  * For each known parameter which is both registered and present in the request,
185                  * set the parameter's value on the query $args.
186                  */
187                 foreach ( $parameter_mappings as $api_param => $wp_param ) {
188                         if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
189                                 $args[ $wp_param ] = $request[ $api_param ];
190                         }
191                 }
192
193                 // Check for & assign any parameters which require special handling or setting.
194                 $args['date_query'] = array();
195
196                 // Set before into date query. Date query must be specified as an array of an array.
197                 if ( isset( $registered['before'], $request['before'] ) ) {
198                         $args['date_query'][0]['before'] = $request['before'];
199                 }
200
201                 // Set after into date query. Date query must be specified as an array of an array.
202                 if ( isset( $registered['after'], $request['after'] ) ) {
203                         $args['date_query'][0]['after'] = $request['after'];
204                 }
205
206                 // Ensure our per_page parameter overrides any provided posts_per_page filter.
207                 if ( isset( $registered['per_page'] ) ) {
208                         $args['posts_per_page'] = $request['per_page'];
209                 }
210
211                 if ( isset( $registered['sticky'], $request['sticky'] ) ) {
212                         $sticky_posts = get_option( 'sticky_posts', array() );
213                         if ( $sticky_posts && $request['sticky'] ) {
214                                 /*
215                                  * As post__in will be used to only get sticky posts,
216                                  * we have to support the case where post__in was already
217                                  * specified.
218                                  */
219                                 $args['post__in'] = $args['post__in'] ? array_intersect( $sticky_posts, $args['post__in'] ) : $sticky_posts;
220
221                                 /*
222                                  * If we intersected, but there are no post ids in common,
223                                  * WP_Query won't return "no posts" for post__in = array()
224                                  * so we have to fake it a bit.
225                                  */
226                                 if ( ! $args['post__in'] ) {
227                                         $args['post__in'] = array( -1 );
228                                 }
229                         } elseif ( $sticky_posts ) {
230                                 /*
231                                  * As post___not_in will be used to only get posts that
232                                  * are not sticky, we have to support the case where post__not_in
233                                  * was already specified.
234                                  */
235                                 $args['post__not_in'] = array_merge( $args['post__not_in'], $sticky_posts );
236                         }
237                 }
238
239                 // Force the post_type argument, since it's not a user input variable.
240                 $args['post_type'] = $this->post_type;
241
242                 /**
243                  * Filters the query arguments for a request.
244                  *
245                  * Enables adding extra arguments or setting defaults for a post collection request.
246                  *
247                  * @since 4.7.0
248                  *
249                  * @link https://developer.wordpress.org/reference/classes/wp_query/
250                  *
251                  * @param array           $args    Key value array of query var to query value.
252                  * @param WP_REST_Request $request The request used.
253                  */
254                 $args = apply_filters( "rest_{$this->post_type}_query", $args, $request );
255                 $query_args = $this->prepare_items_query( $args, $request );
256
257                 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
258
259                 foreach ( $taxonomies as $taxonomy ) {
260                         $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
261                         $tax_exclude = $base . '_exclude';
262
263                         if ( ! empty( $request[ $base ] ) ) {
264                                 $query_args['tax_query'][] = array(
265                                         'taxonomy'         => $taxonomy->name,
266                                         'field'            => 'term_id',
267                                         'terms'            => $request[ $base ],
268                                         'include_children' => false,
269                                 );
270                         }
271
272                         if ( ! empty( $request[ $tax_exclude ] ) ) {
273                                 $query_args['tax_query'][] = array(
274                                         'taxonomy'         => $taxonomy->name,
275                                         'field'            => 'term_id',
276                                         'terms'            => $request[ $tax_exclude ],
277                                         'include_children' => false,
278                                         'operator'         => 'NOT IN',
279                                 );
280                         }
281                 }
282
283                 $posts_query  = new WP_Query();
284                 $query_result = $posts_query->query( $query_args );
285
286                 // Allow access to all password protected posts if the context is edit.
287                 if ( 'edit' === $request['context'] ) {
288                         add_filter( 'post_password_required', '__return_false' );
289                 }
290
291                 $posts = array();
292
293                 foreach ( $query_result as $post ) {
294                         if ( ! $this->check_read_permission( $post ) ) {
295                                 continue;
296                         }
297
298                         $data    = $this->prepare_item_for_response( $post, $request );
299                         $posts[] = $this->prepare_response_for_collection( $data );
300                 }
301
302                 // Reset filter.
303                 if ( 'edit' === $request['context'] ) {
304                         remove_filter( 'post_password_required', '__return_false' );
305                 }
306
307                 $page = (int) $query_args['paged'];
308                 $total_posts = $posts_query->found_posts;
309
310                 if ( $total_posts < 1 ) {
311                         // Out-of-bounds, run the query again without LIMIT for total count.
312                         unset( $query_args['paged'] );
313
314                         $count_query = new WP_Query();
315                         $count_query->query( $query_args );
316                         $total_posts = $count_query->found_posts;
317                 }
318
319                 $max_pages = ceil( $total_posts / (int) $posts_query->query_vars['posts_per_page'] );
320                 $response  = rest_ensure_response( $posts );
321
322                 $response->header( 'X-WP-Total', (int) $total_posts );
323                 $response->header( 'X-WP-TotalPages', (int) $max_pages );
324
325                 $request_params = $request->get_query_params();
326                 $base = add_query_arg( $request_params, rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
327
328                 if ( $page > 1 ) {
329                         $prev_page = $page - 1;
330
331                         if ( $prev_page > $max_pages ) {
332                                 $prev_page = $max_pages;
333                         }
334
335                         $prev_link = add_query_arg( 'page', $prev_page, $base );
336                         $response->link_header( 'prev', $prev_link );
337                 }
338                 if ( $max_pages > $page ) {
339                         $next_page = $page + 1;
340                         $next_link = add_query_arg( 'page', $next_page, $base );
341
342                         $response->link_header( 'next', $next_link );
343                 }
344
345                 return $response;
346         }
347
348         /**
349          * Checks if a given request has access to read a post.
350          *
351          * @since 4.7.0
352          * @access public
353          *
354          * @param WP_REST_Request $request Full details about the request.
355          * @return bool|WP_Error True if the request has read access for the item, WP_Error object otherwise.
356          */
357         public function get_item_permissions_check( $request ) {
358
359                 $post = get_post( (int) $request['id'] );
360
361                 if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
362                         return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => rest_authorization_required_code() ) );
363                 }
364
365                 if ( $post && ! empty( $request['password'] ) ) {
366                         // Check post password, and return error if invalid.
367                         if ( ! hash_equals( $post->post_password, $request['password'] ) ) {
368                                 return new WP_Error( 'rest_post_incorrect_password', __( 'Incorrect post password.' ), array( 'status' => 403 ) );
369                         }
370                 }
371
372                 // Allow access to all password protected posts if the context is edit.
373                 if ( 'edit' === $request['context'] ) {
374                         add_filter( 'post_password_required', '__return_false' );
375                 }
376
377                 if ( $post ) {
378                         return $this->check_read_permission( $post );
379                 }
380
381                 return true;
382         }
383
384         /**
385          * Checks if the user can access password-protected content.
386          *
387          * This method determines whether we need to override the regular password
388          * check in core with a filter.
389          *
390          * @since 4.7.0
391          * @access public
392          *
393          * @param WP_Post         $post    Post to check against.
394          * @param WP_REST_Request $request Request data to check.
395          * @return bool True if the user can access password-protected content, otherwise false.
396          */
397         public function can_access_password_content( $post, $request ) {
398                 if ( empty( $post->post_password ) ) {
399                         // No filter required.
400                         return false;
401                 }
402
403                 // Edit context always gets access to password-protected posts.
404                 if ( 'edit' === $request['context'] ) {
405                         return true;
406                 }
407
408                 // No password, no auth.
409                 if ( empty( $request['password'] ) ) {
410                         return false;
411                 }
412
413                 // Double-check the request password.
414                 return hash_equals( $post->post_password, $request['password'] );
415         }
416
417         /**
418          * Retrieves a single post.
419          *
420          * @since 4.7.0
421          * @access public
422          *
423          * @param WP_REST_Request $request Full details about the request.
424          * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
425          */
426         public function get_item( $request ) {
427                 $id   = (int) $request['id'];
428                 $post = get_post( $id );
429
430                 if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
431                         return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
432                 }
433
434                 $data     = $this->prepare_item_for_response( $post, $request );
435                 $response = rest_ensure_response( $data );
436
437                 if ( is_post_type_viewable( get_post_type_object( $post->post_type ) ) ) {
438                         $response->link_header( 'alternate',  get_permalink( $id ), array( 'type' => 'text/html' ) );
439                 }
440
441                 return $response;
442         }
443
444         /**
445          * Checks if a given request has access to create a post.
446          *
447          * @since 4.7.0
448          * @access public
449          *
450          * @param WP_REST_Request $request Full details about the request.
451          * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
452          */
453         public function create_item_permissions_check( $request ) {
454
455                 $post_type = get_post_type_object( $this->post_type );
456
457                 if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
458                         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() ) );
459                 }
460
461                 if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
462                         return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
463                 }
464
465                 if ( ! current_user_can( $post_type->cap->create_posts ) ) {
466                         return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to create posts as this user.' ), array( 'status' => rest_authorization_required_code() ) );
467                 }
468
469                 if ( ! $this->check_assign_terms_permission( $request ) ) {
470                         return new WP_Error( 'rest_cannot_assign_term', __( 'Sorry, you are not allowed to assign the provided terms.' ), array( 'status' => rest_authorization_required_code() ) );
471                 }
472
473                 return true;
474         }
475
476         /**
477          * Creates a single post.
478          *
479          * @since 4.7.0
480          * @access public
481          *
482          * @param WP_REST_Request $request Full details about the request.
483          * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
484          */
485         public function create_item( $request ) {
486                 if ( ! empty( $request['id'] ) ) {
487                         return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.' ), array( 'status' => 400 ) );
488                 }
489
490                 $prepared_post = $this->prepare_item_for_database( $request );
491
492                 if ( is_wp_error( $prepared_post ) ) {
493                         return $prepared_post;
494                 }
495
496                 $prepared_post->post_type = $this->post_type;
497
498                 $post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true );
499
500                 if ( is_wp_error( $post_id ) ) {
501
502                         if ( 'db_insert_error' === $post_id->get_error_code() ) {
503                                 $post_id->add_data( array( 'status' => 500 ) );
504                         } else {
505                                 $post_id->add_data( array( 'status' => 400 ) );
506                         }
507
508                         return $post_id;
509                 }
510
511                 $post = get_post( $post_id );
512
513                 /**
514                  * Fires after a single post is created or updated via the REST API.
515                  *
516                  * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
517                  *
518                  * @since 4.7.0
519                  *
520                  * @param WP_Post         $post     Inserted or updated post object.
521                  * @param WP_REST_Request $request  Request object.
522                  * @param bool            $creating True when creating a post, false when updating.
523                  */
524                 do_action( "rest_insert_{$this->post_type}", $post, $request, true );
525
526                 $schema = $this->get_item_schema();
527
528                 if ( ! empty( $schema['properties']['sticky'] ) ) {
529                         if ( ! empty( $request['sticky'] ) ) {
530                                 stick_post( $post_id );
531                         } else {
532                                 unstick_post( $post_id );
533                         }
534                 }
535
536                 if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
537                         $this->handle_featured_media( $request['featured_media'], $post_id );
538                 }
539
540                 if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
541                         set_post_format( $post, $request['format'] );
542                 }
543
544                 if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
545                         $this->handle_template( $request['template'], $post_id );
546                 }
547
548                 $terms_update = $this->handle_terms( $post_id, $request );
549
550                 if ( is_wp_error( $terms_update ) ) {
551                         return $terms_update;
552                 }
553
554                 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
555                         $meta_update = $this->meta->update_value( $request['meta'], $post_id );
556
557                         if ( is_wp_error( $meta_update ) ) {
558                                 return $meta_update;
559                         }
560                 }
561
562                 $post = get_post( $post_id );
563                 $fields_update = $this->update_additional_fields_for_object( $post, $request );
564
565                 if ( is_wp_error( $fields_update ) ) {
566                         return $fields_update;
567                 }
568
569                 $request->set_param( 'context', 'edit' );
570
571                 $response = $this->prepare_item_for_response( $post, $request );
572                 $response = rest_ensure_response( $response );
573
574                 $response->set_status( 201 );
575                 $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) );
576
577                 return $response;
578         }
579
580         /**
581          * Checks if a given request has access to update a post.
582          *
583          * @since 4.7.0
584          * @access public
585          *
586          * @param WP_REST_Request $request Full details about the request.
587          * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
588          */
589         public function update_item_permissions_check( $request ) {
590
591                 $post = get_post( $request['id'] );
592                 $post_type = get_post_type_object( $this->post_type );
593
594                 if ( $post && ! $this->check_update_permission( $post ) ) {
595                         return new WP_Error( 'rest_cannot_edit', __( 'Sorry, you are not allowed to edit this post.' ), array( 'status' => rest_authorization_required_code() ) );
596                 }
597
598                 if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
599                         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() ) );
600                 }
601
602                 if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
603                         return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
604                 }
605
606                 if ( ! $this->check_assign_terms_permission( $request ) ) {
607                         return new WP_Error( 'rest_cannot_assign_term', __( 'Sorry, you are not allowed to assign the provided terms.' ), array( 'status' => rest_authorization_required_code() ) );
608                 }
609
610                 return true;
611         }
612
613         /**
614          * Updates a single post.
615          *
616          * @since 4.7.0
617          * @access public
618          *
619          * @param WP_REST_Request $request Full details about the request.
620          * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
621          */
622         public function update_item( $request ) {
623                 $id   = (int) $request['id'];
624                 $post = get_post( $id );
625
626                 if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
627                         return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
628                 }
629
630                 $post = $this->prepare_item_for_database( $request );
631
632                 if ( is_wp_error( $post ) ) {
633                         return $post;
634                 }
635
636                 // convert the post object to an array, otherwise wp_update_post will expect non-escaped input.
637                 $post_id = wp_update_post( wp_slash( (array) $post ), true );
638
639                 if ( is_wp_error( $post_id ) ) {
640                         if ( 'db_update_error' === $post_id->get_error_code() ) {
641                                 $post_id->add_data( array( 'status' => 500 ) );
642                         } else {
643                                 $post_id->add_data( array( 'status' => 400 ) );
644                         }
645                         return $post_id;
646                 }
647
648                 $post = get_post( $post_id );
649
650                 /* This action is documented in lib/endpoints/class-wp-rest-controller.php */
651                 do_action( "rest_insert_{$this->post_type}", $post, $request, false );
652
653                 $schema = $this->get_item_schema();
654
655                 if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
656                         set_post_format( $post, $request['format'] );
657                 }
658
659                 if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
660                         $this->handle_featured_media( $request['featured_media'], $post_id );
661                 }
662
663                 if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) {
664                         if ( ! empty( $request['sticky'] ) ) {
665                                 stick_post( $post_id );
666                         } else {
667                                 unstick_post( $post_id );
668                         }
669                 }
670
671                 if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
672                         $this->handle_template( $request['template'], $post->ID );
673                 }
674
675                 $terms_update = $this->handle_terms( $post->ID, $request );
676
677                 if ( is_wp_error( $terms_update ) ) {
678                         return $terms_update;
679                 }
680
681                 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
682                         $meta_update = $this->meta->update_value( $request['meta'], $post->ID );
683
684                         if ( is_wp_error( $meta_update ) ) {
685                                 return $meta_update;
686                         }
687                 }
688
689                 $post = get_post( $post_id );
690                 $fields_update = $this->update_additional_fields_for_object( $post, $request );
691
692                 if ( is_wp_error( $fields_update ) ) {
693                         return $fields_update;
694                 }
695
696                 $request->set_param( 'context', 'edit' );
697
698                 $response = $this->prepare_item_for_response( $post, $request );
699
700                 return rest_ensure_response( $response );
701         }
702
703         /**
704          * Checks if a given request has access to delete a post.
705          *
706          * @since 4.7.0
707          * @access public
708          *
709          * @param WP_REST_Request $request Full details about the request.
710          * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
711          */
712         public function delete_item_permissions_check( $request ) {
713
714                 $post = get_post( $request['id'] );
715
716                 if ( $post && ! $this->check_delete_permission( $post ) ) {
717                         return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => rest_authorization_required_code() ) );
718                 }
719
720                 return true;
721         }
722
723         /**
724          * Deletes a single post.
725          *
726          * @since 4.7.0
727          * @access public
728          *
729          * @param WP_REST_Request $request Full details about the request.
730          * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
731          */
732         public function delete_item( $request ) {
733                 $id    = (int) $request['id'];
734                 $force = (bool) $request['force'];
735
736                 $post = get_post( $id );
737
738                 if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
739                         return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
740                 }
741
742                 $supports_trash = ( EMPTY_TRASH_DAYS > 0 );
743
744                 if ( 'attachment' === $post->post_type ) {
745                         $supports_trash = $supports_trash && MEDIA_TRASH;
746                 }
747
748                 /**
749                  * Filters whether a post is trashable.
750                  *
751                  * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
752                  *
753                  * Pass false to disable trash support for the post.
754                  *
755                  * @since 4.7.0
756                  *
757                  * @param bool    $supports_trash Whether the post type support trashing.
758                  * @param WP_Post $post           The Post object being considered for trashing support.
759                  */
760                 $supports_trash = apply_filters( "rest_{$this->post_type}_trashable", $supports_trash, $post );
761
762                 if ( ! $this->check_delete_permission( $post ) ) {
763                         return new WP_Error( 'rest_user_cannot_delete_post', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => rest_authorization_required_code() ) );
764                 }
765
766                 $request->set_param( 'context', 'edit' );
767
768
769                 // If we're forcing, then delete permanently.
770                 if ( $force ) {
771                         $previous = $this->prepare_item_for_response( $post, $request );
772                         $result = wp_delete_post( $id, true );
773                         $response = new WP_REST_Response();
774                         $response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data() ) );
775                 } else {
776                         // If we don't support trashing for this type, error out.
777                         if ( ! $supports_trash ) {
778                                 return new WP_Error( 'rest_trash_not_supported', __( 'The post does not support trashing. Set force=true to delete.' ), array( 'status' => 501 ) );
779                         }
780
781                         // Otherwise, only trash if we haven't already.
782                         if ( 'trash' === $post->post_status ) {
783                                 return new WP_Error( 'rest_already_trashed', __( 'The post has already been deleted.' ), array( 'status' => 410 ) );
784                         }
785
786                         // (Note that internally this falls through to `wp_delete_post` if
787                         // the trash is disabled.)
788                         $result = wp_trash_post( $id );
789                         $post = get_post( $id );
790                         $response = $this->prepare_item_for_response( $post, $request );
791                 }
792
793                 if ( ! $result ) {
794                         return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
795                 }
796
797                 /**
798                  * Fires immediately after a single post is deleted or trashed via the REST API.
799                  *
800                  * They dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
801                  *
802                  * @since 4.7.0
803                  *
804                  * @param object           $post     The deleted or trashed post.
805                  * @param WP_REST_Response $response The response data.
806                  * @param WP_REST_Request  $request  The request sent to the API.
807                  */
808                 do_action( "rest_delete_{$this->post_type}", $post, $response, $request );
809
810                 return $response;
811         }
812
813         /**
814          * Determines the allowed query_vars for a get_items() response and prepares
815          * them for WP_Query.
816          *
817          * @since 4.7.0
818          * @access protected
819          *
820          * @param array           $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
821          * @param WP_REST_Request $request       Optional. Full details about the request.
822          * @return array Items query arguments.
823          */
824         protected function prepare_items_query( $prepared_args = array(), $request = null ) {
825                 $query_args = array();
826
827                 foreach ( $prepared_args as $key => $value ) {
828                         /**
829                          * Filters the query_vars used in get_items() for the constructed query.
830                          *
831                          * The dynamic portion of the hook name, `$key`, refers to the query_var key.
832                          *
833                          * @since 4.7.0
834                          *
835                          * @param string $value The query_var value.
836                          */
837                         $query_args[ $key ] = apply_filters( "rest_query_var-{$key}", $value );
838                 }
839
840                 if ( 'post' !== $this->post_type || ! isset( $query_args['ignore_sticky_posts'] ) ) {
841                         $query_args['ignore_sticky_posts'] = true;
842                 }
843
844                 // Map to proper WP_Query orderby param.
845                 if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) {
846                         $orderby_mappings = array(
847                                 'id'      => 'ID',
848                                 'include' => 'post__in',
849                                 'slug'    => 'post_name',
850                         );
851
852                         if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
853                                 $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
854                         }
855                 }
856
857                 return $query_args;
858         }
859
860         /**
861          * Checks the post_date_gmt or modified_gmt and prepare any post or
862          * modified date for single post output.
863          *
864          * @since 4.7.0
865          * @access protected
866          *
867          * @param string      $date_gmt GMT publication time.
868          * @param string|null $date     Optional. Local publication time. Default null.
869          * @return string|null ISO8601/RFC3339 formatted datetime.
870          */
871         protected function prepare_date_response( $date_gmt, $date = null ) {
872                 // Use the date if passed.
873                 if ( isset( $date ) ) {
874                         return mysql_to_rfc3339( $date );
875                 }
876
877                 // Return null if $date_gmt is empty/zeros.
878                 if ( '0000-00-00 00:00:00' === $date_gmt ) {
879                         return null;
880                 }
881
882                 // Return the formatted datetime.
883                 return mysql_to_rfc3339( $date_gmt );
884         }
885
886         /**
887          * Prepares a single post for create or update.
888          *
889          * @since 4.7.0
890          * @access protected
891          *
892          * @param WP_REST_Request $request Request object.
893          * @return stdClass|WP_Error Post object or WP_Error.
894          */
895         protected function prepare_item_for_database( $request ) {
896                 $prepared_post = new stdClass;
897
898                 // Post ID.
899                 if ( isset( $request['id'] ) ) {
900                         $prepared_post->ID = absint( $request['id'] );
901                 }
902
903                 $schema = $this->get_item_schema();
904
905                 // Post title.
906                 if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
907                         if ( is_string( $request['title'] ) ) {
908                                 $prepared_post->post_title = $request['title'];
909                         } elseif ( ! empty( $request['title']['raw'] ) ) {
910                                 $prepared_post->post_title = $request['title']['raw'];
911                         }
912                 }
913
914                 // Post content.
915                 if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) {
916                         if ( is_string( $request['content'] ) ) {
917                                 $prepared_post->post_content = $request['content'];
918                         } elseif ( isset( $request['content']['raw'] ) ) {
919                                 $prepared_post->post_content = $request['content']['raw'];
920                         }
921                 }
922
923                 // Post excerpt.
924                 if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) {
925                         if ( is_string( $request['excerpt'] ) ) {
926                                 $prepared_post->post_excerpt = $request['excerpt'];
927                         } elseif ( isset( $request['excerpt']['raw'] ) ) {
928                                 $prepared_post->post_excerpt = $request['excerpt']['raw'];
929                         }
930                 }
931
932                 // Post type.
933                 if ( empty( $request['id'] ) ) {
934                         // Creating new post, use default type for the controller.
935                         $prepared_post->post_type = $this->post_type;
936                 } else {
937                         // Updating a post, use previous type.
938                         $prepared_post->post_type = get_post_type( $request['id'] );
939                 }
940
941                 $post_type = get_post_type_object( $prepared_post->post_type );
942
943                 // Post status.
944                 if ( ! empty( $schema['properties']['status'] ) && isset( $request['status'] ) ) {
945                         $status = $this->handle_status_param( $request['status'], $post_type );
946
947                         if ( is_wp_error( $status ) ) {
948                                 return $status;
949                         }
950
951                         $prepared_post->post_status = $status;
952                 }
953
954                 // Post date.
955                 if ( ! empty( $schema['properties']['date'] ) && ! empty( $request['date'] ) ) {
956                         $date_data = rest_get_date_with_gmt( $request['date'] );
957
958                         if ( ! empty( $date_data ) ) {
959                                 list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
960                         }
961                 } elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) {
962                         $date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
963
964                         if ( ! empty( $date_data ) ) {
965                                 list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
966                         }
967                 }
968
969                 // Post slug.
970                 if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) {
971                         $prepared_post->post_name = $request['slug'];
972                 }
973
974                 // Author.
975                 if ( ! empty( $schema['properties']['author'] ) && ! empty( $request['author'] ) ) {
976                         $post_author = (int) $request['author'];
977
978                         if ( get_current_user_id() !== $post_author ) {
979                                 $user_obj = get_userdata( $post_author );
980
981                                 if ( ! $user_obj ) {
982                                         return new WP_Error( 'rest_invalid_author', __( 'Invalid author ID.' ), array( 'status' => 400 ) );
983                                 }
984                         }
985
986                         $prepared_post->post_author = $post_author;
987                 }
988
989                 // Post password.
990                 if ( ! empty( $schema['properties']['password'] ) && isset( $request['password'] ) ) {
991                         $prepared_post->post_password = $request['password'];
992
993                         if ( '' !== $request['password'] ) {
994                                 if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
995                                         return new WP_Error( 'rest_invalid_field', __( 'A post can not be sticky and have a password.' ), array( 'status' => 400 ) );
996                                 }
997
998                                 if ( ! empty( $prepared_post->ID ) && is_sticky( $prepared_post->ID ) ) {
999                                         return new WP_Error( 'rest_invalid_field', __( 'A sticky post can not be password protected.' ), array( 'status' => 400 ) );
1000                                 }
1001                         }
1002                 }
1003
1004                 if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
1005                         if ( ! empty( $prepared_post->ID ) && post_password_required( $prepared_post->ID ) ) {
1006                                 return new WP_Error( 'rest_invalid_field', __( 'A password protected post can not be set to sticky.' ), array( 'status' => 400 ) );
1007                         }
1008                 }
1009
1010                 // Parent.
1011                 if ( ! empty( $schema['properties']['parent'] ) && isset( $request['parent'] ) ) {
1012                         if ( 0 === (int) $request['parent'] ) {
1013                                 $prepared_post->post_parent = 0;
1014                         } else {
1015                                 $parent = get_post( (int) $request['parent'] );
1016                                 if ( empty( $parent ) ) {
1017                                         return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post parent ID.' ), array( 'status' => 400 ) );
1018                                 }
1019                                 $prepared_post->post_parent = (int) $parent->ID;
1020                         }
1021                 }
1022
1023                 // Menu order.
1024                 if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) {
1025                         $prepared_post->menu_order = (int) $request['menu_order'];
1026                 }
1027
1028                 // Comment status.
1029                 if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) {
1030                         $prepared_post->comment_status = $request['comment_status'];
1031                 }
1032
1033                 // Ping status.
1034                 if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) {
1035                         $prepared_post->ping_status = $request['ping_status'];
1036                 }
1037
1038                 /**
1039                  * Filters a post before it is inserted via the REST API.
1040                  *
1041                  * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
1042                  *
1043                  * @since 4.7.0
1044                  *
1045                  * @param stdClass        $prepared_post An object representing a single post prepared
1046                  *                                       for inserting or updating the database.
1047                  * @param WP_REST_Request $request       Request object.
1048                  */
1049                 return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request );
1050
1051         }
1052
1053         /**
1054          * Determines validity and normalizes the given status parameter.
1055          *
1056          * @since 4.7.0
1057          * @access protected
1058          *
1059          * @param string $post_status Post status.
1060          * @param object $post_type   Post type.
1061          * @return string|WP_Error Post status or WP_Error if lacking the proper permission.
1062          */
1063         protected function handle_status_param( $post_status, $post_type ) {
1064
1065                 switch ( $post_status ) {
1066                         case 'draft':
1067                         case 'pending':
1068                                 break;
1069                         case 'private':
1070                                 if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
1071                                         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() ) );
1072                                 }
1073                                 break;
1074                         case 'publish':
1075                         case 'future':
1076                                 if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
1077                                         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() ) );
1078                                 }
1079                                 break;
1080                         default:
1081                                 if ( ! get_post_status_object( $post_status ) ) {
1082                                         $post_status = 'draft';
1083                                 }
1084                                 break;
1085                 }
1086
1087                 return $post_status;
1088         }
1089
1090         /**
1091          * Determines the featured media based on a request param.
1092          *
1093          * @since 4.7.0
1094          * @access protected
1095          *
1096          * @param int $featured_media Featured Media ID.
1097          * @param int $post_id        Post ID.
1098          * @return bool|WP_Error Whether the post thumbnail was successfully deleted, otherwise WP_Error.
1099          */
1100         protected function handle_featured_media( $featured_media, $post_id ) {
1101
1102                 $featured_media = (int) $featured_media;
1103                 if ( $featured_media ) {
1104                         $result = set_post_thumbnail( $post_id, $featured_media );
1105                         if ( $result ) {
1106                                 return true;
1107                         } else {
1108                                 return new WP_Error( 'rest_invalid_featured_media', __( 'Invalid featured media ID.' ), array( 'status' => 400 ) );
1109                         }
1110                 } else {
1111                         return delete_post_thumbnail( $post_id );
1112                 }
1113
1114         }
1115
1116         /**
1117          * Sets the template for a post.
1118          *
1119          * @since 4.7.0
1120          * @access public
1121          *
1122          * @param string  $template Page template filename.
1123          * @param integer $post_id  Post ID.
1124          */
1125         public function handle_template( $template, $post_id ) {
1126                 if ( in_array( $template, array_keys( wp_get_theme()->get_page_templates( get_post( $post_id ) ) ), true ) ) {
1127                         update_post_meta( $post_id, '_wp_page_template', $template );
1128                 } else {
1129                         update_post_meta( $post_id, '_wp_page_template', '' );
1130                 }
1131         }
1132
1133         /**
1134          * Updates the post's terms from a REST request.
1135          *
1136          * @since 4.7.0
1137          * @access protected
1138          *
1139          * @param int             $post_id The post ID to update the terms form.
1140          * @param WP_REST_Request $request The request object with post and terms data.
1141          * @return null|WP_Error WP_Error on an error assigning any of the terms, otherwise null.
1142          */
1143         protected function handle_terms( $post_id, $request ) {
1144                 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
1145
1146                 foreach ( $taxonomies as $taxonomy ) {
1147                         $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
1148
1149                         if ( ! isset( $request[ $base ] ) ) {
1150                                 continue;
1151                         }
1152
1153                         $result = wp_set_object_terms( $post_id, $request[ $base ], $taxonomy->name );
1154
1155                         if ( is_wp_error( $result ) ) {
1156                                 return $result;
1157                         }
1158                 }
1159         }
1160
1161         /**
1162          * Checks whether current user can assign all terms sent with the current request.
1163          *
1164          * @since 4.7.0
1165          *
1166          * @param WP_REST_Request $request The request object with post and terms data.
1167          * @return bool Whether the current user can assign the provided terms.
1168          */
1169         protected function check_assign_terms_permission( $request ) {
1170                 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
1171                 foreach ( $taxonomies as $taxonomy ) {
1172                         $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
1173
1174                         if ( ! isset( $request[ $base ] ) ) {
1175                                 continue;
1176                         }
1177
1178                         foreach ( $request[ $base ] as $term_id ) {
1179                                 // Invalid terms will be rejected later.
1180                                 if ( ! get_term( $term_id, $taxonomy->name ) ) {
1181                                         continue;
1182                                 }
1183
1184                                 if ( ! current_user_can( 'assign_term', (int) $term_id ) ) {
1185                                         return false;
1186                                 }
1187                         }
1188                 }
1189
1190                 return true;
1191         }
1192
1193         /**
1194          * Checks if a given post type can be viewed or managed.
1195          *
1196          * @since 4.7.0
1197          * @access protected
1198          *
1199          * @param object|string $post_type Post type name or object.
1200          * @return bool Whether the post type is allowed in REST.
1201          */
1202         protected function check_is_post_type_allowed( $post_type ) {
1203                 if ( ! is_object( $post_type ) ) {
1204                         $post_type = get_post_type_object( $post_type );
1205                 }
1206
1207                 if ( ! empty( $post_type ) && ! empty( $post_type->show_in_rest ) ) {
1208                         return true;
1209                 }
1210
1211                 return false;
1212         }
1213
1214         /**
1215          * Checks if a post can be read.
1216          *
1217          * Correctly handles posts with the inherit status.
1218          *
1219          * @since 4.7.0
1220          * @access public
1221          *
1222          * @param object $post Post object.
1223          * @return bool Whether the post can be read.
1224          */
1225         public function check_read_permission( $post ) {
1226                 $post_type = get_post_type_object( $post->post_type );
1227                 if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
1228                         return false;
1229                 }
1230
1231                 // Is the post readable?
1232                 if ( 'publish' === $post->post_status || current_user_can( $post_type->cap->read_post, $post->ID ) ) {
1233                         return true;
1234                 }
1235
1236                 $post_status_obj = get_post_status_object( $post->post_status );
1237                 if ( $post_status_obj && $post_status_obj->public ) {
1238                         return true;
1239                 }
1240
1241                 // Can we read the parent if we're inheriting?
1242                 if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) {
1243                         $parent = get_post( $post->post_parent );
1244                         return $this->check_read_permission( $parent );
1245                 }
1246
1247                 /*
1248                  * If there isn't a parent, but the status is set to inherit, assume
1249                  * it's published (as per get_post_status()).
1250                  */
1251                 if ( 'inherit' === $post->post_status ) {
1252                         return true;
1253                 }
1254
1255                 return false;
1256         }
1257
1258         /**
1259          * Checks if a post can be edited.
1260          *
1261          * @since 4.7.0
1262          * @access protected
1263          *
1264          * @param object $post Post object.
1265          * @return bool Whether the post can be edited.
1266          */
1267         protected function check_update_permission( $post ) {
1268                 $post_type = get_post_type_object( $post->post_type );
1269
1270                 if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
1271                         return false;
1272                 }
1273
1274                 return current_user_can( $post_type->cap->edit_post, $post->ID );
1275         }
1276
1277         /**
1278          * Checks if a post can be created.
1279          *
1280          * @since 4.7.0
1281          * @access protected
1282          *
1283          * @param object $post Post object.
1284          * @return bool Whether the post can be created.
1285          */
1286         protected function check_create_permission( $post ) {
1287                 $post_type = get_post_type_object( $post->post_type );
1288
1289                 if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
1290                         return false;
1291                 }
1292
1293                 return current_user_can( $post_type->cap->create_posts );
1294         }
1295
1296         /**
1297          * Checks if a post can be deleted.
1298          *
1299          * @since 4.7.0
1300          * @access protected
1301          *
1302          * @param object $post Post object.
1303          * @return bool Whether the post can be deleted.
1304          */
1305         protected function check_delete_permission( $post ) {
1306                 $post_type = get_post_type_object( $post->post_type );
1307
1308                 if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
1309                         return false;
1310                 }
1311
1312                 return current_user_can( $post_type->cap->delete_post, $post->ID );
1313         }
1314
1315         /**
1316          * Prepares a single post output for response.
1317          *
1318          * @since 4.7.0
1319          * @access public
1320          *
1321          * @param WP_Post         $post    Post object.
1322          * @param WP_REST_Request $request Request object.
1323          * @return WP_REST_Response Response object.
1324          */
1325         public function prepare_item_for_response( $post, $request ) {
1326                 $GLOBALS['post'] = $post;
1327
1328                 setup_postdata( $post );
1329
1330                 $schema = $this->get_item_schema();
1331
1332                 // Base fields for every post.
1333                 $data = array();
1334
1335                 if ( ! empty( $schema['properties']['id'] ) ) {
1336                         $data['id'] = $post->ID;
1337                 }
1338
1339                 if ( ! empty( $schema['properties']['date'] ) ) {
1340                         $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
1341                 }
1342
1343                 if ( ! empty( $schema['properties']['date_gmt'] ) ) {
1344                         $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
1345                 }
1346
1347                 if ( ! empty( $schema['properties']['guid'] ) ) {
1348                         $data['guid'] = array(
1349                                 /** This filter is documented in wp-includes/post-template.php */
1350                                 'rendered' => apply_filters( 'get_the_guid', $post->guid ),
1351                                 'raw'      => $post->guid,
1352                         );
1353                 }
1354
1355                 if ( ! empty( $schema['properties']['modified'] ) ) {
1356                         $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
1357                 }
1358
1359                 if ( ! empty( $schema['properties']['modified_gmt'] ) ) {
1360                         $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
1361                 }
1362
1363                 if ( ! empty( $schema['properties']['password'] ) ) {
1364                         $data['password'] = $post->post_password;
1365                 }
1366
1367                 if ( ! empty( $schema['properties']['slug'] ) ) {
1368                         $data['slug'] = $post->post_name;
1369                 }
1370
1371                 if ( ! empty( $schema['properties']['status'] ) ) {
1372                         $data['status'] = $post->post_status;
1373                 }
1374
1375                 if ( ! empty( $schema['properties']['type'] ) ) {
1376                         $data['type'] = $post->post_type;
1377                 }
1378
1379                 if ( ! empty( $schema['properties']['link'] ) ) {
1380                         $data['link'] = get_permalink( $post->ID );
1381                 }
1382
1383                 if ( ! empty( $schema['properties']['title'] ) ) {
1384                         add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
1385
1386                         $data['title'] = array(
1387                                 'raw'      => $post->post_title,
1388                                 'rendered' => get_the_title( $post->ID ),
1389                         );
1390
1391                         remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
1392                 }
1393
1394                 $has_password_filter = false;
1395
1396                 if ( $this->can_access_password_content( $post, $request ) ) {
1397                         // Allow access to the post, permissions already checked before.
1398                         add_filter( 'post_password_required', '__return_false' );
1399
1400                         $has_password_filter = true;
1401                 }
1402
1403                 if ( ! empty( $schema['properties']['content'] ) ) {
1404                         $data['content'] = array(
1405                                 'raw'       => $post->post_content,
1406                                 /** This filter is documented in wp-includes/post-template.php */
1407                                 'rendered'  => post_password_required( $post ) ? '' : apply_filters( 'the_content', $post->post_content ),
1408                                 'protected' => (bool) $post->post_password,
1409                         );
1410                 }
1411
1412                 if ( ! empty( $schema['properties']['excerpt'] ) ) {
1413                         /** This filter is documented in wp-includes/post-template.php */
1414                         $excerpt = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $post->post_excerpt, $post ) );
1415                         $data['excerpt'] = array(
1416                                 'raw'       => $post->post_excerpt,
1417                                 'rendered'  => post_password_required( $post ) ? '' : $excerpt,
1418                                 'protected' => (bool) $post->post_password,
1419                         );
1420                 }
1421
1422                 if ( $has_password_filter ) {
1423                         // Reset filter.
1424                         remove_filter( 'post_password_required', '__return_false' );
1425                 }
1426
1427                 if ( ! empty( $schema['properties']['author'] ) ) {
1428                         $data['author'] = (int) $post->post_author;
1429                 }
1430
1431                 if ( ! empty( $schema['properties']['featured_media'] ) ) {
1432                         $data['featured_media'] = (int) get_post_thumbnail_id( $post->ID );
1433                 }
1434
1435                 if ( ! empty( $schema['properties']['parent'] ) ) {
1436                         $data['parent'] = (int) $post->post_parent;
1437                 }
1438
1439                 if ( ! empty( $schema['properties']['menu_order'] ) ) {
1440                         $data['menu_order'] = (int) $post->menu_order;
1441                 }
1442
1443                 if ( ! empty( $schema['properties']['comment_status'] ) ) {
1444                         $data['comment_status'] = $post->comment_status;
1445                 }
1446
1447                 if ( ! empty( $schema['properties']['ping_status'] ) ) {
1448                         $data['ping_status'] = $post->ping_status;
1449                 }
1450
1451                 if ( ! empty( $schema['properties']['sticky'] ) ) {
1452                         $data['sticky'] = is_sticky( $post->ID );
1453                 }
1454
1455                 if ( ! empty( $schema['properties']['template'] ) ) {
1456                         if ( $template = get_page_template_slug( $post->ID ) ) {
1457                                 $data['template'] = $template;
1458                         } else {
1459                                 $data['template'] = '';
1460                         }
1461                 }
1462
1463                 if ( ! empty( $schema['properties']['format'] ) ) {
1464                         $data['format'] = get_post_format( $post->ID );
1465
1466                         // Fill in blank post format.
1467                         if ( empty( $data['format'] ) ) {
1468                                 $data['format'] = 'standard';
1469                         }
1470                 }
1471
1472                 if ( ! empty( $schema['properties']['meta'] ) ) {
1473                         $data['meta'] = $this->meta->get_value( $post->ID, $request );
1474                 }
1475
1476                 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
1477
1478                 foreach ( $taxonomies as $taxonomy ) {
1479                         $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
1480
1481                         if ( ! empty( $schema['properties'][ $base ] ) ) {
1482                                 $terms = get_the_terms( $post, $taxonomy->name );
1483                                 $data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
1484                         }
1485                 }
1486
1487                 $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
1488                 $data    = $this->add_additional_fields_to_object( $data, $request );
1489                 $data    = $this->filter_response_by_context( $data, $context );
1490
1491                 // Wrap the data in a response object.
1492                 $response = rest_ensure_response( $data );
1493
1494                 $response->add_links( $this->prepare_links( $post ) );
1495
1496                 /**
1497                  * Filters the post data for a response.
1498                  *
1499                  * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
1500                  *
1501                  * @since 4.7.0
1502                  *
1503                  * @param WP_REST_Response $response The response object.
1504                  * @param WP_Post          $post     Post object.
1505                  * @param WP_REST_Request  $request  Request object.
1506                  */
1507                 return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
1508         }
1509
1510         /**
1511          * Overwrites the default protected title format.
1512          *
1513          * By default, WordPress will show password protected posts with a title of
1514          * "Protected: %s", as the REST API communicates the protected status of a post
1515          * in a machine readable format, we remove the "Protected: " prefix.
1516          *
1517          * @return string Protected title format.
1518          */
1519         public function protected_title_format() {
1520                 return '%s';
1521         }
1522
1523         /**
1524          * Prepares links for the request.
1525          *
1526          * @since 4.7.0
1527          * @access protected
1528          *
1529          * @param WP_Post $post Post object.
1530          * @return array Links for the given post.
1531          */
1532         protected function prepare_links( $post ) {
1533                 $base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
1534
1535                 // Entity meta.
1536                 $links = array(
1537                         'self' => array(
1538                                 'href'   => rest_url( trailingslashit( $base ) . $post->ID ),
1539                         ),
1540                         'collection' => array(
1541                                 'href'   => rest_url( $base ),
1542                         ),
1543                         'about'      => array(
1544                                 'href'   => rest_url( 'wp/v2/types/' . $this->post_type ),
1545                         ),
1546                 );
1547
1548                 if ( ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'author' ) )
1549                         && ! empty( $post->post_author ) ) {
1550                         $links['author'] = array(
1551                                 'href'       => rest_url( 'wp/v2/users/' . $post->post_author ),
1552                                 'embeddable' => true,
1553                         );
1554                 }
1555
1556                 if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'comments' ) ) {
1557                         $replies_url = rest_url( 'wp/v2/comments' );
1558                         $replies_url = add_query_arg( 'post', $post->ID, $replies_url );
1559
1560                         $links['replies'] = array(
1561                                 'href'       => $replies_url,
1562                                 'embeddable' => true,
1563                         );
1564                 }
1565
1566                 if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'revisions' ) ) {
1567                         $links['version-history'] = array(
1568                                 'href' => rest_url( trailingslashit( $base ) . $post->ID . '/revisions' ),
1569                         );
1570                 }
1571
1572                 $post_type_obj = get_post_type_object( $post->post_type );
1573
1574                 if ( $post_type_obj->hierarchical && ! empty( $post->post_parent ) ) {
1575                         $links['up'] = array(
1576                                 'href'       => rest_url( trailingslashit( $base ) . (int) $post->post_parent ),
1577                                 'embeddable' => true,
1578                         );
1579                 }
1580
1581                 // If we have a featured media, add that.
1582                 if ( $featured_media = get_post_thumbnail_id( $post->ID ) ) {
1583                         $image_url = rest_url( 'wp/v2/media/' . $featured_media );
1584
1585                         $links['https://api.w.org/featuredmedia'] = array(
1586                                 'href'       => $image_url,
1587                                 'embeddable' => true,
1588                         );
1589                 }
1590
1591                 if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ), true ) ) {
1592                         $attachments_url = rest_url( 'wp/v2/media' );
1593                         $attachments_url = add_query_arg( 'parent', $post->ID, $attachments_url );
1594
1595                         $links['https://api.w.org/attachment'] = array(
1596                                 'href' => $attachments_url,
1597                         );
1598                 }
1599
1600                 $taxonomies = get_object_taxonomies( $post->post_type );
1601
1602                 if ( ! empty( $taxonomies ) ) {
1603                         $links['https://api.w.org/term'] = array();
1604
1605                         foreach ( $taxonomies as $tax ) {
1606                                 $taxonomy_obj = get_taxonomy( $tax );
1607
1608                                 // Skip taxonomies that are not public.
1609                                 if ( empty( $taxonomy_obj->show_in_rest ) ) {
1610                                         continue;
1611                                 }
1612
1613                                 $tax_base = ! empty( $taxonomy_obj->rest_base ) ? $taxonomy_obj->rest_base : $tax;
1614
1615                                 $terms_url = add_query_arg(
1616                                         'post',
1617                                         $post->ID,
1618                                         rest_url( 'wp/v2/' . $tax_base )
1619                                 );
1620
1621                                 $links['https://api.w.org/term'][] = array(
1622                                         'href'       => $terms_url,
1623                                         'taxonomy'   => $tax,
1624                                         'embeddable' => true,
1625                                 );
1626                         }
1627                 }
1628
1629                 return $links;
1630         }
1631
1632         /**
1633          * Retrieves the post's schema, conforming to JSON Schema.
1634          *
1635          * @since 4.7.0
1636          * @access public
1637          *
1638          * @return array Item schema data.
1639          */
1640         public function get_item_schema() {
1641
1642                 $schema = array(
1643                         '$schema'    => 'http://json-schema.org/schema#',
1644                         'title'      => $this->post_type,
1645                         'type'       => 'object',
1646                         // Base properties for every Post.
1647                         'properties' => array(
1648                                 'date'            => array(
1649                                         'description' => __( "The date the object was published, in the site's timezone." ),
1650                                         'type'        => 'string',
1651                                         'format'      => 'date-time',
1652                                         'context'     => array( 'view', 'edit', 'embed' ),
1653                                 ),
1654                                 'date_gmt'        => array(
1655                                         'description' => __( 'The date the object was published, as GMT.' ),
1656                                         'type'        => 'string',
1657                                         'format'      => 'date-time',
1658                                         'context'     => array( 'view', 'edit' ),
1659                                 ),
1660                                 'guid'            => array(
1661                                         'description' => __( 'The globally unique identifier for the object.' ),
1662                                         'type'        => 'object',
1663                                         'context'     => array( 'view', 'edit' ),
1664                                         'readonly'    => true,
1665                                         'properties'  => array(
1666                                                 'raw'      => array(
1667                                                         'description' => __( 'GUID for the object, as it exists in the database.' ),
1668                                                         'type'        => 'string',
1669                                                         'context'     => array( 'edit' ),
1670                                                         'readonly'    => true,
1671                                                 ),
1672                                                 'rendered' => array(
1673                                                         'description' => __( 'GUID for the object, transformed for display.' ),
1674                                                         'type'        => 'string',
1675                                                         'context'     => array( 'view', 'edit' ),
1676                                                         'readonly'    => true,
1677                                                 ),
1678                                         ),
1679                                 ),
1680                                 'id'              => array(
1681                                         'description' => __( 'Unique identifier for the object.' ),
1682                                         'type'        => 'integer',
1683                                         'context'     => array( 'view', 'edit', 'embed' ),
1684                                         'readonly'    => true,
1685                                 ),
1686                                 'link'            => array(
1687                                         'description' => __( 'URL to the object.' ),
1688                                         'type'        => 'string',
1689                                         'format'      => 'uri',
1690                                         'context'     => array( 'view', 'edit', 'embed' ),
1691                                         'readonly'    => true,
1692                                 ),
1693                                 'modified'        => array(
1694                                         'description' => __( "The date the object was last modified, in the site's timezone." ),
1695                                         'type'        => 'string',
1696                                         'format'      => 'date-time',
1697                                         'context'     => array( 'view', 'edit' ),
1698                                         'readonly'    => true,
1699                                 ),
1700                                 'modified_gmt'    => array(
1701                                         'description' => __( 'The date the object was last modified, as GMT.' ),
1702                                         'type'        => 'string',
1703                                         'format'      => 'date-time',
1704                                         'context'     => array( 'view', 'edit' ),
1705                                         'readonly'    => true,
1706                                 ),
1707                                 'slug'            => array(
1708                                         'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
1709                                         'type'        => 'string',
1710                                         'context'     => array( 'view', 'edit', 'embed' ),
1711                                         'arg_options' => array(
1712                                                 'sanitize_callback' => array( $this, 'sanitize_slug' ),
1713                                         ),
1714                                 ),
1715                                 'status'          => array(
1716                                         'description' => __( 'A named status for the object.' ),
1717                                         'type'        => 'string',
1718                                         'enum'        => array_keys( get_post_stati( array( 'internal' => false ) ) ),
1719                                         'context'     => array( 'edit' ),
1720                                 ),
1721                                 'type'            => array(
1722                                         'description' => __( 'Type of Post for the object.' ),
1723                                         'type'        => 'string',
1724                                         'context'     => array( 'view', 'edit', 'embed' ),
1725                                         'readonly'    => true,
1726                                 ),
1727                                 'password'        => array(
1728                                         'description' => __( 'A password to protect access to the content and excerpt.' ),
1729                                         'type'        => 'string',
1730                                         'context'     => array( 'edit' ),
1731                                 ),
1732                         ),
1733                 );
1734
1735                 $post_type_obj = get_post_type_object( $this->post_type );
1736
1737                 if ( $post_type_obj->hierarchical ) {
1738                         $schema['properties']['parent'] = array(
1739                                 'description' => __( 'The ID for the parent of the object.' ),
1740                                 'type'        => 'integer',
1741                                 'context'     => array( 'view', 'edit' ),
1742                         );
1743                 }
1744
1745                 $post_type_attributes = array(
1746                         'title',
1747                         'editor',
1748                         'author',
1749                         'excerpt',
1750                         'thumbnail',
1751                         'comments',
1752                         'revisions',
1753                         'page-attributes',
1754                         'post-formats',
1755                         'custom-fields',
1756                 );
1757                 $fixed_schemas = array(
1758                         'post' => array(
1759                                 'title',
1760                                 'editor',
1761                                 'author',
1762                                 'excerpt',
1763                                 'thumbnail',
1764                                 'comments',
1765                                 'revisions',
1766                                 'post-formats',
1767                                 'custom-fields',
1768                         ),
1769                         'page' => array(
1770                                 'title',
1771                                 'editor',
1772                                 'author',
1773                                 'excerpt',
1774                                 'thumbnail',
1775                                 'comments',
1776                                 'revisions',
1777                                 'page-attributes',
1778                                 'custom-fields',
1779                         ),
1780                         'attachment' => array(
1781                                 'title',
1782                                 'author',
1783                                 'comments',
1784                                 'revisions',
1785                                 'custom-fields',
1786                         ),
1787                 );
1788                 foreach ( $post_type_attributes as $attribute ) {
1789                         if ( isset( $fixed_schemas[ $this->post_type ] ) && ! in_array( $attribute, $fixed_schemas[ $this->post_type ], true ) ) {
1790                                 continue;
1791                         } elseif ( ! isset( $fixed_schemas[ $this->post_type ] ) && ! post_type_supports( $this->post_type, $attribute ) ) {
1792                                 continue;
1793                         }
1794
1795                         switch ( $attribute ) {
1796
1797                                 case 'title':
1798                                         $schema['properties']['title'] = array(
1799                                                 'description' => __( 'The title for the object.' ),
1800                                                 'type'        => 'object',
1801                                                 'context'     => array( 'view', 'edit', 'embed' ),
1802                                                 'arg_options' => array(
1803                                                         'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database()
1804                                                 ),
1805                                                 'properties'  => array(
1806                                                         'raw' => array(
1807                                                                 'description' => __( 'Title for the object, as it exists in the database.' ),
1808                                                                 'type'        => 'string',
1809                                                                 'context'     => array( 'edit' ),
1810                                                         ),
1811                                                         'rendered' => array(
1812                                                                 'description' => __( 'HTML title for the object, transformed for display.' ),
1813                                                                 'type'        => 'string',
1814                                                                 'context'     => array( 'view', 'edit', 'embed' ),
1815                                                                 'readonly'    => true,
1816                                                         ),
1817                                                 ),
1818                                         );
1819                                         break;
1820
1821                                 case 'editor':
1822                                         $schema['properties']['content'] = array(
1823                                                 'description' => __( 'The content for the object.' ),
1824                                                 'type'        => 'object',
1825                                                 'context'     => array( 'view', 'edit' ),
1826                                                 'arg_options' => array(
1827                                                         'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database()
1828                                                 ),
1829                                                 'properties'  => array(
1830                                                         'raw' => array(
1831                                                                 'description' => __( 'Content for the object, as it exists in the database.' ),
1832                                                                 'type'        => 'string',
1833                                                                 'context'     => array( 'edit' ),
1834                                                         ),
1835                                                         'rendered' => array(
1836                                                                 'description' => __( 'HTML content for the object, transformed for display.' ),
1837                                                                 'type'        => 'string',
1838                                                                 'context'     => array( 'view', 'edit' ),
1839                                                                 'readonly'    => true,
1840                                                         ),
1841                                                         'protected'       => array(
1842                                                                 'description' => __( 'Whether the content is protected with a password.' ),
1843                                                                 'type'        => 'boolean',
1844                                                                 'context'     => array( 'view', 'edit', 'embed' ),
1845                                                                 'readonly'    => true,
1846                                                         ),
1847                                                 ),
1848                                         );
1849                                         break;
1850
1851                                 case 'author':
1852                                         $schema['properties']['author'] = array(
1853                                                 'description' => __( 'The ID for the author of the object.' ),
1854                                                 'type'        => 'integer',
1855                                                 'context'     => array( 'view', 'edit', 'embed' ),
1856                                         );
1857                                         break;
1858
1859                                 case 'excerpt':
1860                                         $schema['properties']['excerpt'] = array(
1861                                                 'description' => __( 'The excerpt for the object.' ),
1862                                                 'type'        => 'object',
1863                                                 'context'     => array( 'view', 'edit', 'embed' ),
1864                                                 'arg_options' => array(
1865                                                         'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database()
1866                                                 ),
1867                                                 'properties'  => array(
1868                                                         'raw' => array(
1869                                                                 'description' => __( 'Excerpt for the object, as it exists in the database.' ),
1870                                                                 'type'        => 'string',
1871                                                                 'context'     => array( 'edit' ),
1872                                                         ),
1873                                                         'rendered' => array(
1874                                                                 'description' => __( 'HTML excerpt for the object, transformed for display.' ),
1875                                                                 'type'        => 'string',
1876                                                                 'context'     => array( 'view', 'edit', 'embed' ),
1877                                                                 'readonly'    => true,
1878                                                         ),
1879                                                         'protected'       => array(
1880                                                                 'description' => __( 'Whether the excerpt is protected with a password.' ),
1881                                                                 'type'        => 'boolean',
1882                                                                 'context'     => array( 'view', 'edit', 'embed' ),
1883                                                                 'readonly'    => true,
1884                                                         ),
1885                                                 ),
1886                                         );
1887                                         break;
1888
1889                                 case 'thumbnail':
1890                                         $schema['properties']['featured_media'] = array(
1891                                                 'description' => __( 'The ID of the featured media for the object.' ),
1892                                                 'type'        => 'integer',
1893                                                 'context'     => array( 'view', 'edit' ),
1894                                         );
1895                                         break;
1896
1897                                 case 'comments':
1898                                         $schema['properties']['comment_status'] = array(
1899                                                 'description' => __( 'Whether or not comments are open on the object.' ),
1900                                                 'type'        => 'string',
1901                                                 'enum'        => array( 'open', 'closed' ),
1902                                                 'context'     => array( 'view', 'edit' ),
1903                                         );
1904                                         $schema['properties']['ping_status'] = array(
1905                                                 'description' => __( 'Whether or not the object can be pinged.' ),
1906                                                 'type'        => 'string',
1907                                                 'enum'        => array( 'open', 'closed' ),
1908                                                 'context'     => array( 'view', 'edit' ),
1909                                         );
1910                                         break;
1911
1912                                 case 'page-attributes':
1913                                         $schema['properties']['menu_order'] = array(
1914                                                 'description' => __( 'The order of the object in relation to other object of its type.' ),
1915                                                 'type'        => 'integer',
1916                                                 'context'     => array( 'view', 'edit' ),
1917                                         );
1918                                         break;
1919
1920                                 case 'post-formats':
1921                                         $supports_formats = get_theme_support( 'post-formats' );
1922                                         $schema['properties']['format'] = array(
1923                                                 'description' => __( 'The format for the object.' ),
1924                                                 'type'        => 'string',
1925                                                 'enum'        => array_merge( array( 'standard' ), $supports_formats ? array_values( $supports_formats[0] ) : array() ),
1926                                                 'context'     => array( 'view', 'edit' ),
1927                                         );
1928                                         break;
1929
1930                                 case 'custom-fields':
1931                                         $schema['properties']['meta'] = $this->meta->get_field_schema();
1932                                         break;
1933
1934                         }
1935                 }
1936
1937                 if ( 'post' === $this->post_type ) {
1938                         $schema['properties']['sticky'] = array(
1939                                 'description' => __( 'Whether or not the object should be treated as sticky.' ),
1940                                 'type'        => 'boolean',
1941                                 'context'     => array( 'view', 'edit' ),
1942                         );
1943                 }
1944
1945                 $schema['properties']['template'] = array(
1946                         'description' => __( 'The theme file to use to display the object.' ),
1947                         'type'        => 'string',
1948                         'enum'        => array_merge( array_keys( wp_get_theme()->get_page_templates( null, $this->post_type ) ), array( '' ) ),
1949                         'context'     => array( 'view', 'edit' ),
1950                 );
1951
1952                 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
1953                 foreach ( $taxonomies as $taxonomy ) {
1954                         $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
1955                         $schema['properties'][ $base ] = array(
1956                                 /* translators: %s: taxonomy name */
1957                                 'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ),
1958                                 'type'        => 'array',
1959                                 'items'       => array(
1960                                         'type'    => 'integer',
1961                                 ),
1962                                 'context'     => array( 'view', 'edit' ),
1963                         );
1964                 }
1965
1966                 return $this->add_additional_fields_schema( $schema );
1967         }
1968
1969         /**
1970          * Retrieves the query params for the posts collection.
1971          *
1972          * @since 4.7.0
1973          * @access public
1974          *
1975          * @return array Collection parameters.
1976          */
1977         public function get_collection_params() {
1978                 $params = parent::get_collection_params();
1979
1980                 $params['context']['default'] = 'view';
1981
1982                 $params['after'] = array(
1983                         'description'        => __( 'Limit response to posts published after a given ISO8601 compliant date.' ),
1984                         'type'               => 'string',
1985                         'format'             => 'date-time',
1986                 );
1987
1988                 if ( post_type_supports( $this->post_type, 'author' ) ) {
1989                         $params['author'] = array(
1990                                 'description'         => __( 'Limit result set to posts assigned to specific authors.' ),
1991                                 'type'                => 'array',
1992                                 'items'               => array(
1993                                         'type'            => 'integer',
1994                                 ),
1995                                 'default'             => array(),
1996                         );
1997                         $params['author_exclude'] = array(
1998                                 'description'         => __( 'Ensure result set excludes posts assigned to specific authors.' ),
1999                                 'type'                => 'array',
2000                                 'items'               => array(
2001                                         'type'            => 'integer',
2002                                 ),
2003                                 'default'             => array(),
2004                         );
2005                 }
2006
2007                 $params['before'] = array(
2008                         'description'        => __( 'Limit response to posts published before a given ISO8601 compliant date.' ),
2009                         'type'               => 'string',
2010                         'format'             => 'date-time',
2011                 );
2012
2013                 $params['exclude'] = array(
2014                         'description'        => __( 'Ensure result set excludes specific IDs.' ),
2015                         'type'               => 'array',
2016                         'items'              => array(
2017                                 'type'           => 'integer',
2018                         ),
2019                         'default'            => array(),
2020                 );
2021
2022                 $params['include'] = array(
2023                         'description'        => __( 'Limit result set to specific IDs.' ),
2024                         'type'               => 'array',
2025                         'items'              => array(
2026                                 'type'           => 'integer',
2027                         ),
2028                         'default'            => array(),
2029                 );
2030
2031                 if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
2032                         $params['menu_order'] = array(
2033                                 'description'        => __( 'Limit result set to posts with a specific menu_order value.' ),
2034                                 'type'               => 'integer',
2035                         );
2036                 }
2037
2038                 $params['offset'] = array(
2039                         'description'        => __( 'Offset the result set by a specific number of items.' ),
2040                         'type'               => 'integer',
2041                 );
2042
2043                 $params['order'] = array(
2044                         'description'        => __( 'Order sort attribute ascending or descending.' ),
2045                         'type'               => 'string',
2046                         'default'            => 'desc',
2047                         'enum'               => array( 'asc', 'desc' ),
2048                 );
2049
2050                 $params['orderby'] = array(
2051                         'description'        => __( 'Sort collection by object attribute.' ),
2052                         'type'               => 'string',
2053                         'default'            => 'date',
2054                         'enum'               => array(
2055                                 'date',
2056                                 'relevance',
2057                                 'id',
2058                                 'include',
2059                                 'title',
2060                                 'slug',
2061                         ),
2062                 );
2063
2064                 if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
2065                         $params['orderby']['enum'][] = 'menu_order';
2066                 }
2067
2068                 $post_type_obj = get_post_type_object( $this->post_type );
2069
2070                 if ( $post_type_obj->hierarchical || 'attachment' === $this->post_type ) {
2071                         $params['parent'] = array(
2072                                 'description'       => __( 'Limit result set to those of particular parent IDs.' ),
2073                                 'type'              => 'array',
2074                                 'items'             => array(
2075                                         'type'          => 'integer',
2076                                 ),
2077                                 'default'           => array(),
2078                         );
2079                         $params['parent_exclude'] = array(
2080                                 'description'       => __( 'Limit result set to all items except those of a particular parent ID.' ),
2081                                 'type'              => 'array',
2082                                 'items'             => array(
2083                                         'type'          => 'integer',
2084                                 ),
2085                                 'default'           => array(),
2086                         );
2087                 }
2088
2089                 $params['slug'] = array(
2090                         'description'       => __( 'Limit result set to posts with one or more specific slugs.' ),
2091                         'type'              => 'array',
2092                         'items'             => array(
2093                                 'type'          => 'string',
2094                         ),
2095                         'sanitize_callback' => 'wp_parse_slug_list',
2096                 );
2097
2098                 $params['status'] = array(
2099                         'default'           => 'publish',
2100                         'description'       => __( 'Limit result set to posts assigned one or more statuses.' ),
2101                         'type'              => 'array',
2102                         'items'             => array(
2103                                 'enum'          => array_merge( array_keys( get_post_stati() ), array( 'any' ) ),
2104                                 'type'          => 'string',
2105                         ),
2106                         'sanitize_callback' => array( $this, 'sanitize_post_statuses' ),
2107                 );
2108
2109                 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
2110
2111                 foreach ( $taxonomies as $taxonomy ) {
2112                         $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
2113
2114                         $params[ $base ] = array(
2115                                 /* translators: %s: taxonomy name */
2116                                 'description'       => sprintf( __( 'Limit result set to all items that have the specified term assigned in the %s taxonomy.' ), $base ),
2117                                 'type'              => 'array',
2118                                 'items'             => array(
2119                                         'type'          => 'integer',
2120                                 ),
2121                                 'default'           => array(),
2122                         );
2123
2124                         $params[ $base . '_exclude' ] = array(
2125                                 /* translators: %s: taxonomy name */
2126                                 'description' => sprintf( __( 'Limit result set to all items except those that have the specified term assigned in the %s taxonomy.' ), $base ),
2127                                 'type'        => 'array',
2128                                 'items'       => array(
2129                                         'type'    => 'integer',
2130                                 ),
2131                                 'default'           => array(),
2132                         );
2133                 }
2134
2135                 if ( 'post' === $this->post_type ) {
2136                         $params['sticky'] = array(
2137                                 'description'       => __( 'Limit result set to items that are sticky.' ),
2138                                 'type'              => 'boolean',
2139                         );
2140                 }
2141
2142                 /**
2143                  * Filter collection parameters for the posts controller.
2144                  *
2145                  * The dynamic part of the filter `$this->post_type` refers to the post
2146                  * type slug for the controller.
2147                  *
2148                  * This filter registers the collection parameter, but does not map the
2149                  * collection parameter to an internal WP_Query parameter. Use the
2150                  * `rest_{$this->post_type}_query` filter to set WP_Query parameters.
2151                  *
2152                  * @since 4.7.0
2153                  *
2154                  * @param $params JSON Schema-formatted collection parameters.
2155                  * @param WP_Post_Type $post_type_obj Post type object.
2156                  */
2157                 return apply_filters( "rest_{$this->post_type}_collection_params", $params, $post_type_obj );
2158         }
2159
2160         /**
2161          * Sanitizes and validates the list of post statuses, including whether the
2162          * user can query private statuses.
2163          *
2164          * @since 4.7.0
2165          * @access public
2166          *
2167          * @param  string|array    $statuses  One or more post statuses.
2168          * @param  WP_REST_Request $request   Full details about the request.
2169          * @param  string          $parameter Additional parameter to pass to validation.
2170          * @return array|WP_Error A list of valid statuses, otherwise WP_Error object.
2171          */
2172         public function sanitize_post_statuses( $statuses, $request, $parameter ) {
2173                 $statuses = wp_parse_slug_list( $statuses );
2174
2175                 // The default status is different in WP_REST_Attachments_Controller
2176                 $attributes = $request->get_attributes();
2177                 $default_status = $attributes['args']['status']['default'];
2178
2179                 foreach ( $statuses as $status ) {
2180                         if ( $status === $default_status ) {
2181                                 continue;
2182                         }
2183
2184                         $post_type_obj = get_post_type_object( $this->post_type );
2185
2186                         if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
2187                                 $result = rest_validate_request_arg( $status, $request, $parameter );
2188                                 if ( is_wp_error( $result ) ) {
2189                                         return $result;
2190                                 }
2191                         } else {
2192                                 return new WP_Error( 'rest_forbidden_status', __( 'Status is forbidden.' ), array( 'status' => rest_authorization_required_code() ) );
2193                         }
2194                 }
2195
2196                 return $statuses;
2197         }
2198 }