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


Generated : Fri Oct 10 08:20:03 2025 Cross-referenced by PHPXref