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