44 *
55 * @package WordPress\AI
66 *
7- * @since 1.1.0
7+ * @since x.x.x
88 */
99
1010declare ( strict_types=1 );
2424 *
2525 * This class is kept almost identical to the WordPress core class `WP_Settings_Abilities`
2626 * so the two implementations stay in sync. Differences from the core class are marked with
27- * `// Plugin:` comments. Additionally, all user-facing strings use esc_html__() with the
28- * 'ai' text domain rather than core's __().
27+ * `// Plugin:` comments. Additionally, all user-facing strings use the 'ai' text domain.
2928 *
3029 * @internal This class should not be used outside the plugin and there is no guarantee of backwards compatibility.
3130 *
32- * @since 1.1.0
31+ * @since x.x.x
3332 */
3433class Settings {
3534
3635 /**
3736 * The ability category used for settings abilities.
3837 *
39- * @since 1.1.0
38+ * @since x.x.x
4039 * @var string
4140 */
4241 public const CATEGORY = 'site ' ;
4342
43+ /**
44+ * Settings exposed through the Abilities API, computed once at registration.
45+ *
46+ * Plugin: cached so the input/output schema and the executed result derive from the exact
47+ * same structure, and {@see get_registered_settings()} is only walked once per request.
48+ *
49+ * @since x.x.x
50+ * @var array<string, array{option: string, group: string, default: mixed, schema: array<string, mixed>}>|null
51+ */
52+ private static $ exposed_settings = null ;
53+
4454 /**
4555 * Hooks the ability into the Abilities API.
4656 *
4757 * Plugin: this method has no equivalent in the core class. In core, register() is
4858 * invoked directly from wp_register_core_abilities() (already on the
4959 * `wp_abilities_api_init` hook). The plugin instead hooks register() slightly later
50- * (priority 11) so it can override any core-provided copy, and registers the category
51- * as a fallback in case core has not.
60+ * (priority 11) so it can override any core-provided copy.
5261 *
53- * @since 1.1.0
62+ * @since x.x.x
5463 */
5564 public static function init (): void {
56- add_action ( 'wp_abilities_api_categories_init ' , array ( self ::class, 'register_category ' ), 11 );
5765 add_action ( 'wp_abilities_api_init ' , array ( self ::class, 'register ' ), 11 );
5866 }
5967
60- /**
61- * Registers the `site` ability category if it is not already registered.
62- *
63- * Plugin: this method has no equivalent in the core class; core relies on
64- * wp_register_core_ability_categories() to register the `site` category.
65- *
66- * @since 1.1.0
67- */
68- public static function register_category (): void {
69- if ( wp_has_ability_category ( self ::CATEGORY ) ) {
70- return ;
71- }
72-
73- wp_register_ability_category (
74- self ::CATEGORY ,
75- array (
76- 'label ' => esc_html__ ( 'Site ' , 'ai ' ),
77- 'description ' => esc_html__ ( 'Abilities that retrieve or modify site information and settings. ' , 'ai ' ),
78- )
79- );
80- }
81-
8268 /**
8369 * Registers all settings abilities.
8470 *
8571 * Must run on the `wp_abilities_api_init` hook.
8672 *
87- * @since 1.1.0
73+ * @since x.x.x
8874 */
8975 public static function register (): void {
9076 self ::register_get_settings ();
@@ -100,32 +86,39 @@ public static function register(): void {
10086 /**
10187 * Registers the read-only `core/settings` ability.
10288 *
103- * @since 1.1.0
89+ * @since x.x.x
10490 */
10591 public static function register_get_settings (): void {
10692 // Plugin: unregister any core-provided copy first so the plugin's version wins.
10793 if ( wp_has_ability ( 'core/settings ' ) ) {
10894 wp_unregister_ability ( 'core/settings ' );
10995 }
11096
111- $ settings = self ::get_exposed_settings ();
112- $ groups = array_values ( array_unique ( array_filter ( wp_list_pluck ( $ settings , 'group ' ) ) ) );
113- $ setting_names = array_keys ( $ settings );
114- $ properties = array ();
97+ // Compute once; execute_get_settings() reuses this exact structure.
98+ self ::$ exposed_settings = self ::get_exposed_settings ();
99+
100+ $ settings = self ::$ exposed_settings ;
101+ $ field_names = array_keys ( $ settings );
102+ $ groups = array ();
103+ $ properties = array ();
115104 foreach ( $ settings as $ exposed_name => $ setting ) {
116105 $ properties [ $ exposed_name ] = $ setting ['schema ' ];
106+ if ( '' === $ setting ['group ' ] || in_array ( $ setting ['group ' ], $ groups , true ) ) {
107+ continue ;
108+ }
109+ $ groups [] = $ setting ['group ' ];
117110 }
118111
119112 wp_register_ability (
120113 'core/settings ' ,
121114 array (
122- 'label ' => esc_html__ ( 'Get Settings ' , 'ai ' ),
123- 'description ' => esc_html__ ( '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. ' , 'ai ' ),
115+ 'label ' => __ ( 'Get Settings ' , 'ai ' ),
116+ '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 . ' , 'ai ' ),
124117 'category ' => self ::CATEGORY ,
125- 'input_schema ' => self ::get_settings_input_schema ( $ groups , $ setting_names ),
118+ 'input_schema ' => self ::get_settings_input_schema ( $ groups , $ field_names ),
126119 'output_schema ' => array (
127120 'type ' => 'object ' ,
128- 'description ' => esc_html__ ( 'A map of setting name to its current value. ' , 'ai ' ),
121+ 'description ' => __ ( 'A map of setting name to its current value. ' , 'ai ' ),
129122 'properties ' => $ properties ,
130123 'additionalProperties ' => false ,
131124 ),
@@ -146,28 +139,28 @@ public static function register_get_settings(): void {
146139 /**
147140 * Executes the `core/settings` ability.
148141 *
149- * @since 1.1.0
142+ * @since x.x.x
150143 *
151144 * @param mixed $input Optional. The ability input. Default empty array.
152145 * @return array<string, mixed> Map of exposed setting name to current value.
153146 */
154147 public static function execute_get_settings ( $ input = array () ): array {
155148 $ input = is_array ( $ input ) ? $ input : array ();
156149
157- $ settings = self ::get_exposed_settings ();
158- $ group = isset ( $ input ['group ' ] ) ? ( string ) $ input ['group ' ] : '' ;
159- $ names = isset ( $ input ['settings ' ] ) && is_array ( $ input ['settings ' ] ) ? $ input ['settings ' ] : array ();
150+ $ settings = self ::$ exposed_settings ?? self :: get_exposed_settings ();
151+ $ group = isset ( $ input ['group ' ] ) && is_string ( $ input [ ' group ' ] ) ? $ input ['group ' ] : '' ;
152+ $ fields = isset ( $ input ['fields ' ] ) && is_array ( $ input ['fields ' ] ) ? $ input ['fields ' ] : array ();
160153
161154 $ result = array ();
162155 foreach ( $ settings as $ exposed_name => $ setting ) {
163156 if ( '' !== $ group && $ setting ['group ' ] !== $ group ) {
164157 continue ;
165158 }
166- if ( ! empty ( $ names ) && ! in_array ( $ exposed_name , $ names , true ) ) {
159+ if ( ! empty ( $ fields ) && ! in_array ( $ exposed_name , $ fields , true ) ) {
167160 continue ;
168161 }
169162
170- $ type = isset ( $ setting ['schema ' ]['type ' ] ) ? ( string ) $ setting ['schema ' ]['type ' ] : 'string ' ;
163+ $ type = isset ( $ setting ['schema ' ]['type ' ] ) && is_string ( $ setting [ ' schema ' ][ ' type ' ] ) ? $ setting ['schema ' ]['type ' ] : 'string ' ;
171164 $ value = get_option ( $ setting ['option ' ], $ setting ['default ' ] );
172165
173166 $ result [ $ exposed_name ] = self ::cast_value ( $ value , $ type );
@@ -179,7 +172,7 @@ public static function execute_get_settings( $input = array() ): array {
179172 /**
180173 * Checks whether the current user may use the settings abilities.
181174 *
182- * @since 1.1.0
175+ * @since x.x.x
183176 *
184177 * @return bool True if the current user can manage options.
185178 */
@@ -188,55 +181,38 @@ public static function has_permission(): bool {
188181 }
189182
190183 /**
191- * Builds the input schema for the get ability: filter by group XOR by name.
184+ * Builds the input schema for the get ability: optional filters by group and/or name.
192185 *
193- * @since 1.1.0
186+ * Both `group` and `fields` are optional; supplying both narrows the response to their
187+ * intersection, and supplying neither returns every exposed setting.
194188 *
195- * @param string[] $groups Available settings groups.
196- * @param string[] $setting_names Available exposed setting names.
189+ * @since x.x.x
190+ *
191+ * @param string[] $groups Available settings groups.
192+ * @param string[] $field_names Available exposed setting names.
197193 * @return array<string, mixed> The input JSON Schema.
198194 */
199- protected static function get_settings_input_schema ( array $ groups , array $ setting_names ): array {
195+ protected static function get_settings_input_schema ( array $ groups , array $ field_names ): array {
200196 return array (
201- 'type ' => 'object ' ,
202- ' default ' => array (),
203- // Filter by group OR by name, but not both at once.
204- 'oneOf ' => array (
205- array (
206- 'title ' => esc_html__ ( ' All settings ' , ' ai ' ) ,
207- 'type ' => ' object ' ,
208- 'additionalProperties ' => false ,
197+ 'type ' => 'object ' ,
198+ // Object (not array()) so the serialized schema default is {}, consistent with type:object.
199+ ' default ' => ( object ) array (),
200+ 'properties ' => array (
201+ ' group ' => array (
202+ 'type ' => ' string ' ,
203+ 'enum ' => $ groups ,
204+ 'description ' => __ ( ' Return only settings that belong to this settings group. ' , ' ai ' ) ,
209205 ),
210- array (
211- 'title ' => esc_html__ ( 'Filter by group ' , 'ai ' ),
212- 'type ' => 'object ' ,
213- 'required ' => array ( 'group ' ),
214- 'properties ' => array (
215- 'group ' => array (
216- 'type ' => 'string ' ,
217- 'enum ' => $ groups ,
218- 'description ' => esc_html__ ( 'Return only settings that belong to this settings group. ' , 'ai ' ),
219- ),
206+ 'fields ' => array (
207+ 'type ' => 'array ' ,
208+ 'items ' => array (
209+ 'type ' => 'string ' ,
210+ 'enum ' => $ field_names ,
220211 ),
221- 'additionalProperties ' => false ,
222- ),
223- array (
224- 'title ' => esc_html__ ( 'Filter by name ' , 'ai ' ),
225- 'type ' => 'object ' ,
226- 'required ' => array ( 'settings ' ),
227- 'properties ' => array (
228- 'settings ' => array (
229- 'type ' => 'array ' ,
230- 'items ' => array (
231- 'type ' => 'string ' ,
232- 'enum ' => $ setting_names ,
233- ),
234- 'description ' => esc_html__ ( 'Return only the settings with these names. ' , 'ai ' ),
235- ),
236- ),
237- 'additionalProperties ' => false ,
212+ 'description ' => __ ( 'Return only the settings with these names. ' , 'ai ' ),
238213 ),
239214 ),
215+ 'additionalProperties ' => false ,
240216 );
241217 }
242218
@@ -248,7 +224,7 @@ protected static function get_settings_input_schema( array $groups, array $setti
248224 * underlying option name, the settings group, the registration default, and a JSON Schema
249225 * describing the value.
250226 *
251- * @since 1.1.0
227+ * @since x.x.x
252228 *
253229 * @return array<string, array{option: string, group: string, default: mixed, schema: array<string, mixed>}> Settings keyed by exposed name.
254230 */
@@ -262,11 +238,11 @@ protected static function get_exposed_settings(): array {
262238 }
263239
264240 $ option_name = (string ) $ option_name ;
265- $ exposed_name = is_array ( $ show ) && ! empty ( $ show ['name ' ] ) ? ( string ) $ show ['name ' ] : $ option_name ;
241+ $ exposed_name = is_array ( $ show ) && isset ( $ show ['name ' ] ) && is_string ( $ show [ ' name ' ] ) && '' !== $ show [ ' name ' ] ? $ show ['name ' ] : $ option_name ;
266242
267243 $ settings [ $ exposed_name ] = array (
268244 'option ' => $ option_name ,
269- 'group ' => isset ( $ args ['group ' ] ) ? ( string ) $ args ['group ' ] : '' ,
245+ 'group ' => isset ( $ args ['group ' ] ) && is_string ( $ args [ ' group ' ] ) ? $ args ['group ' ] : '' ,
270246 'default ' => array_key_exists ( 'default ' , $ args ) ? $ args ['default ' ] : false ,
271247 'schema ' => self ::value_schema ( $ args , $ show ),
272248 );
@@ -278,15 +254,15 @@ protected static function get_exposed_settings(): array {
278254 /**
279255 * Builds the JSON Schema describing a single setting's value.
280256 *
281- * @since 1.1.0
257+ * @since x.x.x
282258 *
283259 * @param array<string, mixed> $args The setting registration arguments.
284260 * @param bool|array<string, mixed> $show The setting's `show_in_abilities` value.
285261 * @return array<string, mixed> The value JSON Schema.
286262 */
287263 protected static function value_schema ( array $ args , $ show ): array {
288264 $ schema = array (
289- 'type ' => isset ( $ args ['type ' ] ) ? ( string ) $ args ['type ' ] : 'string ' ,
265+ 'type ' => isset ( $ args ['type ' ] ) && is_string ( $ args [ ' type ' ] ) ? $ args ['type ' ] : 'string ' ,
290266 );
291267 if ( ! empty ( $ args ['label ' ] ) ) {
292268 $ schema ['title ' ] = $ args ['label ' ];
@@ -295,7 +271,9 @@ protected static function value_schema( array $args, $show ): array {
295271 $ schema ['description ' ] = $ args ['description ' ];
296272 }
297273 if ( is_array ( $ show ) && isset ( $ show ['schema ' ] ) && is_array ( $ show ['schema ' ] ) ) {
298- $ schema = array_merge ( $ schema , $ show ['schema ' ] );
274+ /** @var array<string, mixed> $show_schema */
275+ $ show_schema = $ show ['schema ' ];
276+ $ schema = array_merge ( $ schema , $ show_schema );
299277 }
300278
301279 return $ schema ;
@@ -304,7 +282,7 @@ protected static function value_schema( array $args, $show ): array {
304282 /**
305283 * Casts a stored option value to the type declared in its settings registration.
306284 *
307- * @since 1.1.0
285+ * @since x.x.x
308286 *
309287 * @param mixed $value The raw option value.
310288 * @param string $type The registered setting type.
@@ -315,12 +293,15 @@ protected static function cast_value( $value, string $type ) {
315293 case 'boolean ' :
316294 return (bool ) $ value ;
317295 case 'integer ' :
318- return ( int ) $ value ;
296+ return is_scalar ( $ value ) ? ( int ) $ value : 0 ;
319297 case 'number ' :
320- return ( float ) $ value ;
298+ return is_scalar ( $ value ) ? ( float ) $ value : 0.0 ;
321299 case 'array ' :
322- case 'object ' :
323300 return is_array ( $ value ) ? $ value : array ();
301+ case 'object ' :
302+ // Cast to object so an empty/non-array value serializes as {} (not []) and
303+ // satisfies the `object` output schema validated by execute().
304+ return (object ) ( is_array ( $ value ) ? $ value : array () );
324305 default :
325306 return is_scalar ( $ value ) ? (string ) $ value : $ value ;
326307 }
0 commit comments