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