Skip to content
Closed
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
6 changes: 5 additions & 1 deletion src/wp-includes/abilities-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,13 @@
* @type bool|null $idempotent Optional. If true, calling the ability repeatedly with the same arguments
* will have no additional effect on its environment.
* }
* @type bool $public Optional. Whether the ability is intended to be publicly
* available to clients. When true, channel-specific exposure
* flags such as `$show_in_rest` default to true. An explicitly
* set channel flag always takes precedence.
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API.
* When true, the ability can be invoked via HTTP requests.
* Default false.
* Default is the value of `$public` when set, false otherwise.
* }
* @type string $ability_class Optional. Fully-qualified custom class name to instantiate
* instead of the default `WP_Ability` class. The custom class
Expand Down
13 changes: 11 additions & 2 deletions src/wp-includes/abilities-api/class-wp-abilities-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ final class WP_Abilities_Registry {
* @type bool|null $idempotent Optional. If true, calling the ability repeatedly with the same arguments
* will have no additional effect on its environment.
* }
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
* @type bool $public Optional. Whether the ability is intended to be publicly
* available to clients. When true, channel-specific exposure
* flags such as `$show_in_rest` default to true. An explicitly
* set channel flag always takes precedence.
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API.
* Default is the value of `$public` when set, false otherwise.
* }
* @type string $ability_class Optional. Custom class to instantiate instead of WP_Ability.
* }
Expand Down Expand Up @@ -120,7 +125,11 @@ public function register( string $name, array $args ): ?WP_Ability {
* Optional. Additional metadata for the ability.
*
* @type array<string, bool|string> $annotations Optional. Annotation metadata for the ability.
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
* @type bool $public Optional. Whether the ability is intended to be
* publicly available to clients. Seeds the default for
* channel-specific exposure flags like `$show_in_rest`.
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API.
* Default is the value of `$public` when set, false otherwise.
* }
* @type string $ability_class Optional. Custom class to instantiate instead of WP_Ability.
* }
Expand Down
37 changes: 31 additions & 6 deletions src/wp-includes/abilities-api/class-wp-ability.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,12 @@ class WP_Ability {
* @type bool|null $idempotent Optional. If true, calling the ability repeatedly with the same arguments
* will have no additional effect on its environment.
* }
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
* @type bool $public Optional. Whether the ability is intended to be publicly
* available to clients. When true, channel-specific exposure
* flags such as `$show_in_rest` default to true. An explicitly
* set channel flag always takes precedence.
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API.
* Default is the value of `$public` when set, false otherwise.
* }
* }
*/
Expand Down Expand Up @@ -224,7 +229,12 @@ public function __construct( string $name, array $args ) {
* @type bool|null $idempotent Optional. If true, calling the ability repeatedly with the same arguments
* will have no additional effect on its environment.
* }
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
* @type bool $public Optional. Whether the ability is intended to be publicly
* available to clients. When true, channel-specific exposure
* flags such as `$show_in_rest` default to true. An explicitly
* set channel flag always takes precedence.
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API.
* Default is the value of `$public` when set, false otherwise.
* }
* }
* @return array<string, mixed> {
Expand Down Expand Up @@ -252,7 +262,9 @@ public function __construct( string $name, array $args ) {
* @type bool|null $idempotent If true, calling the ability repeatedly with the same arguments
* will have no additional effect on its environment.
* }
* @type bool $show_in_rest Whether to expose this ability in the REST API. Default false.
* @type bool $public Whether the ability is intended to be publicly available
* to clients. Only present when provided during registration.
* @type bool $show_in_rest Whether to expose this ability in the REST API.
* }
* }
* @throws InvalidArgumentException if an argument is invalid.
Expand Down Expand Up @@ -322,14 +334,27 @@ protected function prepare_properties( array $args ): array {
);
}

if ( isset( $args['meta']['public'] ) && ! is_bool( $args['meta']['public'] ) ) {
throw new InvalidArgumentException(
__( 'The ability meta should provide a valid `public` boolean.' )
);
}

// Set defaults for optional meta.
$args['meta'] = wp_parse_args(
$args['meta'] = wp_parse_args(
$args['meta'] ?? array(),
array(
'annotations' => static::$default_annotations,
'show_in_rest' => self::DEFAULT_SHOW_IN_REST,
'annotations' => static::$default_annotations,
)
);

/*
* Resolve `show_in_rest` from most specific to least specific: an explicit
* `show_in_rest` value wins, then the high-level `public` flag seeds the
* default, then the built-in default applies.
*/
$args['meta']['show_in_rest'] = $args['meta']['show_in_rest'] ?? $args['meta']['public'] ?? self::DEFAULT_SHOW_IN_REST;

$args['meta']['annotations'] = wp_parse_args(
$args['meta']['annotations'],
static::$default_annotations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,10 @@ public function get_item_schema(): array {
),
'additionalProperties' => true,
),
'public' => array(
'description' => __( 'Whether the ability author intends the ability to be publicly available to clients. Only present when set by the ability author.' ),
'type' => 'boolean',
),
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
Expand Down
18 changes: 17 additions & 1 deletion tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,6 @@ public function test_register_incorrect_output_schema_type() {
$this->assertNull( $result );
}


/**
* Should reject ability registration with invalid `annotations` type.
*
Expand Down Expand Up @@ -410,6 +409,23 @@ public function test_register_invalid_show_in_rest_type() {
$this->assertNull( $result );
}

/**
* Should reject ability registration with invalid public type.
*
* @ticket 65568
*
* @covers WP_Abilities_Registry::register
* @covers WP_Ability::prepare_properties
*
* @expectedIncorrectUsage WP_Abilities_Registry::register
*/
public function test_register_invalid_public_type() {
self::$test_ability_args['meta']['public'] = 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
89 changes: 89 additions & 0 deletions tests/phpunit/tests/abilities-api/wpAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,95 @@ public function test_show_in_rest_throws_exception() {
new WP_Ability( self::$test_ability_name, $args );
}

/**
* Tests that `public` metadata seeds `show_in_rest` to true when it is not set explicitly.
*
* @ticket 65568
*/
public function test_meta_public_true_defaults_show_in_rest_to_true() {
$args = array_merge(
self::$test_ability_properties,
array(
'meta' => array(
'public' => true,
),
)
);
$ability = new WP_Ability( self::$test_ability_name, $args );

$this->assertTrue(
$ability->get_meta_item( 'show_in_rest' ),
'`show_in_rest` metadata should default to the `public` value.'
);
$this->assertTrue(
$ability->get_meta_item( 'public' ),
'`public` metadata should be stored as provided.'
);
}

/**
* Tests that an explicit `show_in_rest` value of false wins over `public` set to true.
*
* @ticket 65568
*/
public function test_meta_explicit_show_in_rest_false_wins_over_public_true() {
$args = array_merge(
self::$test_ability_properties,
array(
'meta' => array(
'public' => true,
'show_in_rest' => false,
),
)
);
$ability = new WP_Ability( self::$test_ability_name, $args );

$this->assertFalse(
$ability->get_meta_item( 'show_in_rest' ),
'An explicit `show_in_rest` value of false should win over `public` set to true.'
);
}

/**
* Tests that `public` metadata is not added to the stored meta when not provided.
*
* @ticket 65568
*/
public function test_meta_public_is_not_defaulted_when_unset() {
$ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties );

$this->assertArrayNotHasKey(
'public',
$ability->get_meta(),
'`public` metadata should only be present when provided during registration.'
);
$this->assertFalse(
$ability->get_meta_item( 'show_in_rest' ),
'`show_in_rest` metadata should still default to false.'
);
}

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

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

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

/**
* Data provider for testing the execution of the ability.
*
Expand Down
76 changes: 76 additions & 0 deletions tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,66 @@ public function test_get_item_not_show_in_rest(): void {
$this->assertSame( 'rest_ability_not_found', $data['code'] );
}

/**
* Test that an ability with only the `public` meta flag is exposed in REST.
*
* @ticket 65568
*/
public function test_get_item_public_meta_exposes_in_rest(): void {
$this->register_test_ability(
'test/public-ability',
array(
'label' => 'Public Ability',
'description' => 'Exposed in REST via the public meta flag.',
'category' => 'general',
'execute_callback' => '__return_true',
'permission_callback' => '__return_true',
'meta' => array(
'public' => true,
),
)
);

$request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/public-ability' );
$response = $this->server->dispatch( $request );

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

$data = $response->get_data();
$this->assertTrue( $data['meta']['public'] );
$this->assertTrue( $data['meta']['show_in_rest'] );
}

/**
* Test that an explicit `show_in_rest` value of false hides an ability even when `public` is true.
*
* @ticket 65568
*/
public function test_get_item_public_true_show_in_rest_false_is_hidden(): void {
$this->register_test_ability(
'test/public-optout',
array(
'label' => 'Public Opt-out',
'description' => 'Opts out of REST exposure despite the public meta flag.',
'category' => 'general',
'execute_callback' => '__return_true',
'permission_callback' => '__return_true',
'meta' => array(
'public' => true,
'show_in_rest' => false,
),
)
);

$request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/public-optout' );
$response = $this->server->dispatch( $request );

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

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

/**
* Test permission check for listing abilities.
*
Expand Down Expand Up @@ -622,6 +682,22 @@ public function test_get_schema(): void {
$this->assertArrayHasKey( 'category', $properties );
}

/**
* Test that the item schema declares the `public` meta property.
*
* @ticket 65568
*/
public function test_get_schema_meta_declares_public(): void {
$request = new WP_REST_Request( 'OPTIONS', '/wp-abilities/v1/abilities' );
$response = $this->server->dispatch( $request );
$data = $response->get_data();

$meta_properties = $data['schema']['properties']['meta']['properties'];

$this->assertArrayHasKey( 'public', $meta_properties );
$this->assertSame( 'boolean', $meta_properties['public']['type'] );
}

/**
* Test ability name with valid special characters.
*
Expand Down
29 changes: 29 additions & 0 deletions tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,35 @@ public function test_do_not_show_in_rest(): void {
$this->assertSame( 'Ability not found.', $data['message'] );
}

/**
* Test running an ability exposed in REST via the `public` meta flag.
*
* @ticket 65568
*/
public function test_run_public_meta_ability_is_executable(): void {
$this->register_test_ability(
'test/public-ability',
array(
'label' => 'Public Ability',
'description' => 'Exposed in REST via the public meta flag.',
'category' => 'general',
'execute_callback' => '__return_true',
'permission_callback' => '__return_true',
'meta' => array(
'public' => true,
),
)
);

$request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/public-ability/run' );
$request->set_header( 'Content-Type', 'application/json' );

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

$this->assertEquals( 200, $response->get_status() );
$this->assertTrue( $response->get_data() );
}

/**
* Test handling of null is a valid return value.
*
Expand Down
Loading