Opened 5 months ago
Last modified 31 hours ago
#64605 reopened enhancement
Add WordPress core/read-settings 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/read-settings 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 (27)
This ticket was mentioned in PR #10747 on WordPress/wordpress-develop by @jorgefilipecosta.
5 months ago
#1
- Keywords has-patch has-unit-tests added
@jorgefilipecosta commented on PR #10747:
5 months ago
#3
This ticket was mentioned in PR #10892 on WordPress/wordpress-develop by @jorgefilipecosta.
5 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:
5 months ago
#5
Thank you @westonruter for reviewing this PR, all your suggestions were applied.
#6
@
5 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:
5 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
@
5 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
@
4 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
@
3 months ago
- Component AI → Abilities API
Moving tickets related to the Abilities API to a new sub-component.
#15
@
8 weeks ago
Removing trunk version as this is not going to be shipped with WP 7.0 but in the next releases.
#16
@
8 weeks ago
- Keywords abilities removed
Removing abilities and abilities-api custom keywords. This is now indicated by the Abilities API component.
#17
@
8 weeks 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 weeks 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 weeks 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 weeks ago
#20
Let's run development, review, and testing through https://github.com/WordPress/ai/pull/691, then sync all agreed refinements here.
#21
@
2 weeks ago
- Description modified (diff)
- Summary Add WordPress Core get settings ability → Add WordPress core/read-settings ability
@jorgefilipecosta commented on PR #12141:
8 days 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:
8 days ago
#23
All the reviews comments were applied this is ready for another look.
@gziolo commented on PR #12141:
2 days 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:
2 days 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:
2 days 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
@
31 hours ago
- Milestone 7.1 → 7.2
Moved to WordPress 7.2 based on the decision made after evaluating feedback on the merge proposal.
![(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)