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