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