Skip to content

Commit 42388b5

Browse files
committed
Abilities API: Catch exceptions thrown by ability callbacks and return WP_Error.
Wraps `invoke_callback()` in a try/catch so that exceptions thrown by execute or permission callbacks are converted to a `WP_Error` with the `ability_callback_exception` code instead of propagating as uncaught throwables. Developed in: #11544 Props priyankagusani, jamesgiroux, jeffpaul, dkotter, adamsilverstein, justlevine, jorbin, pavanpatil1. Fixes #65058. git-svn-id: https://develop.svn.wordpress.org/trunk@62238 602fd350-edb4-49c9-b593-d223f7449a82
1 parent bf4c174 commit 42388b5

2 files changed

Lines changed: 62 additions & 2 deletions

File tree

src/wp-includes/abilities-api/class-wp-ability.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,15 +502,27 @@ public function validate_input( $input = null ) {
502502
*
503503
* @param callable $callback The callable to invoke.
504504
* @param mixed $input Optional. The input data for the ability. Default `null`.
505-
* @return mixed The result of the callable execution.
505+
* @return mixed The result of the callable execution, or a `WP_Error` if the callback threw.
506506
*/
507507
protected function invoke_callback( callable $callback, $input = null ) {
508508
$args = array();
509509
if ( ! empty( $this->get_input_schema() ) ) {
510510
$args[] = $input;
511511
}
512512

513-
return $callback( ...$args );
513+
try {
514+
return $callback( ...$args );
515+
} catch ( Throwable $e ) {
516+
return new WP_Error(
517+
'ability_callback_exception',
518+
sprintf(
519+
/* translators: 1: Ability name, 2: Exception message. */
520+
__( 'Ability "%1$s" callback threw an exception: %2$s' ),
521+
esc_html( $this->name ),
522+
esc_html( $e->getMessage() )
523+
)
524+
);
525+
}
514526
}
515527

516528
/**

tests/phpunit/tests/abilities-api/wpAbility.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,54 @@ public function test_execute_no_input() {
497497
$this->assertSame( 42, $ability->execute() );
498498
}
499499

500+
/**
501+
* Tests that an exception thrown by the execute callback is converted to a WP_Error
502+
* instead of being propagated as an uncaught throwable.
503+
*
504+
* @ticket 65058
505+
*/
506+
public function test_execute_catches_callback_exception() {
507+
$args = array_merge(
508+
self::$test_ability_properties,
509+
array(
510+
'execute_callback' => static function (): int {
511+
throw new RuntimeException( 'boom' );
512+
},
513+
)
514+
);
515+
516+
$ability = new WP_Ability( self::$test_ability_name, $args );
517+
$result = $ability->execute();
518+
519+
$this->assertWPError( $result, 'Ability::execute() should return WP_Error when the callback throws.' );
520+
$this->assertSame( 'ability_callback_exception', $result->get_error_code() );
521+
$this->assertStringContainsString( 'boom', $result->get_error_message() );
522+
}
523+
524+
/**
525+
* Tests that an exception thrown by the permission callback is converted to a WP_Error
526+
* instead of being propagated as an uncaught throwable.
527+
*
528+
* @ticket 65058
529+
*/
530+
public function test_check_permissions_catches_callback_exception() {
531+
$args = array_merge(
532+
self::$test_ability_properties,
533+
array(
534+
'permission_callback' => static function (): bool {
535+
throw new RuntimeException( 'permission exploded' );
536+
},
537+
)
538+
);
539+
540+
$ability = new WP_Ability( self::$test_ability_name, $args );
541+
$result = $ability->check_permissions();
542+
543+
$this->assertWPError( $result, 'Ability::check_permissions() should return WP_Error when the callback throws.' );
544+
$this->assertSame( 'ability_callback_exception', $result->get_error_code() );
545+
$this->assertStringContainsString( 'permission exploded', $result->get_error_message() );
546+
}
547+
500548
/**
501549
* Tests that before_execute_ability action is fired with correct parameters.
502550
*

0 commit comments

Comments
 (0)