Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5fb3965
Update: Support settings schema on get settings.
jorgefilipecosta Feb 17, 2026
fc9b98f
Test invalid output shcema validation
jorgefilipecosta Feb 17, 2026
daebd4b
remove schema duplocation
jorgefilipecosta Feb 17, 2026
9051fa5
Docs: Clarify register_setting show_in_abilities schema arg
jorgefilipecosta Feb 17, 2026
a808c85
Block Supports: Add autoRegister support for PHP-only block registrat…
t-hamano Feb 17, 2026
296e440
initial version
jorgefilipecosta Feb 9, 2026
bcb067e
add tests
jorgefilipecosta Feb 9, 2026
d95b0c9
fix notices
jorgefilipecosta Feb 9, 2026
ad7ec2a
update ticket
jorgefilipecosta Feb 9, 2026
bd89f7b
lint fixes
jorgefilipecosta Feb 9, 2026
5f4e5ba
don't include empty validation errors
jorgefilipecosta Feb 9, 2026
93a5238
fix variable naming
jorgefilipecosta Feb 9, 2026
7080743
update tests
jorgefilipecosta Feb 9, 2026
022e463
remove explicit validaiton
jorgefilipecosta Feb 9, 2026
4ed8138
fix symetry issue
jorgefilipecosta Feb 9, 2026
00fc880
Update src/wp-includes/abilities/class-wp-settings-abilities.php
jorgefilipecosta Feb 12, 2026
bfd643a
Update src/wp-includes/abilities/class-wp-settings-abilities.php
jorgefilipecosta Feb 12, 2026
af4497e
Update src/wp-includes/abilities/class-wp-settings-abilities.php
jorgefilipecosta Feb 12, 2026
f725789
Update src/wp-includes/abilities/class-wp-settings-abilities.php
jorgefilipecosta Feb 12, 2026
b29022b
Update src/wp-includes/abilities/class-wp-settings-abilities.php
jorgefilipecosta Feb 12, 2026
e9f11bb
Update src/wp-includes/abilities/class-wp-settings-abilities.php
jorgefilipecosta Feb 12, 2026
102331b
simplify get available slugs
jorgefilipecosta Feb 12, 2026
d87cbfa
add type anotattion
jorgefilipecosta Feb 12, 2026
bcda1e8
remove unnessary test
jorgefilipecosta Feb 12, 2026
0758e59
fix get available slugs
jorgefilipecosta Feb 12, 2026
ca3b47e
properly cast value before comparing
jorgefilipecosta Feb 12, 2026
1bd5e10
explicit test clean up
jorgefilipecosta Feb 12, 2026
8ee8c38
minor enhacements
jorgefilipecosta Feb 12, 2026
c257425
lint fix
jorgefilipecosta Feb 12, 2026
f58ccf6
Update src/wp-includes/abilities/class-wp-settings-abilities.php
jorgefilipecosta Feb 13, 2026
ec9627e
Update src/wp-includes/abilities/class-wp-settings-abilities.php
jorgefilipecosta Feb 13, 2026
13c9e86
Update src/wp-includes/abilities/class-wp-settings-abilities.php
jorgefilipecosta Feb 16, 2026
d129749
Update src/wp-includes/abilities/class-wp-settings-abilities.php
jorgefilipecosta Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 160 additions & 18 deletions src/wp-includes/abilities/class-wp-settings-abilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -57,6 +57,7 @@ class WP_Settings_Abilities {
public static function register(): void {
self::init();
self::register_get_settings();
self::register_update_settings();
}

/**
Expand All @@ -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();
}

/**
Expand Down Expand Up @@ -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 ) {
Expand All @@ -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',
Expand Down Expand Up @@ -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(
Expand All @@ -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.
*
Expand Down Expand Up @@ -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 {
Comment on lines +379 to +381

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Add: WordPress Core update settings ability #10892 (comment)
  2. If you want to return a \WP_Error here, you need to remove the hardcoded array from the method signature.

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 static without it affecting our public API.

$input = is_array( $input ) ? $input : array();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @param for $input says it is an array. Is this case here in case an external request is submitting a non-array value? When would an non-array be passed? I realize this is the same as the execute_get_settings() method.


if ( empty( $input['settings'] ) || ! is_array( $input['settings'] ) ) {
return array(
'updated_settings' => (object) array(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we coercing an empty array into an object?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To force the JSON encoding to be {} as opposed to [].

Copy link

@justlevine justlevine Feb 16, 2026

Choose a reason for hiding this comment

The 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 $payload['updated_settings'] : array<string,mixed>|null . It's

  • more self documenting (the possibility of no updated settings),
  • more performant to check (a falsy check you don't need to unpack),
  • more resilient to user/llm error (since they only need to do the falsely check to access the internals without erroring),
  • lets us bypass all these ugly coercions from WP using multidimensional arrays by just setting these to null.

(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';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the concern her just that $args['group] might be unset or can it also contain an empty string we need to coerce to our general fallback?

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 );

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to cast $sanitized_value, and more importantly why would we cast it after we update the option?


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;
}
}

return array(
'updated_settings' => ! empty( $updated_settings ) ? $updated_settings : (object) array(),
);
}

/**
* Casts a value to the appropriate type based on the setting's registered type.
*
Expand Down
61 changes: 61 additions & 0 deletions src/wp-includes/block-supports/auto-register.php
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 );
28 changes: 28 additions & 0 deletions src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -3131,3 +3131,31 @@ function _wp_footnotes_force_filtered_html_on_import_filter( $arg ) {
}
return $arg;
}

/**
* Exposes blocks with autoRegister flag for ServerSideRender in the editor.
*
* Detects blocks that have the autoRegister flag set in their supports
* and passes them to JavaScript for auto-registration with ServerSideRender.
*
* @access private
* @since 7.0.0
*/
function _wp_enqueue_auto_register_blocks() {
$auto_register_blocks = array();
$registered_blocks = WP_Block_Type_Registry::get_instance()->get_all_registered();

foreach ( $registered_blocks as $block_name => $block_type ) {
if ( ! empty( $block_type->supports['autoRegister'] ) && ! empty( $block_type->render_callback ) ) {
$auto_register_blocks[] = $block_name;
}
}

if ( ! empty( $auto_register_blocks ) ) {
wp_add_inline_script(
'wp-block-library',
sprintf( 'window.__unstableAutoRegisterBlocks = %s;', wp_json_encode( $auto_register_blocks ) ),
'before'
);
}
}
1 change: 1 addition & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,7 @@
add_action( 'enqueue_block_editor_assets', 'wp_enqueue_editor_format_library_assets' );
add_action( 'enqueue_block_editor_assets', 'wp_enqueue_block_editor_script_modules' );
add_action( 'enqueue_block_editor_assets', 'wp_enqueue_global_styles_css_custom_properties' );
add_action( 'enqueue_block_editor_assets', '_wp_enqueue_auto_register_blocks' );
add_action( 'wp_print_scripts', 'wp_just_in_time_script_localization' );
add_filter( 'print_scripts_array', 'wp_prototype_before_jquery' );
add_action( 'customize_controls_print_styles', 'wp_resource_hints', 1 );
Expand Down
Loading
Loading