Skip to content

Commit a1d2d15

Browse files
committed
Add public meta flag to control exposure defaults for abilities
Introduce a `public` flag in ability metadata as a single high-level control over client exposure. When set, `public` seeds the default for channel-specific flags such as `show_in_rest`: an explicit `show_in_rest` value wins, then `public`, then the built-in default of `false`. This lets an ability opt in to all channels at once, while keeping the ability to opt out of a single channel. The `public` value is stored in the ability metadata and declared in the REST item schema, so other channels such as MCP and AI agents can read it through the `wp_register_ability_args` filter. Props iamadisingh, gziolo. Fixes #65568. git-svn-id: https://develop.svn.wordpress.org/trunk@62729 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 284bbcf commit a1d2d15

8 files changed

Lines changed: 262 additions & 10 deletions

File tree

‎src/wp-includes/abilities-api.php‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,13 @@
264264
* @type bool|null $idempotent Optional. If true, calling the ability repeatedly with the same arguments
265265
* will have no additional effect on its environment.
266266
* }
267+
* @type bool $public Optional. Whether the ability is intended to be publicly
268+
* available to clients. When true, channel-specific exposure
269+
* flags such as `$show_in_rest` default to true. An explicitly
270+
* set channel flag always takes precedence.
267271
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API.
268272
* When true, the ability can be invoked via HTTP requests.
269-
* Default false.
273+
* Default is the value of `$public` when set, false otherwise.
270274
* }
271275
* @type string $ability_class Optional. Fully-qualified custom class name to instantiate
272276
* instead of the default `WP_Ability` class. The custom class

‎src/wp-includes/abilities-api/class-wp-abilities-registry.php‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,12 @@ final class WP_Abilities_Registry {
7171
* @type bool|null $idempotent Optional. If true, calling the ability repeatedly with the same arguments
7272
* will have no additional effect on its environment.
7373
* }
74-
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
74+
* @type bool $public Optional. Whether the ability is intended to be publicly
75+
* available to clients. When true, channel-specific exposure
76+
* flags such as `$show_in_rest` default to true. An explicitly
77+
* set channel flag always takes precedence.
78+
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API.
79+
* Default is the value of `$public` when set, false otherwise.
7580
* }
7681
* @type string $ability_class Optional. Custom class to instantiate instead of WP_Ability.
7782
* }
@@ -120,7 +125,11 @@ public function register( string $name, array $args ): ?WP_Ability {
120125
* Optional. Additional metadata for the ability.
121126
*
122127
* @type array<string, bool|string> $annotations Optional. Annotation metadata for the ability.
123-
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
128+
* @type bool $public Optional. Whether the ability is intended to be
129+
* publicly available to clients. Seeds the default for
130+
* channel-specific exposure flags like `$show_in_rest`.
131+
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API.
132+
* Default is the value of `$public` when set, false otherwise.
124133
* }
125134
* @type string $ability_class Optional. Custom class to instantiate instead of WP_Ability.
126135
* }

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

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,12 @@ class WP_Ability {
160160
* @type bool|null $idempotent Optional. If true, calling the ability repeatedly with the same arguments
161161
* will have no additional effect on its environment.
162162
* }
163-
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
163+
* @type bool $public Optional. Whether the ability is intended to be publicly
164+
* available to clients. When true, channel-specific exposure
165+
* flags such as `$show_in_rest` default to true. An explicitly
166+
* set channel flag always takes precedence.
167+
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API.
168+
* Default is the value of `$public` when set, false otherwise.
164169
* }
165170
* }
166171
*/
@@ -224,7 +229,12 @@ public function __construct( string $name, array $args ) {
224229
* @type bool|null $idempotent Optional. If true, calling the ability repeatedly with the same arguments
225230
* will have no additional effect on its environment.
226231
* }
227-
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false.
232+
* @type bool $public Optional. Whether the ability is intended to be publicly
233+
* available to clients. When true, channel-specific exposure
234+
* flags such as `$show_in_rest` default to true. An explicitly
235+
* set channel flag always takes precedence.
236+
* @type bool $show_in_rest Optional. Whether to expose this ability in the REST API.
237+
* Default is the value of `$public` when set, false otherwise.
228238
* }
229239
* }
230240
* @return array<string, mixed> {
@@ -252,7 +262,9 @@ public function __construct( string $name, array $args ) {
252262
* @type bool|null $idempotent If true, calling the ability repeatedly with the same arguments
253263
* will have no additional effect on its environment.
254264
* }
255-
* @type bool $show_in_rest Whether to expose this ability in the REST API. Default false.
265+
* @type bool $public Whether the ability is intended to be publicly available
266+
* to clients. Only present when provided during registration.
267+
* @type bool $show_in_rest Whether to expose this ability in the REST API.
256268
* }
257269
* }
258270
* @throws InvalidArgumentException if an argument is invalid.
@@ -322,14 +334,27 @@ protected function prepare_properties( array $args ): array {
322334
);
323335
}
324336

337+
if ( isset( $args['meta']['public'] ) && ! is_bool( $args['meta']['public'] ) ) {
338+
throw new InvalidArgumentException(
339+
__( 'The ability meta should provide a valid `public` boolean.' )
340+
);
341+
}
342+
325343
// Set defaults for optional meta.
326-
$args['meta'] = wp_parse_args(
344+
$args['meta'] = wp_parse_args(
327345
$args['meta'] ?? array(),
328346
array(
329-
'annotations' => static::$default_annotations,
330-
'show_in_rest' => self::DEFAULT_SHOW_IN_REST,
347+
'annotations' => static::$default_annotations,
331348
)
332349
);
350+
351+
/*
352+
* Resolve `show_in_rest` from most specific to least specific: an explicit
353+
* `show_in_rest` value wins, then the high-level `public` flag seeds the
354+
* default, then the built-in default applies.
355+
*/
356+
$args['meta']['show_in_rest'] = $args['meta']['show_in_rest'] ?? $args['meta']['public'] ?? self::DEFAULT_SHOW_IN_REST;
357+
333358
$args['meta']['annotations'] = wp_parse_args(
334359
$args['meta']['annotations'],
335360
static::$default_annotations

‎src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,10 @@ public function get_item_schema(): array {
312312
),
313313
'additionalProperties' => true,
314314
),
315+
'public' => array(
316+
'description' => __( 'Whether the ability author intends the ability to be publicly available to clients. Only present when set by the ability author.' ),
317+
'type' => 'boolean',
318+
),
315319
),
316320
'context' => array( 'view', 'edit' ),
317321
'readonly' => true,

‎tests/phpunit/tests/abilities-api/wpAbilitiesRegistry.php‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,6 @@ public function test_register_incorrect_output_schema_type() {
358358
$this->assertNull( $result );
359359
}
360360

361-
362361
/**
363362
* Should reject ability registration with invalid `annotations` type.
364363
*
@@ -410,6 +409,23 @@ public function test_register_invalid_show_in_rest_type() {
410409
$this->assertNull( $result );
411410
}
412411

412+
/**
413+
* Should reject ability registration with invalid public type.
414+
*
415+
* @ticket 65568
416+
*
417+
* @covers WP_Abilities_Registry::register
418+
* @covers WP_Ability::prepare_properties
419+
*
420+
* @expectedIncorrectUsage WP_Abilities_Registry::register
421+
*/
422+
public function test_register_invalid_public_type() {
423+
self::$test_ability_args['meta']['public'] = 5;
424+
425+
$result = $this->registry->register( self::$test_ability_name, self::$test_ability_args );
426+
$this->assertNull( $result );
427+
}
428+
413429
/**
414430
* Should reject registration for already registered ability.
415431
*

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

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,95 @@ public function test_show_in_rest_throws_exception() {
262262
new WP_Ability( self::$test_ability_name, $args );
263263
}
264264

265+
/**
266+
* Tests that `public` metadata seeds `show_in_rest` to true when it is not set explicitly.
267+
*
268+
* @ticket 65568
269+
*/
270+
public function test_meta_public_true_defaults_show_in_rest_to_true() {
271+
$args = array_merge(
272+
self::$test_ability_properties,
273+
array(
274+
'meta' => array(
275+
'public' => true,
276+
),
277+
)
278+
);
279+
$ability = new WP_Ability( self::$test_ability_name, $args );
280+
281+
$this->assertTrue(
282+
$ability->get_meta_item( 'show_in_rest' ),
283+
'`show_in_rest` metadata should default to the `public` value.'
284+
);
285+
$this->assertTrue(
286+
$ability->get_meta_item( 'public' ),
287+
'`public` metadata should be stored as provided.'
288+
);
289+
}
290+
291+
/**
292+
* Tests that an explicit `show_in_rest` value of false wins over `public` set to true.
293+
*
294+
* @ticket 65568
295+
*/
296+
public function test_meta_explicit_show_in_rest_false_wins_over_public_true() {
297+
$args = array_merge(
298+
self::$test_ability_properties,
299+
array(
300+
'meta' => array(
301+
'public' => true,
302+
'show_in_rest' => false,
303+
),
304+
)
305+
);
306+
$ability = new WP_Ability( self::$test_ability_name, $args );
307+
308+
$this->assertFalse(
309+
$ability->get_meta_item( 'show_in_rest' ),
310+
'An explicit `show_in_rest` value of false should win over `public` set to true.'
311+
);
312+
}
313+
314+
/**
315+
* Tests that `public` metadata is not added to the stored meta when not provided.
316+
*
317+
* @ticket 65568
318+
*/
319+
public function test_meta_public_is_not_defaulted_when_unset() {
320+
$ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties );
321+
322+
$this->assertArrayNotHasKey(
323+
'public',
324+
$ability->get_meta(),
325+
'`public` metadata should only be present when provided during registration.'
326+
);
327+
$this->assertFalse(
328+
$ability->get_meta_item( 'show_in_rest' ),
329+
'`show_in_rest` metadata should still default to false.'
330+
);
331+
}
332+
333+
/**
334+
* Tests that invalid `public` value throws an exception.
335+
*
336+
* @ticket 65568
337+
*/
338+
public function test_meta_public_throws_exception_for_non_boolean() {
339+
$args = array_merge(
340+
self::$test_ability_properties,
341+
array(
342+
'meta' => array(
343+
'public' => 5,
344+
),
345+
)
346+
);
347+
348+
$this->expectException( InvalidArgumentException::class );
349+
$this->expectExceptionMessage( 'The ability meta should provide a valid `public` boolean.' );
350+
351+
new WP_Ability( self::$test_ability_name, $args );
352+
}
353+
265354
/**
266355
* Data provider for testing the execution of the ability.
267356
*

‎tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,66 @@ public function test_get_item_not_show_in_rest(): void {
414414
$this->assertSame( 'rest_ability_not_found', $data['code'] );
415415
}
416416

417+
/**
418+
* Test that an ability with only the `public` meta flag is exposed in REST.
419+
*
420+
* @ticket 65568
421+
*/
422+
public function test_get_item_public_meta_exposes_in_rest(): void {
423+
$this->register_test_ability(
424+
'test/public-ability',
425+
array(
426+
'label' => 'Public Ability',
427+
'description' => 'Exposed in REST via the public meta flag.',
428+
'category' => 'general',
429+
'execute_callback' => '__return_true',
430+
'permission_callback' => '__return_true',
431+
'meta' => array(
432+
'public' => true,
433+
),
434+
)
435+
);
436+
437+
$request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/public-ability' );
438+
$response = $this->server->dispatch( $request );
439+
440+
$this->assertEquals( 200, $response->get_status() );
441+
442+
$data = $response->get_data();
443+
$this->assertTrue( $data['meta']['public'] );
444+
$this->assertTrue( $data['meta']['show_in_rest'] );
445+
}
446+
447+
/**
448+
* Test that an explicit `show_in_rest` value of false hides an ability even when `public` is true.
449+
*
450+
* @ticket 65568
451+
*/
452+
public function test_get_item_public_true_show_in_rest_false_is_hidden(): void {
453+
$this->register_test_ability(
454+
'test/public-optout',
455+
array(
456+
'label' => 'Public Opt-out',
457+
'description' => 'Opts out of REST exposure despite the public meta flag.',
458+
'category' => 'general',
459+
'execute_callback' => '__return_true',
460+
'permission_callback' => '__return_true',
461+
'meta' => array(
462+
'public' => true,
463+
'show_in_rest' => false,
464+
),
465+
)
466+
);
467+
468+
$request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/public-optout' );
469+
$response = $this->server->dispatch( $request );
470+
471+
$this->assertEquals( 404, $response->get_status() );
472+
473+
$data = $response->get_data();
474+
$this->assertSame( 'rest_ability_not_found', $data['code'] );
475+
}
476+
417477
/**
418478
* Test permission check for listing abilities.
419479
*
@@ -622,6 +682,22 @@ public function test_get_schema(): void {
622682
$this->assertArrayHasKey( 'category', $properties );
623683
}
624684

685+
/**
686+
* Test that the item schema declares the `public` meta property.
687+
*
688+
* @ticket 65568
689+
*/
690+
public function test_get_schema_meta_declares_public(): void {
691+
$request = new WP_REST_Request( 'OPTIONS', '/wp-abilities/v1/abilities' );
692+
$response = $this->server->dispatch( $request );
693+
$data = $response->get_data();
694+
695+
$meta_properties = $data['schema']['properties']['meta']['properties'];
696+
697+
$this->assertArrayHasKey( 'public', $meta_properties );
698+
$this->assertSame( 'boolean', $meta_properties['public']['type'] );
699+
}
700+
625701
/**
626702
* Test ability name with valid special characters.
627703
*

‎tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,35 @@ public function test_do_not_show_in_rest(): void {
650650
$this->assertSame( 'Ability not found.', $data['message'] );
651651
}
652652

653+
/**
654+
* Test running an ability exposed in REST via the `public` meta flag.
655+
*
656+
* @ticket 65568
657+
*/
658+
public function test_run_public_meta_ability_is_executable(): void {
659+
$this->register_test_ability(
660+
'test/public-ability',
661+
array(
662+
'label' => 'Public Ability',
663+
'description' => 'Exposed in REST via the public meta flag.',
664+
'category' => 'general',
665+
'execute_callback' => '__return_true',
666+
'permission_callback' => '__return_true',
667+
'meta' => array(
668+
'public' => true,
669+
),
670+
)
671+
);
672+
673+
$request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/public-ability/run' );
674+
$request->set_header( 'Content-Type', 'application/json' );
675+
676+
$response = $this->server->dispatch( $request );
677+
678+
$this->assertEquals( 200, $response->get_status() );
679+
$this->assertTrue( $response->get_data() );
680+
}
681+
653682
/**
654683
* Test handling of null is a valid return value.
655684
*

0 commit comments

Comments
 (0)