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