-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Add: WordPress Core update settings ability #10892
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: trunk
Are you sure you want to change the base?
Changes from all commits
5fb3965
fc9b98f
daebd4b
9051fa5
a808c85
296e440
bcb067e
d95b0c9
ad7ec2a
bd89f7b
5f4e5ba
93a5238
7080743
022e463
4ed8138
00fc880
bfd643a
af4497e
f725789
b29022b
e9f11bb
102331b
d87cbfa
bcda1e8
0758e59
ca3b47e
1bd5e10
8ee8c38
c257425
f58ccf6
ec9627e
13c9e86
d129749
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 |
|---|---|---|
|
|
@@ -29,23 +29,23 @@ class WP_Settings_Abilities { | |
| * @since 7.0.0 | ||
| * @var string[] | ||
| */ | ||
| private static $available_groups; | ||
| private static array $available_groups; | ||
|
|
||
| /** | ||
| * Dynamic output schema built from registered settings. | ||
| * Schema for settings grouped by registration group. | ||
| * | ||
| * @since 7.0.0 | ||
| * @var array | ||
| * @var array<string, mixed> | ||
| */ | ||
| private static $output_schema; | ||
| private static array $settings_schema; | ||
|
|
||
| /** | ||
| * Available setting slugs with show_in_abilities enabled. | ||
| * | ||
| * @since 7.0.0 | ||
| * @var string[] | ||
| */ | ||
| private static $available_slugs; | ||
| private static array $available_slugs; | ||
|
|
||
| /** | ||
| * Registers all settings abilities. | ||
|
|
@@ -57,6 +57,7 @@ class WP_Settings_Abilities { | |
| public static function register(): void { | ||
| self::init(); | ||
| self::register_get_settings(); | ||
| self::register_update_settings(); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -69,7 +70,7 @@ public static function register(): void { | |
| private static function init(): void { | ||
| self::$available_groups = self::get_available_groups(); | ||
| self::$available_slugs = self::get_available_slugs(); | ||
| self::$output_schema = self::build_output_schema(); | ||
| self::$settings_schema = self::build_settings_schema(); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -121,29 +122,23 @@ private static function get_available_groups(): array { | |
| * @return string[] List of unique setting slugs. | ||
| */ | ||
| private static function get_available_slugs(): array { | ||
| $slugs = array(); | ||
|
|
||
| foreach ( self::get_allowed_settings() as $option_name => $args ) { | ||
| $slugs[] = $option_name; | ||
| } | ||
|
|
||
| $slugs = array_keys( self::get_allowed_settings() ); | ||
| sort( $slugs ); | ||
|
|
||
| return $slugs; | ||
| } | ||
|
|
||
| /** | ||
| * Builds a rich output schema from registered settings metadata. | ||
| * Builds a schema for settings grouped by registration group. | ||
| * | ||
| * Creates a JSON Schema that documents each setting group and its settings | ||
| * with their types, titles, descriptions, defaults, and any additional | ||
| * schema properties from show_in_rest. | ||
| * schema properties from show_in_abilities. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @return array JSON Schema for the output. | ||
| * @return array<string, mixed> JSON Schema for settings. | ||
| */ | ||
| private static function build_output_schema(): array { | ||
| private static function build_settings_schema(): array { | ||
| $group_properties = array(); | ||
|
|
||
| foreach ( self::get_allowed_settings() as $option_name => $args ) { | ||
|
|
@@ -163,6 +158,11 @@ private static function build_output_schema(): array { | |
| $setting_schema['description'] = $args['label']; | ||
| } | ||
|
|
||
| // Merge custom schema from show_in_abilities if provided as an array. | ||
| if ( is_array( $args['show_in_abilities'] ) && ! empty( $args['show_in_abilities']['schema'] ) ) { | ||
| $setting_schema = array_merge( $setting_schema, $args['show_in_abilities']['schema'] ); | ||
| } | ||
|
|
||
| if ( ! isset( $group_properties[ $group ] ) ) { | ||
| $group_properties[ $group ] = array( | ||
| 'type' => 'object', | ||
|
|
@@ -238,7 +238,7 @@ private static function register_get_settings(): void { | |
| ), | ||
| ), | ||
| ), | ||
| 'output_schema' => self::$output_schema, | ||
| 'output_schema' => self::$settings_schema, | ||
| 'execute_callback' => array( __CLASS__, 'execute_get_settings' ), | ||
| 'permission_callback' => array( __CLASS__, 'check_manage_options' ), | ||
| 'meta' => array( | ||
|
|
@@ -253,6 +253,54 @@ private static function register_get_settings(): void { | |
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Registers the core/update-settings ability. | ||
| * | ||
| * @since 7.0.0 | ||
| */ | ||
| private static function register_update_settings(): void { | ||
| // Reuse settings schema with updated descriptions for input and output. | ||
| $input_settings_schema = self::$settings_schema; | ||
| $input_settings_schema['description'] = __( 'Settings to update, grouped by registration group. Same structure as returned by core/get-settings.' ); | ||
|
|
||
| $output_settings_schema = self::$settings_schema; | ||
| $output_settings_schema['description'] = __( 'Settings that were successfully updated, grouped by registration group.' ); | ||
|
|
||
| wp_register_ability( | ||
| 'core/update-settings', | ||
| array( | ||
| 'label' => __( 'Update Settings' ), | ||
| 'description' => __( 'Updates registered WordPress settings. Only settings with show_in_abilities enabled can be modified.' ), | ||
| 'category' => 'site', | ||
| 'input_schema' => array( | ||
| 'type' => 'object', | ||
| 'required' => array( 'settings' ), | ||
| 'properties' => array( | ||
| 'settings' => $input_settings_schema, | ||
| ), | ||
| 'additionalProperties' => false, | ||
| ), | ||
| 'output_schema' => array( | ||
| 'type' => 'object', | ||
| 'properties' => array( | ||
| 'updated_settings' => $output_settings_schema, | ||
| ), | ||
| 'additionalProperties' => false, | ||
| ), | ||
| 'execute_callback' => array( __CLASS__, 'execute_update_settings' ), | ||
| 'permission_callback' => array( __CLASS__, 'check_manage_options' ), | ||
| 'meta' => array( | ||
| 'annotations' => array( | ||
| 'readonly' => false, | ||
| 'destructive' => false, | ||
| 'idempotent' => true, | ||
| ), | ||
| 'show_in_rest' => true, | ||
| ), | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Permission callback for settings abilities. | ||
| * | ||
|
|
@@ -315,6 +363,100 @@ public static function execute_get_settings( $input = array() ): array { | |
| return $settings_by_group; | ||
| } | ||
|
|
||
| /** | ||
| * Execute callback for core/update-settings ability. | ||
| * | ||
| * Updates registered settings that are exposed through the Abilities API. | ||
| * Returns updated settings grouped by registration group. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param array<string, mixed> $input { | ||
| * Input parameters. | ||
| * | ||
| * @type array $settings Settings to update, grouped by registration group. | ||
| * } | ||
| * @return array<string, array<string, mixed>|object>|WP_Error Updated settings on success, WP_Error on failure. | ||
| */ | ||
| public static function execute_update_settings( $input = array() ): array { | ||
| $input = is_array( $input ) ? $input : array(); | ||
|
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. The |
||
|
|
||
| if ( empty( $input['settings'] ) || ! is_array( $input['settings'] ) ) { | ||
| return array( | ||
| 'updated_settings' => (object) array(), | ||
|
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. Why are we coercing an empty array into an object?
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. To force the JSON encoding to be 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. Sorry I should have been clear I'm asking from an API perspective not the technical impl. I should have asked: why feed it an empty anything (I only saw that resolved thread afterwards 😅) If there's isn't a strong reason, I'd suggest we just make
(I understand none of these are big concerns individually, but papercuts add up in api design, why I'm asking if there's any downsides to consider) |
||
| ); | ||
| } | ||
|
|
||
| $grouped_settings = $input['settings']; | ||
| $allowed_settings = self::get_allowed_settings(); | ||
|
|
||
| $updated_settings = array(); | ||
|
|
||
| // Iterate through groups (general, reading, writing, etc.). | ||
| foreach ( $grouped_settings as $group => $settings ) { | ||
| if ( ! is_array( $settings ) ) { | ||
| continue; | ||
| } | ||
|
|
||
| // Iterate through settings within each group. | ||
| foreach ( $settings as $option_name => $value ) { | ||
| if ( ! isset( $allowed_settings[ $option_name ] ) ) { | ||
| continue; | ||
| } | ||
|
|
||
| $args = $allowed_settings[ $option_name ]; | ||
|
|
||
| $setting_group = $args['group'] ?? 'general'; | ||
|
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. Is the concern her just that |
||
| if ( $setting_group !== $group ) { | ||
| continue; | ||
| } | ||
|
|
||
| $setting_type = $args['type'] ?? 'string'; | ||
|
|
||
| $schema = array( | ||
| 'type' => $setting_type, | ||
| ); | ||
| if ( is_array( $args['show_in_rest'] ) && isset( $args['show_in_rest']['schema'] ) ) { | ||
| $schema = array_merge( $schema, $args['show_in_rest']['schema'] ); | ||
| } | ||
|
|
||
| $sanitized_value = rest_sanitize_value_from_schema( $value, $schema ); | ||
|
|
||
| if ( isset( $args['sanitize_callback'] ) && is_callable( $args['sanitize_callback'] ) ) { | ||
| $sanitized_value = call_user_func( $args['sanitize_callback'], $sanitized_value ); | ||
| } | ||
|
|
||
| $updated = update_option( $option_name, $sanitized_value ); | ||
|
|
||
| // Cast values for comparison (handles type mismatches from database and REST sanitization). | ||
| $current_value = self::cast_value( get_option( $option_name ), $setting_type ); | ||
| $sanitized_value = self::cast_value( $sanitized_value, $setting_type ); | ||
|
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. Why do we need to cast |
||
|
|
||
| if ( ! $updated && $current_value !== $sanitized_value ) { | ||
| return new WP_Error( | ||
| 'rest_setting_update_failed', | ||
| sprintf( | ||
| /* translators: %s: Option name. */ | ||
| __( 'Failed to update setting: %s.' ), | ||
| $option_name | ||
| ), | ||
| array( 'status' => 500 ) | ||
| ); | ||
| } | ||
|
|
||
| if ( ! isset( $updated_settings[ $group ] ) ) { | ||
| $updated_settings[ $group ] = array(); | ||
| } | ||
|
|
||
| $updated_settings[ $group ][ $option_name ] = $current_value; | ||
| } | ||
jorgefilipecosta marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| return array( | ||
| 'updated_settings' => ! empty( $updated_settings ) ? $updated_settings : (object) array(), | ||
jorgefilipecosta marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Casts a value to the appropriate type based on the setting's registered type. | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| <?php | ||
| /** | ||
| * Auto-register block support. | ||
| * | ||
| * @package WordPress | ||
| * @since 7.0.0 | ||
| */ | ||
|
|
||
| /** | ||
| * Marks user-defined attributes for auto-generated inspector controls. | ||
| * | ||
| * This filter runs during block type registration, before the WP_Block_Type | ||
| * is instantiated. Block supports add their attributes AFTER the block type | ||
| * is created (via {@see WP_Block_Supports::register_attributes()}), so any attributes | ||
| * present at this stage are user-defined. | ||
| * | ||
| * The marker tells generateFieldsFromAttributes() which attributes should | ||
| * get auto-generated inspector controls. Attributes are excluded if they: | ||
| * - Have a 'source' (HTML-derived, edited inline not via inspector) | ||
| * - Have role 'local' (internal state, not user-configurable) | ||
| * - Have an unsupported type (only 'string', 'number', 'integer', 'boolean' are supported) | ||
| * - Were added by block supports (added after this filter runs) | ||
| * | ||
| * @since 7.0.0 | ||
| * @access private | ||
| * | ||
| * @param array<string, mixed> $args Array of arguments for registering a block type. | ||
| * @return array<string, mixed> Modified block type arguments. | ||
| */ | ||
| function wp_mark_auto_generate_control_attributes( array $args ): array { | ||
| if ( empty( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) { | ||
| return $args; | ||
| } | ||
|
|
||
| $has_auto_register = ! empty( $args['supports']['autoRegister'] ); | ||
| if ( ! $has_auto_register ) { | ||
| return $args; | ||
| } | ||
|
|
||
| foreach ( $args['attributes'] as $attr_key => $attr_schema ) { | ||
| // Skip HTML-derived attributes (edited inline, not via inspector). | ||
| if ( ! empty( $attr_schema['source'] ) ) { | ||
| continue; | ||
| } | ||
| // Skip internal attributes (not user-configurable). | ||
| if ( isset( $attr_schema['role'] ) && 'local' === $attr_schema['role'] ) { | ||
| continue; | ||
| } | ||
| // Skip unsupported types (only 'string', 'number', 'integer', 'boolean' are supported). | ||
| $type = $attr_schema['type'] ?? null; | ||
| if ( ! in_array( $type, array( 'string', 'number', 'integer', 'boolean' ), true ) ) { | ||
| continue; | ||
| } | ||
| $args['attributes'][ $attr_key ]['autoGenerateControl'] = true; | ||
| } | ||
|
|
||
| return $args; | ||
| } | ||
|
|
||
| // Priority 5 to mark original attributes before other filters (priority 10+) might add their own. | ||
| add_filter( 'register_block_type_args', 'wp_mark_auto_generate_control_attributes', 5 ); |
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.
arrayfrom the method signature.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.
Also, why is this entire class (including the properties)
static?It makes me really nervous that folks can (ab)use these public static functions anywhere and not just as part of the class instantiation lifecycle.
If it's just about caching the schemas, we can keep the props
private staticwithout it affecting our public API.