[ 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, (null|bool)>
  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, null|bool> $annotations  Optional. Annotation metadata for the ability.
 154       *         @type bool                     $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
 155       *     }
 156       * }
 157       */
 158  	public function __construct( string $name, array $args ) {
 159          $this->name = $name;
 160  
 161          $properties = $this->prepare_properties( $args );
 162  
 163          foreach ( $properties as $property_name => $property_value ) {
 164              if ( ! property_exists( $this, $property_name ) ) {
 165                  _doing_it_wrong(
 166                      __METHOD__,
 167                      sprintf(
 168                          /* translators: %s: Property name. */
 169                          __( 'Property "%1$s" is not a valid property for ability "%2$s". Please check the %3$s class for allowed properties.' ),
 170                          '<code>' . esc_html( $property_name ) . '</code>',
 171                          '<code>' . esc_html( $this->name ) . '</code>',
 172                          '<code>' . self::class . '</code>'
 173                      ),
 174                      '6.9.0'
 175                  );
 176                  continue;
 177              }
 178  
 179              $this->$property_name = $property_value;
 180          }
 181      }
 182  
 183      /**
 184       * Prepares and validates the properties used to instantiate the ability.
 185       *
 186       * Errors are thrown as exceptions instead of WP_Errors to allow for simpler handling and overloading. They are then
 187       * caught and converted to a WP_Error when by WP_Abilities_Registry::register().
 188       *
 189       * @since 6.9.0
 190       *
 191       * @see WP_Abilities_Registry::register()
 192       *
 193       * @param array<string, mixed> $args {
 194       *     An associative array of arguments used to instantiate the ability class.
 195       *
 196       *     @type string               $label                 The human-readable label for the ability.
 197       *     @type string               $description           A detailed description of what the ability does.
 198       *     @type string               $category              The ability category slug this ability belongs to.
 199       *     @type callable             $execute_callback      A callback function to execute when the ability is invoked.
 200       *                                                       Receives optional mixed input and returns mixed result or WP_Error.
 201       *     @type callable             $permission_callback   A callback function to check permissions before execution.
 202       *                                                       Receives optional mixed input and returns bool or WP_Error.
 203       *     @type array<string, mixed> $input_schema          Optional. JSON Schema definition for the ability's input. Required if ability accepts an input.
 204       *     @type array<string, mixed> $output_schema         Optional. JSON Schema definition for the ability's output.
 205       *     @type array<string, mixed> $meta                  {
 206       *         Optional. Additional metadata for the ability.
 207       *
 208       *         @type array<string, null|bool> $annotations  Optional. Annotation metadata for the ability.
 209       *         @type bool                     $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
 210       *     }
 211       * }
 212       * @return array<string, mixed> {
 213       *     An associative array of arguments with validated and prepared properties for the ability class.
 214       *
 215       *     @type string               $label                 The human-readable label for the ability.
 216       *     @type string               $description           A detailed description of what the ability does.
 217       *     @type string               $category              The ability category slug this ability belongs to.
 218       *     @type callable             $execute_callback      A callback function to execute when the ability is invoked.
 219       *                                                       Receives optional mixed input and returns mixed result or WP_Error.
 220       *     @type callable             $permission_callback   A callback function to check permissions before execution.
 221       *                                                       Receives optional mixed input and returns bool or WP_Error.
 222       *     @type array<string, mixed> $input_schema          Optional. JSON Schema definition for the ability's input.
 223       *     @type array<string, mixed> $output_schema         Optional. JSON Schema definition for the ability's output.
 224       *     @type array<string, mixed> $meta                  {
 225       *         Additional metadata for the ability.
 226       *
 227       *         @type array<string, null|bool> $annotations  Optional. Annotation metadata for the ability.
 228       *         @type bool                     $show_in_rest Whether to expose this ability in the REST API. Default false.
 229       *     }
 230       * }
 231       * @throws InvalidArgumentException if an argument is invalid.
 232       */
 233  	protected function prepare_properties( array $args ): array {
 234          // Required args must be present and of the correct type.
 235          if ( empty( $args['label'] ) || ! is_string( $args['label'] ) ) {
 236              throw new InvalidArgumentException(
 237                  __( 'The ability properties must contain a `label` string.' )
 238              );
 239          }
 240  
 241          if ( empty( $args['description'] ) || ! is_string( $args['description'] ) ) {
 242              throw new InvalidArgumentException(
 243                  __( 'The ability properties must contain a `description` string.' )
 244              );
 245          }
 246  
 247          if ( empty( $args['category'] ) || ! is_string( $args['category'] ) ) {
 248              throw new InvalidArgumentException(
 249                  __( 'The ability properties must contain a `category` string.' )
 250              );
 251          }
 252  
 253          if ( empty( $args['execute_callback'] ) || ! is_callable( $args['execute_callback'] ) ) {
 254              throw new InvalidArgumentException(
 255                  __( 'The ability properties must contain a valid `execute_callback` function.' )
 256              );
 257          }
 258  
 259          if ( empty( $args['permission_callback'] ) || ! is_callable( $args['permission_callback'] ) ) {
 260              throw new InvalidArgumentException(
 261                  __( 'The ability properties must provide a valid `permission_callback` function.' )
 262              );
 263          }
 264  
 265          // Optional args only need to be of the correct type if they are present.
 266          if ( isset( $args['input_schema'] ) && ! is_array( $args['input_schema'] ) ) {
 267              throw new InvalidArgumentException(
 268                  __( 'The ability properties should provide a valid `input_schema` definition.' )
 269              );
 270          }
 271  
 272          if ( isset( $args['output_schema'] ) && ! is_array( $args['output_schema'] ) ) {
 273              throw new InvalidArgumentException(
 274                  __( 'The ability properties should provide a valid `output_schema` definition.' )
 275              );
 276          }
 277  
 278          if ( isset( $args['meta'] ) && ! is_array( $args['meta'] ) ) {
 279              throw new InvalidArgumentException(
 280                  __( 'The ability properties should provide a valid `meta` array.' )
 281              );
 282          }
 283  
 284          if ( isset( $args['meta']['annotations'] ) && ! is_array( $args['meta']['annotations'] ) ) {
 285              throw new InvalidArgumentException(
 286                  __( 'The ability meta should provide a valid `annotations` array.' )
 287              );
 288          }
 289  
 290          if ( isset( $args['meta']['show_in_rest'] ) && ! is_bool( $args['meta']['show_in_rest'] ) ) {
 291              throw new InvalidArgumentException(
 292                  __( 'The ability meta should provide a valid `show_in_rest` boolean.' )
 293              );
 294          }
 295  
 296          // Set defaults for optional meta.
 297          $args['meta']                = wp_parse_args(
 298              $args['meta'] ?? array(),
 299              array(
 300                  'annotations'  => static::$default_annotations,
 301                  'show_in_rest' => self::DEFAULT_SHOW_IN_REST,
 302              )
 303          );
 304          $args['meta']['annotations'] = wp_parse_args(
 305              $args['meta']['annotations'],
 306              static::$default_annotations
 307          );
 308  
 309          return $args;
 310      }
 311  
 312      /**
 313       * Retrieves the name of the ability, with its namespace.
 314       * Example: `my-plugin/my-ability`.
 315       *
 316       * @since 6.9.0
 317       *
 318       * @return string The ability name, with its namespace.
 319       */
 320  	public function get_name(): string {
 321          return $this->name;
 322      }
 323  
 324      /**
 325       * Retrieves the human-readable label for the ability.
 326       *
 327       * @since 6.9.0
 328       *
 329       * @return string The human-readable ability label.
 330       */
 331  	public function get_label(): string {
 332          return $this->label;
 333      }
 334  
 335      /**
 336       * Retrieves the detailed description for the ability.
 337       *
 338       * @since 6.9.0
 339       *
 340       * @return string The detailed description for the ability.
 341       */
 342  	public function get_description(): string {
 343          return $this->description;
 344      }
 345  
 346      /**
 347       * Retrieves the ability category for the ability.
 348       *
 349       * @since 6.9.0
 350       *
 351       * @return string The ability category for the ability.
 352       */
 353  	public function get_category(): string {
 354          return $this->category;
 355      }
 356  
 357      /**
 358       * Retrieves the input schema for the ability.
 359       *
 360       * @since 6.9.0
 361       *
 362       * @return array<string, mixed> The input schema for the ability.
 363       */
 364  	public function get_input_schema(): array {
 365          return $this->input_schema;
 366      }
 367  
 368      /**
 369       * Retrieves the output schema for the ability.
 370       *
 371       * @since 6.9.0
 372       *
 373       * @return array<string, mixed> The output schema for the ability.
 374       */
 375  	public function get_output_schema(): array {
 376          return $this->output_schema;
 377      }
 378  
 379      /**
 380       * Retrieves the metadata for the ability.
 381       *
 382       * @since 6.9.0
 383       *
 384       * @return array<string, mixed> The metadata for the ability.
 385       */
 386  	public function get_meta(): array {
 387          return $this->meta;
 388      }
 389  
 390      /**
 391       * Retrieves a specific metadata item for the ability.
 392       *
 393       * @since 6.9.0
 394       *
 395       * @param string $key           The metadata key to retrieve.
 396       * @param mixed  $default_value Optional. The default value to return if the metadata item is not found. Default `null`.
 397       * @return mixed The value of the metadata item, or the default value if not found.
 398       */
 399  	public function get_meta_item( string $key, $default_value = null ) {
 400          return array_key_exists( $key, $this->meta ) ? $this->meta[ $key ] : $default_value;
 401      }
 402  
 403      /**
 404       * Validates input data against the input schema.
 405       *
 406       * @since 6.9.0
 407       *
 408       * @param mixed $input Optional. The input data to validate. Default `null`.
 409       * @return true|WP_Error Returns true if valid or the WP_Error object if validation fails.
 410       */
 411  	public function validate_input( $input = null ) {
 412          $input_schema = $this->get_input_schema();
 413          if ( empty( $input_schema ) ) {
 414              if ( null === $input ) {
 415                  return true;
 416              }
 417  
 418              return new WP_Error(
 419                  'ability_missing_input_schema',
 420                  sprintf(
 421                      /* translators: %s ability name. */
 422                      __( 'Ability "%s" does not define an input schema required to validate the provided input.' ),
 423                      $this->name
 424                  )
 425              );
 426          }
 427  
 428          $valid_input = rest_validate_value_from_schema( $input, $input_schema, 'input' );
 429          if ( is_wp_error( $valid_input ) ) {
 430              return new WP_Error(
 431                  'ability_invalid_input',
 432                  sprintf(
 433                      /* translators: %1$s ability name, %2$s error message. */
 434                      __( 'Ability "%1$s" has invalid input. Reason: %2$s' ),
 435                      $this->name,
 436                      $valid_input->get_error_message()
 437                  )
 438              );
 439          }
 440  
 441          return true;
 442      }
 443  
 444      /**
 445       * Invokes a callable, ensuring the input is passed through only if the input schema is defined.
 446       *
 447       * @since 6.9.0
 448       *
 449       * @param callable $callback The callable to invoke.
 450       * @param mixed    $input    Optional. The input data for the ability. Default `null`.
 451       * @return mixed The result of the callable execution.
 452       */
 453  	protected function invoke_callback( callable $callback, $input = null ) {
 454          $args = array();
 455          if ( ! empty( $this->get_input_schema() ) ) {
 456              $args[] = $input;
 457          }
 458  
 459          return $callback( ...$args );
 460      }
 461  
 462      /**
 463       * Checks whether the ability has the necessary permissions.
 464       *
 465       * Please note that input is not automatically validated against the input schema.
 466       * Use `validate_input()` method to validate input before calling this method if needed.
 467       *
 468       * @since 6.9.0
 469       *
 470       * @see validate_input()
 471       *
 472       * @param mixed $input Optional. The valid input data for permission checking. Default `null`.
 473       * @return bool|WP_Error Whether the ability has the necessary permission.
 474       */
 475  	public function check_permissions( $input = null ) {
 476          return $this->invoke_callback( $this->permission_callback, $input );
 477      }
 478  
 479      /**
 480       * Executes the ability callback.
 481       *
 482       * @since 6.9.0
 483       *
 484       * @param mixed $input Optional. The input data for the ability. Default `null`.
 485       * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
 486       */
 487  	protected function do_execute( $input = null ) {
 488          if ( ! is_callable( $this->execute_callback ) ) {
 489              return new WP_Error(
 490                  'ability_invalid_execute_callback',
 491                  /* translators: %s ability name. */
 492                  sprintf( __( 'Ability "%s" does not have a valid execute callback.' ), $this->name )
 493              );
 494          }
 495  
 496          return $this->invoke_callback( $this->execute_callback, $input );
 497      }
 498  
 499      /**
 500       * Validates output data against the output schema.
 501       *
 502       * @since 6.9.0
 503       *
 504       * @param mixed $output The output data to validate.
 505       * @return true|WP_Error Returns true if valid, or a WP_Error object if validation fails.
 506       */
 507  	protected function validate_output( $output ) {
 508          $output_schema = $this->get_output_schema();
 509          if ( empty( $output_schema ) ) {
 510              return true;
 511          }
 512  
 513          $valid_output = rest_validate_value_from_schema( $output, $output_schema, 'output' );
 514          if ( is_wp_error( $valid_output ) ) {
 515              return new WP_Error(
 516                  'ability_invalid_output',
 517                  sprintf(
 518                      /* translators: %1$s ability name, %2$s error message. */
 519                      __( 'Ability "%1$s" has invalid output. Reason: %2$s' ),
 520                      $this->name,
 521                      $valid_output->get_error_message()
 522                  )
 523              );
 524          }
 525  
 526          return true;
 527      }
 528  
 529      /**
 530       * Executes the ability after input validation and running a permission check.
 531       * Before returning the return value, it also validates the output.
 532       *
 533       * @since 6.9.0
 534       *
 535       * @param mixed $input Optional. The input data for the ability. Default `null`.
 536       * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
 537       */
 538  	public function execute( $input = null ) {
 539          $is_valid = $this->validate_input( $input );
 540          if ( is_wp_error( $is_valid ) ) {
 541              return $is_valid;
 542          }
 543  
 544          $has_permissions = $this->check_permissions( $input );
 545          if ( true !== $has_permissions ) {
 546              if ( is_wp_error( $has_permissions ) ) {
 547                  // Don't leak the permission check error to someone without the correct perms.
 548                  _doing_it_wrong(
 549                      __METHOD__,
 550                      esc_html( $has_permissions->get_error_message() ),
 551                      '6.9.0'
 552                  );
 553              }
 554  
 555              return new WP_Error(
 556                  'ability_invalid_permissions',
 557                  /* translators: %s ability name. */
 558                  sprintf( __( 'Ability "%s" does not have necessary permission.' ), $this->name )
 559              );
 560          }
 561  
 562          /**
 563           * Fires before an ability gets executed, after input validation and permissions check.
 564           *
 565           * @since 6.9.0
 566           *
 567           * @param string $ability_name The name of the ability.
 568           * @param mixed  $input        The input data for the ability.
 569           */
 570          do_action( 'wp_before_execute_ability', $this->name, $input );
 571  
 572          $result = $this->do_execute( $input );
 573          if ( is_wp_error( $result ) ) {
 574              return $result;
 575          }
 576  
 577          $is_valid = $this->validate_output( $result );
 578          if ( is_wp_error( $is_valid ) ) {
 579              return $is_valid;
 580          }
 581  
 582          /**
 583           * Fires immediately after an ability finished executing.
 584           *
 585           * @since 6.9.0
 586           *
 587           * @param string $ability_name The name of the ability.
 588           * @param mixed  $input        The input data for the ability.
 589           * @param mixed  $result       The result of the ability execution.
 590           */
 591          do_action( 'wp_after_execute_ability', $this->name, $input, $result );
 592  
 593          return $result;
 594      }
 595  
 596      /**
 597       * Wakeup magic method.
 598       *
 599       * @since 6.9.0
 600       * @throws LogicException If the ability object is unserialized.
 601       *                        This is a security hardening measure to prevent unserialization of the ability.
 602       */
 603  	public function __wakeup(): void {
 604          throw new LogicException( __CLASS__ . ' should never be unserialized.' );
 605      }
 606  
 607      /**
 608       * Sleep magic method.
 609       *
 610       * @since 6.9.0
 611       * @throws LogicException If the ability object is serialized.
 612       *                        This is a security hardening measure to prevent serialization of the ability.
 613       */
 614  	public function __sleep(): array {
 615          throw new LogicException( __CLASS__ . ' should never be serialized' );
 616      }
 617  }


Generated : Wed Oct 22 08:20:04 2025 Cross-referenced by PHPXref