From 86b252106a9f8887409e6eee8895c9bd3e237e29 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 16 Jan 2026 17:10:43 +0000 Subject: [PATCH 1/5] Initial implementation --- src/wp-includes/abilities.php | 4 + .../abilities/class-wp-settings-abilities.php | 187 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 src/wp-includes/abilities/class-wp-settings-abilities.php diff --git a/src/wp-includes/abilities.php b/src/wp-includes/abilities.php index 0320df3b9f38a..3ea82e8867f06 100644 --- a/src/wp-includes/abilities.php +++ b/src/wp-includes/abilities.php @@ -9,6 +9,8 @@ declare( strict_types = 1 ); +require_once __DIR__ . '/abilities/class-wp-settings-abilities.php'; + /** * Registers the core ability categories. * @@ -259,4 +261,6 @@ function wp_register_core_abilities(): void { ), ) ); + + WP_Settings_Abilities::register(); } diff --git a/src/wp-includes/abilities/class-wp-settings-abilities.php b/src/wp-includes/abilities/class-wp-settings-abilities.php new file mode 100644 index 0000000000000..bcc74e8dbf4be --- /dev/null +++ b/src/wp-includes/abilities/class-wp-settings-abilities.php @@ -0,0 +1,187 @@ + __( 'Get Settings' ), + 'description' => __( 'Returns registered WordPress settings exposed to the REST API, grouped by their registration group. Returns key-value pairs similar to the REST API settings endpoint.' ), + 'category' => 'site', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'group' => array( + 'type' => 'string', + 'description' => __( 'Optional: Filter settings by group name (e.g., general, reading, writing, discussion).' ), + ), + ), + 'additionalProperties' => false, + 'default' => array(), + ), + 'output_schema' => array( + 'type' => 'object', + 'description' => __( 'Settings grouped by registration group. Each group contains key-value pairs where the key is the setting name (or REST alias) and the value is the current setting value.' ), + 'additionalProperties' => array( + 'type' => 'object', + 'description' => __( 'A settings group containing setting name to value mappings.' ), + 'additionalProperties' => true, + ), + ), + 'execute_callback' => array( __CLASS__, 'execute_get_settings' ), + 'permission_callback' => array( __CLASS__, 'check_manage_options' ), + 'meta' => array( + 'annotations' => array( + 'readOnlyHint' => true, + 'destructiveHint' => false, + 'idempotentHint' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + } + + /** + * Permission callback for settings abilities. + * + * @since 6.9.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 6.9.0 + * + * @param array $input { + * Optional. Input parameters. + * + * @type string $group Optional. Filter settings by group name. + * } + * @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; + + $registered_settings = get_registered_settings(); + $settings_by_group = array(); + + foreach ( $registered_settings as $option_name => $args ) { + // Only include settings exposed to REST API. + if ( empty( $args['show_in_rest'] ) ) { + continue; + } + + $group = $args['group'] ?? 'general'; + + // Skip if filtering by group and doesn't match. + if ( $filter_group && $group !== $filter_group ) { + continue; + } + + // Determine the REST name (may be aliased via show_in_rest.name). + $rest_name = $option_name; + if ( is_array( $args['show_in_rest'] ) && ! empty( $args['show_in_rest']['name'] ) ) { + $rest_name = $args['show_in_rest']['name']; + } + + // Get default value. + $default = $args['default'] ?? null; + if ( is_array( $args['show_in_rest'] ) && isset( $args['show_in_rest']['schema']['default'] ) ) { + $default = $args['show_in_rest']['schema']['default']; + } + + // Get current value. + $value = get_option( $option_name, $default ); + + // Cast value to proper type. + $value = self::cast_value( $value, $args['type'] ?? 'string' ); + + // Initialize group if needed. + if ( ! isset( $settings_by_group[ $group ] ) ) { + $settings_by_group[ $group ] = array(); + } + + $settings_by_group[ $group ][ $rest_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 6.9.0 + * + * @param mixed $value The value to cast. + * @param string $type The registered type (string, boolean, integer, number, array, object). + * @return mixed 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; + } + } +} From 16f89cb77505d132e1bc079096b7ae0cb16f69d2 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 16 Jan 2026 17:17:23 +0000 Subject: [PATCH 2/5] Improve input schema --- .../abilities/class-wp-settings-abilities.php | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/abilities/class-wp-settings-abilities.php b/src/wp-includes/abilities/class-wp-settings-abilities.php index bcc74e8dbf4be..937d7457eca25 100644 --- a/src/wp-includes/abilities/class-wp-settings-abilities.php +++ b/src/wp-includes/abilities/class-wp-settings-abilities.php @@ -23,6 +23,14 @@ */ class WP_Settings_Abilities { + /** + * Available setting groups with show_in_rest enabled. + * + * @since 6.9.0 + * @var array + */ + private static $available_groups; + /** * Registers all settings abilities. * @@ -31,9 +39,47 @@ class WP_Settings_Abilities { * @return void */ public static function register(): void { + self::init(); self::register_get_settings(); } + /** + * Initializes shared data for settings abilities. + * + * @since 6.9.0 + * + * @return void + */ + private static function init(): void { + self::$available_groups = self::get_available_groups(); + } + + /** + * Gets unique setting groups that have show_in_rest enabled. + * + * @since 6.9.0 + * + * @return array List of unique group names. + */ + private static function get_available_groups(): array { + $groups = array(); + + foreach ( get_registered_settings() as $args ) { + if ( empty( $args['show_in_rest'] ) ) { + continue; + } + + $group = $args['group'] ?? 'general'; + if ( ! in_array( $group, $groups, true ) ) { + $groups[] = $group; + } + } + + sort( $groups ); + + return $groups; + } + /** * Registers the core/get-settings ability. * @@ -53,7 +99,8 @@ private static function register_get_settings(): void { 'properties' => array( 'group' => array( 'type' => 'string', - 'description' => __( 'Optional: Filter settings by group name (e.g., general, reading, writing, discussion).' ), + 'description' => __( 'Filter settings by group name. If omitted, returns all groups.' ), + 'enum' => self::$available_groups, ), ), 'additionalProperties' => false, From 8f72312d7f983a90c0398f94892613f1c91d38d3 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 16 Jan 2026 17:26:51 +0000 Subject: [PATCH 3/5] Improve output schema --- .../abilities/class-wp-settings-abilities.php | 98 +++++++++++++++++-- 1 file changed, 89 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/abilities/class-wp-settings-abilities.php b/src/wp-includes/abilities/class-wp-settings-abilities.php index 937d7457eca25..718ea8a93c63c 100644 --- a/src/wp-includes/abilities/class-wp-settings-abilities.php +++ b/src/wp-includes/abilities/class-wp-settings-abilities.php @@ -31,6 +31,14 @@ class WP_Settings_Abilities { */ private static $available_groups; + /** + * Dynamic output schema built from registered settings. + * + * @since 6.9.0 + * @var array + */ + private static $output_schema; + /** * Registers all settings abilities. * @@ -52,6 +60,7 @@ public static function register(): void { */ private static function init(): void { self::$available_groups = self::get_available_groups(); + self::$output_schema = self::build_output_schema(); } /** @@ -80,6 +89,85 @@ private static function get_available_groups(): array { return $groups; } + /** + * 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 6.9.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 ( empty( $args['show_in_rest'] ) ) { + continue; + } + + $group = $args['group'] ?? 'general'; + + // Determine the REST name (may be aliased via show_in_rest.name). + $rest_name = $option_name; + if ( is_array( $args['show_in_rest'] ) && ! empty( $args['show_in_rest']['name'] ) ) { + $rest_name = $args['show_in_rest']['name']; + } + + // Build setting schema from registered metadata. + $setting_schema = array( + 'type' => $args['type'] ?? 'string', + ); + + // Add title from label if available. + if ( ! empty( $args['label'] ) ) { + $setting_schema['title'] = $args['label']; + } + + // Use description if set, otherwise fall back to label. + if ( ! empty( $args['description'] ) ) { + $setting_schema['description'] = $args['description']; + } elseif ( ! empty( $args['label'] ) ) { + $setting_schema['description'] = $args['label']; + } + + // Include default if set. + if ( isset( $args['default'] ) ) { + $setting_schema['default'] = $args['default']; + } + + // Merge any schema from show_in_rest (enum, format, etc.). + if ( is_array( $args['show_in_rest'] ) && ! empty( $args['show_in_rest']['schema'] ) ) { + $setting_schema = array_merge( $setting_schema, $args['show_in_rest']['schema'] ); + } + + // Initialize group if needed. + if ( ! isset( $group_properties[ $group ] ) ) { + $group_properties[ $group ] = array( + 'type' => 'object', + 'description' => sprintf( + /* translators: %s: Settings group name. */ + __( '%s settings.' ), + ucfirst( $group ) + ), + 'properties' => array(), + ); + } + + $group_properties[ $group ]['properties'][ $rest_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, + ); + } + /** * Registers the core/get-settings ability. * @@ -106,15 +194,7 @@ private static function register_get_settings(): void { 'additionalProperties' => false, 'default' => array(), ), - 'output_schema' => array( - 'type' => 'object', - 'description' => __( 'Settings grouped by registration group. Each group contains key-value pairs where the key is the setting name (or REST alias) and the value is the current setting value.' ), - 'additionalProperties' => array( - 'type' => 'object', - 'description' => __( 'A settings group containing setting name to value mappings.' ), - 'additionalProperties' => true, - ), - ), + 'output_schema' => self::$output_schema, 'execute_callback' => array( __CLASS__, 'execute_get_settings' ), 'permission_callback' => array( __CLASS__, 'check_manage_options' ), 'meta' => array( From 31ccd504bc0b311a9b8550b63f359b19a34cc89f Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 16 Jan 2026 17:41:23 +0000 Subject: [PATCH 4/5] remove irrelevant comments --- .../abilities/class-wp-settings-abilities.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/wp-includes/abilities/class-wp-settings-abilities.php b/src/wp-includes/abilities/class-wp-settings-abilities.php index 718ea8a93c63c..40bcba97cf135 100644 --- a/src/wp-includes/abilities/class-wp-settings-abilities.php +++ b/src/wp-includes/abilities/class-wp-settings-abilities.php @@ -110,40 +110,33 @@ private static function build_output_schema(): array { $group = $args['group'] ?? 'general'; - // Determine the REST name (may be aliased via show_in_rest.name). $rest_name = $option_name; if ( is_array( $args['show_in_rest'] ) && ! empty( $args['show_in_rest']['name'] ) ) { $rest_name = $args['show_in_rest']['name']; } - // Build setting schema from registered metadata. $setting_schema = array( 'type' => $args['type'] ?? 'string', ); - // Add title from label if available. if ( ! empty( $args['label'] ) ) { $setting_schema['title'] = $args['label']; } - // Use description if set, otherwise fall back to label. if ( ! empty( $args['description'] ) ) { $setting_schema['description'] = $args['description']; } elseif ( ! empty( $args['label'] ) ) { $setting_schema['description'] = $args['label']; } - // Include default if set. if ( isset( $args['default'] ) ) { $setting_schema['default'] = $args['default']; } - // Merge any schema from show_in_rest (enum, format, etc.). if ( is_array( $args['show_in_rest'] ) && ! empty( $args['show_in_rest']['schema'] ) ) { $setting_schema = array_merge( $setting_schema, $args['show_in_rest']['schema'] ); } - // Initialize group if needed. if ( ! isset( $group_properties[ $group ] ) ) { $group_properties[ $group ] = array( 'type' => 'object', @@ -243,37 +236,29 @@ public static function execute_get_settings( $input = array() ): array { $settings_by_group = array(); foreach ( $registered_settings as $option_name => $args ) { - // Only include settings exposed to REST API. if ( empty( $args['show_in_rest'] ) ) { continue; } $group = $args['group'] ?? 'general'; - // Skip if filtering by group and doesn't match. if ( $filter_group && $group !== $filter_group ) { continue; } - // Determine the REST name (may be aliased via show_in_rest.name). $rest_name = $option_name; if ( is_array( $args['show_in_rest'] ) && ! empty( $args['show_in_rest']['name'] ) ) { $rest_name = $args['show_in_rest']['name']; } - // Get default value. $default = $args['default'] ?? null; if ( is_array( $args['show_in_rest'] ) && isset( $args['show_in_rest']['schema']['default'] ) ) { $default = $args['show_in_rest']['schema']['default']; } - // Get current value. $value = get_option( $option_name, $default ); - - // Cast value to proper type. $value = self::cast_value( $value, $args['type'] ?? 'string' ); - // Initialize group if needed. if ( ! isset( $settings_by_group[ $group ] ) ) { $settings_by_group[ $group ] = array(); } From aeea42585e31d8df6e5ff355daed7f51ed1751c8 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 16 Jan 2026 19:27:53 +0000 Subject: [PATCH 5/5] description update --- src/wp-includes/abilities/class-wp-settings-abilities.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/abilities/class-wp-settings-abilities.php b/src/wp-includes/abilities/class-wp-settings-abilities.php index 40bcba97cf135..59d56c00f5476 100644 --- a/src/wp-includes/abilities/class-wp-settings-abilities.php +++ b/src/wp-includes/abilities/class-wp-settings-abilities.php @@ -173,7 +173,7 @@ private static function register_get_settings(): void { 'core/get-settings', array( 'label' => __( 'Get Settings' ), - 'description' => __( 'Returns registered WordPress settings exposed to the REST API, grouped by their registration group. Returns key-value pairs similar to the REST API settings endpoint.' ), + 'description' => __( 'Returns registered WordPress settings grouped by their registration group. Returns key-value pairs per setting.' ), 'category' => 'site', 'input_schema' => array( 'type' => 'object',