Opened 8 months ago
Last modified 11 hours ago
#64605 reopened enhancement
Add WordPress `core/settings-get` ability
| Reported by: | jorgefilipecosta | Owned by: | jorgefilipecosta |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.2 |
| Component: | Abilities API | Version: | |
| Severity: | normal | Keywords: | has-patch has-unit-tests |
| Cc: | Focuses: |
Description (last modified by )
This ticket tracks the addition of a core/settings-get ability to the WordPress Abilities API. This ability should dynamically discover and exposes all WordPress settings. Abilities that are registered with show_in_abilities = true are exposed.
Change History (43)
This ticket was mentioned in PR #10747 on WordPress/wordpress-develop by @jorgefilipecosta.
8 months ago
#1
- Keywords has-patch has-unit-tests added
@jorgefilipecosta commented on PR #10747:
8 months ago
#3
This ticket was mentioned in PR #10892 on WordPress/wordpress-develop by @jorgefilipecosta.
8 months ago
#4
Part of: https://github.com/WordPress/ai/issues/40
Inspired by the work on https://github.com/galatanovidiu/mcp-adapter-implementation-example/tree/experiment/layerd-mcp-tools/includes/Abilities by @galatanovidiu.
Ticket: https://core.trac.wordpress.org/ticket/64605
This PR adds a core/update-settings ability to the WordPress Abilities API. This ability allows updating WordPress settings that have show_in_abilities = true. It complements the existing core/get-settings ability by providing a symmetric API for reading and writing settings.
The input and output structures are designed for symmetry with core/get-settings:
- Input accepts settings grouped by registration group (same structure returned by
core/get-settings) - Output returns
updated_settings(with updated values) andvalidation_errors(with error messages) in the same grouped structure
## Organization
Following the pattern established in #10747, this PR extends the WP_Settings_Abilities class in src/wp-includes/abilities/class-wp-settings-abilities.php. The implementation maximizes code reuse:
- Schema Reuse: Uses a permissive input schema that allows the execute callback to handle validation
- Shared Methods: Reuses
get_allowed_settings(),check_manage_options(), andcast_value()from the existing implementation - Registration:
register_update_settings()registers the ability with appropriate annotations (readonly: false,destructive: false,idempotent: true) - Execution:
execute_update_settings()validates, sanitizes, and updates settings, returning both successful updates and validation errors
## Test plan
- Open
/wp-admin/post-new.php - Open the browser console and run the following examples:
// Update a single setting await wp.apiFetch({ path: '/wp-abilities/v1/abilities/core/update-settings/run', method: 'POST', data: { input: { settings: { general: { blogname: 'New Site Title' } } } } });
- [ ] Verify the setting is updated in the database
// Update multiple settings across groups await wp.apiFetch({ path: '/wp-abilities/v1/abilities/core/update-settings/run', method: 'POST', data: { input: { settings: { general: { blogname: 'New Site Title', blogdescription: 'New Tagline' }, reading: { posts_per_page: 15 } } } } });
- [ ] Verify multiple settings across different groups are updated
// Check the ability schema await wp.apiFetch({ path: '/wp-abilities/v1/abilities/core/update-settings' });
- [ ] Verify the input schema documents the settings structure
- [ ] Verify the output schema documents
updated_settingsandvalidation_errors
// Workflow: Get settings, modify, update (symmetry test) const settings = await wp.apiFetch({ path: '/wp-abilities/v1/abilities/core/get-settings/run' }); settings.general.blogname = 'Modified Title'; await wp.apiFetch({ path: '/wp-abilities/v1/abilities/core/update-settings/run', method: 'POST', data: { input: { settings } } });
- [ ] Verify the get -> modify -> update workflow works seamlessly
// Test partial success with invalid settings await wp.apiFetch({ path: '/wp-abilities/v1/abilities/core/update-settings/run', method: 'POST', data: { input: { settings: { general: { blogname: 'Valid Title', unknown_setting: 'should fail' } } } } });
- [ ] Verify
blognameis inupdated_settingsandunknown_settingis invalidation_errors
- [ ] Test permission check: Verify non-admin users cannot access the ability (requires
manage_optionscapability) - [ ] Verify settings without
show_in_abilitiescannot be modified - [ ] Verify settings placed in wrong groups are rejected with appropriate error messages
@jorgefilipecosta commented on PR #10892:
8 months ago
#5
Thank you @westonruter for reviewing this PR, all your suggestions were applied.
#6
@
8 months ago
I'm surprised this was merged without #64596 or https://github.com/WordPress/wordpress-develop/pull/10892.
If we can't get #64596 resolved in time (in either direction), I would suggest reverting this. As @jorbin noted on that ticket we want to make sure the API is in good shape instead of something that has a strong chance we're going to deprecate.
@justlevine commented on PR #10892:
8 months ago
#7
Related to https://core.trac.wordpress.org/ticket/64605, but since that got merged I'll ask here. How exactly does show_in_abilities work here?
Is show_in_abilities: bool|array{schema:array<string,mixed}, or are we in an awkward place where show_in_abilities is a bool, but we're going to infer any schemas from show_in_rest['schema'] ? I'm also not seeing anywhere in this or that PR where's we're loading/coercing the existing schema, am I missing it or was it left out for some reason?
#9
@
7 months ago
- Resolution fixed
- Status closed → reopened
It looks like [61690] reverted [61600], which auto-closed this ticket as fixed.
I'm not clear if the intention is to work on a new implementation of core/get-settings this as a part of #64455 going forward or not, so I'm reopening.
If this was intentionally left as closed, then the resolution needs to be changed appropriately given the revert.
#10
@
7 months ago
I'm not clear if the intention is to work on a new implementation of core/get-settings this as a part of #64455 going forward or not, so I'm reopening.
Not sure either (I'd prefer 1 ticket per ability grouping), but either way it's been bumped from 7.0, so someone with perms, please feel free to update the milestone to 7.1
#13
@
6 months ago
- Component AI → Abilities API
Moving tickets related to the Abilities API to a new sub-component.
#15
@
5 months ago
Removing trunk version as this is not going to be shipped with WP 7.0 but in the next releases.
#16
@
5 months ago
- Keywords abilities removed
Removing abilities and abilities-api custom keywords. This is now indicated by the Abilities API component.
#17
@
5 months ago
- Keywords has-patch, has-unit-tests → has-patch has-unit-tests
- Summary Add: WordPress Core get settings ability → Add WordPress Core get settings ability
- Version trunk
Since this is an enhancement, there's no first version of WordPress this can be reproduced in. Removing trunk version.
This ticket was mentioned in PR #12141 on WordPress/wordpress-develop by @jorgefilipecosta.
4 months ago
#18
Part of: https://github.com/WordPress/ai/issues/40
Ticket: https://core.trac.wordpress.org/ticket/64605
## Summary
Adds a read-only core/settings ability to the Abilities API. It returns WordPress settings — those flagged with a new show_in_abilities registration arg — as a flat name => value map, with per-setting metadata (type, title, description, default) carried in the output schema. Callers can filter by settings group or by slugs (mutually exclusive). Requires manage_options.
This is a flat-output alternative to #10747. The logic lives in a new internal WP_Settings_Abilities class (src/wp-includes/abilities/class-wp-settings-abilities.php), structured so a future core/manage-settings write ability can reuse its helpers (get_exposed_settings(), value_schema(), cast_value()).
## Test plan
In wp-admin, open the browser console:
const { executeAbility } = await import( '@wordpress/abilities' ); await executeAbility( 'core/settings', {} ); // all exposed settings await executeAbility( 'core/settings', { group: 'reading' } ); // one group await executeAbility( 'core/settings', { slugs: [ 'blogname' ] } ); // specific names
- [ ] Returns a flat
name => valuemap with typed values (e.g.posts_per_pageis an int,use_smiliesa bool) - [ ]
groupandslugsfilters narrow the result; supplying both at once is rejected - [ ] A non-admin user (no
manage_options) is denied
PHPUnit coverage added in tests/phpunit/tests/abilities-api/wpRegisterCoreSettingsAbility.php.
@jorgefilipecosta commented on PR #12141:
4 months ago
#19
@jorgefilipecosta can you link this to the trac ticket please? I don't have edit perms in this repo. core.trac.wordpress.org/ticket/64605
Nice catch the ticket mention was added.
@gziolo commented on PR #12141:
4 months ago
#20
Let's run development, review, and testing through https://github.com/WordPress/ai/pull/691, then sync all agreed refinements here.
#21
@
3 months ago
- Description modified (diff)
- Summary Add WordPress Core get settings ability → Add WordPress core/read-settings ability
@jorgefilipecosta commented on PR #12141:
3 months ago
#22
I left my feedback.
Two questions regarding
core/get-site-info:
- It covers some similar settings but is scoped to the site. Should it also respect the
show_in_abilitiescheck wherever applicable?
- Should it get the default value in the schema aligned to
(object) array()as here?
Hi @gziolo I think the answer is yes to both, but I would prefer to do that in a separate PR to avoid this one being too huge.
@jorgefilipecosta commented on PR #12141:
3 months ago
#23
All the reviews comments were applied this is ready for another look.
@gziolo commented on PR #12141:
3 months ago
#24
### One out-of-schema setting value makes the whole core/read-settings call fail
I want to raise a behavior question and seal the answer with a test.
The ability validates its full output against the output schema. WP_Ability::execute() runs validate_output(), which calls rest_validate_value_from_schema() on the whole result. So if a single stored value does not match its setting schema, the entire call returns a WP_Error and the client can read none of the settings.
This is reachable in practice. default_ping_status and default_comment_status are exposed with an open|closed enum. Their stored value can drift outside that enum through a direct update_option(), an import, or older data. sanitize_option() only turns '0' and '' into 'closed', so another string like 'not-a-valid-status' is stored as is. admin_email with format: email is a second, lower chance trigger.
An earlier commit (714d164) already handled this. It ran each value through rest_validate_value_from_schema() and rest_sanitize_value_from_schema(), mirroring WP_REST_Settings_Controller::prepare_value(), and dropped only the value that did not fit. The later sync commit (a6167bf) replaced that with the primitive cast_value() to match the AI plugin, which brought the fragility back.
Here is a test that pins the scenario down:
/** * A stored value that does not match its setting's schema is dropped from the * response instead of making the whole ability fail. * * `default_ping_status` is exposed with an `open|closed` enum. If the stored option * drifts outside that enum (through a direct `update_option()`, an import, or legacy * data), the ability must still return the other settings. Otherwise the ability's * output validation rejects the entire result and every setting becomes unreadable * because of one bad value. * * @ticket 64605 */ public function test_core_read_settings_drops_values_that_fail_their_schema(): void { $this->become_admin(); // Store a value outside the setting's open|closed enum. sanitize_option() only // coerces '0' and '' to 'closed', so this value persists as-is. update_option( 'default_ping_status', 'not-a-valid-status' ); $result = wp_get_ability( 'core/read-settings' )->execute( array() ); // One non-conforming value must not turn the whole response into an error. $this->assertNotWPError( $result, 'A single non-conforming setting value must not fail the entire ability.' ); $this->assertIsArray( $result ); // The other settings are still returned... $this->assertArrayHasKey( 'blogname', $result ); // ...while only the non-conforming value is dropped. $this->assertArrayNotHasKey( 'default_ping_status', $result ); }
Current result on this branch:
Ability "core/read-settings" has invalid output. Reason: output[default_ping_status] is not one of open and closed.
With the earlier validate-and-sanitize approach restored, the test passes. Only default_ping_status is dropped and the rest are returned.
Decision to make: what should happen when a stored value does not match its schema?
- A. Drop that one value and still return the rest. This is the earlier approach and what the test above encodes. This is my preference.
- B. Keep the current cast and accept that one drifted value fails the whole call. If we choose this, we should still add a test that documents the behavior, so it is intentional and not a surprise.
Since the plugin class is kept in sync with the core class (its docblock says so), whatever we pick should also land in the plugin so the two stay aligned. If we go with A, I am happy to help with the plugin change too.
@gziolo commented on PR #12141:
3 months ago
#25
### Consider wiring register_initial_settings on wp_abilities_api_init too
A note on the register() bootstrap, separate from the value-casting question above.
The ability builds its schema from the registered settings at registration time, so those settings must exist when wp_abilities_api_init fires. Today register_initial_settings is only hooked to rest_api_init, and that action fires lazily inside rest_get_server() when the REST server is first built. On cron, WP-CLI, or any direct ability call that never builds the REST server, the settings are not registered yet. That is why register() calls register_initial_settings() itself with the did_action/doing_action guard.
It works, but it makes the ability responsible for core's bootstrap ordering. A cleaner shape could be to treat the settings as a shared dependency and wire them into both subsystems that use them:
add_action( 'rest_api_init', 'register_initial_settings', 10 ); add_action( 'wp_abilities_api_init', 'register_initial_settings', 1 ); // before core abilities (priority 10)
To make this safe to run from more than one hook, register_initial_settings() could get a small internal check that returns early when the core settings are already registered. register_setting() appends to $new_allowed_options on every call, so without such a guard a second run duplicates those entries and re-fires the register_setting action. The check should look at the actual registration state (for example whether a known core setting like blogname is present) rather than a static flag, so it stays correct when the registry is reset, as the tests do.
With that in place the settings register once and correctly in both cases, REST and Abilities, and the ability no longer needs its own did_action/doing_action logic.
What do you think? If it sounds good I am happy to help wire it up.
@gziolo commented on PR #12141:
3 months ago
#26
Thanks @jorgefilipecosta! Nice work on this. The port is faithful to the plugin, the option.php change is clean and well-documented, and CI is green. We are nearly there.
The remaining points are the ones I raised above. The main one is the bad-value handling, since it needs an actual fix here and in the plugin so they stay in sync. The rest are shared conventions and small polish: the WP_Abilities_ class naming (across the abilities PRs) and the empty-object input default (across the sibling abilities). Once the bad-value case is settled, this is good to go from my side.
#27
@
3 months ago
- Milestone 7.1 → 7.2
Moved to WordPress 7.2 based on the decision made after evaluating feedback on the merge proposal.
#28
@
6 weeks ago
Tested https://github.com/WordPress/wordpress-develop/pull/12141
locally on the PR branch.
## Environment
- OS: Linux
- Local env: wordpress-develop Docker (
npm run test:php) - Branch: add/core-settings-ability
## Test
npm run test:php -- --filter wpRegisterCoreReadSettingsAbility
## Result
- OK (9 tests, 32 assertions)
Notes:
- PHPUnit reported "_doing_it_wrong" notices about ability categories/abilities already being registered during bootstrap (
site,user,core/get-site-info,core/get-user-info,core/get-environment-info,core/read-settings). - These did not fail the suite; all assertions passed.
@gziolo commented on PR #12141:
3 days ago
#29
Let’s ensure we also carry over the adjustments in the AI plugin that rename the ability:
#30
@
3 days ago
- Description modified (diff)
- Summary Add WordPress core/read-settings ability → Add WordPress `core/settings-get` ability
@jorgefilipecosta commented on PR #12141:
2 days ago
#31
Tested core/settings-get through the client Abilities API, on a 7.2-alpha-64115 nightly with this PR applied.
No admin screen loads the client modules by default, so load them with a mu-plugin first:
add_action( 'admin_enqueue_scripts', static function () { wp_enqueue_script_module( '@wordpress/core-abilities' ); } );
Then, in the browser console on any admin screen:
// Wait until the client has loaded the server abilities. await ( await import( document.getElementById( '@wordpress/core-abilities-js-module' ).src ) ).ready; const { executeAbility } = await import( '@wordpress/abilities' );
All exposed settings. Values are typed, and match /wp/v2/settings:
await executeAbility( 'core/settings-get' );
{
"blogname": "My WordPress Website",
"blogdescription": "",
"siteurl": "http://127.0.0.1:9412",
"admin_email": "admin@localhost.com",
"timezone_string": "",
"date_format": "F j, Y",
"time_format": "g:i a",
"start_of_week": 1,
"WPLANG": "en_US",
"use_smilies": true,
"default_category": 1,
"default_post_format": "0",
"posts_per_page": 10,
"show_on_front": "posts",
"page_on_front": 0,
"page_for_posts": 0,
"default_ping_status": "open",
"default_comment_status": "open"
}
By group:
await executeAbility( 'core/settings-get', { group: 'reading' } );
{ "posts_per_page": 10, "show_on_front": "posts", "page_on_front": 0, "page_for_posts": 0 }
By setting name:
await executeAbility( 'core/settings-get', { fields: [ 'blogname', 'posts_per_page' ] } );
{ "blogname": "My WordPress Website", "posts_per_page": 10 }
Both, which returns their intersection:
await executeAbility( 'core/settings-get', { group: 'reading', fields: [ 'blogname', 'posts_per_page' ] } );
{ "posts_per_page": 10 }
A stored value that fails its schema is left out. After update_option( 'default_ping_status', 'not-a-valid-status' ):
await executeAbility( 'core/settings-get', { group: 'discussion' } );
{ "default_comment_status": "open" }
/wp/v2/settings answers null for that setting.
Invalid input is rejected:
await executeAbility( 'core/settings-get', { group: 'nope' } ); // ability_invalid_input: Ability "core/settings-get" has invalid input. Reason: input[group] is not one of general, writing, reading, discussion.
A user without manage_options, here a subscriber:
await executeAbility( 'core/settings-get' ); // rest_ability_cannot_execute: Sorry, you are not allowed to execute this ability.
_Tested and written with AI assistance (Claude Code)._
@jorgefilipecosta commented on PR #12141:
2 days ago
#32
Hi @gziolo, thank you for the reviews, the feedback was applied:
- A value that does not match its schema is now left out and the other settings are returned (option A). Values are read the same way as /wp/v2/settings.
- Core settings are now also registered on wp_abilities_api_init with priority 1. A small private function restores $new_allowed_options so https://github.com/WordPress/ai/issues/1080 does not come back.
- The class was renamed to WP_Abilities_Settings, the ability is not registered when no setting is exposed, and the isset suggestion was applied.
- The rename and label from https://github.com/WordPress/ai/pull/1087 were carried over.
- wp_page_for_privacy_policy is now also exposed.
Regarding the sibling abilities, I kept array() as the input default. Since 7.1 the abilities endpoints already send an empty object default as {}, so I guess there is no need to change them, and core/settings-get also uses array() now.
Let me know if there is anything else 👍
@gziolo commented on PR #12141:
42 hours ago
#33
https://github.com/jorgefilipecosta/wordpress-develop/pull/91 – we should consider offering a solid strategy around settings registration that is predictable not only for WP core registered settings but also for those coming from plugins. I propsed we enforce that enabling show_in_abilities warns when setting gets registered after init making it clear when to register the setting.
@gziolo commented on PR #12141:
41 hours ago
#34
I pushed 749c2eb430 with two changes.
1. The ability is now public.
core/settings-get now sets 'public' => true, like the other core abilities. This carries over https://github.com/WordPress/ai/pull/1122. In core, public also enables show_in_rest, so the old flag is no longer needed.
2. show_in_abilities => true reuses the REST API setup.
When show_in_abilities is true, the setting now uses the same name and schema as in show_in_rest. When it is an array, it is used as is.
Because of this, all core settings now use only 'show_in_abilities' => true. We no longer repeat the email format or the open|closed enum. The names now match /wp/v2/settings:
| Option | Ability name |
|---|---|
blogname | title
|
blogdescription | description
|
siteurl | url
|
admin_email | email
|
timezone_string | timezone
|
WPLANG | language
|
wp_page_for_privacy_policy | page_for_privacy_policy
|
The other settings already had the same name in both APIs.
One small side effect: siteurl now also gets the uri format from REST. If the stored value is not a valid URI, the ability leaves it out, like the REST API does.
New tests check that the names match the REST API, and that an array in show_in_abilities replaces the REST arguments.
Two follow-ups:
- The PR description and the manual test comment still use the old names, like
blogname. - The AI plugin still uses the old names, so it needs a similar change.
@gziolo commented on PR #12141:
20 hours ago
#35
I skimmed the changes since my approval, and they look like good improvements. Still good to go from my side. 👍 The two failing CI jobs timed out, so a re-run should fix them.
@jorgefilipecosta commented on PR #12141:
16 hours ago
#36
we should consider offering a solid strategy around settings registration that is predictable not only for WP core registered settings but also for those coming from plugins.
Hi @gziolo, as discussed in https://github.com/jorgefilipecosta/wordpress-develop/pull/91 the current approach was kept, and the show_in_abilities docs now say settings registered after the abilities initialize are not exposed. I agree splitting register_setting() makes sense as a separate ticket 👍
@jorgefilipecosta commented on PR #12141:
16 hours ago
#37
Two follow-ups:
- The PR description and the manual test comment still use the old names, like
blogname.- The AI plugin still uses the old names, so it needs a similar change.
Hi @gziolo, thank you for the enhancements, the description and the testing comment were updated to use the new names. The AI plugin change is part of https://github.com/WordPress/ai/pull/764 👍
@jorgefilipecosta commented on PR #12141:
16 hours ago
#38
The two failing CI jobs timed out, so a re-run should fix them.
Thank you for the review @gziolo, I rebased the PR and all the CI jobs are passing now 👍
@jorgefilipecosta commented on PR #12141:
11 hours ago
#39
Should it get the default value in the schema aligned to
(object) array()as here?
Hi @gziolo, the input default now uses array() like the other core abilities, the {} encoding is covered in https://github.com/WordPress/wordpress-develop/pull/14068 👍
@jorgefilipecosta commented on PR #12141:
11 hours ago
#40
Decision: what should happen for a value that does not match its schema?
- A (my preference): drop that value and return the rest. The test above encodes this.
Hi @gziolo, option A was applied, a value that fails its schema is left out and your test was added 👍
@jorgefilipecosta commented on PR #12141:
11 hours ago
#41
Consider wiring
register_initial_settingsonwp_abilities_api_inittoo
Hi @gziolo, the initial settings are now also registered on wp_abilities_api_init with priority 1 and $new_allowed_options is restored 👍
@jorgefilipecosta commented on PR #12141:
11 hours ago
#42
The remaining points are the ones I raised above. The main one is the bad-value handling, since it needs an actual fix here and in the plugin so they stay in sync.
Thank you for the review @gziolo, all the feedback was applied.
@jorgefilipecosta commented on PR #12141:
11 hours ago
#43
Let’s ensure we also carry over the adjustments in the AI plugin that rename the ability:
Hi @gziolo, the rename and label from https://github.com/WordPress/ai/pull/1087 were carried over 👍
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
Part of: https://github.com/WordPress/ai/issues/40
Inspired by the work on https://github.com/galatanovidiu/mcp-adapter-implementation-example/tree/experiment/layerd-mcp-tools/includes/Abilities by @galatanovidiu.
Ticket: https://core.trac.wordpress.org/ticket/64605
## Summary
This PR adds a
core/get-settingsability to the WordPress Abilities API. This ability dynamically discovers and exposes all WordPress settings. For now all abilities that are registershow_in_rest = trueare exposed. We should probably have a setting related to the ability exposition but can be a follow.## Organization
Following the pattern established in https://github.com/WordPress/wordpress-develop/pull/10665, this PR adds a new class
WP_Settings_Abilitiesinsrc/wp-includes/abilities/class-wp-settings-abilities.php. The class is organized into:init()method that collects available groups and builds the output schemabuild_output_schema()creates a rich JSON Schema from registered settings metadataregister_get_settings()registers the ability with dynamic schemasexecute_get_settings()retrieves and returns settings grouped by their registration group## Test plan
http://localhost:6888/site-wp-dev-1/wp-admin/post-new.phpenumof available groupsmanage_optionscapability)