Skip to content

Commit 1f35f01

Browse files
committed
Abilities API: Add wp_ability_invoked action
Introduces a new `wp_ability_invoked` action that fires at the start of `WP_Ability::execute()`, before input normalization, validation, or permission checks. This gives observers a reliable entry point for every invocation regardless of outcome (short-circuit, validation failure, permission denial, or successful execution). Also extends the existing `wp_before_execute_ability` and `wp_after_execute_ability` actions with a new `$ability` parameter exposing the `WP_Ability` instance. Follow-up for #64989. Props sukhendu2002, peterwilsoncc, gziolo. Fixes #65248. git-svn-id: https://develop.svn.wordpress.org/trunk@62418 602fd350-edb4-49c9-b593-d223f7449a82
1 parent b18bf6a commit 1f35f01

2 files changed

Lines changed: 156 additions & 15 deletions

File tree

src/wp-includes/abilities-api/class-wp-ability.php

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -725,12 +725,28 @@ protected function validate_output( $output ) {
725725
* Before returning the return value, it also validates the output.
726726
*
727727
* @since 6.9.0
728+
* @since 7.1.0 Added the `wp_ability_invoked` action.
728729
* @since 7.1.0 Added the `wp_pre_execute_ability` filter.
729730
*
730731
* @param mixed $input Optional. The input data for the ability. Default `null`.
731732
* @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
732733
*/
733734
public function execute( $input = null ) {
735+
/**
736+
* Fires when an ability is invoked, before any processing takes place.
737+
*
738+
* This action fires for every call regardless of outcome (validation failure,
739+
* permission denial, short-circuit, or successful execution), and before input
740+
* normalization so the raw input is captured as-is.
741+
*
742+
* @since 7.1.0
743+
*
744+
* @param string $ability_name The name of the ability.
745+
* @param mixed $input The raw input data for the ability, before normalization.
746+
* @param WP_Ability $ability The ability instance.
747+
*/
748+
do_action( 'wp_ability_invoked', $this->name, $input, $this );
749+
734750
/**
735751
* Filters whether to short-circuit ability execution.
736752
*
@@ -791,11 +807,13 @@ public function execute( $input = null ) {
791807
* Fires before an ability gets executed, after input validation and permissions check.
792808
*
793809
* @since 6.9.0
810+
* @since 7.1.0 Added the `$ability` parameter.
794811
*
795-
* @param string $ability_name The name of the ability.
796-
* @param mixed $input The input data for the ability.
812+
* @param string $ability_name The name of the ability.
813+
* @param mixed $input The input data for the ability.
814+
* @param WP_Ability $ability The ability instance.
797815
*/
798-
do_action( 'wp_before_execute_ability', $this->name, $input );
816+
do_action( 'wp_before_execute_ability', $this->name, $input, $this );
799817

800818
$result = $this->do_execute( $input );
801819
if ( is_wp_error( $result ) ) {
@@ -811,12 +829,14 @@ public function execute( $input = null ) {
811829
* Fires immediately after an ability finished executing.
812830
*
813831
* @since 6.9.0
832+
* @since 7.1.0 Added the `$ability` parameter.
814833
*
815-
* @param string $ability_name The name of the ability.
816-
* @param mixed $input The input data for the ability.
817-
* @param mixed $result The result of the ability execution.
834+
* @param string $ability_name The name of the ability.
835+
* @param mixed $input The input data for the ability.
836+
* @param mixed $result The result of the ability execution.
837+
* @param WP_Ability $ability The ability instance.
818838
*/
819-
do_action( 'wp_after_execute_ability', $this->name, $input, $result );
839+
do_action( 'wp_after_execute_ability', $this->name, $input, $result, $this );
820840

821841
return $result;
822842
}

tests/phpunit/tests/abilities-api/wpAbility.php

Lines changed: 129 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ public function test_check_permissions_catches_callback_exception() {
553553
public function test_before_execute_ability_action() {
554554
$action_ability_name = null;
555555
$action_input = null;
556+
$action_ability = null;
556557

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

571572
add_action(
572573
'wp_before_execute_ability',
573-
static function ( $ability_name, $input ) use ( &$action_ability_name, &$action_input ) {
574+
static function ( $ability_name, $input, $ability ) use ( &$action_ability_name, &$action_input, &$action_ability ) {
574575
$action_ability_name = $ability_name;
575576
$action_input = $input;
577+
$action_ability = $ability;
576578
},
577579
10,
578-
2
580+
3
579581
);
580582

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

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

@@ -594,6 +597,7 @@ static function ( $ability_name, $input ) use ( &$action_ability_name, &$action_
594597
public function test_before_execute_ability_action_no_input() {
595598
$action_ability_name = null;
596599
$action_input = null;
600+
$action_ability = null;
597601

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

607611
add_action(
608612
'wp_before_execute_ability',
609-
static function ( $ability_name, $input ) use ( &$action_ability_name, &$action_input ) {
613+
static function ( $ability_name, $input, $ability ) use ( &$action_ability_name, &$action_input, &$action_ability ) {
610614
$action_ability_name = $ability_name;
611615
$action_input = $input;
616+
$action_ability = $ability;
612617
},
613618
10,
614-
2
619+
3
615620
);
616621

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

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

@@ -631,6 +637,7 @@ public function test_after_execute_ability_action() {
631637
$action_ability_name = null;
632638
$action_input = null;
633639
$action_result = null;
640+
$action_ability = null;
634641

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

649656
add_action(
650657
'wp_after_execute_ability',
651-
static function ( $ability_name, $input, $result ) use ( &$action_ability_name, &$action_input, &$action_result ) {
658+
static function ( $ability_name, $input, $result, $ability ) use ( &$action_ability_name, &$action_input, &$action_result, &$action_ability ) {
652659
$action_ability_name = $ability_name;
653660
$action_input = $input;
654661
$action_result = $result;
662+
$action_ability = $ability;
655663
},
656664
10,
657-
3
665+
4
658666
);
659667

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

@@ -675,6 +684,7 @@ public function test_after_execute_ability_action_no_input() {
675684
$action_ability_name = null;
676685
$action_input = null;
677686
$action_result = null;
687+
$action_ability = null;
678688

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

689699
add_action(
690700
'wp_after_execute_ability',
691-
static function ( $ability_name, $input, $result ) use ( &$action_ability_name, &$action_input, &$action_result ) {
701+
static function ( $ability_name, $input, $result, $ability ) use ( &$action_ability_name, &$action_input, &$action_result, &$action_ability ) {
692702
$action_ability_name = $ability_name;
693703
$action_input = $input;
694704
$action_result = $result;
705+
$action_ability = $ability;
695706
},
696707
10,
697-
3
708+
4
698709
);
699710

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

@@ -1696,4 +1708,113 @@ static function () {
16961708
$this->assertInstanceOf( WP_Error::class, $result );
16971709
$this->assertSame( 'custom_output_error', $result->get_error_code() );
16981710
}
1711+
1712+
/**
1713+
* Tests that wp_ability_invoked action fires with correct parameters and raw input before normalization.
1714+
*
1715+
* @ticket 65248
1716+
*/
1717+
public function test_ability_invoked_action_fires_with_correct_params() {
1718+
$args = array_merge(
1719+
self::$test_ability_properties,
1720+
array(
1721+
'input_schema' => array(
1722+
'type' => 'integer',
1723+
'description' => 'Test input parameter.',
1724+
'default' => 42,
1725+
),
1726+
'execute_callback' => static function ( int $input ): int {
1727+
return $input;
1728+
},
1729+
)
1730+
);
1731+
1732+
$action = new MockAction();
1733+
add_action( 'wp_ability_invoked', array( $action, 'action' ), 10, 3 );
1734+
1735+
$ability = new WP_Ability( self::$test_ability_name, $args );
1736+
$ability->execute();
1737+
1738+
$action_args = $action->get_args();
1739+
$this->assertSame( self::$test_ability_name, $action_args[0][0], 'Action should receive correct ability name.' );
1740+
$this->assertNull( $action_args[0][1], 'Action should receive raw null input, not the schema default.' );
1741+
$this->assertSame( $ability, $action_args[0][2], 'Action should receive the ability instance.' );
1742+
}
1743+
1744+
/**
1745+
* Tests that wp_ability_invoked action fires when execution is short-circuited.
1746+
*
1747+
* @ticket 65248
1748+
*/
1749+
public function test_ability_invoked_action_fires_on_pre_execute_short_circuit() {
1750+
$action = new MockAction();
1751+
add_action( 'wp_ability_invoked', array( $action, 'action' ) );
1752+
1753+
add_filter(
1754+
'wp_pre_execute_ability',
1755+
static function () {
1756+
return 'short-circuited';
1757+
}
1758+
);
1759+
1760+
$ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties );
1761+
$ability->execute();
1762+
1763+
$this->assertSame( 1, $action->get_call_count(), 'wp_ability_invoked should fire before a pre-execute short-circuit.' );
1764+
}
1765+
1766+
/**
1767+
* Tests that wp_ability_invoked action fires on permission failure.
1768+
*
1769+
* @ticket 65248
1770+
*/
1771+
public function test_ability_invoked_action_fires_on_permission_failure() {
1772+
$action = new MockAction();
1773+
add_action( 'wp_ability_invoked', array( $action, 'action' ) );
1774+
1775+
$ability = new WP_Ability(
1776+
self::$test_ability_name,
1777+
array_merge(
1778+
self::$test_ability_properties,
1779+
array(
1780+
'permission_callback' => static function (): bool {
1781+
return false;
1782+
},
1783+
)
1784+
)
1785+
);
1786+
$ability->execute();
1787+
1788+
$this->assertSame( 1, $action->get_call_count(), 'wp_ability_invoked should fire before permission failure.' );
1789+
}
1790+
1791+
/**
1792+
* Tests that wp_ability_invoked action fires on input validation failure.
1793+
*
1794+
* @ticket 65248
1795+
*/
1796+
public function test_ability_invoked_action_fires_on_validation_failure() {
1797+
$action = new MockAction();
1798+
add_action( 'wp_ability_invoked', array( $action, 'action' ) );
1799+
1800+
$ability = new WP_Ability(
1801+
self::$test_ability_name,
1802+
array_merge(
1803+
self::$test_ability_properties,
1804+
array(
1805+
'input_schema' => array(
1806+
'type' => 'integer',
1807+
'description' => 'Int input.',
1808+
'required' => true,
1809+
),
1810+
'execute_callback' => static function ( int $input ): int {
1811+
return $input;
1812+
},
1813+
)
1814+
)
1815+
);
1816+
$ability->execute( 'not_an_integer' );
1817+
1818+
$this->assertSame( 1, $action->get_call_count(), 'wp_ability_invoked should fire before input validation failure.' );
1819+
}
16991820
}

0 commit comments

Comments
 (0)