[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

   1  <?php
   2  /**
   3   * Abilities API
   4   *
   5   * Defines WP_Ability class.
   6   *
   7   * @package WordPress
   8   * @subpackage Abilities API
   9   * @since 6.9.0
  10   */
  11  
  12  declare( strict_types = 1 );
  13  
  14  /**
  15   * Encapsulates the properties and methods related to a specific ability in the registry.
  16   *
  17   * @since 6.9.0
  18   *
  19   * @see WP_Abilities_Registry
  20   */
  21  class WP_Ability {
  22  
  23      /**
  24       * The default value for the `show_in_rest` meta.
  25       *
  26       * @since 6.9.0
  27       * @var bool
  28       */
  29      protected const DEFAULT_SHOW_IN_REST = false;
  30  
  31      /**
  32       * The default value for the `public` meta.
  33       *
  34       * @since 7.1.0
  35       * @var bool
  36       */
  37      protected const DEFAULT_PUBLIC = false;
  38  
  39      /**
  40       * The default ability annotations.
  41       * They are not guaranteed to provide a faithful description of ability behavior.
  42       *
  43       * @since 6.9.0
  44       * @var array<string, bool|null>
  45       */
  46      protected static $default_annotations = array(
  47          // If true, the ability does not modify its environment.
  48          'readonly'    => null,
  49          /*
  50           * If true, the ability may perform destructive updates to its environment.
  51           * If false, the ability performs only additive updates.
  52           */
  53          'destructive' => null,
  54          /*
  55           * If true, calling the ability repeatedly with the same arguments will have no additional effect
  56           * on its environment.
  57           */
  58          'idempotent'  => null,
  59      );
  60  
  61      /**
  62       * The name of the ability, with its namespace.
  63       * Example: `my-plugin/my-ability`.
  64       *
  65       * @since 6.9.0
  66       * @var string
  67       */
  68      protected $name;
  69  
  70      /**
  71       * The human-readable ability label.
  72       *
  73       * @since 6.9.0
  74       * @var string
  75       */
  76      protected $label;
  77  
  78      /**
  79       * The detailed ability description.
  80       *
  81       * @since 6.9.0
  82       * @var string
  83       */
  84      protected $description;
  85  
  86      /**
  87       * The ability category.
  88       *
  89       * @since 6.9.0
  90       * @var string
  91       */
  92      protected $category;
  93  
  94      /**
  95       * The optional ability input schema.
  96       *
  97       * @since 6.9.0
  98       * @var array<string, mixed>
  99       */
 100      protected $input_schema = array();
 101  
 102      /**
 103       * The optional ability output schema.
 104       *
 105       * @since 6.9.0
 106       * @var array<string, mixed>
 107       */
 108      protected $output_schema = array();
 109  
 110      /**
 111       * The ability execute callback.
 112       *
 113       * @since 6.9.0
 114       * @var callable(mixed): (mixed|WP_Error)
 115       */
 116      protected $execute_callback;
 117  
 118      /**
 119       * The optional ability permission callback.
 120       *
 121       * @since 6.9.0
 122       * @var callable(mixed): (bool|WP_Error)
 123       */
 124      protected $permission_callback;
 125  
 126      /**
 127       * The optional capabilities the current user must have to run the ability.
 128       *
 129       * @since 7.2.0
 130       * @var list<string>
 131       */
 132      protected $required_capabilities = array();
 133  
 134      /**
 135       * The optional ability metadata.
 136       *
 137       * @since 6.9.0
 138       * @var array<string, mixed>
 139       */
 140      protected $meta;
 141  
 142      /**
 143       * Constructor.
 144       *
 145       * Do not use this constructor directly. Instead, use the `wp_register_ability()` function.
 146       *
 147       * @access private
 148       *
 149       * @since 6.9.0
 150       * @since 7.1.0 Added the `public` meta argument.
 151       * @since 7.2.0 Added the `required_capabilities` argument.
 152       *
 153       * @see wp_register_ability()
 154       *
 155       * @param string               $name The name of the ability, with its namespace.
 156       * @param array<string, mixed> $args {
 157       *     An associative array of arguments for the ability.
 158       *
 159       *     @type string               $label                 The human-readable label for the ability.
 160       *     @type string               $description           A detailed description of what the ability does.
 161       *     @type string               $category              The ability category slug this ability belongs to.
 162       *     @type callable             $execute_callback      A callback function to execute when the ability is invoked.
 163       *                                                       Receives optional mixed input and returns mixed result or WP_Error.
 164       *     @type callable             $permission_callback   A callback function to check permissions before execution.
 165       *                                                       Receives optional mixed input and returns bool or WP_Error.
 166       *                                                       Required unless `$required_capabilities` lists at least one capability.
 167       *     @type list<string>         $required_capabilities Optional. Minimum capabilities required for every execution of the
 168       *                                                       ability, regardless of input. They are checked before
 169       *                                                       `$permission_callback`, which handles input-dependent requirements.
 170       *     @type array<string, mixed> $input_schema          Optional. JSON Schema definition for the ability's input.
 171       *     @type array<string, mixed> $output_schema         Optional. JSON Schema definition for the ability's output.
 172       *     @type array<string, mixed> $meta                  {
 173       *         Optional. Additional metadata for the ability.
 174       *
 175       *         @type array<string, bool|null> $annotations  {
 176       *             Optional. Semantic annotations describing the ability's behavioral characteristics.
 177       *             These annotations are hints for tooling and documentation.
 178       *
 179       *             @type bool|null $readonly    Optional. If true, the ability does not modify its environment.
 180       *             @type bool|null $destructive Optional. If true, the ability may perform destructive updates to its environment.
 181       *                                          If false, the ability performs only additive updates.
 182       *             @type bool|null $idempotent  Optional. If true, calling the ability repeatedly with the same arguments
 183       *                                          will have no additional effect on its environment.
 184       *         }
 185       *         @type bool                     $public       Optional. Whether the ability is meant to be available
 186       *                                                      to clients such as the REST API, MCP, or AI agents.
 187       *                                                      Seeds the default for per-channel flags like
 188       *                                                      `$show_in_rest`. Defaults to false.
 189       *         @type bool                     $show_in_rest Optional. Whether to expose this ability in the REST API.
 190       *                                                      Default is the value of `$public` when set, false otherwise.
 191       *     }
 192       * }
 193       */
 194  	public function __construct( string $name, array $args ) {
 195          $this->name = $name;
 196  
 197          $properties = $this->prepare_properties( $args );
 198  
 199          foreach ( $properties as $property_name => $property_value ) {
 200              if ( ! property_exists( $this, $property_name ) ) {
 201                  _doing_it_wrong(
 202                      __METHOD__,
 203                      sprintf(
 204                          /* translators: %s: Property name. */
 205                          __( 'Property "%1$s" is not a valid property for ability "%2$s". Please check the %3$s class for allowed properties.' ),
 206                          '<code>' . esc_html( $property_name ) . '</code>',
 207                          '<code>' . esc_html( $this->name ) . '</code>',
 208                          '<code>' . __CLASS__ . '</code>'
 209                      ),
 210                      '6.9.0'
 211                  );
 212                  continue;
 213              }
 214  
 215              $this->$property_name = $property_value;
 216          }
 217      }
 218  
 219      /**
 220       * Prepares and validates the properties used to instantiate the ability.
 221       *
 222       * Errors are thrown as exceptions instead of WP_Errors to allow for simpler handling and overloading. They are then
 223       * caught and converted to a WP_Error by WP_Abilities_Registry::register().
 224       *
 225       * @since 6.9.0
 226       * @since 7.1.0 Added the `public` meta argument.
 227       * @since 7.2.0 Added the `required_capabilities` argument.
 228       *
 229       * @see WP_Abilities_Registry::register()
 230       *
 231       * @param array<string, mixed> $args {
 232       *     An associative array of arguments used to instantiate the ability class.
 233       *
 234       *     @type string               $label                 The human-readable label for the ability.
 235       *     @type string               $description           A detailed description of what the ability does.
 236       *     @type string               $category              The ability category slug this ability belongs to.
 237       *     @type callable             $execute_callback      A callback function to execute when the ability is invoked.
 238       *                                                       Receives optional mixed input and returns mixed result or WP_Error.
 239       *     @type callable             $permission_callback   A callback function to check permissions before execution.
 240       *                                                       Receives optional mixed input and returns bool or WP_Error.
 241       *                                                       Required unless `$required_capabilities` lists at least one capability.
 242       *     @type list<string>         $required_capabilities Optional. Minimum capabilities required for every execution of the
 243       *                                                       ability, regardless of input. They are checked before
 244       *                                                       `$permission_callback`, which handles input-dependent requirements.
 245       *     @type array<string, mixed> $input_schema          Optional. JSON Schema definition for the ability's input. Required if ability accepts an input.
 246       *     @type array<string, mixed> $output_schema         Optional. JSON Schema definition for the ability's output.
 247       *     @type array<string, mixed> $meta                  {
 248       *         Optional. Additional metadata for the ability.
 249       *
 250       *         @type array<string, bool|null> $annotations  {
 251       *             Optional. Semantic annotations describing the ability's behavioral characteristics.
 252       *             These annotations are hints for tooling and documentation.
 253       *
 254       *             @type bool|null $readonly    Optional. If true, the ability does not modify its environment.
 255       *             @type bool|null $destructive Optional. If true, the ability may perform destructive updates to its environment.
 256       *                                          If false, the ability performs only additive updates.
 257       *             @type bool|null $idempotent  Optional. If true, calling the ability repeatedly with the same arguments
 258       *                                          will have no additional effect on its environment.
 259       *         }
 260       *         @type bool                     $public       Optional. Whether the ability is meant to be available
 261       *                                                      to clients such as the REST API, MCP, or AI agents.
 262       *                                                      Seeds the default for per-channel flags like
 263       *                                                      `$show_in_rest`. Defaults to false.
 264       *         @type bool                     $show_in_rest Optional. Whether to expose this ability in the REST API.
 265       *                                                      Default is the value of `$public` when set, false otherwise.
 266       *     }
 267       * }
 268       * @return array<string, mixed> {
 269       *     An associative array of arguments with validated and prepared properties for the ability class.
 270       *
 271       *     @type string               $label                 The human-readable label for the ability.
 272       *     @type string               $description           A detailed description of what the ability does.
 273       *     @type string               $category              The ability category slug this ability belongs to.
 274       *     @type callable             $execute_callback      A callback function to execute when the ability is invoked.
 275       *                                                       Receives optional mixed input and returns mixed result or WP_Error.
 276       *     @type callable             $permission_callback   A callback function to check permissions before execution.
 277       *                                                       Receives optional mixed input and returns bool or WP_Error.
 278       *                                                       Required unless `$required_capabilities` lists at least one capability.
 279       *     @type list<string>         $required_capabilities Optional. Minimum capabilities required for every execution of the
 280       *                                                       ability, regardless of input. They are checked before
 281       *                                                       `$permission_callback`, which handles input-dependent requirements.
 282       *     @type array<string, mixed> $input_schema          Optional. JSON Schema definition for the ability's input.
 283       *     @type array<string, mixed> $output_schema         Optional. JSON Schema definition for the ability's output.
 284       *     @type array<string, mixed> $meta                  {
 285       *         Additional metadata for the ability.
 286       *
 287       *         @type array<string, bool|null> $annotations  {
 288       *             Semantic annotations describing the ability's behavioral characteristics.
 289       *             These annotations are hints for tooling and documentation.
 290       *
 291       *             @type bool|null $readonly    If true, the ability does not modify its environment.
 292       *             @type bool|null $destructive If true, the ability may perform destructive updates to its environment.
 293       *                                          If false, the ability performs only additive updates.
 294       *             @type bool|null $idempotent  If true, calling the ability repeatedly with the same arguments
 295       *                                          will have no additional effect on its environment.
 296       *         }
 297       *         @type bool                     $public       Whether the ability is meant to be available to clients
 298       *                                                      such as the REST API, MCP, or AI agents. Defaults to
 299       *                                                      false.
 300       *         @type bool                     $show_in_rest Whether to expose this ability in the REST API.
 301       *     }
 302       * }
 303       * @throws InvalidArgumentException if an argument is invalid.
 304       */
 305  	protected function prepare_properties( array $args ): array {
 306          // Required args must be present and of the correct type.
 307          if ( empty( $args['label'] ) || ! is_string( $args['label'] ) ) {
 308              throw new InvalidArgumentException(
 309                  __( 'The ability properties must contain a `label` string.' )
 310              );
 311          }
 312  
 313          if ( empty( $args['description'] ) || ! is_string( $args['description'] ) ) {
 314              throw new InvalidArgumentException(
 315                  __( 'The ability properties must contain a `description` string.' )
 316              );
 317          }
 318  
 319          if ( empty( $args['category'] ) || ! is_string( $args['category'] ) ) {
 320              throw new InvalidArgumentException(
 321                  __( 'The ability properties must contain a `category` string.' )
 322              );
 323          }
 324  
 325          // If we are not overriding `ability_class` parameter during instantiation, then we need to validate the execute_callback.
 326          if ( get_class( $this ) === self::class && ( empty( $args['execute_callback'] ) || ! is_callable( $args['execute_callback'] ) ) ) {
 327              throw new InvalidArgumentException(
 328                  __( 'The ability properties must contain a valid `execute_callback` function.' )
 329              );
 330          }
 331  
 332          if ( isset( $args['required_capabilities'] ) && ! self::is_valid_capability_list( $args['required_capabilities'] ) ) {
 333              throw new InvalidArgumentException(
 334                  __( 'The ability properties should provide a valid `required_capabilities` list of non-empty strings.' )
 335              );
 336          }
 337  
 338          /*
 339           * Validate the permission_callback, unless a custom `ability_class` is used.
 340           * It can be left out when the ability declares required capabilities.
 341           */
 342          if ( get_class( $this ) === self::class ) {
 343              if ( isset( $args['permission_callback'] ) ) {
 344                  if ( ! is_callable( $args['permission_callback'] ) ) {
 345                      throw new InvalidArgumentException(
 346                          __( 'The ability properties must provide a valid `permission_callback` function.' )
 347                      );
 348                  }
 349              } elseif ( empty( $args['required_capabilities'] ) ) {
 350                  throw new InvalidArgumentException(
 351                      __( 'The ability properties must provide a valid `permission_callback` function or a non-empty `required_capabilities` list.' )
 352                  );
 353              }
 354          }
 355  
 356          // Optional args only need to be of the correct type if they are present.
 357          if ( isset( $args['input_schema'] ) && ! is_array( $args['input_schema'] ) ) {
 358              throw new InvalidArgumentException(
 359                  __( 'The ability properties should provide a valid `input_schema` definition.' )
 360              );
 361          }
 362  
 363          if ( isset( $args['output_schema'] ) && ! is_array( $args['output_schema'] ) ) {
 364              throw new InvalidArgumentException(
 365                  __( 'The ability properties should provide a valid `output_schema` definition.' )
 366              );
 367          }
 368  
 369          if ( isset( $args['meta'] ) && ! is_array( $args['meta'] ) ) {
 370              throw new InvalidArgumentException(
 371                  __( 'The ability properties should provide a valid `meta` array.' )
 372              );
 373          }
 374  
 375          if ( isset( $args['meta']['annotations'] ) && ! is_array( $args['meta']['annotations'] ) ) {
 376              throw new InvalidArgumentException(
 377                  __( 'The ability meta should provide a valid `annotations` array.' )
 378              );
 379          }
 380  
 381          if ( isset( $args['meta']['show_in_rest'] ) && ! is_bool( $args['meta']['show_in_rest'] ) ) {
 382              throw new InvalidArgumentException(
 383                  __( 'The ability meta should provide a valid `show_in_rest` boolean.' )
 384              );
 385          }
 386  
 387          if ( isset( $args['meta']['public'] ) && ! is_bool( $args['meta']['public'] ) ) {
 388              throw new InvalidArgumentException(
 389                  __( 'The ability meta should provide a valid `public` boolean.' )
 390              );
 391          }
 392  
 393          $args['required_capabilities'] ??= array();
 394  
 395          // Set defaults for optional meta.
 396          $args['meta'] = wp_parse_args(
 397              $args['meta'] ?? array(),
 398              array(
 399                  'annotations' => static::$default_annotations,
 400              )
 401          );
 402  
 403          $args['meta']['annotations'] = wp_parse_args(
 404              $args['meta']['annotations'],
 405              static::$default_annotations
 406          );
 407  
 408          /*
 409           * Resolve `show_in_rest` from most specific to least specific: an explicit
 410           * `show_in_rest` value wins, then the high-level `public` flag seeds the
 411           * default, then the built-in default applies.
 412           */
 413          $args['meta']['show_in_rest'] ??= $args['meta']['public'] ?? self::DEFAULT_SHOW_IN_REST;
 414          $args['meta']['public']       ??= self::DEFAULT_PUBLIC;
 415  
 416          return $args;
 417      }
 418  
 419      /**
 420       * Retrieves the name of the ability, with its namespace.
 421       * Example: `my-plugin/my-ability`.
 422       *
 423       * @since 6.9.0
 424       *
 425       * @return string The ability name, with its namespace.
 426       */
 427  	public function get_name(): string {
 428          return $this->name;
 429      }
 430  
 431      /**
 432       * Retrieves the human-readable label for the ability.
 433       *
 434       * @since 6.9.0
 435       *
 436       * @return string The human-readable ability label.
 437       */
 438  	public function get_label(): string {
 439          return $this->label;
 440      }
 441  
 442      /**
 443       * Retrieves the detailed description for the ability.
 444       *
 445       * @since 6.9.0
 446       *
 447       * @return string The detailed description for the ability.
 448       */
 449  	public function get_description(): string {
 450          return $this->description;
 451      }
 452  
 453      /**
 454       * Retrieves the ability category for the ability.
 455       *
 456       * @since 6.9.0
 457       *
 458       * @return string The ability category for the ability.
 459       */
 460  	public function get_category(): string {
 461          return $this->category;
 462      }
 463  
 464      /**
 465       * Retrieves the input schema for the ability.
 466       *
 467       * @since 6.9.0
 468       *
 469       * @return array<string, mixed> The input schema for the ability.
 470       */
 471  	public function get_input_schema(): array {
 472          return $this->input_schema;
 473      }
 474  
 475      /**
 476       * Retrieves the output schema for the ability.
 477       *
 478       * @since 6.9.0
 479       *
 480       * @return array<string, mixed> The output schema for the ability.
 481       */
 482  	public function get_output_schema(): array {
 483          return $this->output_schema;
 484      }
 485  
 486      /**
 487       * Retrieves the metadata for the ability.
 488       *
 489       * @since 6.9.0
 490       *
 491       * @return array<string, mixed> The metadata for the ability.
 492       */
 493  	public function get_meta(): array {
 494          return $this->meta;
 495      }
 496  
 497      /**
 498       * Retrieves the capabilities the current user must have to run the ability.
 499       *
 500       * @since 7.2.0
 501       *
 502       * @return list<string> The required capabilities. Empty when the ability declares none.
 503       */
 504  	public function get_required_capabilities(): array {
 505          return $this->required_capabilities;
 506      }
 507  
 508      /**
 509       * Retrieves a specific metadata item for the ability.
 510       *
 511       * @since 6.9.0
 512       *
 513       * @param string $key           The metadata key to retrieve.
 514       * @param mixed  $default_value Optional. The default value to return if the metadata item is not found. Default `null`.
 515       * @return mixed The value of the metadata item, or the default value if not found.
 516       */
 517  	public function get_meta_item( string $key, $default_value = null ) {
 518          return array_key_exists( $key, $this->meta ) ? $this->meta[ $key ] : $default_value;
 519      }
 520  
 521      /**
 522       * Normalizes the input for the ability, applying the default value from the input schema when needed.
 523       *
 524       * When no input is provided and the input schema is defined with a top-level `default` key, this method returns
 525       * the value of that key. If the input schema does not define a `default`, or if the input schema is empty,
 526       * this method returns null. If input is provided, it is returned as-is.
 527       *
 528       * The {@see 'wp_ability_normalize_input'} filter fires after the built-in default-value handling,
 529       * allowing plugins to transform the result.
 530       *
 531       * @since 6.9.0
 532       * @since 7.1.0 Added the `wp_ability_normalize_input` filter.
 533       *
 534       * @param mixed $input Optional. The raw input provided for the ability. Default `null`.
 535       * @return mixed The normalized input, or a `WP_Error` if a filter returned one.
 536       */
 537  	public function normalize_input( $input = null ) {
 538          if ( null === $input ) {
 539              $input_schema = $this->get_input_schema();
 540              if ( array_key_exists( 'default', $input_schema ) ) {
 541                  $input = $input_schema['default'];
 542              }
 543          }
 544  
 545          /**
 546           * Filters the normalized input for an ability.
 547           *
 548           * Fires after `normalize_input()` has applied any default value declared in the input schema,
 549           * giving plugins a chance to adjust the input before it is consumed downstream. Common uses
 550           * include defaulting beyond what JSON Schema can express, prompt enrichment, and injecting
 551           * caller metadata.
 552           *
 553           * Returning a `WP_Error` causes callers that propagate it (such as `execute()`) to halt
 554           * before validation, permission checks, and the registered execute callback.
 555           *
 556           * @since 7.1.0
 557           *
 558           * @param mixed      $input        The normalized input data.
 559           * @param string     $ability_name The name of the ability.
 560           * @param WP_Ability $ability      The ability instance.
 561           */
 562          return apply_filters( 'wp_ability_normalize_input', $input, $this->name, $this );
 563      }
 564  
 565      /**
 566       * Coerces validated input to the types declared in the input schema.
 567       *
 568       * Also applies schema format sanitization, such as stripping tags for
 569       * `text-field` and sanitizing URLs for `uri`, using rest_sanitize_value_from_schema().
 570       *
 571       * Call validate_input() before this method. Coercing unvalidated input can
 572       * remove unknown properties or cast invalid values, hiding validation errors.
 573       * This method does not apply defaults or run input validation filters.
 574       *
 575       * If sanitization produces an error, including in a nested value, an invalid
 576       * input error is returned to prevent execution with uncoerced values.
 577       *
 578       * @since 7.2.0
 579       *
 580       * @see validate_input()
 581       *
 582       * @param mixed $input Validated ability input.
 583       * @return mixed|WP_Error Coerced input, or WP_Error when it cannot be safely coerced.
 584       */
 585  	public function coerce_input( $input ) {
 586          $schema = $this->get_input_schema();
 587          if ( null === $input || empty( $schema ) ) {
 588              return $input;
 589          }
 590  
 591          $sanitized = rest_sanitize_value_from_schema( $input, $schema, 'input' );
 592  
 593          // Values unique before coercion can collide after casting, even in nested arrays.
 594          $error = self::get_input_error( $sanitized );
 595          if ( is_wp_error( $error ) ) {
 596              return new WP_Error(
 597                  'ability_invalid_input',
 598                  sprintf(
 599                      /* translators: %1$s ability name, %2$s error message. */
 600                      __( 'Ability "%1$s" has invalid input. Reason: %2$s' ),
 601                      $this->name,
 602                      $error->get_error_message()
 603                  )
 604              );
 605          }
 606  
 607          return $sanitized;
 608      }
 609  
 610      /**
 611       * Retrieves the first WP_Error in a sanitized value.
 612       *
 613       * @since 7.2.0
 614       *
 615       * @param mixed $value The value to inspect.
 616       * @return WP_Error|null The first error found, or null if the value contains no errors.
 617       */
 618  	private static function get_input_error( $value ): ?WP_Error {
 619          if ( is_wp_error( $value ) ) {
 620              return $value;
 621          }
 622  
 623          if ( is_array( $value ) ) {
 624              foreach ( $value as $item ) {
 625                  $error = self::get_input_error( $item );
 626                  if ( is_wp_error( $error ) ) {
 627                      return $error;
 628                  }
 629              }
 630          }
 631  
 632          return null;
 633      }
 634  
 635      /**
 636       * Validates input data against the input schema.
 637       *
 638       * @since 6.9.0
 639       * @since 7.1.0 Added the `wp_ability_validate_input` filter.
 640       *
 641       * @param mixed $input Optional. The input data to validate. Default `null`.
 642       * @return true|WP_Error Returns true if valid or the WP_Error object if validation fails.
 643       */
 644  	public function validate_input( $input = null ) {
 645          $input_schema = $this->get_input_schema();
 646          if ( empty( $input_schema ) ) {
 647              if ( null === $input ) {
 648                  return true;
 649              }
 650  
 651              return new WP_Error(
 652                  'ability_missing_input_schema',
 653                  sprintf(
 654                      /* translators: %s ability name. */
 655                      __( 'Ability "%s" does not define an input schema required to validate the provided input.' ),
 656                      $this->name
 657                  )
 658              );
 659          }
 660  
 661          $valid_input = rest_validate_value_from_schema( $input, $input_schema, 'input' );
 662          if ( is_wp_error( $valid_input ) ) {
 663              $is_valid = new WP_Error(
 664                  'ability_invalid_input',
 665                  sprintf(
 666                      /* translators: %1$s ability name, %2$s error message. */
 667                      __( 'Ability "%1$s" has invalid input. Reason: %2$s' ),
 668                      $this->name,
 669                      $valid_input->get_error_message()
 670                  )
 671              );
 672          } else {
 673              $is_valid = true;
 674          }
 675  
 676          /**
 677           * Filters the input validation result for an ability.
 678           *
 679           * Allows developers to add custom validation logic on top of the default
 680           * JSON Schema validation. If default validation already failed, the filter
 681           * receives the WP_Error object and can add additional error information or
 682           * override it. If default validation passed, the filter can add additional
 683           * validation checks and return a WP_Error if those checks fail.
 684           *
 685           * @since 7.1.0
 686           *
 687           * @param true|WP_Error $is_valid     The validation result from default validation.
 688           * @param mixed         $input        The input data being validated.
 689           * @param string        $ability_name The name of the ability.
 690           */
 691          $validity = apply_filters( 'wp_ability_validate_input', $is_valid, $input, $this->name );
 692          if ( false === $validity ) {
 693              return new WP_Error( 'ability_invalid_input', __( 'Invalid input.' ) );
 694          }
 695          if ( is_wp_error( $validity ) && $validity->has_errors() ) {
 696              return $validity;
 697          }
 698          return true;
 699      }
 700  
 701      /**
 702       * Checks whether a value is a list of non-empty capability strings.
 703       *
 704       * @since 7.2.0
 705       *
 706       * @param mixed $capabilities The value to check.
 707       * @return bool Whether the value is a valid list of capabilities.
 708       */
 709  	private static function is_valid_capability_list( $capabilities ): bool {
 710          if ( ! is_array( $capabilities ) || ! array_is_list( $capabilities ) ) {
 711              return false;
 712          }
 713  
 714          return array_all(
 715              $capabilities,
 716              fn( $capability ) => is_string( $capability ) && '' !== trim( $capability )
 717          );
 718      }
 719  
 720      /**
 721       * Invokes a callable, ensuring the input is passed through only if the input schema is defined.
 722       *
 723       * @since 6.9.0
 724       * @since 7.1.0 Exceptions thrown by the callback are now caught and returned as a `WP_Error`.
 725       *
 726       * @param callable $callback The callable to invoke.
 727       * @param mixed    $input    Optional. The input data for the ability. Default `null`.
 728       * @return mixed The result of the callable execution, or a `WP_Error` if the callback threw.
 729       */
 730  	protected function invoke_callback( callable $callback, $input = null ) {
 731          $args = array();
 732          if ( ! empty( $this->get_input_schema() ) ) {
 733              $args[] = $input;
 734          }
 735  
 736          try {
 737              return $callback( ...$args );
 738          } catch ( Throwable $e ) {
 739              return new WP_Error(
 740                  'ability_callback_exception',
 741                  sprintf(
 742                      /* translators: 1: Ability name, 2: Exception message. */
 743                      __( 'Ability "%1$s" callback threw an exception: %2$s' ),
 744                      $this->name,
 745                      esc_html( $e->getMessage() )
 746                  )
 747              );
 748          }
 749      }
 750  
 751      /**
 752       * Checks whether the ability has the necessary permissions.
 753       *
 754       * Please note that input is not automatically validated against the input schema.
 755       * Use `validate_input()` method to validate input before calling this method if needed.
 756       *
 757       * The checks run in this order:
 758       *
 759       *  1. When the ability declares `required_capabilities`, each one is checked with
 760       *     `current_user_can()`. The check fails right away if one is missing.
 761       *  2. When all capabilities pass and the ability has a `permission_callback`, it runs
 762       *     with the given input.
 763       *
 764       * The {@see 'wp_ability_permission_result'} filter fires on the final result, allowing
 765       * plugins to override it.
 766       *
 767       * @since 6.9.0
 768       * @since 7.1.0 Added the `wp_ability_permission_result` filter.
 769       * @since 7.2.0 Added the `required_capabilities` check.
 770       *
 771       * @see validate_input()
 772       *
 773       * @param mixed $input Optional. The valid input data for permission checking. Default `null`.
 774       * @return bool|WP_Error Whether the ability has the necessary permission.
 775       */
 776  	public function check_permissions( $input = null ) {
 777          // A permission callback that is set must be callable.
 778          $is_invalid_callback = null !== $this->permission_callback && ! is_callable( $this->permission_callback );
 779  
 780          // Without a permission callback, the ability must declare required capabilities.
 781          $is_missing_check = null === $this->permission_callback && empty( $this->required_capabilities );
 782  
 783          if ( $is_invalid_callback || $is_missing_check ) {
 784              return new WP_Error(
 785                  'ability_invalid_permission_callback',
 786                  /* translators: %s ability name. */
 787                  sprintf( __( 'Ability "%s" must provide a valid permission callback, or omit it and declare required capabilities.' ), $this->name )
 788              );
 789          }
 790  
 791          // Both checks must pass. A check that isn't set is skipped.
 792          $permission = true;
 793          foreach ( $this->required_capabilities as $capability ) {
 794              if ( ! current_user_can( $capability ) ) {
 795                  $permission = false;
 796                  break;
 797              }
 798          }
 799  
 800          if ( $permission && null !== $this->permission_callback ) {
 801              $permission = $this->invoke_callback( $this->permission_callback, $input );
 802          }
 803  
 804          /**
 805           * Filters the result of an ability's permission check.
 806           *
 807           * Fires after the required capabilities check and the `permission_callback`. Plugins can use this
 808           * to layer additional authorization rules on top of the ability's own permission logic — for example,
 809           * multi-factor authorization gates or temporary permission elevation for trusted contexts.
 810           *
 811           * The filter receives `false` when a required capability is missing. Otherwise, it receives the
 812           * result of `permission_callback`, or `true` when there is none. Filters can return `true` to grant,
 813           * `false` to deny, or a `WP_Error` to deny with a specific error code and message. Any other return
 814           * value is coerced to `false`.
 815           *
 816           * @since 7.1.0
 817           * @since 7.2.0 Also fires with `false` when a required capability is missing.
 818           *
 819           * @param bool|WP_Error $permission   The result of the required capabilities check and the
 820           *                                    `permission_callback`.
 821           * @param string        $ability_name The name of the ability.
 822           * @param mixed         $input        The input data for the permission check.
 823           * @param WP_Ability    $ability      The ability instance.
 824           */
 825          $result = apply_filters( 'wp_ability_permission_result', $permission, $this->name, $input, $this );
 826          if ( ! is_bool( $result ) && ! is_wp_error( $result ) ) {
 827              $result = false;
 828          }
 829          return $result;
 830      }
 831  
 832      /**
 833       * Executes the ability callback.
 834       *
 835       * The {@see 'wp_ability_execute_result'} filter fires before this method returns, allowing
 836       * plugins to transform the result produced by the registered `execute_callback`.
 837       *
 838       * @since 6.9.0
 839       * @since 7.1.0 Added the `wp_ability_execute_result` filter.
 840       *
 841       * @param mixed $input Optional. The input data for the ability. Default `null`.
 842       * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
 843       */
 844  	protected function do_execute( $input = null ) {
 845          if ( ! is_callable( $this->execute_callback ) ) {
 846              $result = new WP_Error(
 847                  'ability_invalid_execute_callback',
 848                  /* translators: %s ability name. */
 849                  sprintf( __( 'Ability "%s" does not have a valid execute callback.' ), $this->name )
 850              );
 851          } else {
 852              $result = $this->invoke_callback( $this->execute_callback, $input );
 853          }
 854  
 855          /**
 856           * Filters the result returned by an ability's execute callback.
 857           *
 858           * Fires after the registered execute callback runs. Plugins can use this to transform the
 859           * result — response formatting, stripping internal metadata, content safety filtering,
 860           * response enrichment, or recovering from a failure by returning a successful value.
 861           *
 862           * The filter receives whatever the registered callback produced, including a `WP_Error`
 863           * if execution failed. Filters may pass the `WP_Error` through unchanged, override it with
 864           * a recovered result, or convert a successful result into a `WP_Error`.
 865           *
 866           * @since 7.1.0
 867           *
 868           * @param mixed      $result       The result returned by the registered `execute_callback`,
 869           *                                 or a `WP_Error` if execution failed.
 870           * @param string     $ability_name The name of the ability.
 871           * @param mixed      $input        The normalized input data.
 872           * @param WP_Ability $ability      The ability instance.
 873           */
 874          return apply_filters( 'wp_ability_execute_result', $result, $this->name, $input, $this );
 875      }
 876  
 877      /**
 878       * Validates output data against the output schema.
 879       *
 880       * @since 6.9.0
 881       * @since 7.1.0 Added the `wp_ability_validate_output` filter.
 882       *
 883       * @param mixed $output The output data to validate.
 884       * @return true|WP_Error Returns true if valid, or a WP_Error object if validation fails.
 885       */
 886  	protected function validate_output( $output ) {
 887          $output_schema = $this->get_output_schema();
 888          if ( empty( $output_schema ) ) {
 889              $is_valid = true;
 890          } else {
 891              $valid_output = rest_validate_value_from_schema( $output, $output_schema, 'output' );
 892              if ( is_wp_error( $valid_output ) ) {
 893                  $is_valid = new WP_Error(
 894                      'ability_invalid_output',
 895                      sprintf(
 896                          /* translators: %1$s ability name, %2$s error message. */
 897                          __( 'Ability "%1$s" has invalid output. Reason: %2$s' ),
 898                          $this->name,
 899                          $valid_output->get_error_message()
 900                      )
 901                  );
 902              } else {
 903                  $is_valid = true;
 904              }
 905          }
 906  
 907          /**
 908           * Filters the output validation result for an ability.
 909           *
 910           * Allows developers to add custom validation logic on top of the default
 911           * JSON Schema validation. If default validation already failed, the filter
 912           * receives the WP_Error object and can add additional error information or
 913           * override it. If default validation passed, the filter can add additional
 914           * validation checks and return a WP_Error if those checks fail.
 915           *
 916           * @since 7.1.0
 917           *
 918           * @param true|WP_Error $is_valid     The validation result from default validation.
 919           * @param mixed         $output       The output data being validated.
 920           * @param string        $ability_name The name of the ability.
 921           */
 922          $validity = apply_filters( 'wp_ability_validate_output', $is_valid, $output, $this->name );
 923          if ( false === $validity ) {
 924              return new WP_Error( 'ability_invalid_output', __( 'Invalid output.' ) );
 925          }
 926          if ( is_wp_error( $validity ) && $validity->has_errors() ) {
 927              return $validity;
 928          }
 929          return true;
 930      }
 931  
 932      /**
 933       * Executes the ability after input validation, coercion, and a permission check.
 934       * Before returning the return value, it also validates the output.
 935       *
 936       * @since 6.9.0
 937       * @since 7.1.0 Added the `wp_ability_invoked` action.
 938       * @since 7.1.0 Added the `wp_pre_execute_ability` filter.
 939       * @since 7.2.0 Coerces validated input before permission checks and execution.
 940       *
 941       * @param mixed $input Optional. The input data for the ability. Default `null`.
 942       * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
 943       */
 944  	public function execute( $input = null ) {
 945          /**
 946           * Fires when an ability is invoked, before any processing takes place.
 947           *
 948           * This action fires for every call regardless of outcome (validation failure,
 949           * permission denial, short-circuit, or successful execution), and before input
 950           * normalization so the raw input is captured as-is.
 951           *
 952           * @since 7.1.0
 953           *
 954           * @param string     $ability_name The name of the ability.
 955           * @param mixed      $input        The raw input data for the ability, before normalization.
 956           * @param WP_Ability $ability      The ability instance.
 957           */
 958          do_action( 'wp_ability_invoked', $this->name, $input, $this );
 959  
 960          $pre_execute_sentinel = new WP_Filter_Sentinel();
 961  
 962          /**
 963           * Filters whether to short-circuit ability execution.
 964           *
 965           * Returning a value other than the received default bypasses the rest of `execute()` —
 966           * input normalization, input validation, permission checks, the registered execute callback,
 967           * output validation, and the surrounding actions — and the value is returned to the caller
 968           * as-is. Useful for cached responses, rate limiting, maintenance mode, and test mocking.
 969           *
 970           * To continue with normal execution, return `$pre` unchanged. This preserves any value
 971           * (including `null`, `false`, or arbitrary objects) as a valid short-circuit result.
 972           *
 973           * Because validation is bypassed, callers that short-circuit are responsible for the
 974           * integrity of any value they consume from `$input`.
 975           *
 976           * @since 7.1.0
 977           *
 978           * @param mixed      $pre          The pre-computed result. Return this value unchanged to continue execution.
 979           *                                 Default `WP_Filter_Sentinel` instance unique to this invocation.
 980           * @param string     $ability_name The name of the ability.
 981           * @param mixed      $input        The raw input passed to `execute()`.
 982           * @param WP_Ability $ability      The ability instance.
 983           */
 984          $pre = apply_filters( 'wp_pre_execute_ability', $pre_execute_sentinel, $this->name, $input, $this );
 985          if ( $pre !== $pre_execute_sentinel ) {
 986              return $pre;
 987          }
 988  
 989          $input = $this->normalize_input( $input );
 990          if ( is_wp_error( $input ) ) {
 991              return $input;
 992          }
 993  
 994          $is_valid = $this->validate_input( $input );
 995          if ( is_wp_error( $is_valid ) ) {
 996              return $is_valid;
 997          }
 998  
 999          $input = $this->coerce_input( $input );
1000          if ( is_wp_error( $input ) ) {
1001              return $input;
1002          }
1003  
1004          $has_permissions = $this->check_permissions( $input );
1005          if ( true !== $has_permissions ) {
1006              if ( is_wp_error( $has_permissions ) ) {
1007                  // Don't leak the permission check error to someone without the correct perms.
1008                  _doing_it_wrong(
1009                      __METHOD__,
1010                      esc_html( $has_permissions->get_error_message() ),
1011                      '6.9.0'
1012                  );
1013              }
1014  
1015              return new WP_Error(
1016                  'ability_invalid_permissions',
1017                  /* translators: %s ability name. */
1018                  sprintf( __( 'Ability "%s" does not have necessary permission.' ), $this->name )
1019              );
1020          }
1021  
1022          /**
1023           * Fires before an ability gets executed, after input validation and permissions check.
1024           *
1025           * @since 6.9.0
1026           * @since 7.1.0 Added the `$ability` parameter.
1027           *
1028           * @param string     $ability_name The name of the ability.
1029           * @param mixed      $input        The validated and coerced input data for the ability.
1030           * @param WP_Ability $ability      The ability instance.
1031           */
1032          do_action( 'wp_before_execute_ability', $this->name, $input, $this );
1033  
1034          $result = $this->do_execute( $input );
1035          if ( is_wp_error( $result ) ) {
1036              return $result;
1037          }
1038  
1039          $is_valid = $this->validate_output( $result );
1040          if ( is_wp_error( $is_valid ) ) {
1041              return $is_valid;
1042          }
1043  
1044          /**
1045           * Fires immediately after an ability finished executing.
1046           *
1047           * @since 6.9.0
1048           * @since 7.1.0 Added the `$ability` parameter.
1049           *
1050           * @param string     $ability_name The name of the ability.
1051           * @param mixed      $input        The validated and coerced input data for the ability.
1052           * @param mixed      $result       The result of the ability execution.
1053           * @param WP_Ability $ability      The ability instance.
1054           */
1055          do_action( 'wp_after_execute_ability', $this->name, $input, $result, $this );
1056  
1057          return $result;
1058      }
1059  
1060      /**
1061       * Wakeup magic method.
1062       *
1063       * @since 6.9.0
1064       * @throws LogicException If the ability object is unserialized.
1065       *                        This is a security hardening measure to prevent unserialization of the ability.
1066       */
1067  	public function __wakeup(): void {
1068          throw new LogicException( __CLASS__ . ' should never be unserialized.' );
1069      }
1070  
1071      /**
1072       * Sleep magic method.
1073       *
1074       * @since 6.9.0
1075       * @throws LogicException If the ability object is serialized.
1076       *                        This is a security hardening measure to prevent serialization of the ability.
1077       */
1078  	public function __sleep(): array {
1079          throw new LogicException( __CLASS__ . ' should never be serialized.' );
1080      }
1081  }


Generated : Mon Nov 2 08:20:36 2026 Cross-referenced by PHPXref