Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fe6fa08
Abilities API: Add a core/settings ability
jorgefilipecosta Jun 9, 2026
58b920f
Abilities API: rename the core/settings 'slugs' input to 'settings'
jorgefilipecosta Jun 15, 2026
401c161
Abilities API: cover a custom registered setting in the core/settings…
jorgefilipecosta Jun 15, 2026
3c547c7
Abilities API: refine the core/settings ability per review.
jorgefilipecosta Jun 17, 2026
af0601e
Abilities API: use list<string> for the core/settings input schema do…
jorgefilipecosta Jun 19, 2026
13f4d31
Abilities API: drop the unreachable recompute branch in execute_get_s…
jorgefilipecosta Jun 19, 2026
59e1819
Abilities API: document the show_in_abilities registration-timing con…
jorgefilipecosta Jun 19, 2026
6c864a2
Abilities API: scope the WP_Settings_Abilities class.
jorgefilipecosta Jun 22, 2026
ea0d988
Abilities API: make WP_Settings_Abilities instance-based.
jorgefilipecosta Jun 22, 2026
6f4b639
Update tests/phpunit/tests/abilities-api/wpRegisterCoreSettingsAbilit…
jorgefilipecosta Jul 2, 2026
5671a25
Abilities API: rename the core/settings ability to core/read-settings.
jorgefilipecosta Jul 2, 2026
3ac5606
Abilities API: document why the settings abilities use a dedicated cl…
jorgefilipecosta Jul 2, 2026
6c98af6
Abilities API: serialize empty input schema defaults as objects.
jorgefilipecosta Jul 2, 2026
067e791
Abilities API: use ticket 64605 in the core/read-settings tests.
jorgefilipecosta Jul 2, 2026
ae862f0
Abilities API: register initial settings before core/read-settings sn…
jorgefilipecosta Jul 3, 2026
714d164
Abilities API: validate and sanitize core/read-settings values agains…
jorgefilipecosta Jul 7, 2026
a6167bf
Abilities API: Sync core/read-settings with the AI plugin
jorgefilipecosta Jul 10, 2026
f0b383d
Add workspace configuration and Trac ticket draft
jorgefilipecosta Jul 10, 2026
87b8381
Revert "Add workspace configuration and Trac ticket draft"
jorgefilipecosta Jul 10, 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
5 changes: 5 additions & 0 deletions 5 src/wp-includes/abilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

declare( strict_types = 1 );

require_once __DIR__ . '/abilities/class-wp-settings-abilities.php';

/**
* Registers the core ability categories.
*
Expand Down Expand Up @@ -351,4 +353,7 @@ function wp_register_core_abilities(): void {
),
)
);

// Register the settings abilities (currently the read-only `core/read-settings`).
( new WP_Settings_Abilities() )->register();
}
316 changes: 316 additions & 0 deletions 316 src/wp-includes/abilities/class-wp-settings-abilities.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
<?php
/**
* Abilities API: WP_Settings_Abilities class.
*
* @package WordPress
* @subpackage Abilities API
* @since 7.1.0
*/

declare( strict_types = 1 );

/**
* Core class used to register settings-related abilities.
*
* Provides the read-only `core/read-settings` ability and the shared building blocks
* (exposed-settings discovery, schema generation, and value casting) that are intended to
* also back a future write-oriented `core/manage-settings` ability.
*
* Unlike the other core abilities, which are self-contained closures registered directly
* in wp_register_core_abilities(), the settings abilities live in a dedicated class
* because they share state: the set of exposed settings is computed once at registration
* and reused by the input schema, the output schema, and the execute callback, and the
* same helpers are meant to be shared with the future write ability.
*
* The exposed settings are captured when the ability registers on `wp_abilities_api_init`.
* That hook fires lazily on first use of the abilities registry, which is not ordered
* relative to `rest_api_init` (where core registers its own settings) and can happen
* without it entirely, e.g. on cron or WP-CLI. register() therefore ensures core's
* initial settings are registered before the snapshot is computed. Plugin settings
* flagged with `show_in_abilities` must be registered before the abilities registry is
* first used in a request; registering them on `init` is reliable.
*
* This class is part of WordPress' internal implementation of the core abilities and is
* not part of the public API. It may be changed or removed at any time without notice.
* Do not use it directly or rely on its existence.
*
* @since 7.1.0
*
* @access private
*/
final class WP_Settings_Abilities {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nitpick, here and in other open PRs, it might make more sense to follow naming conventions from other places and use WP_Abilities_ prefix and follow with class-wp-abilities- as file name. This would better mirror how it would look when using namespaces: WordPress/Abilities/Settings.


/**
* The ability category used for settings abilities.
*
* @since 7.1.0
* @var string
*/
private const CATEGORY = 'site';

/**
* Settings exposed through the Abilities API, computed once at registration.
*
* Cached so the input/output schema and the executed result derive from the exact same
* structure, and {@see get_registered_settings()} is only walked once per request.
*
* @since 7.1.0
* @var array<string, array{option: string, group: string, default: mixed, schema: array<string, mixed>}>|null
*/
private $exposed_settings = null;

/**
* Registers all settings abilities.
*
* Must run on the `wp_abilities_api_init` hook.
*
* @since 7.1.0
*/
public function register(): void {
/*
* Core's initial settings register on `rest_api_init`, which fires lazily and
* independently of `wp_abilities_api_init`: on cron, WP-CLI, or any request where
* abilities are used before the REST server loads, it may not have fired — or may
* be mid-fire at a priority before register_initial_settings() runs. Ensure the
* core settings exist before the exposed-settings snapshot below is computed;
* re-registering them again later on `rest_api_init` is harmless.
*/
if ( ! did_action( 'rest_api_init' ) || doing_action( 'rest_api_init' ) ) {
register_initial_settings();
}

$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();
*/
Comment thread
jorgefilipecosta marked this conversation as resolved.
}

/**
* Registers the read-only `core/read-settings` ability.
*
* @since 7.1.0
*/
private function register_get_settings(): void {
// Compute once; execute_get_settings() reuses this exact structure.
$this->exposed_settings = $this->get_exposed_settings();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I noticed that Core settings get registered on the rest_api_init hook:

add_action( 'rest_api_init', 'register_initial_settings', 10 );

It's worth double-checking whether there won't be a race condition with wp_abilities_api_init.

The long-term question is whether get_exposed_settings() could be computed lazily inside the execute/schema callbacks instead of being cached at registration? That would remove the ordering coupling and match the sibling's live‑compute model.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch on the ordering. wp_abilities_api_init fires lazily and isn't ordered relative to rest_api_init, so register() now calls register_initial_settings() before computing the snapshot whenever rest_api_init hasn't fired (or is mid-fire before priority 10). Added the guard in ae862f0 and documented the timing in 59e1819; re-registering later on rest_api_init is harmless.

On computing it lazily: I kept the snapshot cached at registration on purpose. The input schema, output schema, and execute callback all have to derive from the exact same set of exposed settings, so computing it once avoids walking get_registered_settings() three times per request and any risk of the schema and the output drifting apart. With the ordering handled explicitly there's no coupling left to remove, so I dropped the unreachable recompute branch in 13f4d31.


$settings = $this->exposed_settings;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If there are no settings registered, then there is no need to register the ability as it won't return anything anyway.

$field_names = array_keys( $settings );
$groups = array();
$properties = array();
foreach ( $settings as $exposed_name => $setting ) {
$properties[ $exposed_name ] = $setting['schema'];
if ( '' === $setting['group'] || in_array( $setting['group'], $groups, true ) ) {
continue;
}
$groups[] = $setting['group'];
}

wp_register_ability(
'core/read-settings',
array(
'label' => __( 'Read Settings' ),
'description' => __( 'Returns WordPress settings as a flat map of setting name to value. By default returns all settings exposed to abilities, or optionally a subset filtered by settings group, by setting name, or both.' ),
'category' => self::CATEGORY,
'input_schema' => $this->get_settings_input_schema( $groups, $field_names ),
'output_schema' => array(
'type' => 'object',
'description' => __( 'A map of setting name to its current value.' ),
'properties' => $properties,
'additionalProperties' => false,
),
'execute_callback' => array( $this, 'execute_get_settings' ),
'permission_callback' => array( $this, 'has_permission' ),
'meta' => array(
'annotations' => array(
'readonly' => true,
'destructive' => false,
'idempotent' => true,
),
'show_in_rest' => true,
),
)
);
}

/**
* Executes the `core/read-settings` ability.
*
* @since 7.1.0
*
* @param mixed $input Optional. The ability input. Default empty array.
* @return array<string, mixed> Map of exposed setting name to current value.
*/
public function execute_get_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();
}

$group = isset( $input['group'] ) && is_string( $input['group'] ) ? $input['group'] : '';
$fields = isset( $input['fields'] ) && is_array( $input['fields'] ) ? $input['fields'] : array();

$result = array();
foreach ( $settings as $exposed_name => $setting ) {
if ( '' !== $group && $setting['group'] !== $group ) {
continue;
}
if ( ! empty( $fields ) && ! in_array( $exposed_name, $fields, true ) ) {
continue;
}

$type = isset( $setting['schema']['type'] ) && is_string( $setting['schema']['type'] ) ? $setting['schema']['type'] : 'string';
$value = get_option( $setting['option'], $setting['default'] );

$result[ $exposed_name ] = $this->cast_value( $value, $type );
}

return $result;
}

/**
* Checks whether the current user may use the settings abilities.
*
* @since 7.1.0
*
* @return bool True if the current user can manage options.
*/
public function has_permission(): bool {
return current_user_can( 'manage_options' );
}

/**
* Builds the input schema for the get ability: optional filters by group and/or name.
*
* Both `group` and `fields` are optional; supplying both narrows the response to their
* intersection, and supplying neither returns every exposed setting.
*
* @since 7.1.0
*
* @param list<string> $groups Available settings groups.
* @param list<string> $field_names Available exposed setting names.
* @return array<string, mixed> The input JSON Schema.
*/
private function get_settings_input_schema( array $groups, array $field_names ): array {
return array(
'type' => 'object',
// Object (not array()) so the serialized schema default is {}, consistent with type:object.
'default' => (object) array(),
'properties' => array(
'group' => array(
'type' => 'string',
'enum' => $groups,
'description' => __( 'Return only settings that belong to this settings group.' ),
),
'fields' => array(
'type' => 'array',
'items' => array(
'type' => 'string',
'enum' => $field_names,
),
'description' => __( 'Return only the settings with these names.' ),
),
),
'additionalProperties' => false,
);
}

/**
* Returns the settings exposed through the Abilities API.
*
* Reads {@see get_registered_settings()} and keeps only settings flagged with a truthy
* `show_in_abilities` argument. Each entry is keyed by its exposed name and carries the
* underlying option name, the settings group, the registration default, and a JSON Schema
* describing the value.
*
* @since 7.1.0
*
* @return array<string, array{option: string, group: string, default: mixed, schema: array<string, mixed>}> Settings keyed by exposed name.
*/
private function get_exposed_settings(): array {
$settings = array();

foreach ( get_registered_settings() as $option_name => $args ) {
$show = $args['show_in_abilities'] ?? false;
if ( empty( $show ) ) {
continue;
}

$option_name = (string) $option_name;
$exposed_name = is_array( $show ) && isset( $show['name'] ) && is_string( $show['name'] ) && '' !== $show['name'] ? $show['name'] : $option_name;

$settings[ $exposed_name ] = array(
'option' => $option_name,
'group' => isset( $args['group'] ) && is_string( $args['group'] ) ? $args['group'] : '',
'default' => array_key_exists( 'default', $args ) ? $args['default'] : false,
'schema' => $this->value_schema( $args, $show ),
);
}

return $settings;
}

/**
* Builds the JSON Schema describing a single setting's value.
*
* @since 7.1.0
*
* @param array<string, mixed> $args The setting registration arguments.
* @param bool|array<string, mixed> $show The setting's `show_in_abilities` value.
* @return array<string, mixed> The value JSON Schema.
*/
private function value_schema( array $args, $show ): array {
$schema = array(
'type' => isset( $args['type'] ) && is_string( $args['type'] ) ? $args['type'] : 'string',
);
if ( ! empty( $args['label'] ) ) {
$schema['title'] = $args['label'];
}
if ( ! empty( $args['description'] ) ) {
$schema['description'] = $args['description'];
}
if ( is_array( $show ) && isset( $show['schema'] ) && is_array( $show['schema'] ) ) {

@gziolo gziolo Jul 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit, this should be just fine as:

Suggested change
if ( is_array( $show ) && isset( $show['schema'] ) && is_array( $show['schema'] ) ) {
if ( isset( $show['schema'] ) && is_array( $show['schema'] ) ) {

isset() safely checks the nested key.

/** @var array<string, mixed> $show_schema */
$show_schema = $show['schema'];
$schema = array_merge( $schema, $show_schema );
}

return $schema;
}

/**
* Casts a stored option value to the type declared in its settings registration.
*
* @since 7.1.0
*
* @param mixed $value The raw option value.
* @param string $type The registered setting type.
* @return mixed The value cast to the declared type.
*/
private function cast_value( $value, string $type ) {
switch ( $type ) {
case 'boolean':
return (bool) $value;
case 'integer':
return is_scalar( $value ) ? (int) $value : 0;
case 'number':
return is_scalar( $value ) ? (float) $value : 0.0;
case 'array':
return is_array( $value ) ? $value : array();
case 'object':
// Cast to object so an empty/non-array value serializes as {} (not []) and
// satisfies the `object` output schema validated by execute().
return (object) ( is_array( $value ) ? $value : array() );
default:
return is_scalar( $value ) ? (string) $value : $value;
}
}
}
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.