[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/abilities/ -> class-wp-abilities-settings.php (source)

   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 other core abilities, which are self-contained closures registered directly
  20   * in wp_register_core_abilities(), 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                  'permission_callback' => function (): bool {
  93                      return $this->check_permission();
  94                  },
  95                  'meta'                => array(
  96                      'annotations' => array(
  97                          'readonly'    => true,
  98                          'destructive' => false,
  99                          'idempotent'  => true,
 100                      ),
 101                      'public'      => true,
 102                  ),
 103              )
 104          );
 105      }
 106  
 107      /**
 108       * Executes the `core/settings-get` ability.
 109       *
 110       * @since 7.2.0
 111       *
 112       * @param mixed $input Optional. The ability input. Default empty array.
 113       * @return array<string, mixed> Map of exposed setting name to current value.
 114       */
 115  	private function execute_settings_get( $input = array() ): array {
 116          $input  = rest_sanitize_object( $input );
 117          $group  = isset( $input['group'] ) && is_string( $input['group'] ) ? $input['group'] : '';
 118          $fields = rest_sanitize_array( $input['fields'] ?? array() );
 119  
 120          $result = array();
 121          foreach ( $this->exposed_settings as $exposed_name => $setting ) {
 122              if ( '' !== $group && $setting['group'] !== $group ) {
 123                  continue;
 124              }
 125              if ( ! empty( $fields ) && ! in_array( (string) $exposed_name, $fields, true ) ) {
 126                  continue;
 127              }
 128  
 129              /*
 130               * As the settings endpoint does, read a setting that was never saved as its registered
 131               * default, or leave it out when it has none: get_option() would return false, which a
 132               * boolean schema accepts as a value and an array schema reads as an empty list.
 133               */
 134              $value = get_option( $setting['option'], $setting['default'] );
 135              if ( null === $value ) {
 136                  continue;
 137              }
 138  
 139              // WordPress stores false as '', which the boolean schema rejects.
 140              if ( '' === $value && 'boolean' === $setting['schema']['type'] ) {
 141                  $value = false;
 142              }
 143  
 144              /*
 145               * Leave out a value its schema rejects, before sanitizing (which could make it pass) or
 146               * after (which could make it fail), instead of failing output validation for every setting.
 147               */
 148              if ( is_wp_error( rest_validate_value_from_schema( $value, $setting['schema'] ) ) ) {
 149                  continue;
 150              }
 151  
 152              $value = rest_sanitize_value_from_schema( $value, $setting['schema'] );
 153              if ( is_wp_error( rest_validate_value_from_schema( $value, $setting['schema'] ) ) ) {
 154                  continue;
 155              }
 156  
 157              // Object (not array()) so an empty object value is serialized as {}, consistent with type:object.
 158              $result[ $exposed_name ] = 'object' === $setting['schema']['type'] ? (object) $value : $value;
 159          }
 160  
 161          return $result;
 162      }
 163  
 164      /**
 165       * Permission callback for the settings abilities.
 166       *
 167       * The settings abilities read site options, so they are gated on the `manage_options`
 168       * capability regardless of the input.
 169       *
 170       * @since 7.2.0
 171       *
 172       * @return bool True if the current user can manage options.
 173       */
 174  	private function check_permission(): bool {
 175          return current_user_can( 'manage_options' );
 176      }
 177  
 178      /**
 179       * Builds the input schema for the `core/settings-get` ability: optional filters by group and/or name.
 180       *
 181       * Both `group` and `fields` are optional; supplying both narrows the response to their
 182       * intersection, and supplying neither returns every exposed setting.
 183       *
 184       * @since 7.2.0
 185       *
 186       * @param list<string> $groups      Available settings groups.
 187       * @param list<string> $field_names Available exposed setting names.
 188       * @return array<string, mixed> The input JSON Schema.
 189       */
 190  	private function get_settings_get_input_schema( array $groups, array $field_names ): array {
 191          return array(
 192              'type'                 => 'object',
 193              'default'              => array(),
 194              'properties'           => array(
 195                  'group'  => array(
 196                      'type'        => 'string',
 197                      'enum'        => $groups,
 198                      'description' => __( 'Return only settings that belong to this settings group. If omitted, settings of every group are returned.' ),
 199                  ),
 200                  'fields' => array(
 201                      'type'        => 'array',
 202                      'uniqueItems' => true,
 203                      'items'       => array(
 204                          'type' => 'string',
 205                          'enum' => $field_names,
 206                      ),
 207                      '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.' ),
 208                  ),
 209              ),
 210              'additionalProperties' => false,
 211          );
 212      }
 213  
 214      /**
 215       * Builds the output schema for the `core/settings-get` ability.
 216       *
 217       * No setting is marked required because the `group` and `fields` inputs let the caller
 218       * request any subset, and a setting whose value does not match its schema is left out.
 219       *
 220       * @since 7.2.0
 221       *
 222       * @return array<string, mixed> The output JSON Schema.
 223       */
 224  	private function get_settings_get_output_schema(): array {
 225          return array(
 226              'type'                 => 'object',
 227              'description'          => __( 'A map of setting name to its current value.' ),
 228              'properties'           => wp_list_pluck( $this->exposed_settings, 'schema' ),
 229              'additionalProperties' => false,
 230          );
 231      }
 232  
 233      /**
 234       * Returns the settings exposed through the Abilities API.
 235       *
 236       * Reads {@see get_registered_settings()} and keeps only settings flagged with a truthy
 237       * `show_in_abilities` argument, of a type the settings endpoint supports. Each entry is
 238       * keyed by its exposed name and carries the underlying option name, the settings group,
 239       * the registered default (null when there is none), and a JSON Schema describing the value.
 240       *
 241       * Two settings exposed under the same name trigger a notice, and the later one is exposed.
 242       *
 243       * @since 7.2.0
 244       *
 245       * @return array<string, array{option: string, group: string, default: mixed, schema: array<string, mixed>}> Settings keyed by exposed name.
 246       */
 247  	private function get_exposed_settings(): array {
 248          $settings = array();
 249  
 250          foreach ( get_registered_settings() as $option_name => $args ) {
 251              if ( empty( $args['show_in_abilities'] ) ) {
 252                  continue;
 253              }
 254  
 255              $show = $this->get_exposure_args( $args );
 256  
 257              $schema = $this->value_schema( $args, $show );
 258              if ( ! in_array( $schema['type'], array( 'number', 'integer', 'string', 'boolean', 'array', 'object' ), true ) ) {
 259                  continue;
 260              }
 261  
 262              $option_name  = (string) $option_name;
 263              $exposed_name = empty( $show['name'] ) ? $option_name : (string) $show['name'];
 264  
 265              if ( isset( $settings[ $exposed_name ] ) ) {
 266                  _doing_it_wrong(
 267                      __METHOD__,
 268                      sprintf(
 269                          /* translators: 1: Option name, 2: Setting name exposed to abilities, 3: Option name. */
 270                          __( '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.' ),
 271                          esc_html( $option_name ),
 272                          esc_html( $exposed_name ),
 273                          esc_html( $settings[ $exposed_name ]['option'] )
 274                      ),
 275                      '7.2.0'
 276                  );
 277              }
 278  
 279              $settings[ $exposed_name ] = array(
 280                  'option'  => $option_name,
 281                  'group'   => $args['group'] ?? '',
 282                  'default' => $args['default'] ?? null,
 283                  'schema'  => $schema,
 284              );
 285          }
 286  
 287          return $settings;
 288      }
 289  
 290      /**
 291       * Returns the name and schema overrides used to expose a setting to abilities.
 292       *
 293       * When `show_in_abilities` is `true`, the setting is exposed the same way as in the
 294       * REST API: it uses the `name` and `schema` from `show_in_rest`. An array is used
 295       * as is.
 296       *
 297       * @since 7.2.0
 298       *
 299       * @param array<string, mixed> $args The setting registration arguments.
 300       * @return array<string, mixed> The exposure arguments, with optional `name` and `schema` keys.
 301       */
 302  	private function get_exposure_args( array $args ): array {
 303          if ( is_array( $args['show_in_abilities'] ) ) {
 304              return $args['show_in_abilities'];
 305          }
 306  
 307          return is_array( $args['show_in_rest'] ) ? $args['show_in_rest'] : array();
 308      }
 309  
 310      /**
 311       * Builds the JSON Schema describing a single setting's value.
 312       *
 313       * As in the settings endpoint, objects in the schema reject properties they do not declare,
 314       * unless the schema allows them.
 315       *
 316       * @since 7.2.0
 317       *
 318       * @param array<string, mixed> $args The setting registration arguments.
 319       * @param array<string, mixed> $show The exposure arguments, see get_exposure_args().
 320       * @return array<string, mixed> The value JSON Schema.
 321       */
 322  	private function value_schema( array $args, $show ): array {
 323          $schema = array(
 324              'type' => $args['type'],
 325          );
 326          if ( ! empty( $args['label'] ) ) {
 327              $schema['title'] = $args['label'];
 328          }
 329          if ( ! empty( $args['description'] ) ) {
 330              $schema['description'] = $args['description'];
 331          }
 332          if ( isset( $show['schema'] ) && is_array( $show['schema'] ) ) {
 333              /** @var array<string, mixed> $show_schema */
 334              $show_schema = $show['schema'];
 335              $schema      = array_merge( $schema, $show_schema );
 336          }
 337  
 338          return rest_default_additional_properties_to_false( $schema );
 339      }
 340  }


Generated : Tue Oct 13 08:20:33 2026 Cross-referenced by PHPXref