Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions docs/1.intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ It acts as a central registry, making it easier for different parts of WordPress
- **Registry:** A central, singleton object (`WP_Abilities_Registry`) that holds all registered abilities. It provides methods for registering, unregistering, finding, and querying abilities.
- **Callback:** The PHP function or method executed when an ability is called via `WP_Ability::execute()`.
- **Schema:** JSON Schema definitions for an ability's expected input (`input_schema`) and its returned output (`output_schema`). This allows for validation and helps agents understand how to use the ability.
- **Permission Callback:** An optional function that determines if the current user can execute a specific ability.
- **Permission Callback:** An optional function that determines if the current user can execute a specific ability.
- **Namespace:** The first part of an ability name (before the slash), typically matching the plugin or component name that registers the ability.

## Goals and Benefits
Expand Down Expand Up @@ -76,7 +76,8 @@ function my_plugin_register_ability(){
'execute_callback' => 'my_plugin_get_siteinfo',
'permission_callback' => function( $input ) {
return current_user_can( 'manage_options' );
}
},
'show_in_rest' => true,
));
}
```
Expand Down
1 change: 1 addition & 0 deletions docs/2.getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ function my_plugin_register_abilities() {
'meta' => array(
'category' => 'site-info',
),
'show_in_rest' => true, // Optional: expose via REST API
) );
}

Expand Down
3 changes: 3 additions & 0 deletions docs/3.registering-abilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ The `$args` array accepts the following keys:
- The callback receives one optional argument: it can have any type as defined in the input schema (e.g., `array`, `object`, `string`, etc.).
- The callback should return a boolean (`true` if the user has permission, `false` otherwise), or a `WP_Error` object on failure.
- If the input does not validate against the input schema, the permission callback will not be called, and a `WP_Error` will be returned instead.
- `show_in_rest` (`boolean`, **Optional**): Whether to expose this ability via the REST API. Default: `false`.
- When `true`, the ability will be listed in REST API responses and can be executed via REST endpoints.
- When `false`, the ability will be hidden from REST API listings and cannot be executed via REST endpoints, but remains available for internal PHP usage.
- `meta` (`array`, **Optional**): An associative array for storing arbitrary additional metadata about the ability.

## Ability ID Convention
Expand Down
9 changes: 9 additions & 0 deletions docs/5.rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ The WordPress Abilities API provides REST endpoints that allow external systems

Access to all Abilities REST API endpoints requires an authenticated user (see the [Authentication](#authentication) section). Access to execute individual Abilities is restricted based on the `permission_callback()` of the Ability.

## Controlling REST API Exposure

By default, registered abilities are **not** exposed via the REST API. You can control whether an individual ability appears in the REST API by using the `show_in_rest` argument when registering the ability:

- `show_in_rest => true`: The ability is listed in REST API responses and can be executed via REST endpoints.
- `show_in_rest => false` (default): The ability is hidden from REST API listings and cannot be executed via REST endpoints. The ability remains available for internal PHP usage via `wp_execute_ability()`.

Abilities with `show_in_rest => false` will return a `rest_ability_not_found` error if accessed via REST endpoints.

## Schema

The Abilities API endpoints are available under the `/wp/v2/abilities` namespace.
Expand Down
3 changes: 2 additions & 1 deletion includes/abilities-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* alphanumeric characters, dashes and the forward slash.
* @param array<string,mixed> $args An associative array of arguments for the ability. This should include
* `label`, `description`, `input_schema`, `output_schema`, `execute_callback`,
* `permission_callback`, `annotations`, `meta`, and `ability_class`.
* `permission_callback`, `annotations`, `meta`, `show_in_rest`, and `ability_class`.
* @return ?\WP_Ability An instance of registered ability on success, null on failure.
*
* @phpstan-param array{
Expand All @@ -37,6 +37,7 @@
* output_schema?: array<string,mixed>,
* annotations?: array<string,mixed>,
* meta?: array<string,mixed>,
* show_in_rest?: bool,
* ability_class?: class-string<\WP_Ability>,
* ...<string, mixed>
* } $args
Expand Down
1 change: 1 addition & 0 deletions includes/abilities-api/class-wp-abilities-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ final class WP_Abilities_Registry {
* output_schema?: array<string,mixed>,
* annotations?: array<string,mixed>,
* meta?: array<string,mixed>,
* show_in_rest?: bool,
* ability_class?: class-string<\WP_Ability>,
* ...<string, mixed>
* } $args
Expand Down
32 changes: 29 additions & 3 deletions includes/abilities-api/class-wp-ability.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ class WP_Ability {
*/
protected $meta = array();

/**
* Whether to show the ability in the REST API.
*
* @since n.e.x.t
* @var bool
*/
protected $show_in_rest = false;

/**
* Constructor.
*
Expand All @@ -128,9 +136,9 @@ class WP_Ability {
* @see wp_register_ability()
*
* @param string $name The name of the ability, with its namespace.
* @param array<string,mixed> $args An associative array of arguments for the ability. This should
* include `label`, `description`, `input_schema`, `output_schema`,
* `execute_callback`, `permission_callback`, `annotations`, and `meta`.
* @param array<string,mixed> $args An associative array of arguments for the ability. This should include
* `label`, `description`, `input_schema`, `output_schema`, `execute_callback`,
* `permission_callback`, `annotations`, `meta`, and `show_in_rest`.
*/
public function __construct( string $name, array $args ) {
$this->name = $name;
Expand Down Expand Up @@ -180,6 +188,7 @@ public function __construct( string $name, array $args ) {
* output_schema?: array<string,mixed>,
* annotations?: array<string,mixed>,
* meta?: array<string,mixed>,
* show_in_rest?: bool,
* ...<string, mixed>,
* } $args
*/
Expand Down Expand Up @@ -234,6 +243,12 @@ protected function prepare_properties( array $args ): array {
);
}

if ( isset( $args['show_in_rest'] ) && ! is_bool( $args['show_in_rest'] ) ) {
throw new \InvalidArgumentException(
esc_html__( 'The ability properties should provide a valid `show_in_rest` boolean.' )
);
}

// Set defaults for optional args.
$args['annotations'] = wp_parse_args(
$args['annotations'] ?? array(),
Expand Down Expand Up @@ -321,6 +336,17 @@ public function get_meta(): array {
return $this->meta;
}

/**
* Checks whether the ability should be shown in the REST API.
*
* @since n.e.x.t
*
* @return bool True if the ability should be shown in the REST API, false otherwise.
*/
public function show_in_rest(): bool {
return $this->show_in_rest;
}

/**
* Validates input data against the input schema.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ public function register_routes(): void {
* @return \WP_REST_Response Response object on success.
*/
public function get_items( $request ) {
$abilities = wp_get_abilities();
$abilities = array_filter(
wp_get_abilities(),
static function ( $ability ) {
return $ability->show_in_rest();
}
);

// Handle pagination with explicit defaults.
$params = $request->get_params();
Expand Down Expand Up @@ -150,8 +155,7 @@ public function get_items( $request ) {
*/
public function get_item( $request ) {
$ability = wp_get_ability( $request->get_param( 'name' ) );

if ( ! $ability ) {
if ( ! $ability || ! $ability->show_in_rest() ) {
return new \WP_Error(
'rest_ability_not_found',
__( 'Ability not found.' ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ public function run_ability( $request ) {
*/
public function run_ability_permissions_check( $request ) {
$ability = wp_get_ability( $request->get_param( 'name' ) );
if ( ! $ability ) {
if ( ! $ability || ! $ability->show_in_rest() ) {
return new \WP_Error(
'rest_ability_not_found',
__( 'Ability not found.' ),
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/abilities-api/wpAbilitiesRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public function set_up(): void {
'meta' => array(
'category' => 'math',
),
'show_in_rest' => true,
);
}

Expand Down Expand Up @@ -297,6 +298,21 @@ public function test_register_invalid_meta_type() {
$this->assertNull( $result );
}

/**
* Should reject ability registration with invalid show in REST type.
*
* @covers WP_Abilities_Registry::register
* @covers WP_Ability::prepare_properties
*
* @expectedIncorrectUsage WP_Abilities_Registry::register
*/
public function test_register_invalid_show_in_rest_type() {
self::$test_ability_args['show_in_rest'] = 5;

$result = $this->registry->register( self::$test_ability_name, self::$test_ability_args );
$this->assertNull( $result );
}

/**
* Should reject registration for already registered ability.
*
Expand Down
47 changes: 43 additions & 4 deletions tests/unit/abilities-api/wpAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ public function set_up(): void {
'readonly' => true,
'destructive' => false,
),
'meta' => array(
'category' => 'math',
),
);
}

Expand Down Expand Up @@ -101,8 +98,9 @@ public function test_get_all_annotations_overridden() {

$this->assertSame( $annotations, $ability->get_annotations() );
}

/**
* Tests that invalid annotations throw an exception.
* Tests that invalid `annotations` value throws an exception.
*/
public function test_annotations_throws_exception() {
$args = array_merge(
Expand All @@ -118,6 +116,47 @@ public function test_annotations_throws_exception() {
new WP_Ability( self::$test_ability_name, $args );
}

/**
* Tests that `show_in_rest` defaults to false when not provided.
*/
public function test_show_in_rest_defaults_to_false() {
$ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties );

$this->assertFalse( $ability->show_in_rest(), '`show_in_rest` should default to false.' );
}

/**
* Tests that `show_in_rest` can be set to true.
*/
public function test_show_in_rest_can_be_set_to_true() {
$args = array_merge(
self::$test_ability_properties,
array(
'show_in_rest' => true,
)
);
$ability = new WP_Ability( self::$test_ability_name, $args );

$this->assertTrue( $ability->show_in_rest(), '`show_in_rest` should be true.' );
}

/**
* Tests that invalid `show_in_rest` value throws an exception.
*/
public function test_show_in_rest_throws_exception() {
$args = array_merge(
self::$test_ability_properties,
array(
'show_in_rest' => 5,
)
);

$this->expectException( InvalidArgumentException::class );
$this->expectExceptionMessage( 'The ability properties should provide a valid `show_in_rest` boolean.' );

new WP_Ability( self::$test_ability_name, $args );
}

/**
* Data provider for testing the execution of the ability.
*/
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/abilities-api/wpRegisterAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public function set_up(): void {
'meta' => array(
'category' => 'math',
),
'show_in_rest' => true,
);
}

Expand Down Expand Up @@ -147,6 +148,7 @@ public function test_register_valid_ability(): void {
$result->get_annotations()
);
$this->assertSame( self::$test_ability_args['meta'], $result->get_meta() );
$this->assertSame( self::$test_ability_args['show_in_rest'], $result->show_in_rest() );
$this->assertTrue(
$result->check_permissions(
array(
Expand Down
33 changes: 32 additions & 1 deletion tests/unit/rest-api/wpRestAbilitiesListController.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ private function register_test_abilities(): void {
'meta' => array(
'category' => 'math',
),
'show_in_rest' => true,
)
);

Expand Down Expand Up @@ -169,6 +170,20 @@ private function register_test_abilities(): void {
'meta' => array(
'category' => 'system',
),
'show_in_rest' => true,
)
);

// Ability that does not show in REST.
wp_register_ability(
'test/not-show-in-rest',
array(
'label' => 'Hidden from REST',
'description' => 'It does not show in REST.',
'execute_callback' => static function (): int {
return 0;
},
'permission_callback' => '__return_true',
)
);

Expand All @@ -183,6 +198,7 @@ private function register_test_abilities(): void {
return "Result from ability {$i}";
},
'permission_callback' => '__return_true',
'show_in_rest' => true,
)
);
}
Expand All @@ -206,6 +222,7 @@ public function test_get_items(): void {
$ability_names = wp_list_pluck( $data, 'name' );
$this->assertContains( 'test/calculator', $ability_names );
$this->assertContains( 'test/system-info', $ability_names );
$this->assertNotContains( 'test/not-show-in-rest', $ability_names );
}

/**
Expand Down Expand Up @@ -242,6 +259,19 @@ public function test_get_item_not_found(): void {
$this->assertEquals( 'rest_ability_not_found', $data['code'] );
}

/**
* Test getting an ability that does not show in REST returns 404.
*/
public function test_get_item_not_show_in_rest(): void {
$request = new WP_REST_Request( 'GET', '/wp/v2/abilities/test/not-show-in-rest' );
$response = $this->server->dispatch( $request );

$this->assertEquals( 404, $response->get_status() );

$data = $response->get_data();
$this->assertEquals( 'rest_ability_not_found', $data['code'] );
}

/**
* Test permission check for listing abilities.
*/
Expand Down Expand Up @@ -269,7 +299,7 @@ public function test_pagination_headers(): void {
$this->assertArrayHasKey( 'X-WP-Total', $headers );
$this->assertArrayHasKey( 'X-WP-TotalPages', $headers );

$total_abilities = count( wp_get_abilities() );
$total_abilities = count( wp_get_abilities() ) - 1; // Exclude the one that doesn't show in REST.
$this->assertEquals( $total_abilities, (int) $headers['X-WP-Total'] );
$this->assertEquals( ceil( $total_abilities / 10 ), (int) $headers['X-WP-TotalPages'] );
}
Expand Down Expand Up @@ -442,6 +472,7 @@ public function test_ability_name_with_valid_special_characters(): void {
return array( 'success' => true );
},
'permission_callback' => '__return_true',
'show_in_rest' => true,
)
);

Expand Down
Loading