[ 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, (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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Wed Oct 22 08:20:04 2025 | Cross-referenced by PHPXref |