diff --git a/includes/abilities-api/class-wp-ability.php b/includes/abilities-api/class-wp-ability.php index d116080..4209674 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -479,7 +479,7 @@ public function validate_input( $input = null ) { $valid_input = rest_validate_value_from_schema( $input, $input_schema, 'input' ); if ( is_wp_error( $valid_input ) ) { - return new WP_Error( + $is_valid = new WP_Error( 'ability_invalid_input', sprintf( /* translators: %1$s ability name, %2$s error message. */ @@ -488,9 +488,25 @@ public function validate_input( $input = null ) { $valid_input->get_error_message() ) ); + } else { + $is_valid = true; } - return true; + /** + * Filters the input validation result for an ability. + * + * Allows developers to add custom validation logic on top of the default JSON Schema validation. + * If the default validation already failed, the filter receives the WP_Error object and can + * add additional error information or override it. If the default validation passed, the filter + * can add additional validation checks and return a WP_Error if those checks fail. + * + * @since 7.0.0 + * + * @param true|WP_Error $is_valid The validation result from default validation. + * @param mixed $input The input data being validated. + * @param string $ability_name The name of the ability. + */ + return apply_filters( 'wp_ability_validate_input', $is_valid, $input, $this->name ); } /** @@ -567,23 +583,39 @@ protected function do_execute( $input = null ) { 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, 'output' ); - if ( is_wp_error( $valid_output ) ) { - return new WP_Error( - 'ability_invalid_output', - sprintf( - /* translators: %1$s ability name, %2$s error message. */ - __( 'Ability "%1$s" has invalid output. Reason: %2$s' ), - esc_html( $this->name ), - $valid_output->get_error_message() - ) - ); + $is_valid = true; + } else { + $valid_output = rest_validate_value_from_schema( $output, $output_schema, 'output' ); + if ( is_wp_error( $valid_output ) ) { + $is_valid = new WP_Error( + 'ability_invalid_output', + sprintf( + /* translators: %1$s ability name, %2$s error message. */ + __( 'Ability "%1$s" has invalid output. Reason: %2$s' ), + esc_html( $this->name ), + $valid_output->get_error_message() + ) + ); + } else { + $is_valid = true; + } } - return true; + /** + * Filters the output validation result for an ability. + * + * Allows developers to add custom validation logic on top of the default JSON Schema validation. + * If the default validation already failed, the filter receives the WP_Error object and can + * add additional error information or override it. If the default validation passed, the filter + * can add additional validation checks and return a WP_Error if those checks fail. + * + * @since 7.0.0 + * + * @param true|WP_Error $is_valid The validation result from default validation. + * @param mixed $output The output data being validated. + * @param string $ability_name The name of the ability. + */ + return apply_filters( 'wp_ability_validate_output', $is_valid, $output, $this->name ); } /** diff --git a/tests/unit/abilities-api/wpAbility.php b/tests/unit/abilities-api/wpAbility.php index 73a5fbf..f860890 100644 --- a/tests/unit/abilities-api/wpAbility.php +++ b/tests/unit/abilities-api/wpAbility.php @@ -778,4 +778,311 @@ public function test_after_action_not_fired_on_output_validation_error() { $this->assertFalse( $after_action_fired, 'after_execute_ability action should not be fired when output validation fails' ); $this->assertInstanceOf( WP_Error::class, $result, 'Should return WP_Error for output validation failure' ); } + + /** + * Tests wp_ability_validate_input filter receives all parameters. + * + * @ticket 64311 + */ + public function test_validate_input_filter_receives_all_parameters() { + $captured = array(); + + $args = array_merge( + self::$test_ability_properties, + array( + 'input_schema' => array( + 'type' => 'string', + 'description' => 'Test input string.', + 'required' => true, + ), + 'execute_callback' => static function ( string $input ): int { + return strlen( $input ); + }, + ) + ); + + add_filter( + 'wp_ability_validate_input', + static function ( $is_valid, $input, $ability_name ) use ( &$captured ) { + $captured = array( $is_valid, $input, $ability_name ); + return $is_valid; + }, + 10, + 3 + ); + + $ability = new WP_Ability( self::$test_ability_name, $args ); + $ability->execute( 'hello' ); + + $this->assertTrue( $captured[0] ); + $this->assertSame( 'hello', $captured[1] ); + $this->assertSame( self::$test_ability_name, $captured[2] ); + } + + /** + * Tests wp_ability_validate_input filter can override validation failure. + * + * @ticket 64311 + */ + public function test_validate_input_filter_overrides_validation_failure() { + $args = array_merge( + self::$test_ability_properties, + array( + 'input_schema' => array( + 'type' => 'integer', + 'description' => 'Test input integer.', + 'required' => true, + ), + 'output_schema' => array( + 'type' => 'integer', + 'description' => 'Result integer.', + 'required' => true, + ), + 'execute_callback' => static function () { + return 99; + }, + ) + ); + + add_filter( + 'wp_ability_validate_input', + static function ( $is_valid ) { + return true; // Override any validation error with pass. + }, + 10, + 1 + ); + + $ability = new WP_Ability( self::$test_ability_name, $args ); + $result = $ability->execute( 'invalid' ); + + $this->assertSame( 99, $result ); + } + + /** + * Tests wp_ability_validate_output filter receives all parameters. + * + * @ticket 64311 + */ + public function test_validate_output_filter_receives_all_parameters() { + $captured = array(); + + $args = array_merge( + self::$test_ability_properties, + array( + 'output_schema' => array( + 'type' => 'integer', + 'description' => 'The result integer.', + 'required' => true, + ), + 'execute_callback' => static function (): int { + return 42; + }, + ) + ); + + add_filter( + 'wp_ability_validate_output', + static function ( $is_valid, $output, $ability_name ) use ( &$captured ) { + $captured = array( $is_valid, $output, $ability_name ); + return $is_valid; + }, + 10, + 3 + ); + + $ability = new WP_Ability( self::$test_ability_name, $args ); + $ability->execute(); + + $this->assertTrue( $captured[0] ); + $this->assertSame( 42, $captured[1] ); + $this->assertSame( self::$test_ability_name, $captured[2] ); + } + + /** + * Tests wp_ability_validate_output filter can override validation failure. + * + * @ticket 64311 + */ + public function test_validate_output_filter_overrides_validation_failure() { + $args = array_merge( + self::$test_ability_properties, + array( + 'output_schema' => array( + 'type' => 'string', + 'description' => 'The result string.', + 'required' => true, + ), + 'execute_callback' => static function (): int { + return 42; + }, + ) + ); + + add_filter( + 'wp_ability_validate_output', + static function () { + return true; // Override any validation error with pass. + }, + 10, + 1 + ); + + $ability = new WP_Ability( self::$test_ability_name, $args ); + $result = $ability->execute(); + + $this->assertSame( 42, $result ); + } + + /** + * Tests wp_ability_validate_input filter receives WP_Error on validation failure. + * + * @ticket 64311 + */ + public function test_validate_input_filter_receives_error_on_invalid_input() { + $error_code = null; + + $args = array_merge( + self::$test_ability_properties, + array( + 'input_schema' => array( + 'type' => 'integer', + 'description' => 'Test input integer.', + 'required' => true, + ), + 'execute_callback' => static function ( int $input ): int { + return $input * 2; + }, + ) + ); + + add_filter( + 'wp_ability_validate_input', + static function ( $is_valid ) use ( &$error_code ) { + if ( is_wp_error( $is_valid ) ) { + $error_code = $is_valid->get_error_code(); + } + return $is_valid; + }, + 10, + 1 + ); + + $ability = new WP_Ability( self::$test_ability_name, $args ); + $ability->execute( 'invalid' ); + + $this->assertSame( 'ability_invalid_input', $error_code ); + } + + /** + * Tests wp_ability_validate_input filter can replace error with custom error. + * + * @ticket 64311 + */ + public function test_validate_input_filter_replaces_error_with_custom() { + $args = array_merge( + self::$test_ability_properties, + array( + 'input_schema' => array( + 'type' => 'integer', + 'description' => 'Test input integer.', + 'required' => true, + ), + 'execute_callback' => static function ( int $input ): int { + return $input * 2; + }, + ) + ); + + add_filter( + 'wp_ability_validate_input', + static function () { + return new WP_Error( 'custom_error', 'Custom message.' ); + }, + 10, + 1 + ); + + $ability = new WP_Ability( self::$test_ability_name, $args ); + $result = $ability->execute( 'invalid' ); + + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertSame( 'custom_error', $result->get_error_code() ); + } + + /** + * Tests wp_ability_validate_output filter receives WP_Error on validation failure. + * + * @ticket 64311 + */ + public function test_validate_output_filter_receives_error_on_invalid_output() { + $error_code = null; + + $args = array_merge( + self::$test_ability_properties, + array( + 'output_schema' => array( + 'type' => 'string', + 'description' => 'The result string.', + 'required' => true, + ), + 'execute_callback' => static function (): int { + return 42; + }, + ) + ); + + add_filter( + 'wp_ability_validate_output', + static function ( $is_valid ) use ( &$error_code ) { + if ( is_wp_error( $is_valid ) ) { + $error_code = $is_valid->get_error_code(); + } + return $is_valid; + }, + 10, + 1 + ); + + $ability = new WP_Ability( self::$test_ability_name, $args ); + $ability->execute(); + + $this->assertSame( 'ability_invalid_output', $error_code ); + } + + /** + * Tests wp_ability_validate_output filter can replace error with custom error. + * + * @ticket 64311 + */ + public function test_validate_output_filter_replaces_error_with_custom() { + $args = array_merge( + self::$test_ability_properties, + array( + 'output_schema' => array( + 'type' => 'string', + 'description' => 'The result string.', + 'required' => true, + ), + 'execute_callback' => static function (): int { + return 42; + }, + ) + ); + + add_filter( + 'wp_ability_validate_output', + static function () { + return new WP_Error( 'custom_output_error', 'Custom output message.' ); + }, + 10, + 1 + ); + + $ability = new WP_Ability( self::$test_ability_name, $args ); + $result = $ability->execute(); + + $this->assertInstanceOf( WP_Error::class, $result ); + $this->assertSame( 'custom_output_error', $result->get_error_code() ); + } }