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 4 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
71 changes: 48 additions & 23 deletions docs/4.using-abilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,20 @@ $site_info_ability = wp_get_ability( 'my-plugin/get-site-info' );

if ( $site_info_ability ) {
// Ability exists and is registered
$site_info = $site_info_ability->execute();
if ( is_wp_error( $site_info ) ) {
// Handle WP_Error
echo 'Error: ' . $site_info->get_error_message();
$result = $site_info_ability->execute();
if ( is_wp_error( $result ) ) {
// Handle any error (permission, validation, or execution)
echo 'Error: ' . $result->get_error_message();
} else {
// Use $site_info array
echo 'Site Name: ' . $site_info['name'];
// Use result data
echo 'Site Name: ' . $result['name'];
}
} else {
// Ability not found or not registered
}
```

## Getting All Registered Abilities (`wp_get_abilities`)
## Getting All Registered Abilities (`wp_get_abilities`)**
Comment thread
jonathanbossenger marked this conversation as resolved.
Outdated

To get an array of all registered abilities:

Expand Down Expand Up @@ -74,10 +74,10 @@ $ability = wp_get_ability( 'my-plugin/get-site-info' );
if ( $ability ) {
$site_info = $ability->execute(); // No input required
Comment thread
jonathanbossenger marked this conversation as resolved.
Outdated
if ( is_wp_error( $site_info ) ) {
// Handle WP_Error
// Handle any error (permission, validation, or execution)
echo 'Error: ' . $site_info->get_error_message();
} else {
// Use $site_info array
// Use result data
echo 'Site Name: ' . $site_info['name'];
}
}
Expand All @@ -92,7 +92,7 @@ if ( $ability ) {

$result = $ability->execute( $input );
if ( is_wp_error( $result ) ) {
// Handle WP_Error
// Handle any error (permission, validation, or execution)
echo 'Error: ' . $result->get_error_message();
} else {
// Use $result
Expand All @@ -114,7 +114,7 @@ if ( $ability ) {

$result = $ability->execute( $input );
if ( is_wp_error( $result ) ) {
// Handle WP_Error
// Handle any error (permission, validation, or execution)
echo 'Error: ' . $result->get_error_message();
} elseif ( $result['sent'] ) {
echo 'Email sent successfully!';
Expand All @@ -124,9 +124,10 @@ if ( $ability ) {
}
```

## Checking Permissions (`$ability->check_permissions()`)
## Understanding Permissions

Abilities handle permission checking automatically during execution. The `execute()` method will check permissions and return a `WP_Error` if access is denied. You rarely need to check permissions separately.

You can check if the current user has permissions to execute the ability, also without executing it. The `check_permissions()` method returns either `true`, `false`, or a `WP_Error` object. `true` means permission is granted, `false` means the user simply lacks permission, and a `WP_Error` return value typically indicates a failure in the permission check process (such as an internal error or misconfiguration). You must use `is_wp_error()` to handle errors properly and distinguish between permission denial and actual errors:

```php
$ability = wp_get_ability( 'my-plugin/update-option' );
Expand All @@ -136,18 +137,42 @@ if ( $ability ) {
'option_value' => 'New Site Name',
);

// Check permission before execution - always use is_wp_error() first
$has_permissions = $ability->check_permissions( $input );
if ( true === $has_permissions ) {
// Permissions granted – safe to execute.
echo 'You have permissions to execute this ability.';
// Recommended approach: execute() handles permission checking internally
$result = $ability->execute( $input );
if ( is_wp_error( $result ) ) {
// Handle any error (permission, validation, or execution)
echo 'Error: ' . $result->get_error_message();
} else {
// Don't leak permission errors to unauthenticated users.
if ( is_wp_error( $has_permissions ) ) {
error_log( 'Permissions check failed: ' . $has_permissions->get_error_message() );
}
// Success - use the result
if ( $result['success'] ) {
echo 'Option updated successfully!';
echo 'Previous value: ' . $result['previous_value'];
}
}
}
```

## Advanced Error Handling

If you need to handle specific error types differently, you can check the error code:

echo 'You do not have permissions to execute this ability.';
```php
$ability = wp_get_ability( 'my-plugin/update-option' );
if ( $ability ) {
$result = $ability->execute( $input );

// Handle specific error types if needed
if ( is_wp_error( $result ) && 'ability_invalid_permissions' === $result->get_error_code() ) {
// Handle permission errors specifically
echo 'You do not have permission to execute this ability.';
} elseif ( is_wp_error( $result ) ) {
// Handle other errors (validation, execution, etc.)
echo 'Error: ' . $result->get_error_message();
} else {
// Success - use the result
if ( $result['success'] ) {
echo 'Option updated successfully!';
}
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ public function run_ability_permissions_check( $request ) {
}

$input = $this->get_input_from_request( $request );
if ( ! $ability->check_permissions( $input ) ) {
if ( true !== $ability->check_permissions( $input ) ) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sharing relevant feedback that I initially mentioned in WordPress/wordpress-develop#9410 (comment): I don't think we should replace a contextually more specific error (from the actual permission callback) with a generic "Sorry you can't do this" error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I've updated the code to preserve specific error messages from the permission callback instead of replacing them with a generic error.

The permission check now:

  1. Returns the WP_Error directly if check_permissions() returns one (preserves context)
  2. Only falls back to the generic error if it returns false

resolved here https://github.com/WordPress/abilities api/pull/95/commits/27b7b1a8215023b128cbcc55a27f51ac23e8774c

return new \WP_Error(
'rest_ability_cannot_execute',
__( 'Sorry, you are not allowed to execute this ability.' ),
Expand Down
22 changes: 11 additions & 11 deletions tests/unit/rest-api/wpRestAbilitiesRunController.php
Original file line number Diff line number Diff line change
Expand Up @@ -356,10 +356,10 @@ public function test_resource_ability_requires_get(): void {

$response = $this->server->dispatch( $request );

$this->assertSame( 405, $response->get_status() );
$this->assertSame( 403, $response->get_status() );
$data = $response->get_data();
$this->assertSame( 'rest_ability_invalid_method', $data['code'] );
$this->assertSame( 'Resource abilities require GET method.', $data['message'] );
$this->assertSame( 'rest_ability_cannot_execute', $data['code'] );
$this->assertSame( 'Sorry, you are not allowed to execute this ability.', $data['message'] );
}


Expand Down Expand Up @@ -561,8 +561,8 @@ public function test_get_request_with_non_array_input(): void {
);

$response = $this->server->dispatch( $request );
// When input is not an array, WordPress returns 400 Bad Request
$this->assertEquals( 400, $response->get_status() );
// Our security fix now catches invalid input in permission check
$this->assertEquals( 403, $response->get_status() );
}

/**
Expand All @@ -580,8 +580,8 @@ public function test_post_request_with_non_array_input(): void {
);

$response = $this->server->dispatch( $request );
// When input is not an array, WordPress returns 400 Bad Request
$this->assertEquals( 400, $response->get_status() );
// Our security fix now catches invalid input in permission check
$this->assertEquals( 403, $response->get_status() );
}

/**
Expand Down Expand Up @@ -662,12 +662,12 @@ public function test_input_validation_failure_returns_error(): void {

$response = $this->server->dispatch( $request );

// Should return error when input validation fails.
$this->assertSame( 400, $response->get_status() );
// Our security fix now catches input validation failures in permission check
$this->assertSame( 403, $response->get_status() );
$data = $response->get_data();
$this->assertSame( 'ability_invalid_input', $data['code'] );
$this->assertSame( 'rest_ability_cannot_execute', $data['code'] );
$this->assertSame(
'Ability "test/strict-input" has invalid input. Reason: required_field is a required property of input.',
'Sorry, you are not allowed to execute this ability.',
$data['message']
);
}
Expand Down