PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.1
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.1
2.14.1 2.14.0 2.13.0 2.12.0 2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 All 56 releases
thinkrank / dependencies / vendor / wordpress / abilities-api / includes / abilities-api / class-wp-ability.php

class-wp-ability.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.14.1, at dependencies/vendor/wordpress/abilities-api/includes/abilities-api/class-wp-ability.php

644 lines 20.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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, 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>' . __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 * Normalizes the input for the ability, applying the default value from the input schema when needed.
405 *
406 * When no input is provided and the input schema is defined with a top-level `default` key, this method returns
407 * the value of that key. If the input schema does not define a `default`, or if the input schema is empty,
408 * this method returns null. If input is provided, it is returned as-is.
409 *
410 * @since 6.9.0
411 *
412 * @param mixed $input Optional. The raw input provided for the ability. Default `null`.
413 * @return mixed The same input, or the default from schema, or `null` if default not set.
414 */
415 public function normalize_input( $input = null ) {
416 if ( null !== $input ) {
417 return $input;
418 }
419
420 $input_schema = $this->get_input_schema();
421 if ( ! empty( $input_schema ) && array_key_exists( 'default', $input_schema ) ) {
422 return $input_schema['default'];
423 }
424
425 return null;
426 }
427
428 /**
429 * Validates input data against the input schema.
430 *
431 * @since 6.9.0
432 *
433 * @param mixed $input Optional. The input data to validate. Default `null`.
434 * @return true|WP_Error Returns true if valid or the WP_Error object if validation fails.
435 */
436 public function validate_input( $input = null ) {
437 $input_schema = $this->get_input_schema();
438 if ( empty( $input_schema ) ) {
439 if ( null === $input ) {
440 return true;
441 }
442
443 return new WP_Error(
444 'ability_missing_input_schema',
445 sprintf(
446 /* translators: %s ability name. */
447 __( 'Ability "%s" does not define an input schema required to validate the provided input.' ),
448 esc_html( $this->name )
449 )
450 );
451 }
452
453 $valid_input = rest_validate_value_from_schema( $input, $input_schema, 'input' );
454 if ( is_wp_error( $valid_input ) ) {
455 return new WP_Error(
456 'ability_invalid_input',
457 sprintf(
458 /* translators: %1$s ability name, %2$s error message. */
459 __( 'Ability "%1$s" has invalid input. Reason: %2$s' ),
460 esc_html( $this->name ),
461 $valid_input->get_error_message()
462 )
463 );
464 }
465
466 return true;
467 }
468
469 /**
470 * Invokes a callable, ensuring the input is passed through only if the input schema is defined.
471 *
472 * @since 6.9.0
473 *
474 * @param callable $callback The callable to invoke.
475 * @param mixed $input Optional. The input data for the ability. Default `null`.
476 * @return mixed The result of the callable execution.
477 */
478 protected function invoke_callback( callable $callback, $input = null ) {
479 $args = array();
480 if ( ! empty( $this->get_input_schema() ) ) {
481 $args[] = $input;
482 }
483
484 return $callback( ...$args );
485 }
486
487 /**
488 * Checks whether the ability has the necessary permissions.
489 *
490 * Please note that input is not automatically validated against the input schema.
491 * Use `validate_input()` method to validate input before calling this method if needed.
492 *
493 * @since 6.9.0
494 *
495 * @see validate_input()
496 *
497 * @param mixed $input Optional. The valid input data for permission checking. Default `null`.
498 * @return bool|WP_Error Whether the ability has the necessary permission.
499 */
500 public function check_permissions( $input = null ) {
501 return $this->invoke_callback( $this->permission_callback, $input );
502 }
503
504 /**
505 * Executes the ability callback.
506 *
507 * @since 6.9.0
508 *
509 * @param mixed $input Optional. The input data for the ability. Default `null`.
510 * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
511 */
512 protected function do_execute( $input = null ) {
513 if ( ! is_callable( $this->execute_callback ) ) {
514 return new WP_Error(
515 'ability_invalid_execute_callback',
516 /* translators: %s ability name. */
517 sprintf( __( 'Ability "%s" does not have a valid execute callback.' ), esc_html( $this->name ) )
518 );
519 }
520
521 return $this->invoke_callback( $this->execute_callback, $input );
522 }
523
524 /**
525 * Validates output data against the output schema.
526 *
527 * @since 6.9.0
528 *
529 * @param mixed $output The output data to validate.
530 * @return true|WP_Error Returns true if valid, or a WP_Error object if validation fails.
531 */
532 protected function validate_output( $output ) {
533 $output_schema = $this->get_output_schema();
534 if ( empty( $output_schema ) ) {
535 return true;
536 }
537
538 $valid_output = rest_validate_value_from_schema( $output, $output_schema, 'output' );
539 if ( is_wp_error( $valid_output ) ) {
540 return new WP_Error(
541 'ability_invalid_output',
542 sprintf(
543 /* translators: %1$s ability name, %2$s error message. */
544 __( 'Ability "%1$s" has invalid output. Reason: %2$s' ),
545 esc_html( $this->name ),
546 $valid_output->get_error_message()
547 )
548 );
549 }
550
551 return true;
552 }
553
554 /**
555 * Executes the ability after input validation and running a permission check.
556 * Before returning the return value, it also validates the output.
557 *
558 * @since 6.9.0
559 *
560 * @param mixed $input Optional. The input data for the ability. Default `null`.
561 * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
562 */
563 public function execute( $input = null ) {
564 $input = $this->normalize_input( $input );
565 $is_valid = $this->validate_input( $input );
566 if ( is_wp_error( $is_valid ) ) {
567 return $is_valid;
568 }
569
570 $has_permissions = $this->check_permissions( $input );
571 if ( true !== $has_permissions ) {
572 if ( is_wp_error( $has_permissions ) ) {
573 // Don't leak the permission check error to someone without the correct perms.
574 _doing_it_wrong(
575 __METHOD__,
576 esc_html( $has_permissions->get_error_message() ),
577 '6.9.0'
578 );
579 }
580
581 return new WP_Error(
582 'ability_invalid_permissions',
583 /* translators: %s ability name. */
584 sprintf( __( 'Ability "%s" does not have necessary permission.' ), esc_html( $this->name ) )
585 );
586 }
587
588 /**
589 * Fires before an ability gets executed, after input validation and permissions check.
590 *
591 * @since 6.9.0
592 *
593 * @param string $ability_name The name of the ability.
594 * @param mixed $input The input data for the ability.
595 */
596 do_action( 'wp_before_execute_ability', $this->name, $input );
597
598 $result = $this->do_execute( $input );
599 if ( is_wp_error( $result ) ) {
600 return $result;
601 }
602
603 $is_valid = $this->validate_output( $result );
604 if ( is_wp_error( $is_valid ) ) {
605 return $is_valid;
606 }
607
608 /**
609 * Fires immediately after an ability finished executing.
610 *
611 * @since 6.9.0
612 *
613 * @param string $ability_name The name of the ability.
614 * @param mixed $input The input data for the ability.
615 * @param mixed $result The result of the ability execution.
616 */
617 do_action( 'wp_after_execute_ability', $this->name, $input, $result );
618
619 return $result;
620 }
621
622 /**
623 * Wakeup magic method.
624 *
625 * @since 6.9.0
626 * @throws LogicException If the ability object is unserialized.
627 * This is a security hardening measure to prevent unserialization of the ability.
628 */
629 public function __wakeup(): void {
630 throw new LogicException( __CLASS__ . ' should never be unserialized.' );
631 }
632
633 /**
634 * Sleep magic method.
635 *
636 * @since 6.9.0
637 * @throws LogicException If the ability object is serialized.
638 * This is a security hardening measure to prevent serialization of the ability.
639 */
640 public function __sleep(): array {
641 throw new LogicException( __CLASS__ . ' should never be serialized.' );
642 }
643 }
644