| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Abilities API: WP_Abilities_Settings class. 4 * 5 * @package WordPress 6 * @subpackage Abilities API 7 * @since 7.2.0 8 */ 9 10 declare( strict_types = 1 ); 11 12 /** 13 * Core class used to register settings-related abilities. 14 * 15 * Provides the read-only `core/settings-get` ability and the shared building blocks 16 * (exposed-settings discovery and schema generation) that are intended to also back a 17 * future write-oriented `core/settings-update` ability. 18 * 19 * Unlike the core abilities registered directly in wp_register_core_abilities() as 20 * self-contained closures, the settings abilities live in a dedicated class 21 * because they share state: the set of exposed settings is computed once at registration 22 * and reused by the input schema, the output schema, and the execute callback, and the 23 * same helpers are meant to be shared with the future write ability. 24 * 25 * The exposed settings are captured when the ability registers, the first time the abilities 26 * registry is used in a request. Settings registered later in that request are not exposed. 27 * Core registers its own settings in time, see _wp_register_initial_settings_for_abilities(). 28 * 29 * Only register() is public. The ability callbacks are closures that call private 30 * methods, so callers go through the Abilities API, such as 31 * `wp_get_ability( 'core/settings-get' )->execute()`, which validates the input and 32 * checks permissions before running them. 33 * 34 * @since 7.2.0 35 * 36 * @access private 37 */ 38 final class WP_Abilities_Settings { 39 40 /** 41 * The ability category used for settings abilities. 42 * 43 * @since 7.2.0 44 * @var string 45 */ 46 private const CATEGORY = 'site'; 47 48 /** 49 * Settings exposed through the Abilities API, computed once at registration. 50 * 51 * @since 7.2.0 52 * @var array<string, array{option: string, group: string, default: mixed, schema: array<string, mixed>}> 53 */ 54 private $exposed_settings = array(); 55 56 /** 57 * Registers all settings abilities. 58 * 59 * Must run on the `wp_abilities_api_init` hook. Registers nothing when no setting is 60 * exposed to abilities. 61 * 62 * @since 7.2.0 63 */ 64 public function register(): void { 65 $this->exposed_settings = $this->get_exposed_settings(); 66 if ( empty( $this->exposed_settings ) ) { 67 return; 68 } 69 70 $this->register_settings_get(); 71 } 72 73 /** 74 * Registers the read-only `core/settings-get` ability. 75 * 76 * @since 7.2.0 77 */ 78 private function register_settings_get(): void { 79 $groups = array_values( array_unique( array_filter( array_column( $this->exposed_settings, 'group' ) ) ) ); 80 81 wp_register_ability( 82 'core/settings-get', 83 array( 84 'label' => __( 'Get Settings' ), 85 '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. A setting whose value does not match its schema is left out. Requires an authenticated user who can manage options.' ), 86 'category' => self::CATEGORY, 87 'input_schema' => $this->get_settings_get_input_schema( $groups, array_map( 'strval', array_keys( $this->exposed_settings ) ) ), 88 'output_schema' => $this->get_settings_get_output_schema(), 89 'execute_callback' => function ( $input = array() ): array { 90 return $this->execute_settings_get( $input ); 91 }, 92 'required_capabilities' => array( 'manage_options' ), 93 'meta' => array( 94 'annotations' => array( 95 'readonly' => true, 96 'destructive' => false, 97 'idempotent' => true, 98 ), 99 'public' => true, 100 ), 101 ) 102 ); 103 } 104 105 /** 106 * Executes the `core/settings-get` ability. 107 * 108 * @since 7.2.0 109 * 110 * @param mixed $input Optional. The ability input. Default empty array. 111 * @return array<string, mixed> Map of exposed setting name to current value. 112 */ 113 private function execute_settings_get( $input = array() ): array { 114 $input = rest_sanitize_object( $input ); 115 $group = isset( $input['group'] ) && is_string( $input['group'] ) ? $input['group'] : ''; 116 $fields = rest_sanitize_array( $input['fields'] ?? array() ); 117 118 $result = array(); 119 foreach ( $this->exposed_settings as $exposed_name => $setting ) { 120 if ( '' !== $group && $setting['group'] !== $group ) { 121 continue; 122 } 123 if ( ! empty( $fields ) && ! in_array( (string) $exposed_name, $fields, true ) ) { 124 continue; 125 } 126 127 /* 128 * As the settings endpoint does, read a setting that was never saved as its registered 129 * default, or leave it out when it has none: get_option() would return false, which a 130 * boolean schema accepts as a value and an array schema reads as an empty list. 131 */ 132 $value = get_option( $setting['option'], $setting['default'] ); 133 if ( null === $value ) { 134 continue; 135 } 136 137 // WordPress stores false as '', which the boolean schema rejects. 138 if ( '' === $value && 'boolean' === $setting['schema']['type'] ) { 139 $value = false; 140 } 141 142 /* 143 * Leave out a value its schema rejects, before sanitizing (which could make it pass) or 144 * after (which could make it fail), instead of failing output validation for every setting. 145 */ 146 if ( is_wp_error( rest_validate_value_from_schema( $value, $setting['schema'] ) ) ) { 147 continue; 148 } 149 150 $value = rest_sanitize_value_from_schema( $value, $setting['schema'] ); 151 if ( is_wp_error( rest_validate_value_from_schema( $value, $setting['schema'] ) ) ) { 152 continue; 153 } 154 155 // Object (not array()) so an empty object value is serialized as {}, consistent with type:object. 156 $result[ $exposed_name ] = 'object' === $setting['schema']['type'] ? (object) $value : $value; 157 } 158 159 return $result; 160 } 161 162 /** 163 * Builds the input schema for the `core/settings-get` ability: optional filters by group and/or name. 164 * 165 * Both `group` and `fields` are optional; supplying both narrows the response to their 166 * intersection, and supplying neither returns every exposed setting. 167 * 168 * @since 7.2.0 169 * 170 * @param list<string> $groups Available settings groups. 171 * @param list<string> $field_names Available exposed setting names. 172 * @return array<string, mixed> The input JSON Schema. 173 */ 174 private function get_settings_get_input_schema( array $groups, array $field_names ): array { 175 return array( 176 'type' => 'object', 177 'default' => array(), 178 'properties' => array( 179 'group' => array( 180 'type' => 'string', 181 'enum' => $groups, 182 'description' => __( 'Return only settings that belong to this settings group. If omitted, settings of every group are returned.' ), 183 ), 184 'fields' => array( 185 'type' => 'array', 186 'uniqueItems' => true, 187 'items' => array( 188 'type' => 'string', 189 'enum' => $field_names, 190 ), 191 'description' => __( 'Return only the settings with these names. If omitted or empty, all settings allowed by the group filter are returned. When both filters are given, only settings matching both are returned.' ), 192 ), 193 ), 194 'additionalProperties' => false, 195 ); 196 } 197 198 /** 199 * Builds the output schema for the `core/settings-get` ability. 200 * 201 * No setting is marked required because the `group` and `fields` inputs let the caller 202 * request any subset, and a setting whose value does not match its schema is left out. 203 * 204 * @since 7.2.0 205 * 206 * @return array<string, mixed> The output JSON Schema. 207 */ 208 private function get_settings_get_output_schema(): array { 209 return array( 210 'type' => 'object', 211 'description' => __( 'A map of setting name to its current value.' ), 212 'properties' => wp_list_pluck( $this->exposed_settings, 'schema' ), 213 'additionalProperties' => false, 214 ); 215 } 216 217 /** 218 * Returns the settings exposed through the Abilities API. 219 * 220 * Reads {@see get_registered_settings()} and keeps only settings flagged with a truthy 221 * `show_in_abilities` argument, of a type the settings endpoint supports. Each entry is 222 * keyed by its exposed name and carries the underlying option name, the settings group, 223 * the registered default (null when there is none), and a JSON Schema describing the value. 224 * 225 * Two settings exposed under the same name trigger a notice, and the later one is exposed. 226 * 227 * @since 7.2.0 228 * 229 * @return array<string, array{option: string, group: string, default: mixed, schema: array<string, mixed>}> Settings keyed by exposed name. 230 */ 231 private function get_exposed_settings(): array { 232 $settings = array(); 233 234 foreach ( get_registered_settings() as $option_name => $args ) { 235 if ( empty( $args['show_in_abilities'] ) ) { 236 continue; 237 } 238 239 $show = $this->get_exposure_args( $args ); 240 241 $schema = $this->value_schema( $args, $show ); 242 if ( ! in_array( $schema['type'], array( 'number', 'integer', 'string', 'boolean', 'array', 'object' ), true ) ) { 243 continue; 244 } 245 246 $option_name = (string) $option_name; 247 $exposed_name = empty( $show['name'] ) ? $option_name : (string) $show['name']; 248 249 if ( isset( $settings[ $exposed_name ] ) ) { 250 _doing_it_wrong( 251 __METHOD__, 252 sprintf( 253 /* translators: 1: Option name, 2: Setting name exposed to abilities, 3: Option name. */ 254 __( 'The "%1$s" setting is exposed to abilities as "%2$s", a name already used by the "%3$s" setting, which is no longer exposed.' ), 255 esc_html( $option_name ), 256 esc_html( $exposed_name ), 257 esc_html( $settings[ $exposed_name ]['option'] ) 258 ), 259 '7.2.0' 260 ); 261 } 262 263 $settings[ $exposed_name ] = array( 264 'option' => $option_name, 265 'group' => $args['group'] ?? '', 266 'default' => $args['default'] ?? null, 267 'schema' => $schema, 268 ); 269 } 270 271 return $settings; 272 } 273 274 /** 275 * Returns the name and schema overrides used to expose a setting to abilities. 276 * 277 * When `show_in_abilities` is `true`, the setting is exposed the same way as in the 278 * REST API: it uses the `name` and `schema` from `show_in_rest`. An array is used 279 * as is. 280 * 281 * @since 7.2.0 282 * 283 * @param array<string, mixed> $args The setting registration arguments. 284 * @return array<string, mixed> The exposure arguments, with optional `name` and `schema` keys. 285 */ 286 private function get_exposure_args( array $args ): array { 287 if ( is_array( $args['show_in_abilities'] ) ) { 288 return $args['show_in_abilities']; 289 } 290 291 return is_array( $args['show_in_rest'] ) ? $args['show_in_rest'] : array(); 292 } 293 294 /** 295 * Builds the JSON Schema describing a single setting's value. 296 * 297 * As in the settings endpoint, objects in the schema reject properties they do not declare, 298 * unless the schema allows them. 299 * 300 * @since 7.2.0 301 * 302 * @param array<string, mixed> $args The setting registration arguments. 303 * @param array<string, mixed> $show The exposure arguments, see get_exposure_args(). 304 * @return array<string, mixed> The value JSON Schema. 305 */ 306 private function value_schema( array $args, $show ): array { 307 $schema = array( 308 'type' => $args['type'], 309 ); 310 if ( ! empty( $args['label'] ) ) { 311 $schema['title'] = $args['label']; 312 } 313 if ( ! empty( $args['description'] ) ) { 314 $schema['description'] = $args['description']; 315 } 316 if ( isset( $show['schema'] ) && is_array( $show['schema'] ) ) { 317 /** @var array<string, mixed> $show_schema */ 318 $show_schema = $show['schema']; 319 $schema = array_merge( $schema, $show_schema ); 320 } 321 322 return rest_default_additional_properties_to_false( $schema ); 323 } 324 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Tue Nov 3 08:20:36 2026 | Cross-referenced by PHPXref |