From a3637ccbcbe59fd797b25c39383706d720087d81 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Sun, 17 Aug 2025 00:36:47 +0300 Subject: [PATCH 1/4] dev!: return `\WP_Error` from `WP_Ability` methods --- includes/abilities-api/class-wp-ability.php | 95 ++++------ ...class-wp-rest-abilities-run-controller.php | 14 +- .../unit/abilities-api/wpRegisterAbility.php | 96 +++++----- .../rest-api/wpRestAbilitiesRunController.php | 168 +++++++++--------- 4 files changed, 174 insertions(+), 199 deletions(-) diff --git a/includes/abilities-api/class-wp-ability.php b/includes/abilities-api/class-wp-ability.php index f5ee8fbb..1e30adea 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -180,32 +180,17 @@ public function get_meta(): array { * @since 0.1.0 * * @param array $input Optional. The input data to validate. - * @return bool Returns true if valid, false if validation fails. + * @return true|\WP_Error Returns true if valid or the WP_Error object if validation fails. */ - protected function validate_input( array $input = array() ): bool { + protected function validate_input( array $input = array() ) { $input_schema = $this->get_input_schema(); if ( empty( $input_schema ) ) { return true; } $valid_input = rest_validate_value_from_schema( $input, $input_schema ); - if ( is_wp_error( $valid_input ) ) { - _doing_it_wrong( - __METHOD__, - esc_html( - sprintf( - /* translators: %1$s ability name, %2$s error message. */ - __( 'Invalid input provided for ability "%1$s": %2$s.' ), - $this->name, - $valid_input->get_error_message() - ) - ), - '0.1.0' - ); - return false; - } - return true; + return is_wp_error( $valid_input ) ? $valid_input : true; } /** @@ -216,11 +201,12 @@ protected function validate_input( array $input = array() ): bool { * @since 0.1.0 * * @param array $input Optional. The input data for permission checking. - * @return bool Whether the ability has the necessary permission. + * @return true|\WP_Error Whether the ability has the necessary permission. */ - public function has_permission( array $input = array() ): bool { - if ( ! $this->validate_input( $input ) ) { - return false; + public function has_permission( array $input = array() ) { + $is_valid = $this->validate_input( $input ); + if ( is_wp_error( $is_valid ) ) { + return $is_valid; } if ( ! is_callable( $this->permission_callback ) ) { @@ -240,16 +226,13 @@ public function has_permission( array $input = array() ): bool { */ protected function do_execute( array $input ) { if ( ! is_callable( $this->execute_callback ) ) { - _doing_it_wrong( - __METHOD__, - esc_html( - /* translators: %s ability name. */ - sprintf( __( 'Ability "%s" does not have a valid execute callback.' ), $this->name ) - ), - '0.1.0' + return new \WP_Error( + 'ability_invalid_execute_callback', + /* translators: %s ability name. */ + sprintf( __( 'Ability "%s" does not have a valid execute callback.' ), $this->name ) ); - return null; } + return call_user_func( $this->execute_callback, $input ); } @@ -259,32 +242,17 @@ protected function do_execute( array $input ) { * @since 0.1.0 * * @param mixed $output The output data to validate. - * @return bool Returns true if valid, false if validation fails. + * @return true|\WP_Error Returns true if valid, false if validation fails. */ - protected function validate_output( $output ): bool { + protected function validate_output( $output ) { $output_schema = $this->get_output_schema(); if ( empty( $output_schema ) ) { return true; } $valid_output = rest_validate_value_from_schema( $output, $output_schema ); - if ( is_wp_error( $valid_output ) ) { - _doing_it_wrong( - __METHOD__, - esc_html( - sprintf( - /* translators: %1$s ability name, %2$s error message. */ - __( 'Invalid output provided for ability "%1$s": %2$s.' ), - $this->name, - $valid_output->get_error_message() - ) - ), - '0.1.0' - ); - return false; - } - return true; + return is_wp_error( $valid_output ) ? $valid_output : true; } /** @@ -297,16 +265,23 @@ protected function validate_output( $output ): bool { * @return mixed|\WP_Error The result of the ability execution, or WP_Error on failure. */ public function execute( array $input = array() ) { - if ( ! $this->has_permission( $input ) ) { - _doing_it_wrong( - __METHOD__, - esc_html( - /* translators: %s ability name. */ - sprintf( __( 'Ability "%s" does not have necessary permission.' ), $this->name ) - ), - '0.1.0' + $has_permissions = $this->has_permission( $input ); + + if ( true !== $has_permissions ) { + if ( is_wp_error( $has_permissions ) ) { + // Don't leak the error to someone without the correct perms. + _doing_it_wrong( + __METHOD__, + esc_html( $has_permissions->get_error_message() ), + '0.1.0' + ); + } + + return new \WP_Error( + 'ability_invalid_permissions', + /* translators: %s ability name. */ + sprintf( __( 'Ability "%s" does not have necessary permission.' ), $this->name ) ); - return null; } $result = $this->do_execute( $input ); @@ -314,11 +289,9 @@ public function execute( array $input = array() ) { return $result; } - if ( ! $this->validate_output( $result ) ) { - return null; - } + $is_valid = $this->validate_output( $result ); - return $result; + return is_wp_error( $is_valid ) ? $is_valid : $result; } /** diff --git a/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php b/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php index 02c67345..18add4e3 100644 --- a/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php +++ b/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php @@ -146,19 +146,7 @@ public function run_ability( $request ) { $result = $ability->execute( $input ); if ( is_wp_error( $result ) ) { - return new \WP_Error( - 'rest_ability_execution_failed', - $result->get_error_message(), - array( 'status' => 500 ) - ); - } - - if ( is_null( $result ) ) { - return new \WP_Error( - 'rest_ability_execution_failed', - __( 'Ability execution failed. Please check permissions and input parameters.', 'abilities-api' ), - array( 'status' => 500 ) - ); + return $result; } $output_validation = $this->validate_output( $ability, $result ); diff --git a/tests/unit/abilities-api/wpRegisterAbility.php b/tests/unit/abilities-api/wpRegisterAbility.php index e3709a12..591362fc 100644 --- a/tests/unit/abilities-api/wpRegisterAbility.php +++ b/tests/unit/abilities-api/wpRegisterAbility.php @@ -43,10 +43,10 @@ public function set_up(): void { 'description' => 'The result of adding the two numbers.', 'required' => true, ), - 'execute_callback' => function ( array $input ): int { + 'execute_callback' => static function ( array $input ): int { return $input['a'] + $input['b']; }, - 'permission_callback' => function (): bool { + 'permission_callback' => static function (): bool { return true; }, 'meta' => array( @@ -60,9 +60,11 @@ public function set_up(): void { */ public function tear_down(): void { foreach ( wp_get_abilities() as $ability ) { - if ( str_starts_with( $ability->get_name(), 'test/' ) ) { - wp_unregister_ability( $ability->get_name() ); + if ( ! str_starts_with( $ability->get_name(), 'test/' ) ) { + continue; } + + wp_unregister_ability( $ability->get_name() ); } parent::tear_down(); @@ -141,13 +143,11 @@ public function test_register_valid_ability(): void { /** * Tests executing an ability with no permissions. - * - * @expectedIncorrectUsage WP_Ability::execute */ public function test_register_ability_no_permissions(): void { do_action( 'abilities_api_init' ); - self::$test_ability_properties['permission_callback'] = function (): bool { + self::$test_ability_properties['permission_callback'] = static function (): bool { return false; }; $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); @@ -160,80 +160,90 @@ public function test_register_ability_no_permissions(): void { ) ) ); - $this->assertNull( - $result->execute( - array( - 'a' => 2, - 'b' => 3, - ) + + $actual = $result->execute( + array( + 'a' => 2, + 'b' => 3, ) ); + $this->assertWPError( + $actual, + 'Execution should fail due to no permissions' + ); + $this->assertEquals( 'ability_invalid_permissions', $actual->get_error_code() ); } /** * Tests executing an ability with input not matching schema. - * - * @expectedIncorrectUsage WP_Ability::validate_input - * @expectedIncorrectUsage WP_Ability::execute */ public function test_execute_ability_no_input_schema_match(): void { do_action( 'abilities_api_init' ); $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); - $this->assertNull( - $result->execute( - array( - 'a' => 2, - 'b' => 3, - 'unknown' => 1, - ) + $this->setExpectedIncorrectUsage( 'WP_Ability::execute' ); + + $actual = $result->execute( + array( + 'a' => 2, + 'b' => 3, + 'unknown' => 1, ) ); + + $this->assertWPError( + $actual, + 'Execution should fail due to input not matching schema' + ); + $this->assertEquals( 'ability_invalid_permissions', $actual->get_error_code() ); } /** * Tests executing an ability with output not matching schema. - * - * @expectedIncorrectUsage WP_Ability::validate_output */ public function test_execute_ability_no_output_schema_match(): void { do_action( 'abilities_api_init' ); - self::$test_ability_properties['execute_callback'] = function (): bool { + self::$test_ability_properties['execute_callback'] = static function (): bool { return true; }; $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); - $this->assertNull( - $result->execute( - array( - 'a' => 2, - 'b' => 3, - ) + $actual = $result->execute( + array( + 'a' => 2, + 'b' => 3, ) ); + $this->assertWPError( + $actual, + 'Execution should fail due to output not matching schema', + ); + $this->assertEquals( 'rest_invalid_type', $actual->get_error_code() ); } /** * Tests permission callback receiving input not matching schema. - * - * @expectedIncorrectUsage WP_Ability::validate_input */ public function test_permission_callback_no_input_schema_match(): void { do_action( 'abilities_api_init' ); $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); - $this->assertFalse( - $result->has_permission( - array( - 'a' => 2, - 'b' => 3, - 'unknown' => 1, - ) + $actual = $result->has_permission( + array( + 'a' => 2, + 'b' => 3, + 'unknown' => 1, ) ); + + $this->assertWPError( + $actual, + 'Permission check should fail due to input not matching schema' + ); + $this->assertEquals( 'rest_additional_properties_forbidden', $actual->get_error_code() ); } /** @@ -243,7 +253,7 @@ public function test_permission_callback_receives_input(): void { do_action( 'abilities_api_init' ); $received_input = null; - self::$test_ability_properties['permission_callback'] = function ( array $input ) use ( &$received_input ): bool { + self::$test_ability_properties['permission_callback'] = static function ( array $input ) use ( &$received_input ): bool { $received_input = $input; // Allow only if 'a' is greater than 'b' return $input['a'] > $input['b']; @@ -310,7 +320,7 @@ public function test_get_existing_ability() { $name = self::$test_ability_name; $properties = self::$test_ability_properties; - $callback = function ( $instance ) use ( $name, $properties ) { + $callback = static function ( $instance ) use ( $name, $properties ) { wp_register_ability( $name, $properties ); }; diff --git a/tests/unit/rest-api/wpRestAbilitiesRunController.php b/tests/unit/rest-api/wpRestAbilitiesRunController.php index cb1befdb..b6fd0412 100644 --- a/tests/unit/rest-api/wpRestAbilitiesRunController.php +++ b/tests/unit/rest-api/wpRestAbilitiesRunController.php @@ -10,7 +10,7 @@ class Tests_REST_API_WpRestAbilitiesRunController extends WP_UnitTestCase { /** * REST Server instance. * - * @var WP_REST_Server + * @var \WP_REST_Server */ protected $server; @@ -70,9 +70,11 @@ public function set_up(): void { */ public function tear_down(): void { foreach ( wp_get_abilities() as $ability ) { - if ( str_starts_with( $ability->get_name(), 'test/' ) ) { - wp_unregister_ability( $ability->get_name() ); + if ( ! str_starts_with( $ability->get_name(), 'test/' ) ) { + continue; } + + wp_unregister_ability( $ability->get_name() ); } global $wp_rest_server; @@ -109,10 +111,10 @@ private function register_test_abilities(): void { 'output_schema' => array( 'type' => 'number', ), - 'execute_callback' => function ( array $input ) { + 'execute_callback' => static function ( array $input ) { return $input['a'] + $input['b']; }, - 'permission_callback' => function ( array $input ) { + 'permission_callback' => static function ( array $input ) { return current_user_can( 'edit_posts' ); }, 'meta' => array( @@ -143,18 +145,18 @@ private function register_test_abilities(): void { 'login' => array( 'type' => 'string' ), ), ), - 'execute_callback' => function ( array $input ) { + 'execute_callback' => static function ( array $input ) { $user_id = $input['user_id'] ?? get_current_user_id(); $user = get_user_by( 'id', $user_id ); if ( ! $user ) { - return new WP_Error( 'user_not_found', 'User not found' ); + return new \WP_Error( 'user_not_found', 'User not found' ); } return array( 'id' => $user->ID, 'login' => $user->user_login, ); }, - 'permission_callback' => function ( array $input ) { + 'permission_callback' => static function ( array $input ) { return is_user_logged_in(); }, 'meta' => array( @@ -180,10 +182,10 @@ private function register_test_abilities(): void { 'output_schema' => array( 'type' => 'string', ), - 'execute_callback' => function ( array $input ) { + 'execute_callback' => static function ( array $input ) { return 'Success: ' . $input['data']; }, - 'permission_callback' => function ( array $input ) { + 'permission_callback' => static function ( array $input ) { // Only allow if secret matches return isset( $input['secret'] ) && 'valid_secret' === $input['secret']; }, @@ -199,7 +201,7 @@ private function register_test_abilities(): void { array( 'label' => 'Null Return', 'description' => 'Returns null', - 'execute_callback' => function () { + 'execute_callback' => static function () { return null; }, 'permission_callback' => '__return_true', @@ -215,8 +217,8 @@ private function register_test_abilities(): void { array( 'label' => 'Error Return', 'description' => 'Returns error', - 'execute_callback' => function () { - return new WP_Error( 'test_error', 'This is a test error' ); + 'execute_callback' => static function () { + return new \WP_Error( 'test_error', 'This is a test error' ); }, 'permission_callback' => '__return_true', 'meta' => array( @@ -234,7 +236,7 @@ private function register_test_abilities(): void { 'output_schema' => array( 'type' => 'number', ), - 'execute_callback' => function () { + 'execute_callback' => static function () { return 'not a number'; // Invalid - schema expects number }, 'permission_callback' => '__return_true', @@ -257,7 +259,7 @@ private function register_test_abilities(): void { 'param2' => array( 'type' => 'integer' ), ), ), - 'execute_callback' => function ( array $input ) { + 'execute_callback' => static function ( array $input ) { return $input; }, 'permission_callback' => '__return_true', @@ -320,7 +322,7 @@ public function test_tool_ability_requires_post(): void { array( 'label' => 'Open Tool', 'description' => 'Tool with no permission requirements', - 'execute_callback' => function () { + 'execute_callback' => static function () { return 'success'; }, 'permission_callback' => '__return_true', @@ -361,8 +363,6 @@ public function test_resource_ability_requires_get(): void { * Test output validation against schema. * Note: When output validation fails in WP_Ability::execute(), it returns null, * which causes the REST controller to return 'rest_ability_execution_failed'. - * - * @expectedIncorrectUsage WP_Ability::validate_output */ public function test_output_validation(): void { $request = new WP_REST_Request( 'POST', '/wp/v2/abilities/test/invalid-output/run' ); @@ -374,8 +374,8 @@ public function test_output_validation(): void { $this->assertEquals( 500, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'rest_ability_execution_failed', $data['code'] ); - $this->assertStringContainsString( 'Ability execution failed', $data['message'] ); + $this->assertEquals( 'rest_invalid_type', $data['code'] ); + $this->assertStringContainsString( 'is not of type number.', $data['message'] ); } /** @@ -441,7 +441,7 @@ public function test_contextual_permission_check(): void { } /** - * Test handling of null return from ability. + * Test handling of null is a valid return value. */ public function test_null_return_handling(): void { $request = new WP_REST_Request( 'POST', '/wp/v2/abilities/test/null-return/run' ); @@ -450,10 +450,9 @@ public function test_null_return_handling(): void { $response = $this->server->dispatch( $request ); - $this->assertEquals( 500, $response->get_status() ); + $this->assertEquals( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'rest_ability_execution_failed', $data['code'] ); - $this->assertStringContainsString( 'Ability execution failed', $data['message'] ); + $this->assertNull( $data ); } /** @@ -468,7 +467,7 @@ public function test_wp_error_return_handling(): void { $this->assertEquals( 500, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'rest_ability_execution_failed', $data['code'] ); + $this->assertEquals( 'test_error', $data['code'] ); $this->assertEquals( 'This is a test error', $data['message'] ); } @@ -526,16 +525,18 @@ public function test_invalid_json_in_post_body(): void { */ public function test_get_request_with_nested_input_array(): void { $request = new WP_REST_Request( 'GET', '/wp/v2/abilities/test/query-params/run' ); - $request->set_query_params( array( - 'input' => array( - 'level1' => array( - 'level2' => array( - 'value' => 'nested', + $request->set_query_params( + array( + 'input' => array( + 'level1' => array( + 'level2' => array( + 'value' => 'nested', + ), ), + 'array' => array( 1, 2, 3 ), ), - 'array' => array( 1, 2, 3 ), - ), - ) ); + ) + ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); @@ -550,9 +551,11 @@ public function test_get_request_with_nested_input_array(): void { */ public function test_get_request_with_non_array_input(): void { $request = new WP_REST_Request( 'GET', '/wp/v2/abilities/test/query-params/run' ); - $request->set_query_params( array( - 'input' => 'not-an-array', // String instead of array - ) ); + $request->set_query_params( + array( + 'input' => 'not-an-array', // String instead of array + ) + ); $response = $this->server->dispatch( $request ); // When input is not an array, WordPress returns 400 Bad Request @@ -565,9 +568,13 @@ public function test_get_request_with_non_array_input(): void { public function test_post_request_with_non_array_input(): void { $request = new WP_REST_Request( 'POST', '/wp/v2/abilities/test/calculator/run' ); $request->set_header( 'Content-Type', 'application/json' ); - $request->set_body( wp_json_encode( array( - 'input' => 'string-value', // String instead of array - ) ) ); + $request->set_body( + wp_json_encode( + array( + 'input' => 'string-value', // String instead of array + ) + ) + ); $response = $this->server->dispatch( $request ); // When input is not an array, WordPress returns 400 Bad Request @@ -576,16 +583,15 @@ public function test_post_request_with_non_array_input(): void { /** * Test ability with invalid output that fails validation. - * @expectedIncorrectUsage WP_Ability::validate_output */ public function test_output_validation_failure_returns_error(): void { // Register ability with strict output schema. wp_register_ability( 'test/strict-output', array( - 'label' => 'Strict Output', - 'description' => 'Ability with strict output schema', - 'output_schema' => array( + 'label' => 'Strict Output', + 'description' => 'Ability with strict output schema', + 'output_schema' => array( 'type' => 'object', 'properties' => array( 'status' => array( @@ -595,12 +601,12 @@ public function test_output_validation_failure_returns_error(): void { ), 'required' => array( 'status' ), ), - 'execute_callback' => function( $input ) { + 'execute_callback' => static function ( $input ) { // Return invalid output that doesn't match schema return array( 'wrong_field' => 'value' ); }, 'permission_callback' => '__return_true', - 'meta' => array( 'type' => 'tool' ), + 'meta' => array( 'type' => 'tool' ), ) ); @@ -613,21 +619,20 @@ public function test_output_validation_failure_returns_error(): void { // Should return error when output validation fails $this->assertEquals( 500, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'rest_ability_execution_failed', $data['code'] ); + $this->assertEquals( 'rest_property_required', $data['code'] ); } /** * Test ability with invalid input that fails validation. - * @expectedIncorrectUsage WP_Ability::validate_input */ public function test_input_validation_failure_returns_error(): void { // Register ability with strict input schema. wp_register_ability( 'test/strict-input', array( - 'label' => 'Strict Input', - 'description' => 'Ability with strict input schema', - 'input_schema' => array( + 'label' => 'Strict Input', + 'description' => 'Ability with strict input schema', + 'input_schema' => array( 'type' => 'object', 'properties' => array( 'required_field' => array( @@ -636,11 +641,11 @@ public function test_input_validation_failure_returns_error(): void { ), 'required' => array( 'required_field' ), ), - 'execute_callback' => function( $input ) { + 'execute_callback' => static function ( $input ) { return array( 'status' => 'success' ); }, 'permission_callback' => '__return_true', - 'meta' => array( 'type' => 'tool' ), + 'meta' => array( 'type' => 'tool' ), ) ); @@ -651,10 +656,10 @@ public function test_input_validation_failure_returns_error(): void { $response = $this->server->dispatch( $request ); - // Should return error when input validation fails (403 due to permission check) - $this->assertEquals( 403, $response->get_status() ); + // Should return error when input validation fails. + $this->assertEquals( 400, $response->get_status() ); $data = $response->get_data(); - $this->assertEquals( 'rest_cannot_execute', $data['code'] ); + $this->assertEquals( 'rest_invalid_param', $data['code'] ); } /** @@ -665,18 +670,18 @@ public function test_ability_without_type_defaults_to_tool(): void { wp_register_ability( 'test/no-type', array( - 'label' => 'No Type', - 'description' => 'Ability without type', - 'execute_callback' => function( $input ) { + 'label' => 'No Type', + 'description' => 'Ability without type', + 'execute_callback' => static function ( $input ) { return array( 'executed' => true ); }, 'permission_callback' => '__return_true', - 'meta' => array(), // No type specified + 'meta' => array(), // No type specified ) ); // Should require POST (default tool behavior) - $get_request = new WP_REST_Request( 'GET', '/wp/v2/abilities/test/no-type/run' ); + $get_request = new WP_REST_Request( 'GET', '/wp/v2/abilities/test/no-type/run' ); $get_response = $this->server->dispatch( $get_request ); $this->assertEquals( 405, $get_response->get_status() ); @@ -699,7 +704,7 @@ public function test_permission_check_passes_when_callback_not_set(): void { array( 'label' => 'No Permission Callback', 'description' => 'Ability without permission callback', - 'execute_callback' => function( $input ) { + 'execute_callback' => static function ( $input ) { return array( 'executed' => true ); }, 'meta' => array( 'type' => 'tool' ), @@ -730,31 +735,31 @@ public function test_empty_input_handling(): void { wp_register_ability( 'test/resource-empty', array( - 'label' => 'Resource Empty', - 'description' => 'Resource with empty input', - 'execute_callback' => function( $input ) { + 'label' => 'Resource Empty', + 'description' => 'Resource with empty input', + 'execute_callback' => static function ( $input ) { return array( 'input_was_empty' => empty( $input ) ); }, 'permission_callback' => '__return_true', - 'meta' => array( 'type' => 'resource' ), + 'meta' => array( 'type' => 'resource' ), ) ); wp_register_ability( 'test/tool-empty', array( - 'label' => 'Tool Empty', - 'description' => 'Tool with empty input', - 'execute_callback' => function( $input ) { + 'label' => 'Tool Empty', + 'description' => 'Tool with empty input', + 'execute_callback' => static function ( $input ) { return array( 'input_was_empty' => empty( $input ) ); }, 'permission_callback' => '__return_true', - 'meta' => array( 'type' => 'tool' ), + 'meta' => array( 'type' => 'tool' ), ) ); // Test GET with no input parameter - $get_request = new WP_REST_Request( 'GET', '/wp/v2/abilities/test/resource-empty/run' ); + $get_request = new WP_REST_Request( 'GET', '/wp/v2/abilities/test/resource-empty/run' ); $get_response = $this->server->dispatch( $get_request ); $this->assertEquals( 200, $get_response->get_status() ); $this->assertTrue( $get_response->get_data()['input_was_empty'] ); @@ -813,13 +818,13 @@ public function test_php_type_strings_in_input(): void { wp_register_ability( 'test/echo', array( - 'label' => 'Echo', - 'description' => 'Echoes input', - 'execute_callback' => function( $input ) { + 'label' => 'Echo', + 'description' => 'Echoes input', + 'execute_callback' => static function ( $input ) { return array( 'echo' => $input ); }, 'permission_callback' => '__return_true', - 'meta' => array( 'type' => 'tool' ), + 'meta' => array( 'type' => 'tool' ), ) ); @@ -854,13 +859,13 @@ public function test_mixed_encoding_in_input(): void { wp_register_ability( 'test/echo-encoding', array( - 'label' => 'Echo Encoding', - 'description' => 'Echoes input with encoding', - 'execute_callback' => function( $input ) { + 'label' => 'Echo Encoding', + 'description' => 'Echoes input with encoding', + 'execute_callback' => static function ( $input ) { return array( 'echo' => $input ); }, 'permission_callback' => '__return_true', - 'meta' => array( 'type' => 'tool' ), + 'meta' => array( 'type' => 'tool' ), ) ); @@ -916,7 +921,7 @@ public function test_invalid_http_methods( string $method ): void { array( 'label' => 'Method Test', 'description' => 'Test ability for HTTP method validation', - 'execute_callback' => function() { + 'execute_callback' => static function () { return array( 'success' => true ); }, 'permission_callback' => '__return_true', // No permission requirements @@ -924,7 +929,7 @@ public function test_invalid_http_methods( string $method ): void { ) ); - $request = new WP_REST_Request( $method, '/wp/v2/abilities/test/method-test/run' ); + $request = new WP_REST_Request( $method, '/wp/v2/abilities/test/method-test/run' ); $response = $this->server->dispatch( $request ); // Tool abilities should only accept POST, so these should return 405 @@ -937,10 +942,9 @@ public function test_invalid_http_methods( string $method ): void { * Test OPTIONS method handling. */ public function test_options_method_handling(): void { - $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/abilities/test/calculator/run' ); + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/abilities/test/calculator/run' ); $response = $this->server->dispatch( $request ); // OPTIONS requests return 200 with allowed methods $this->assertEquals( 200, $response->get_status() ); } - } From 751a3b691176d7d17c01953f10be143199286fa6 Mon Sep 17 00:00:00 2001 From: Dovid Levine Date: Sun, 17 Aug 2025 00:42:05 +0300 Subject: [PATCH 2/4] docs: fix return on validate_output --- includes/abilities-api/class-wp-ability.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/abilities-api/class-wp-ability.php b/includes/abilities-api/class-wp-ability.php index 1e30adea..0406c64a 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -242,7 +242,7 @@ protected function do_execute( array $input ) { * @since 0.1.0 * * @param mixed $output The output data to validate. - * @return true|\WP_Error Returns true if valid, false if validation fails. + * @return true|\WP_Error Returns true if valid, or a WP_Error object if validation fails. */ protected function validate_output( $output ) { $output_schema = $this->get_output_schema(); From f515cc91c25e9e30eed5c011eb25a64451ae5635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Tue, 19 Aug 2025 08:13:23 +0200 Subject: [PATCH 3/4] Update class-wp-rest-abilities-run-controller.php --- .../endpoints/class-wp-rest-abilities-run-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php b/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php index 6d57780a..dcc2d1c7 100644 --- a/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php +++ b/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php @@ -145,7 +145,7 @@ public function run_ability( $request ) { $result = $ability->execute( $input ); - if ( is_wp_error( $result ) ) {<<<<<<< dev/wp_ability-return-error + if ( is_wp_error( $result ) ) { return $result; } From 3885a9e2c5e066b49c2bd7e4b658db46059fc3da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Tue, 19 Aug 2025 08:19:42 +0200 Subject: [PATCH 4/4] Update wpRestAbilitiesRunController.php --- tests/unit/rest-api/wpRestAbilitiesRunController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/rest-api/wpRestAbilitiesRunController.php b/tests/unit/rest-api/wpRestAbilitiesRunController.php index b74b67eb..82abc6c3 100644 --- a/tests/unit/rest-api/wpRestAbilitiesRunController.php +++ b/tests/unit/rest-api/wpRestAbilitiesRunController.php @@ -752,7 +752,7 @@ public function test_empty_input_handling(): void { array( 'label' => 'Tool Empty', 'description' => 'Tool with empty input', - 'execute_callback' => static function ( $ 'execute_callback' => function ( $input ) { + 'execute_callback' => static function ( $input ) { return array( 'input_was_empty' => empty( $input ) ); }, 'permission_callback' => '__return_true',