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