From c049ddca7d862fbd2b4a372c34ef301ac2176122 Mon Sep 17 00:00:00 2001 From: artpi Date: Fri, 12 Dec 2025 14:49:24 +0100 Subject: [PATCH 1/6] Enhance WP_Ability validation for execute_callback This commit updates the WP_Ability class to validate the execute_callback only when the ability_class parameter is not overridden during instantiation. --- .../abilities-api/class-wp-ability.php | 3 +- .../abilities-api/wpAbilitiesRegistry.php | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/abilities-api/class-wp-ability.php b/src/wp-includes/abilities-api/class-wp-ability.php index d116080c1ccdc..2a56773f942c7 100644 --- a/src/wp-includes/abilities-api/class-wp-ability.php +++ b/src/wp-includes/abilities-api/class-wp-ability.php @@ -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( __( 'The ability properties must contain a valid `execute_callback` function.' ) ); diff --git a/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php b/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php index 319063196d0c5..fbe066a7bbeca 100644 --- a/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php +++ b/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php @@ -257,6 +257,30 @@ public function test_register_incorrect_execute_callback_type() { $this->assertNull( $result ); } + /** + * Should allow ability registration with custom ability_class that overrides do_execute. + * + * @ticket 64098 + * + * @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. * @@ -651,3 +675,32 @@ static function ( $args, $name ) { $this->assertNotSame( $filtered_ability->get_label(), $unfiltered_ability->get_label(), 'The filter incorrectly modified the args for an ability it should not have.' ); } } + +/** + * 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; + } +} From 64c4ba811d9c62e06a107946291ccdee60998b73 Mon Sep 17 00:00:00 2001 From: artpi Date: Fri, 12 Dec 2025 14:55:45 +0100 Subject: [PATCH 2/6] Ticket number update --- tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php b/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php index fbe066a7bbeca..45435f4892f59 100644 --- a/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php +++ b/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php @@ -260,7 +260,7 @@ public function test_register_incorrect_execute_callback_type() { /** * Should allow ability registration with custom ability_class that overrides do_execute. * - * @ticket 64098 + * @ticket 64407 * * @covers WP_Abilities_Registry::register * @covers WP_Ability::prepare_properties From 8c7da5d7d5d94d14720a665fbe1ed16846a36d66 Mon Sep 17 00:00:00 2001 From: artpi Date: Fri, 12 Dec 2025 14:59:11 +0100 Subject: [PATCH 3/6] Lintfix --- test.php | 31 +++++++++++++++++++ .../abilities-api/wpAbilitiesRegistry.php | 7 ++++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 test.php diff --git a/test.php b/test.php new file mode 100644 index 0000000000000..e6ecbed1dab29 --- /dev/null +++ b/test.php @@ -0,0 +1,31 @@ + true ); + } +} + +add_action( 'wp_abilities_api_init', function() { + wp_register_ability( + 'test/custom-class-ability', + array( + 'label' => 'Test Custom Ability', + 'description' => 'Test ability with custom class.', + 'category' => 'site', + 'ability_class' => My_Test_Ability::class, + 'execute_callback' => function() { + // We have to provide an execute callback that will never be called because of checks in wp_ability. + return null; + }, + 'permission_callback' => function() { + return true; + }, + ) + ); +} ); + +wp_get_ability('test/custom-class-ability')->execute(); diff --git a/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php b/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php index 45435f4892f59..c220281aa16e0 100644 --- a/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php +++ b/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php @@ -277,7 +277,12 @@ public function test_register_with_custom_ability_class_without_execute_callback $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 ) ); + $execute_result = $result->execute( + array( + 'a' => 5, + 'b' => 3, + ) + ); $this->assertSame( 15, $execute_result, 'Custom do_execute should multiply instead of add.' ); } From af85b49d43ac4ecb0d8d42253cbf3187296e0cb3 Mon Sep 17 00:00:00 2001 From: artpi Date: Fri, 12 Dec 2025 14:59:49 +0100 Subject: [PATCH 4/6] This file should not be here --- test.php | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 test.php diff --git a/test.php b/test.php deleted file mode 100644 index e6ecbed1dab29..0000000000000 --- a/test.php +++ /dev/null @@ -1,31 +0,0 @@ - true ); - } -} - -add_action( 'wp_abilities_api_init', function() { - wp_register_ability( - 'test/custom-class-ability', - array( - 'label' => 'Test Custom Ability', - 'description' => 'Test ability with custom class.', - 'category' => 'site', - 'ability_class' => My_Test_Ability::class, - 'execute_callback' => function() { - // We have to provide an execute callback that will never be called because of checks in wp_ability. - return null; - }, - 'permission_callback' => function() { - return true; - }, - ) - ); -} ); - -wp_get_ability('test/custom-class-ability')->execute(); From d8a71e5e8b093e359fde031d9e1ad1ff7c2eb9c7 Mon Sep 17 00:00:00 2001 From: artpi Date: Fri, 12 Dec 2025 15:06:42 +0100 Subject: [PATCH 5/6] Move Tests_Custom_Ability_Class to a seperate file --- .../class-tests-custom-ability-class.php | 29 +++++++++++++++++ .../abilities-api/wpAbilitiesRegistry.php | 31 ++----------------- 2 files changed, 31 insertions(+), 29 deletions(-) create mode 100644 tests/phpunit/includes/class-tests-custom-ability-class.php diff --git a/tests/phpunit/includes/class-tests-custom-ability-class.php b/tests/phpunit/includes/class-tests-custom-ability-class.php new file mode 100644 index 0000000000000..6e600e5c013e1 --- /dev/null +++ b/tests/phpunit/includes/class-tests-custom-ability-class.php @@ -0,0 +1,29 @@ +registry = new WP_Abilities_Registry(); @@ -680,32 +682,3 @@ static function ( $args, $name ) { $this->assertNotSame( $filtered_ability->get_label(), $unfiltered_ability->get_label(), 'The filter incorrectly modified the args for an ability it should not have.' ); } } - -/** - * 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; - } -} From 5f091d012fce00490b357e89ab330b1b2e74d161 Mon Sep 17 00:00:00 2001 From: artpi Date: Mon, 15 Dec 2025 12:22:35 +0100 Subject: [PATCH 6/6] Make the permission callback optional too for ability class --- src/wp-includes/abilities-api/class-wp-ability.php | 3 ++- tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/abilities-api/class-wp-ability.php b/src/wp-includes/abilities-api/class-wp-ability.php index 2a56773f942c7..3af7f7fc9844e 100644 --- a/src/wp-includes/abilities-api/class-wp-ability.php +++ b/src/wp-includes/abilities-api/class-wp-ability.php @@ -284,7 +284,8 @@ protected function prepare_properties( array $args ): array { ); } - if ( empty( $args['permission_callback'] ) || ! is_callable( $args['permission_callback'] ) ) { + // If we are not overriding `ability_class` parameter during instantiation, then we need to validate the permission_callback. + if ( get_class( $this ) === self::class && ( empty( $args['permission_callback'] ) || ! is_callable( $args['permission_callback'] ) ) ) { throw new InvalidArgumentException( __( 'The ability properties must provide a valid `permission_callback` function.' ) ); diff --git a/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php b/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php index 1f697900dc490..32479d69e2f8c 100644 --- a/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php +++ b/tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php @@ -268,8 +268,9 @@ public function test_register_incorrect_execute_callback_type() { * @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. + // Remove execute_callback and permission_callback since the custom class provides its own implementation. unset( self::$test_ability_args['execute_callback'] ); + unset( self::$test_ability_args['permission_callback'] ); self::$test_ability_args['ability_class'] = 'Tests_Custom_Ability_Class';