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