| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Tue May 5 08:20:14 2026 | Cross-referenced by PHPXref |