wp/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php
changeset 18 be944660c56a
parent 16 a86126ab1dd4
child 19 3d72ae0968f4
equal deleted inserted replaced
17:34716fd837a4 18:be944660c56a
    30 	 * @var WP_REST_Post_Meta_Fields
    30 	 * @var WP_REST_Post_Meta_Fields
    31 	 */
    31 	 */
    32 	protected $meta;
    32 	protected $meta;
    33 
    33 
    34 	/**
    34 	/**
       
    35 	 * Passwordless post access permitted.
       
    36 	 *
       
    37 	 * @since 5.7.1
       
    38 	 * @var int[]
       
    39 	 */
       
    40 	protected $password_check_passed = array();
       
    41 
       
    42 	/**
    35 	 * Constructor.
    43 	 * Constructor.
    36 	 *
    44 	 *
    37 	 * @since 4.7.0
    45 	 * @since 4.7.0
    38 	 *
    46 	 *
    39 	 * @param string $post_type Post type.
    47 	 * @param string $post_type Post type.
    46 
    54 
    47 		$this->meta = new WP_REST_Post_Meta_Fields( $this->post_type );
    55 		$this->meta = new WP_REST_Post_Meta_Fields( $this->post_type );
    48 	}
    56 	}
    49 
    57 
    50 	/**
    58 	/**
    51 	 * Registers the routes for the objects of the controller.
    59 	 * Registers the routes for posts.
    52 	 *
    60 	 *
    53 	 * @since 4.7.0
    61 	 * @since 4.7.0
    54 	 *
    62 	 *
    55 	 * @see register_rest_route()
    63 	 * @see register_rest_route()
    56 	 */
    64 	 */
    90 			$this->namespace,
    98 			$this->namespace,
    91 			'/' . $this->rest_base . '/(?P<id>[\d]+)',
    99 			'/' . $this->rest_base . '/(?P<id>[\d]+)',
    92 			array(
   100 			array(
    93 				'args'   => array(
   101 				'args'   => array(
    94 					'id' => array(
   102 					'id' => array(
    95 						'description' => __( 'Unique identifier for the object.' ),
   103 						'description' => __( 'Unique identifier for the post.' ),
    96 						'type'        => 'integer',
   104 						'type'        => 'integer',
    97 					),
   105 					),
    98 				),
   106 				),
    99 				array(
   107 				array(
   100 					'methods'             => WP_REST_Server::READABLE,
   108 					'methods'             => WP_REST_Server::READABLE,
   147 
   155 
   148 		return true;
   156 		return true;
   149 	}
   157 	}
   150 
   158 
   151 	/**
   159 	/**
       
   160 	 * Override the result of the post password check for REST requested posts.
       
   161 	 *
       
   162 	 * Allow users to read the content of password protected posts if they have
       
   163 	 * previously passed a permission check or if they have the `edit_post` capability
       
   164 	 * for the post being checked.
       
   165 	 *
       
   166 	 * @since 5.7.1
       
   167 	 *
       
   168 	 * @param bool    $required Whether the post requires a password check.
       
   169 	 * @param WP_Post $post     The post been password checked.
       
   170 	 * @return bool Result of password check taking in to account REST API considerations.
       
   171 	 */
       
   172 	public function check_password_required( $required, $post ) {
       
   173 		if ( ! $required ) {
       
   174 			return $required;
       
   175 		}
       
   176 
       
   177 		$post = get_post( $post );
       
   178 
       
   179 		if ( ! $post ) {
       
   180 			return $required;
       
   181 		}
       
   182 
       
   183 		if ( ! empty( $this->password_check_passed[ $post->ID ] ) ) {
       
   184 			// Password previously checked and approved.
       
   185 			return false;
       
   186 		}
       
   187 
       
   188 		return ! current_user_can( 'edit_post', $post->ID );
       
   189 	}
       
   190 
       
   191 	/**
   152 	 * Retrieves a collection of posts.
   192 	 * Retrieves a collection of posts.
   153 	 *
   193 	 *
   154 	 * @since 4.7.0
   194 	 * @since 4.7.0
   155 	 *
   195 	 *
   156 	 * @param WP_REST_Request $request Full details about the request.
   196 	 * @param WP_REST_Request $request Full details about the request.
   214 		}
   254 		}
   215 
   255 
   216 		// Check for & assign any parameters which require special handling or setting.
   256 		// Check for & assign any parameters which require special handling or setting.
   217 		$args['date_query'] = array();
   257 		$args['date_query'] = array();
   218 
   258 
   219 		// Set before into date query. Date query must be specified as an array of an array.
       
   220 		if ( isset( $registered['before'], $request['before'] ) ) {
   259 		if ( isset( $registered['before'], $request['before'] ) ) {
   221 			$args['date_query'][0]['before'] = $request['before'];
   260 			$args['date_query'][] = array(
   222 		}
   261 				'before' => $request['before'],
   223 
   262 				'column' => 'post_date',
   224 		// Set after into date query. Date query must be specified as an array of an array.
   263 			);
       
   264 		}
       
   265 
       
   266 		if ( isset( $registered['modified_before'], $request['modified_before'] ) ) {
       
   267 			$args['date_query'][] = array(
       
   268 				'before' => $request['modified_before'],
       
   269 				'column' => 'post_modified',
       
   270 			);
       
   271 		}
       
   272 
   225 		if ( isset( $registered['after'], $request['after'] ) ) {
   273 		if ( isset( $registered['after'], $request['after'] ) ) {
   226 			$args['date_query'][0]['after'] = $request['after'];
   274 			$args['date_query'][] = array(
       
   275 				'after'  => $request['after'],
       
   276 				'column' => 'post_date',
       
   277 			);
       
   278 		}
       
   279 
       
   280 		if ( isset( $registered['modified_after'], $request['modified_after'] ) ) {
       
   281 			$args['date_query'][] = array(
       
   282 				'after'  => $request['modified_after'],
       
   283 				'column' => 'post_modified',
       
   284 			);
   227 		}
   285 		}
   228 
   286 
   229 		// Ensure our per_page parameter overrides any provided posts_per_page filter.
   287 		// Ensure our per_page parameter overrides any provided posts_per_page filter.
   230 		if ( isset( $registered['per_page'] ) ) {
   288 		if ( isset( $registered['per_page'] ) ) {
   231 			$args['posts_per_page'] = $request['per_page'];
   289 			$args['posts_per_page'] = $request['per_page'];
   260 				 */
   318 				 */
   261 				$args['post__not_in'] = array_merge( $args['post__not_in'], $sticky_posts );
   319 				$args['post__not_in'] = array_merge( $args['post__not_in'], $sticky_posts );
   262 			}
   320 			}
   263 		}
   321 		}
   264 
   322 
       
   323 		$args = $this->prepare_tax_query( $args, $request );
       
   324 
   265 		// Force the post_type argument, since it's not a user input variable.
   325 		// Force the post_type argument, since it's not a user input variable.
   266 		$args['post_type'] = $this->post_type;
   326 		$args['post_type'] = $this->post_type;
   267 
   327 
   268 		/**
   328 		/**
   269 		 * Filters the query arguments for a request.
   329 		 * Filters WP_Query arguments when querying posts via the REST API.
       
   330 		 *
       
   331 		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
       
   332 		 *
       
   333 		 * Possible hook names include:
       
   334 		 *
       
   335 		 *  - `rest_post_query`
       
   336 		 *  - `rest_page_query`
       
   337 		 *  - `rest_attachment_query`
   270 		 *
   338 		 *
   271 		 * Enables adding extra arguments or setting defaults for a post collection request.
   339 		 * Enables adding extra arguments or setting defaults for a post collection request.
   272 		 *
   340 		 *
   273 		 * @since 4.7.0
   341 		 * @since 4.7.0
       
   342 		 * @since 5.7.0 Moved after the `tax_query` query arg is generated.
   274 		 *
   343 		 *
   275 		 * @link https://developer.wordpress.org/reference/classes/wp_query/
   344 		 * @link https://developer.wordpress.org/reference/classes/wp_query/
   276 		 *
   345 		 *
   277 		 * @param array           $args    Key value array of query var to query value.
   346 		 * @param array           $args    Array of arguments for WP_Query.
   278 		 * @param WP_REST_Request $request The request used.
   347 		 * @param WP_REST_Request $request The REST API request.
   279 		 */
   348 		 */
   280 		$args       = apply_filters( "rest_{$this->post_type}_query", $args, $request );
   349 		$args       = apply_filters( "rest_{$this->post_type}_query", $args, $request );
   281 		$query_args = $this->prepare_items_query( $args, $request );
   350 		$query_args = $this->prepare_items_query( $args, $request );
   282 
   351 
   283 		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
       
   284 
       
   285 		if ( ! empty( $request['tax_relation'] ) ) {
       
   286 			$query_args['tax_query'] = array( 'relation' => $request['tax_relation'] );
       
   287 		}
       
   288 
       
   289 		foreach ( $taxonomies as $taxonomy ) {
       
   290 			$base        = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
       
   291 			$tax_exclude = $base . '_exclude';
       
   292 
       
   293 			if ( ! empty( $request[ $base ] ) ) {
       
   294 				$query_args['tax_query'][] = array(
       
   295 					'taxonomy'         => $taxonomy->name,
       
   296 					'field'            => 'term_id',
       
   297 					'terms'            => $request[ $base ],
       
   298 					'include_children' => false,
       
   299 				);
       
   300 			}
       
   301 
       
   302 			if ( ! empty( $request[ $tax_exclude ] ) ) {
       
   303 				$query_args['tax_query'][] = array(
       
   304 					'taxonomy'         => $taxonomy->name,
       
   305 					'field'            => 'term_id',
       
   306 					'terms'            => $request[ $tax_exclude ],
       
   307 					'include_children' => false,
       
   308 					'operator'         => 'NOT IN',
       
   309 				);
       
   310 			}
       
   311 		}
       
   312 
       
   313 		$posts_query  = new WP_Query();
   352 		$posts_query  = new WP_Query();
   314 		$query_result = $posts_query->query( $query_args );
   353 		$query_result = $posts_query->query( $query_args );
   315 
   354 
   316 		// Allow access to all password protected posts if the context is edit.
   355 		// Allow access to all password protected posts if the context is edit.
   317 		if ( 'edit' === $request['context'] ) {
   356 		if ( 'edit' === $request['context'] ) {
   318 			add_filter( 'post_password_required', '__return_false' );
   357 			add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
   319 		}
   358 		}
   320 
   359 
   321 		$posts = array();
   360 		$posts = array();
   322 
   361 
   323 		foreach ( $query_result as $post ) {
   362 		foreach ( $query_result as $post ) {
   329 			$posts[] = $this->prepare_response_for_collection( $data );
   368 			$posts[] = $this->prepare_response_for_collection( $data );
   330 		}
   369 		}
   331 
   370 
   332 		// Reset filter.
   371 		// Reset filter.
   333 		if ( 'edit' === $request['context'] ) {
   372 		if ( 'edit' === $request['context'] ) {
   334 			remove_filter( 'post_password_required', '__return_false' );
   373 			remove_filter( 'post_password_required', array( $this, 'check_password_required' ) );
   335 		}
   374 		}
   336 
   375 
   337 		$page        = (int) $query_args['paged'];
   376 		$page        = (int) $query_args['paged'];
   338 		$total_posts = $posts_query->found_posts;
   377 		$total_posts = $posts_query->found_posts;
   339 
   378 
   415 	 * Checks if a given request has access to read a post.
   454 	 * Checks if a given request has access to read a post.
   416 	 *
   455 	 *
   417 	 * @since 4.7.0
   456 	 * @since 4.7.0
   418 	 *
   457 	 *
   419 	 * @param WP_REST_Request $request Full details about the request.
   458 	 * @param WP_REST_Request $request Full details about the request.
   420 	 * @return bool|WP_Error True if the request has read access for the item, WP_Error object otherwise.
   459 	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
   421 	 */
   460 	 */
   422 	public function get_item_permissions_check( $request ) {
   461 	public function get_item_permissions_check( $request ) {
   423 		$post = $this->get_post( $request['id'] );
   462 		$post = $this->get_post( $request['id'] );
   424 		if ( is_wp_error( $post ) ) {
   463 		if ( is_wp_error( $post ) ) {
   425 			return $post;
   464 			return $post;
   444 			}
   483 			}
   445 		}
   484 		}
   446 
   485 
   447 		// Allow access to all password protected posts if the context is edit.
   486 		// Allow access to all password protected posts if the context is edit.
   448 		if ( 'edit' === $request['context'] ) {
   487 		if ( 'edit' === $request['context'] ) {
   449 			add_filter( 'post_password_required', '__return_false' );
   488 			add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
   450 		}
   489 		}
   451 
   490 
   452 		if ( $post ) {
   491 		if ( $post ) {
   453 			return $this->check_read_permission( $post );
   492 			return $this->check_read_permission( $post );
   454 		}
   493 		}
   472 		if ( empty( $post->post_password ) ) {
   511 		if ( empty( $post->post_password ) ) {
   473 			// No filter required.
   512 			// No filter required.
   474 			return false;
   513 			return false;
   475 		}
   514 		}
   476 
   515 
   477 		// Edit context always gets access to password-protected posts.
   516 		/*
   478 		if ( 'edit' === $request['context'] ) {
   517 		 * Users always gets access to password protected content in the edit
       
   518 		 * context if they have the `edit_post` meta capability.
       
   519 		 */
       
   520 		if (
       
   521 			'edit' === $request['context'] &&
       
   522 			current_user_can( 'edit_post', $post->ID )
       
   523 		) {
   479 			return true;
   524 			return true;
   480 		}
   525 		}
   481 
   526 
   482 		// No password, no auth.
   527 		// No password, no auth.
   483 		if ( empty( $request['password'] ) ) {
   528 		if ( empty( $request['password'] ) ) {
   589 			return $prepared_post;
   634 			return $prepared_post;
   590 		}
   635 		}
   591 
   636 
   592 		$prepared_post->post_type = $this->post_type;
   637 		$prepared_post->post_type = $this->post_type;
   593 
   638 
   594 		$post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true );
   639 		$post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true, false );
   595 
   640 
   596 		if ( is_wp_error( $post_id ) ) {
   641 		if ( is_wp_error( $post_id ) ) {
   597 
   642 
   598 			if ( 'db_insert_error' === $post_id->get_error_code() ) {
   643 			if ( 'db_insert_error' === $post_id->get_error_code() ) {
   599 				$post_id->add_data( array( 'status' => 500 ) );
   644 				$post_id->add_data( array( 'status' => 500 ) );
   608 
   653 
   609 		/**
   654 		/**
   610 		 * Fires after a single post is created or updated via the REST API.
   655 		 * Fires after a single post is created or updated via the REST API.
   611 		 *
   656 		 *
   612 		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
   657 		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
       
   658 		 *
       
   659 		 * Possible hook names include:
       
   660 		 *
       
   661 		 *  - `rest_insert_post`
       
   662 		 *  - `rest_insert_page`
       
   663 		 *  - `rest_insert_attachment`
   613 		 *
   664 		 *
   614 		 * @since 4.7.0
   665 		 * @since 4.7.0
   615 		 *
   666 		 *
   616 		 * @param WP_Post         $post     Inserted or updated post object.
   667 		 * @param WP_Post         $post     Inserted or updated post object.
   617 		 * @param WP_REST_Request $request  Request object.
   668 		 * @param WP_REST_Request $request  Request object.
   667 		/**
   718 		/**
   668 		 * Fires after a single post is completely created or updated via the REST API.
   719 		 * Fires after a single post is completely created or updated via the REST API.
   669 		 *
   720 		 *
   670 		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
   721 		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
   671 		 *
   722 		 *
       
   723 		 * Possible hook names include:
       
   724 		 *
       
   725 		 *  - `rest_after_insert_post`
       
   726 		 *  - `rest_after_insert_page`
       
   727 		 *  - `rest_after_insert_attachment`
       
   728 		 *
   672 		 * @since 5.0.0
   729 		 * @since 5.0.0
   673 		 *
   730 		 *
   674 		 * @param WP_Post         $post     Inserted or updated post object.
   731 		 * @param WP_Post         $post     Inserted or updated post object.
   675 		 * @param WP_REST_Request $request  Request object.
   732 		 * @param WP_REST_Request $request  Request object.
   676 		 * @param bool            $creating True when creating a post, false when updating.
   733 		 * @param bool            $creating True when creating a post, false when updating.
   677 		 */
   734 		 */
   678 		do_action( "rest_after_insert_{$this->post_type}", $post, $request, true );
   735 		do_action( "rest_after_insert_{$this->post_type}", $post, $request, true );
   679 
   736 
       
   737 		wp_after_insert_post( $post, false, null );
       
   738 
   680 		$response = $this->prepare_item_for_response( $post, $request );
   739 		$response = $this->prepare_item_for_response( $post, $request );
   681 		$response = rest_ensure_response( $response );
   740 		$response = rest_ensure_response( $response );
   682 
   741 
   683 		$response->set_status( 201 );
   742 		$response->set_status( 201 );
   684 		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) );
   743 		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) );
   749 		$valid_check = $this->get_post( $request['id'] );
   808 		$valid_check = $this->get_post( $request['id'] );
   750 		if ( is_wp_error( $valid_check ) ) {
   809 		if ( is_wp_error( $valid_check ) ) {
   751 			return $valid_check;
   810 			return $valid_check;
   752 		}
   811 		}
   753 
   812 
   754 		$post = $this->prepare_item_for_database( $request );
   813 		$post_before = get_post( $request['id'] );
       
   814 		$post        = $this->prepare_item_for_database( $request );
   755 
   815 
   756 		if ( is_wp_error( $post ) ) {
   816 		if ( is_wp_error( $post ) ) {
   757 			return $post;
   817 			return $post;
   758 		}
   818 		}
   759 
   819 
   760 		// Convert the post object to an array, otherwise wp_update_post() will expect non-escaped input.
   820 		// Convert the post object to an array, otherwise wp_update_post() will expect non-escaped input.
   761 		$post_id = wp_update_post( wp_slash( (array) $post ), true );
   821 		$post_id = wp_update_post( wp_slash( (array) $post ), true, false );
   762 
   822 
   763 		if ( is_wp_error( $post_id ) ) {
   823 		if ( is_wp_error( $post_id ) ) {
   764 			if ( 'db_update_error' === $post_id->get_error_code() ) {
   824 			if ( 'db_update_error' === $post_id->get_error_code() ) {
   765 				$post_id->add_data( array( 'status' => 500 ) );
   825 				$post_id->add_data( array( 'status' => 500 ) );
   766 			} else {
   826 			} else {
   826 		}
   886 		}
   827 
   887 
   828 		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
   888 		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
   829 		do_action( "rest_after_insert_{$this->post_type}", $post, $request, false );
   889 		do_action( "rest_after_insert_{$this->post_type}", $post, $request, false );
   830 
   890 
       
   891 		wp_after_insert_post( $post, true, $post_before );
       
   892 
   831 		$response = $this->prepare_item_for_response( $post, $request );
   893 		$response = $this->prepare_item_for_response( $post, $request );
   832 
   894 
   833 		return rest_ensure_response( $response );
   895 		return rest_ensure_response( $response );
   834 	}
   896 	}
   835 
   897 
   883 
   945 
   884 		/**
   946 		/**
   885 		 * Filters whether a post is trashable.
   947 		 * Filters whether a post is trashable.
   886 		 *
   948 		 *
   887 		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
   949 		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
       
   950 		 *
       
   951 		 * Possible hook names include:
       
   952 		 *
       
   953 		 *  - `rest_post_trashable`
       
   954 		 *  - `rest_page_trashable`
       
   955 		 *  - `rest_attachment_trashable`
   888 		 *
   956 		 *
   889 		 * Pass false to disable Trash support for the post.
   957 		 * Pass false to disable Trash support for the post.
   890 		 *
   958 		 *
   891 		 * @since 4.7.0
   959 		 * @since 4.7.0
   892 		 *
   960 		 *
  1046 	 *
  1114 	 *
  1047 	 * @param WP_REST_Request $request Request object.
  1115 	 * @param WP_REST_Request $request Request object.
  1048 	 * @return stdClass|WP_Error Post object or WP_Error.
  1116 	 * @return stdClass|WP_Error Post object or WP_Error.
  1049 	 */
  1117 	 */
  1050 	protected function prepare_item_for_database( $request ) {
  1118 	protected function prepare_item_for_database( $request ) {
  1051 		$prepared_post = new stdClass();
  1119 		$prepared_post  = new stdClass();
       
  1120 		$current_status = '';
  1052 
  1121 
  1053 		// Post ID.
  1122 		// Post ID.
  1054 		if ( isset( $request['id'] ) ) {
  1123 		if ( isset( $request['id'] ) ) {
  1055 			$existing_post = $this->get_post( $request['id'] );
  1124 			$existing_post = $this->get_post( $request['id'] );
  1056 			if ( is_wp_error( $existing_post ) ) {
  1125 			if ( is_wp_error( $existing_post ) ) {
  1057 				return $existing_post;
  1126 				return $existing_post;
  1058 			}
  1127 			}
  1059 
  1128 
  1060 			$prepared_post->ID = $existing_post->ID;
  1129 			$prepared_post->ID = $existing_post->ID;
       
  1130 			$current_status    = $existing_post->post_status;
  1061 		}
  1131 		}
  1062 
  1132 
  1063 		$schema = $this->get_item_schema();
  1133 		$schema = $this->get_item_schema();
  1064 
  1134 
  1065 		// Post title.
  1135 		// Post title.
  1099 		}
  1169 		}
  1100 
  1170 
  1101 		$post_type = get_post_type_object( $prepared_post->post_type );
  1171 		$post_type = get_post_type_object( $prepared_post->post_type );
  1102 
  1172 
  1103 		// Post status.
  1173 		// Post status.
  1104 		if ( ! empty( $schema['properties']['status'] ) && isset( $request['status'] ) ) {
  1174 		if (
       
  1175 			! empty( $schema['properties']['status'] ) &&
       
  1176 			isset( $request['status'] ) &&
       
  1177 			( ! $current_status || $current_status !== $request['status'] )
       
  1178 		) {
  1105 			$status = $this->handle_status_param( $request['status'], $post_type );
  1179 			$status = $this->handle_status_param( $request['status'], $post_type );
  1106 
  1180 
  1107 			if ( is_wp_error( $status ) ) {
  1181 			if ( is_wp_error( $status ) ) {
  1108 				return $status;
  1182 				return $status;
  1109 			}
  1183 			}
  1239 		/**
  1313 		/**
  1240 		 * Filters a post before it is inserted via the REST API.
  1314 		 * Filters a post before it is inserted via the REST API.
  1241 		 *
  1315 		 *
  1242 		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
  1316 		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
  1243 		 *
  1317 		 *
       
  1318 		 * Possible hook names include:
       
  1319 		 *
       
  1320 		 *  - `rest_pre_insert_post`
       
  1321 		 *  - `rest_pre_insert_page`
       
  1322 		 *  - `rest_pre_insert_attachment`
       
  1323 		 *
  1244 		 * @since 4.7.0
  1324 		 * @since 4.7.0
  1245 		 *
  1325 		 *
  1246 		 * @param stdClass        $prepared_post An object representing a single post prepared
  1326 		 * @param stdClass        $prepared_post An object representing a single post prepared
  1247 		 *                                       for inserting or updating the database.
  1327 		 *                                       for inserting or updating the database.
  1248 		 * @param WP_REST_Request $request       Request object.
  1328 		 * @param WP_REST_Request $request       Request object.
  1249 		 */
  1329 		 */
  1250 		return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request );
  1330 		return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request );
  1251 
  1331 
       
  1332 	}
       
  1333 
       
  1334 	/**
       
  1335 	 * Checks whether the status is valid for the given post.
       
  1336 	 *
       
  1337 	 * Allows for sending an update request with the current status, even if that status would not be acceptable.
       
  1338 	 *
       
  1339 	 * @since 5.6.0
       
  1340 	 *
       
  1341 	 * @param string          $status  The provided status.
       
  1342 	 * @param WP_REST_Request $request The request object.
       
  1343 	 * @param string          $param   The parameter name.
       
  1344 	 * @return true|WP_Error True if the status is valid, or WP_Error if not.
       
  1345 	 */
       
  1346 	public function check_status( $status, $request, $param ) {
       
  1347 		if ( $request['id'] ) {
       
  1348 			$post = $this->get_post( $request['id'] );
       
  1349 
       
  1350 			if ( ! is_wp_error( $post ) && $post->post_status === $status ) {
       
  1351 				return true;
       
  1352 			}
       
  1353 		}
       
  1354 
       
  1355 		$args = $request->get_attributes()['args'][ $param ];
       
  1356 
       
  1357 		return rest_validate_value_from_schema( $status, $args, $param );
  1252 	}
  1358 	}
  1253 
  1359 
  1254 	/**
  1360 	/**
  1255 	 * Determines validity and normalizes the given status parameter.
  1361 	 * Determines validity and normalizes the given status parameter.
  1256 	 *
  1362 	 *
  1338 		if ( ! $template ) {
  1444 		if ( ! $template ) {
  1339 			return true;
  1445 			return true;
  1340 		}
  1446 		}
  1341 
  1447 
  1342 		if ( $request['id'] ) {
  1448 		if ( $request['id'] ) {
       
  1449 			$post             = get_post( $request['id'] );
  1343 			$current_template = get_page_template_slug( $request['id'] );
  1450 			$current_template = get_page_template_slug( $request['id'] );
  1344 		} else {
  1451 		} else {
       
  1452 			$post             = null;
  1345 			$current_template = '';
  1453 			$current_template = '';
  1346 		}
  1454 		}
  1347 
  1455 
  1348 		// Always allow for updating a post to the same template, even if that template is no longer supported.
  1456 		// Always allow for updating a post to the same template, even if that template is no longer supported.
  1349 		if ( $template === $current_template ) {
  1457 		if ( $template === $current_template ) {
  1350 			return true;
  1458 			return true;
  1351 		}
  1459 		}
  1352 
  1460 
  1353 		// If this is a create request, get_post() will return null and wp theme will fallback to the passed post type.
  1461 		// If this is a create request, get_post() will return null and wp theme will fallback to the passed post type.
  1354 		$allowed_templates = wp_get_theme()->get_page_templates( get_post( $request['id'] ), $this->post_type );
  1462 		$allowed_templates = wp_get_theme()->get_page_templates( $post, $this->post_type );
  1355 
  1463 
  1356 		if ( isset( $allowed_templates[ $template ] ) ) {
  1464 		if ( isset( $allowed_templates[ $template ] ) ) {
  1357 			return true;
  1465 			return true;
  1358 		}
  1466 		}
  1359 
  1467 
  1368 	 * Sets the template for a post.
  1476 	 * Sets the template for a post.
  1369 	 *
  1477 	 *
  1370 	 * @since 4.7.0
  1478 	 * @since 4.7.0
  1371 	 * @since 4.9.0 Added the `$validate` parameter.
  1479 	 * @since 4.9.0 Added the `$validate` parameter.
  1372 	 *
  1480 	 *
  1373 	 * @param string  $template Page template filename.
  1481 	 * @param string $template Page template filename.
  1374 	 * @param integer $post_id  Post ID.
  1482 	 * @param int    $post_id  Post ID.
  1375 	 * @param bool    $validate Whether to validate that the template selected is valid.
  1483 	 * @param bool   $validate Whether to validate that the template selected is valid.
  1376 	 */
  1484 	 */
  1377 	public function handle_template( $template, $post_id, $validate = false ) {
  1485 	public function handle_template( $template, $post_id, $validate = false ) {
  1378 
  1486 
  1379 		if ( $validate && ! array_key_exists( $template, wp_get_theme()->get_page_templates( get_post( $post_id ) ) ) ) {
  1487 		if ( $validate && ! array_key_exists( $template, wp_get_theme()->get_page_templates( get_post( $post_id ) ) ) ) {
  1380 			$template = '';
  1488 			$template = '';
  1664 		}
  1772 		}
  1665 
  1773 
  1666 		$has_password_filter = false;
  1774 		$has_password_filter = false;
  1667 
  1775 
  1668 		if ( $this->can_access_password_content( $post, $request ) ) {
  1776 		if ( $this->can_access_password_content( $post, $request ) ) {
       
  1777 			$this->password_check_passed[ $post->ID ] = true;
  1669 			// Allow access to the post, permissions already checked before.
  1778 			// Allow access to the post, permissions already checked before.
  1670 			add_filter( 'post_password_required', '__return_false' );
  1779 			add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
  1671 
  1780 
  1672 			$has_password_filter = true;
  1781 			$has_password_filter = true;
  1673 		}
  1782 		}
  1674 
  1783 
  1675 		if ( rest_is_field_included( 'content', $fields ) ) {
  1784 		if ( rest_is_field_included( 'content', $fields ) ) {
  1703 			);
  1812 			);
  1704 		}
  1813 		}
  1705 
  1814 
  1706 		if ( $has_password_filter ) {
  1815 		if ( $has_password_filter ) {
  1707 			// Reset filter.
  1816 			// Reset filter.
  1708 			remove_filter( 'post_password_required', '__return_false' );
  1817 			remove_filter( 'post_password_required', array( $this, 'check_password_required' ) );
  1709 		}
  1818 		}
  1710 
  1819 
  1711 		if ( rest_is_field_included( 'author', $fields ) ) {
  1820 		if ( rest_is_field_included( 'author', $fields ) ) {
  1712 			$data['author'] = (int) $post->post_author;
  1821 			$data['author'] = (int) $post->post_author;
  1713 		}
  1822 		}
  1810 				$response->add_link( $rel, $self );
  1919 				$response->add_link( $rel, $self );
  1811 			}
  1920 			}
  1812 		}
  1921 		}
  1813 
  1922 
  1814 		/**
  1923 		/**
  1815 		 * Filters the post data for a response.
  1924 		 * Filters the post data for a REST API response.
  1816 		 *
  1925 		 *
  1817 		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
  1926 		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
       
  1927 		 *
       
  1928 		 * Possible hook names include:
       
  1929 		 *
       
  1930 		 *  - `rest_prepare_post`
       
  1931 		 *  - `rest_prepare_page`
       
  1932 		 *  - `rest_prepare_attachment`
  1818 		 *
  1933 		 *
  1819 		 * @since 4.7.0
  1934 		 * @since 4.7.0
  1820 		 *
  1935 		 *
  1821 		 * @param WP_REST_Response $response The response object.
  1936 		 * @param WP_REST_Response $response The response object.
  1822 		 * @param WP_Post          $post     Post object.
  1937 		 * @param WP_Post          $post     Post object.
  2036 			'title'      => $this->post_type,
  2151 			'title'      => $this->post_type,
  2037 			'type'       => 'object',
  2152 			'type'       => 'object',
  2038 			// Base properties for every Post.
  2153 			// Base properties for every Post.
  2039 			'properties' => array(
  2154 			'properties' => array(
  2040 				'date'         => array(
  2155 				'date'         => array(
  2041 					'description' => __( "The date the object was published, in the site's timezone." ),
  2156 					'description' => __( "The date the post was published, in the site's timezone." ),
  2042 					'type'        => array( 'string', 'null' ),
  2157 					'type'        => array( 'string', 'null' ),
  2043 					'format'      => 'date-time',
  2158 					'format'      => 'date-time',
  2044 					'context'     => array( 'view', 'edit', 'embed' ),
  2159 					'context'     => array( 'view', 'edit', 'embed' ),
  2045 				),
  2160 				),
  2046 				'date_gmt'     => array(
  2161 				'date_gmt'     => array(
  2047 					'description' => __( 'The date the object was published, as GMT.' ),
  2162 					'description' => __( 'The date the post was published, as GMT.' ),
  2048 					'type'        => array( 'string', 'null' ),
  2163 					'type'        => array( 'string', 'null' ),
  2049 					'format'      => 'date-time',
  2164 					'format'      => 'date-time',
  2050 					'context'     => array( 'view', 'edit' ),
  2165 					'context'     => array( 'view', 'edit' ),
  2051 				),
  2166 				),
  2052 				'guid'         => array(
  2167 				'guid'         => array(
  2053 					'description' => __( 'The globally unique identifier for the object.' ),
  2168 					'description' => __( 'The globally unique identifier for the post.' ),
  2054 					'type'        => 'object',
  2169 					'type'        => 'object',
  2055 					'context'     => array( 'view', 'edit' ),
  2170 					'context'     => array( 'view', 'edit' ),
  2056 					'readonly'    => true,
  2171 					'readonly'    => true,
  2057 					'properties'  => array(
  2172 					'properties'  => array(
  2058 						'raw'      => array(
  2173 						'raw'      => array(
  2059 							'description' => __( 'GUID for the object, as it exists in the database.' ),
  2174 							'description' => __( 'GUID for the post, as it exists in the database.' ),
  2060 							'type'        => 'string',
  2175 							'type'        => 'string',
  2061 							'context'     => array( 'edit' ),
  2176 							'context'     => array( 'edit' ),
  2062 							'readonly'    => true,
  2177 							'readonly'    => true,
  2063 						),
  2178 						),
  2064 						'rendered' => array(
  2179 						'rendered' => array(
  2065 							'description' => __( 'GUID for the object, transformed for display.' ),
  2180 							'description' => __( 'GUID for the post, transformed for display.' ),
  2066 							'type'        => 'string',
  2181 							'type'        => 'string',
  2067 							'context'     => array( 'view', 'edit' ),
  2182 							'context'     => array( 'view', 'edit' ),
  2068 							'readonly'    => true,
  2183 							'readonly'    => true,
  2069 						),
  2184 						),
  2070 					),
  2185 					),
  2071 				),
  2186 				),
  2072 				'id'           => array(
  2187 				'id'           => array(
  2073 					'description' => __( 'Unique identifier for the object.' ),
  2188 					'description' => __( 'Unique identifier for the post.' ),
  2074 					'type'        => 'integer',
  2189 					'type'        => 'integer',
  2075 					'context'     => array( 'view', 'edit', 'embed' ),
  2190 					'context'     => array( 'view', 'edit', 'embed' ),
  2076 					'readonly'    => true,
  2191 					'readonly'    => true,
  2077 				),
  2192 				),
  2078 				'link'         => array(
  2193 				'link'         => array(
  2079 					'description' => __( 'URL to the object.' ),
  2194 					'description' => __( 'URL to the post.' ),
  2080 					'type'        => 'string',
  2195 					'type'        => 'string',
  2081 					'format'      => 'uri',
  2196 					'format'      => 'uri',
  2082 					'context'     => array( 'view', 'edit', 'embed' ),
  2197 					'context'     => array( 'view', 'edit', 'embed' ),
  2083 					'readonly'    => true,
  2198 					'readonly'    => true,
  2084 				),
  2199 				),
  2085 				'modified'     => array(
  2200 				'modified'     => array(
  2086 					'description' => __( "The date the object was last modified, in the site's timezone." ),
  2201 					'description' => __( "The date the post was last modified, in the site's timezone." ),
  2087 					'type'        => 'string',
  2202 					'type'        => 'string',
  2088 					'format'      => 'date-time',
  2203 					'format'      => 'date-time',
  2089 					'context'     => array( 'view', 'edit' ),
  2204 					'context'     => array( 'view', 'edit' ),
  2090 					'readonly'    => true,
  2205 					'readonly'    => true,
  2091 				),
  2206 				),
  2092 				'modified_gmt' => array(
  2207 				'modified_gmt' => array(
  2093 					'description' => __( 'The date the object was last modified, as GMT.' ),
  2208 					'description' => __( 'The date the post was last modified, as GMT.' ),
  2094 					'type'        => 'string',
  2209 					'type'        => 'string',
  2095 					'format'      => 'date-time',
  2210 					'format'      => 'date-time',
  2096 					'context'     => array( 'view', 'edit' ),
  2211 					'context'     => array( 'view', 'edit' ),
  2097 					'readonly'    => true,
  2212 					'readonly'    => true,
  2098 				),
  2213 				),
  2099 				'slug'         => array(
  2214 				'slug'         => array(
  2100 					'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
  2215 					'description' => __( 'An alphanumeric identifier for the post unique to its type.' ),
  2101 					'type'        => 'string',
  2216 					'type'        => 'string',
  2102 					'context'     => array( 'view', 'edit', 'embed' ),
  2217 					'context'     => array( 'view', 'edit', 'embed' ),
  2103 					'arg_options' => array(
  2218 					'arg_options' => array(
  2104 						'sanitize_callback' => array( $this, 'sanitize_slug' ),
  2219 						'sanitize_callback' => array( $this, 'sanitize_slug' ),
  2105 					),
  2220 					),
  2106 				),
  2221 				),
  2107 				'status'       => array(
  2222 				'status'       => array(
  2108 					'description' => __( 'A named status for the object.' ),
  2223 					'description' => __( 'A named status for the post.' ),
  2109 					'type'        => 'string',
  2224 					'type'        => 'string',
  2110 					'enum'        => array_keys( get_post_stati( array( 'internal' => false ) ) ),
  2225 					'enum'        => array_keys( get_post_stati( array( 'internal' => false ) ) ),
  2111 					'context'     => array( 'view', 'edit' ),
  2226 					'context'     => array( 'view', 'edit' ),
       
  2227 					'arg_options' => array(
       
  2228 						'validate_callback' => array( $this, 'check_status' ),
       
  2229 					),
  2112 				),
  2230 				),
  2113 				'type'         => array(
  2231 				'type'         => array(
  2114 					'description' => __( 'Type of Post for the object.' ),
  2232 					'description' => __( 'Type of post.' ),
  2115 					'type'        => 'string',
  2233 					'type'        => 'string',
  2116 					'context'     => array( 'view', 'edit', 'embed' ),
  2234 					'context'     => array( 'view', 'edit', 'embed' ),
  2117 					'readonly'    => true,
  2235 					'readonly'    => true,
  2118 				),
  2236 				),
  2119 				'password'     => array(
  2237 				'password'     => array(
  2125 		);
  2243 		);
  2126 
  2244 
  2127 		$post_type_obj = get_post_type_object( $this->post_type );
  2245 		$post_type_obj = get_post_type_object( $this->post_type );
  2128 		if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
  2246 		if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
  2129 			$schema['properties']['permalink_template'] = array(
  2247 			$schema['properties']['permalink_template'] = array(
  2130 				'description' => __( 'Permalink template for the object.' ),
  2248 				'description' => __( 'Permalink template for the post.' ),
  2131 				'type'        => 'string',
  2249 				'type'        => 'string',
  2132 				'context'     => array( 'edit' ),
  2250 				'context'     => array( 'edit' ),
  2133 				'readonly'    => true,
  2251 				'readonly'    => true,
  2134 			);
  2252 			);
  2135 
  2253 
  2136 			$schema['properties']['generated_slug'] = array(
  2254 			$schema['properties']['generated_slug'] = array(
  2137 				'description' => __( 'Slug automatically generated from the object title.' ),
  2255 				'description' => __( 'Slug automatically generated from the post title.' ),
  2138 				'type'        => 'string',
  2256 				'type'        => 'string',
  2139 				'context'     => array( 'edit' ),
  2257 				'context'     => array( 'edit' ),
  2140 				'readonly'    => true,
  2258 				'readonly'    => true,
  2141 			);
  2259 			);
  2142 		}
  2260 		}
  2143 
  2261 
  2144 		if ( $post_type_obj->hierarchical ) {
  2262 		if ( $post_type_obj->hierarchical ) {
  2145 			$schema['properties']['parent'] = array(
  2263 			$schema['properties']['parent'] = array(
  2146 				'description' => __( 'The ID for the parent of the object.' ),
  2264 				'description' => __( 'The ID for the parent of the post.' ),
  2147 				'type'        => 'integer',
  2265 				'type'        => 'integer',
  2148 				'context'     => array( 'view', 'edit' ),
  2266 				'context'     => array( 'view', 'edit' ),
  2149 			);
  2267 			);
  2150 		}
  2268 		}
  2151 
  2269 
  2202 
  2320 
  2203 			switch ( $attribute ) {
  2321 			switch ( $attribute ) {
  2204 
  2322 
  2205 				case 'title':
  2323 				case 'title':
  2206 					$schema['properties']['title'] = array(
  2324 					$schema['properties']['title'] = array(
  2207 						'description' => __( 'The title for the object.' ),
  2325 						'description' => __( 'The title for the post.' ),
  2208 						'type'        => 'object',
  2326 						'type'        => 'object',
  2209 						'context'     => array( 'view', 'edit', 'embed' ),
  2327 						'context'     => array( 'view', 'edit', 'embed' ),
  2210 						'arg_options' => array(
  2328 						'arg_options' => array(
  2211 							'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
  2329 							'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
  2212 							'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
  2330 							'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
  2213 						),
  2331 						),
  2214 						'properties'  => array(
  2332 						'properties'  => array(
  2215 							'raw'      => array(
  2333 							'raw'      => array(
  2216 								'description' => __( 'Title for the object, as it exists in the database.' ),
  2334 								'description' => __( 'Title for the post, as it exists in the database.' ),
  2217 								'type'        => 'string',
  2335 								'type'        => 'string',
  2218 								'context'     => array( 'edit' ),
  2336 								'context'     => array( 'edit' ),
  2219 							),
  2337 							),
  2220 							'rendered' => array(
  2338 							'rendered' => array(
  2221 								'description' => __( 'HTML title for the object, transformed for display.' ),
  2339 								'description' => __( 'HTML title for the post, transformed for display.' ),
  2222 								'type'        => 'string',
  2340 								'type'        => 'string',
  2223 								'context'     => array( 'view', 'edit', 'embed' ),
  2341 								'context'     => array( 'view', 'edit', 'embed' ),
  2224 								'readonly'    => true,
  2342 								'readonly'    => true,
  2225 							),
  2343 							),
  2226 						),
  2344 						),
  2227 					);
  2345 					);
  2228 					break;
  2346 					break;
  2229 
  2347 
  2230 				case 'editor':
  2348 				case 'editor':
  2231 					$schema['properties']['content'] = array(
  2349 					$schema['properties']['content'] = array(
  2232 						'description' => __( 'The content for the object.' ),
  2350 						'description' => __( 'The content for the post.' ),
  2233 						'type'        => 'object',
  2351 						'type'        => 'object',
  2234 						'context'     => array( 'view', 'edit' ),
  2352 						'context'     => array( 'view', 'edit' ),
  2235 						'arg_options' => array(
  2353 						'arg_options' => array(
  2236 							'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
  2354 							'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
  2237 							'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
  2355 							'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
  2238 						),
  2356 						),
  2239 						'properties'  => array(
  2357 						'properties'  => array(
  2240 							'raw'           => array(
  2358 							'raw'           => array(
  2241 								'description' => __( 'Content for the object, as it exists in the database.' ),
  2359 								'description' => __( 'Content for the post, as it exists in the database.' ),
  2242 								'type'        => 'string',
  2360 								'type'        => 'string',
  2243 								'context'     => array( 'edit' ),
  2361 								'context'     => array( 'edit' ),
  2244 							),
  2362 							),
  2245 							'rendered'      => array(
  2363 							'rendered'      => array(
  2246 								'description' => __( 'HTML content for the object, transformed for display.' ),
  2364 								'description' => __( 'HTML content for the post, transformed for display.' ),
  2247 								'type'        => 'string',
  2365 								'type'        => 'string',
  2248 								'context'     => array( 'view', 'edit' ),
  2366 								'context'     => array( 'view', 'edit' ),
  2249 								'readonly'    => true,
  2367 								'readonly'    => true,
  2250 							),
  2368 							),
  2251 							'block_version' => array(
  2369 							'block_version' => array(
  2252 								'description' => __( 'Version of the content block format used by the object.' ),
  2370 								'description' => __( 'Version of the content block format used by the post.' ),
  2253 								'type'        => 'integer',
  2371 								'type'        => 'integer',
  2254 								'context'     => array( 'edit' ),
  2372 								'context'     => array( 'edit' ),
  2255 								'readonly'    => true,
  2373 								'readonly'    => true,
  2256 							),
  2374 							),
  2257 							'protected'     => array(
  2375 							'protected'     => array(
  2264 					);
  2382 					);
  2265 					break;
  2383 					break;
  2266 
  2384 
  2267 				case 'author':
  2385 				case 'author':
  2268 					$schema['properties']['author'] = array(
  2386 					$schema['properties']['author'] = array(
  2269 						'description' => __( 'The ID for the author of the object.' ),
  2387 						'description' => __( 'The ID for the author of the post.' ),
  2270 						'type'        => 'integer',
  2388 						'type'        => 'integer',
  2271 						'context'     => array( 'view', 'edit', 'embed' ),
  2389 						'context'     => array( 'view', 'edit', 'embed' ),
  2272 					);
  2390 					);
  2273 					break;
  2391 					break;
  2274 
  2392 
  2275 				case 'excerpt':
  2393 				case 'excerpt':
  2276 					$schema['properties']['excerpt'] = array(
  2394 					$schema['properties']['excerpt'] = array(
  2277 						'description' => __( 'The excerpt for the object.' ),
  2395 						'description' => __( 'The excerpt for the post.' ),
  2278 						'type'        => 'object',
  2396 						'type'        => 'object',
  2279 						'context'     => array( 'view', 'edit', 'embed' ),
  2397 						'context'     => array( 'view', 'edit', 'embed' ),
  2280 						'arg_options' => array(
  2398 						'arg_options' => array(
  2281 							'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
  2399 							'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
  2282 							'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
  2400 							'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
  2283 						),
  2401 						),
  2284 						'properties'  => array(
  2402 						'properties'  => array(
  2285 							'raw'       => array(
  2403 							'raw'       => array(
  2286 								'description' => __( 'Excerpt for the object, as it exists in the database.' ),
  2404 								'description' => __( 'Excerpt for the post, as it exists in the database.' ),
  2287 								'type'        => 'string',
  2405 								'type'        => 'string',
  2288 								'context'     => array( 'edit' ),
  2406 								'context'     => array( 'edit' ),
  2289 							),
  2407 							),
  2290 							'rendered'  => array(
  2408 							'rendered'  => array(
  2291 								'description' => __( 'HTML excerpt for the object, transformed for display.' ),
  2409 								'description' => __( 'HTML excerpt for the post, transformed for display.' ),
  2292 								'type'        => 'string',
  2410 								'type'        => 'string',
  2293 								'context'     => array( 'view', 'edit', 'embed' ),
  2411 								'context'     => array( 'view', 'edit', 'embed' ),
  2294 								'readonly'    => true,
  2412 								'readonly'    => true,
  2295 							),
  2413 							),
  2296 							'protected' => array(
  2414 							'protected' => array(
  2303 					);
  2421 					);
  2304 					break;
  2422 					break;
  2305 
  2423 
  2306 				case 'thumbnail':
  2424 				case 'thumbnail':
  2307 					$schema['properties']['featured_media'] = array(
  2425 					$schema['properties']['featured_media'] = array(
  2308 						'description' => __( 'The ID of the featured media for the object.' ),
  2426 						'description' => __( 'The ID of the featured media for the post.' ),
  2309 						'type'        => 'integer',
  2427 						'type'        => 'integer',
  2310 						'context'     => array( 'view', 'edit', 'embed' ),
  2428 						'context'     => array( 'view', 'edit', 'embed' ),
  2311 					);
  2429 					);
  2312 					break;
  2430 					break;
  2313 
  2431 
  2314 				case 'comments':
  2432 				case 'comments':
  2315 					$schema['properties']['comment_status'] = array(
  2433 					$schema['properties']['comment_status'] = array(
  2316 						'description' => __( 'Whether or not comments are open on the object.' ),
  2434 						'description' => __( 'Whether or not comments are open on the post.' ),
  2317 						'type'        => 'string',
  2435 						'type'        => 'string',
  2318 						'enum'        => array( 'open', 'closed' ),
  2436 						'enum'        => array( 'open', 'closed' ),
  2319 						'context'     => array( 'view', 'edit' ),
  2437 						'context'     => array( 'view', 'edit' ),
  2320 					);
  2438 					);
  2321 					$schema['properties']['ping_status']    = array(
  2439 					$schema['properties']['ping_status']    = array(
  2322 						'description' => __( 'Whether or not the object can be pinged.' ),
  2440 						'description' => __( 'Whether or not the post can be pinged.' ),
  2323 						'type'        => 'string',
  2441 						'type'        => 'string',
  2324 						'enum'        => array( 'open', 'closed' ),
  2442 						'enum'        => array( 'open', 'closed' ),
  2325 						'context'     => array( 'view', 'edit' ),
  2443 						'context'     => array( 'view', 'edit' ),
  2326 					);
  2444 					);
  2327 					break;
  2445 					break;
  2328 
  2446 
  2329 				case 'page-attributes':
  2447 				case 'page-attributes':
  2330 					$schema['properties']['menu_order'] = array(
  2448 					$schema['properties']['menu_order'] = array(
  2331 						'description' => __( 'The order of the object in relation to other object of its type.' ),
  2449 						'description' => __( 'The order of the post in relation to other posts.' ),
  2332 						'type'        => 'integer',
  2450 						'type'        => 'integer',
  2333 						'context'     => array( 'view', 'edit' ),
  2451 						'context'     => array( 'view', 'edit' ),
  2334 					);
  2452 					);
  2335 					break;
  2453 					break;
  2336 
  2454 
  2337 				case 'post-formats':
  2455 				case 'post-formats':
  2338 					// Get the native post formats and remove the array keys.
  2456 					// Get the native post formats and remove the array keys.
  2339 					$formats = array_values( get_post_format_slugs() );
  2457 					$formats = array_values( get_post_format_slugs() );
  2340 
  2458 
  2341 					$schema['properties']['format'] = array(
  2459 					$schema['properties']['format'] = array(
  2342 						'description' => __( 'The format for the object.' ),
  2460 						'description' => __( 'The format for the post.' ),
  2343 						'type'        => 'string',
  2461 						'type'        => 'string',
  2344 						'enum'        => $formats,
  2462 						'enum'        => $formats,
  2345 						'context'     => array( 'view', 'edit' ),
  2463 						'context'     => array( 'view', 'edit' ),
  2346 					);
  2464 					);
  2347 					break;
  2465 					break;
  2353 			}
  2471 			}
  2354 		}
  2472 		}
  2355 
  2473 
  2356 		if ( 'post' === $this->post_type ) {
  2474 		if ( 'post' === $this->post_type ) {
  2357 			$schema['properties']['sticky'] = array(
  2475 			$schema['properties']['sticky'] = array(
  2358 				'description' => __( 'Whether or not the object should be treated as sticky.' ),
  2476 				'description' => __( 'Whether or not the post should be treated as sticky.' ),
  2359 				'type'        => 'boolean',
  2477 				'type'        => 'boolean',
  2360 				'context'     => array( 'view', 'edit' ),
  2478 				'context'     => array( 'view', 'edit' ),
  2361 			);
  2479 			);
  2362 		}
  2480 		}
  2363 
  2481 
  2364 		$schema['properties']['template'] = array(
  2482 		$schema['properties']['template'] = array(
  2365 			'description' => __( 'The theme file to use to display the object.' ),
  2483 			'description' => __( 'The theme file to use to display the post.' ),
  2366 			'type'        => 'string',
  2484 			'type'        => 'string',
  2367 			'context'     => array( 'view', 'edit' ),
  2485 			'context'     => array( 'view', 'edit' ),
  2368 			'arg_options' => array(
  2486 			'arg_options' => array(
  2369 				'validate_callback' => array( $this, 'check_template' ),
  2487 				'validate_callback' => array( $this, 'check_template' ),
  2370 			),
  2488 			),
  2378 			if ( array_key_exists( $base, $schema['properties'] ) ) {
  2496 			if ( array_key_exists( $base, $schema['properties'] ) ) {
  2379 				$taxonomy_field_name_with_conflict = ! empty( $taxonomy->rest_base ) ? 'rest_base' : 'name';
  2497 				$taxonomy_field_name_with_conflict = ! empty( $taxonomy->rest_base ) ? 'rest_base' : 'name';
  2380 				_doing_it_wrong(
  2498 				_doing_it_wrong(
  2381 					'register_taxonomy',
  2499 					'register_taxonomy',
  2382 					sprintf(
  2500 					sprintf(
  2383 						/* translators: 1. The taxonomy name, 2. The property name, either 'rest_base' or 'name', 3. The conflicting value. */
  2501 						/* translators: 1: The taxonomy name, 2: The property name, either 'rest_base' or 'name', 3: The conflicting value. */
  2384 						__( '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.' ),
  2502 						__( '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.' ),
  2385 						$taxonomy->name,
  2503 						$taxonomy->name,
  2386 						$taxonomy_field_name_with_conflict,
  2504 						$taxonomy_field_name_with_conflict,
  2387 						$base
  2505 						$base
  2388 					),
  2506 					),
  2390 				);
  2508 				);
  2391 			}
  2509 			}
  2392 
  2510 
  2393 			$schema['properties'][ $base ] = array(
  2511 			$schema['properties'][ $base ] = array(
  2394 				/* translators: %s: Taxonomy name. */
  2512 				/* translators: %s: Taxonomy name. */
  2395 				'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ),
  2513 				'description' => sprintf( __( 'The terms assigned to the post in the %s taxonomy.' ), $taxonomy->name ),
  2396 				'type'        => 'array',
  2514 				'type'        => 'array',
  2397 				'items'       => array(
  2515 				'items'       => array(
  2398 					'type' => 'integer',
  2516 					'type' => 'integer',
  2399 				),
  2517 				),
  2400 				'context'     => array( 'view', 'edit' ),
  2518 				'context'     => array( 'view', 'edit' ),
  2409 
  2527 
  2410 		// Take a snapshot of which fields are in the schema pre-filtering.
  2528 		// Take a snapshot of which fields are in the schema pre-filtering.
  2411 		$schema_fields = array_keys( $schema['properties'] );
  2529 		$schema_fields = array_keys( $schema['properties'] );
  2412 
  2530 
  2413 		/**
  2531 		/**
  2414 		 * Filter the post's schema.
  2532 		 * Filters the post's schema.
  2415 		 *
  2533 		 *
  2416 		 * The dynamic portion of the filter, `$this->post_type`, refers to the
  2534 		 * The dynamic portion of the filter, `$this->post_type`, refers to the
  2417 		 * post type slug for the controller.
  2535 		 * post type slug for the controller.
  2418 		 *
  2536 		 *
  2419 		 * @since 5.4.0
  2537 		 * @since 5.4.0
  2569 
  2687 
  2570 	/**
  2688 	/**
  2571 	 * Retrieves the query params for the posts collection.
  2689 	 * Retrieves the query params for the posts collection.
  2572 	 *
  2690 	 *
  2573 	 * @since 4.7.0
  2691 	 * @since 4.7.0
       
  2692 	 * @since 5.4.0 The `tax_relation` query parameter was added.
       
  2693 	 * @since 5.7.0 The `modified_after` and `modified_before` query parameters were added.
  2574 	 *
  2694 	 *
  2575 	 * @return array Collection parameters.
  2695 	 * @return array Collection parameters.
  2576 	 */
  2696 	 */
  2577 	public function get_collection_params() {
  2697 	public function get_collection_params() {
  2578 		$query_params = parent::get_collection_params();
  2698 		$query_params = parent::get_collection_params();
  2579 
  2699 
  2580 		$query_params['context']['default'] = 'view';
  2700 		$query_params['context']['default'] = 'view';
  2581 
  2701 
  2582 		$query_params['after'] = array(
  2702 		$query_params['after'] = array(
  2583 			'description' => __( 'Limit response to posts published after a given ISO8601 compliant date.' ),
  2703 			'description' => __( 'Limit response to posts published after a given ISO8601 compliant date.' ),
       
  2704 			'type'        => 'string',
       
  2705 			'format'      => 'date-time',
       
  2706 		);
       
  2707 
       
  2708 		$query_params['modified_after'] = array(
       
  2709 			'description' => __( 'Limit response to posts modified after a given ISO8601 compliant date.' ),
  2584 			'type'        => 'string',
  2710 			'type'        => 'string',
  2585 			'format'      => 'date-time',
  2711 			'format'      => 'date-time',
  2586 		);
  2712 		);
  2587 
  2713 
  2588 		if ( post_type_supports( $this->post_type, 'author' ) ) {
  2714 		if ( post_type_supports( $this->post_type, 'author' ) ) {
  2608 			'description' => __( 'Limit response to posts published before a given ISO8601 compliant date.' ),
  2734 			'description' => __( 'Limit response to posts published before a given ISO8601 compliant date.' ),
  2609 			'type'        => 'string',
  2735 			'type'        => 'string',
  2610 			'format'      => 'date-time',
  2736 			'format'      => 'date-time',
  2611 		);
  2737 		);
  2612 
  2738 
       
  2739 		$query_params['modified_before'] = array(
       
  2740 			'description' => __( 'Limit response to posts modified before a given ISO8601 compliant date.' ),
       
  2741 			'type'        => 'string',
       
  2742 			'format'      => 'date-time',
       
  2743 		);
       
  2744 
  2613 		$query_params['exclude'] = array(
  2745 		$query_params['exclude'] = array(
  2614 			'description' => __( 'Ensure result set excludes specific IDs.' ),
  2746 			'description' => __( 'Ensure result set excludes specific IDs.' ),
  2615 			'type'        => 'array',
  2747 			'type'        => 'array',
  2616 			'items'       => array(
  2748 			'items'       => array(
  2617 				'type' => 'integer',
  2749 				'type' => 'integer',
  2646 			'default'     => 'desc',
  2778 			'default'     => 'desc',
  2647 			'enum'        => array( 'asc', 'desc' ),
  2779 			'enum'        => array( 'asc', 'desc' ),
  2648 		);
  2780 		);
  2649 
  2781 
  2650 		$query_params['orderby'] = array(
  2782 		$query_params['orderby'] = array(
  2651 			'description' => __( 'Sort collection by object attribute.' ),
  2783 			'description' => __( 'Sort collection by post attribute.' ),
  2652 			'type'        => 'string',
  2784 			'type'        => 'string',
  2653 			'default'     => 'date',
  2785 			'default'     => 'date',
  2654 			'enum'        => array(
  2786 			'enum'        => array(
  2655 				'author',
  2787 				'author',
  2656 				'date',
  2788 				'date',
  2708 				'type' => 'string',
  2840 				'type' => 'string',
  2709 			),
  2841 			),
  2710 			'sanitize_callback' => array( $this, 'sanitize_post_statuses' ),
  2842 			'sanitize_callback' => array( $this, 'sanitize_post_statuses' ),
  2711 		);
  2843 		);
  2712 
  2844 
  2713 		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
  2845 		$query_params = $this->prepare_taxonomy_limit_schema( $query_params );
  2714 
       
  2715 		if ( ! empty( $taxonomies ) ) {
       
  2716 			$query_params['tax_relation'] = array(
       
  2717 				'description' => __( 'Limit result set based on relationship between multiple taxonomies.' ),
       
  2718 				'type'        => 'string',
       
  2719 				'enum'        => array( 'AND', 'OR' ),
       
  2720 			);
       
  2721 		}
       
  2722 
       
  2723 		foreach ( $taxonomies as $taxonomy ) {
       
  2724 			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
       
  2725 
       
  2726 			$query_params[ $base ] = array(
       
  2727 				/* translators: %s: Taxonomy name. */
       
  2728 				'description' => sprintf( __( 'Limit result set to all items that have the specified term assigned in the %s taxonomy.' ), $base ),
       
  2729 				'type'        => 'array',
       
  2730 				'items'       => array(
       
  2731 					'type' => 'integer',
       
  2732 				),
       
  2733 				'default'     => array(),
       
  2734 			);
       
  2735 
       
  2736 			$query_params[ $base . '_exclude' ] = array(
       
  2737 				/* translators: %s: Taxonomy name. */
       
  2738 				'description' => sprintf( __( 'Limit result set to all items except those that have the specified term assigned in the %s taxonomy.' ), $base ),
       
  2739 				'type'        => 'array',
       
  2740 				'items'       => array(
       
  2741 					'type' => 'integer',
       
  2742 				),
       
  2743 				'default'     => array(),
       
  2744 			);
       
  2745 		}
       
  2746 
  2846 
  2747 		if ( 'post' === $this->post_type ) {
  2847 		if ( 'post' === $this->post_type ) {
  2748 			$query_params['sticky'] = array(
  2848 			$query_params['sticky'] = array(
  2749 				'description' => __( 'Limit result set to items that are sticky.' ),
  2849 				'description' => __( 'Limit result set to items that are sticky.' ),
  2750 				'type'        => 'boolean',
  2850 				'type'        => 'boolean',
  2751 			);
  2851 			);
  2752 		}
  2852 		}
  2753 
  2853 
  2754 		/**
  2854 		/**
  2755 		 * Filter collection parameters for the posts controller.
  2855 		 * Filters collection parameters for the posts controller.
  2756 		 *
  2856 		 *
  2757 		 * The dynamic part of the filter `$this->post_type` refers to the post
  2857 		 * The dynamic part of the filter `$this->post_type` refers to the post
  2758 		 * type slug for the controller.
  2858 		 * type slug for the controller.
  2759 		 *
  2859 		 *
  2760 		 * This filter registers the collection parameter, but does not map the
  2860 		 * This filter registers the collection parameter, but does not map the
  2808 			}
  2908 			}
  2809 		}
  2909 		}
  2810 
  2910 
  2811 		return $statuses;
  2911 		return $statuses;
  2812 	}
  2912 	}
       
  2913 
       
  2914 	/**
       
  2915 	 * Prepares the 'tax_query' for a collection of posts.
       
  2916 	 *
       
  2917 	 * @since 5.7.0
       
  2918 	 *
       
  2919 	 * @param array           $args    WP_Query arguments.
       
  2920 	 * @param WP_REST_Request $request Full details about the request.
       
  2921 	 * @return array Updated query arguments.
       
  2922 	 */
       
  2923 	private function prepare_tax_query( array $args, WP_REST_Request $request ) {
       
  2924 		$relation = $request['tax_relation'];
       
  2925 
       
  2926 		if ( $relation ) {
       
  2927 			$args['tax_query'] = array( 'relation' => $relation );
       
  2928 		}
       
  2929 
       
  2930 		$taxonomies = wp_list_filter(
       
  2931 			get_object_taxonomies( $this->post_type, 'objects' ),
       
  2932 			array( 'show_in_rest' => true )
       
  2933 		);
       
  2934 
       
  2935 		foreach ( $taxonomies as $taxonomy ) {
       
  2936 			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
       
  2937 
       
  2938 			$tax_include = $request[ $base ];
       
  2939 			$tax_exclude = $request[ $base . '_exclude' ];
       
  2940 
       
  2941 			if ( $tax_include ) {
       
  2942 				$terms            = array();
       
  2943 				$include_children = false;
       
  2944 				$operator         = 'IN';
       
  2945 
       
  2946 				if ( rest_is_array( $tax_include ) ) {
       
  2947 					$terms = $tax_include;
       
  2948 				} elseif ( rest_is_object( $tax_include ) ) {
       
  2949 					$terms            = empty( $tax_include['terms'] ) ? array() : $tax_include['terms'];
       
  2950 					$include_children = ! empty( $tax_include['include_children'] );
       
  2951 
       
  2952 					if ( isset( $tax_include['operator'] ) && 'AND' === $tax_include['operator'] ) {
       
  2953 						$operator = 'AND';
       
  2954 					}
       
  2955 				}
       
  2956 
       
  2957 				if ( $terms ) {
       
  2958 					$args['tax_query'][] = array(
       
  2959 						'taxonomy'         => $taxonomy->name,
       
  2960 						'field'            => 'term_id',
       
  2961 						'terms'            => $terms,
       
  2962 						'include_children' => $include_children,
       
  2963 						'operator'         => $operator,
       
  2964 					);
       
  2965 				}
       
  2966 			}
       
  2967 
       
  2968 			if ( $tax_exclude ) {
       
  2969 				$terms            = array();
       
  2970 				$include_children = false;
       
  2971 
       
  2972 				if ( rest_is_array( $tax_exclude ) ) {
       
  2973 					$terms = $tax_exclude;
       
  2974 				} elseif ( rest_is_object( $tax_exclude ) ) {
       
  2975 					$terms            = empty( $tax_exclude['terms'] ) ? array() : $tax_exclude['terms'];
       
  2976 					$include_children = ! empty( $tax_exclude['include_children'] );
       
  2977 				}
       
  2978 
       
  2979 				if ( $terms ) {
       
  2980 					$args['tax_query'][] = array(
       
  2981 						'taxonomy'         => $taxonomy->name,
       
  2982 						'field'            => 'term_id',
       
  2983 						'terms'            => $terms,
       
  2984 						'include_children' => $include_children,
       
  2985 						'operator'         => 'NOT IN',
       
  2986 					);
       
  2987 				}
       
  2988 			}
       
  2989 		}
       
  2990 
       
  2991 		return $args;
       
  2992 	}
       
  2993 
       
  2994 	/**
       
  2995 	 * Prepares the collection schema for including and excluding items by terms.
       
  2996 	 *
       
  2997 	 * @since 5.7.0
       
  2998 	 *
       
  2999 	 * @param array $query_params Collection schema.
       
  3000 	 * @return array Updated schema.
       
  3001 	 */
       
  3002 	private function prepare_taxonomy_limit_schema( array $query_params ) {
       
  3003 		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
       
  3004 
       
  3005 		if ( ! $taxonomies ) {
       
  3006 			return $query_params;
       
  3007 		}
       
  3008 
       
  3009 		$query_params['tax_relation'] = array(
       
  3010 			'description' => __( 'Limit result set based on relationship between multiple taxonomies.' ),
       
  3011 			'type'        => 'string',
       
  3012 			'enum'        => array( 'AND', 'OR' ),
       
  3013 		);
       
  3014 
       
  3015 		$limit_schema = array(
       
  3016 			'type'  => array( 'object', 'array' ),
       
  3017 			'oneOf' => array(
       
  3018 				array(
       
  3019 					'title'       => __( 'Term ID List' ),
       
  3020 					'description' => __( 'Match terms with the listed IDs.' ),
       
  3021 					'type'        => 'array',
       
  3022 					'items'       => array(
       
  3023 						'type' => 'integer',
       
  3024 					),
       
  3025 				),
       
  3026 				array(
       
  3027 					'title'                => __( 'Term ID Taxonomy Query' ),
       
  3028 					'description'          => __( 'Perform an advanced term query.' ),
       
  3029 					'type'                 => 'object',
       
  3030 					'properties'           => array(
       
  3031 						'terms'            => array(
       
  3032 							'description' => __( 'Term IDs.' ),
       
  3033 							'type'        => 'array',
       
  3034 							'items'       => array(
       
  3035 								'type' => 'integer',
       
  3036 							),
       
  3037 							'default'     => array(),
       
  3038 						),
       
  3039 						'include_children' => array(
       
  3040 							'description' => __( 'Whether to include child terms in the terms limiting the result set.' ),
       
  3041 							'type'        => 'boolean',
       
  3042 							'default'     => false,
       
  3043 						),
       
  3044 					),
       
  3045 					'additionalProperties' => false,
       
  3046 				),
       
  3047 			),
       
  3048 		);
       
  3049 
       
  3050 		$include_schema = array_merge(
       
  3051 			array(
       
  3052 				/* translators: %s: Taxonomy name. */
       
  3053 				'description' => __( 'Limit result set to items with specific terms assigned in the %s taxonomy.' ),
       
  3054 			),
       
  3055 			$limit_schema
       
  3056 		);
       
  3057 		// 'operator' is supported only for 'include' queries.
       
  3058 		$include_schema['oneOf'][1]['properties']['operator'] = array(
       
  3059 			'description' => __( 'Whether items must be assigned all or any of the specified terms.' ),
       
  3060 			'type'        => 'string',
       
  3061 			'enum'        => array( 'AND', 'OR' ),
       
  3062 			'default'     => 'OR',
       
  3063 		);
       
  3064 
       
  3065 		$exclude_schema = array_merge(
       
  3066 			array(
       
  3067 				/* translators: %s: Taxonomy name. */
       
  3068 				'description' => __( 'Limit result set to items except those with specific terms assigned in the %s taxonomy.' ),
       
  3069 			),
       
  3070 			$limit_schema
       
  3071 		);
       
  3072 
       
  3073 		foreach ( $taxonomies as $taxonomy ) {
       
  3074 			$base         = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
       
  3075 			$base_exclude = $base . '_exclude';
       
  3076 
       
  3077 			$query_params[ $base ]                = $include_schema;
       
  3078 			$query_params[ $base ]['description'] = sprintf( $query_params[ $base ]['description'], $base );
       
  3079 
       
  3080 			$query_params[ $base_exclude ]                = $exclude_schema;
       
  3081 			$query_params[ $base_exclude ]['description'] = sprintf( $query_params[ $base_exclude ]['description'], $base );
       
  3082 
       
  3083 			if ( ! $taxonomy->hierarchical ) {
       
  3084 				unset( $query_params[ $base ]['oneOf'][1]['properties']['include_children'] );
       
  3085 				unset( $query_params[ $base_exclude ]['oneOf'][1]['properties']['include_children'] );
       
  3086 			}
       
  3087 		}
       
  3088 
       
  3089 		return $query_params;
       
  3090 	}
  2813 }
  3091 }