class-wp-rest-posts-controller.php (94409B)
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 * Post type. 20 * 21 * @since 4.7.0 22 * @var string 23 */ 24 protected $post_type; 25 26 /** 27 * Instance of a post meta fields object. 28 * 29 * @since 4.7.0 30 * @var WP_REST_Post_Meta_Fields 31 */ 32 protected $meta; 33 34 /** 35 * Passwordless post access permitted. 36 * 37 * @since 5.7.1 38 * @var int[] 39 */ 40 protected $password_check_passed = array(); 41 42 /** 43 * Constructor. 44 * 45 * @since 4.7.0 46 * 47 * @param string $post_type Post type. 48 */ 49 public function __construct( $post_type ) { 50 $this->post_type = $post_type; 51 $this->namespace = 'wp/v2'; 52 $obj = get_post_type_object( $post_type ); 53 $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name; 54 55 $this->meta = new WP_REST_Post_Meta_Fields( $this->post_type ); 56 } 57 58 /** 59 * Registers the routes for posts. 60 * 61 * @since 4.7.0 62 * 63 * @see register_rest_route() 64 */ 65 public function register_routes() { 66 67 register_rest_route( 68 $this->namespace, 69 '/' . $this->rest_base, 70 array( 71 array( 72 'methods' => WP_REST_Server::READABLE, 73 'callback' => array( $this, 'get_items' ), 74 'permission_callback' => array( $this, 'get_items_permissions_check' ), 75 'args' => $this->get_collection_params(), 76 ), 77 array( 78 'methods' => WP_REST_Server::CREATABLE, 79 'callback' => array( $this, 'create_item' ), 80 'permission_callback' => array( $this, 'create_item_permissions_check' ), 81 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), 82 ), 83 'schema' => array( $this, 'get_public_item_schema' ), 84 ) 85 ); 86 87 $schema = $this->get_item_schema(); 88 $get_item_args = array( 89 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 90 ); 91 if ( isset( $schema['properties']['password'] ) ) { 92 $get_item_args['password'] = array( 93 'description' => __( 'The password for the post if it is password protected.' ), 94 'type' => 'string', 95 ); 96 } 97 register_rest_route( 98 $this->namespace, 99 '/' . $this->rest_base . '/(?P<id>[\d]+)', 100 array( 101 'args' => array( 102 'id' => array( 103 'description' => __( 'Unique identifier for the post.' ), 104 'type' => 'integer', 105 ), 106 ), 107 array( 108 'methods' => WP_REST_Server::READABLE, 109 'callback' => array( $this, 'get_item' ), 110 'permission_callback' => array( $this, 'get_item_permissions_check' ), 111 'args' => $get_item_args, 112 ), 113 array( 114 'methods' => WP_REST_Server::EDITABLE, 115 'callback' => array( $this, 'update_item' ), 116 'permission_callback' => array( $this, 'update_item_permissions_check' ), 117 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), 118 ), 119 array( 120 'methods' => WP_REST_Server::DELETABLE, 121 'callback' => array( $this, 'delete_item' ), 122 'permission_callback' => array( $this, 'delete_item_permissions_check' ), 123 'args' => array( 124 'force' => array( 125 'type' => 'boolean', 126 'default' => false, 127 'description' => __( 'Whether to bypass Trash and force deletion.' ), 128 ), 129 ), 130 ), 131 'schema' => array( $this, 'get_public_item_schema' ), 132 ) 133 ); 134 } 135 136 /** 137 * Checks if a given request has access to read posts. 138 * 139 * @since 4.7.0 140 * 141 * @param WP_REST_Request $request Full details about the request. 142 * @return true|WP_Error True if the request has read access, WP_Error object otherwise. 143 */ 144 public function get_items_permissions_check( $request ) { 145 146 $post_type = get_post_type_object( $this->post_type ); 147 148 if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) { 149 return new WP_Error( 150 'rest_forbidden_context', 151 __( 'Sorry, you are not allowed to edit posts in this post type.' ), 152 array( 'status' => rest_authorization_required_code() ) 153 ); 154 } 155 156 return true; 157 } 158 159 /** 160 * Override the result of the post password check for REST requested posts. 161 * 162 * Allow users to read the content of password protected posts if they have 163 * previously passed a permission check or if they have the `edit_post` capability 164 * for the post being checked. 165 * 166 * @since 5.7.1 167 * 168 * @param bool $required Whether the post requires a password check. 169 * @param WP_Post $post The post been password checked. 170 * @return bool Result of password check taking in to account REST API considerations. 171 */ 172 public function check_password_required( $required, $post ) { 173 if ( ! $required ) { 174 return $required; 175 } 176 177 $post = get_post( $post ); 178 179 if ( ! $post ) { 180 return $required; 181 } 182 183 if ( ! empty( $this->password_check_passed[ $post->ID ] ) ) { 184 // Password previously checked and approved. 185 return false; 186 } 187 188 return ! current_user_can( 'edit_post', $post->ID ); 189 } 190 191 /** 192 * Retrieves a collection of posts. 193 * 194 * @since 4.7.0 195 * 196 * @param WP_REST_Request $request Full details about the request. 197 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 198 */ 199 public function get_items( $request ) { 200 201 // Ensure a search string is set in case the orderby is set to 'relevance'. 202 if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] && empty( $request['search'] ) ) { 203 return new WP_Error( 204 'rest_no_search_term_defined', 205 __( 'You need to define a search term to order by relevance.' ), 206 array( 'status' => 400 ) 207 ); 208 } 209 210 // Ensure an include parameter is set in case the orderby is set to 'include'. 211 if ( ! empty( $request['orderby'] ) && 'include' === $request['orderby'] && empty( $request['include'] ) ) { 212 return new WP_Error( 213 'rest_orderby_include_missing_include', 214 __( 'You need to define an include parameter to order by include.' ), 215 array( 'status' => 400 ) 216 ); 217 } 218 219 // Retrieve the list of registered collection query parameters. 220 $registered = $this->get_collection_params(); 221 $args = array(); 222 223 /* 224 * This array defines mappings between public API query parameters whose 225 * values are accepted as-passed, and their internal WP_Query parameter 226 * name equivalents (some are the same). Only values which are also 227 * present in $registered will be set. 228 */ 229 $parameter_mappings = array( 230 'author' => 'author__in', 231 'author_exclude' => 'author__not_in', 232 'exclude' => 'post__not_in', 233 'include' => 'post__in', 234 'menu_order' => 'menu_order', 235 'offset' => 'offset', 236 'order' => 'order', 237 'orderby' => 'orderby', 238 'page' => 'paged', 239 'parent' => 'post_parent__in', 240 'parent_exclude' => 'post_parent__not_in', 241 'search' => 's', 242 'slug' => 'post_name__in', 243 'status' => 'post_status', 244 ); 245 246 /* 247 * For each known parameter which is both registered and present in the request, 248 * set the parameter's value on the query $args. 249 */ 250 foreach ( $parameter_mappings as $api_param => $wp_param ) { 251 if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { 252 $args[ $wp_param ] = $request[ $api_param ]; 253 } 254 } 255 256 // Check for & assign any parameters which require special handling or setting. 257 $args['date_query'] = array(); 258 259 if ( isset( $registered['before'], $request['before'] ) ) { 260 $args['date_query'][] = array( 261 'before' => $request['before'], 262 'column' => 'post_date', 263 ); 264 } 265 266 if ( isset( $registered['modified_before'], $request['modified_before'] ) ) { 267 $args['date_query'][] = array( 268 'before' => $request['modified_before'], 269 'column' => 'post_modified', 270 ); 271 } 272 273 if ( isset( $registered['after'], $request['after'] ) ) { 274 $args['date_query'][] = array( 275 'after' => $request['after'], 276 'column' => 'post_date', 277 ); 278 } 279 280 if ( isset( $registered['modified_after'], $request['modified_after'] ) ) { 281 $args['date_query'][] = array( 282 'after' => $request['modified_after'], 283 'column' => 'post_modified', 284 ); 285 } 286 287 // Ensure our per_page parameter overrides any provided posts_per_page filter. 288 if ( isset( $registered['per_page'] ) ) { 289 $args['posts_per_page'] = $request['per_page']; 290 } 291 292 if ( isset( $registered['sticky'], $request['sticky'] ) ) { 293 $sticky_posts = get_option( 'sticky_posts', array() ); 294 if ( ! is_array( $sticky_posts ) ) { 295 $sticky_posts = array(); 296 } 297 if ( $request['sticky'] ) { 298 /* 299 * As post__in will be used to only get sticky posts, 300 * we have to support the case where post__in was already 301 * specified. 302 */ 303 $args['post__in'] = $args['post__in'] ? array_intersect( $sticky_posts, $args['post__in'] ) : $sticky_posts; 304 305 /* 306 * If we intersected, but there are no post IDs in common, 307 * WP_Query won't return "no posts" for post__in = array() 308 * so we have to fake it a bit. 309 */ 310 if ( ! $args['post__in'] ) { 311 $args['post__in'] = array( 0 ); 312 } 313 } elseif ( $sticky_posts ) { 314 /* 315 * As post___not_in will be used to only get posts that 316 * are not sticky, we have to support the case where post__not_in 317 * was already specified. 318 */ 319 $args['post__not_in'] = array_merge( $args['post__not_in'], $sticky_posts ); 320 } 321 } 322 323 $args = $this->prepare_tax_query( $args, $request ); 324 325 // Force the post_type argument, since it's not a user input variable. 326 $args['post_type'] = $this->post_type; 327 328 /** 329 * Filters WP_Query arguments when querying posts via the REST API. 330 * 331 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. 332 * 333 * Possible hook names include: 334 * 335 * - `rest_post_query` 336 * - `rest_page_query` 337 * - `rest_attachment_query` 338 * 339 * Enables adding extra arguments or setting defaults for a post collection request. 340 * 341 * @since 4.7.0 342 * @since 5.7.0 Moved after the `tax_query` query arg is generated. 343 * 344 * @link https://developer.wordpress.org/reference/classes/wp_query/ 345 * 346 * @param array $args Array of arguments for WP_Query. 347 * @param WP_REST_Request $request The REST API request. 348 */ 349 $args = apply_filters( "rest_{$this->post_type}_query", $args, $request ); 350 $query_args = $this->prepare_items_query( $args, $request ); 351 352 $posts_query = new WP_Query(); 353 $query_result = $posts_query->query( $query_args ); 354 355 // Allow access to all password protected posts if the context is edit. 356 if ( 'edit' === $request['context'] ) { 357 add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 ); 358 } 359 360 $posts = array(); 361 362 foreach ( $query_result as $post ) { 363 if ( ! $this->check_read_permission( $post ) ) { 364 continue; 365 } 366 367 $data = $this->prepare_item_for_response( $post, $request ); 368 $posts[] = $this->prepare_response_for_collection( $data ); 369 } 370 371 // Reset filter. 372 if ( 'edit' === $request['context'] ) { 373 remove_filter( 'post_password_required', array( $this, 'check_password_required' ) ); 374 } 375 376 $page = (int) $query_args['paged']; 377 $total_posts = $posts_query->found_posts; 378 379 if ( $total_posts < 1 ) { 380 // Out-of-bounds, run the query again without LIMIT for total count. 381 unset( $query_args['paged'] ); 382 383 $count_query = new WP_Query(); 384 $count_query->query( $query_args ); 385 $total_posts = $count_query->found_posts; 386 } 387 388 $max_pages = ceil( $total_posts / (int) $posts_query->query_vars['posts_per_page'] ); 389 390 if ( $page > $max_pages && $total_posts > 0 ) { 391 return new WP_Error( 392 'rest_post_invalid_page_number', 393 __( 'The page number requested is larger than the number of pages available.' ), 394 array( 'status' => 400 ) 395 ); 396 } 397 398 $response = rest_ensure_response( $posts ); 399 400 $response->header( 'X-WP-Total', (int) $total_posts ); 401 $response->header( 'X-WP-TotalPages', (int) $max_pages ); 402 403 $request_params = $request->get_query_params(); 404 $base = add_query_arg( urlencode_deep( $request_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); 405 406 if ( $page > 1 ) { 407 $prev_page = $page - 1; 408 409 if ( $prev_page > $max_pages ) { 410 $prev_page = $max_pages; 411 } 412 413 $prev_link = add_query_arg( 'page', $prev_page, $base ); 414 $response->link_header( 'prev', $prev_link ); 415 } 416 if ( $max_pages > $page ) { 417 $next_page = $page + 1; 418 $next_link = add_query_arg( 'page', $next_page, $base ); 419 420 $response->link_header( 'next', $next_link ); 421 } 422 423 return $response; 424 } 425 426 /** 427 * Get the post, if the ID is valid. 428 * 429 * @since 4.7.2 430 * 431 * @param int $id Supplied ID. 432 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. 433 */ 434 protected function get_post( $id ) { 435 $error = new WP_Error( 436 'rest_post_invalid_id', 437 __( 'Invalid post ID.' ), 438 array( 'status' => 404 ) 439 ); 440 441 if ( (int) $id <= 0 ) { 442 return $error; 443 } 444 445 $post = get_post( (int) $id ); 446 if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { 447 return $error; 448 } 449 450 return $post; 451 } 452 453 /** 454 * Checks if a given request has access to read a post. 455 * 456 * @since 4.7.0 457 * 458 * @param WP_REST_Request $request Full details about the request. 459 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. 460 */ 461 public function get_item_permissions_check( $request ) { 462 $post = $this->get_post( $request['id'] ); 463 if ( is_wp_error( $post ) ) { 464 return $post; 465 } 466 467 if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) { 468 return new WP_Error( 469 'rest_forbidden_context', 470 __( 'Sorry, you are not allowed to edit this post.' ), 471 array( 'status' => rest_authorization_required_code() ) 472 ); 473 } 474 475 if ( $post && ! empty( $request['password'] ) ) { 476 // Check post password, and return error if invalid. 477 if ( ! hash_equals( $post->post_password, $request['password'] ) ) { 478 return new WP_Error( 479 'rest_post_incorrect_password', 480 __( 'Incorrect post password.' ), 481 array( 'status' => 403 ) 482 ); 483 } 484 } 485 486 // Allow access to all password protected posts if the context is edit. 487 if ( 'edit' === $request['context'] ) { 488 add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 ); 489 } 490 491 if ( $post ) { 492 return $this->check_read_permission( $post ); 493 } 494 495 return true; 496 } 497 498 /** 499 * Checks if the user can access password-protected content. 500 * 501 * This method determines whether we need to override the regular password 502 * check in core with a filter. 503 * 504 * @since 4.7.0 505 * 506 * @param WP_Post $post Post to check against. 507 * @param WP_REST_Request $request Request data to check. 508 * @return bool True if the user can access password-protected content, otherwise false. 509 */ 510 public function can_access_password_content( $post, $request ) { 511 if ( empty( $post->post_password ) ) { 512 // No filter required. 513 return false; 514 } 515 516 /* 517 * Users always gets access to password protected content in the edit 518 * context if they have the `edit_post` meta capability. 519 */ 520 if ( 521 'edit' === $request['context'] && 522 current_user_can( 'edit_post', $post->ID ) 523 ) { 524 return true; 525 } 526 527 // No password, no auth. 528 if ( empty( $request['password'] ) ) { 529 return false; 530 } 531 532 // Double-check the request password. 533 return hash_equals( $post->post_password, $request['password'] ); 534 } 535 536 /** 537 * Retrieves a single post. 538 * 539 * @since 4.7.0 540 * 541 * @param WP_REST_Request $request Full details about the request. 542 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 543 */ 544 public function get_item( $request ) { 545 $post = $this->get_post( $request['id'] ); 546 if ( is_wp_error( $post ) ) { 547 return $post; 548 } 549 550 $data = $this->prepare_item_for_response( $post, $request ); 551 $response = rest_ensure_response( $data ); 552 553 if ( is_post_type_viewable( get_post_type_object( $post->post_type ) ) ) { 554 $response->link_header( 'alternate', get_permalink( $post->ID ), array( 'type' => 'text/html' ) ); 555 } 556 557 return $response; 558 } 559 560 /** 561 * Checks if a given request has access to create a post. 562 * 563 * @since 4.7.0 564 * 565 * @param WP_REST_Request $request Full details about the request. 566 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise. 567 */ 568 public function create_item_permissions_check( $request ) { 569 if ( ! empty( $request['id'] ) ) { 570 return new WP_Error( 571 'rest_post_exists', 572 __( 'Cannot create existing post.' ), 573 array( 'status' => 400 ) 574 ); 575 } 576 577 $post_type = get_post_type_object( $this->post_type ); 578 579 if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) { 580 return new WP_Error( 581 'rest_cannot_edit_others', 582 __( 'Sorry, you are not allowed to create posts as this user.' ), 583 array( 'status' => rest_authorization_required_code() ) 584 ); 585 } 586 587 if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) { 588 return new WP_Error( 589 'rest_cannot_assign_sticky', 590 __( 'Sorry, you are not allowed to make posts sticky.' ), 591 array( 'status' => rest_authorization_required_code() ) 592 ); 593 } 594 595 if ( ! current_user_can( $post_type->cap->create_posts ) ) { 596 return new WP_Error( 597 'rest_cannot_create', 598 __( 'Sorry, you are not allowed to create posts as this user.' ), 599 array( 'status' => rest_authorization_required_code() ) 600 ); 601 } 602 603 if ( ! $this->check_assign_terms_permission( $request ) ) { 604 return new WP_Error( 605 'rest_cannot_assign_term', 606 __( 'Sorry, you are not allowed to assign the provided terms.' ), 607 array( 'status' => rest_authorization_required_code() ) 608 ); 609 } 610 611 return true; 612 } 613 614 /** 615 * Creates a single post. 616 * 617 * @since 4.7.0 618 * 619 * @param WP_REST_Request $request Full details about the request. 620 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 621 */ 622 public function create_item( $request ) { 623 if ( ! empty( $request['id'] ) ) { 624 return new WP_Error( 625 'rest_post_exists', 626 __( 'Cannot create existing post.' ), 627 array( 'status' => 400 ) 628 ); 629 } 630 631 $prepared_post = $this->prepare_item_for_database( $request ); 632 633 if ( is_wp_error( $prepared_post ) ) { 634 return $prepared_post; 635 } 636 637 $prepared_post->post_type = $this->post_type; 638 639 $post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true, false ); 640 641 if ( is_wp_error( $post_id ) ) { 642 643 if ( 'db_insert_error' === $post_id->get_error_code() ) { 644 $post_id->add_data( array( 'status' => 500 ) ); 645 } else { 646 $post_id->add_data( array( 'status' => 400 ) ); 647 } 648 649 return $post_id; 650 } 651 652 $post = get_post( $post_id ); 653 654 /** 655 * Fires after a single post is created or updated via the REST API. 656 * 657 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. 658 * 659 * Possible hook names include: 660 * 661 * - `rest_insert_post` 662 * - `rest_insert_page` 663 * - `rest_insert_attachment` 664 * 665 * @since 4.7.0 666 * 667 * @param WP_Post $post Inserted or updated post object. 668 * @param WP_REST_Request $request Request object. 669 * @param bool $creating True when creating a post, false when updating. 670 */ 671 do_action( "rest_insert_{$this->post_type}", $post, $request, true ); 672 673 $schema = $this->get_item_schema(); 674 675 if ( ! empty( $schema['properties']['sticky'] ) ) { 676 if ( ! empty( $request['sticky'] ) ) { 677 stick_post( $post_id ); 678 } else { 679 unstick_post( $post_id ); 680 } 681 } 682 683 if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) { 684 $this->handle_featured_media( $request['featured_media'], $post_id ); 685 } 686 687 if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) { 688 set_post_format( $post, $request['format'] ); 689 } 690 691 if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) { 692 $this->handle_template( $request['template'], $post_id, true ); 693 } 694 695 $terms_update = $this->handle_terms( $post_id, $request ); 696 697 if ( is_wp_error( $terms_update ) ) { 698 return $terms_update; 699 } 700 701 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { 702 $meta_update = $this->meta->update_value( $request['meta'], $post_id ); 703 704 if ( is_wp_error( $meta_update ) ) { 705 return $meta_update; 706 } 707 } 708 709 $post = get_post( $post_id ); 710 $fields_update = $this->update_additional_fields_for_object( $post, $request ); 711 712 if ( is_wp_error( $fields_update ) ) { 713 return $fields_update; 714 } 715 716 $request->set_param( 'context', 'edit' ); 717 718 /** 719 * Fires after a single post is completely created or updated via the REST API. 720 * 721 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. 722 * 723 * Possible hook names include: 724 * 725 * - `rest_after_insert_post` 726 * - `rest_after_insert_page` 727 * - `rest_after_insert_attachment` 728 * 729 * @since 5.0.0 730 * 731 * @param WP_Post $post Inserted or updated post object. 732 * @param WP_REST_Request $request Request object. 733 * @param bool $creating True when creating a post, false when updating. 734 */ 735 do_action( "rest_after_insert_{$this->post_type}", $post, $request, true ); 736 737 wp_after_insert_post( $post, false, null ); 738 739 $response = $this->prepare_item_for_response( $post, $request ); 740 $response = rest_ensure_response( $response ); 741 742 $response->set_status( 201 ); 743 $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) ); 744 745 return $response; 746 } 747 748 /** 749 * Checks if a given request has access to update a post. 750 * 751 * @since 4.7.0 752 * 753 * @param WP_REST_Request $request Full details about the request. 754 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise. 755 */ 756 public function update_item_permissions_check( $request ) { 757 $post = $this->get_post( $request['id'] ); 758 if ( is_wp_error( $post ) ) { 759 return $post; 760 } 761 762 $post_type = get_post_type_object( $this->post_type ); 763 764 if ( $post && ! $this->check_update_permission( $post ) ) { 765 return new WP_Error( 766 'rest_cannot_edit', 767 __( 'Sorry, you are not allowed to edit this post.' ), 768 array( 'status' => rest_authorization_required_code() ) 769 ); 770 } 771 772 if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) { 773 return new WP_Error( 774 'rest_cannot_edit_others', 775 __( 'Sorry, you are not allowed to update posts as this user.' ), 776 array( 'status' => rest_authorization_required_code() ) 777 ); 778 } 779 780 if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) { 781 return new WP_Error( 782 'rest_cannot_assign_sticky', 783 __( 'Sorry, you are not allowed to make posts sticky.' ), 784 array( 'status' => rest_authorization_required_code() ) 785 ); 786 } 787 788 if ( ! $this->check_assign_terms_permission( $request ) ) { 789 return new WP_Error( 790 'rest_cannot_assign_term', 791 __( 'Sorry, you are not allowed to assign the provided terms.' ), 792 array( 'status' => rest_authorization_required_code() ) 793 ); 794 } 795 796 return true; 797 } 798 799 /** 800 * Updates a single post. 801 * 802 * @since 4.7.0 803 * 804 * @param WP_REST_Request $request Full details about the request. 805 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 806 */ 807 public function update_item( $request ) { 808 $valid_check = $this->get_post( $request['id'] ); 809 if ( is_wp_error( $valid_check ) ) { 810 return $valid_check; 811 } 812 813 $post_before = get_post( $request['id'] ); 814 $post = $this->prepare_item_for_database( $request ); 815 816 if ( is_wp_error( $post ) ) { 817 return $post; 818 } 819 820 // Convert the post object to an array, otherwise wp_update_post() will expect non-escaped input. 821 $post_id = wp_update_post( wp_slash( (array) $post ), true, false ); 822 823 if ( is_wp_error( $post_id ) ) { 824 if ( 'db_update_error' === $post_id->get_error_code() ) { 825 $post_id->add_data( array( 'status' => 500 ) ); 826 } else { 827 $post_id->add_data( array( 'status' => 400 ) ); 828 } 829 return $post_id; 830 } 831 832 $post = get_post( $post_id ); 833 834 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ 835 do_action( "rest_insert_{$this->post_type}", $post, $request, false ); 836 837 $schema = $this->get_item_schema(); 838 839 if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) { 840 set_post_format( $post, $request['format'] ); 841 } 842 843 if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) { 844 $this->handle_featured_media( $request['featured_media'], $post_id ); 845 } 846 847 if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) { 848 if ( ! empty( $request['sticky'] ) ) { 849 stick_post( $post_id ); 850 } else { 851 unstick_post( $post_id ); 852 } 853 } 854 855 if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) { 856 $this->handle_template( $request['template'], $post->ID ); 857 } 858 859 $terms_update = $this->handle_terms( $post->ID, $request ); 860 861 if ( is_wp_error( $terms_update ) ) { 862 return $terms_update; 863 } 864 865 if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { 866 $meta_update = $this->meta->update_value( $request['meta'], $post->ID ); 867 868 if ( is_wp_error( $meta_update ) ) { 869 return $meta_update; 870 } 871 } 872 873 $post = get_post( $post_id ); 874 $fields_update = $this->update_additional_fields_for_object( $post, $request ); 875 876 if ( is_wp_error( $fields_update ) ) { 877 return $fields_update; 878 } 879 880 $request->set_param( 'context', 'edit' ); 881 882 // Filter is fired in WP_REST_Attachments_Controller subclass. 883 if ( 'attachment' === $this->post_type ) { 884 $response = $this->prepare_item_for_response( $post, $request ); 885 return rest_ensure_response( $response ); 886 } 887 888 /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ 889 do_action( "rest_after_insert_{$this->post_type}", $post, $request, false ); 890 891 wp_after_insert_post( $post, true, $post_before ); 892 893 $response = $this->prepare_item_for_response( $post, $request ); 894 895 return rest_ensure_response( $response ); 896 } 897 898 /** 899 * Checks if a given request has access to delete a post. 900 * 901 * @since 4.7.0 902 * 903 * @param WP_REST_Request $request Full details about the request. 904 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise. 905 */ 906 public function delete_item_permissions_check( $request ) { 907 $post = $this->get_post( $request['id'] ); 908 if ( is_wp_error( $post ) ) { 909 return $post; 910 } 911 912 if ( $post && ! $this->check_delete_permission( $post ) ) { 913 return new WP_Error( 914 'rest_cannot_delete', 915 __( 'Sorry, you are not allowed to delete this post.' ), 916 array( 'status' => rest_authorization_required_code() ) 917 ); 918 } 919 920 return true; 921 } 922 923 /** 924 * Deletes a single post. 925 * 926 * @since 4.7.0 927 * 928 * @param WP_REST_Request $request Full details about the request. 929 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 930 */ 931 public function delete_item( $request ) { 932 $post = $this->get_post( $request['id'] ); 933 if ( is_wp_error( $post ) ) { 934 return $post; 935 } 936 937 $id = $post->ID; 938 $force = (bool) $request['force']; 939 940 $supports_trash = ( EMPTY_TRASH_DAYS > 0 ); 941 942 if ( 'attachment' === $post->post_type ) { 943 $supports_trash = $supports_trash && MEDIA_TRASH; 944 } 945 946 /** 947 * Filters whether a post is trashable. 948 * 949 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. 950 * 951 * Possible hook names include: 952 * 953 * - `rest_post_trashable` 954 * - `rest_page_trashable` 955 * - `rest_attachment_trashable` 956 * 957 * Pass false to disable Trash support for the post. 958 * 959 * @since 4.7.0 960 * 961 * @param bool $supports_trash Whether the post type support trashing. 962 * @param WP_Post $post The Post object being considered for trashing support. 963 */ 964 $supports_trash = apply_filters( "rest_{$this->post_type}_trashable", $supports_trash, $post ); 965 966 if ( ! $this->check_delete_permission( $post ) ) { 967 return new WP_Error( 968 'rest_user_cannot_delete_post', 969 __( 'Sorry, you are not allowed to delete this post.' ), 970 array( 'status' => rest_authorization_required_code() ) 971 ); 972 } 973 974 $request->set_param( 'context', 'edit' ); 975 976 // If we're forcing, then delete permanently. 977 if ( $force ) { 978 $previous = $this->prepare_item_for_response( $post, $request ); 979 $result = wp_delete_post( $id, true ); 980 $response = new WP_REST_Response(); 981 $response->set_data( 982 array( 983 'deleted' => true, 984 'previous' => $previous->get_data(), 985 ) 986 ); 987 } else { 988 // If we don't support trashing for this type, error out. 989 if ( ! $supports_trash ) { 990 return new WP_Error( 991 'rest_trash_not_supported', 992 /* translators: %s: force=true */ 993 sprintf( __( "The post does not support trashing. Set '%s' to delete." ), 'force=true' ), 994 array( 'status' => 501 ) 995 ); 996 } 997 998 // Otherwise, only trash if we haven't already. 999 if ( 'trash' === $post->post_status ) { 1000 return new WP_Error( 1001 'rest_already_trashed', 1002 __( 'The post has already been deleted.' ), 1003 array( 'status' => 410 ) 1004 ); 1005 } 1006 1007 // (Note that internally this falls through to `wp_delete_post()` 1008 // if the Trash is disabled.) 1009 $result = wp_trash_post( $id ); 1010 $post = get_post( $id ); 1011 $response = $this->prepare_item_for_response( $post, $request ); 1012 } 1013 1014 if ( ! $result ) { 1015 return new WP_Error( 1016 'rest_cannot_delete', 1017 __( 'The post cannot be deleted.' ), 1018 array( 'status' => 500 ) 1019 ); 1020 } 1021 1022 /** 1023 * Fires immediately after a single post is deleted or trashed via the REST API. 1024 * 1025 * They dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. 1026 * 1027 * @since 4.7.0 1028 * 1029 * @param WP_Post $post The deleted or trashed post. 1030 * @param WP_REST_Response $response The response data. 1031 * @param WP_REST_Request $request The request sent to the API. 1032 */ 1033 do_action( "rest_delete_{$this->post_type}", $post, $response, $request ); 1034 1035 return $response; 1036 } 1037 1038 /** 1039 * Determines the allowed query_vars for a get_items() response and prepares 1040 * them for WP_Query. 1041 * 1042 * @since 4.7.0 1043 * 1044 * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. 1045 * @param WP_REST_Request $request Optional. Full details about the request. 1046 * @return array Items query arguments. 1047 */ 1048 protected function prepare_items_query( $prepared_args = array(), $request = null ) { 1049 $query_args = array(); 1050 1051 foreach ( $prepared_args as $key => $value ) { 1052 /** 1053 * Filters the query_vars used in get_items() for the constructed query. 1054 * 1055 * The dynamic portion of the hook name, `$key`, refers to the query_var key. 1056 * 1057 * @since 4.7.0 1058 * 1059 * @param string $value The query_var value. 1060 */ 1061 $query_args[ $key ] = apply_filters( "rest_query_var-{$key}", $value ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 1062 } 1063 1064 if ( 'post' !== $this->post_type || ! isset( $query_args['ignore_sticky_posts'] ) ) { 1065 $query_args['ignore_sticky_posts'] = true; 1066 } 1067 1068 // Map to proper WP_Query orderby param. 1069 if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) { 1070 $orderby_mappings = array( 1071 'id' => 'ID', 1072 'include' => 'post__in', 1073 'slug' => 'post_name', 1074 'include_slugs' => 'post_name__in', 1075 ); 1076 1077 if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) { 1078 $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ]; 1079 } 1080 } 1081 1082 return $query_args; 1083 } 1084 1085 /** 1086 * Checks the post_date_gmt or modified_gmt and prepare any post or 1087 * modified date for single post output. 1088 * 1089 * @since 4.7.0 1090 * 1091 * @param string $date_gmt GMT publication time. 1092 * @param string|null $date Optional. Local publication time. Default null. 1093 * @return string|null ISO8601/RFC3339 formatted datetime. 1094 */ 1095 protected function prepare_date_response( $date_gmt, $date = null ) { 1096 // Use the date if passed. 1097 if ( isset( $date ) ) { 1098 return mysql_to_rfc3339( $date ); 1099 } 1100 1101 // Return null if $date_gmt is empty/zeros. 1102 if ( '0000-00-00 00:00:00' === $date_gmt ) { 1103 return null; 1104 } 1105 1106 // Return the formatted datetime. 1107 return mysql_to_rfc3339( $date_gmt ); 1108 } 1109 1110 /** 1111 * Prepares a single post for create or update. 1112 * 1113 * @since 4.7.0 1114 * 1115 * @param WP_REST_Request $request Request object. 1116 * @return stdClass|WP_Error Post object or WP_Error. 1117 */ 1118 protected function prepare_item_for_database( $request ) { 1119 $prepared_post = new stdClass(); 1120 $current_status = ''; 1121 1122 // Post ID. 1123 if ( isset( $request['id'] ) ) { 1124 $existing_post = $this->get_post( $request['id'] ); 1125 if ( is_wp_error( $existing_post ) ) { 1126 return $existing_post; 1127 } 1128 1129 $prepared_post->ID = $existing_post->ID; 1130 $current_status = $existing_post->post_status; 1131 } 1132 1133 $schema = $this->get_item_schema(); 1134 1135 // Post title. 1136 if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) { 1137 if ( is_string( $request['title'] ) ) { 1138 $prepared_post->post_title = $request['title']; 1139 } elseif ( ! empty( $request['title']['raw'] ) ) { 1140 $prepared_post->post_title = $request['title']['raw']; 1141 } 1142 } 1143 1144 // Post content. 1145 if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) { 1146 if ( is_string( $request['content'] ) ) { 1147 $prepared_post->post_content = $request['content']; 1148 } elseif ( isset( $request['content']['raw'] ) ) { 1149 $prepared_post->post_content = $request['content']['raw']; 1150 } 1151 } 1152 1153 // Post excerpt. 1154 if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) { 1155 if ( is_string( $request['excerpt'] ) ) { 1156 $prepared_post->post_excerpt = $request['excerpt']; 1157 } elseif ( isset( $request['excerpt']['raw'] ) ) { 1158 $prepared_post->post_excerpt = $request['excerpt']['raw']; 1159 } 1160 } 1161 1162 // Post type. 1163 if ( empty( $request['id'] ) ) { 1164 // Creating new post, use default type for the controller. 1165 $prepared_post->post_type = $this->post_type; 1166 } else { 1167 // Updating a post, use previous type. 1168 $prepared_post->post_type = get_post_type( $request['id'] ); 1169 } 1170 1171 $post_type = get_post_type_object( $prepared_post->post_type ); 1172 1173 // Post status. 1174 if ( 1175 ! empty( $schema['properties']['status'] ) && 1176 isset( $request['status'] ) && 1177 ( ! $current_status || $current_status !== $request['status'] ) 1178 ) { 1179 $status = $this->handle_status_param( $request['status'], $post_type ); 1180 1181 if ( is_wp_error( $status ) ) { 1182 return $status; 1183 } 1184 1185 $prepared_post->post_status = $status; 1186 } 1187 1188 // Post date. 1189 if ( ! empty( $schema['properties']['date'] ) && ! empty( $request['date'] ) ) { 1190 $current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date : false; 1191 $date_data = rest_get_date_with_gmt( $request['date'] ); 1192 1193 if ( ! empty( $date_data ) && $current_date !== $date_data[0] ) { 1194 list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data; 1195 $prepared_post->edit_date = true; 1196 } 1197 } elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) { 1198 $current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date_gmt : false; 1199 $date_data = rest_get_date_with_gmt( $request['date_gmt'], true ); 1200 1201 if ( ! empty( $date_data ) && $current_date !== $date_data[1] ) { 1202 list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data; 1203 $prepared_post->edit_date = true; 1204 } 1205 } 1206 1207 // Sending a null date or date_gmt value resets date and date_gmt to their 1208 // default values (`0000-00-00 00:00:00`). 1209 if ( 1210 ( ! empty( $schema['properties']['date_gmt'] ) && $request->has_param( 'date_gmt' ) && null === $request['date_gmt'] ) || 1211 ( ! empty( $schema['properties']['date'] ) && $request->has_param( 'date' ) && null === $request['date'] ) 1212 ) { 1213 $prepared_post->post_date_gmt = null; 1214 $prepared_post->post_date = null; 1215 } 1216 1217 // Post slug. 1218 if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) { 1219 $prepared_post->post_name = $request['slug']; 1220 } 1221 1222 // Author. 1223 if ( ! empty( $schema['properties']['author'] ) && ! empty( $request['author'] ) ) { 1224 $post_author = (int) $request['author']; 1225 1226 if ( get_current_user_id() !== $post_author ) { 1227 $user_obj = get_userdata( $post_author ); 1228 1229 if ( ! $user_obj ) { 1230 return new WP_Error( 1231 'rest_invalid_author', 1232 __( 'Invalid author ID.' ), 1233 array( 'status' => 400 ) 1234 ); 1235 } 1236 } 1237 1238 $prepared_post->post_author = $post_author; 1239 } 1240 1241 // Post password. 1242 if ( ! empty( $schema['properties']['password'] ) && isset( $request['password'] ) ) { 1243 $prepared_post->post_password = $request['password']; 1244 1245 if ( '' !== $request['password'] ) { 1246 if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) { 1247 return new WP_Error( 1248 'rest_invalid_field', 1249 __( 'A post can not be sticky and have a password.' ), 1250 array( 'status' => 400 ) 1251 ); 1252 } 1253 1254 if ( ! empty( $prepared_post->ID ) && is_sticky( $prepared_post->ID ) ) { 1255 return new WP_Error( 1256 'rest_invalid_field', 1257 __( 'A sticky post can not be password protected.' ), 1258 array( 'status' => 400 ) 1259 ); 1260 } 1261 } 1262 } 1263 1264 if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) { 1265 if ( ! empty( $prepared_post->ID ) && post_password_required( $prepared_post->ID ) ) { 1266 return new WP_Error( 1267 'rest_invalid_field', 1268 __( 'A password protected post can not be set to sticky.' ), 1269 array( 'status' => 400 ) 1270 ); 1271 } 1272 } 1273 1274 // Parent. 1275 if ( ! empty( $schema['properties']['parent'] ) && isset( $request['parent'] ) ) { 1276 if ( 0 === (int) $request['parent'] ) { 1277 $prepared_post->post_parent = 0; 1278 } else { 1279 $parent = get_post( (int) $request['parent'] ); 1280 1281 if ( empty( $parent ) ) { 1282 return new WP_Error( 1283 'rest_post_invalid_id', 1284 __( 'Invalid post parent ID.' ), 1285 array( 'status' => 400 ) 1286 ); 1287 } 1288 1289 $prepared_post->post_parent = (int) $parent->ID; 1290 } 1291 } 1292 1293 // Menu order. 1294 if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) { 1295 $prepared_post->menu_order = (int) $request['menu_order']; 1296 } 1297 1298 // Comment status. 1299 if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) { 1300 $prepared_post->comment_status = $request['comment_status']; 1301 } 1302 1303 // Ping status. 1304 if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) { 1305 $prepared_post->ping_status = $request['ping_status']; 1306 } 1307 1308 if ( ! empty( $schema['properties']['template'] ) ) { 1309 // Force template to null so that it can be handled exclusively by the REST controller. 1310 $prepared_post->page_template = null; 1311 } 1312 1313 /** 1314 * Filters a post before it is inserted via the REST API. 1315 * 1316 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. 1317 * 1318 * Possible hook names include: 1319 * 1320 * - `rest_pre_insert_post` 1321 * - `rest_pre_insert_page` 1322 * - `rest_pre_insert_attachment` 1323 * 1324 * @since 4.7.0 1325 * 1326 * @param stdClass $prepared_post An object representing a single post prepared 1327 * for inserting or updating the database. 1328 * @param WP_REST_Request $request Request object. 1329 */ 1330 return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request ); 1331 1332 } 1333 1334 /** 1335 * Checks whether the status is valid for the given post. 1336 * 1337 * Allows for sending an update request with the current status, even if that status would not be acceptable. 1338 * 1339 * @since 5.6.0 1340 * 1341 * @param string $status The provided status. 1342 * @param WP_REST_Request $request The request object. 1343 * @param string $param The parameter name. 1344 * @return true|WP_Error True if the status is valid, or WP_Error if not. 1345 */ 1346 public function check_status( $status, $request, $param ) { 1347 if ( $request['id'] ) { 1348 $post = $this->get_post( $request['id'] ); 1349 1350 if ( ! is_wp_error( $post ) && $post->post_status === $status ) { 1351 return true; 1352 } 1353 } 1354 1355 $args = $request->get_attributes()['args'][ $param ]; 1356 1357 return rest_validate_value_from_schema( $status, $args, $param ); 1358 } 1359 1360 /** 1361 * Determines validity and normalizes the given status parameter. 1362 * 1363 * @since 4.7.0 1364 * 1365 * @param string $post_status Post status. 1366 * @param WP_Post_Type $post_type Post type. 1367 * @return string|WP_Error Post status or WP_Error if lacking the proper permission. 1368 */ 1369 protected function handle_status_param( $post_status, $post_type ) { 1370 1371 switch ( $post_status ) { 1372 case 'draft': 1373 case 'pending': 1374 break; 1375 case 'private': 1376 if ( ! current_user_can( $post_type->cap->publish_posts ) ) { 1377 return new WP_Error( 1378 'rest_cannot_publish', 1379 __( 'Sorry, you are not allowed to create private posts in this post type.' ), 1380 array( 'status' => rest_authorization_required_code() ) 1381 ); 1382 } 1383 break; 1384 case 'publish': 1385 case 'future': 1386 if ( ! current_user_can( $post_type->cap->publish_posts ) ) { 1387 return new WP_Error( 1388 'rest_cannot_publish', 1389 __( 'Sorry, you are not allowed to publish posts in this post type.' ), 1390 array( 'status' => rest_authorization_required_code() ) 1391 ); 1392 } 1393 break; 1394 default: 1395 if ( ! get_post_status_object( $post_status ) ) { 1396 $post_status = 'draft'; 1397 } 1398 break; 1399 } 1400 1401 return $post_status; 1402 } 1403 1404 /** 1405 * Determines the featured media based on a request param. 1406 * 1407 * @since 4.7.0 1408 * 1409 * @param int $featured_media Featured Media ID. 1410 * @param int $post_id Post ID. 1411 * @return bool|WP_Error Whether the post thumbnail was successfully deleted, otherwise WP_Error. 1412 */ 1413 protected function handle_featured_media( $featured_media, $post_id ) { 1414 1415 $featured_media = (int) $featured_media; 1416 if ( $featured_media ) { 1417 $result = set_post_thumbnail( $post_id, $featured_media ); 1418 if ( $result ) { 1419 return true; 1420 } else { 1421 return new WP_Error( 1422 'rest_invalid_featured_media', 1423 __( 'Invalid featured media ID.' ), 1424 array( 'status' => 400 ) 1425 ); 1426 } 1427 } else { 1428 return delete_post_thumbnail( $post_id ); 1429 } 1430 1431 } 1432 1433 /** 1434 * Check whether the template is valid for the given post. 1435 * 1436 * @since 4.9.0 1437 * 1438 * @param string $template Page template filename. 1439 * @param WP_REST_Request $request Request. 1440 * @return bool|WP_Error True if template is still valid or if the same as existing value, or false if template not supported. 1441 */ 1442 public function check_template( $template, $request ) { 1443 1444 if ( ! $template ) { 1445 return true; 1446 } 1447 1448 if ( $request['id'] ) { 1449 $post = get_post( $request['id'] ); 1450 $current_template = get_page_template_slug( $request['id'] ); 1451 } else { 1452 $post = null; 1453 $current_template = ''; 1454 } 1455 1456 // Always allow for updating a post to the same template, even if that template is no longer supported. 1457 if ( $template === $current_template ) { 1458 return true; 1459 } 1460 1461 // If this is a create request, get_post() will return null and wp theme will fallback to the passed post type. 1462 $allowed_templates = wp_get_theme()->get_page_templates( $post, $this->post_type ); 1463 1464 if ( isset( $allowed_templates[ $template ] ) ) { 1465 return true; 1466 } 1467 1468 return new WP_Error( 1469 'rest_invalid_param', 1470 /* translators: 1: Parameter, 2: List of valid values. */ 1471 sprintf( __( '%1$s is not one of %2$s.' ), 'template', implode( ', ', array_keys( $allowed_templates ) ) ) 1472 ); 1473 } 1474 1475 /** 1476 * Sets the template for a post. 1477 * 1478 * @since 4.7.0 1479 * @since 4.9.0 Added the `$validate` parameter. 1480 * 1481 * @param string $template Page template filename. 1482 * @param int $post_id Post ID. 1483 * @param bool $validate Whether to validate that the template selected is valid. 1484 */ 1485 public function handle_template( $template, $post_id, $validate = false ) { 1486 1487 if ( $validate && ! array_key_exists( $template, wp_get_theme()->get_page_templates( get_post( $post_id ) ) ) ) { 1488 $template = ''; 1489 } 1490 1491 update_post_meta( $post_id, '_wp_page_template', $template ); 1492 } 1493 1494 /** 1495 * Updates the post's terms from a REST request. 1496 * 1497 * @since 4.7.0 1498 * 1499 * @param int $post_id The post ID to update the terms form. 1500 * @param WP_REST_Request $request The request object with post and terms data. 1501 * @return null|WP_Error WP_Error on an error assigning any of the terms, otherwise null. 1502 */ 1503 protected function handle_terms( $post_id, $request ) { 1504 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); 1505 1506 foreach ( $taxonomies as $taxonomy ) { 1507 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; 1508 1509 if ( ! isset( $request[ $base ] ) ) { 1510 continue; 1511 } 1512 1513 $result = wp_set_object_terms( $post_id, $request[ $base ], $taxonomy->name ); 1514 1515 if ( is_wp_error( $result ) ) { 1516 return $result; 1517 } 1518 } 1519 } 1520 1521 /** 1522 * Checks whether current user can assign all terms sent with the current request. 1523 * 1524 * @since 4.7.0 1525 * 1526 * @param WP_REST_Request $request The request object with post and terms data. 1527 * @return bool Whether the current user can assign the provided terms. 1528 */ 1529 protected function check_assign_terms_permission( $request ) { 1530 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); 1531 foreach ( $taxonomies as $taxonomy ) { 1532 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; 1533 1534 if ( ! isset( $request[ $base ] ) ) { 1535 continue; 1536 } 1537 1538 foreach ( $request[ $base ] as $term_id ) { 1539 // Invalid terms will be rejected later. 1540 if ( ! get_term( $term_id, $taxonomy->name ) ) { 1541 continue; 1542 } 1543 1544 if ( ! current_user_can( 'assign_term', (int) $term_id ) ) { 1545 return false; 1546 } 1547 } 1548 } 1549 1550 return true; 1551 } 1552 1553 /** 1554 * Checks if a given post type can be viewed or managed. 1555 * 1556 * @since 4.7.0 1557 * 1558 * @param WP_Post_Type|string $post_type Post type name or object. 1559 * @return bool Whether the post type is allowed in REST. 1560 */ 1561 protected function check_is_post_type_allowed( $post_type ) { 1562 if ( ! is_object( $post_type ) ) { 1563 $post_type = get_post_type_object( $post_type ); 1564 } 1565 1566 if ( ! empty( $post_type ) && ! empty( $post_type->show_in_rest ) ) { 1567 return true; 1568 } 1569 1570 return false; 1571 } 1572 1573 /** 1574 * Checks if a post can be read. 1575 * 1576 * Correctly handles posts with the inherit status. 1577 * 1578 * @since 4.7.0 1579 * 1580 * @param WP_Post $post Post object. 1581 * @return bool Whether the post can be read. 1582 */ 1583 public function check_read_permission( $post ) { 1584 $post_type = get_post_type_object( $post->post_type ); 1585 if ( ! $this->check_is_post_type_allowed( $post_type ) ) { 1586 return false; 1587 } 1588 1589 // Is the post readable? 1590 if ( 'publish' === $post->post_status || current_user_can( 'read_post', $post->ID ) ) { 1591 return true; 1592 } 1593 1594 $post_status_obj = get_post_status_object( $post->post_status ); 1595 if ( $post_status_obj && $post_status_obj->public ) { 1596 return true; 1597 } 1598 1599 // Can we read the parent if we're inheriting? 1600 if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) { 1601 $parent = get_post( $post->post_parent ); 1602 if ( $parent ) { 1603 return $this->check_read_permission( $parent ); 1604 } 1605 } 1606 1607 /* 1608 * If there isn't a parent, but the status is set to inherit, assume 1609 * it's published (as per get_post_status()). 1610 */ 1611 if ( 'inherit' === $post->post_status ) { 1612 return true; 1613 } 1614 1615 return false; 1616 } 1617 1618 /** 1619 * Checks if a post can be edited. 1620 * 1621 * @since 4.7.0 1622 * 1623 * @param WP_Post $post Post object. 1624 * @return bool Whether the post can be edited. 1625 */ 1626 protected function check_update_permission( $post ) { 1627 $post_type = get_post_type_object( $post->post_type ); 1628 1629 if ( ! $this->check_is_post_type_allowed( $post_type ) ) { 1630 return false; 1631 } 1632 1633 return current_user_can( 'edit_post', $post->ID ); 1634 } 1635 1636 /** 1637 * Checks if a post can be created. 1638 * 1639 * @since 4.7.0 1640 * 1641 * @param WP_Post $post Post object. 1642 * @return bool Whether the post can be created. 1643 */ 1644 protected function check_create_permission( $post ) { 1645 $post_type = get_post_type_object( $post->post_type ); 1646 1647 if ( ! $this->check_is_post_type_allowed( $post_type ) ) { 1648 return false; 1649 } 1650 1651 return current_user_can( $post_type->cap->create_posts ); 1652 } 1653 1654 /** 1655 * Checks if a post can be deleted. 1656 * 1657 * @since 4.7.0 1658 * 1659 * @param WP_Post $post Post object. 1660 * @return bool Whether the post can be deleted. 1661 */ 1662 protected function check_delete_permission( $post ) { 1663 $post_type = get_post_type_object( $post->post_type ); 1664 1665 if ( ! $this->check_is_post_type_allowed( $post_type ) ) { 1666 return false; 1667 } 1668 1669 return current_user_can( 'delete_post', $post->ID ); 1670 } 1671 1672 /** 1673 * Prepares a single post output for response. 1674 * 1675 * @since 4.7.0 1676 * 1677 * @param WP_Post $post Post object. 1678 * @param WP_REST_Request $request Request object. 1679 * @return WP_REST_Response Response object. 1680 */ 1681 public function prepare_item_for_response( $post, $request ) { 1682 $GLOBALS['post'] = $post; 1683 1684 setup_postdata( $post ); 1685 1686 $fields = $this->get_fields_for_response( $request ); 1687 1688 // Base fields for every post. 1689 $data = array(); 1690 1691 if ( rest_is_field_included( 'id', $fields ) ) { 1692 $data['id'] = $post->ID; 1693 } 1694 1695 if ( rest_is_field_included( 'date', $fields ) ) { 1696 $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date ); 1697 } 1698 1699 if ( rest_is_field_included( 'date_gmt', $fields ) ) { 1700 /* 1701 * For drafts, `post_date_gmt` may not be set, indicating that the date 1702 * of the draft should be updated each time it is saved (see #38883). 1703 * In this case, shim the value based on the `post_date` field 1704 * with the site's timezone offset applied. 1705 */ 1706 if ( '0000-00-00 00:00:00' === $post->post_date_gmt ) { 1707 $post_date_gmt = get_gmt_from_date( $post->post_date ); 1708 } else { 1709 $post_date_gmt = $post->post_date_gmt; 1710 } 1711 $data['date_gmt'] = $this->prepare_date_response( $post_date_gmt ); 1712 } 1713 1714 if ( rest_is_field_included( 'guid', $fields ) ) { 1715 $data['guid'] = array( 1716 /** This filter is documented in wp-includes/post-template.php */ 1717 'rendered' => apply_filters( 'get_the_guid', $post->guid, $post->ID ), 1718 'raw' => $post->guid, 1719 ); 1720 } 1721 1722 if ( rest_is_field_included( 'modified', $fields ) ) { 1723 $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified ); 1724 } 1725 1726 if ( rest_is_field_included( 'modified_gmt', $fields ) ) { 1727 /* 1728 * For drafts, `post_modified_gmt` may not be set (see `post_date_gmt` comments 1729 * above). In this case, shim the value based on the `post_modified` field 1730 * with the site's timezone offset applied. 1731 */ 1732 if ( '0000-00-00 00:00:00' === $post->post_modified_gmt ) { 1733 $post_modified_gmt = gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) - ( get_option( 'gmt_offset' ) * 3600 ) ); 1734 } else { 1735 $post_modified_gmt = $post->post_modified_gmt; 1736 } 1737 $data['modified_gmt'] = $this->prepare_date_response( $post_modified_gmt ); 1738 } 1739 1740 if ( rest_is_field_included( 'password', $fields ) ) { 1741 $data['password'] = $post->post_password; 1742 } 1743 1744 if ( rest_is_field_included( 'slug', $fields ) ) { 1745 $data['slug'] = $post->post_name; 1746 } 1747 1748 if ( rest_is_field_included( 'status', $fields ) ) { 1749 $data['status'] = $post->post_status; 1750 } 1751 1752 if ( rest_is_field_included( 'type', $fields ) ) { 1753 $data['type'] = $post->post_type; 1754 } 1755 1756 if ( rest_is_field_included( 'link', $fields ) ) { 1757 $data['link'] = get_permalink( $post->ID ); 1758 } 1759 1760 if ( rest_is_field_included( 'title', $fields ) ) { 1761 $data['title'] = array(); 1762 } 1763 if ( rest_is_field_included( 'title.raw', $fields ) ) { 1764 $data['title']['raw'] = $post->post_title; 1765 } 1766 if ( rest_is_field_included( 'title.rendered', $fields ) ) { 1767 add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); 1768 1769 $data['title']['rendered'] = get_the_title( $post->ID ); 1770 1771 remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); 1772 } 1773 1774 $has_password_filter = false; 1775 1776 if ( $this->can_access_password_content( $post, $request ) ) { 1777 $this->password_check_passed[ $post->ID ] = true; 1778 // Allow access to the post, permissions already checked before. 1779 add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 ); 1780 1781 $has_password_filter = true; 1782 } 1783 1784 if ( rest_is_field_included( 'content', $fields ) ) { 1785 $data['content'] = array(); 1786 } 1787 if ( rest_is_field_included( 'content.raw', $fields ) ) { 1788 $data['content']['raw'] = $post->post_content; 1789 } 1790 if ( rest_is_field_included( 'content.rendered', $fields ) ) { 1791 /** This filter is documented in wp-includes/post-template.php */ 1792 $data['content']['rendered'] = post_password_required( $post ) ? '' : apply_filters( 'the_content', $post->post_content ); 1793 } 1794 if ( rest_is_field_included( 'content.protected', $fields ) ) { 1795 $data['content']['protected'] = (bool) $post->post_password; 1796 } 1797 if ( rest_is_field_included( 'content.block_version', $fields ) ) { 1798 $data['content']['block_version'] = block_version( $post->post_content ); 1799 } 1800 1801 if ( rest_is_field_included( 'excerpt', $fields ) ) { 1802 /** This filter is documented in wp-includes/post-template.php */ 1803 $excerpt = apply_filters( 'get_the_excerpt', $post->post_excerpt, $post ); 1804 1805 /** This filter is documented in wp-includes/post-template.php */ 1806 $excerpt = apply_filters( 'the_excerpt', $excerpt ); 1807 1808 $data['excerpt'] = array( 1809 'raw' => $post->post_excerpt, 1810 'rendered' => post_password_required( $post ) ? '' : $excerpt, 1811 'protected' => (bool) $post->post_password, 1812 ); 1813 } 1814 1815 if ( $has_password_filter ) { 1816 // Reset filter. 1817 remove_filter( 'post_password_required', array( $this, 'check_password_required' ) ); 1818 } 1819 1820 if ( rest_is_field_included( 'author', $fields ) ) { 1821 $data['author'] = (int) $post->post_author; 1822 } 1823 1824 if ( rest_is_field_included( 'featured_media', $fields ) ) { 1825 $data['featured_media'] = (int) get_post_thumbnail_id( $post->ID ); 1826 } 1827 1828 if ( rest_is_field_included( 'parent', $fields ) ) { 1829 $data['parent'] = (int) $post->post_parent; 1830 } 1831 1832 if ( rest_is_field_included( 'menu_order', $fields ) ) { 1833 $data['menu_order'] = (int) $post->menu_order; 1834 } 1835 1836 if ( rest_is_field_included( 'comment_status', $fields ) ) { 1837 $data['comment_status'] = $post->comment_status; 1838 } 1839 1840 if ( rest_is_field_included( 'ping_status', $fields ) ) { 1841 $data['ping_status'] = $post->ping_status; 1842 } 1843 1844 if ( rest_is_field_included( 'sticky', $fields ) ) { 1845 $data['sticky'] = is_sticky( $post->ID ); 1846 } 1847 1848 if ( rest_is_field_included( 'template', $fields ) ) { 1849 $template = get_page_template_slug( $post->ID ); 1850 if ( $template ) { 1851 $data['template'] = $template; 1852 } else { 1853 $data['template'] = ''; 1854 } 1855 } 1856 1857 if ( rest_is_field_included( 'format', $fields ) ) { 1858 $data['format'] = get_post_format( $post->ID ); 1859 1860 // Fill in blank post format. 1861 if ( empty( $data['format'] ) ) { 1862 $data['format'] = 'standard'; 1863 } 1864 } 1865 1866 if ( rest_is_field_included( 'meta', $fields ) ) { 1867 $data['meta'] = $this->meta->get_value( $post->ID, $request ); 1868 } 1869 1870 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); 1871 1872 foreach ( $taxonomies as $taxonomy ) { 1873 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; 1874 1875 if ( rest_is_field_included( $base, $fields ) ) { 1876 $terms = get_the_terms( $post, $taxonomy->name ); 1877 $data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array(); 1878 } 1879 } 1880 1881 $post_type_obj = get_post_type_object( $post->post_type ); 1882 if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) { 1883 $permalink_template_requested = rest_is_field_included( 'permalink_template', $fields ); 1884 $generated_slug_requested = rest_is_field_included( 'generated_slug', $fields ); 1885 1886 if ( $permalink_template_requested || $generated_slug_requested ) { 1887 if ( ! function_exists( 'get_sample_permalink' ) ) { 1888 require_once ABSPATH . 'wp-admin/includes/post.php'; 1889 } 1890 1891 $sample_permalink = get_sample_permalink( $post->ID, $post->post_title, '' ); 1892 1893 if ( $permalink_template_requested ) { 1894 $data['permalink_template'] = $sample_permalink[0]; 1895 } 1896 1897 if ( $generated_slug_requested ) { 1898 $data['generated_slug'] = $sample_permalink[1]; 1899 } 1900 } 1901 } 1902 1903 $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 1904 $data = $this->add_additional_fields_to_object( $data, $request ); 1905 $data = $this->filter_response_by_context( $data, $context ); 1906 1907 // Wrap the data in a response object. 1908 $response = rest_ensure_response( $data ); 1909 1910 $links = $this->prepare_links( $post ); 1911 $response->add_links( $links ); 1912 1913 if ( ! empty( $links['self']['href'] ) ) { 1914 $actions = $this->get_available_actions( $post, $request ); 1915 1916 $self = $links['self']['href']; 1917 1918 foreach ( $actions as $rel ) { 1919 $response->add_link( $rel, $self ); 1920 } 1921 } 1922 1923 /** 1924 * Filters the post data for a REST API response. 1925 * 1926 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. 1927 * 1928 * Possible hook names include: 1929 * 1930 * - `rest_prepare_post` 1931 * - `rest_prepare_page` 1932 * - `rest_prepare_attachment` 1933 * 1934 * @since 4.7.0 1935 * 1936 * @param WP_REST_Response $response The response object. 1937 * @param WP_Post $post Post object. 1938 * @param WP_REST_Request $request Request object. 1939 */ 1940 return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request ); 1941 } 1942 1943 /** 1944 * Overwrites the default protected title format. 1945 * 1946 * By default, WordPress will show password protected posts with a title of 1947 * "Protected: %s", as the REST API communicates the protected status of a post 1948 * in a machine readable format, we remove the "Protected: " prefix. 1949 * 1950 * @since 4.7.0 1951 * 1952 * @return string Protected title format. 1953 */ 1954 public function protected_title_format() { 1955 return '%s'; 1956 } 1957 1958 /** 1959 * Prepares links for the request. 1960 * 1961 * @since 4.7.0 1962 * 1963 * @param WP_Post $post Post object. 1964 * @return array Links for the given post. 1965 */ 1966 protected function prepare_links( $post ) { 1967 $base = sprintf( '%s/%s', $this->namespace, $this->rest_base ); 1968 1969 // Entity meta. 1970 $links = array( 1971 'self' => array( 1972 'href' => rest_url( trailingslashit( $base ) . $post->ID ), 1973 ), 1974 'collection' => array( 1975 'href' => rest_url( $base ), 1976 ), 1977 'about' => array( 1978 'href' => rest_url( 'wp/v2/types/' . $this->post_type ), 1979 ), 1980 ); 1981 1982 if ( ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'author' ) ) 1983 && ! empty( $post->post_author ) ) { 1984 $links['author'] = array( 1985 'href' => rest_url( 'wp/v2/users/' . $post->post_author ), 1986 'embeddable' => true, 1987 ); 1988 } 1989 1990 if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'comments' ) ) { 1991 $replies_url = rest_url( 'wp/v2/comments' ); 1992 $replies_url = add_query_arg( 'post', $post->ID, $replies_url ); 1993 1994 $links['replies'] = array( 1995 'href' => $replies_url, 1996 'embeddable' => true, 1997 ); 1998 } 1999 2000 if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'revisions' ) ) { 2001 $revisions = wp_get_post_revisions( $post->ID, array( 'fields' => 'ids' ) ); 2002 $revisions_count = count( $revisions ); 2003 2004 $links['version-history'] = array( 2005 'href' => rest_url( trailingslashit( $base ) . $post->ID . '/revisions' ), 2006 'count' => $revisions_count, 2007 ); 2008 2009 if ( $revisions_count > 0 ) { 2010 $last_revision = array_shift( $revisions ); 2011 2012 $links['predecessor-version'] = array( 2013 'href' => rest_url( trailingslashit( $base ) . $post->ID . '/revisions/' . $last_revision ), 2014 'id' => $last_revision, 2015 ); 2016 } 2017 } 2018 2019 $post_type_obj = get_post_type_object( $post->post_type ); 2020 2021 if ( $post_type_obj->hierarchical && ! empty( $post->post_parent ) ) { 2022 $links['up'] = array( 2023 'href' => rest_url( trailingslashit( $base ) . (int) $post->post_parent ), 2024 'embeddable' => true, 2025 ); 2026 } 2027 2028 // If we have a featured media, add that. 2029 $featured_media = get_post_thumbnail_id( $post->ID ); 2030 if ( $featured_media ) { 2031 $image_url = rest_url( 'wp/v2/media/' . $featured_media ); 2032 2033 $links['https://api.w.org/featuredmedia'] = array( 2034 'href' => $image_url, 2035 'embeddable' => true, 2036 ); 2037 } 2038 2039 if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ), true ) ) { 2040 $attachments_url = rest_url( 'wp/v2/media' ); 2041 $attachments_url = add_query_arg( 'parent', $post->ID, $attachments_url ); 2042 2043 $links['https://api.w.org/attachment'] = array( 2044 'href' => $attachments_url, 2045 ); 2046 } 2047 2048 $taxonomies = get_object_taxonomies( $post->post_type ); 2049 2050 if ( ! empty( $taxonomies ) ) { 2051 $links['https://api.w.org/term'] = array(); 2052 2053 foreach ( $taxonomies as $tax ) { 2054 $taxonomy_obj = get_taxonomy( $tax ); 2055 2056 // Skip taxonomies that are not public. 2057 if ( empty( $taxonomy_obj->show_in_rest ) ) { 2058 continue; 2059 } 2060 2061 $tax_base = ! empty( $taxonomy_obj->rest_base ) ? $taxonomy_obj->rest_base : $tax; 2062 2063 $terms_url = add_query_arg( 2064 'post', 2065 $post->ID, 2066 rest_url( 'wp/v2/' . $tax_base ) 2067 ); 2068 2069 $links['https://api.w.org/term'][] = array( 2070 'href' => $terms_url, 2071 'taxonomy' => $tax, 2072 'embeddable' => true, 2073 ); 2074 } 2075 } 2076 2077 return $links; 2078 } 2079 2080 /** 2081 * Get the link relations available for the post and current user. 2082 * 2083 * @since 4.9.8 2084 * 2085 * @param WP_Post $post Post object. 2086 * @param WP_REST_Request $request Request object. 2087 * @return array List of link relations. 2088 */ 2089 protected function get_available_actions( $post, $request ) { 2090 2091 if ( 'edit' !== $request['context'] ) { 2092 return array(); 2093 } 2094 2095 $rels = array(); 2096 2097 $post_type = get_post_type_object( $post->post_type ); 2098 2099 if ( 'attachment' !== $this->post_type && current_user_can( $post_type->cap->publish_posts ) ) { 2100 $rels[] = 'https://api.w.org/action-publish'; 2101 } 2102 2103 if ( current_user_can( 'unfiltered_html' ) ) { 2104 $rels[] = 'https://api.w.org/action-unfiltered-html'; 2105 } 2106 2107 if ( 'post' === $post_type->name ) { 2108 if ( current_user_can( $post_type->cap->edit_others_posts ) && current_user_can( $post_type->cap->publish_posts ) ) { 2109 $rels[] = 'https://api.w.org/action-sticky'; 2110 } 2111 } 2112 2113 if ( post_type_supports( $post_type->name, 'author' ) ) { 2114 if ( current_user_can( $post_type->cap->edit_others_posts ) ) { 2115 $rels[] = 'https://api.w.org/action-assign-author'; 2116 } 2117 } 2118 2119 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); 2120 2121 foreach ( $taxonomies as $tax ) { 2122 $tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name; 2123 $create_cap = is_taxonomy_hierarchical( $tax->name ) ? $tax->cap->edit_terms : $tax->cap->assign_terms; 2124 2125 if ( current_user_can( $create_cap ) ) { 2126 $rels[] = 'https://api.w.org/action-create-' . $tax_base; 2127 } 2128 2129 if ( current_user_can( $tax->cap->assign_terms ) ) { 2130 $rels[] = 'https://api.w.org/action-assign-' . $tax_base; 2131 } 2132 } 2133 2134 return $rels; 2135 } 2136 2137 /** 2138 * Retrieves the post's schema, conforming to JSON Schema. 2139 * 2140 * @since 4.7.0 2141 * 2142 * @return array Item schema data. 2143 */ 2144 public function get_item_schema() { 2145 if ( $this->schema ) { 2146 return $this->add_additional_fields_schema( $this->schema ); 2147 } 2148 2149 $schema = array( 2150 '$schema' => 'http://json-schema.org/draft-04/schema#', 2151 'title' => $this->post_type, 2152 'type' => 'object', 2153 // Base properties for every Post. 2154 'properties' => array( 2155 'date' => array( 2156 'description' => __( "The date the post was published, in the site's timezone." ), 2157 'type' => array( 'string', 'null' ), 2158 'format' => 'date-time', 2159 'context' => array( 'view', 'edit', 'embed' ), 2160 ), 2161 'date_gmt' => array( 2162 'description' => __( 'The date the post was published, as GMT.' ), 2163 'type' => array( 'string', 'null' ), 2164 'format' => 'date-time', 2165 'context' => array( 'view', 'edit' ), 2166 ), 2167 'guid' => array( 2168 'description' => __( 'The globally unique identifier for the post.' ), 2169 'type' => 'object', 2170 'context' => array( 'view', 'edit' ), 2171 'readonly' => true, 2172 'properties' => array( 2173 'raw' => array( 2174 'description' => __( 'GUID for the post, as it exists in the database.' ), 2175 'type' => 'string', 2176 'context' => array( 'edit' ), 2177 'readonly' => true, 2178 ), 2179 'rendered' => array( 2180 'description' => __( 'GUID for the post, transformed for display.' ), 2181 'type' => 'string', 2182 'context' => array( 'view', 'edit' ), 2183 'readonly' => true, 2184 ), 2185 ), 2186 ), 2187 'id' => array( 2188 'description' => __( 'Unique identifier for the post.' ), 2189 'type' => 'integer', 2190 'context' => array( 'view', 'edit', 'embed' ), 2191 'readonly' => true, 2192 ), 2193 'link' => array( 2194 'description' => __( 'URL to the post.' ), 2195 'type' => 'string', 2196 'format' => 'uri', 2197 'context' => array( 'view', 'edit', 'embed' ), 2198 'readonly' => true, 2199 ), 2200 'modified' => array( 2201 'description' => __( "The date the post was last modified, in the site's timezone." ), 2202 'type' => 'string', 2203 'format' => 'date-time', 2204 'context' => array( 'view', 'edit' ), 2205 'readonly' => true, 2206 ), 2207 'modified_gmt' => array( 2208 'description' => __( 'The date the post was last modified, as GMT.' ), 2209 'type' => 'string', 2210 'format' => 'date-time', 2211 'context' => array( 'view', 'edit' ), 2212 'readonly' => true, 2213 ), 2214 'slug' => array( 2215 'description' => __( 'An alphanumeric identifier for the post unique to its type.' ), 2216 'type' => 'string', 2217 'context' => array( 'view', 'edit', 'embed' ), 2218 'arg_options' => array( 2219 'sanitize_callback' => array( $this, 'sanitize_slug' ), 2220 ), 2221 ), 2222 'status' => array( 2223 'description' => __( 'A named status for the post.' ), 2224 'type' => 'string', 2225 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ), 2226 'context' => array( 'view', 'edit' ), 2227 'arg_options' => array( 2228 'validate_callback' => array( $this, 'check_status' ), 2229 ), 2230 ), 2231 'type' => array( 2232 'description' => __( 'Type of post.' ), 2233 'type' => 'string', 2234 'context' => array( 'view', 'edit', 'embed' ), 2235 'readonly' => true, 2236 ), 2237 'password' => array( 2238 'description' => __( 'A password to protect access to the content and excerpt.' ), 2239 'type' => 'string', 2240 'context' => array( 'edit' ), 2241 ), 2242 ), 2243 ); 2244 2245 $post_type_obj = get_post_type_object( $this->post_type ); 2246 if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) { 2247 $schema['properties']['permalink_template'] = array( 2248 'description' => __( 'Permalink template for the post.' ), 2249 'type' => 'string', 2250 'context' => array( 'edit' ), 2251 'readonly' => true, 2252 ); 2253 2254 $schema['properties']['generated_slug'] = array( 2255 'description' => __( 'Slug automatically generated from the post title.' ), 2256 'type' => 'string', 2257 'context' => array( 'edit' ), 2258 'readonly' => true, 2259 ); 2260 } 2261 2262 if ( $post_type_obj->hierarchical ) { 2263 $schema['properties']['parent'] = array( 2264 'description' => __( 'The ID for the parent of the post.' ), 2265 'type' => 'integer', 2266 'context' => array( 'view', 'edit' ), 2267 ); 2268 } 2269 2270 $post_type_attributes = array( 2271 'title', 2272 'editor', 2273 'author', 2274 'excerpt', 2275 'thumbnail', 2276 'comments', 2277 'revisions', 2278 'page-attributes', 2279 'post-formats', 2280 'custom-fields', 2281 ); 2282 $fixed_schemas = array( 2283 'post' => array( 2284 'title', 2285 'editor', 2286 'author', 2287 'excerpt', 2288 'thumbnail', 2289 'comments', 2290 'revisions', 2291 'post-formats', 2292 'custom-fields', 2293 ), 2294 'page' => array( 2295 'title', 2296 'editor', 2297 'author', 2298 'excerpt', 2299 'thumbnail', 2300 'comments', 2301 'revisions', 2302 'page-attributes', 2303 'custom-fields', 2304 ), 2305 'attachment' => array( 2306 'title', 2307 'author', 2308 'comments', 2309 'revisions', 2310 'custom-fields', 2311 ), 2312 ); 2313 2314 foreach ( $post_type_attributes as $attribute ) { 2315 if ( isset( $fixed_schemas[ $this->post_type ] ) && ! in_array( $attribute, $fixed_schemas[ $this->post_type ], true ) ) { 2316 continue; 2317 } elseif ( ! isset( $fixed_schemas[ $this->post_type ] ) && ! post_type_supports( $this->post_type, $attribute ) ) { 2318 continue; 2319 } 2320 2321 switch ( $attribute ) { 2322 2323 case 'title': 2324 $schema['properties']['title'] = array( 2325 'description' => __( 'The title for the post.' ), 2326 'type' => 'object', 2327 'context' => array( 'view', 'edit', 'embed' ), 2328 'arg_options' => array( 2329 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). 2330 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). 2331 ), 2332 'properties' => array( 2333 'raw' => array( 2334 'description' => __( 'Title for the post, as it exists in the database.' ), 2335 'type' => 'string', 2336 'context' => array( 'edit' ), 2337 ), 2338 'rendered' => array( 2339 'description' => __( 'HTML title for the post, transformed for display.' ), 2340 'type' => 'string', 2341 'context' => array( 'view', 'edit', 'embed' ), 2342 'readonly' => true, 2343 ), 2344 ), 2345 ); 2346 break; 2347 2348 case 'editor': 2349 $schema['properties']['content'] = array( 2350 'description' => __( 'The content for the post.' ), 2351 'type' => 'object', 2352 'context' => array( 'view', 'edit' ), 2353 'arg_options' => array( 2354 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). 2355 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). 2356 ), 2357 'properties' => array( 2358 'raw' => array( 2359 'description' => __( 'Content for the post, as it exists in the database.' ), 2360 'type' => 'string', 2361 'context' => array( 'edit' ), 2362 ), 2363 'rendered' => array( 2364 'description' => __( 'HTML content for the post, transformed for display.' ), 2365 'type' => 'string', 2366 'context' => array( 'view', 'edit' ), 2367 'readonly' => true, 2368 ), 2369 'block_version' => array( 2370 'description' => __( 'Version of the content block format used by the post.' ), 2371 'type' => 'integer', 2372 'context' => array( 'edit' ), 2373 'readonly' => true, 2374 ), 2375 'protected' => array( 2376 'description' => __( 'Whether the content is protected with a password.' ), 2377 'type' => 'boolean', 2378 'context' => array( 'view', 'edit', 'embed' ), 2379 'readonly' => true, 2380 ), 2381 ), 2382 ); 2383 break; 2384 2385 case 'author': 2386 $schema['properties']['author'] = array( 2387 'description' => __( 'The ID for the author of the post.' ), 2388 'type' => 'integer', 2389 'context' => array( 'view', 'edit', 'embed' ), 2390 ); 2391 break; 2392 2393 case 'excerpt': 2394 $schema['properties']['excerpt'] = array( 2395 'description' => __( 'The excerpt for the post.' ), 2396 'type' => 'object', 2397 'context' => array( 'view', 'edit', 'embed' ), 2398 'arg_options' => array( 2399 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). 2400 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). 2401 ), 2402 'properties' => array( 2403 'raw' => array( 2404 'description' => __( 'Excerpt for the post, as it exists in the database.' ), 2405 'type' => 'string', 2406 'context' => array( 'edit' ), 2407 ), 2408 'rendered' => array( 2409 'description' => __( 'HTML excerpt for the post, transformed for display.' ), 2410 'type' => 'string', 2411 'context' => array( 'view', 'edit', 'embed' ), 2412 'readonly' => true, 2413 ), 2414 'protected' => array( 2415 'description' => __( 'Whether the excerpt is protected with a password.' ), 2416 'type' => 'boolean', 2417 'context' => array( 'view', 'edit', 'embed' ), 2418 'readonly' => true, 2419 ), 2420 ), 2421 ); 2422 break; 2423 2424 case 'thumbnail': 2425 $schema['properties']['featured_media'] = array( 2426 'description' => __( 'The ID of the featured media for the post.' ), 2427 'type' => 'integer', 2428 'context' => array( 'view', 'edit', 'embed' ), 2429 ); 2430 break; 2431 2432 case 'comments': 2433 $schema['properties']['comment_status'] = array( 2434 'description' => __( 'Whether or not comments are open on the post.' ), 2435 'type' => 'string', 2436 'enum' => array( 'open', 'closed' ), 2437 'context' => array( 'view', 'edit' ), 2438 ); 2439 $schema['properties']['ping_status'] = array( 2440 'description' => __( 'Whether or not the post can be pinged.' ), 2441 'type' => 'string', 2442 'enum' => array( 'open', 'closed' ), 2443 'context' => array( 'view', 'edit' ), 2444 ); 2445 break; 2446 2447 case 'page-attributes': 2448 $schema['properties']['menu_order'] = array( 2449 'description' => __( 'The order of the post in relation to other posts.' ), 2450 'type' => 'integer', 2451 'context' => array( 'view', 'edit' ), 2452 ); 2453 break; 2454 2455 case 'post-formats': 2456 // Get the native post formats and remove the array keys. 2457 $formats = array_values( get_post_format_slugs() ); 2458 2459 $schema['properties']['format'] = array( 2460 'description' => __( 'The format for the post.' ), 2461 'type' => 'string', 2462 'enum' => $formats, 2463 'context' => array( 'view', 'edit' ), 2464 ); 2465 break; 2466 2467 case 'custom-fields': 2468 $schema['properties']['meta'] = $this->meta->get_field_schema(); 2469 break; 2470 2471 } 2472 } 2473 2474 if ( 'post' === $this->post_type ) { 2475 $schema['properties']['sticky'] = array( 2476 'description' => __( 'Whether or not the post should be treated as sticky.' ), 2477 'type' => 'boolean', 2478 'context' => array( 'view', 'edit' ), 2479 ); 2480 } 2481 2482 $schema['properties']['template'] = array( 2483 'description' => __( 'The theme file to use to display the post.' ), 2484 'type' => 'string', 2485 'context' => array( 'view', 'edit' ), 2486 'arg_options' => array( 2487 'validate_callback' => array( $this, 'check_template' ), 2488 ), 2489 ); 2490 2491 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); 2492 2493 foreach ( $taxonomies as $taxonomy ) { 2494 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; 2495 2496 if ( array_key_exists( $base, $schema['properties'] ) ) { 2497 $taxonomy_field_name_with_conflict = ! empty( $taxonomy->rest_base ) ? 'rest_base' : 'name'; 2498 _doing_it_wrong( 2499 'register_taxonomy', 2500 sprintf( 2501 /* translators: 1: The taxonomy name, 2: The property name, either 'rest_base' or 'name', 3: The conflicting value. */ 2502 __( 'The "%1$s" taxonomy "%2$s" property (%3$s) conflicts with an existing property on the REST API Posts Controller. Specify a custom "rest_base" when registering the taxonomy to avoid this error.' ), 2503 $taxonomy->name, 2504 $taxonomy_field_name_with_conflict, 2505 $base 2506 ), 2507 '5.4.0' 2508 ); 2509 } 2510 2511 $schema['properties'][ $base ] = array( 2512 /* translators: %s: Taxonomy name. */ 2513 'description' => sprintf( __( 'The terms assigned to the post in the %s taxonomy.' ), $taxonomy->name ), 2514 'type' => 'array', 2515 'items' => array( 2516 'type' => 'integer', 2517 ), 2518 'context' => array( 'view', 'edit' ), 2519 ); 2520 } 2521 2522 $schema_links = $this->get_schema_links(); 2523 2524 if ( $schema_links ) { 2525 $schema['links'] = $schema_links; 2526 } 2527 2528 // Take a snapshot of which fields are in the schema pre-filtering. 2529 $schema_fields = array_keys( $schema['properties'] ); 2530 2531 /** 2532 * Filters the post's schema. 2533 * 2534 * The dynamic portion of the filter, `$this->post_type`, refers to the 2535 * post type slug for the controller. 2536 * 2537 * @since 5.4.0 2538 * 2539 * @param array $schema Item schema data. 2540 */ 2541 $schema = apply_filters( "rest_{$this->post_type}_item_schema", $schema ); 2542 2543 // Emit a _doing_it_wrong warning if user tries to add new properties using this filter. 2544 $new_fields = array_diff( array_keys( $schema['properties'] ), $schema_fields ); 2545 if ( count( $new_fields ) > 0 ) { 2546 _doing_it_wrong( 2547 __METHOD__, 2548 sprintf( 2549 /* translators: %s: register_rest_field */ 2550 __( 'Please use %s to add new schema properties.' ), 2551 'register_rest_field' 2552 ), 2553 '5.4.0' 2554 ); 2555 } 2556 2557 $this->schema = $schema; 2558 2559 return $this->add_additional_fields_schema( $this->schema ); 2560 } 2561 2562 /** 2563 * Retrieve Link Description Objects that should be added to the Schema for the posts collection. 2564 * 2565 * @since 4.9.8 2566 * 2567 * @return array 2568 */ 2569 protected function get_schema_links() { 2570 2571 $href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" ); 2572 2573 $links = array(); 2574 2575 if ( 'attachment' !== $this->post_type ) { 2576 $links[] = array( 2577 'rel' => 'https://api.w.org/action-publish', 2578 'title' => __( 'The current user can publish this post.' ), 2579 'href' => $href, 2580 'targetSchema' => array( 2581 'type' => 'object', 2582 'properties' => array( 2583 'status' => array( 2584 'type' => 'string', 2585 'enum' => array( 'publish', 'future' ), 2586 ), 2587 ), 2588 ), 2589 ); 2590 } 2591 2592 $links[] = array( 2593 'rel' => 'https://api.w.org/action-unfiltered-html', 2594 'title' => __( 'The current user can post unfiltered HTML markup and JavaScript.' ), 2595 'href' => $href, 2596 'targetSchema' => array( 2597 'type' => 'object', 2598 'properties' => array( 2599 'content' => array( 2600 'raw' => array( 2601 'type' => 'string', 2602 ), 2603 ), 2604 ), 2605 ), 2606 ); 2607 2608 if ( 'post' === $this->post_type ) { 2609 $links[] = array( 2610 'rel' => 'https://api.w.org/action-sticky', 2611 'title' => __( 'The current user can sticky this post.' ), 2612 'href' => $href, 2613 'targetSchema' => array( 2614 'type' => 'object', 2615 'properties' => array( 2616 'sticky' => array( 2617 'type' => 'boolean', 2618 ), 2619 ), 2620 ), 2621 ); 2622 } 2623 2624 if ( post_type_supports( $this->post_type, 'author' ) ) { 2625 $links[] = array( 2626 'rel' => 'https://api.w.org/action-assign-author', 2627 'title' => __( 'The current user can change the author on this post.' ), 2628 'href' => $href, 2629 'targetSchema' => array( 2630 'type' => 'object', 2631 'properties' => array( 2632 'author' => array( 2633 'type' => 'integer', 2634 ), 2635 ), 2636 ), 2637 ); 2638 } 2639 2640 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); 2641 2642 foreach ( $taxonomies as $tax ) { 2643 $tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name; 2644 2645 /* translators: %s: Taxonomy name. */ 2646 $assign_title = sprintf( __( 'The current user can assign terms in the %s taxonomy.' ), $tax->name ); 2647 /* translators: %s: Taxonomy name. */ 2648 $create_title = sprintf( __( 'The current user can create terms in the %s taxonomy.' ), $tax->name ); 2649 2650 $links[] = array( 2651 'rel' => 'https://api.w.org/action-assign-' . $tax_base, 2652 'title' => $assign_title, 2653 'href' => $href, 2654 'targetSchema' => array( 2655 'type' => 'object', 2656 'properties' => array( 2657 $tax_base => array( 2658 'type' => 'array', 2659 'items' => array( 2660 'type' => 'integer', 2661 ), 2662 ), 2663 ), 2664 ), 2665 ); 2666 2667 $links[] = array( 2668 'rel' => 'https://api.w.org/action-create-' . $tax_base, 2669 'title' => $create_title, 2670 'href' => $href, 2671 'targetSchema' => array( 2672 'type' => 'object', 2673 'properties' => array( 2674 $tax_base => array( 2675 'type' => 'array', 2676 'items' => array( 2677 'type' => 'integer', 2678 ), 2679 ), 2680 ), 2681 ), 2682 ); 2683 } 2684 2685 return $links; 2686 } 2687 2688 /** 2689 * Retrieves the query params for the posts collection. 2690 * 2691 * @since 4.7.0 2692 * @since 5.4.0 The `tax_relation` query parameter was added. 2693 * @since 5.7.0 The `modified_after` and `modified_before` query parameters were added. 2694 * 2695 * @return array Collection parameters. 2696 */ 2697 public function get_collection_params() { 2698 $query_params = parent::get_collection_params(); 2699 2700 $query_params['context']['default'] = 'view'; 2701 2702 $query_params['after'] = array( 2703 'description' => __( 'Limit response to posts published after a given ISO8601 compliant date.' ), 2704 'type' => 'string', 2705 'format' => 'date-time', 2706 ); 2707 2708 $query_params['modified_after'] = array( 2709 'description' => __( 'Limit response to posts modified after a given ISO8601 compliant date.' ), 2710 'type' => 'string', 2711 'format' => 'date-time', 2712 ); 2713 2714 if ( post_type_supports( $this->post_type, 'author' ) ) { 2715 $query_params['author'] = array( 2716 'description' => __( 'Limit result set to posts assigned to specific authors.' ), 2717 'type' => 'array', 2718 'items' => array( 2719 'type' => 'integer', 2720 ), 2721 'default' => array(), 2722 ); 2723 $query_params['author_exclude'] = array( 2724 'description' => __( 'Ensure result set excludes posts assigned to specific authors.' ), 2725 'type' => 'array', 2726 'items' => array( 2727 'type' => 'integer', 2728 ), 2729 'default' => array(), 2730 ); 2731 } 2732 2733 $query_params['before'] = array( 2734 'description' => __( 'Limit response to posts published before a given ISO8601 compliant date.' ), 2735 'type' => 'string', 2736 'format' => 'date-time', 2737 ); 2738 2739 $query_params['modified_before'] = array( 2740 'description' => __( 'Limit response to posts modified before a given ISO8601 compliant date.' ), 2741 'type' => 'string', 2742 'format' => 'date-time', 2743 ); 2744 2745 $query_params['exclude'] = array( 2746 'description' => __( 'Ensure result set excludes specific IDs.' ), 2747 'type' => 'array', 2748 'items' => array( 2749 'type' => 'integer', 2750 ), 2751 'default' => array(), 2752 ); 2753 2754 $query_params['include'] = array( 2755 'description' => __( 'Limit result set to specific IDs.' ), 2756 'type' => 'array', 2757 'items' => array( 2758 'type' => 'integer', 2759 ), 2760 'default' => array(), 2761 ); 2762 2763 if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) { 2764 $query_params['menu_order'] = array( 2765 'description' => __( 'Limit result set to posts with a specific menu_order value.' ), 2766 'type' => 'integer', 2767 ); 2768 } 2769 2770 $query_params['offset'] = array( 2771 'description' => __( 'Offset the result set by a specific number of items.' ), 2772 'type' => 'integer', 2773 ); 2774 2775 $query_params['order'] = array( 2776 'description' => __( 'Order sort attribute ascending or descending.' ), 2777 'type' => 'string', 2778 'default' => 'desc', 2779 'enum' => array( 'asc', 'desc' ), 2780 ); 2781 2782 $query_params['orderby'] = array( 2783 'description' => __( 'Sort collection by post attribute.' ), 2784 'type' => 'string', 2785 'default' => 'date', 2786 'enum' => array( 2787 'author', 2788 'date', 2789 'id', 2790 'include', 2791 'modified', 2792 'parent', 2793 'relevance', 2794 'slug', 2795 'include_slugs', 2796 'title', 2797 ), 2798 ); 2799 2800 if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) { 2801 $query_params['orderby']['enum'][] = 'menu_order'; 2802 } 2803 2804 $post_type = get_post_type_object( $this->post_type ); 2805 2806 if ( $post_type->hierarchical || 'attachment' === $this->post_type ) { 2807 $query_params['parent'] = array( 2808 'description' => __( 'Limit result set to items with particular parent IDs.' ), 2809 'type' => 'array', 2810 'items' => array( 2811 'type' => 'integer', 2812 ), 2813 'default' => array(), 2814 ); 2815 $query_params['parent_exclude'] = array( 2816 'description' => __( 'Limit result set to all items except those of a particular parent ID.' ), 2817 'type' => 'array', 2818 'items' => array( 2819 'type' => 'integer', 2820 ), 2821 'default' => array(), 2822 ); 2823 } 2824 2825 $query_params['slug'] = array( 2826 'description' => __( 'Limit result set to posts with one or more specific slugs.' ), 2827 'type' => 'array', 2828 'items' => array( 2829 'type' => 'string', 2830 ), 2831 'sanitize_callback' => 'wp_parse_slug_list', 2832 ); 2833 2834 $query_params['status'] = array( 2835 'default' => 'publish', 2836 'description' => __( 'Limit result set to posts assigned one or more statuses.' ), 2837 'type' => 'array', 2838 'items' => array( 2839 'enum' => array_merge( array_keys( get_post_stati() ), array( 'any' ) ), 2840 'type' => 'string', 2841 ), 2842 'sanitize_callback' => array( $this, 'sanitize_post_statuses' ), 2843 ); 2844 2845 $query_params = $this->prepare_taxonomy_limit_schema( $query_params ); 2846 2847 if ( 'post' === $this->post_type ) { 2848 $query_params['sticky'] = array( 2849 'description' => __( 'Limit result set to items that are sticky.' ), 2850 'type' => 'boolean', 2851 ); 2852 } 2853 2854 /** 2855 * Filters collection parameters for the posts controller. 2856 * 2857 * The dynamic part of the filter `$this->post_type` refers to the post 2858 * type slug for the controller. 2859 * 2860 * This filter registers the collection parameter, but does not map the 2861 * collection parameter to an internal WP_Query parameter. Use the 2862 * `rest_{$this->post_type}_query` filter to set WP_Query parameters. 2863 * 2864 * @since 4.7.0 2865 * 2866 * @param array $query_params JSON Schema-formatted collection parameters. 2867 * @param WP_Post_Type $post_type Post type object. 2868 */ 2869 return apply_filters( "rest_{$this->post_type}_collection_params", $query_params, $post_type ); 2870 } 2871 2872 /** 2873 * Sanitizes and validates the list of post statuses, including whether the 2874 * user can query private statuses. 2875 * 2876 * @since 4.7.0 2877 * 2878 * @param string|array $statuses One or more post statuses. 2879 * @param WP_REST_Request $request Full details about the request. 2880 * @param string $parameter Additional parameter to pass to validation. 2881 * @return array|WP_Error A list of valid statuses, otherwise WP_Error object. 2882 */ 2883 public function sanitize_post_statuses( $statuses, $request, $parameter ) { 2884 $statuses = wp_parse_slug_list( $statuses ); 2885 2886 // The default status is different in WP_REST_Attachments_Controller. 2887 $attributes = $request->get_attributes(); 2888 $default_status = $attributes['args']['status']['default']; 2889 2890 foreach ( $statuses as $status ) { 2891 if ( $status === $default_status ) { 2892 continue; 2893 } 2894 2895 $post_type_obj = get_post_type_object( $this->post_type ); 2896 2897 if ( current_user_can( $post_type_obj->cap->edit_posts ) || 'private' === $status && current_user_can( $post_type_obj->cap->read_private_posts ) ) { 2898 $result = rest_validate_request_arg( $status, $request, $parameter ); 2899 if ( is_wp_error( $result ) ) { 2900 return $result; 2901 } 2902 } else { 2903 return new WP_Error( 2904 'rest_forbidden_status', 2905 __( 'Status is forbidden.' ), 2906 array( 'status' => rest_authorization_required_code() ) 2907 ); 2908 } 2909 } 2910 2911 return $statuses; 2912 } 2913 2914 /** 2915 * Prepares the 'tax_query' for a collection of posts. 2916 * 2917 * @since 5.7.0 2918 * 2919 * @param array $args WP_Query arguments. 2920 * @param WP_REST_Request $request Full details about the request. 2921 * @return array Updated query arguments. 2922 */ 2923 private function prepare_tax_query( array $args, WP_REST_Request $request ) { 2924 $relation = $request['tax_relation']; 2925 2926 if ( $relation ) { 2927 $args['tax_query'] = array( 'relation' => $relation ); 2928 } 2929 2930 $taxonomies = wp_list_filter( 2931 get_object_taxonomies( $this->post_type, 'objects' ), 2932 array( 'show_in_rest' => true ) 2933 ); 2934 2935 foreach ( $taxonomies as $taxonomy ) { 2936 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; 2937 2938 $tax_include = $request[ $base ]; 2939 $tax_exclude = $request[ $base . '_exclude' ]; 2940 2941 if ( $tax_include ) { 2942 $terms = array(); 2943 $include_children = false; 2944 $operator = 'IN'; 2945 2946 if ( rest_is_array( $tax_include ) ) { 2947 $terms = $tax_include; 2948 } elseif ( rest_is_object( $tax_include ) ) { 2949 $terms = empty( $tax_include['terms'] ) ? array() : $tax_include['terms']; 2950 $include_children = ! empty( $tax_include['include_children'] ); 2951 2952 if ( isset( $tax_include['operator'] ) && 'AND' === $tax_include['operator'] ) { 2953 $operator = 'AND'; 2954 } 2955 } 2956 2957 if ( $terms ) { 2958 $args['tax_query'][] = array( 2959 'taxonomy' => $taxonomy->name, 2960 'field' => 'term_id', 2961 'terms' => $terms, 2962 'include_children' => $include_children, 2963 'operator' => $operator, 2964 ); 2965 } 2966 } 2967 2968 if ( $tax_exclude ) { 2969 $terms = array(); 2970 $include_children = false; 2971 2972 if ( rest_is_array( $tax_exclude ) ) { 2973 $terms = $tax_exclude; 2974 } elseif ( rest_is_object( $tax_exclude ) ) { 2975 $terms = empty( $tax_exclude['terms'] ) ? array() : $tax_exclude['terms']; 2976 $include_children = ! empty( $tax_exclude['include_children'] ); 2977 } 2978 2979 if ( $terms ) { 2980 $args['tax_query'][] = array( 2981 'taxonomy' => $taxonomy->name, 2982 'field' => 'term_id', 2983 'terms' => $terms, 2984 'include_children' => $include_children, 2985 'operator' => 'NOT IN', 2986 ); 2987 } 2988 } 2989 } 2990 2991 return $args; 2992 } 2993 2994 /** 2995 * Prepares the collection schema for including and excluding items by terms. 2996 * 2997 * @since 5.7.0 2998 * 2999 * @param array $query_params Collection schema. 3000 * @return array Updated schema. 3001 */ 3002 private function prepare_taxonomy_limit_schema( array $query_params ) { 3003 $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); 3004 3005 if ( ! $taxonomies ) { 3006 return $query_params; 3007 } 3008 3009 $query_params['tax_relation'] = array( 3010 'description' => __( 'Limit result set based on relationship between multiple taxonomies.' ), 3011 'type' => 'string', 3012 'enum' => array( 'AND', 'OR' ), 3013 ); 3014 3015 $limit_schema = array( 3016 'type' => array( 'object', 'array' ), 3017 'oneOf' => array( 3018 array( 3019 'title' => __( 'Term ID List' ), 3020 'description' => __( 'Match terms with the listed IDs.' ), 3021 'type' => 'array', 3022 'items' => array( 3023 'type' => 'integer', 3024 ), 3025 ), 3026 array( 3027 'title' => __( 'Term ID Taxonomy Query' ), 3028 'description' => __( 'Perform an advanced term query.' ), 3029 'type' => 'object', 3030 'properties' => array( 3031 'terms' => array( 3032 'description' => __( 'Term IDs.' ), 3033 'type' => 'array', 3034 'items' => array( 3035 'type' => 'integer', 3036 ), 3037 'default' => array(), 3038 ), 3039 'include_children' => array( 3040 'description' => __( 'Whether to include child terms in the terms limiting the result set.' ), 3041 'type' => 'boolean', 3042 'default' => false, 3043 ), 3044 ), 3045 'additionalProperties' => false, 3046 ), 3047 ), 3048 ); 3049 3050 $include_schema = array_merge( 3051 array( 3052 /* translators: %s: Taxonomy name. */ 3053 'description' => __( 'Limit result set to items with specific terms assigned in the %s taxonomy.' ), 3054 ), 3055 $limit_schema 3056 ); 3057 // 'operator' is supported only for 'include' queries. 3058 $include_schema['oneOf'][1]['properties']['operator'] = array( 3059 'description' => __( 'Whether items must be assigned all or any of the specified terms.' ), 3060 'type' => 'string', 3061 'enum' => array( 'AND', 'OR' ), 3062 'default' => 'OR', 3063 ); 3064 3065 $exclude_schema = array_merge( 3066 array( 3067 /* translators: %s: Taxonomy name. */ 3068 'description' => __( 'Limit result set to items except those with specific terms assigned in the %s taxonomy.' ), 3069 ), 3070 $limit_schema 3071 ); 3072 3073 foreach ( $taxonomies as $taxonomy ) { 3074 $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; 3075 $base_exclude = $base . '_exclude'; 3076 3077 $query_params[ $base ] = $include_schema; 3078 $query_params[ $base ]['description'] = sprintf( $query_params[ $base ]['description'], $base ); 3079 3080 $query_params[ $base_exclude ] = $exclude_schema; 3081 $query_params[ $base_exclude ]['description'] = sprintf( $query_params[ $base_exclude ]['description'], $base ); 3082 3083 if ( ! $taxonomy->hierarchical ) { 3084 unset( $query_params[ $base ]['oneOf'][1]['properties']['include_children'] ); 3085 unset( $query_params[ $base_exclude ]['oneOf'][1]['properties']['include_children'] ); 3086 } 3087 } 3088 3089 return $query_params; 3090 } 3091 }