-
Notifications
You must be signed in to change notification settings - Fork 156
Add a core/manage-settings ability #764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
9f64920
162d8c4
6413203
68d5383
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,10 +17,10 @@ | |||||||
| /** | ||||||||
| * Class - Settings | ||||||||
| * | ||||||||
| * Registers the read-only `core/settings` ability, which returns WordPress settings as a | ||||||||
| * flat map of setting name to value. Only settings flagged with `show_in_abilities` are | ||||||||
| * exposed. It is structured to also back a future write-oriented `core/manage-settings` | ||||||||
| * ability via the shared helpers (get_exposed_settings(), value_schema(), cast_value()). | ||||||||
| * Registers the read-only `core/settings` ability and the write-oriented `core/manage-settings` | ||||||||
| * ability. Both operate on the same settings — those flagged with `show_in_abilities` — as a flat | ||||||||
| * map of setting name to value: `core/settings` reads them and `core/manage-settings` updates them, | ||||||||
| * sharing the helpers get_exposed_settings(), value_schema() and cast_value(). | ||||||||
| * | ||||||||
| * This class is kept almost identical to the WordPress core class `WP_Settings_Abilities` | ||||||||
| * so the two implementations stay in sync. Differences from the core class are marked with | ||||||||
|
|
@@ -74,13 +74,7 @@ public function init(): void { | |||||||
| */ | ||||||||
| public function register(): void { | ||||||||
| $this->register_get_settings(); | ||||||||
|
|
||||||||
| /* | ||||||||
| * A future write-oriented ability can be registered here, reusing the shared | ||||||||
| * helpers below (get_exposed_settings(), value_schema(), cast_value()): | ||||||||
| * | ||||||||
| * $this->register_manage_settings(); | ||||||||
| */ | ||||||||
| $this->register_manage_settings(); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
|
|
@@ -136,6 +130,62 @@ private function register_get_settings(): void { | |||||||
| ); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Registers the write-oriented `core/manage-settings` ability. | ||||||||
| * | ||||||||
| * The input and output schemas reuse each exposed setting's own schema, so every setting | ||||||||
| * readable via `core/settings` is also writable through this ability. | ||||||||
| * | ||||||||
| * Plugin: core reserves this ability in WP_Settings_Abilities::register() but does not yet | ||||||||
| * implement it; the plugin ships it here, reusing the snapshot register_get_settings() computed. | ||||||||
| * | ||||||||
| * @since x.x.x | ||||||||
| */ | ||||||||
| private function register_manage_settings(): void { | ||||||||
| // Plugin: unregister any core-provided copy first so the plugin's version wins. | ||||||||
| if ( wp_has_ability( 'core/manage-settings' ) ) { | ||||||||
| wp_unregister_ability( 'core/manage-settings' ); | ||||||||
| } | ||||||||
|
|
||||||||
| $settings = (array) $this->exposed_settings; | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit, if there are no exposed settings for some reason, then maybe this ability should never get registered. |
||||||||
| $properties = array(); | ||||||||
| foreach ( $settings as $exposed_name => $setting ) { | ||||||||
| $properties[ $exposed_name ] = $setting['schema']; | ||||||||
| } | ||||||||
|
|
||||||||
| wp_register_ability( | ||||||||
| 'core/manage-settings', | ||||||||
| array( | ||||||||
| 'label' => __( 'Manage Settings', 'ai' ), | ||||||||
| 'description' => __( 'Updates one or more WordPress settings exposed to abilities. Accepts a map of setting name to its new value and returns the updated values.', 'ai' ), | ||||||||
| 'category' => self::CATEGORY, | ||||||||
| 'input_schema' => array( | ||||||||
| 'type' => 'object', | ||||||||
| 'description' => __( 'A map of setting name to the new value to store. At least one setting is required.', 'ai' ), | ||||||||
| 'properties' => $properties, | ||||||||
| 'minProperties' => 1, | ||||||||
| 'additionalProperties' => false, | ||||||||
| ), | ||||||||
| 'output_schema' => array( | ||||||||
| 'type' => 'object', | ||||||||
| 'description' => __( 'A map of each updated setting name to its new value.', 'ai' ), | ||||||||
| 'properties' => $properties, | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit, for symmetry, it could use the same schema:
Suggested change
|
||||||||
| 'additionalProperties' => false, | ||||||||
| ), | ||||||||
| 'execute_callback' => array( $this, 'execute_manage_settings' ), | ||||||||
| 'permission_callback' => array( $this, 'has_permission' ), | ||||||||
| 'meta' => array( | ||||||||
| 'annotations' => array( | ||||||||
| 'readonly' => false, | ||||||||
| 'destructive' => false, | ||||||||
| 'idempotent' => true, | ||||||||
| ), | ||||||||
| 'show_in_rest' => true, | ||||||||
| ), | ||||||||
| ) | ||||||||
| ); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Executes the `core/settings` ability. | ||||||||
| * | ||||||||
|
|
@@ -175,6 +225,53 @@ public function execute_get_settings( $input = array() ): array { | |||||||
| return $result; | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Executes the `core/manage-settings` ability. | ||||||||
| * | ||||||||
| * The Abilities API validates the input against the registered input schema (each setting's own | ||||||||
| * value schema, with `additionalProperties` disabled) before this runs, so every value reaching | ||||||||
| * here is known and valid; an invalid value aborts the call before any option is written. Each | ||||||||
| * value is sanitized against its schema and stored, then read back and cast for the response. | ||||||||
| * | ||||||||
| * @since x.x.x | ||||||||
| * | ||||||||
| * @param mixed $input The ability input: a map of exposed setting name to its new value. | ||||||||
| * @return array<string, mixed> Map of each updated setting name to its stored value. | ||||||||
| */ | ||||||||
| public function execute_manage_settings( $input = array() ): array { | ||||||||
| $input = is_array( $input ) ? $input : array(); | ||||||||
|
|
||||||||
| $settings = $this->exposed_settings; | ||||||||
| if ( null === $settings ) { | ||||||||
| // The cache is populated in register_get_settings() before the ability is | ||||||||
| // registered, so this is unreachable in practice; bail defensively otherwise. | ||||||||
| return array(); | ||||||||
| } | ||||||||
|
|
||||||||
| $result = array(); | ||||||||
| foreach ( $input as $exposed_name => $value ) { | ||||||||
| if ( ! is_string( $exposed_name ) || ! isset( $settings[ $exposed_name ] ) ) { | ||||||||
| // `additionalProperties: false` already rejects unknown keys upstream; guard defensively. | ||||||||
| continue; | ||||||||
| } | ||||||||
|
|
||||||||
| $setting = $settings[ $exposed_name ]; | ||||||||
|
|
||||||||
| // Sanitize against the declared schema before storing; update_option() additionally | ||||||||
| // runs the setting's own registered sanitize_callback. | ||||||||
| $value = rest_sanitize_value_from_schema( $value, $setting['schema'], $exposed_name ); | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious why sanitization is necessary here. Is it applied automatically for the REST API endpoint? I don't see explicit calls in the update method: There is also a special case where passing |
||||||||
|
|
||||||||
| update_option( $setting['option'], $value ); | ||||||||
|
|
||||||||
| $type = isset( $setting['schema']['type'] ) && is_string( $setting['schema']['type'] ) ? $setting['schema']['type'] : 'string'; | ||||||||
| $stored = get_option( $setting['option'], $setting['default'] ); | ||||||||
|
|
||||||||
| $result[ $exposed_name ] = $this->cast_value( $stored, $type ); | ||||||||
| } | ||||||||
|
|
||||||||
| return $result; | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Checks whether the current user may use the settings abilities. | ||||||||
| * | ||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe
ai/includes/Abilities/Settings/Settings.php
Lines 91 to 92 in 68d5383
should run first in
register()and bail out if there are no settings exposed.