@@ -436,22 +436,41 @@ public function get_meta_item( string $key, $default_value = null ) {
436436 * the value of that key. If the input schema does not define a `default`, or if the input schema is empty,
437437 * this method returns null. If input is provided, it is returned as-is.
438438 *
439+ * The {@see 'wp_ability_normalize_input'} filter fires after the built-in default-value handling,
440+ * allowing plugins to transform the result.
441+ *
439442 * @since 6.9.0
443+ * @since 7.1.0 Added the `wp_ability_normalize_input` filter.
440444 *
441445 * @param mixed $input Optional. The raw input provided for the ability. Default `null`.
442- * @return mixed The same input, or the default from schema, or `null ` if default not set .
446+ * @return mixed The normalized input, or a `WP_Error ` if a filter returned one .
443447 */
444448 public function normalize_input ( $ input = null ) {
445- if ( null !== $ input ) {
446- return $ input ;
447- }
448-
449- $ input_schema = $ this ->get_input_schema ();
450- if ( ! empty ( $ input_schema ) && array_key_exists ( 'default ' , $ input_schema ) ) {
451- return $ input_schema ['default ' ];
449+ if ( null === $ input ) {
450+ $ input_schema = $ this ->get_input_schema ();
451+ if ( array_key_exists ( 'default ' , $ input_schema ) ) {
452+ $ input = $ input_schema ['default ' ];
453+ }
452454 }
453455
454- return null ;
456+ /**
457+ * Filters the normalized input for an ability.
458+ *
459+ * Fires after `normalize_input()` has applied any default value declared in the input schema,
460+ * giving plugins a chance to adjust the input before it is consumed downstream. Common uses
461+ * include defaulting beyond what JSON Schema can express, prompt enrichment, and injecting
462+ * caller metadata.
463+ *
464+ * Returning a `WP_Error` causes callers that propagate it (such as `execute()`) to halt
465+ * before validation, permission checks, and the registered execute callback.
466+ *
467+ * @since 7.1.0
468+ *
469+ * @param mixed $input The normalized input data.
470+ * @param string $ability_name The name of the ability.
471+ * @param WP_Ability $ability The ability instance.
472+ */
473+ return apply_filters ( 'wp_ability_normalize_input ' , $ input , $ this ->name , $ this );
455474 }
456475
457476 /**
@@ -531,7 +550,11 @@ protected function invoke_callback( callable $callback, $input = null ) {
531550 * Please note that input is not automatically validated against the input schema.
532551 * Use `validate_input()` method to validate input before calling this method if needed.
533552 *
553+ * The {@see 'wp_ability_permission_result'} filter fires after the registered
554+ * `permission_callback` returns, allowing plugins to override the result.
555+ *
534556 * @since 6.9.0
557+ * @since 7.1.0 Added the `wp_ability_permission_result` filter.
535558 *
536559 * @see validate_input()
537560 *
@@ -547,27 +570,76 @@ public function check_permissions( $input = null ) {
547570 );
548571 }
549572
550- return $ this ->invoke_callback ( $ this ->permission_callback , $ input );
573+ $ permission = $ this ->invoke_callback ( $ this ->permission_callback , $ input );
574+
575+ /**
576+ * Filters the result of an ability's permission check.
577+ *
578+ * Fires after the registered `permission_callback` returns. Plugins can use this to layer
579+ * additional authorization rules on top of the ability's own permission logic — for example,
580+ * multi-factor authorization gates or temporary permission elevation for trusted contexts.
581+ *
582+ * Filters can return `true` to grant, `false` to deny, or a `WP_Error` to deny with a specific
583+ * error code and message. The filter receives whatever the `permission_callback` produced.
584+ * Any other return value is coerced to `false`.
585+ *
586+ * @since 7.1.0
587+ *
588+ * @param bool|WP_Error $permission The permission result returned by `permission_callback`.
589+ * @param string $ability_name The name of the ability.
590+ * @param mixed $input The input data for the permission check.
591+ * @param WP_Ability $ability The ability instance.
592+ */
593+ $ result = apply_filters ( 'wp_ability_permission_result ' , $ permission , $ this ->name , $ input , $ this );
594+ if ( ! is_bool ( $ result ) && ! is_wp_error ( $ result ) ) {
595+ $ result = false ;
596+ }
597+ return $ result ;
551598 }
552599
553600 /**
554601 * Executes the ability callback.
555602 *
603+ * The {@see 'wp_ability_execute_result'} filter fires before this method returns, allowing
604+ * plugins to transform the result produced by the registered `execute_callback`.
605+ *
556606 * @since 6.9.0
607+ * @since 7.1.0 Added the `wp_ability_execute_result` filter.
557608 *
558609 * @param mixed $input Optional. The input data for the ability. Default `null`.
559610 * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
560611 */
561612 protected function do_execute ( $ input = null ) {
562613 if ( ! is_callable ( $ this ->execute_callback ) ) {
563- return new WP_Error (
614+ $ result = new WP_Error (
564615 'ability_invalid_execute_callback ' ,
565616 /* translators: %s ability name. */
566617 sprintf ( __ ( 'Ability "%s" does not have a valid execute callback. ' ), esc_html ( $ this ->name ) )
567618 );
619+ } else {
620+ $ result = $ this ->invoke_callback ( $ this ->execute_callback , $ input );
568621 }
569622
570- return $ this ->invoke_callback ( $ this ->execute_callback , $ input );
623+ /**
624+ * Filters the result returned by an ability's execute callback.
625+ *
626+ * Fires after the registered execute callback runs. Plugins can use this to transform the
627+ * result — response formatting, stripping internal metadata, content safety filtering,
628+ * response enrichment, or recovering from a failure by returning a successful value.
629+ *
630+ * The filter receives whatever the registered callback produced, including a `WP_Error`
631+ * if execution failed. Filters may pass the `WP_Error` through unchanged, override it with
632+ * a recovered result, or convert a successful result into a `WP_Error`.
633+ *
634+ * @since 7.1.0
635+ *
636+ * @param mixed $result The result returned by the registered `execute_callback`,
637+ * or a `WP_Error` if execution failed.
638+ * @param string $ability_name The name of the ability.
639+ * @param mixed $input The normalized input data.
640+ * @param WP_Ability $ability The ability instance.
641+ */
642+ return apply_filters ( 'wp_ability_execute_result ' , $ result , $ this ->name , $ input , $ this );
571643 }
572644
573645 /**
@@ -605,12 +677,45 @@ protected function validate_output( $output ) {
605677 * Before returning the return value, it also validates the output.
606678 *
607679 * @since 6.9.0
680+ * @since 7.1.0 Added the `wp_pre_execute_ability` filter.
608681 *
609682 * @param mixed $input Optional. The input data for the ability. Default `null`.
610683 * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
611684 */
612685 public function execute ( $ input = null ) {
613- $ input = $ this ->normalize_input ( $ input );
686+ /**
687+ * Filters whether to short-circuit ability execution.
688+ *
689+ * Returning a value other than the received default bypasses the rest of `execute()` —
690+ * input normalization, input validation, permission checks, the registered execute callback,
691+ * output validation, and the surrounding actions — and the value is returned to the caller
692+ * as-is. Useful for cached responses, rate limiting, maintenance mode, and test mocking.
693+ *
694+ * To continue with normal execution, return `$pre` unchanged. This preserves any value
695+ * (including `null`, `false`, or arbitrary objects) as a valid short-circuit result.
696+ *
697+ * Because validation is bypassed, callers that short-circuit are responsible for the
698+ * integrity of any value they consume from `$input`.
699+ *
700+ * @since 7.1.0
701+ *
702+ * @param mixed $pre The pre-computed result. Return this value unchanged to continue execution.
703+ * Default `WP_Filter_Sentinel` instance unique to this invocation.
704+ * @param string $ability_name The name of the ability.
705+ * @param mixed $input The raw input passed to `execute()`.
706+ * @param WP_Ability $ability The ability instance.
707+ */
708+ $ pre_execute_sentinel = new WP_Filter_Sentinel ();
709+ $ pre = apply_filters ( 'wp_pre_execute_ability ' , $ pre_execute_sentinel , $ this ->name , $ input , $ this );
710+ if ( $ pre !== $ pre_execute_sentinel ) {
711+ return $ pre ;
712+ }
713+
714+ $ input = $ this ->normalize_input ( $ input );
715+ if ( is_wp_error ( $ input ) ) {
716+ return $ input ;
717+ }
718+
614719 $ is_valid = $ this ->validate_input ( $ input );
615720 if ( is_wp_error ( $ is_valid ) ) {
616721 return $ is_valid ;
0 commit comments