[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/rest-api/endpoints/ -> class-wp-rest-posts-controller.php (source)

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


Generated : Wed Nov 4 08:20:38 2026 Cross-referenced by PHPXref