diff --git a/src/wp-includes/abilities-api.php b/src/wp-includes/abilities-api.php index 4e98aaacdfb72..37cb1a28072f2 100644 --- a/src/wp-includes/abilities-api.php +++ b/src/wp-includes/abilities-api.php @@ -15,7 +15,7 @@ * - Define permission checks and execution callbacks. * - Organize abilities into logical categories. * - Validate inputs and outputs using JSON Schema. - * - Expose abilities through the REST API. + * - Expose abilities to clients such as the REST API. * * ## Working with Abilities * @@ -48,7 +48,7 @@ * 'required' => true, * ), * 'meta' => array( - * 'show_in_rest' => true, + * 'public' => true, * ), * ) * ); @@ -118,10 +118,10 @@ * 'execute_callback' => 'my_plugin_analyze_text', * 'permission_callback' => 'my_plugin_can_analyze_text', * 'meta' => array( - * 'annotations' => array( + * 'annotations' => array( * 'readonly' => true, * ), - * 'show_in_rest' => true, + * 'public' => true, * ), * ) * ); @@ -208,16 +208,24 @@ * return current_user_can( 'edit_posts' ); * } * - * ### REST API Integration + * ### Client Exposure * - * Abilities can be exposed through the REST API by setting `show_in_rest` - * to `true` in the meta configuration: + * Set the high-level `public` flag to make an ability available to clients + * such as the REST API, MCP, or AI agents: * * 'meta' => array( - * 'show_in_rest' => true, + * 'public' => true, * ), * - * This allows abilities to be invoked via HTTP requests to the WordPress REST API. + * The `public` flag seeds the default for each per-channel flag. For the REST + * API it seeds `show_in_rest`, which lets the ability be invoked via HTTP + * requests. Set a per-channel flag directly to override that default. For + * example, keep a public ability out of the REST API: + * + * 'meta' => array( + * 'public' => true, + * 'show_in_rest' => false, + * ), * * @since 6.9.0 * @@ -264,10 +272,10 @@ * @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 $public Optional. Whether the ability is meant to be available to + * clients such as the REST API, MCP, or AI agents. Seeds + * the default for per-channel flags like `$show_in_rest`. + * Defaults to false. * @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 is the value of `$public` when set, false otherwise. diff --git a/src/wp-includes/abilities-api/class-wp-abilities-registry.php b/src/wp-includes/abilities-api/class-wp-abilities-registry.php index 505ad56c8145d..7d1f26d7690ca 100644 --- a/src/wp-includes/abilities-api/class-wp-abilities-registry.php +++ b/src/wp-includes/abilities-api/class-wp-abilities-registry.php @@ -71,10 +71,10 @@ 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 $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 $public Optional. Whether the ability is meant to be available + * to clients such as the REST API, MCP, or AI agents. + * Seeds the default for per-channel flags like + * `$show_in_rest`. Defaults to false. * @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. * } @@ -125,9 +125,10 @@ public function register( string $name, array $args ): ?WP_Ability { * Optional. Additional metadata for the ability. * * @type array $annotations Optional. Annotation metadata for the ability. - * @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 $public Optional. Whether the ability is meant to be + * available to clients such as the REST API, MCP, or AI + * agents. Seeds the default for per-channel flags like + * `$show_in_rest`. Defaults to false. * @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. * } diff --git a/src/wp-includes/abilities-api/class-wp-ability.php b/src/wp-includes/abilities-api/class-wp-ability.php index 077a137a2b1c1..2127417a887b9 100644 --- a/src/wp-includes/abilities-api/class-wp-ability.php +++ b/src/wp-includes/abilities-api/class-wp-ability.php @@ -28,6 +28,14 @@ class WP_Ability { */ protected const DEFAULT_SHOW_IN_REST = false; + /** + * The default value for the `public` meta. + * + * @since 7.1.0 + * @var bool + */ + protected const DEFAULT_PUBLIC = false; + /** * The default ability annotations. * They are not guaranteed to provide a faithful description of ability behavior. @@ -160,10 +168,10 @@ 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 $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 $public Optional. Whether the ability is meant to be available + * to clients such as the REST API, MCP, or AI agents. + * Seeds the default for per-channel flags like + * `$show_in_rest`. Defaults to false. * @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. * } @@ -229,10 +237,10 @@ 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 $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 $public Optional. Whether the ability is meant to be available + * to clients such as the REST API, MCP, or AI agents. + * Seeds the default for per-channel flags like + * `$show_in_rest`. Defaults to false. * @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. * } @@ -262,8 +270,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 $public Whether the ability is intended to be publicly available - * to clients. Only present when provided during registration. + * @type bool $public Whether the ability is meant to be available to clients + * such as the REST API, MCP, or AI agents. Defaults to + * false. * @type bool $show_in_rest Whether to expose this ability in the REST API. * } * } @@ -348,17 +357,18 @@ protected function prepare_properties( array $args ): array { ) ); + $args['meta']['annotations'] = wp_parse_args( + $args['meta']['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 - ); + $args['meta']['public'] = $args['meta']['public'] ?? self::DEFAULT_PUBLIC; return $args; } diff --git a/src/wp-includes/abilities.php b/src/wp-includes/abilities.php index 0eb87a4581589..236b99836a3b9 100644 --- a/src/wp-includes/abilities.php +++ b/src/wp-includes/abilities.php @@ -132,12 +132,12 @@ function wp_register_core_abilities(): void { return current_user_can( 'manage_options' ); }, 'meta' => array( - 'annotations' => array( + 'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true, ), - 'show_in_rest' => true, + 'public' => true, ), ) ); @@ -256,12 +256,12 @@ function wp_register_core_abilities(): void { return is_user_logged_in(); }, 'meta' => array( - 'annotations' => array( + 'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true, ), - 'show_in_rest' => true, + 'public' => true, ), ) ); @@ -342,12 +342,12 @@ function wp_register_core_abilities(): void { return current_user_can( 'manage_options' ); }, 'meta' => array( - 'annotations' => array( + 'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true, ), - 'show_in_rest' => true, + 'public' => true, ), ) ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php index 5b7bca0b71138..21b417e83a1db 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php @@ -313,7 +313,7 @@ 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.' ), + 'description' => __( 'Whether the ability is meant to be available to clients such as the REST API, MCP, or AI agents. Defaults to false, but individual channel settings such as show_in_rest can override it.' ), 'type' => 'boolean', ), ), diff --git a/tests/phpunit/tests/abilities-api/wpAbility.php b/tests/phpunit/tests/abilities-api/wpAbility.php index 491d6337c08ea..f613736211f77 100644 --- a/tests/phpunit/tests/abilities-api/wpAbility.php +++ b/tests/phpunit/tests/abilities-api/wpAbility.php @@ -312,17 +312,21 @@ public function test_meta_explicit_show_in_rest_false_wins_over_public_true() { } /** - * Tests that `public` metadata is not added to the stored meta when not provided. + * Tests that `public` metadata defaults to false when not provided. * * @ticket 65568 */ - public function test_meta_public_is_not_defaulted_when_unset() { + public function test_meta_public_defaults_to_false_when_unset() { $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); - $this->assertArrayNotHasKey( + $this->assertArrayHasKey( 'public', $ability->get_meta(), - '`public` metadata should only be present when provided during registration.' + '`public` metadata should always be present in the stored meta.' + ); + $this->assertFalse( + $ability->get_meta_item( 'public' ), + '`public` metadata should default to false when not provided.' ); $this->assertFalse( $ability->get_meta_item( 'show_in_rest' ), @@ -330,6 +334,32 @@ public function test_meta_public_is_not_defaulted_when_unset() { ); } + /** + * Tests that a null `public` value is treated as unset. + * + * @ticket 65568 + */ + public function test_meta_public_null_is_treated_as_unset() { + $args = array_merge( + self::$test_ability_properties, + array( + 'meta' => array( + 'public' => null, + ), + ) + ); + $ability = new WP_Ability( self::$test_ability_name, $args ); + + $this->assertFalse( + $ability->get_meta_item( 'public' ), + 'A null `public` value should use the default value of false.' + ); + $this->assertFalse( + $ability->get_meta_item( 'show_in_rest' ), + '`show_in_rest` should use its default value when `public` is null.' + ); + } + /** * Tests that invalid `public` value throws an exception. * diff --git a/tests/phpunit/tests/abilities-api/wpRegisterAbility.php b/tests/phpunit/tests/abilities-api/wpRegisterAbility.php index 133343635cf59..348b1306ba61e 100644 --- a/tests/phpunit/tests/abilities-api/wpRegisterAbility.php +++ b/tests/phpunit/tests/abilities-api/wpRegisterAbility.php @@ -192,6 +192,7 @@ public function test_register_valid_ability(): void { array( 'annotations' => $expected_annotations, 'show_in_rest' => true, + 'public' => false, ) ); diff --git a/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php b/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php index 83011b727a994..9f15250022c7a 100644 --- a/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php +++ b/tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php @@ -61,6 +61,7 @@ public function test_core_get_site_info_ability_is_registered(): void { $ability = wp_get_ability( 'core/get-site-info' ); $this->assertInstanceOf( WP_Ability::class, $ability ); + $this->assertTrue( $ability->get_meta_item( 'public', false ) ); $this->assertTrue( $ability->get_meta_item( 'show_in_rest', false ) ); $input_schema = $ability->get_input_schema(); @@ -203,6 +204,7 @@ public function test_core_get_user_info_ability_is_registered(): void { $ability = wp_get_ability( 'core/get-user-info' ); $this->assertInstanceOf( WP_Ability::class, $ability ); + $this->assertTrue( $ability->get_meta_item( 'public', false ) ); $this->assertTrue( $ability->get_meta_item( 'show_in_rest', false ) ); $input_schema = $ability->get_input_schema(); @@ -308,6 +310,7 @@ public function test_core_get_environment_info_ability_is_registered(): void { $ability = wp_get_ability( 'core/get-environment-info' ); $this->assertInstanceOf( WP_Ability::class, $ability ); + $this->assertTrue( $ability->get_meta_item( 'public', false ) ); $this->assertTrue( $ability->get_meta_item( 'show_in_rest', false ) ); $input_schema = $ability->get_input_schema();