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