run:R W Run
11.52 KB
2026-03-11 16:18:51
R W Run
7.32 KB
2026-03-11 16:18:51
R W Run
5.76 KB
2026-03-11 16:18:51
R W Run
22.23 KB
2026-03-11 16:18:51
R W Run
error_log
📄class-wp-ability.php
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
12declare( 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 */
21class 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 when 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 ( empty( $args['execute_callback'] ) || ! is_callable( $args['execute_callback'] ) ) {
281 throw new InvalidArgumentException(
282 __( 'The ability properties must contain a valid `execute_callback` function.' )
283 );
284 }
285
286 if ( empty( $args['permission_callback'] ) || ! is_callable( $args['permission_callback'] ) ) {
287 throw new InvalidArgumentException(
288 __( 'The ability properties must provide a valid `permission_callback` function.' )
289 );
290 }
291
292 // Optional args only need to be of the correct type if they are present.
293 if ( isset( $args['input_schema'] ) && ! is_array( $args['input_schema'] ) ) {
294 throw new InvalidArgumentException(
295 __( 'The ability properties should provide a valid `input_schema` definition.' )
296 );
297 }
298
299 if ( isset( $args['output_schema'] ) && ! is_array( $args['output_schema'] ) ) {
300 throw new InvalidArgumentException(
301 __( 'The ability properties should provide a valid `output_schema` definition.' )
302 );
303 }
304
305 if ( isset( $args['meta'] ) && ! is_array( $args['meta'] ) ) {
306 throw new InvalidArgumentException(
307 __( 'The ability properties should provide a valid `meta` array.' )
308 );
309 }
310
311 if ( isset( $args['meta']['annotations'] ) && ! is_array( $args['meta']['annotations'] ) ) {
312 throw new InvalidArgumentException(
313 __( 'The ability meta should provide a valid `annotations` array.' )
314 );
315 }
316
317 if ( isset( $args['meta']['show_in_rest'] ) && ! is_bool( $args['meta']['show_in_rest'] ) ) {
318 throw new InvalidArgumentException(
319 __( 'The ability meta should provide a valid `show_in_rest` boolean.' )
320 );
321 }
322
323 // Set defaults for optional meta.
324 $args['meta'] = wp_parse_args(
325 $args['meta'] ?? array(),
326 array(
327 'annotations' => static::$default_annotations,
328 'show_in_rest' => self::DEFAULT_SHOW_IN_REST,
329 )
330 );
331 $args['meta']['annotations'] = wp_parse_args(
332 $args['meta']['annotations'],
333 static::$default_annotations
334 );
335
336 return $args;
337 }
338
339 /**
340 * Retrieves the name of the ability, with its namespace.
341 * Example: `my-plugin/my-ability`.
342 *
343 * @since 6.9.0
344 *
345 * @return string The ability name, with its namespace.
346 */
347 public function get_name(): string {
348 return $this->name;
349 }
350
351 /**
352 * Retrieves the human-readable label for the ability.
353 *
354 * @since 6.9.0
355 *
356 * @return string The human-readable ability label.
357 */
358 public function get_label(): string {
359 return $this->label;
360 }
361
362 /**
363 * Retrieves the detailed description for the ability.
364 *
365 * @since 6.9.0
366 *
367 * @return string The detailed description for the ability.
368 */
369 public function get_description(): string {
370 return $this->description;
371 }
372
373 /**
374 * Retrieves the ability category for the ability.
375 *
376 * @since 6.9.0
377 *
378 * @return string The ability category for the ability.
379 */
380 public function get_category(): string {
381 return $this->category;
382 }
383
384 /**
385 * Retrieves the input schema for the ability.
386 *
387 * @since 6.9.0
388 *
389 * @return array<string, mixed> The input schema for the ability.
390 */
391 public function get_input_schema(): array {
392 return $this->input_schema;
393 }
394
395 /**
396 * Retrieves the output schema for the ability.
397 *
398 * @since 6.9.0
399 *
400 * @return array<string, mixed> The output schema for the ability.
401 */
402 public function get_output_schema(): array {
403 return $this->output_schema;
404 }
405
406 /**
407 * Retrieves the metadata for the ability.
408 *
409 * @since 6.9.0
410 *
411 * @return array<string, mixed> The metadata for the ability.
412 */
413 public function get_meta(): array {
414 return $this->meta;
415 }
416
417 /**
418 * Retrieves a specific metadata item for the ability.
419 *
420 * @since 6.9.0
421 *
422 * @param string $key The metadata key to retrieve.
423 * @param mixed $default_value Optional. The default value to return if the metadata item is not found. Default `null`.
424 * @return mixed The value of the metadata item, or the default value if not found.
425 */
426 public function get_meta_item( string $key, $default_value = null ) {
427 return array_key_exists( $key, $this->meta ) ? $this->meta[ $key ] : $default_value;
428 }
429
430 /**
431 * Normalizes the input for the ability, applying the default value from the input schema when needed.
432 *
433 * When no input is provided and the input schema is defined with a top-level `default` key, this method returns
434 * the value of that key. If the input schema does not define a `default`, or if the input schema is empty,
435 * this method returns null. If input is provided, it is returned as-is.
436 *
437 * @since 6.9.0
438 *
439 * @param mixed $input Optional. The raw input provided for the ability. Default `null`.
440 * @return mixed The same input, or the default from schema, or `null` if default not set.
441 */
442 public function normalize_input( $input = null ) {
443 if ( null !== $input ) {
444 return $input;
445 }
446
447 $input_schema = $this->get_input_schema();
448 if ( ! empty( $input_schema ) && array_key_exists( 'default', $input_schema ) ) {
449 return $input_schema['default'];
450 }
451
452 return null;
453 }
454
455 /**
456 * Validates input data against the input schema.
457 *
458 * @since 6.9.0
459 *
460 * @param mixed $input Optional. The input data to validate. Default `null`.
461 * @return true|WP_Error Returns true if valid or the WP_Error object if validation fails.
462 */
463 public function validate_input( $input = null ) {
464 $input_schema = $this->get_input_schema();
465 if ( empty( $input_schema ) ) {
466 if ( null === $input ) {
467 return true;
468 }
469
470 return new WP_Error(
471 'ability_missing_input_schema',
472 sprintf(
473 /* translators: %s ability name. */
474 __( 'Ability "%s" does not define an input schema required to validate the provided input.' ),
475 esc_html( $this->name )
476 )
477 );
478 }
479
480 $valid_input = rest_validate_value_from_schema( $input, $input_schema, 'input' );
481 if ( is_wp_error( $valid_input ) ) {
482 return new WP_Error(
483 'ability_invalid_input',
484 sprintf(
485 /* translators: %1$s ability name, %2$s error message. */
486 __( 'Ability "%1$s" has invalid input. Reason: %2$s' ),
487 esc_html( $this->name ),
488 $valid_input->get_error_message()
489 )
490 );
491 }
492
493 return true;
494 }
495
496 /**
497 * Invokes a callable, ensuring the input is passed through only if the input schema is defined.
498 *
499 * @since 6.9.0
500 *
501 * @param callable $callback The callable to invoke.
502 * @param mixed $input Optional. The input data for the ability. Default `null`.
503 * @return mixed The result of the callable execution.
504 */
505 protected function invoke_callback( callable $callback, $input = null ) {
506 $args = array();
507 if ( ! empty( $this->get_input_schema() ) ) {
508 $args[] = $input;
509 }
510
511 return $callback( ...$args );
512 }
513
514 /**
515 * Checks whether the ability has the necessary permissions.
516 *
517 * Please note that input is not automatically validated against the input schema.
518 * Use `validate_input()` method to validate input before calling this method if needed.
519 *
520 * @since 6.9.0
521 *
522 * @see validate_input()
523 *
524 * @param mixed $input Optional. The valid input data for permission checking. Default `null`.
525 * @return bool|WP_Error Whether the ability has the necessary permission.
526 */
527 public function check_permissions( $input = null ) {
528 if ( ! is_callable( $this->permission_callback ) ) {
529 return new WP_Error(
530 'ability_invalid_permission_callback',
531 /* translators: %s ability name. */
532 sprintf( __( 'Ability "%s" does not have a valid permission callback.' ), esc_html( $this->name ) )
533 );
534 }
535
536 return $this->invoke_callback( $this->permission_callback, $input );
537 }
538
539 /**
540 * Executes the ability callback.
541 *
542 * @since 6.9.0
543 *
544 * @param mixed $input Optional. The input data for the ability. Default `null`.
545 * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
546 */
547 protected function do_execute( $input = null ) {
548 if ( ! is_callable( $this->execute_callback ) ) {
549 return new WP_Error(
550 'ability_invalid_execute_callback',
551 /* translators: %s ability name. */
552 sprintf( __( 'Ability "%s" does not have a valid execute callback.' ), esc_html( $this->name ) )
553 );
554 }
555
556 return $this->invoke_callback( $this->execute_callback, $input );
557 }
558
559 /**
560 * Validates output data against the output schema.
561 *
562 * @since 6.9.0
563 *
564 * @param mixed $output The output data to validate.
565 * @return true|WP_Error Returns true if valid, or a WP_Error object if validation fails.
566 */
567 protected function validate_output( $output ) {
568 $output_schema = $this->get_output_schema();
569 if ( empty( $output_schema ) ) {
570 return true;
571 }
572
573 $valid_output = rest_validate_value_from_schema( $output, $output_schema, 'output' );
574 if ( is_wp_error( $valid_output ) ) {
575 return new WP_Error(
576 'ability_invalid_output',
577 sprintf(
578 /* translators: %1$s ability name, %2$s error message. */
579 __( 'Ability "%1$s" has invalid output. Reason: %2$s' ),
580 esc_html( $this->name ),
581 $valid_output->get_error_message()
582 )
583 );
584 }
585
586 return true;
587 }
588
589 /**
590 * Executes the ability after input validation and running a permission check.
591 * Before returning the return value, it also validates the output.
592 *
593 * @since 6.9.0
594 *
595 * @param mixed $input Optional. The input data for the ability. Default `null`.
596 * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
597 */
598 public function execute( $input = null ) {
599 $input = $this->normalize_input( $input );
600 $is_valid = $this->validate_input( $input );
601 if ( is_wp_error( $is_valid ) ) {
602 return $is_valid;
603 }
604
605 $has_permissions = $this->check_permissions( $input );
606 if ( true !== $has_permissions ) {
607 if ( is_wp_error( $has_permissions ) ) {
608 // Don't leak the permission check error to someone without the correct perms.
609 _doing_it_wrong(
610 __METHOD__,
611 esc_html( $has_permissions->get_error_message() ),
612 '6.9.0'
613 );
614 }
615
616 return new WP_Error(
617 'ability_invalid_permissions',
618 /* translators: %s ability name. */
619 sprintf( __( 'Ability "%s" does not have necessary permission.' ), esc_html( $this->name ) )
620 );
621 }
622
623 /**
624 * Fires before an ability gets executed, after input validation and permissions check.
625 *
626 * @since 6.9.0
627 *
628 * @param string $ability_name The name of the ability.
629 * @param mixed $input The input data for the ability.
630 */
631 do_action( 'wp_before_execute_ability', $this->name, $input );
632
633 $result = $this->do_execute( $input );
634 if ( is_wp_error( $result ) ) {
635 return $result;
636 }
637
638 $is_valid = $this->validate_output( $result );
639 if ( is_wp_error( $is_valid ) ) {
640 return $is_valid;
641 }
642
643 /**
644 * Fires immediately after an ability finished executing.
645 *
646 * @since 6.9.0
647 *
648 * @param string $ability_name The name of the ability.
649 * @param mixed $input The input data for the ability.
650 * @param mixed $result The result of the ability execution.
651 */
652 do_action( 'wp_after_execute_ability', $this->name, $input, $result );
653
654 return $result;
655 }
656
657 /**
658 * Wakeup magic method.
659 *
660 * @since 6.9.0
661 * @throws LogicException If the ability object is unserialized.
662 * This is a security hardening measure to prevent unserialization of the ability.
663 */
664 public function __wakeup(): void {
665 throw new LogicException( __CLASS__ . ' should never be unserialized.' );
666 }
667
668 /**
669 * Sleep magic method.
670 *
671 * @since 6.9.0
672 * @throws LogicException If the ability object is serialized.
673 * This is a security hardening measure to prevent serialization of the ability.
674 */
675 public function __sleep(): array {
676 throw new LogicException( __CLASS__ . ' should never be serialized.' );
677 }
678}
679