[ 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 ability metadata.
 128       *
 129       * @since 6.9.0
 130       * @var array<string, mixed>
 131       */
 132      protected $meta;
 133  
 134      /**
 135       * Constructor.
 136       *
 137       * Do not use this constructor directly. Instead, use the `wp_register_ability()` function.
 138       *
 139       * @access private
 140       *
 141       * @since 6.9.0
 142       * @since 7.1.0 Added the `public` meta argument.
 143       *
 144       * @see wp_register_ability()
 145       *
 146       * @param string               $name The name of the ability, with its namespace.
 147       * @param array<string, mixed> $args {
 148       *     An associative array of arguments for the ability.
 149       *
 150       *     @type string               $label                 The human-readable label for the ability.
 151       *     @type string               $description           A detailed description of what the ability does.
 152       *     @type string               $category              The ability category slug this ability belongs to.
 153       *     @type callable             $execute_callback      A callback function to execute when the ability is invoked.
 154       *                                                       Receives optional mixed input and returns mixed result or WP_Error.
 155       *     @type callable             $permission_callback   A callback function to check permissions before execution.
 156       *                                                       Receives optional mixed input and returns bool or WP_Error.
 157       *     @type array<string, mixed> $input_schema          Optional. JSON Schema definition for the ability's input.
 158       *     @type array<string, mixed> $output_schema         Optional. JSON Schema definition for the ability's output.
 159       *     @type array<string, mixed> $meta                  {
 160       *         Optional. Additional metadata for the ability.
 161       *
 162       *         @type array<string, bool|null> $annotations  {
 163       *             Optional. Semantic annotations describing the ability's behavioral characteristics.
 164       *             These annotations are hints for tooling and documentation.
 165       *
 166       *             @type bool|null $readonly    Optional. If true, the ability does not modify its environment.
 167       *             @type bool|null $destructive Optional. If true, the ability may perform destructive updates to its environment.
 168       *                                          If false, the ability performs only additive updates.
 169       *             @type bool|null $idempotent  Optional. If true, calling the ability repeatedly with the same arguments
 170       *                                          will have no additional effect on its environment.
 171       *         }
 172       *         @type bool                     $public       Optional. Whether the ability is meant to be available
 173       *                                                      to clients such as the REST API, MCP, or AI agents.
 174       *                                                      Seeds the default for per-channel flags like
 175       *                                                      `$show_in_rest`. Defaults to false.
 176       *         @type bool                     $show_in_rest Optional. Whether to expose this ability in the REST API.
 177       *                                                      Default is the value of `$public` when set, false otherwise.
 178       *     }
 179       * }
 180       */
 181  	public function __construct( string $name, array $args ) {
 182          $this->name = $name;
 183  
 184          $properties = $this->prepare_properties( $args );
 185  
 186          foreach ( $properties as $property_name => $property_value ) {
 187              if ( ! property_exists( $this, $property_name ) ) {
 188                  _doing_it_wrong(
 189                      __METHOD__,
 190                      sprintf(
 191                          /* translators: %s: Property name. */
 192                          __( 'Property "%1$s" is not a valid property for ability "%2$s". Please check the %3$s class for allowed properties.' ),
 193                          '<code>' . esc_html( $property_name ) . '</code>',
 194                          '<code>' . esc_html( $this->name ) . '</code>',
 195                          '<code>' . __CLASS__ . '</code>'
 196                      ),
 197                      '6.9.0'
 198                  );
 199                  continue;
 200              }
 201  
 202              $this->$property_name = $property_value;
 203          }
 204      }
 205  
 206      /**
 207       * Prepares and validates the properties used to instantiate the ability.
 208       *
 209       * Errors are thrown as exceptions instead of WP_Errors to allow for simpler handling and overloading. They are then
 210       * caught and converted to a WP_Error by WP_Abilities_Registry::register().
 211       *
 212       * @since 6.9.0
 213       * @since 7.1.0 Added the `public` meta argument.
 214       *
 215       * @see WP_Abilities_Registry::register()
 216       *
 217       * @param array<string, mixed> $args {
 218       *     An associative array of arguments used to instantiate the ability class.
 219       *
 220       *     @type string               $label                 The human-readable label for the ability.
 221       *     @type string               $description           A detailed description of what the ability does.
 222       *     @type string               $category              The ability category slug this ability belongs to.
 223       *     @type callable             $execute_callback      A callback function to execute when the ability is invoked.
 224       *                                                       Receives optional mixed input and returns mixed result or WP_Error.
 225       *     @type callable             $permission_callback   A callback function to check permissions before execution.
 226       *                                                       Receives optional mixed input and returns bool or WP_Error.
 227       *     @type array<string, mixed> $input_schema          Optional. JSON Schema definition for the ability's input. Required if ability accepts an input.
 228       *     @type array<string, mixed> $output_schema         Optional. JSON Schema definition for the ability's output.
 229       *     @type array<string, mixed> $meta                  {
 230       *         Optional. Additional metadata for the ability.
 231       *
 232       *         @type array<string, bool|null> $annotations  {
 233       *             Optional. Semantic annotations describing the ability's behavioral characteristics.
 234       *             These annotations are hints for tooling and documentation.
 235       *
 236       *             @type bool|null $readonly    Optional. If true, the ability does not modify its environment.
 237       *             @type bool|null $destructive Optional. If true, the ability may perform destructive updates to its environment.
 238       *                                          If false, the ability performs only additive updates.
 239       *             @type bool|null $idempotent  Optional. If true, calling the ability repeatedly with the same arguments
 240       *                                          will have no additional effect on its environment.
 241       *         }
 242       *         @type bool                     $public       Optional. Whether the ability is meant to be available
 243       *                                                      to clients such as the REST API, MCP, or AI agents.
 244       *                                                      Seeds the default for per-channel flags like
 245       *                                                      `$show_in_rest`. Defaults to false.
 246       *         @type bool                     $show_in_rest Optional. Whether to expose this ability in the REST API.
 247       *                                                      Default is the value of `$public` when set, false otherwise.
 248       *     }
 249       * }
 250       * @return array<string, mixed> {
 251       *     An associative array of arguments with validated and prepared properties for the ability class.
 252       *
 253       *     @type string               $label                 The human-readable label for the ability.
 254       *     @type string               $description           A detailed description of what the ability does.
 255       *     @type string               $category              The ability category slug this ability belongs to.
 256       *     @type callable             $execute_callback      A callback function to execute when the ability is invoked.
 257       *                                                       Receives optional mixed input and returns mixed result or WP_Error.
 258       *     @type callable             $permission_callback   A callback function to check permissions before execution.
 259       *                                                       Receives optional mixed input and returns bool or WP_Error.
 260       *     @type array<string, mixed> $input_schema          Optional. JSON Schema definition for the ability's input.
 261       *     @type array<string, mixed> $output_schema         Optional. JSON Schema definition for the ability's output.
 262       *     @type array<string, mixed> $meta                  {
 263       *         Additional metadata for the ability.
 264       *
 265       *         @type array<string, bool|null> $annotations  {
 266       *             Semantic annotations describing the ability's behavioral characteristics.
 267       *             These annotations are hints for tooling and documentation.
 268       *
 269       *             @type bool|null $readonly    If true, the ability does not modify its environment.
 270       *             @type bool|null $destructive If true, the ability may perform destructive updates to its environment.
 271       *                                          If false, the ability performs only additive updates.
 272       *             @type bool|null $idempotent  If true, calling the ability repeatedly with the same arguments
 273       *                                          will have no additional effect on its environment.
 274       *         }
 275       *         @type bool                     $public       Whether the ability is meant to be available to clients
 276       *                                                      such as the REST API, MCP, or AI agents. Defaults to
 277       *                                                      false.
 278       *         @type bool                     $show_in_rest Whether to expose this ability in the REST API.
 279       *     }
 280       * }
 281       * @throws InvalidArgumentException if an argument is invalid.
 282       */
 283  	protected function prepare_properties( array $args ): array {
 284          // Required args must be present and of the correct type.
 285          if ( empty( $args['label'] ) || ! is_string( $args['label'] ) ) {
 286              throw new InvalidArgumentException(
 287                  __( 'The ability properties must contain a `label` string.' )
 288              );
 289          }
 290  
 291          if ( empty( $args['description'] ) || ! is_string( $args['description'] ) ) {
 292              throw new InvalidArgumentException(
 293                  __( 'The ability properties must contain a `description` string.' )
 294              );
 295          }
 296  
 297          if ( empty( $args['category'] ) || ! is_string( $args['category'] ) ) {
 298              throw new InvalidArgumentException(
 299                  __( 'The ability properties must contain a `category` string.' )
 300              );
 301          }
 302  
 303          // If we are not overriding `ability_class` parameter during instantiation, then we need to validate the execute_callback.
 304          if ( get_class( $this ) === self::class && ( empty( $args['execute_callback'] ) || ! is_callable( $args['execute_callback'] ) ) ) {
 305              throw new InvalidArgumentException(
 306                  __( 'The ability properties must contain a valid `execute_callback` function.' )
 307              );
 308          }
 309  
 310          // If we are not overriding `ability_class` parameter during instantiation, then we need to validate the permission_callback.
 311          if ( get_class( $this ) === self::class && ( empty( $args['permission_callback'] ) || ! is_callable( $args['permission_callback'] ) ) ) {
 312              throw new InvalidArgumentException(
 313                  __( 'The ability properties must provide a valid `permission_callback` function.' )
 314              );
 315          }
 316  
 317          // Optional args only need to be of the correct type if they are present.
 318          if ( isset( $args['input_schema'] ) && ! is_array( $args['input_schema'] ) ) {
 319              throw new InvalidArgumentException(
 320                  __( 'The ability properties should provide a valid `input_schema` definition.' )
 321              );
 322          }
 323  
 324          if ( isset( $args['output_schema'] ) && ! is_array( $args['output_schema'] ) ) {
 325              throw new InvalidArgumentException(
 326                  __( 'The ability properties should provide a valid `output_schema` definition.' )
 327              );
 328          }
 329  
 330          if ( isset( $args['meta'] ) && ! is_array( $args['meta'] ) ) {
 331              throw new InvalidArgumentException(
 332                  __( 'The ability properties should provide a valid `meta` array.' )
 333              );
 334          }
 335  
 336          if ( isset( $args['meta']['annotations'] ) && ! is_array( $args['meta']['annotations'] ) ) {
 337              throw new InvalidArgumentException(
 338                  __( 'The ability meta should provide a valid `annotations` array.' )
 339              );
 340          }
 341  
 342          if ( isset( $args['meta']['show_in_rest'] ) && ! is_bool( $args['meta']['show_in_rest'] ) ) {
 343              throw new InvalidArgumentException(
 344                  __( 'The ability meta should provide a valid `show_in_rest` boolean.' )
 345              );
 346          }
 347  
 348          if ( isset( $args['meta']['public'] ) && ! is_bool( $args['meta']['public'] ) ) {
 349              throw new InvalidArgumentException(
 350                  __( 'The ability meta should provide a valid `public` boolean.' )
 351              );
 352          }
 353  
 354          // Set defaults for optional meta.
 355          $args['meta'] = wp_parse_args(
 356              $args['meta'] ?? array(),
 357              array(
 358                  'annotations' => static::$default_annotations,
 359              )
 360          );
 361  
 362          $args['meta']['annotations'] = wp_parse_args(
 363              $args['meta']['annotations'],
 364              static::$default_annotations
 365          );
 366  
 367          /*
 368           * Resolve `show_in_rest` from most specific to least specific: an explicit
 369           * `show_in_rest` value wins, then the high-level `public` flag seeds the
 370           * default, then the built-in default applies.
 371           */
 372          $args['meta']['show_in_rest'] = $args['meta']['show_in_rest'] ?? $args['meta']['public'] ?? self::DEFAULT_SHOW_IN_REST;
 373          $args['meta']['public']       = $args['meta']['public'] ?? self::DEFAULT_PUBLIC;
 374  
 375          return $args;
 376      }
 377  
 378      /**
 379       * Retrieves the name of the ability, with its namespace.
 380       * Example: `my-plugin/my-ability`.
 381       *
 382       * @since 6.9.0
 383       *
 384       * @return string The ability name, with its namespace.
 385       */
 386  	public function get_name(): string {
 387          return $this->name;
 388      }
 389  
 390      /**
 391       * Retrieves the human-readable label for the ability.
 392       *
 393       * @since 6.9.0
 394       *
 395       * @return string The human-readable ability label.
 396       */
 397  	public function get_label(): string {
 398          return $this->label;
 399      }
 400  
 401      /**
 402       * Retrieves the detailed description for the ability.
 403       *
 404       * @since 6.9.0
 405       *
 406       * @return string The detailed description for the ability.
 407       */
 408  	public function get_description(): string {
 409          return $this->description;
 410      }
 411  
 412      /**
 413       * Retrieves the ability category for the ability.
 414       *
 415       * @since 6.9.0
 416       *
 417       * @return string The ability category for the ability.
 418       */
 419  	public function get_category(): string {
 420          return $this->category;
 421      }
 422  
 423      /**
 424       * Retrieves the input schema for the ability.
 425       *
 426       * @since 6.9.0
 427       *
 428       * @return array<string, mixed> The input schema for the ability.
 429       */
 430  	public function get_input_schema(): array {
 431          return $this->input_schema;
 432      }
 433  
 434      /**
 435       * Retrieves the output schema for the ability.
 436       *
 437       * @since 6.9.0
 438       *
 439       * @return array<string, mixed> The output schema for the ability.
 440       */
 441  	public function get_output_schema(): array {
 442          return $this->output_schema;
 443      }
 444  
 445      /**
 446       * Retrieves the metadata for the ability.
 447       *
 448       * @since 6.9.0
 449       *
 450       * @return array<string, mixed> The metadata for the ability.
 451       */
 452  	public function get_meta(): array {
 453          return $this->meta;
 454      }
 455  
 456      /**
 457       * Retrieves a specific metadata item for the ability.
 458       *
 459       * @since 6.9.0
 460       *
 461       * @param string $key           The metadata key to retrieve.
 462       * @param mixed  $default_value Optional. The default value to return if the metadata item is not found. Default `null`.
 463       * @return mixed The value of the metadata item, or the default value if not found.
 464       */
 465  	public function get_meta_item( string $key, $default_value = null ) {
 466          return array_key_exists( $key, $this->meta ) ? $this->meta[ $key ] : $default_value;
 467      }
 468  
 469      /**
 470       * Normalizes the input for the ability, applying the default value from the input schema when needed.
 471       *
 472       * When no input is provided and the input schema is defined with a top-level `default` key, this method returns
 473       * the value of that key. If the input schema does not define a `default`, or if the input schema is empty,
 474       * this method returns null. If input is provided, it is returned as-is.
 475       *
 476       * The {@see 'wp_ability_normalize_input'} filter fires after the built-in default-value handling,
 477       * allowing plugins to transform the result.
 478       *
 479       * @since 6.9.0
 480       * @since 7.1.0 Added the `wp_ability_normalize_input` filter.
 481       *
 482       * @param mixed $input Optional. The raw input provided for the ability. Default `null`.
 483       * @return mixed The normalized input, or a `WP_Error` if a filter returned one.
 484       */
 485  	public function normalize_input( $input = null ) {
 486          if ( null === $input ) {
 487              $input_schema = $this->get_input_schema();
 488              if ( array_key_exists( 'default', $input_schema ) ) {
 489                  $input = $input_schema['default'];
 490              }
 491          }
 492  
 493          /**
 494           * Filters the normalized input for an ability.
 495           *
 496           * Fires after `normalize_input()` has applied any default value declared in the input schema,
 497           * giving plugins a chance to adjust the input before it is consumed downstream. Common uses
 498           * include defaulting beyond what JSON Schema can express, prompt enrichment, and injecting
 499           * caller metadata.
 500           *
 501           * Returning a `WP_Error` causes callers that propagate it (such as `execute()`) to halt
 502           * before validation, permission checks, and the registered execute callback.
 503           *
 504           * @since 7.1.0
 505           *
 506           * @param mixed      $input        The normalized input data.
 507           * @param string     $ability_name The name of the ability.
 508           * @param WP_Ability $ability      The ability instance.
 509           */
 510          return apply_filters( 'wp_ability_normalize_input', $input, $this->name, $this );
 511      }
 512  
 513      /**
 514       * Validates input data against the input schema.
 515       *
 516       * @since 6.9.0
 517       * @since 7.1.0 Added the `wp_ability_validate_input` filter.
 518       *
 519       * @param mixed $input Optional. The input data to validate. Default `null`.
 520       * @return true|WP_Error Returns true if valid or the WP_Error object if validation fails.
 521       */
 522  	public function validate_input( $input = null ) {
 523          $input_schema = $this->get_input_schema();
 524          if ( empty( $input_schema ) ) {
 525              if ( null === $input ) {
 526                  return true;
 527              }
 528  
 529              return new WP_Error(
 530                  'ability_missing_input_schema',
 531                  sprintf(
 532                      /* translators: %s ability name. */
 533                      __( 'Ability "%s" does not define an input schema required to validate the provided input.' ),
 534                      $this->name
 535                  )
 536              );
 537          }
 538  
 539          $valid_input = rest_validate_value_from_schema( $input, $input_schema, 'input' );
 540          if ( is_wp_error( $valid_input ) ) {
 541              $is_valid = new WP_Error(
 542                  'ability_invalid_input',
 543                  sprintf(
 544                      /* translators: %1$s ability name, %2$s error message. */
 545                      __( 'Ability "%1$s" has invalid input. Reason: %2$s' ),
 546                      $this->name,
 547                      $valid_input->get_error_message()
 548                  )
 549              );
 550          } else {
 551              $is_valid = true;
 552          }
 553  
 554          /**
 555           * Filters the input validation result for an ability.
 556           *
 557           * Allows developers to add custom validation logic on top of the default
 558           * JSON Schema validation. If default validation already failed, the filter
 559           * receives the WP_Error object and can add additional error information or
 560           * override it. If default validation passed, the filter can add additional
 561           * validation checks and return a WP_Error if those checks fail.
 562           *
 563           * @since 7.1.0
 564           *
 565           * @param true|WP_Error $is_valid     The validation result from default validation.
 566           * @param mixed         $input        The input data being validated.
 567           * @param string        $ability_name The name of the ability.
 568           */
 569          $validity = apply_filters( 'wp_ability_validate_input', $is_valid, $input, $this->name );
 570          if ( false === $validity ) {
 571              return new WP_Error( 'ability_invalid_input', __( 'Invalid input.' ) );
 572          }
 573          if ( is_wp_error( $validity ) && $validity->has_errors() ) {
 574              return $validity;
 575          }
 576          return true;
 577      }
 578  
 579      /**
 580       * Invokes a callable, ensuring the input is passed through only if the input schema is defined.
 581       *
 582       * @since 6.9.0
 583       * @since 7.1.0 Exceptions thrown by the callback are now caught and returned as a `WP_Error`.
 584       *
 585       * @param callable $callback The callable to invoke.
 586       * @param mixed    $input    Optional. The input data for the ability. Default `null`.
 587       * @return mixed The result of the callable execution, or a `WP_Error` if the callback threw.
 588       */
 589  	protected function invoke_callback( callable $callback, $input = null ) {
 590          $args = array();
 591          if ( ! empty( $this->get_input_schema() ) ) {
 592              $args[] = $input;
 593          }
 594  
 595          try {
 596              return $callback( ...$args );
 597          } catch ( Throwable $e ) {
 598              return new WP_Error(
 599                  'ability_callback_exception',
 600                  sprintf(
 601                      /* translators: 1: Ability name, 2: Exception message. */
 602                      __( 'Ability "%1$s" callback threw an exception: %2$s' ),
 603                      $this->name,
 604                      esc_html( $e->getMessage() )
 605                  )
 606              );
 607          }
 608      }
 609  
 610      /**
 611       * Checks whether the ability has the necessary permissions.
 612       *
 613       * Please note that input is not automatically validated against the input schema.
 614       * Use `validate_input()` method to validate input before calling this method if needed.
 615       *
 616       * The {@see 'wp_ability_permission_result'} filter fires after the registered
 617       * `permission_callback` returns, allowing plugins to override the result.
 618       *
 619       * @since 6.9.0
 620       * @since 7.1.0 Added the `wp_ability_permission_result` filter.
 621       *
 622       * @see validate_input()
 623       *
 624       * @param mixed $input Optional. The valid input data for permission checking. Default `null`.
 625       * @return bool|WP_Error Whether the ability has the necessary permission.
 626       */
 627  	public function check_permissions( $input = null ) {
 628          if ( ! is_callable( $this->permission_callback ) ) {
 629              return new WP_Error(
 630                  'ability_invalid_permission_callback',
 631                  /* translators: %s ability name. */
 632                  sprintf( __( 'Ability "%s" does not have a valid permission callback.' ), $this->name )
 633              );
 634          }
 635  
 636          $permission = $this->invoke_callback( $this->permission_callback, $input );
 637  
 638          /**
 639           * Filters the result of an ability's permission check.
 640           *
 641           * Fires after the registered `permission_callback` returns. Plugins can use this to layer
 642           * additional authorization rules on top of the ability's own permission logic — for example,
 643           * multi-factor authorization gates or temporary permission elevation for trusted contexts.
 644           *
 645           * Filters can return `true` to grant, `false` to deny, or a `WP_Error` to deny with a specific
 646           * error code and message. The filter receives whatever the `permission_callback` produced.
 647           * Any other return value is coerced to `false`.
 648           *
 649           * @since 7.1.0
 650           *
 651           * @param bool|WP_Error $permission   The permission result returned by `permission_callback`.
 652           * @param string        $ability_name The name of the ability.
 653           * @param mixed         $input        The input data for the permission check.
 654           * @param WP_Ability    $ability      The ability instance.
 655           */
 656          $result = apply_filters( 'wp_ability_permission_result', $permission, $this->name, $input, $this );
 657          if ( ! is_bool( $result ) && ! is_wp_error( $result ) ) {
 658              $result = false;
 659          }
 660          return $result;
 661      }
 662  
 663      /**
 664       * Executes the ability callback.
 665       *
 666       * The {@see 'wp_ability_execute_result'} filter fires before this method returns, allowing
 667       * plugins to transform the result produced by the registered `execute_callback`.
 668       *
 669       * @since 6.9.0
 670       * @since 7.1.0 Added the `wp_ability_execute_result` filter.
 671       *
 672       * @param mixed $input Optional. The input data for the ability. Default `null`.
 673       * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
 674       */
 675  	protected function do_execute( $input = null ) {
 676          if ( ! is_callable( $this->execute_callback ) ) {
 677              $result = new WP_Error(
 678                  'ability_invalid_execute_callback',
 679                  /* translators: %s ability name. */
 680                  sprintf( __( 'Ability "%s" does not have a valid execute callback.' ), $this->name )
 681              );
 682          } else {
 683              $result = $this->invoke_callback( $this->execute_callback, $input );
 684          }
 685  
 686          /**
 687           * Filters the result returned by an ability's execute callback.
 688           *
 689           * Fires after the registered execute callback runs. Plugins can use this to transform the
 690           * result — response formatting, stripping internal metadata, content safety filtering,
 691           * response enrichment, or recovering from a failure by returning a successful value.
 692           *
 693           * The filter receives whatever the registered callback produced, including a `WP_Error`
 694           * if execution failed. Filters may pass the `WP_Error` through unchanged, override it with
 695           * a recovered result, or convert a successful result into a `WP_Error`.
 696           *
 697           * @since 7.1.0
 698           *
 699           * @param mixed      $result       The result returned by the registered `execute_callback`,
 700           *                                 or a `WP_Error` if execution failed.
 701           * @param string     $ability_name The name of the ability.
 702           * @param mixed      $input        The normalized input data.
 703           * @param WP_Ability $ability      The ability instance.
 704           */
 705          return apply_filters( 'wp_ability_execute_result', $result, $this->name, $input, $this );
 706      }
 707  
 708      /**
 709       * Validates output data against the output schema.
 710       *
 711       * @since 6.9.0
 712       * @since 7.1.0 Added the `wp_ability_validate_output` filter.
 713       *
 714       * @param mixed $output The output data to validate.
 715       * @return true|WP_Error Returns true if valid, or a WP_Error object if validation fails.
 716       */
 717  	protected function validate_output( $output ) {
 718          $output_schema = $this->get_output_schema();
 719          if ( empty( $output_schema ) ) {
 720              $is_valid = true;
 721          } else {
 722              $valid_output = rest_validate_value_from_schema( $output, $output_schema, 'output' );
 723              if ( is_wp_error( $valid_output ) ) {
 724                  $is_valid = new WP_Error(
 725                      'ability_invalid_output',
 726                      sprintf(
 727                          /* translators: %1$s ability name, %2$s error message. */
 728                          __( 'Ability "%1$s" has invalid output. Reason: %2$s' ),
 729                          $this->name,
 730                          $valid_output->get_error_message()
 731                      )
 732                  );
 733              } else {
 734                  $is_valid = true;
 735              }
 736          }
 737  
 738          /**
 739           * Filters the output validation result for an ability.
 740           *
 741           * Allows developers to add custom validation logic on top of the default
 742           * JSON Schema validation. If default validation already failed, the filter
 743           * receives the WP_Error object and can add additional error information or
 744           * override it. If default validation passed, the filter can add additional
 745           * validation checks and return a WP_Error if those checks fail.
 746           *
 747           * @since 7.1.0
 748           *
 749           * @param true|WP_Error $is_valid     The validation result from default validation.
 750           * @param mixed         $output       The output data being validated.
 751           * @param string        $ability_name The name of the ability.
 752           */
 753          $validity = apply_filters( 'wp_ability_validate_output', $is_valid, $output, $this->name );
 754          if ( false === $validity ) {
 755              return new WP_Error( 'ability_invalid_output', __( 'Invalid output.' ) );
 756          }
 757          if ( is_wp_error( $validity ) && $validity->has_errors() ) {
 758              return $validity;
 759          }
 760          return true;
 761      }
 762  
 763      /**
 764       * Executes the ability after input validation and running a permission check.
 765       * Before returning the return value, it also validates the output.
 766       *
 767       * @since 6.9.0
 768       * @since 7.1.0 Added the `wp_ability_invoked` action.
 769       * @since 7.1.0 Added the `wp_pre_execute_ability` filter.
 770       *
 771       * @param mixed $input Optional. The input data for the ability. Default `null`.
 772       * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
 773       */
 774  	public function execute( $input = null ) {
 775          /**
 776           * Fires when an ability is invoked, before any processing takes place.
 777           *
 778           * This action fires for every call regardless of outcome (validation failure,
 779           * permission denial, short-circuit, or successful execution), and before input
 780           * normalization so the raw input is captured as-is.
 781           *
 782           * @since 7.1.0
 783           *
 784           * @param string     $ability_name The name of the ability.
 785           * @param mixed      $input        The raw input data for the ability, before normalization.
 786           * @param WP_Ability $ability      The ability instance.
 787           */
 788          do_action( 'wp_ability_invoked', $this->name, $input, $this );
 789  
 790          $pre_execute_sentinel = new WP_Filter_Sentinel();
 791  
 792          /**
 793           * Filters whether to short-circuit ability execution.
 794           *
 795           * Returning a value other than the received default bypasses the rest of `execute()` —
 796           * input normalization, input validation, permission checks, the registered execute callback,
 797           * output validation, and the surrounding actions — and the value is returned to the caller
 798           * as-is. Useful for cached responses, rate limiting, maintenance mode, and test mocking.
 799           *
 800           * To continue with normal execution, return `$pre` unchanged. This preserves any value
 801           * (including `null`, `false`, or arbitrary objects) as a valid short-circuit result.
 802           *
 803           * Because validation is bypassed, callers that short-circuit are responsible for the
 804           * integrity of any value they consume from `$input`.
 805           *
 806           * @since 7.1.0
 807           *
 808           * @param mixed      $pre          The pre-computed result. Return this value unchanged to continue execution.
 809           *                                 Default `WP_Filter_Sentinel` instance unique to this invocation.
 810           * @param string     $ability_name The name of the ability.
 811           * @param mixed      $input        The raw input passed to `execute()`.
 812           * @param WP_Ability $ability      The ability instance.
 813           */
 814          $pre = apply_filters( 'wp_pre_execute_ability', $pre_execute_sentinel, $this->name, $input, $this );
 815          if ( $pre !== $pre_execute_sentinel ) {
 816              return $pre;
 817          }
 818  
 819          $input = $this->normalize_input( $input );
 820          if ( is_wp_error( $input ) ) {
 821              return $input;
 822          }
 823  
 824          $is_valid = $this->validate_input( $input );
 825          if ( is_wp_error( $is_valid ) ) {
 826              return $is_valid;
 827          }
 828  
 829          $has_permissions = $this->check_permissions( $input );
 830          if ( true !== $has_permissions ) {
 831              if ( is_wp_error( $has_permissions ) ) {
 832                  // Don't leak the permission check error to someone without the correct perms.
 833                  _doing_it_wrong(
 834                      __METHOD__,
 835                      esc_html( $has_permissions->get_error_message() ),
 836                      '6.9.0'
 837                  );
 838              }
 839  
 840              return new WP_Error(
 841                  'ability_invalid_permissions',
 842                  /* translators: %s ability name. */
 843                  sprintf( __( 'Ability "%s" does not have necessary permission.' ), $this->name )
 844              );
 845          }
 846  
 847          /**
 848           * Fires before an ability gets executed, after input validation and permissions check.
 849           *
 850           * @since 6.9.0
 851           * @since 7.1.0 Added the `$ability` parameter.
 852           *
 853           * @param string     $ability_name The name of the ability.
 854           * @param mixed      $input        The input data for the ability.
 855           * @param WP_Ability $ability      The ability instance.
 856           */
 857          do_action( 'wp_before_execute_ability', $this->name, $input, $this );
 858  
 859          $result = $this->do_execute( $input );
 860          if ( is_wp_error( $result ) ) {
 861              return $result;
 862          }
 863  
 864          $is_valid = $this->validate_output( $result );
 865          if ( is_wp_error( $is_valid ) ) {
 866              return $is_valid;
 867          }
 868  
 869          /**
 870           * Fires immediately after an ability finished executing.
 871           *
 872           * @since 6.9.0
 873           * @since 7.1.0 Added the `$ability` parameter.
 874           *
 875           * @param string     $ability_name The name of the ability.
 876           * @param mixed      $input        The input data for the ability.
 877           * @param mixed      $result       The result of the ability execution.
 878           * @param WP_Ability $ability      The ability instance.
 879           */
 880          do_action( 'wp_after_execute_ability', $this->name, $input, $result, $this );
 881  
 882          return $result;
 883      }
 884  
 885      /**
 886       * Wakeup magic method.
 887       *
 888       * @since 6.9.0
 889       * @throws LogicException If the ability object is unserialized.
 890       *                        This is a security hardening measure to prevent unserialization of the ability.
 891       */
 892  	public function __wakeup(): void {
 893          throw new LogicException( __CLASS__ . ' should never be unserialized.' );
 894      }
 895  
 896      /**
 897       * Sleep magic method.
 898       *
 899       * @since 6.9.0
 900       * @throws LogicException If the ability object is serialized.
 901       *                        This is a security hardening measure to prevent serialization of the ability.
 902       */
 903  	public function __sleep(): array {
 904          throw new LogicException( __CLASS__ . ' should never be serialized.' );
 905      }
 906  }


Generated : Thu Sep 3 08:20:25 2026 Cross-referenced by PHPXref