Skip to content

Commit 08fc8aa

Browse files
Abilities API: refine the core/settings ability per review.
- Simplify the input schema: replace the group-XOR-name `oneOf` with optional, combinable `group` and `fields` filters (rename `slugs` -> `fields`, matching core/get-site-info). Default to an empty object so the type:object schema default serializes as {}. - Memoize the exposed settings so the input/output schema and execute() derive from a single walk of get_registered_settings(). - Cast object-typed values to objects so they serialize as {} (not []) and satisfy the output schema validated by execute(). - Harden value handling against loosely-typed registration data. - Tests: assert keys order-insensitively and cover combined group+fields filtering.
1 parent d9ff702 commit 08fc8aa

2 files changed

Lines changed: 84 additions & 82 deletions

File tree

src/wp-includes/abilities/class-wp-settings-abilities.php

Lines changed: 63 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ class WP_Settings_Abilities {
3434
*/
3535
const CATEGORY = 'site';
3636

37+
/**
38+
* Settings exposed through the Abilities API, computed once at registration.
39+
*
40+
* Cached so the input/output schema and the executed result derive from the exact same
41+
* structure, and {@see get_registered_settings()} is only walked once per request.
42+
*
43+
* @since 7.1.0
44+
* @var array<string, array{option: string, group: string, default: mixed, schema: array<string, mixed>}>|null
45+
*/
46+
private static $exposed_settings = null;
47+
3748
/**
3849
* Registers all settings abilities.
3950
*
@@ -58,21 +69,28 @@ public static function register(): void {
5869
* @since 7.1.0
5970
*/
6071
public static function register_get_settings(): void {
61-
$settings = self::get_exposed_settings();
62-
$groups = array_values( array_unique( array_filter( wp_list_pluck( $settings, 'group' ) ) ) );
63-
$setting_names = array_keys( $settings );
64-
$properties = array();
72+
// Compute once; execute_get_settings() reuses this exact structure.
73+
self::$exposed_settings = self::get_exposed_settings();
74+
75+
$settings = self::$exposed_settings;
76+
$field_names = array_keys( $settings );
77+
$groups = array();
78+
$properties = array();
6579
foreach ( $settings as $exposed_name => $setting ) {
6680
$properties[ $exposed_name ] = $setting['schema'];
81+
if ( '' === $setting['group'] || in_array( $setting['group'], $groups, true ) ) {
82+
continue;
83+
}
84+
$groups[] = $setting['group'];
6785
}
6886

6987
wp_register_ability(
7088
'core/settings',
7189
array(
7290
'label' => __( 'Get Settings' ),
73-
'description' => __( 'Returns WordPress settings as a flat map of setting name to value. By default returns all settings exposed to abilities, or optionally a subset filtered by settings group or by setting name.' ),
91+
'description' => __( 'Returns WordPress settings as a flat map of setting name to value. By default returns all settings exposed to abilities, or optionally a subset filtered by settings group, by setting name, or both.' ),
7492
'category' => self::CATEGORY,
75-
'input_schema' => self::get_settings_input_schema( $groups, $setting_names ),
93+
'input_schema' => self::get_settings_input_schema( $groups, $field_names ),
7694
'output_schema' => array(
7795
'type' => 'object',
7896
'description' => __( 'A map of setting name to its current value.' ),
@@ -104,20 +122,20 @@ public static function register_get_settings(): void {
104122
public static function execute_get_settings( $input = array() ): array {
105123
$input = is_array( $input ) ? $input : array();
106124

107-
$settings = self::get_exposed_settings();
108-
$group = isset( $input['group'] ) ? (string) $input['group'] : '';
109-
$names = isset( $input['settings'] ) && is_array( $input['settings'] ) ? $input['settings'] : array();
125+
$settings = self::$exposed_settings ?? self::get_exposed_settings();
126+
$group = isset( $input['group'] ) && is_string( $input['group'] ) ? $input['group'] : '';
127+
$fields = isset( $input['fields'] ) && is_array( $input['fields'] ) ? $input['fields'] : array();
110128

111129
$result = array();
112130
foreach ( $settings as $exposed_name => $setting ) {
113131
if ( '' !== $group && $setting['group'] !== $group ) {
114132
continue;
115133
}
116-
if ( ! empty( $names ) && ! in_array( $exposed_name, $names, true ) ) {
134+
if ( ! empty( $fields ) && ! in_array( $exposed_name, $fields, true ) ) {
117135
continue;
118136
}
119137

120-
$type = isset( $setting['schema']['type'] ) ? (string) $setting['schema']['type'] : 'string';
138+
$type = isset( $setting['schema']['type'] ) && is_string( $setting['schema']['type'] ) ? $setting['schema']['type'] : 'string';
121139
$value = get_option( $setting['option'], $setting['default'] );
122140

123141
$result[ $exposed_name ] = self::cast_value( $value, $type );
@@ -138,55 +156,38 @@ public static function has_permission(): bool {
138156
}
139157

140158
/**
141-
* Builds the input schema for the get ability: filter by group XOR by name.
159+
* Builds the input schema for the get ability: optional filters by group and/or name.
160+
*
161+
* Both `group` and `fields` are optional; supplying both narrows the response to their
162+
* intersection, and supplying neither returns every exposed setting.
142163
*
143164
* @since 7.1.0
144165
*
145-
* @param string[] $groups Available settings groups.
146-
* @param string[] $setting_names Available exposed setting names.
166+
* @param string[] $groups Available settings groups.
167+
* @param string[] $field_names Available exposed setting names.
147168
* @return array<string, mixed> The input JSON Schema.
148169
*/
149-
protected static function get_settings_input_schema( array $groups, array $setting_names ): array {
170+
protected static function get_settings_input_schema( array $groups, array $field_names ): array {
150171
return array(
151-
'type' => 'object',
152-
'default' => array(),
153-
// Filter by group OR by name, but not both at once.
154-
'oneOf' => array(
155-
array(
156-
'title' => __( 'All settings' ),
157-
'type' => 'object',
158-
'additionalProperties' => false,
159-
),
160-
array(
161-
'title' => __( 'Filter by group' ),
162-
'type' => 'object',
163-
'required' => array( 'group' ),
164-
'properties' => array(
165-
'group' => array(
166-
'type' => 'string',
167-
'enum' => $groups,
168-
'description' => __( 'Return only settings that belong to this settings group.' ),
169-
),
170-
),
171-
'additionalProperties' => false,
172+
'type' => 'object',
173+
// Object (not array()) so the serialized schema default is {}, consistent with type:object.
174+
'default' => (object) array(),
175+
'properties' => array(
176+
'group' => array(
177+
'type' => 'string',
178+
'enum' => $groups,
179+
'description' => __( 'Return only settings that belong to this settings group.' ),
172180
),
173-
array(
174-
'title' => __( 'Filter by name' ),
175-
'type' => 'object',
176-
'required' => array( 'settings' ),
177-
'properties' => array(
178-
'settings' => array(
179-
'type' => 'array',
180-
'items' => array(
181-
'type' => 'string',
182-
'enum' => $setting_names,
183-
),
184-
'description' => __( 'Return only the settings with these names.' ),
185-
),
181+
'fields' => array(
182+
'type' => 'array',
183+
'items' => array(
184+
'type' => 'string',
185+
'enum' => $field_names,
186186
),
187-
'additionalProperties' => false,
187+
'description' => __( 'Return only the settings with these names.' ),
188188
),
189189
),
190+
'additionalProperties' => false,
190191
);
191192
}
192193

@@ -212,11 +213,11 @@ protected static function get_exposed_settings(): array {
212213
}
213214

214215
$option_name = (string) $option_name;
215-
$exposed_name = is_array( $show ) && ! empty( $show['name'] ) ? (string) $show['name'] : $option_name;
216+
$exposed_name = is_array( $show ) && isset( $show['name'] ) && is_string( $show['name'] ) && '' !== $show['name'] ? $show['name'] : $option_name;
216217

217218
$settings[ $exposed_name ] = array(
218219
'option' => $option_name,
219-
'group' => isset( $args['group'] ) ? (string) $args['group'] : '',
220+
'group' => isset( $args['group'] ) && is_string( $args['group'] ) ? $args['group'] : '',
220221
'default' => array_key_exists( 'default', $args ) ? $args['default'] : false,
221222
'schema' => self::value_schema( $args, $show ),
222223
);
@@ -236,7 +237,7 @@ protected static function get_exposed_settings(): array {
236237
*/
237238
protected static function value_schema( array $args, $show ): array {
238239
$schema = array(
239-
'type' => isset( $args['type'] ) ? (string) $args['type'] : 'string',
240+
'type' => isset( $args['type'] ) && is_string( $args['type'] ) ? $args['type'] : 'string',
240241
);
241242
if ( ! empty( $args['label'] ) ) {
242243
$schema['title'] = $args['label'];
@@ -245,7 +246,9 @@ protected static function value_schema( array $args, $show ): array {
245246
$schema['description'] = $args['description'];
246247
}
247248
if ( is_array( $show ) && isset( $show['schema'] ) && is_array( $show['schema'] ) ) {
248-
$schema = array_merge( $schema, $show['schema'] );
249+
/** @var array<string, mixed> $show_schema */
250+
$show_schema = $show['schema'];
251+
$schema = array_merge( $schema, $show_schema );
249252
}
250253

251254
return $schema;
@@ -265,12 +268,15 @@ protected static function cast_value( $value, string $type ) {
265268
case 'boolean':
266269
return (bool) $value;
267270
case 'integer':
268-
return (int) $value;
271+
return is_scalar( $value ) ? (int) $value : 0;
269272
case 'number':
270-
return (float) $value;
273+
return is_scalar( $value ) ? (float) $value : 0.0;
271274
case 'array':
272-
case 'object':
273275
return is_array( $value ) ? $value : array();
276+
case 'object':
277+
// Cast to object so an empty/non-array value serializes as {} (not []) and
278+
// satisfies the `object` output schema validated by execute().
279+
return (object) ( is_array( $value ) ? $value : array() );
274280
default:
275281
return is_scalar( $value ) ? (string) $value : $value;
276282
}

tests/phpunit/tests/abilities-api/wpRegisterCoreSettingsAbility.php

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -95,26 +95,22 @@ public function test_core_settings_ability_is_registered(): void {
9595
}
9696

9797
/**
98-
* The input schema exposes mutually exclusive `group` and `settings` filters.
98+
* The input schema exposes optional `group` and `fields` filters.
9999
*
100100
* @ticket 64146
101101
*/
102-
public function test_core_settings_input_schema_is_one_of_group_or_settings(): void {
102+
public function test_core_settings_input_schema_exposes_group_and_fields_filters(): void {
103103
$schema = wp_get_ability( 'core/settings' )->get_input_schema();
104104

105105
$this->assertSame( 'object', $schema['type'] );
106106
$this->assertArrayHasKey( 'default', $schema );
107-
$this->assertCount( 3, $schema['oneOf'] );
107+
$this->assertArrayNotHasKey( 'oneOf', $schema );
108108

109-
$group_branch = $schema['oneOf'][1];
110-
$this->assertSame( array( 'group' ), $group_branch['required'] );
111-
$this->assertContains( 'general', $group_branch['properties']['group']['enum'] );
112-
$this->assertContains( 'reading', $group_branch['properties']['group']['enum'] );
109+
$this->assertContains( 'general', $schema['properties']['group']['enum'] );
110+
$this->assertContains( 'reading', $schema['properties']['group']['enum'] );
113111

114-
$settings_branch = $schema['oneOf'][2];
115-
$this->assertSame( array( 'settings' ), $settings_branch['required'] );
116-
$this->assertContains( 'blogname', $settings_branch['properties']['settings']['items']['enum'] );
117-
$this->assertContains( 'posts_per_page', $settings_branch['properties']['settings']['items']['enum'] );
112+
$this->assertContains( 'blogname', $schema['properties']['fields']['items']['enum'] );
113+
$this->assertContains( 'posts_per_page', $schema['properties']['fields']['items']['enum'] );
118114
}
119115

120116
/**
@@ -152,35 +148,36 @@ public function test_core_settings_filters_by_group(): void {
152148
}
153149

154150
/**
155-
* The `settings` filter narrows the response to the requested setting names.
151+
* The `fields` filter narrows the response to the requested setting names.
156152
*
157153
* @ticket 64146
158154
*/
159-
public function test_core_settings_filters_by_settings(): void {
155+
public function test_core_settings_filters_by_fields(): void {
160156
$this->become_admin();
161157

162-
$result = wp_get_ability( 'core/settings' )->execute( array( 'settings' => array( 'blogname', 'posts_per_page' ) ) );
158+
$result = wp_get_ability( 'core/settings' )->execute( array( 'fields' => array( 'blogname', 'posts_per_page' ) ) );
163159

164-
$this->assertSame( array( 'blogname', 'posts_per_page' ), array_keys( $result ) );
160+
$this->assertEqualSets( array( 'blogname', 'posts_per_page' ), array_keys( $result ) );
165161
}
166162

167163
/**
168-
* Supplying both `group` and `settings` violates the `oneOf` and is rejected.
164+
* Supplying both `group` and `fields` narrows the response to their intersection.
169165
*
170166
* @ticket 64146
171167
*/
172-
public function test_core_settings_rejects_group_and_settings_together(): void {
168+
public function test_core_settings_combines_group_and_fields_filters(): void {
173169
$this->become_admin();
174170

171+
// `blogname` is in the `general` group and `posts_per_page` in `reading`; only the
172+
// latter satisfies both filters.
175173
$result = wp_get_ability( 'core/settings' )->execute(
176174
array(
177-
'group' => 'reading',
178-
'settings' => array( 'blogname' ),
175+
'group' => 'reading',
176+
'fields' => array( 'blogname', 'posts_per_page' ),
179177
)
180178
);
181179

182-
$this->assertWPError( $result );
183-
$this->assertSame( 'ability_invalid_input', $result->get_error_code() );
180+
$this->assertEqualSets( array( 'posts_per_page' ), array_keys( $result ) );
184181
}
185182

186183
/**
@@ -205,16 +202,15 @@ public function test_core_settings_requires_manage_options(): void {
205202
public function test_core_settings_exposes_a_custom_registered_setting(): void {
206203
$ability = wp_get_ability( 'core/settings' );
207204

208-
// Present in both the input `settings` enum and the output schema built at registration.
209-
$settings_branch = $ability->get_input_schema()['oneOf'][2];
210-
$this->assertContains( 'core_settings_ability_test_option', $settings_branch['properties']['settings']['items']['enum'] );
205+
// Present in both the input `fields` enum and the output schema built at registration.
206+
$this->assertContains( 'core_settings_ability_test_option', $ability->get_input_schema()['properties']['fields']['items']['enum'] );
211207
$this->assertArrayHasKey( 'core_settings_ability_test_option', $ability->get_output_schema()['properties'] );
212208

213209
// And returned, correctly typed, by execute.
214210
$this->become_admin();
215211
update_option( 'core_settings_ability_test_option', 7 );
216212

217-
$result = $ability->execute( array( 'settings' => array( 'core_settings_ability_test_option' ) ) );
213+
$result = $ability->execute( array( 'fields' => array( 'core_settings_ability_test_option' ) ) );
218214

219215
$this->assertSame( array( 'core_settings_ability_test_option' => 7 ), $result );
220216
}

0 commit comments

Comments
 (0)