Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
81 changes: 53 additions & 28 deletions docs/4.using-abilities.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 4. Using Abilities (`wp_get_ability`, `wp_get_abilities`)
### 4. Using Abilities (`wp_get_ability`, `wp_get_abilities`)

Once abilities are registered, they can be retrieved and executed using global functions from the Abilities API.

## Getting a Specific Ability (`wp_get_ability`)
**Getting a Specific Ability (`wp_get_ability`)**

To get a single ability object by its name (namespace/ability-name):

Expand All @@ -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`)**

To get an array of all registered abilities:

Expand All @@ -56,7 +56,7 @@ foreach ( $all_abilities as $name => $ability ) {
}
```

## Executing an Ability (`$ability->execute()`)
**Executing an Ability (`$ability->execute()`)**

Once you have a `WP_Ability` object (usually from `wp_get_ability`), you execute it using the `execute()` method.

Expand All @@ -74,10 +74,10 @@ $ability = wp_get_ability( 'my-plugin/get-site-info' );
if ( $ability ) {
$site_info = $ability->execute(); // No input required
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,23 +137,47 @@ 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!';
}
}
}
```

## Inspecting Ability Properties
**Inspecting Ability Properties**

The `WP_Ability` class provides several getter methods to inspect ability properties:

Expand All @@ -179,7 +204,7 @@ if ( $ability ) {
}
```

## Error Handling Patterns
**Error Handling Patterns**

The Abilities API uses several error handling mechanisms:

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
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
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