[ 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 ability annotations.
  33       * They are not guaranteed to provide a faithful description of ability behavior.
  34       *
  35       * @since 6.9.0
  36       * @var array<string, bool|null>
  37       */
  38      protected static $default_annotations = array(
  39          // If true, the ability does not modify its environment.
  40          'readonly'    => null,
  41          /*
  42           * If true, the ability may perform destructive updates to its environment.
  43           * If false, the ability performs only additive updates.
  44           */
  45          'destructive' => null,
  46          /*
  47           * If true, calling the ability repeatedly with the same arguments will have no additional effect
  48           * on its environment.
  49           */
  50          'idempotent'  => null,
  51      );
  52  
  53      /**
  54       * The name of the ability, with its namespace.
  55       * Example: `my-plugin/my-ability`.
  56       *
  57       * @since 6.9.0
  58       * @var string
  59       */
  60      protected $name;
  61  
  62      /**
  63       * The human-readable ability label.
  64       *
  65       * @since 6.9.0
  66       * @var string
  67       */
  68      protected $label;
  69  
  70      /**
  71       * The detailed ability description.
  72       *
  73       * @since 6.9.0
  74       * @var string
  75       */
  76      protected $description;
  77  
  78      /**
  79       * The ability category.
  80       *
  81       * @since 6.9.0
  82       * @var string
  83       */
  84      protected $category;
  85  
  86      /**
  87       * The optional ability input schema.
  88       *
  89       * @since 6.9.0
  90       * @var array<string, mixed>
  91       */
  92      protected $input_schema = array();
  93  
  94      /**
  95       * The optional ability output schema.
  96       *
  97       * @since 6.9.0
  98       * @var array<string, mixed>
  99       */
 100      protected $output_schema = array();
 101  
 102      /**
 103       * The ability execute callback.
 104       *
 105       * @since 6.9.0
 106       * @var callable( mixed $input= ): (mixed|WP_Error)
 107       */
 108      protected $execute_callback;
 109  
 110      /**
 111       * The optional ability permission callback.
 112       *
 113       * @since 6.9.0
 114       * @var callable( mixed $input= ): (bool|WP_Error)
 115       */
 116      protected $permission_callback;
 117  
 118      /**
 119       * The optional ability metadata.
 120       *
 121       * @since 6.9.0
 122       * @var array<string, mixed>
 123       */
 124      protected $meta;
 125  
 126      /**
 127       * Constructor.
 128       *
 129       * Do not use this constructor directly. Instead, use the `wp_register_ability()` function.
 130       *
 131       * @access private
 132       *
 133       * @since 6.9.0
 134       *
 135       * @see wp_register_ability()
 136       *
 137       * @param string               $name The name of the ability, with its namespace.
 138       * @param array<string, mixed> $args {
 139       *     An associative array of arguments for the ability.
 140       *
 141       *     @type string               $label                 The human-readable label for the ability.
 142       *     @type string               $description           A detailed description of what the ability does.
 143       *     @type string               $category              The ability category slug this ability belongs to.
 144       *     @type callable             $execute_callback      A callback function to execute when the ability is invoked.
 145       *                                                       Receives optional mixed input and returns mixed result or WP_Error.
 146       *     @type callable             $permission_callback   A callback function to check permissions before execution.
 147       *                                                       Receives optional mixed input and returns bool or WP_Error.
 148       *     @type array<string, mixed> $input_schema          Optional. JSON Schema definition for the ability's input.
 149       *     @type array<string, mixed> $output_schema         Optional. JSON Schema definition for the ability's output.
 150       *     @type array<string, mixed> $meta                  {
 151       *         Optional. Additional metadata for the ability.
 152       *
 153       *         @type array<string, bool|null> $annotations  {
 154       *             Optional. Semantic annotations describing the ability's behavioral characteristics.
 155       *             These annotations are hints for tooling and documentation.
 156       *
 157       *             @type bool|null $readonly    Optional. If true, the ability does not modify its environment.
 158       *             @type bool|null $destructive Optional. If true, the ability may perform destructive updates to its environment.
 159       *                                          If false, the ability performs only additive updates.
 160       *             @type bool|null $idempotent  Optional. If true, calling the ability repeatedly with the same arguments
 161       *                                          will have no additional effect on its environment.
 162       *         }
 163       *         @type bool                     $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
 164       *     }
 165       * }
 166       */
 167  	public function __construct( string $name, array $args ) {
 168          $this->name = $name;
 169  
 170          $properties = $this->prepare_properties( $args );
 171  
 172          foreach ( $properties as $property_name => $property_value ) {
 173              if ( ! property_exists( $this, $property_name ) ) {
 174                  _doing_it_wrong(
 175                      __METHOD__,
 176                      sprintf(
 177                          /* translators: %s: Property name. */
 178                          __( 'Property "%1$s" is not a valid property for ability "%2$s". Please check the %3$s class for allowed properties.' ),
 179                          '<code>' . esc_html( $property_name ) . '</code>',
 180                          '<code>' . esc_html( $this->name ) . '</code>',
 181                          '<code>' . __CLASS__ . '</code>'
 182                      ),
 183                      '6.9.0'
 184                  );
 185                  continue;
 186              }
 187  
 188              $this->$property_name = $property_value;
 189          }
 190      }
 191  
 192      /**
 193       * Prepares and validates the properties used to instantiate the ability.
 194       *
 195       * Errors are thrown as exceptions instead of WP_Errors to allow for simpler handling and overloading. They are then
 196       * caught and converted to a WP_Error by WP_Abilities_Registry::register().
 197       *
 198       * @since 6.9.0
 199       *
 200       * @see WP_Abilities_Registry::register()
 201       *
 202       * @param array<string, mixed> $args {
 203       *     An associative array of arguments used to instantiate the ability class.
 204       *
 205       *     @type string               $label                 The human-readable label for the ability.
 206       *     @type string               $description           A detailed description of what the ability does.
 207       *     @type string               $category              The ability category slug this ability belongs to.
 208       *     @type callable             $execute_callback      A callback function to execute when the ability is invoked.
 209       *                                                       Receives optional mixed input and returns mixed result or WP_Error.
 210       *     @type callable             $permission_callback   A callback function to check permissions before execution.
 211       *                                                       Receives optional mixed input and returns bool or WP_Error.
 212       *     @type array<string, mixed> $input_schema          Optional. JSON Schema definition for the ability's input. Required if ability accepts an input.
 213       *     @type array<string, mixed> $output_schema         Optional. JSON Schema definition for the ability's output.
 214       *     @type array<string, mixed> $meta                  {
 215       *         Optional. Additional metadata for the ability.
 216       *
 217       *         @type array<string, bool|null> $annotations  {
 218       *             Optional. Semantic annotations describing the ability's behavioral characteristics.
 219       *             These annotations are hints for tooling and documentation.
 220       *
 221       *             @type bool|null $readonly    Optional. If true, the ability does not modify its environment.
 222       *             @type bool|null $destructive Optional. If true, the ability may perform destructive updates to its environment.
 223       *                                          If false, the ability performs only additive updates.
 224       *             @type bool|null $idempotent  Optional. If true, calling the ability repeatedly with the same arguments
 225       *                                          will have no additional effect on its environment.
 226       *         }
 227       *         @type bool                     $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
 228       *     }
 229       * }
 230       * @return array<string, mixed> {
 231       *     An associative array of arguments with validated and prepared properties for the ability class.
 232       *
 233       *     @type string               $label                 The human-readable label for the ability.
 234       *     @type string               $description           A detailed description of what the ability does.
 235       *     @type string               $category              The ability category slug this ability belongs to.
 236       *     @type callable             $execute_callback      A callback function to execute when the ability is invoked.
 237       *                                                       Receives optional mixed input and returns mixed result or WP_Error.
 238       *     @type callable             $permission_callback   A callback function to check permissions before execution.
 239       *                                                       Receives optional mixed input and returns bool or WP_Error.
 240       *     @type array<string, mixed> $input_schema          Optional. JSON Schema definition for the ability's input.
 241       *     @type array<string, mixed> $output_schema         Optional. JSON Schema definition for the ability's output.
 242       *     @type array<string, mixed> $meta                  {
 243       *         Additional metadata for the ability.
 244       *
 245       *         @type array<string, bool|null> $annotations  {
 246       *             Semantic annotations describing the ability's behavioral characteristics.
 247       *             These annotations are hints for tooling and documentation.
 248       *
 249       *             @type bool|null $readonly    If true, the ability does not modify its environment.
 250       *             @type bool|null $destructive If true, the ability may perform destructive updates to its environment.
 251       *                                          If false, the ability performs only additive updates.
 252       *             @type bool|null $idempotent  If true, calling the ability repeatedly with the same arguments
 253       *                                          will have no additional effect on its environment.
 254       *         }
 255       *         @type bool                     $show_in_rest Whether to expose this ability in the REST API. Default false.
 256       *     }
 257       * }
 258       * @throws InvalidArgumentException if an argument is invalid.
 259       */
 260  	protected function prepare_properties( array $args ): array {
 261          // Required args must be present and of the correct type.
 262          if ( empty( $args['label'] ) || ! is_string( $args['label'] ) ) {
 263              throw new InvalidArgumentException(
 264                  __( 'The ability properties must contain a `label` string.' )
 265              );
 266          }
 267  
 268          if ( empty( $args['description'] ) || ! is_string( $args['description'] ) ) {
 269              throw new InvalidArgumentException(
 270                  __( 'The ability properties must contain a `description` string.' )
 271              );
 272          }
 273  
 274          if ( empty( $args['category'] ) || ! is_string( $args['category'] ) ) {
 275              throw new InvalidArgumentException(
 276                  __( 'The ability properties must contain a `category` string.' )
 277              );
 278          }
 279  
 280          // If we are not overriding `ability_class` parameter during instantiation, then we need to validate the execute_callback.
 281          if ( get_class( $this ) === self::class && ( empty( $args['execute_callback'] ) || ! is_callable( $args['execute_callback'] ) ) ) {
 282              throw new InvalidArgumentException(
 283                  __( 'The ability properties must contain a valid `execute_callback` function.' )
 284              );
 285          }
 286  
 287          // If we are not overriding `ability_class` parameter during instantiation, then we need to validate the permission_callback.
 288          if ( get_class( $this ) === self::class && ( empty( $args['permission_callback'] ) || ! is_callable( $args['permission_callback'] ) ) ) {
 289              throw new InvalidArgumentException(
 290                  __( 'The ability properties must provide a valid `permission_callback` function.' )
 291              );
 292          }
 293  
 294          // Optional args only need to be of the correct type if they are present.
 295          if ( isset( $args['input_schema'] ) && ! is_array( $args['input_schema'] ) ) {
 296              throw new InvalidArgumentException(
 297                  __( 'The ability properties should provide a valid `input_schema` definition.' )
 298              );
 299          }
 300  
 301          if ( isset( $args['output_schema'] ) && ! is_array( $args['output_schema'] ) ) {
 302              throw new InvalidArgumentException(
 303                  __( 'The ability properties should provide a valid `output_schema` definition.' )
 304              );
 305          }
 306  
 307          if ( isset( $args['meta'] ) && ! is_array( $args['meta'] ) ) {
 308              throw new InvalidArgumentException(
 309                  __( 'The ability properties should provide a valid `meta` array.' )
 310              );
 311          }
 312  
 313          if ( isset( $args['meta']['annotations'] ) && ! is_array( $args['meta']['annotations'] ) ) {
 314              throw new InvalidArgumentException(
 315                  __( 'The ability meta should provide a valid `annotations` array.' )
 316              );
 317          }
 318  
 319          if ( isset( $args['meta']['show_in_rest'] ) && ! is_bool( $args['meta']['show_in_rest'] ) ) {
 320              throw new InvalidArgumentException(
 321                  __( 'The ability meta should provide a valid `show_in_rest` boolean.' )
 322              );
 323          }
 324  
 325          // Set defaults for optional meta.
 326          $args['meta']                = wp_parse_args(
 327              $args['meta'] ?? array(),
 328              array(
 329                  'annotations'  => static::$default_annotations,
 330                  'show_in_rest' => self::DEFAULT_SHOW_IN_REST,
 331              )
 332          );
 333          $args['meta']['annotations'] = wp_parse_args(
 334              $args['meta']['annotations'],
 335              static::$default_annotations
 336          );
 337  
 338          return $args;
 339      }
 340  
 341      /**
 342       * Retrieves the name of the ability, with its namespace.
 343       * Example: `my-plugin/my-ability`.
 344       *
 345       * @since 6.9.0
 346       *
 347       * @return string The ability name, with its namespace.
 348       */
 349  	public function get_name(): string {
 350          return $this->name;
 351      }
 352  
 353      /**
 354       * Retrieves the human-readable label for the ability.
 355       *
 356       * @since 6.9.0
 357       *
 358       * @return string The human-readable ability label.
 359       */
 360  	public function get_label(): string {
 361          return $this->label;
 362      }
 363  
 364      /**
 365       * Retrieves the detailed description for the ability.
 366       *
 367       * @since 6.9.0
 368       *
 369       * @return string The detailed description for the ability.
 370       */
 371  	public function get_description(): string {
 372          return $this->description;
 373      }
 374  
 375      /**
 376       * Retrieves the ability category for the ability.
 377       *
 378       * @since 6.9.0
 379       *
 380       * @return string The ability category for the ability.
 381       */
 382  	public function get_category(): string {
 383          return $this->category;
 384      }
 385  
 386      /**
 387       * Retrieves the input schema for the ability.
 388       *
 389       * @since 6.9.0
 390       *
 391       * @return array<string, mixed> The input schema for the ability.
 392       */
 393  	public function get_input_schema(): array {
 394          return $this->input_schema;
 395      }
 396  
 397      /**
 398       * Retrieves the output schema for the ability.
 399       *
 400       * @since 6.9.0
 401       *
 402       * @return array<string, mixed> The output schema for the ability.
 403       */
 404  	public function get_output_schema(): array {
 405          return $this->output_schema;
 406      }
 407  
 408      /**
 409       * Retrieves the metadata for the ability.
 410       *
 411       * @since 6.9.0
 412       *
 413       * @return array<string, mixed> The metadata for the ability.
 414       */
 415  	public function get_meta(): array {
 416          return $this->meta;
 417      }
 418  
 419      /**
 420       * Retrieves a specific metadata item for the ability.
 421       *
 422       * @since 6.9.0
 423       *
 424       * @param string $key           The metadata key to retrieve.
 425       * @param mixed  $default_value Optional. The default value to return if the metadata item is not found. Default `null`.
 426       * @return mixed The value of the metadata item, or the default value if not found.
 427       */
 428  	public function get_meta_item( string $key, $default_value = null ) {
 429          return array_key_exists( $key, $this->meta ) ? $this->meta[ $key ] : $default_value;
 430      }
 431  
 432      /**
 433       * Normalizes the input for the ability, applying the default value from the input schema when needed.
 434       *
 435       * When no input is provided and the input schema is defined with a top-level `default` key, this method returns
 436       * the value of that key. If the input schema does not define a `default`, or if the input schema is empty,
 437       * this method returns null. If input is provided, it is returned as-is.
 438       *
 439       * @since 6.9.0
 440       *
 441       * @param mixed $input Optional. The raw input provided for the ability. Default `null`.
 442       * @return mixed The same input, or the default from schema, or `null` if default not set.
 443       */
 444  	public function normalize_input( $input = null ) {
 445          if ( null !== $input ) {
 446              return $input;
 447          }
 448  
 449          $input_schema = $this->get_input_schema();
 450          if ( ! empty( $input_schema ) && array_key_exists( 'default', $input_schema ) ) {
 451              return $input_schema['default'];
 452          }
 453  
 454          return null;
 455      }
 456  
 457      /**
 458       * Validates input data against the input schema.
 459       *
 460       * @since 6.9.0
 461       *
 462       * @param mixed $input Optional. The input data to validate. Default `null`.
 463       * @return true|WP_Error Returns true if valid or the WP_Error object if validation fails.
 464       */
 465  	public function validate_input( $input = null ) {
 466          $input_schema = $this->get_input_schema();
 467          if ( empty( $input_schema ) ) {
 468              if ( null === $input ) {
 469                  return true;
 470              }
 471  
 472              return new WP_Error(
 473                  'ability_missing_input_schema',
 474                  sprintf(
 475                      /* translators: %s ability name. */
 476                      __( 'Ability "%s" does not define an input schema required to validate the provided input.' ),
 477                      esc_html( $this->name )
 478                  )
 479              );
 480          }
 481  
 482          $valid_input = rest_validate_value_from_schema( $input, $input_schema, 'input' );
 483          if ( is_wp_error( $valid_input ) ) {
 484              return new WP_Error(
 485                  'ability_invalid_input',
 486                  sprintf(
 487                      /* translators: %1$s ability name, %2$s error message. */
 488                      __( 'Ability "%1$s" has invalid input. Reason: %2$s' ),
 489                      esc_html( $this->name ),
 490                      $valid_input->get_error_message()
 491                  )
 492              );
 493          }
 494  
 495          return true;
 496      }
 497  
 498      /**
 499       * Invokes a callable, ensuring the input is passed through only if the input schema is defined.
 500       *
 501       * @since 6.9.0
 502       *
 503       * @param callable $callback The callable to invoke.
 504       * @param mixed    $input    Optional. The input data for the ability. Default `null`.
 505       * @return mixed The result of the callable execution.
 506       */
 507  	protected function invoke_callback( callable $callback, $input = null ) {
 508          $args = array();
 509          if ( ! empty( $this->get_input_schema() ) ) {
 510              $args[] = $input;
 511          }
 512  
 513          return $callback( ...$args );
 514      }
 515  
 516      /**
 517       * Checks whether the ability has the necessary permissions.
 518       *
 519       * Please note that input is not automatically validated against the input schema.
 520       * Use `validate_input()` method to validate input before calling this method if needed.
 521       *
 522       * @since 6.9.0
 523       *
 524       * @see validate_input()
 525       *
 526       * @param mixed $input Optional. The valid input data for permission checking. Default `null`.
 527       * @return bool|WP_Error Whether the ability has the necessary permission.
 528       */
 529  	public function check_permissions( $input = null ) {
 530          if ( ! is_callable( $this->permission_callback ) ) {
 531              return new WP_Error(
 532                  'ability_invalid_permission_callback',
 533                  /* translators: %s ability name. */
 534                  sprintf( __( 'Ability "%s" does not have a valid permission callback.' ), esc_html( $this->name ) )
 535              );
 536          }
 537  
 538          return $this->invoke_callback( $this->permission_callback, $input );
 539      }
 540  
 541      /**
 542       * Executes the ability callback.
 543       *
 544       * @since 6.9.0
 545       *
 546       * @param mixed $input Optional. The input data for the ability. Default `null`.
 547       * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
 548       */
 549  	protected function do_execute( $input = null ) {
 550          if ( ! is_callable( $this->execute_callback ) ) {
 551              return new WP_Error(
 552                  'ability_invalid_execute_callback',
 553                  /* translators: %s ability name. */
 554                  sprintf( __( 'Ability "%s" does not have a valid execute callback.' ), esc_html( $this->name ) )
 555              );
 556          }
 557  
 558          return $this->invoke_callback( $this->execute_callback, $input );
 559      }
 560  
 561      /**
 562       * Validates output data against the output schema.
 563       *
 564       * @since 6.9.0
 565       *
 566       * @param mixed $output The output data to validate.
 567       * @return true|WP_Error Returns true if valid, or a WP_Error object if validation fails.
 568       */
 569  	protected function validate_output( $output ) {
 570          $output_schema = $this->get_output_schema();
 571          if ( empty( $output_schema ) ) {
 572              return true;
 573          }
 574  
 575          $valid_output = rest_validate_value_from_schema( $output, $output_schema, 'output' );
 576          if ( is_wp_error( $valid_output ) ) {
 577              return new WP_Error(
 578                  'ability_invalid_output',
 579                  sprintf(
 580                      /* translators: %1$s ability name, %2$s error message. */
 581                      __( 'Ability "%1$s" has invalid output. Reason: %2$s' ),
 582                      esc_html( $this->name ),
 583                      $valid_output->get_error_message()
 584                  )
 585              );
 586          }
 587  
 588          return true;
 589      }
 590  
 591      /**
 592       * Executes the ability after input validation and running a permission check.
 593       * Before returning the return value, it also validates the output.
 594       *
 595       * @since 6.9.0
 596       *
 597       * @param mixed $input Optional. The input data for the ability. Default `null`.
 598       * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
 599       */
 600  	public function execute( $input = null ) {
 601          $input    = $this->normalize_input( $input );
 602          $is_valid = $this->validate_input( $input );
 603          if ( is_wp_error( $is_valid ) ) {
 604              return $is_valid;
 605          }
 606  
 607          $has_permissions = $this->check_permissions( $input );
 608          if ( true !== $has_permissions ) {
 609              if ( is_wp_error( $has_permissions ) ) {
 610                  // Don't leak the permission check error to someone without the correct perms.
 611                  _doing_it_wrong(
 612                      __METHOD__,
 613                      esc_html( $has_permissions->get_error_message() ),
 614                      '6.9.0'
 615                  );
 616              }
 617  
 618              return new WP_Error(
 619                  'ability_invalid_permissions',
 620                  /* translators: %s ability name. */
 621                  sprintf( __( 'Ability "%s" does not have necessary permission.' ), esc_html( $this->name ) )
 622              );
 623          }
 624  
 625          /**
 626           * Fires before an ability gets executed, after input validation and permissions check.
 627           *
 628           * @since 6.9.0
 629           *
 630           * @param string $ability_name The name of the ability.
 631           * @param mixed  $input        The input data for the ability.
 632           */
 633          do_action( 'wp_before_execute_ability', $this->name, $input );
 634  
 635          $result = $this->do_execute( $input );
 636          if ( is_wp_error( $result ) ) {
 637              return $result;
 638          }
 639  
 640          $is_valid = $this->validate_output( $result );
 641          if ( is_wp_error( $is_valid ) ) {
 642              return $is_valid;
 643          }
 644  
 645          /**
 646           * Fires immediately after an ability finished executing.
 647           *
 648           * @since 6.9.0
 649           *
 650           * @param string $ability_name The name of the ability.
 651           * @param mixed  $input        The input data for the ability.
 652           * @param mixed  $result       The result of the ability execution.
 653           */
 654          do_action( 'wp_after_execute_ability', $this->name, $input, $result );
 655  
 656          return $result;
 657      }
 658  
 659      /**
 660       * Wakeup magic method.
 661       *
 662       * @since 6.9.0
 663       * @throws LogicException If the ability object is unserialized.
 664       *                        This is a security hardening measure to prevent unserialization of the ability.
 665       */
 666  	public function __wakeup(): void {
 667          throw new LogicException( __CLASS__ . ' should never be unserialized.' );
 668      }
 669  
 670      /**
 671       * Sleep magic method.
 672       *
 673       * @since 6.9.0
 674       * @throws LogicException If the ability object is serialized.
 675       *                        This is a security hardening measure to prevent serialization of the ability.
 676       */
 677  	public function __sleep(): array {
 678          throw new LogicException( __CLASS__ . ' should never be serialized.' );
 679      }
 680  }


Generated : Tue May 5 08:20:14 2026 Cross-referenced by PHPXref