st_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) { return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( ! $this->check_assign_terms_permission( $request ) ) { return new WP_Error( 'rest_cannot_assign_term', __( 'Sorry, you are not allowed to assign the provided terms.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Updates a single post. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function update_item( $request ) { $valid_check = $this->get_post( $request['id'] ); if ( is_wp_error( $valid_check ) ) { return $valid_check; } $post_before = get_post( $request['id'] ); $post = $this->prepare_item_for_database( $request ); if ( is_wp_error( $post ) ) { return $post; } if ( ! empty( $post->post_status ) ) { $post_status = $post->post_status; } else { $post_status = $post_before->post_status; } /* * `wp_unique_post_slug()` returns the same slug for 'draft' or 'pending' posts. * * To ensure that a unique slug is generated, pass the post data with the 'publish' status. */ if ( ! empty( $post->post_name ) && in_array( $post_status, array( 'draft', 'pending' ), true ) ) { $post_parent = ! empty( $post->post_parent ) ? $post->post_parent : 0; $post->post_name = wp_unique_post_slug( $post->post_name, $post->ID, 'publish', $post->post_type, $post_parent ); } // Convert the post object to an array, otherwise wp_update_post() will expect non-escaped input. $post_id = wp_update_post( wp_slash( (array) $post ), true, false ); if ( is_wp_error( $post_id ) ) { if ( 'db_update_error' === $post_id->get_error_code() ) { $post_id->add_data( array( 'status' => 500 ) ); } else { $post_id->add_data( array( 'status' => 400 ) ); } return $post_id; } $post = get_post( $post_id ); /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ do_action( "rest_insert_{$this->post_type}", $post, $request, false ); $schema = $this->get_item_schema(); if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) { set_post_format( $post, $request['format'] ); } if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) { $this->handle_featured_media( $request['featured_media'], $post_id ); } if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) { if ( ! empty( $request['sticky'] ) ) { stick_post( $post_id ); } else { unstick_post( $post_id ); } } if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) { $this->handle_template( $request['template'], $post->ID ); } $terms_update = $this->handle_terms( $post->ID, $request ); if ( is_wp_error( $terms_update ) ) { return $terms_update; } if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { $meta_update = $this->meta->update_value( $request['meta'], $post->ID ); if ( is_wp_error( $meta_update ) ) { return $meta_update; } } $post = get_post( $post_id ); $fields_update = $this->update_additional_fields_for_object( $post, $request ); if ( is_wp_error( $fields_update ) ) { return $fields_update; } $request->set_param( 'context', 'edit' ); // Filter is fired in WP_REST_Attachments_Controller subclass. if ( 'attachment' === $this->post_type ) { $response = $this->prepare_item_for_response( $post, $request ); return rest_ensure_response( $response ); } /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ do_action( "rest_after_insert_{$this->post_type}", $post, $request, false ); wp_after_insert_post( $post, true, $post_before ); $response = $this->prepare_item_for_response( $post, $request ); return rest_ensure_response( $response ); } /** * Checks if a given request has access to delete a post. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise. */ public function delete_item_permissions_check( $request ) { $post = $this->get_post( $request['id'] ); if ( is_wp_error( $post ) ) { return $post; } if ( $post && ! $this->check_delete_permission( $post ) ) { return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } /** * Deletes a single post. * * @since 4.7.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function delete_item( $request ) { $post = $this->get_post( $request['id'] ); if ( is_wp_error( $post ) ) { return $post; } $id = $post->ID; $force = (bool) $request['force']; $supports_trash = ( EMPTY_TRASH_DAYS > 0 ); if ( 'attachment' === $post->post_type ) { $supports_trash = $supports_trash && MEDIA_TRASH; } /** * Filters whether a post is trashable. * * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. * * Possible hook names include: * * - `rest_post_trashable` * - `rest_page_trashable` * - `rest_attachment_trashable` * * Pass false to disable Trash support for the post. * * @since 4.7.0 * * @param bool $supports_trash Whether the post type support trashing. * @param WP_Post $post The Post object being considered for trashing support. */ $supports_trash = apply_filters( "rest_{$this->post_type}_trashable", $supports_trash, $post ); if ( ! $this->check_delete_permission( $post ) ) { return new WP_Error( 'rest_user_cannot_delete_post', __( 'Sorry, you are not allowed to delete this post.' ), array( 'status' => rest_authorization_required_code() ) ); } $request->set_param( 'context', 'edit' ); // If we're forcing, then delete permanently. if ( $force ) { $previous = $this->prepare_item_for_response( $post, $request ); $result = wp_delete_post( $id, true ); $response = new WP_REST_Response(); $response->set_data( array( 'deleted' => true, 'previous' => $previous->get_data(), ) ); } else { // If we don't support trashing for this type, error out. if ( ! $supports_trash ) { return new WP_Error( 'rest_trash_not_supported', /* translators: %s: force=true */ sprintf( __( "The post does not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) ); } // Otherwise, only trash if we haven't already. if ( 'trash' === $post->post_status ) { return new WP_Error( 'rest_already_trashed', __( 'The post has already been deleted.' ), array( 'status' => 410 ) ); } /* * (Note that internally this falls through to `wp_delete_post()` * if the Trash is disabled.) */ $result = wp_trash_post( $id ); $post = get_post( $id ); $response = $this->prepare_item_for_response( $post, $request ); } if ( ! $result ) { return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) ); } /** * Fires immediately after a single post is deleted or trashed via the REST API. * * They dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. * * Possible hook names include: * * - `rest_delete_post` * - `rest_delete_page` * - `rest_delete_attachment` * * @since 4.7.0 * * @param WP_Post $post The deleted or trashed post. * @param WP_REST_Response $response The response data. * @param WP_REST_Request $request The request sent to the API. */ do_action( "rest_delete_{$this->post_type}", $post, $response, $request ); return $response; } /** * Determines the allowed query_vars for a get_items() response and prepares * them for WP_Query. * * @since 4.7.0 * * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. * @param WP_REST_Request $request Optional. Full details about the request. * @return array Items query arguments. */ protected function prepare_items_query( $prepared_args = array(), $request = null ) { $query_args = array(); foreach ( $prepared_args as $key => $value ) { /** * Filters the query_vars used in get_items() for the constructed query. * * The dynamic portion of the hook name, `$key`, refers to the query_var key. * * @since 4.7.0 * * @param string $value The query_var value. */ $query_args[ $key ] = apply_filters( "rest_query_var-{$key}", $value ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } if ( 'post' !== $this->post_type || ! isset( $query_args['ignore_sticky_posts'] ) ) { $query_args['ignore_sticky_posts'] = true; } // Map to proper WP_Query orderby param. if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) { $orderby_mappings = array( 'id' => 'ID', 'include' => 'post__in', 'slug' => 'post_name', 'include_slugs' => 'post_name__in', ); if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) { $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ]; } } return $query_args; } /** * Checks the post_date_gmt or modified_gmt and prepare any post or * modified date for single post output. * * @since 4.7.0 * * @param string $date_gmt GMT publication time. * @param string|null $date Optional. Local publication time. Default null. * @return string|null ISO8601/RFC3339 formatted datetime. */ protected function prepare_date_response( $date_gmt, $date = null ) { // Use the date if passed. if ( isset( $date ) ) { return mysql_to_rfc3339( $date ); } // Return null if $date_gmt is empty/zeros. if ( '0000-00-00 00:00:00' === $date_gmt ) { return null; } // Return the formatted datetime. return mysql_to_rfc3339( $date_gmt ); } /** * Prepares a single post for create or update. * * @since 4.7.0 * * @param WP_REST_Request $request Request object. * @return stdClass|WP_Error Post object or WP_Error. */ protected function prepare_item_for_database( $request ) { $prepared_post = new stdClass(); $current_status = ''; // Post ID. if ( isset( $request['id'] ) ) { $existing_post = $this->get_post( $request['id'] ); if ( is_wp_error( $existing_post ) ) { return $existing_post; } $prepared_post->ID = $existing_post->ID; $current_status = $existing_post->post_status; } $schema = $this->get_item_schema(); // Post title. if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) { if ( is_string( $request['title'] ) ) { $prepared_post->post_title = $request['title']; } elseif ( ! empty( $request['title']['raw'] ) ) { $prepared_post->post_title = $request['title']['raw']; } } // Post content. if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) { if ( is_string( $request['content'] ) ) { $prepared_post->post_content = $request['content']; } elseif ( isset( $request['content']['raw'] ) ) { $prepared_post->post_content = $request['content']['raw']; } } // Post excerpt. if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) { if ( is_string( $request['excerpt'] ) ) { $prepared_post->post_excerpt = $request['excerpt']; } elseif ( isset( $request['excerpt']['raw'] ) ) { $prepared_post->post_excerpt = $request['excerpt']['raw']; } } // Post type. if ( empty( $request['id'] ) ) { // Creating new post, use default type for the controller. $prepared_post->post_type = $this->post_type; } else { // Updating a post, use previous type. $prepared_post->post_type = get_post_type( $request['id'] ); } $post_type = get_post_type_object( $prepared_post->post_type ); // Post status. if ( ! empty( $schema['properties']['status'] ) && isset( $request['status'] ) && ( ! $current_status || $current_status !== $request['status'] ) ) { $status = $this->handle_status_param( $request['status'], $post_type ); if ( is_wp_error( $status ) ) { return $status; } $prepared_post->post_status = $status; } // Post date. if ( ! empty( $schema['properties']['date'] ) && ! empty( $request['date'] ) ) { $current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date : false; $date_data = rest_get_date_with_gmt( $request['date'] ); if ( ! empty( $date_data ) && $current_date !== $date_data[0] ) { list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data; $prepared_post->edit_date = true; } } elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) { $current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date_gmt : false; $date_data = rest_get_date_with_gmt( $request['date_gmt'], true ); if ( ! empty( $date_data ) && $current_date !== $date_data[1] ) { list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data; $prepared_post->edit_date = true; } } /* * Sending a null date or date_gmt value resets date and date_gmt to their * default values (`0000-00-00 00:00:00`). */ if ( ( ! empty( $schema['properties']['date_gmt'] ) && $request->has_param( 'date_gmt' ) && null === $request['date_gmt'] ) || ( ! empty( $schema['properties']['date'] ) && $request->has_param( 'date' ) && null === $request['date'] ) ) { $prepared_post->post_date_gmt = null; $prepared_post->post_date = null; } // Post slug. if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) { $prepared_post->post_name = $request['slug']; } // Author. if ( ! empty( $schema['properties']['author'] ) && ! empty( $request['author'] ) ) { $post_author = (int) $request['author']; if ( get_current_user_id() !== $post_author ) { $user_obj = get_userdata( $post_author ); if ( ! $user_obj ) { return new WP_Error( 'rest_invalid_author', __( 'Invalid author ID.' ), array( 'status' => 400 ) ); } } $prepared_post->post_author = $post_author; } // Post password. if ( ! empty( $schema['properties']['password'] ) && isset( $request['password'] ) ) { $prepared_post->post_password = $request['password']; if ( '' !== $request['password'] ) { if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) { return new WP_Error( 'rest_invalid_field', __( 'A post can not be sticky and have a password.' ), array( 'status' => 400 ) ); } if ( ! empty( $prepared_post->ID ) && is_sticky( $prepared_post->ID ) ) { return new WP_Error( 'rest_invalid_field', __( 'A sticky post can not be password protected.' ), array( 'status' => 400 ) ); } } } if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) { if ( ! empty( $prepared_post->ID ) && post_password_required( $prepared_post->ID ) ) { return new WP_Error( 'rest_invalid_field', __( 'A password protected post can not be set to sticky.' ), array( 'status' => 400 ) ); } } // Parent. if ( ! empty( $schema['properties']['parent'] ) && isset( $request['parent'] ) ) { if ( 0 === (int) $request['parent'] ) { $prepared_post->post_parent = 0; } else { $parent = get_post( (int) $request['parent'] ); if ( empty( $parent ) ) { return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post parent ID.' ), array( 'status' => 400 ) ); } $prepared_post->post_parent = (int) $parent->ID; } } // Menu order. if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) { $prepared_post->menu_order = (int) $request['menu_order']; } // Comment status. if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) { $prepared_post->comment_status = $request['comment_status']; } // Ping status. if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) { $prepared_post->ping_status = $request['ping_status']; } if ( ! empty( $schema['properties']['template'] ) ) { // Force template to null so that it can be handled exclusively by the REST controller. $prepared_post->page_template = null; } /** * Filters a post before it is inserted via the REST API. * * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. * * Possible hook names include: * * - `rest_pre_insert_post` * - `rest_pre_insert_page` * - `rest_pre_insert_attachment` * * @since 4.7.0 * * @param stdClass $prepared_post An object representing a single post prepared * for inserting or updating the database. * @param WP_REST_Request $request Request object. */ return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request ); } /** * Checks whether the status is valid for the given post. * * Allows for sending an update request with the current status, even if that status would not be acceptable. * * @since 5.6.0 * * @param string $status The provided status. * @param WP_REST_Request $request The request object. * @param string $param The parameter name. * @return true|WP_Error True if the status is valid, or WP_Error if not. */ public function check_status( $status, $request, $param ) { if ( $request['id'] ) { $post = $this->get_post( $request['id'] ); if ( ! is_wp_error( $post ) && $post->post_status === $status ) { return true; } } $args = $request->get_attributes()['args'][ $param ]; return rest_validate_value_from_schema( $status, $args, $param ); } /** * Determines validity and normalizes the given status parameter. * * @since 4.7.0 * * @param string $post_status Post status. * @param WP_Post_Type $post_type Post type. * @return string|WP_Error Post status or WP_Error if lacking the proper permission. */ protected function handle_status_param( $post_status, $post_type ) { switch ( $post_status ) { case 'draft': case 'pending': break; case 'private': if ( ! current_user_can( $post_type->cap->publish_posts ) ) { return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to create private posts in this post type.' ), array( 'status' => rest_authorization_required_code() ) ); } break; case 'publish': case 'future': if ( ! current_user_can( $post_type->cap->publish_posts ) ) { return new WP_Error( 'rest_cannot_publish', __( 'Sorry, you are not allowed to publish posts in this post type.' ), array( 'status' => rest_authorization_required_code() ) ); } break; default: if ( ! get_post_status_object( $post_status ) ) { $post_status = 'draft'; } break; } return $post_status; } /** * Determines the featured media based on a request param. * * @since 4.7.0 * * @param int $featured_media Featured Media ID. * @param int $post_id Post ID. * @return bool|WP_Error Whether the post thumbnail was successfully deleted, otherwise WP_Error. */ protected function handle_featured_media( $featured_media, $post_id ) { $featured_media = (int)