Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
3 changes: 2 additions & 1 deletion src/wp-includes/abilities-api/class-wp-ability.php
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,8 @@ protected function prepare_properties( array $args ): array {
);
}

if ( empty( $args['execute_callback'] ) || ! is_callable( $args['execute_callback'] ) ) {
// If we are not overriding `ability_class` parameter during instantiation, then we need to validate the execute_callback.
if ( get_class( $this ) === self::class && ( empty( $args['execute_callback'] ) || ! is_callable( $args['execute_callback'] ) ) ) {
throw new InvalidArgumentException(
Copy link
Member

@jorgefilipecosta jorgefilipecosta Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also apply the same logic for permission_callback, as we still have this condition:

		if ( empty( $args['permission_callback'] ) || ! is_callable( $args['permission_callback'] ) ) {
			throw new InvalidArgumentException(
				__( 'The ability properties must provide a valid `permission_callback` function.' )
			);
		}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!
5f091d0

__( 'The ability properties must contain a valid `execute_callback` function.' )
);
Expand Down
29 changes: 29 additions & 0 deletions tests/phpunit/includes/class-tests-custom-ability-class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
/**
* Test custom ability class that extends WP_Ability.
*
* This class overrides do_execute() and check_permissions() directly,
* allowing registration without execute_callback or permission_callback.
*/
class Tests_Custom_Ability_Class extends WP_Ability {

/**
* Custom execute implementation that multiplies instead of adds.
*
* @param mixed $input The input data.
* @return int The result of multiplying a and b.
*/
protected function do_execute( $input = null ) {
return $input['a'] * $input['b'];
}

/**
* Custom permission check that always returns true.
*
* @param mixed $input The input data.
* @return bool Always true.
*/
public function check_permissions( $input = null ) {
return true;
}
}
31 changes: 31 additions & 0 deletions tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class Tests_Abilities_API_WpAbilitiesRegistry extends WP_UnitTestCase {
* Set up each test method.
*/
public function set_up(): void {
require_once DIR_TESTDATA . '/../includes/class-tests-custom-ability-class.php';

parent::set_up();

$this->registry = new WP_Abilities_Registry();
Expand Down Expand Up @@ -257,6 +259,35 @@ public function test_register_incorrect_execute_callback_type() {
$this->assertNull( $result );
}

/**
* Should allow ability registration with custom ability_class that overrides do_execute.
*
* @ticket 64407
*
* @covers WP_Abilities_Registry::register
* @covers WP_Ability::prepare_properties
*/
public function test_register_with_custom_ability_class_without_execute_callback() {
// Remove execute_callback since the custom class provides its own implementation.
unset( self::$test_ability_args['execute_callback'] );

self::$test_ability_args['ability_class'] = 'Tests_Custom_Ability_Class';

$result = $this->registry->register( self::$test_ability_name, self::$test_ability_args );

$this->assertInstanceOf( WP_Ability::class, $result, 'Should return a WP_Ability instance.' );
$this->assertInstanceOf( Tests_Custom_Ability_Class::class, $result, 'Should return an instance of the custom class.' );

// Verify the custom execute method works.
$execute_result = $result->execute(
array(
'a' => 5,
'b' => 3,
)
);
$this->assertSame( 15, $execute_result, 'Custom do_execute should multiply instead of add.' );
}

/**
* Should reject ability registration without an execute callback.
*
Expand Down
Loading