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