Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions src/wp-includes/abilities-api/class-wp-ability.php
Original file line number Diff line number Diff line change
Expand Up @@ -725,12 +725,28 @@ protected function validate_output( $output ) {
* Before returning the return value, it also validates the output.
*
* @since 6.9.0
* @since 7.1.0 Added the `wp_ability_invoked` action.
* @since 7.1.0 Added the `wp_pre_execute_ability` filter.
*
* @param mixed $input Optional. The input data for the ability. Default `null`.
* @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
*/
public function execute( $input = null ) {
/**
* Fires when an ability is invoked, before any processing takes place.
*
* This action fires for every call regardless of outcome (validation failure,
* permission denial, short-circuit, or successful execution), and before input
* normalization so the raw input is captured as-is.
*
* @since 7.1.0
*
* @param string $ability_name The name of the ability.
* @param mixed $input The raw input data for the ability, before normalization.
* @param WP_Ability $ability The ability instance.
*/
do_action( 'wp_ability_invoked', $this->name, $input, $this );

/**
* Filters whether to short-circuit ability execution.
*
Expand Down Expand Up @@ -791,11 +807,13 @@ public function execute( $input = null ) {
* Fires before an ability gets executed, after input validation and permissions check.
*
* @since 6.9.0
* @since 7.1.0 Added the `$ability` parameter.
*
* @param string $ability_name The name of the ability.
* @param mixed $input The input data for the ability.
* @param string $ability_name The name of the ability.
* @param mixed $input The input data for the ability.
* @param WP_Ability $ability The ability instance.
*/
do_action( 'wp_before_execute_ability', $this->name, $input );
do_action( 'wp_before_execute_ability', $this->name, $input, $this );
Comment thread
gziolo marked this conversation as resolved.

$result = $this->do_execute( $input );
if ( is_wp_error( $result ) ) {
Expand All @@ -811,12 +829,14 @@ public function execute( $input = null ) {
* Fires immediately after an ability finished executing.
*
* @since 6.9.0
* @since 7.1.0 Added the `$ability` parameter.
*
* @param string $ability_name The name of the ability.
* @param mixed $input The input data for the ability.
* @param mixed $result The result of the ability execution.
* @param string $ability_name The name of the ability.
* @param mixed $input The input data for the ability.
* @param mixed $result The result of the ability execution.
* @param WP_Ability $ability The ability instance.
*/
do_action( 'wp_after_execute_ability', $this->name, $input, $result );
do_action( 'wp_after_execute_ability', $this->name, $input, $result, $this );

return $result;
}
Expand Down
137 changes: 129 additions & 8 deletions tests/phpunit/tests/abilities-api/wpAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,7 @@ public function test_check_permissions_catches_callback_exception() {
public function test_before_execute_ability_action() {
$action_ability_name = null;
$action_input = null;
$action_ability = null;

$args = array_merge(
self::$test_ability_properties,
Expand All @@ -570,19 +571,21 @@ public function test_before_execute_ability_action() {

add_action(
'wp_before_execute_ability',
static function ( $ability_name, $input ) use ( &$action_ability_name, &$action_input ) {
static function ( $ability_name, $input, $ability ) use ( &$action_ability_name, &$action_input, &$action_ability ) {
$action_ability_name = $ability_name;
$action_input = $input;
$action_ability = $ability;
},
10,
2
3
);

$ability = new WP_Ability( self::$test_ability_name, $args );
$result = $ability->execute( 5 );

$this->assertSame( self::$test_ability_name, $action_ability_name, 'Action should receive correct ability name' );
$this->assertSame( 5, $action_input, 'Action should receive correct input' );
$this->assertSame( $ability, $action_ability, 'Action should receive the ability instance' );
$this->assertSame( 10, $result, 'Ability should execute correctly' );
}

Expand All @@ -594,6 +597,7 @@ static function ( $ability_name, $input ) use ( &$action_ability_name, &$action_
public function test_before_execute_ability_action_no_input() {
$action_ability_name = null;
$action_input = null;
$action_ability = null;

$args = array_merge(
self::$test_ability_properties,
Expand All @@ -606,19 +610,21 @@ public function test_before_execute_ability_action_no_input() {

add_action(
'wp_before_execute_ability',
static function ( $ability_name, $input ) use ( &$action_ability_name, &$action_input ) {
static function ( $ability_name, $input, $ability ) use ( &$action_ability_name, &$action_input, &$action_ability ) {
$action_ability_name = $ability_name;
$action_input = $input;
$action_ability = $ability;
},
10,
2
3
);

$ability = new WP_Ability( self::$test_ability_name, $args );
$result = $ability->execute();

$this->assertSame( self::$test_ability_name, $action_ability_name, 'Action should receive correct ability name' );
$this->assertNull( $action_input, 'Action should receive null input when no input provided' );
$this->assertSame( $ability, $action_ability, 'Action should receive the ability instance' );
$this->assertSame( 42, $result, 'Ability should execute correctly' );
}

Expand All @@ -631,6 +637,7 @@ public function test_after_execute_ability_action() {
$action_ability_name = null;
$action_input = null;
$action_result = null;
$action_ability = null;

$args = array_merge(
self::$test_ability_properties,
Expand All @@ -648,13 +655,14 @@ public function test_after_execute_ability_action() {

add_action(
'wp_after_execute_ability',
static function ( $ability_name, $input, $result ) use ( &$action_ability_name, &$action_input, &$action_result ) {
static function ( $ability_name, $input, $result, $ability ) use ( &$action_ability_name, &$action_input, &$action_result, &$action_ability ) {
$action_ability_name = $ability_name;
$action_input = $input;
$action_result = $result;
$action_ability = $ability;
},
10,
3
4
);

$ability = new WP_Ability( self::$test_ability_name, $args );
Expand All @@ -663,6 +671,7 @@ static function ( $ability_name, $input, $result ) use ( &$action_ability_name,
$this->assertSame( self::$test_ability_name, $action_ability_name, 'Action should receive correct ability name' );
$this->assertSame( 7, $action_input, 'Action should receive correct input' );
$this->assertSame( 21, $action_result, 'Action should receive correct result' );
$this->assertSame( $ability, $action_ability, 'Action should receive the ability instance' );
$this->assertSame( 21, $result, 'Ability should execute correctly' );
}

Expand All @@ -675,6 +684,7 @@ public function test_after_execute_ability_action_no_input() {
$action_ability_name = null;
$action_input = null;
$action_result = null;
$action_ability = null;

$args = array_merge(
self::$test_ability_properties,
Expand All @@ -688,13 +698,14 @@ public function test_after_execute_ability_action_no_input() {

add_action(
'wp_after_execute_ability',
static function ( $ability_name, $input, $result ) use ( &$action_ability_name, &$action_input, &$action_result ) {
static function ( $ability_name, $input, $result, $ability ) use ( &$action_ability_name, &$action_input, &$action_result, &$action_ability ) {
$action_ability_name = $ability_name;
$action_input = $input;
$action_result = $result;
$action_ability = $ability;
},
10,
3
4
);

$ability = new WP_Ability( self::$test_ability_name, $args );
Expand All @@ -703,6 +714,7 @@ static function ( $ability_name, $input, $result ) use ( &$action_ability_name,
$this->assertSame( self::$test_ability_name, $action_ability_name, 'Action should receive correct ability name' );
$this->assertNull( $action_input, 'Action should receive null input when no input provided' );
$this->assertSame( 'test-result', $action_result, 'Action should receive correct result' );
$this->assertSame( $ability, $action_ability, 'Action should receive the ability instance' );
$this->assertSame( 'test-result', $result, 'Ability should execute correctly' );
}

Expand Down Expand Up @@ -1696,4 +1708,113 @@ static function () {
$this->assertInstanceOf( WP_Error::class, $result );
$this->assertSame( 'custom_output_error', $result->get_error_code() );
}

/**
* Tests that wp_ability_invoked action fires with correct parameters and raw input before normalization.
*
* @ticket 65248
*/
public function test_ability_invoked_action_fires_with_correct_params() {
$args = array_merge(
self::$test_ability_properties,
array(
'input_schema' => array(
'type' => 'integer',
'description' => 'Test input parameter.',
'default' => 42,
),
'execute_callback' => static function ( int $input ): int {
return $input;
},
)
);

$action = new MockAction();
add_action( 'wp_ability_invoked', array( $action, 'action' ), 10, 3 );

$ability = new WP_Ability( self::$test_ability_name, $args );
$ability->execute();

$action_args = $action->get_args();
$this->assertSame( self::$test_ability_name, $action_args[0][0], 'Action should receive correct ability name.' );
$this->assertNull( $action_args[0][1], 'Action should receive raw null input, not the schema default.' );
$this->assertSame( $ability, $action_args[0][2], 'Action should receive the ability instance.' );
}

/**
* Tests that wp_ability_invoked action fires when execution is short-circuited.
*
* @ticket 65248
*/
public function test_ability_invoked_action_fires_on_pre_execute_short_circuit() {
$action = new MockAction();
add_action( 'wp_ability_invoked', array( $action, 'action' ) );

add_filter(
'wp_pre_execute_ability',
static function () {
return 'short-circuited';
}
);

$ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties );
$ability->execute();

$this->assertSame( 1, $action->get_call_count(), 'wp_ability_invoked should fire before a pre-execute short-circuit.' );
}

/**
* Tests that wp_ability_invoked action fires on permission failure.
*
* @ticket 65248
*/
public function test_ability_invoked_action_fires_on_permission_failure() {
$action = new MockAction();
add_action( 'wp_ability_invoked', array( $action, 'action' ) );

$ability = new WP_Ability(
self::$test_ability_name,
array_merge(
self::$test_ability_properties,
array(
'permission_callback' => static function (): bool {
return false;
},
)
)
);
$ability->execute();

$this->assertSame( 1, $action->get_call_count(), 'wp_ability_invoked should fire before permission failure.' );
}

/**
* Tests that wp_ability_invoked action fires on input validation failure.
*
* @ticket 65248
*/
public function test_ability_invoked_action_fires_on_validation_failure() {
$action = new MockAction();
add_action( 'wp_ability_invoked', array( $action, 'action' ) );

$ability = new WP_Ability(
self::$test_ability_name,
array_merge(
self::$test_ability_properties,
array(
'input_schema' => array(
'type' => 'integer',
'description' => 'Int input.',
'required' => true,
),
'execute_callback' => static function ( int $input ): int {
return $input;
},
)
)
);
$ability->execute( 'not_an_integer' );

$this->assertSame( 1, $action->get_call_count(), 'wp_ability_invoked should fire before input validation failure.' );
}
}
Loading