Make WordPress Core


Ignore:
Timestamp:
05/21/2026 07:56:31 AM (2 days ago)
Author:
gziolo
Message:

Abilities API: Add execution lifecycle filters to WP_Ability methods

Introduce four filters that give plugins hook points across the ability execution lifecycle, complementing the existing observation-only actions
(wp_before_execute_ability, wp_after_execute_ability):

  • wp_pre_execute_ability: short-circuits execute() when it returns a value other than the supplied default.
  • wp_ability_normalize_input: transforms input inside normalize_input(), and returning WP_Error halts execution.
  • wp_ability_permission_result: overrides the permission_callback result inside check_permissions(), consistently for execute() and direct callers.
  • wp_ability_execute_result: transforms the result inside do_execute() before output validation, and can recover from execute callback failures.

The input and result filters fire before their respective schema validation steps, so validate_input() and validate_output() remain the final integrity gates. Only wp_pre_execute_ability can bypass validation, with the caller owning the returned value's shape.

Add WP_Filter_Sentinel, a reusable marker class loaded alongside WP_Hook, whose per-instance identity lets a filter default be distinguished from any
user value — including null, false, or arbitrary objects — via ===.

Update WP_REST_Abilities_V1_Run_Controller::check_ability_permissions() to propagate WP_Error results from normalize_input() directly, defaulting to
status 400 while preserving filter-set statuses (e.g. 422, 429).

Props gziolo, westonruter, migueluy.
Fixes #64989.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/abilities-api/class-wp-ability.php

    r62238 r62397  
    437437     * this method returns null. If input is provided, it is returned as-is.
    438438     *
    439      * @since 6.9.0
     439     * The {@see 'wp_ability_normalize_input'} filter fires after the built-in default-value handling,
     440     * allowing plugins to transform the result.
     441     *
     442     * @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'];
    452         }
    453 
    454         return null;
     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            }
     454        }
     455
     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
     
    532551     * Use `validate_input()` method to validate input before calling this method if needed.
    533552     *
    534      * @since 6.9.0
     553     * The {@see 'wp_ability_permission_result'} filter fires after the registered
     554     * `permission_callback` returns, allowing plugins to override the result.
     555     *
     556     * @since 6.9.0
     557     * @since 7.1.0 Added the `wp_ability_permission_result` filter.
    535558     *
    536559     * @see validate_input()
     
    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
     
    554601     * Executes the ability callback.
    555602     *
    556      * @since 6.9.0
     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     *
     606     * @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`.
     
    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            );
    568         }
    569 
    570         return $this->invoke_callback( $this->execute_callback, $input );
     619        } else {
     620            $result = $this->invoke_callback( $this->execute_callback, $input );
     621        }
     622
     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
     
    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`.
     
    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 ) ) {
Note: See TracChangeset for help on using the changeset viewer.