Skip to content
This repository was archived by the owner on Feb 5, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 2 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
68 changes: 51 additions & 17 deletions includes/abilities-api/class-wp-ability.php
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -488,9 +488,26 @@ 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.
* @param WP_Ability $ability The ability instance.
Comment thread
justlevine marked this conversation as resolved.
Outdated
*/
return apply_filters( 'wp_ability_validate_input', $is_valid, $input, $this->name, $this );
}

/**
Expand Down Expand Up @@ -567,23 +584,40 @@ 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.
* @param WP_Ability $ability The ability instance.
*/
return apply_filters( 'wp_ability_validate_output', $is_valid, $output, $this->name, $this );
}

/**
Expand Down
293 changes: 293 additions & 0 deletions tests/unit/abilities-api/wpAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -778,4 +778,297 @@ 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.
*/
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, $ability ) use ( &$captured ) {
$captured = array( $is_valid, $input, $ability_name, $ability );
return $is_valid;
},
10,
4
);

$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] );
$this->assertInstanceOf( WP_Ability::class, $captured[3] );
}

/**
* Tests wp_ability_validate_input filter can override validation failure.
*/
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.
*/
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, $ability ) use ( &$captured ) {
$captured = array( $is_valid, $output, $ability_name, $ability );
return $is_valid;
},
10,
4
);

$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] );
$this->assertInstanceOf( WP_Ability::class, $captured[3] );
}

/**
* Tests wp_ability_validate_output filter can override validation failure.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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() );
}
}
Loading