From 19e3266e39918463f55674e2b128d5fcd20ee2e6 Mon Sep 17 00:00:00 2001 From: Ovidiu Galatan Date: Thu, 13 Aug 2026 14:36:09 +0300 Subject: [PATCH] REST API: Extract settings and post operations into functions shared with abilities. Move the settings and post write operations out of their REST controllers into plain functions in `option.php` and `post.php`, so that the REST API and the Abilities API can both consume them without either depending on the other. Fifteen controller methods now delegate to twenty functions. No method was removed or renamed, every signature and return contract is unchanged, and every filter still fires from its original layer. Two constraints shaped the approach: - A method can only be extracted if its extension points do not receive `WP_REST_Request` and it is not a subclass override point. All six core subclasses of `WP_REST_Posts_Controller` override `prepare_item_for_database()`, so the base method body calls the shared function while the method itself stays in place. Overrides and `parent::` calls keep working. - Controllers do not call abilities. `wp_unregister_ability()` is public API, so a controller that dispatched through an ability could be disabled by a plugin unregistering it. Six abilities are registered over the same functions. The four content abilities take `post_type` as an argument rather than being registered per post type. This is an experiment opened for discussion. It adds no test coverage of its own. --- src/wp-includes/abilities.php | 353 +++++++ src/wp-includes/option.php | 215 +++++ src/wp-includes/post.php | 904 ++++++++++++++++++ .../class-wp-rest-posts-controller.php | 469 +-------- .../class-wp-rest-settings-controller.php | 160 +--- 5 files changed, 1513 insertions(+), 588 deletions(-) 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(); } /**