Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions 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 @@ -259,4 +261,6 @@ function wp_register_core_abilities(): void {
),
)
);

WP_Settings_Abilities::register();
}
341 changes: 341 additions & 0 deletions src/wp-includes/abilities/class-wp-settings-abilities.php
Copy link
Member

Choose a reason for hiding this comment

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

@jorgefilipecosta Out of curiosity, why are we coupling this to the REST API? Why would this Ability limit itself to settings that are configured to work with REST? Or am I misunderstanding something?

Copy link
Member

Choose a reason for hiding this comment

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

I think I'm inclined away from using a preexisting configuration in an unexpected way.

Copy link
Member Author

Choose a reason for hiding this comment

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

@jorgefilipecosta Out of curiosity, why are we coupling this to the REST API? Why would this Ability limit itself to settings that are configured to work with REST? Or am I misunderstanding something?

Let's imagine someone registers a private option myoptionSecret123 and explicitly disables it from the REST API so that REST API consumers can't discover it exists. Since abilities are exposed via the REST API, if we don't respect the REST API flag, we'd end up exposing myoptionSecret123 through the abilities endpoint. Which would defeat the purpose of making it private.

I think I'm inclined away from using a preexisting configuration in an unexpected way.

I hear you, though I'd argue it's not entirely unexpected. What we have now is: when consuming get-settings outside the REST endpoint, we show all settings; when consuming it through the abilities REST endpoint, we don't show settings that were disabled from REST. So we're essentially honoring what the setting already declared, just in a different endpoint.
That said, we could discuss the alternative path: adding a new flag to register_setting() specifically for abilities, something like show_in_abilities. But that flag would need to be opt-in (to avoid the scenario above where we accidentally expose something private). This means we'd need to update all core settings to opt in, and we wouldn't have access to plugin settings until they opt in as well, which isn't ideal for adoption.

Copy link
Member

@JasonTheAdams JasonTheAdams Jan 21, 2026

Choose a reason for hiding this comment

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

I get where you're coming from, but I feel like we're describing a public type configuration, akin to post types, that is intended for broad implication. Thinking from a non-core, 3rd party perspective here, this is a non-obvious side-effect of a setting with a very specific name — show_in_rest. While unlikely, this could have security implications if someone is controlling how the setting is exposed in REST in some subsequent manner.

Despite the inconvenience, I don't think it's a good idea to introduce side effects to configurations like that.

Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
<?php
/**
* Registers core settings abilities.
*
* This is a utility class to encapsulate the registration of settings-related abilities.
* It is not intended to be instantiated or consumed directly by any other code or plugin.
*
* @package WordPress
* @subpackage Abilities_API
* @since 7.0.0
*
* @internal This class is not part of the public API.
* @access private
*/

declare( strict_types=1 );

/**
* Registers core settings abilities.
*
* @since 7.0.0
* @access private
*/
class WP_Settings_Abilities {

/**
* Available setting groups with show_in_rest enabled.
*
* @since 7.0.0
* @var array
*/
private static $available_groups;

/**
* Dynamic output schema built from registered settings.
*
* @since 7.0.0
* @var array
*/
private static $output_schema;

/**
* Available setting slugs with show_in_rest enabled.
*
* @since 7.0.0
* @var array
*/
private static $available_slugs;

/**
* Registers all settings abilities.
*
* @since 7.0.0
*
* @return void
*/
public static function register(): void {
self::init();
self::register_get_settings();
}

/**
* Initializes shared data for settings abilities.
*
* @since 7.0.0
*
* @return 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();
}

/**
* Gets unique setting groups that have show_in_rest enabled.
*
* @since 7.0.0
*
* @return array List of unique group names.
*/
private static function get_available_groups(): array {
$groups = array();

foreach ( get_registered_settings() as $args ) {
if ( wp_is_serving_rest_request() && empty( $args['show_in_rest'] ) ) {
continue;
}

$group = $args['group'] ?? 'general';
if ( ! in_array( $group, $groups, true ) ) {
$groups[] = $group;
}
}

sort( $groups );

return $groups;
}

/**
* Gets unique setting slugs that have show_in_rest enabled.
*
* @since 7.0.0
*
* @return array List of unique setting slugs.
*/
private static function get_available_slugs(): array {
$slugs = array();

foreach ( get_registered_settings() as $option_name => $args ) {
if ( wp_is_serving_rest_request() && empty( $args['show_in_rest'] ) ) {
continue;
}

$slugs[] = $option_name;
}

sort( $slugs );

return $slugs;
}

/**
* Builds a rich output schema from registered settings metadata.
*
* 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.
*
* @since 7.0.0
*
* @return array JSON Schema for the output.
*/
private static function build_output_schema(): array {
$group_properties = array();

foreach ( get_registered_settings() as $option_name => $args ) {
if ( wp_is_serving_rest_request() && empty( $args['show_in_rest'] ) ) {
continue;
}

$group = $args['group'] ?? 'general';

$setting_schema = array(
'type' => $args['type'] ?? 'string',
);

if ( ! empty( $args['label'] ) ) {
$setting_schema['title'] = $args['label'];
}

if ( ! empty( $args['description'] ) ) {
$setting_schema['description'] = $args['description'];
} elseif ( ! empty( $args['label'] ) ) {
$setting_schema['description'] = $args['label'];
}

if ( ! isset( $group_properties[ $group ] ) ) {
$group_properties[ $group ] = array(
'type' => 'object',
'properties' => array(),
'additionalProperties' => false,
);
}

$group_properties[ $group ]['properties'][ $option_name ] = $setting_schema;
}

ksort( $group_properties );

return array(
'type' => 'object',
'description' => __( 'Settings grouped by registration group. Each group contains settings with their current values.' ),
'properties' => $group_properties,
'additionalProperties' => false,
);
}

/**
* Registers the core/get-settings ability.
*
* @since 7.0.0
*
* @return void
*/
private static function register_get_settings(): void {
wp_register_ability(
'core/get-settings',
array(
'label' => __( 'Get Settings' ),
'description' => __( 'Returns registered WordPress settings grouped by their registration group. Returns key-value pairs per setting.' ),
'category' => 'site',
'input_schema' => array(
'default' => (object) array(),
'oneOf' => array(
// Branch 1: No filter (empty object).
array(
'type' => 'object',
'additionalProperties' => false,
'maxProperties' => 0,
),
// Branch 2: Filter by group only.
array(
'type' => 'object',
'properties' => array(
'group' => array(
'type' => 'string',
'description' => __( 'Filter settings by group name.' ),
'enum' => self::$available_groups,
),
),
'required' => array( 'group' ),
'additionalProperties' => false,
),
// Branch 3: Filter by slugs only.
array(
'type' => 'object',
'properties' => array(
'slugs' => array(
'type' => 'array',
'description' => __( 'Filter settings by specific setting slugs.' ),
'items' => array(
'type' => 'string',
'enum' => self::$available_slugs,
),
),
),
'required' => array( 'slugs' ),
'additionalProperties' => false,
),
),
),
'output_schema' => self::$output_schema,
'execute_callback' => array( __CLASS__, 'execute_get_settings' ),
'permission_callback' => array( __CLASS__, 'check_manage_options' ),
'meta' => array(
'annotations' => array(
'readonly' => true,
'destructive' => false,
'idempotent' => true,
),
'show_in_rest' => true,
),
)
);
}

/**
* Permission callback for settings abilities.
*
* @since 7.0.0
*
* @return bool True if the current user can manage options, false otherwise.
*/
public static function check_manage_options(): bool {
return current_user_can( 'manage_options' );
}

/**
* Execute callback for core/get-settings ability.
*
* Retrieves all registered settings that are exposed to the REST API,
* grouped by their registration group.
*
* @since 7.0.0
*
* @param array $input {
* Optional. Input parameters.
*
* @type string $group Optional. Filter settings by group name. Cannot be used with slugs.
* @type string[] $slugs Optional. Filter settings by specific setting slugs. Cannot be used with group.
* }
* @return array Settings grouped by registration group.
*/
public static function execute_get_settings( $input = array() ): array {
$input = is_array( $input ) ? $input : array();
$filter_group = ! empty( $input['group'] ) ? $input['group'] : null;
$filter_slugs = ! empty( $input['slugs'] ) ? $input['slugs'] : null;

$registered_settings = get_registered_settings();
$settings_by_group = array();

foreach ( $registered_settings as $option_name => $args ) {
if ( wp_is_serving_rest_request() && empty( $args['show_in_rest'] ) ) {
continue;
}

$group = $args['group'] ?? 'general';

if ( $filter_group && $group !== $filter_group ) {
continue;
}

if ( $filter_slugs && ! in_array( $option_name, $filter_slugs, true ) ) {
continue;
}

$default = $args['default'] ?? null;

$value = get_option( $option_name, $default );
$value = self::cast_value( $value, $args['type'] ?? 'string' );

if ( ! isset( $settings_by_group[ $group ] ) ) {
$settings_by_group[ $group ] = array();
}

$settings_by_group[ $group ][ $option_name ] = $value;
}

ksort( $settings_by_group );

return $settings_by_group;
}

/**
* Casts a value to the appropriate type based on the setting's registered type.
*
* @since 7.0.0
*
* @param mixed $value The value to cast.
* @param string $type The registered type (string, boolean, integer, number, array, object).
* @return string|bool|int|float|array The cast value.
*/
private static function cast_value( $value, string $type ) {
switch ( $type ) {
case 'boolean':
return (bool) $value;
case 'integer':
return (int) $value;
case 'number':
return (float) $value;
case 'array':
case 'object':
return is_array( $value ) ? $value : array();
case 'string':
default:
return (string) $value;
}
}
}
Loading