PluginProbe
The Innovative Form Builder – IvyForms / 1.5.1
The Innovative Form Builder – IvyForms v1.5.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / vendor / wordpress / abilities-api / includes / abilities-api / class-wp-ability.php

class-wp-ability.php in The Innovative Form Builder – IvyForms 1.5.1, at backend/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