diff --git a/src/wp-includes/abilities.php b/src/wp-includes/abilities.php index 236b99836a3b9..c29c6a2d23e68 100644 --- a/src/wp-includes/abilities.php +++ b/src/wp-includes/abilities.php @@ -30,6 +30,14 @@ function wp_register_core_ability_categories(): void { 'description' => __( 'Abilities that retrieve or modify user information and settings.' ), ) ); + + wp_register_ability_category( + 'content', + array( + 'label' => __( 'Content' ), + 'description' => __( 'Abilities that retrieve or modify content items of any post type.' ), + ) + ); } /** @@ -351,4 +359,349 @@ function wp_register_core_abilities(): void { ), ) ); + wp_register_ability( + 'core/get-settings', + array( + 'label' => __( 'Get Site Settings' ), + 'description' => __( 'Returns the site settings registered for exposure, cast to the types they were registered with.' ), + 'category' => $category_site, + 'input_schema' => array( + 'type' => 'object', + 'properties' => array(), + 'additionalProperties' => false, + 'default' => array(), + ), + 'output_schema' => array( + 'type' => 'object', + 'description' => __( 'The registered settings, keyed by setting name. Properties are determined at runtime by the registered settings.' ), + 'additionalProperties' => true, + ), + 'execute_callback' => static function ( $input = array() ) { + return wp_get_settings_values(); + }, + 'permission_callback' => 'wp_current_user_can_manage_settings', + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + ), + ), + ) + ); + + wp_register_ability( + 'core/update-settings', + array( + 'label' => __( 'Update Site Settings' ), + 'description' => __( 'Updates one or more registered site settings. A setting given an explicit null value is reset to its default.' ), + 'category' => $category_site, + 'input_schema' => array( + 'type' => 'object', + 'description' => __( 'Setting names mapped to their new values. Accepted properties are determined at runtime by the registered settings.' ), + 'additionalProperties' => true, + 'default' => array(), + ), + 'output_schema' => array( + 'type' => 'object', + 'description' => __( 'The full set of registered settings after the update.' ), + 'additionalProperties' => true, + ), + 'execute_callback' => static function ( $input = array() ) { + return wp_update_settings_values( is_array( $input ) ? $input : array() ); + }, + 'permission_callback' => 'wp_current_user_can_manage_settings', + 'meta' => array( + 'annotations' => array( + 'readonly' => false, + 'destructive' => true, + 'idempotent' => true, + ), + ), + ) + ); + + wp_register_ability( + 'core/get-item', + array( + 'label' => __( 'Get Content Item' ), + 'description' => __( 'Returns a single content item of any exposed post type, addressed by ID. The post type is an argument, so one ability covers posts, pages, and custom post types.' ), + 'category' => 'content', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the item to return.' ), + ), + 'post_type' => array( + 'type' => 'string', + 'description' => __( 'Optional. The expected post type. When provided, the item must be of this type.' ), + ), + ), + 'required' => array( 'id' ), + 'additionalProperties' => false, + ), + 'output_schema' => array( + 'type' => 'object', + 'description' => __( 'The content item. Which fields are present depends on what the post type supports.' ), + 'additionalProperties' => true, + ), + 'execute_callback' => static function ( $input = array() ) { + $post = get_post( (int) $input['id'] ); + + if ( ! $post instanceof WP_Post || ! wp_is_post_type_exposed( $post->post_type ) ) { + return new WP_Error( + 'invalid_item', + __( 'Invalid item ID.' ), + array( 'status' => 404 ) + ); + } + + if ( ! empty( $input['post_type'] ) && $post->post_type !== $input['post_type'] ) { + return new WP_Error( + 'invalid_item', + __( 'Invalid item ID.' ), + array( 'status' => 404 ) + ); + } + + return wp_get_post_item_data( $post ); + }, + 'permission_callback' => static function ( $input = array() ) { + $post = get_post( (int) $input['id'] ); + + if ( ! $post instanceof WP_Post ) { + return false; + } + + return wp_check_read_post_permission( $post ); + }, + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + ), + ), + ) + ); + + wp_register_ability( + 'core/create-item', + array( + 'label' => __( 'Create Content Item' ), + 'description' => __( 'Creates a content item of any exposed post type. The post type is an argument, so one ability covers posts, pages, and custom post types. Media uploads are not handled here.' ), + 'category' => 'content', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'post_type' => array( + 'type' => 'string', + 'default' => 'post', + 'description' => __( 'The post type to create. Defaults to "post".' ), + ), + 'title' => array( 'type' => 'string' ), + 'content' => array( 'type' => 'string' ), + 'excerpt' => array( 'type' => 'string' ), + 'slug' => array( 'type' => 'string' ), + 'status' => array( 'type' => 'string' ), + 'author' => array( 'type' => 'integer' ), + 'parent' => array( 'type' => 'integer' ), + 'menu_order' => array( 'type' => 'integer' ), + 'comment_status' => array( + 'type' => 'string', + 'enum' => array( 'open', 'closed' ), + ), + 'ping_status' => array( + 'type' => 'string', + 'enum' => array( 'open', 'closed' ), + ), + 'password' => array( 'type' => 'string' ), + 'date' => array( 'type' => 'string' ), + 'date_gmt' => array( 'type' => 'string' ), + 'sticky' => array( 'type' => 'boolean' ), + 'format' => array( 'type' => 'string' ), + 'template' => array( 'type' => 'string' ), + 'featured_media' => array( 'type' => 'integer' ), + 'terms' => array( + 'type' => 'object', + 'description' => __( 'Term IDs to assign, keyed by taxonomy REST base.' ), + 'additionalProperties' => true, + ), + ), + 'additionalProperties' => false, + ), + 'output_schema' => array( + 'type' => 'object', + 'description' => __( 'The created content item.' ), + 'additionalProperties' => true, + ), + 'execute_callback' => static function ( $input = array() ) { + $post_type = ! empty( $input['post_type'] ) ? $input['post_type'] : 'post'; + + return wp_create_post_item( $post_type, $input ); + }, + 'permission_callback' => static function ( $input = array() ) { + $post_type = ! empty( $input['post_type'] ) ? $input['post_type'] : 'post'; + + if ( ! wp_is_post_type_exposed( $post_type ) ) { + return false; + } + + /* + * Abilities nest term assignments under `terms`; the shared permission + * helper reads them keyed by taxonomy REST base, as REST sends them. + */ + $params = $input; + if ( isset( $input['terms'] ) && is_array( $input['terms'] ) ) { + $params = array_merge( $input, $input['terms'] ); + } + + return true === wp_check_create_post_permission( $post_type, $params ); + }, + 'meta' => array( + 'annotations' => array( + 'readonly' => false, + 'destructive' => false, + 'idempotent' => false, + ), + ), + ) + ); + + wp_register_ability( + 'core/update-item', + array( + 'label' => __( 'Update Content Item' ), + 'description' => __( 'Updates a content item of any exposed post type, addressed by ID. Only the fields supplied are changed.' ), + 'category' => 'content', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the item to update.' ), + ), + 'title' => array( 'type' => 'string' ), + 'content' => array( 'type' => 'string' ), + 'excerpt' => array( 'type' => 'string' ), + 'slug' => array( 'type' => 'string' ), + 'status' => array( 'type' => 'string' ), + 'author' => array( 'type' => 'integer' ), + 'parent' => array( 'type' => 'integer' ), + 'menu_order' => array( 'type' => 'integer' ), + 'comment_status' => array( + 'type' => 'string', + 'enum' => array( 'open', 'closed' ), + ), + 'ping_status' => array( + 'type' => 'string', + 'enum' => array( 'open', 'closed' ), + ), + 'password' => array( 'type' => 'string' ), + 'date' => array( 'type' => 'string' ), + 'date_gmt' => array( 'type' => 'string' ), + 'sticky' => array( 'type' => 'boolean' ), + 'format' => array( 'type' => 'string' ), + 'template' => array( 'type' => 'string' ), + 'featured_media' => array( 'type' => 'integer' ), + 'terms' => array( + 'type' => 'object', + 'description' => __( 'Term IDs to assign, keyed by taxonomy REST base.' ), + 'additionalProperties' => true, + ), + ), + 'required' => array( 'id' ), + 'additionalProperties' => false, + ), + 'output_schema' => array( + 'type' => 'object', + 'description' => __( 'The updated content item.' ), + 'additionalProperties' => true, + ), + 'execute_callback' => static function ( $input = array() ) { + return wp_update_post_item( (int) $input['id'], $input ); + }, + 'permission_callback' => static function ( $input = array() ) { + $post = get_post( (int) $input['id'] ); + + if ( ! $post instanceof WP_Post || ! wp_is_post_type_exposed( $post->post_type ) ) { + return false; + } + + /* + * Abilities nest term assignments under `terms`; the shared permission + * helper reads them keyed by taxonomy REST base, as REST sends them. + */ + $params = $input; + if ( isset( $input['terms'] ) && is_array( $input['terms'] ) ) { + $params = array_merge( $input, $input['terms'] ); + } + + return true === wp_check_update_post_permission( $post, $params ); + }, + 'meta' => array( + 'annotations' => array( + 'readonly' => false, + 'destructive' => true, + 'idempotent' => true, + ), + ), + ) + ); + + wp_register_ability( + 'core/delete-item', + array( + 'label' => __( 'Delete Content Item' ), + 'description' => __( 'Moves a content item of any exposed post type to the Trash, or deletes it permanently when force is true.' ), + 'category' => 'content', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The ID of the item to delete.' ), + ), + 'force' => array( + 'type' => 'boolean', + 'default' => false, + 'description' => __( 'Whether to bypass the Trash and delete the item permanently.' ), + ), + ), + 'required' => array( 'id' ), + 'additionalProperties' => false, + ), + 'output_schema' => array( + 'type' => 'object', + 'properties' => array( + 'deleted' => array( + 'type' => 'boolean', + 'description' => __( 'Whether the item was deleted.' ), + ), + 'previous' => array( + 'type' => 'object', + 'description' => __( 'The item as it was before deletion.' ), + 'additionalProperties' => true, + ), + ), + ), + 'execute_callback' => static function ( $input = array() ) { + return wp_delete_post_item( (int) $input['id'], ! empty( $input['force'] ) ); + }, + 'permission_callback' => static function ( $input = array() ) { + $post = get_post( (int) $input['id'] ); + + if ( ! $post instanceof WP_Post ) { + return false; + } + + return wp_check_delete_post_permission( $post ); + }, + 'meta' => array( + 'annotations' => array( + 'readonly' => false, + 'destructive' => true, + 'idempotent' => false, + ), + ), + ) + ); } diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index 8bd6a1821162e..b7d0d93ed86af 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -3223,6 +3223,221 @@ function get_registered_settings() { return $wp_registered_settings; } +/** +* Retrieves the registered settings that opt in to being exposed over the REST API. +* +* Normalizes each opted-in setting from {@see get_registered_settings()} into the +* shape shared by the settings abilities and the settings REST endpoint. +* +* @since 7.1.0 +* +* @return array[] Array of registered options, keyed by the public setting name. +*/ +function wp_get_registered_setting_options() { + $rest_options = array(); + + foreach ( get_registered_settings() as $name => $args ) { + if ( empty( $args['show_in_rest'] ) ) { + continue; + } + + $rest_args = array(); + + if ( is_array( $args['show_in_rest'] ) ) { + $rest_args = $args['show_in_rest']; + } + + $defaults = array( + 'name' => ! empty( $rest_args['name'] ) ? $rest_args['name'] : $name, + 'schema' => array(), + ); + + $rest_args = array_merge( $defaults, $rest_args ); + + $default_schema = array( + 'type' => empty( $args['type'] ) ? null : $args['type'], + 'title' => empty( $args['label'] ) ? '' : $args['label'], + 'description' => empty( $args['description'] ) ? '' : $args['description'], + 'default' => $args['default'] ?? null, + ); + + $rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] ); + $rest_args['option_name'] = $name; + + // Skip over settings that don't have a defined type in the schema. + if ( empty( $rest_args['schema']['type'] ) ) { + continue; + } + + /* + * Allow the supported types for settings, as we don't want invalid types + * to be updated with arbitrary values that we can't do decent sanitizing for. + */ + if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'integer', 'string', 'boolean', 'array', 'object' ), true ) ) { + continue; + } + + $rest_args['schema'] = rest_default_additional_properties_to_false( $rest_args['schema'] ); + + $rest_options[ $rest_args['name'] ] = $rest_args; + } + + return $rest_options; +} + +/** +* Prepares a setting value for output based on its schema. +* +* Because get_option() is lossy, values are cast to the type they are registered +* with. A value that does not validate is returned as null, which is +* non-destructive and signals "not set" to consumers. +* +* @since 7.1.0 +* +* @param mixed $value Value to prepare. +* @param array $schema Schema to match. +* @return mixed The prepared value, or null when the value does not validate. +*/ +function wp_prepare_setting_value( $value, $schema ) { + /* + * If the value is not valid by the schema, set the value to null. + * Null values are specifically non-destructive, so this will not cause + * overwriting the current invalid value to null. + */ + if ( is_wp_error( rest_validate_value_from_schema( $value, $schema ) ) ) { + return null; + } + + return rest_sanitize_value_from_schema( $value, $schema ); +} + +/** +* Determines whether the current user may read and manage site settings. +* +* Shared by every consumer of the settings functions so the rule is stated once. +* +* @since 7.1.0 +* +* @return bool True if the current user may manage settings. +*/ +function wp_current_user_can_manage_settings() { + return current_user_can( 'manage_options' ); +} + +/** +* Retrieves the values of the settings registered for exposure. +* +* @since 7.1.0 +* +* @return array Setting values keyed by the public setting name. +*/ +function wp_get_settings_values() { + $options = wp_get_registered_setting_options(); + $response = array(); + + foreach ( $options as $name => $args ) { + /** + * Filters the value of a setting recognized by the REST API. + * + * Allow hijacking the setting value and overriding the built-in behavior by returning a + * non-null value. The returned value will be presented as the setting value instead. + * + * @since 4.7.0 + * + * @param mixed $result Value to use for the requested setting. Can be a scalar + * matching the registered schema for the setting, or null to + * follow the default get_option() behavior. + * @param string $name Setting name (as shown in REST API responses). + * @param array $args Arguments passed to register_setting() for this setting. + */ + $response[ $name ] = apply_filters( 'rest_pre_get_setting', null, $name, $args ); + + if ( is_null( $response[ $name ] ) ) { + // Default to a null value as "null" in the response means "not set". + $response[ $name ] = get_option( $args['option_name'], $args['schema']['default'] ); + } + + $response[ $name ] = wp_prepare_setting_value( $response[ $name ], $args['schema'] ); + } + + return $response; +} + +/** +* Updates the values of the settings registered for exposure. +* +* Only keys present in $values are considered. A key present with a null value +* resets that setting to its default. +* +* @since 7.1.0 +* +* @param array $values Setting names mapped to their new values. +* @return array|WP_Error The full set of setting values after the update, or WP_Error on failure. +*/ +function wp_update_settings_values( $values ) { + $options = wp_get_registered_setting_options(); + $params = is_array( $values ) ? $values : array(); + + foreach ( $options as $name => $args ) { + if ( ! array_key_exists( $name, $params ) ) { + continue; + } + + /** + * Filters whether to preempt a setting value update via the REST API. + * + * Allows hijacking the setting update logic and overriding the built-in behavior by + * returning true. + * + * @since 4.7.0 + * + * @param bool $result Whether to override the default behavior for updating the + * value of a setting. + * @param string $name Setting name (as shown in REST API responses). + * @param mixed $value Updated setting value. + * @param array $args Arguments passed to register_setting() for this setting. + */ + $updated = apply_filters( 'rest_pre_update_setting', false, $name, $params[ $name ], $args ); + + if ( $updated ) { + continue; + } + + /* + * A null value for an option would have the same effect as + * deleting the option from the database, and relying on the + * default value. + */ + if ( is_null( $params[ $name ] ) ) { + /* + * A null value is returned in the response for any option + * that has a non-scalar value. + * + * To protect clients from accidentally including the null + * values from a response object in a request, we do not allow + * options with values that don't pass validation to be updated to null. + * Without this added protection a client could mistakenly + * delete all options that have invalid values from the + * database. + */ + if ( is_wp_error( rest_validate_value_from_schema( get_option( $args['option_name'], false ), $args['schema'] ) ) ) { + return new WP_Error( + 'rest_invalid_stored_value', + /* translators: %s: Property name. */ + sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), + array( 'status' => 500 ) + ); + } + + delete_option( $args['option_name'] ); + } else { + update_option( $args['option_name'], $params[ $name ] ); + } + } + + return wp_get_settings_values(); +} + /** * Filters the default value for the option. * diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 2db73e9a20476..a6654a7ff5c8f 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -5836,6 +5836,910 @@ function wp_set_post_terms( $post_id = 0, $terms = '', $taxonomy = 'post_tag', $ return wp_set_object_terms( $post_id, $terms, $taxonomy, $append ); } + +/** +* Determines whether the current user may assign the post terms in the given parameters. +* +* Only taxonomies registered with `show_in_rest` are considered. Parameters are keyed +* by the taxonomy REST base, falling back to the taxonomy name. +* +* @since 7.1.0 +* +* @param string $post_type Post type name. +* @param array $params Parameters keyed by taxonomy REST base. +* @return bool Whether the current user can assign the provided terms. +*/ +function wp_check_post_terms_assign_permission( $post_type, $params ) { + $taxonomies = wp_list_filter( get_object_taxonomies( $post_type, 'objects' ), array( 'show_in_rest' => true ) ); + + foreach ( $taxonomies as $taxonomy ) { + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + + if ( ! isset( $params[ $base ] ) ) { + continue; + } + + foreach ( (array) $params[ $base ] as $term_id ) { + // Invalid terms will be rejected later. + if ( ! get_term( $term_id, $taxonomy->name ) ) { + continue; + } + + if ( ! current_user_can( 'assign_term', (int) $term_id ) ) { + return false; + } + } + } + + return true; +} + +/** +* Assigns the post terms supplied in the given parameters. +* +* @since 7.1.0 +* +* @param string $post_type Post type name. +* @param int $post_id The post ID to assign the terms to. +* @param array $params Parameters keyed by taxonomy REST base. +* @return null|WP_Error WP_Error on an error assigning any of the terms, otherwise null. +*/ +function wp_set_post_terms_from_params( $post_type, $post_id, $params ) { + $taxonomies = wp_list_filter( get_object_taxonomies( $post_type, 'objects' ), array( 'show_in_rest' => true ) ); + + foreach ( $taxonomies as $taxonomy ) { + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + + if ( ! isset( $params[ $base ] ) ) { + continue; + } + + $result = wp_set_object_terms( $post_id, $params[ $base ], $taxonomy->name ); + + if ( is_wp_error( $result ) ) { + return $result; + } + } + + return null; +} + +/** +* Determines whether the current user may create a post of the given type with the given parameters. +* +* @since 7.1.0 +* +* @param string $post_type Post type name. +* @param array $params The parameters the post would be created with. +* @return true|WP_Error True if the current user may create the post, WP_Error otherwise. +*/ +function wp_check_create_post_permission( $post_type, $params ) { + if ( ! empty( $params['id'] ) ) { + return new WP_Error( + 'rest_post_exists', + __( 'Cannot create existing post.' ), + array( 'status' => 400 ) + ); + } + + $post_type_object = get_post_type_object( $post_type ); + + if ( ! empty( $params['author'] ) && get_current_user_id() !== $params['author'] && ! current_user_can( $post_type_object->cap->edit_others_posts ) ) { + return new WP_Error( + 'rest_cannot_edit_others', + __( 'Sorry, you are not allowed to create posts as this user.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + if ( ! empty( $params['sticky'] ) && ! current_user_can( $post_type_object->cap->edit_others_posts ) && ! current_user_can( $post_type_object->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 ( ! current_user_can( $post_type_object->cap->create_posts ) ) { + return new WP_Error( + 'rest_cannot_create', + __( 'Sorry, you are not allowed to create posts as this user.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + if ( ! wp_check_post_terms_assign_permission( $post_type, $params ) ) { + 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; +} + + +/** +* Determines whether a post type is exposed for programmatic access. +* +* @since 7.1.0 +* +* @param WP_Post_Type|string $post_type Post type name or object. +* @return bool Whether the post type is exposed. +*/ +function wp_is_post_type_exposed( $post_type ) { + if ( ! is_object( $post_type ) ) { + $post_type = get_post_type_object( $post_type ); + } + + if ( ! empty( $post_type ) && ! empty( $post_type->show_in_rest ) ) { + return true; + } + + return false; +} + +/** +* Determines whether the current user may read the given post. +* +* @since 7.1.0 +* +* @param WP_Post $post Post object. +* @return bool Whether the current user may read the post. +*/ +function wp_check_read_post_permission( $post ) { + $post_type = get_post_type_object( $post->post_type ); + if ( ! wp_is_post_type_exposed( $post_type ) ) { + return false; + } + + // Is the post readable? + if ( 'publish' === $post->post_status || current_user_can( 'read_post', $post->ID ) ) { + return true; + } + + $post_status_obj = get_post_status_object( $post->post_status ); + if ( $post_status_obj && $post_status_obj->public ) { + return true; + } + + // Can we read the parent if we're inheriting? + if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) { + $parent = get_post( $post->post_parent ); + if ( $parent ) { + return wp_check_read_post_permission( $parent ); + } + } + + /* + * If there isn't a parent, but the status is set to inherit, assume + * it's published (as per get_post_status()). + */ + if ( 'inherit' === $post->post_status ) { + return true; + } + + return false; +} + +/** +* Determines whether the current user may assign the given status to a post of the given type. +* +* An unrecognized status is downgraded to 'draft' rather than rejected. +* +* @since 7.1.0 +* +* @param string $post_status The desired post status. +* @param WP_Post_Type $post_type Post type object. +* @return string|WP_Error The status to use, or WP_Error if the current user may not assign it. +*/ +function wp_check_post_status_permission( $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; +} + +/** +* Builds a post-type-agnostic representation of a post. +* +* Fields that depend on post type support (sticky, format, featured media, template) +* are included only when the post type supports them. The post password is never +* included. +* +* @since 7.1.0 +* +* @param int|WP_Post $post Post ID or post object. +* @return array|null The post data, or null if the post does not exist. +*/ +function wp_get_post_item_data( $post ) { + $post = get_post( $post ); + + if ( ! $post instanceof WP_Post ) { + return null; + } + + $post_type = $post->post_type; + + $data = array( + 'id' => (int) $post->ID, + 'type' => $post_type, + 'status' => $post->post_status, + 'title' => $post->post_title, + 'content' => $post->post_content, + 'excerpt' => $post->post_excerpt, + 'slug' => $post->post_name, + 'link' => (string) get_permalink( $post->ID ), + 'author' => (int) $post->post_author, + 'parent' => (int) $post->post_parent, + 'menu_order' => (int) $post->menu_order, + 'comment_status' => $post->comment_status, + 'ping_status' => $post->ping_status, + 'date' => mysql_to_rfc3339( $post->post_date ), + 'date_gmt' => mysql_to_rfc3339( $post->post_date_gmt ), + 'modified' => mysql_to_rfc3339( $post->post_modified ), + 'modified_gmt' => mysql_to_rfc3339( $post->post_modified_gmt ), + ); + + if ( 'post' === $post_type ) { + $data['sticky'] = is_sticky( $post->ID ); + } + + if ( post_type_supports( $post_type, 'post-formats' ) ) { + $format = get_post_format( $post->ID ); + $data['format'] = $format ? $format : 'standard'; + } + + if ( post_type_supports( $post_type, 'thumbnail' ) ) { + $data['featured_media'] = (int) get_post_thumbnail_id( $post->ID ); + } + + $template = get_page_template_slug( $post->ID ); + if ( '' !== $template && false !== $template ) { + $data['template'] = $template; + } + + $terms = array(); + foreach ( wp_list_filter( get_object_taxonomies( $post_type, 'objects' ), array( 'show_in_rest' => true ) ) as $taxonomy ) { + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + $term_ids = wp_get_object_terms( $post->ID, $taxonomy->name, array( 'fields' => 'ids' ) ); + $terms[ $base ] = is_wp_error( $term_ids ) ? array() : array_map( 'intval', $term_ids ); + } + + if ( ! empty( $terms ) ) { + $data['terms'] = $terms; + } + + return $data; +} + +/** +* Creates a post of any exposed post type from a flat set of parameters. +* +* This performs no permission check. Callers must check permission first, for +* example with {@see wp_check_create_post_permission()}. +* +* @since 7.1.0 +* +* @param string $post_type Post type name. +* @param array $params The post fields to create the post with. +* @return array|WP_Error The created post data, or WP_Error on failure. +*/ +/** +* Maps a flat set of post parameters onto an object ready for the database. +* +* Shared by the REST posts controller and by the content abilities so the mapping +* exists once. Callers are responsible for permission checks and for applying any +* transport-specific filters to the result. +* +* @since 7.1.0 +* +* @param string $post_type Post type name. +* @param array $params The post fields, keyed by public field name. +* @param array|null $schema Optional. Item schema used to gate which fields are accepted. +* When null, every field is accepted. Default null. +* @param WP_Post|null $existing_post Optional. The post being updated, when there is one. Default null. +* @return stdClass|WP_Error An object ready for wp_insert_post()/wp_update_post(), or WP_Error on failure. +*/ +function wp_prepare_post_params_for_database( $post_type, $params, $schema = null, $existing_post = null ) { + $prepared_post = new stdClass(); + $current_status = ''; + + $has_field = static function ( $field ) use ( $schema ) { + return null === $schema || ! empty( $schema['properties'][ $field ] ); + }; + + // Post ID. + if ( isset( $params['id'] ) ) { + if ( ! $existing_post instanceof WP_Post ) { + $existing_post = get_post( (int) $params['id'] ); + } + + if ( ! $existing_post instanceof WP_Post ) { + return new WP_Error( + 'rest_post_invalid_id', + __( 'Invalid post ID.' ), + array( 'status' => 404 ) + ); + } + + $prepared_post->ID = $existing_post->ID; + $current_status = $existing_post->post_status; + } + + // Post title. + if ( $has_field( 'title' ) && isset( $params['title'] ) ) { + if ( is_string( $params['title'] ) ) { + $prepared_post->post_title = $params['title']; + } elseif ( ! empty( $params['title']['raw'] ) ) { + $prepared_post->post_title = $params['title']['raw']; + } + } + + // Post content. + if ( $has_field( 'content' ) && isset( $params['content'] ) ) { + if ( is_string( $params['content'] ) ) { + $prepared_post->post_content = $params['content']; + } elseif ( isset( $params['content']['raw'] ) ) { + $prepared_post->post_content = $params['content']['raw']; + } + } + + // Post excerpt. + if ( $has_field( 'excerpt' ) && isset( $params['excerpt'] ) ) { + if ( is_string( $params['excerpt'] ) ) { + $prepared_post->post_excerpt = $params['excerpt']; + } elseif ( isset( $params['excerpt']['raw'] ) ) { + $prepared_post->post_excerpt = $params['excerpt']['raw']; + } + } + + // Post type. + if ( empty( $params['id'] ) ) { + // Creating a new post, use the requested type. + $prepared_post->post_type = $post_type; + } else { + // Updating a post, use the previous type. + $prepared_post->post_type = $existing_post->post_type; + } + + $post_type_object = get_post_type_object( $prepared_post->post_type ); + + // Post status. + if ( + $has_field( 'status' ) && + isset( $params['status'] ) && + ( ! $current_status || $current_status !== $params['status'] ) + ) { + $status = wp_check_post_status_permission( $params['status'], $post_type_object ); + + if ( is_wp_error( $status ) ) { + return $status; + } + + $prepared_post->post_status = $status; + } + + // Post date. + if ( $has_field( 'date' ) && ! empty( $params['date'] ) ) { + $current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date : false; + $date_data = rest_get_date_with_gmt( $params['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 ( $has_field( 'date_gmt' ) && ! empty( $params['date_gmt'] ) ) { + $current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date_gmt : false; + $date_data = rest_get_date_with_gmt( $params['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 ( + ( $has_field( 'date_gmt' ) && array_key_exists( 'date_gmt', $params ) && null === $params['date_gmt'] ) || + ( $has_field( 'date' ) && array_key_exists( 'date', $params ) && null === $params['date'] ) + ) { + $prepared_post->post_date_gmt = null; + $prepared_post->post_date = null; + } + + // Post slug. + if ( $has_field( 'slug' ) && isset( $params['slug'] ) ) { + $prepared_post->post_name = $params['slug']; + } + + // Author. + if ( $has_field( 'author' ) && ! empty( $params['author'] ) ) { + $post_author = (int) $params['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 ( $has_field( 'password' ) && isset( $params['password'] ) ) { + $prepared_post->post_password = $params['password']; + + if ( '' !== $params['password'] ) { + if ( $has_field( 'sticky' ) && ! empty( $params['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 ( $has_field( 'sticky' ) && ! empty( $params['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 ( $has_field( 'parent' ) && isset( $params['parent'] ) ) { + if ( 0 === (int) $params['parent'] ) { + $prepared_post->post_parent = 0; + } else { + $parent = get_post( (int) $params['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 ( $has_field( 'menu_order' ) && isset( $params['menu_order'] ) ) { + $prepared_post->menu_order = (int) $params['menu_order']; + } + + // Comment status. + if ( $has_field( 'comment_status' ) && ! empty( $params['comment_status'] ) ) { + $prepared_post->comment_status = $params['comment_status']; + } + + // Ping status. + if ( $has_field( 'ping_status' ) && ! empty( $params['ping_status'] ) ) { + $prepared_post->ping_status = $params['ping_status']; + } + + if ( $has_field( 'template' ) ) { + // Force template to null so that it is handled exclusively by the caller. + $prepared_post->page_template = null; + } + + $content_like_post_types = array( 'post', 'page', 'wp_block', 'wp_navigation' ); + + /** + * Filters which post types should have Block Hooks applied. + * + * Allows themes and plugins to add or remove post types that should + * have Block Hooks functionality enabled. + * + * @since 7.0.0 + * + * @param string[] $content_like_post_types Array of post type names that support Block Hooks. + * @param string $post_type The current post type being processed. + * @param stdClass|WP_Post $prepared_post The prepared post object. + */ + $content_like_post_types = apply_filters( 'rest_block_hooks_post_types', $content_like_post_types, $post_type, $prepared_post ); + + if ( in_array( $post_type, $content_like_post_types, true ) ) { + $prepared_post = update_ignored_hooked_blocks_postmeta( $prepared_post ); + } + + return $prepared_post; +} + +/** +* Applies the post fields that can only be set once the post row exists. +* +* @since 7.1.0 +* @access private +* +* @param string $post_type Post type name. +* @param int $post_id The post ID. +* @param array $params The post fields. +* @return true|WP_Error True on success, WP_Error on failure. +*/ +function _wp_apply_post_item_extras( $post_type, $post_id, $params ) { + if ( 'post' === $post_type && array_key_exists( 'sticky', $params ) ) { + if ( ! empty( $params['sticky'] ) ) { + stick_post( $post_id ); + } else { + unstick_post( $post_id ); + } + } + + if ( ! empty( $params['format'] ) && post_type_supports( $post_type, 'post-formats' ) ) { + set_post_format( $post_id, $params['format'] ); + } + + if ( isset( $params['featured_media'] ) && post_type_supports( $post_type, 'thumbnail' ) ) { + $thumbnail_id = (int) $params['featured_media']; + + if ( $thumbnail_id ) { + if ( ! set_post_thumbnail( $post_id, $thumbnail_id ) ) { + return new WP_Error( + 'rest_invalid_featured_media', + __( 'Invalid featured media ID.' ), + array( 'status' => 400 ) + ); + } + } else { + delete_post_thumbnail( $post_id ); + } + } + + if ( ! empty( $params['template'] ) ) { + $allowed_templates = array_keys( wp_get_theme()->get_page_templates( get_post( $post_id ) ) ); + + if ( in_array( $params['template'], $allowed_templates, true ) ) { + update_post_meta( $post_id, '_wp_page_template', $params['template'] ); + } + } + + $term_params = isset( $params['terms'] ) && is_array( $params['terms'] ) ? $params['terms'] : array(); + + $terms_update = wp_set_post_terms_from_params( $post_type, $post_id, $term_params ); + + if ( is_wp_error( $terms_update ) ) { + return $terms_update; + } + + return true; +} + +/** +* Creates a post of any exposed post type from a flat set of parameters. +* +* This performs no permission check. Callers must check permission first, for +* example with {@see wp_check_create_post_permission()}. +* +* @since 7.1.0 +* +* @param string $post_type Post type name. +* @param array $params The post fields to create the post with. +* @return array|WP_Error The created post data, or WP_Error on failure. +*/ +function wp_create_post_item( $post_type, $params ) { + $post_type_object = get_post_type_object( $post_type ); + + if ( ! $post_type_object instanceof WP_Post_Type || ! wp_is_post_type_exposed( $post_type_object ) ) { + return new WP_Error( + 'invalid_post_type', + __( 'Invalid post type.' ), + array( 'status' => 400 ) + ); + } + + unset( $params['id'] ); + + $prepared_post = wp_prepare_post_params_for_database( $post_type, $params ); + + if ( is_wp_error( $prepared_post ) ) { + return $prepared_post; + } + + if ( ! empty( $prepared_post->post_name ) + && ! empty( $prepared_post->post_status ) + && in_array( $prepared_post->post_status, array( 'draft', 'pending' ), true ) + ) { + /* + * `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. + */ + $prepared_post->post_name = wp_unique_post_slug( + $prepared_post->post_name, + 0, + 'publish', + $prepared_post->post_type, + isset( $prepared_post->post_parent ) ? $prepared_post->post_parent : 0 + ); + } + + $post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true, false ); + + if ( is_wp_error( $post_id ) ) { + $post_id->add_data( array( 'status' => 'db_insert_error' === $post_id->get_error_code() ? 500 : 400 ) ); + + return $post_id; + } + + $extras = _wp_apply_post_item_extras( $post_type, $post_id, $params ); + + if ( is_wp_error( $extras ) ) { + return $extras; + } + + wp_after_insert_post( get_post( $post_id ), false, null ); + + return wp_get_post_item_data( $post_id ); +} + + +/** +* Determines whether the current user may edit the given post. +* +* @since 7.1.0 +* +* @param WP_Post $post Post object. +* @return bool Whether the current user may edit the post. +*/ +function wp_check_edit_post_permission( $post ) { + $post_type = get_post_type_object( $post->post_type ); + + if ( ! wp_is_post_type_exposed( $post_type ) ) { + return false; + } + + return current_user_can( 'edit_post', $post->ID ); +} + +/** +* Determines whether the current user may delete the given post. +* +* @since 7.1.0 +* +* @param WP_Post $post Post object. +* @return bool Whether the current user may delete the post. +*/ +function wp_check_delete_post_permission( $post ) { + $post_type = get_post_type_object( $post->post_type ); + + if ( ! wp_is_post_type_exposed( $post_type ) ) { + return false; + } + + return current_user_can( 'delete_post', $post->ID ); +} + +/** +* Determines whether the current user may update the given post with the given parameters. +* +* @since 7.1.0 +* +* @param WP_Post $post Post object. +* @param array $params The parameters the post would be updated with. +* @return true|WP_Error True if the current user may update the post, WP_Error otherwise. +*/ +function wp_check_update_post_permission( $post, $params ) { + $post_type = get_post_type_object( $post->post_type ); + + if ( ! wp_check_edit_post_permission( $post ) ) { + return new WP_Error( + 'rest_cannot_edit', + __( 'Sorry, you are not allowed to edit this post.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + if ( ! empty( $params['author'] ) && get_current_user_id() !== $params['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) { + return new WP_Error( + 'rest_cannot_edit_others', + __( 'Sorry, you are not allowed to update posts as this user.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + if ( ! empty( $params['sticky'] ) && ! current_user_can( $post_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 ( ! wp_check_post_terms_assign_permission( $post->post_type, $params ) ) { + 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 post of any exposed post type from a flat set of parameters. +* +* This performs no permission check. Callers must check permission first, for +* example with {@see wp_check_update_post_permission()}. +* +* @since 7.1.0 +* +* @param int $post_id The ID of the post to update. +* @param array $params The post fields to update. +* @return array|WP_Error The updated post data, or WP_Error on failure. +*/ +function wp_update_post_item( $post_id, $params ) { + $post_before = get_post( (int) $post_id ); + + if ( ! $post_before instanceof WP_Post || ! wp_is_post_type_exposed( $post_before->post_type ) ) { + return new WP_Error( + 'invalid_item', + __( 'Invalid item ID.' ), + array( 'status' => 404 ) + ); + } + + $post_type = $post_before->post_type; + $params['id'] = $post_before->ID; + + $prepared_post = wp_prepare_post_params_for_database( $post_type, $params, null, $post_before ); + + if ( is_wp_error( $prepared_post ) ) { + return $prepared_post; + } + + $updated_id = wp_update_post( wp_slash( (array) $prepared_post ), true, false ); + + if ( is_wp_error( $updated_id ) ) { + $updated_id->add_data( array( 'status' => 'db_update_error' === $updated_id->get_error_code() ? 500 : 400 ) ); + + return $updated_id; + } + + $extras = _wp_apply_post_item_extras( $post_type, $updated_id, $params ); + + if ( is_wp_error( $extras ) ) { + return $extras; + } + + wp_after_insert_post( get_post( $updated_id ), true, $post_before ); + + return wp_get_post_item_data( $updated_id ); +} + +/** +* Trashes or permanently deletes a post of any exposed post type. +* +* This performs no permission check. Callers must check permission first, for +* example with {@see wp_check_delete_post_permission()}. +* +* @since 7.1.0 +* +* @param int $post_id The ID of the post to delete. +* @param bool $force Optional. Whether to bypass the Trash and delete permanently. Default false. +* @return array|WP_Error An array with `deleted` and `previous` keys, or WP_Error on failure. +*/ +function wp_delete_post_item( $post_id, $force = false ) { + $post = get_post( (int) $post_id ); + + if ( ! $post instanceof WP_Post || ! wp_is_post_type_exposed( $post->post_type ) ) { + return new WP_Error( + 'invalid_item', + __( 'Invalid item ID.' ), + array( 'status' => 404 ) + ); + } + + $supports_trash = ( EMPTY_TRASH_DAYS > 0 ); + + if ( 'attachment' === $post->post_type ) { + $supports_trash = $supports_trash && MEDIA_TRASH; + } + + /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ + $supports_trash = apply_filters( "rest_{$post->post_type}_trashable", $supports_trash, $post ); + + if ( $force ) { + $previous = wp_get_post_item_data( $post ); + + if ( ! wp_delete_post( $post->ID, true ) ) { + return new WP_Error( + 'rest_cannot_delete', + __( 'The post cannot be deleted.' ), + array( 'status' => 500 ) + ); + } + + return array( + 'deleted' => true, + 'previous' => $previous, + ); + } + + 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 ) + ); + } + + if ( 'trash' === $post->post_status ) { + return new WP_Error( + 'rest_already_trashed', + __( 'The post has already been deleted.' ), + array( 'status' => 410 ) + ); + } + + if ( ! wp_trash_post( $post->ID ) ) { + return new WP_Error( + 'rest_cannot_delete', + __( 'The post cannot be deleted.' ), + array( 'status' => 500 ) + ); + } + + return array( + 'deleted' => true, + 'previous' => wp_get_post_item_data( $post->ID ), + ); +} + /** * Sets categories for a post. * diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index ee3e6b4959869..06ef0255359ab 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -685,49 +685,7 @@ public function get_item( $request ) { * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise. */ public function create_item_permissions_check( $request ) { - if ( ! empty( $request['id'] ) ) { - return new WP_Error( - 'rest_post_exists', - __( 'Cannot create existing post.' ), - array( 'status' => 400 ) - ); - } - - $post_type = get_post_type_object( $this->post_type ); - - if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) { - return new WP_Error( - 'rest_cannot_edit_others', - __( 'Sorry, you are not allowed to create posts as this user.' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_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 ( ! current_user_can( $post_type->cap->create_posts ) ) { - return new WP_Error( - 'rest_cannot_create', - __( 'Sorry, you are not allowed to create posts as this user.' ), - 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; + return wp_check_create_post_permission( $this->post_type, $request->get_params() ); } /** @@ -892,45 +850,12 @@ public function create_item( $request ) { */ public function update_item_permissions_check( $request ) { $post = $this->get_post( $request['id'] ); + if ( is_wp_error( $post ) ) { return $post; } - $post_type = get_post_type_object( $this->post_type ); - - if ( $post && ! $this->check_update_permission( $post ) ) { - return new WP_Error( - 'rest_cannot_edit', - __( 'Sorry, you are not allowed to edit this post.' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) { - return new WP_Error( - 'rest_cannot_edit_others', - __( 'Sorry, you are not allowed to update posts as this user.' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_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; + return wp_check_update_post_permission( $post, $request->get_params() ); } /** @@ -1286,248 +1211,44 @@ protected function prepare_date_response( $date_gmt, $date = null ) { * @return stdClass|WP_Error Post object or WP_Error. */ protected function prepare_item_for_database( $request ) { - $prepared_post = new stdClass(); - $current_status = ''; + $existing_post = null; - // 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; - } - - /** - * Applies Block Hooks to content-like post types. - * - * Content-like post types are those that support the editor and would benefit - * from Block Hooks functionality. This replaces the individual post type filters - * that were previously hardcoded in default-filters.php. - * - * @since 7.0.0 - */ - $content_like_post_types = array( 'post', 'page', 'wp_block', 'wp_navigation' ); - - /** - * Filters which post types should have Block Hooks applied. - * - * Allows themes and plugins to add or remove post types that should - * have Block Hooks functionality enabled in the REST API. - * - * @since 7.0.0 - * - * @param string[] $content_like_post_types Array of post type names that support Block Hooks. - * @param string $post_type The current post type being processed. - * @param stdClass|WP_Post $prepared_post The prepared post object. - */ - $content_like_post_types = apply_filters( 'rest_block_hooks_post_types', $content_like_post_types, $this->post_type, $prepared_post ); + $prepared_post = wp_prepare_post_params_for_database( + $this->post_type, + $request->get_params(), + $this->get_item_schema(), + $existing_post + ); - if ( in_array( $this->post_type, $content_like_post_types, true ) ) { - $prepared_post = update_ignored_hooked_blocks_postmeta( $prepared_post ); + if ( is_wp_error( $prepared_post ) ) { + return $prepared_post; } /** - * 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. - */ + * 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 ); } @@ -1567,38 +1288,7 @@ public function check_status( $status, $request, $param ) { * @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; + return wp_check_post_status_permission( $post_status, $post_type ); } /** @@ -1700,23 +1390,7 @@ public function handle_template( $template, $post_id, $validate = false ) { * @return null|WP_Error WP_Error on an error assigning any of the terms, otherwise null. */ protected function handle_terms( $post_id, $request ) { - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); - - foreach ( $taxonomies as $taxonomy ) { - $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - - if ( ! isset( $request[ $base ] ) ) { - continue; - } - - $result = wp_set_object_terms( $post_id, $request[ $base ], $taxonomy->name ); - - if ( is_wp_error( $result ) ) { - return $result; - } - } - - return null; + return wp_set_post_terms_from_params( $this->post_type, $post_id, $request->get_params() ); } /** @@ -1728,27 +1402,7 @@ protected function handle_terms( $post_id, $request ) { * @return bool Whether the current user can assign the provided terms. */ protected function check_assign_terms_permission( $request ) { - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); - foreach ( $taxonomies as $taxonomy ) { - $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - - if ( ! isset( $request[ $base ] ) ) { - continue; - } - - foreach ( (array) $request[ $base ] as $term_id ) { - // Invalid terms will be rejected later. - if ( ! get_term( $term_id, $taxonomy->name ) ) { - continue; - } - - if ( ! current_user_can( 'assign_term', (int) $term_id ) ) { - return false; - } - } - } - - return true; + return wp_check_post_terms_assign_permission( $this->post_type, $request->get_params() ); } /** @@ -1760,15 +1414,7 @@ protected function check_assign_terms_permission( $request ) { * @return bool Whether the post type is allowed in REST. */ protected function check_is_post_type_allowed( $post_type ) { - if ( ! is_object( $post_type ) ) { - $post_type = get_post_type_object( $post_type ); - } - - if ( ! empty( $post_type ) && ! empty( $post_type->show_in_rest ) ) { - return true; - } - - return false; + return wp_is_post_type_exposed( $post_type ); } /** @@ -1782,38 +1428,7 @@ protected function check_is_post_type_allowed( $post_type ) { * @return bool Whether the post can be read. */ public function check_read_permission( $post ) { - $post_type = get_post_type_object( $post->post_type ); - if ( ! $this->check_is_post_type_allowed( $post_type ) ) { - return false; - } - - // Is the post readable? - if ( 'publish' === $post->post_status || current_user_can( 'read_post', $post->ID ) ) { - return true; - } - - $post_status_obj = get_post_status_object( $post->post_status ); - if ( $post_status_obj && $post_status_obj->public ) { - return true; - } - - // Can we read the parent if we're inheriting? - if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) { - $parent = get_post( $post->post_parent ); - if ( $parent ) { - return $this->check_read_permission( $parent ); - } - } - - /* - * If there isn't a parent, but the status is set to inherit, assume - * it's published (as per get_post_status()). - */ - if ( 'inherit' === $post->post_status ) { - return true; - } - - return false; + return wp_check_read_post_permission( $post ); } /** @@ -1825,13 +1440,7 @@ public function check_read_permission( $post ) { * @return bool Whether the post can be edited. */ protected function check_update_permission( $post ) { - $post_type = get_post_type_object( $post->post_type ); - - if ( ! $this->check_is_post_type_allowed( $post_type ) ) { - return false; - } - - return current_user_can( 'edit_post', $post->ID ); + return wp_check_edit_post_permission( $post ); } /** @@ -1861,13 +1470,7 @@ protected function check_create_permission( $post ) { * @return bool Whether the post can be deleted. */ protected function check_delete_permission( $post ) { - $post_type = get_post_type_object( $post->post_type ); - - if ( ! $this->check_is_post_type_allowed( $post_type ) ) { - return false; - } - - return current_user_can( 'delete_post', $post->ID ); + return wp_check_delete_post_permission( $post ); } /** diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php index 142836c7c8921..110fab8679a5b 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php @@ -65,7 +65,7 @@ public function register_routes() { * @return bool True if the request has read access for the item, otherwise false. */ public function get_item_permissions_check( $request ) { - return current_user_can( 'manage_options' ); + return wp_current_user_can_manage_settings(); } /** @@ -77,39 +77,7 @@ public function get_item_permissions_check( $request ) { * @return array|WP_Error Array on success, or WP_Error object on failure. */ public function get_item( $request ) { - $options = $this->get_registered_options(); - $response = array(); - - foreach ( $options as $name => $args ) { - /** - * Filters the value of a setting recognized by the REST API. - * - * Allow hijacking the setting value and overriding the built-in behavior by returning a - * non-null value. The returned value will be presented as the setting value instead. - * - * @since 4.7.0 - * - * @param mixed $result Value to use for the requested setting. Can be a scalar - * matching the registered schema for the setting, or null to - * follow the default get_option() behavior. - * @param string $name Setting name (as shown in REST API responses). - * @param array $args Arguments passed to register_setting() for this setting. - */ - $response[ $name ] = apply_filters( 'rest_pre_get_setting', null, $name, $args ); - - if ( is_null( $response[ $name ] ) ) { - // Default to a null value as "null" in the response means "not set". - $response[ $name ] = get_option( $args['option_name'], $args['schema']['default'] ); - } - - /* - * Because get_option() is lossy, we have to - * cast values to the type they are registered with. - */ - $response[ $name ] = $this->prepare_value( $response[ $name ], $args['schema'] ); - } - - return $response; + return wp_get_settings_values(); } /** @@ -122,16 +90,7 @@ public function get_item( $request ) { * @return mixed The prepared value. */ protected function prepare_value( $value, $schema ) { - /* - * If the value is not valid by the schema, set the value to null. - * Null values are specifically non-destructive, so this will not cause - * overwriting the current invalid value to null. - */ - if ( is_wp_error( rest_validate_value_from_schema( $value, $schema ) ) ) { - return null; - } - - return rest_sanitize_value_from_schema( $value, $schema ); + return wp_prepare_setting_value( $value, $schema ); } /** @@ -143,68 +102,7 @@ protected function prepare_value( $value, $schema ) { * @return array|WP_Error Array on success, or error object on failure. */ public function update_item( $request ) { - $options = $this->get_registered_options(); - - $params = $request->get_params(); - - foreach ( $options as $name => $args ) { - if ( ! array_key_exists( $name, $params ) ) { - continue; - } - - /** - * Filters whether to preempt a setting value update via the REST API. - * - * Allows hijacking the setting update logic and overriding the built-in behavior by - * returning true. - * - * @since 4.7.0 - * - * @param bool $result Whether to override the default behavior for updating the - * value of a setting. - * @param string $name Setting name (as shown in REST API responses). - * @param mixed $value Updated setting value. - * @param array $args Arguments passed to register_setting() for this setting. - */ - $updated = apply_filters( 'rest_pre_update_setting', false, $name, $request[ $name ], $args ); - - if ( $updated ) { - continue; - } - - /* - * A null value for an option would have the same effect as - * deleting the option from the database, and relying on the - * default value. - */ - if ( is_null( $request[ $name ] ) ) { - /* - * A null value is returned in the response for any option - * that has a non-scalar value. - * - * To protect clients from accidentally including the null - * values from a response object in a request, we do not allow - * options with values that don't pass validation to be updated to null. - * Without this added protection a client could mistakenly - * delete all options that have invalid values from the - * database. - */ - if ( is_wp_error( rest_validate_value_from_schema( get_option( $args['option_name'], false ), $args['schema'] ) ) ) { - return new WP_Error( - 'rest_invalid_stored_value', - /* translators: %s: Property name. */ - sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), - array( 'status' => 500 ) - ); - } - - delete_option( $args['option_name'] ); - } else { - update_option( $args['option_name'], $request[ $name ] ); - } - } - - return $this->get_item( $request ); + return wp_update_settings_values( $request->get_params() ); } /** @@ -215,55 +113,7 @@ public function update_item( $request ) { * @return array Array of registered options. */ protected function get_registered_options() { - $rest_options = array(); - - foreach ( get_registered_settings() as $name => $args ) { - if ( empty( $args['show_in_rest'] ) ) { - continue; - } - - $rest_args = array(); - - if ( is_array( $args['show_in_rest'] ) ) { - $rest_args = $args['show_in_rest']; - } - - $defaults = array( - 'name' => ! empty( $rest_args['name'] ) ? $rest_args['name'] : $name, - 'schema' => array(), - ); - - $rest_args = array_merge( $defaults, $rest_args ); - - $default_schema = array( - 'type' => empty( $args['type'] ) ? null : $args['type'], - 'title' => empty( $args['label'] ) ? '' : $args['label'], - 'description' => empty( $args['description'] ) ? '' : $args['description'], - 'default' => $args['default'] ?? null, - ); - - $rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] ); - $rest_args['option_name'] = $name; - - // Skip over settings that don't have a defined type in the schema. - if ( empty( $rest_args['schema']['type'] ) ) { - continue; - } - - /* - * Allow the supported types for settings, as we don't want invalid types - * to be updated with arbitrary values that we can't do decent sanitizing for. - */ - if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'integer', 'string', 'boolean', 'array', 'object' ), true ) ) { - continue; - } - - $rest_args['schema'] = rest_default_additional_properties_to_false( $rest_args['schema'] ); - - $rest_options[ $rest_args['name'] ] = $rest_args; - } - - return $rest_options; + return wp_get_registered_setting_options(); } /**