-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Add: WordPress Core get settings ability #10747
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
Open
jorgefilipecosta
wants to merge
16
commits into
WordPress:trunk
Choose a base branch
from
jorgefilipecosta:add/get-settings-ability
base: trunk
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+345
−0
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
0ff2067
Initial implementation
jorgefilipecosta 8e5d7fb
Improve input schema
jorgefilipecosta 93f1983
Improve output schema
jorgefilipecosta 1eacfc3
remove irrelevant comments
jorgefilipecosta 5e6b094
description update
jorgefilipecosta ff2a4ef
Update src/wp-includes/abilities/class-wp-settings-abilities.php
jorgefilipecosta c226a10
Update src/wp-includes/abilities/class-wp-settings-abilities.php
jorgefilipecosta dbb09f8
Add slugs parameter to core/get-settings ability
jorgefilipecosta 74da151
Add return type union to cast_value() method
jorgefilipecosta 8d18089
remove schema 04 causing validation issues
jorgefilipecosta 50a4af4
use raw option names
jorgefilipecosta d1aeb91
only apply rest filters when serving rest requests
jorgefilipecosta 0514b24
add proper one off input validation
jorgefilipecosta a3da0d9
Use 7.0 as the version
jorgefilipecosta 00a6bbd
fix i18n issue
jorgefilipecosta 1e63bc2
older php versions code fix
jorgefilipecosta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
341 changes: 341 additions & 0 deletions
341
src/wp-includes/abilities/class-wp-settings-abilities.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
@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?
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.
I think I'm inclined away from using a preexisting configuration in an unexpected way.
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.
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 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.
Uh oh!
There was an error while loading. Please reload this page.
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.
I get where you're coming from, but I feel like we're describing a
publictype 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.