diff --git a/features/ability.feature b/features/ability.feature index 4ccaae4..b2d602d 100644 --- a/features/ability.feature +++ b/features/ability.feature @@ -384,18 +384,171 @@ Feature: Manage abilities registered via the WordPress Abilities API. } ); """ - When I run `wp ability list --namespace=test-plugin --show-in-rest=true --field=name` + When I run `wp ability list --namespace=test-plugin --show-in-rest --field=name` Then STDOUT should be: """ test-plugin/public-ability """ - When I run `wp ability list --namespace=test-plugin --show-in-rest=false --field=name` + When I run `wp ability list --namespace=test-plugin --no-show-in-rest --field=name` Then STDOUT should be: """ test-plugin/private-ability """ + @require-wp-7.1 + Scenario: Display the public flag and its precedence over channel flags. + Given a wp-content/mu-plugins/test-ability.php file: + """ + 'Test Category', + 'description' => 'A test category.', + ) ); + } ); + + add_action( 'wp_abilities_api_init', function() { + wp_register_ability( 'test-plugin/public-ability', array( + 'label' => 'Public Ability', + 'description' => 'A public ability.', + 'category' => 'test-category', + 'execute_callback' => function( $input ) { + return array( 'result' => 'done' ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( 'public' => true ), + ) ); + + wp_register_ability( 'test-plugin/public-not-rest', array( + 'label' => 'Public But Not REST', + 'description' => 'A public ability hidden from REST.', + 'category' => 'test-category', + 'execute_callback' => function( $input ) { + return array( 'result' => 'done' ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( 'public' => true, 'show_in_rest' => false ), + ) ); + + wp_register_ability( 'test-plugin/plain-ability', array( + 'label' => 'Plain Ability', + 'description' => 'An ability without exposure metadata.', + 'category' => 'test-category', + 'execute_callback' => function( $input ) { + return array( 'result' => 'done' ); + }, + 'permission_callback' => '__return_true', + ) ); + } ); + """ + + When I run `wp ability list --namespace=test-plugin --fields=name,public,show_in_rest` + Then STDOUT should be a table containing rows: + | name | public | show_in_rest | + | test-plugin/public-ability | 1 | 1 | + | test-plugin/public-not-rest | 1 | 0 | + | test-plugin/plain-ability | 0 | 0 | + + When I run `wp ability get test-plugin/public-ability --format=json` + Then STDOUT should be JSON containing: + """ + {"public":"1","show_in_rest":"1"} + """ + + When I run `wp ability get test-plugin/public-not-rest --format=json` + Then STDOUT should be JSON containing: + """ + {"public":"1","show_in_rest":"0"} + """ + + @require-wp-7.1 + Scenario: Filter abilities by public. + Given a wp-content/mu-plugins/test-ability.php file: + """ + 'Test Category', + 'description' => 'A test category.', + ) ); + } ); + + add_action( 'wp_abilities_api_init', function() { + wp_register_ability( 'test-plugin/public-ability', array( + 'label' => 'Public Ability', + 'description' => 'A public ability.', + 'category' => 'test-category', + 'execute_callback' => function( $input ) { + return array( 'result' => 'done' ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( 'public' => true ), + ) ); + + wp_register_ability( 'test-plugin/internal-ability', array( + 'label' => 'Internal Ability', + 'description' => 'An ability kept away from clients.', + 'category' => 'test-category', + 'execute_callback' => function( $input ) { + return array( 'result' => 'done' ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( 'public' => false ), + ) ); + } ); + """ + + When I run `wp ability list --namespace=test-plugin --public --field=name` + Then STDOUT should be: + """ + test-plugin/public-ability + """ + + When I run `wp ability list --namespace=test-plugin --no-public --field=name` + Then STDOUT should be: + """ + test-plugin/internal-ability + """ + + @require-wp-6.9 + Scenario: Inspect raw ability meta. + Given a wp-content/mu-plugins/test-ability.php file: + """ + 'Test Category', + 'description' => 'A test category.', + ) ); + } ); + + add_action( 'wp_abilities_api_init', function() { + wp_register_ability( 'test-plugin/channel-ability', array( + 'label' => 'Channel Ability', + 'description' => 'An ability with channel-specific metadata.', + 'category' => 'test-category', + 'execute_callback' => function( $input ) { + return array( 'result' => 'done' ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( 'mcp' => array( 'public' => false ) ), + ) ); + } ); + """ + + When I run `wp ability get test-plugin/channel-ability --field=meta` + Then STDOUT should contain: + """ + "mcp":{"public":false} + """ + + When I run `wp ability get test-plugin/channel-ability` + Then STDOUT should not contain: + """ + mcp + """ + @require-wp-6.9 Scenario: Display ability annotations. Given a wp-content/mu-plugins/test-ability.php file: diff --git a/src/Ability_Command.php b/src/Ability_Command.php index 48fc9a4..3a21351 100644 --- a/src/Ability_Command.php +++ b/src/Ability_Command.php @@ -28,7 +28,7 @@ * +---------------------------+----------------------+----------+------------------------------------------+ * * # Get details of a specific ability. - * $ wp ability get core/get-site-info --fields=name,label,category,readonly,show_in_rest + * $ wp ability get core/get-site-info --fields=name,label,category,readonly,public,show_in_rest * +---------------+----------------------+ * | Field | Value | * +---------------+----------------------+ @@ -36,6 +36,7 @@ * | label | Get Site Information | * | category | site | * | readonly | 1 | + * | public | 1 | * | show_in_rest | 1 | * +---------------+----------------------+ * @@ -92,6 +93,7 @@ class Ability_Command extends WP_CLI_Command { 'readonly', 'destructive', 'idempotent', + 'public', 'show_in_rest', ]; @@ -106,8 +108,11 @@ class Ability_Command extends WP_CLI_Command { * [--namespace=] * : Filter abilities by namespace prefix (e.g., 'core' for 'core/*' abilities). * - * [--show-in-rest=] - * : Filter abilities by REST API exposure. + * [--public] + * : Only list abilities flagged for client exposure. Pass --no-public to invert. + * + * [--show-in-rest] + * : Only list abilities exposed to the REST API. Pass --no-show-in-rest to invert. * * [--field=] * : Prints the value of a single field for each ability. @@ -142,8 +147,15 @@ class Ability_Command extends WP_CLI_Command { * * readonly * * destructive * * idempotent + * * public * * show_in_rest * + * The `public` field reports the high-level client exposure flag as declared + * by the ability. As of WordPress 7.1 it seeds the default for channel-specific + * flags such as `show_in_rest`, which take precedence when set explicitly. + * On earlier versions the flag is stored but has no effect, so `public` and + * `show_in_rest` may disagree. + * * ## EXAMPLES * * # List all abilities. @@ -162,7 +174,13 @@ class Ability_Command extends WP_CLI_Command { * $ wp ability list --namespace=core * * # List abilities exposed to REST API. - * $ wp ability list --show-in-rest=true + * $ wp ability list --show-in-rest + * + * # List abilities meant to be available to clients. + * $ wp ability list --public + * + * # Find abilities that opt out of REST despite being public. + * $ wp ability list --public --no-show-in-rest * * # List abilities as JSON. * $ wp ability list --format=json @@ -182,6 +200,7 @@ public function list_( $args, $assoc_args ): void { $abilities = wp_get_abilities(); $category_slug = Utils\get_flag_value( $assoc_args, 'category' ); $namespace = Utils\get_flag_value( $assoc_args, 'namespace' ); + $public = Utils\get_flag_value( $assoc_args, 'public' ); $show_in_rest = Utils\get_flag_value( $assoc_args, 'show-in-rest' ); $items = []; @@ -203,11 +222,18 @@ public function list_( $args, $assoc_args ): void { } } + // Filter by public if specified. + if ( null !== $public ) { + $ability_public = '1' === $ability_data['public']; + if ( $public !== $ability_public ) { + continue; + } + } + // Filter by show_in_rest if specified. if ( null !== $show_in_rest ) { - $show_in_rest_bool = filter_var( $show_in_rest, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); - $ability_rest = '1' === $ability_data['show_in_rest']; - if ( null !== $show_in_rest_bool && $show_in_rest_bool !== $ability_rest ) { + $ability_rest = '1' === $ability_data['show_in_rest']; + if ( $show_in_rest !== $ability_rest ) { continue; } } @@ -246,6 +272,8 @@ public function list_( $args, $assoc_args ): void { * * ## AVAILABLE FIELDS * + * These fields will be displayed by default: + * * * name * * label * * category @@ -255,8 +283,22 @@ public function list_( $args, $assoc_args ): void { * * readonly * * destructive * * idempotent + * * public * * show_in_rest * + * These fields are optionally available: + * + * * meta + * + * The `public` field reports the high-level client exposure flag as declared + * by the ability. As of WordPress 7.1 it seeds the default for channel-specific + * flags such as `show_in_rest`, which take precedence when set explicitly. + * On earlier versions the flag is stored but has no effect, so `public` and + * `show_in_rest` may disagree. + * + * The `meta` field renders the raw metadata as JSON. It is the only way to + * inspect channel-specific settings registered by plugins, such as `mcp`. + * * ## EXAMPLES * * # Get details of a specific ability. @@ -273,9 +315,14 @@ public function list_( $args, $assoc_args ): void { * | readonly | 1 | * | destructive | 0 | * | idempotent | 1 | + * | public | 1 | * | show_in_rest | 1 | * +---------------+----------------------+ * + * # Inspect channel-specific settings that have no field of their own. + * $ wp ability get my-plugin/my-ability --field=meta + * {"annotations":{"readonly":true,"destructive":false,"idempotent":null},"mcp":{"public":false},"show_in_rest":true,"public":true} + * * # Get ability as JSON. * $ wp ability get core/get-site-info --format=json * @@ -650,6 +697,7 @@ private function format_ability_for_list( $ability ) { 'readonly' => $this->format_annotation( $annotations['readonly'] ), 'destructive' => $this->format_annotation( $annotations['destructive'] ), 'idempotent' => $this->format_annotation( $annotations['idempotent'] ), + 'public' => $ability->get_meta_item( 'public', false ) ? '1' : '0', 'show_in_rest' => $ability->get_meta_item( 'show_in_rest', false ) ? '1' : '0', ]; } @@ -662,6 +710,7 @@ private function format_ability_for_list( $ability ) { */ private function format_ability_for_get( $ability ) { $annotations = $this->get_annotations( $ability ); + $meta = $ability->get_meta(); return [ 'name' => $ability->get_name(), @@ -673,7 +722,9 @@ private function format_ability_for_get( $ability ) { 'readonly' => $this->format_annotation( $annotations['readonly'] ), 'destructive' => $this->format_annotation( $annotations['destructive'] ), 'idempotent' => $this->format_annotation( $annotations['idempotent'] ), + 'public' => $ability->get_meta_item( 'public', false ) ? '1' : '0', 'show_in_rest' => $ability->get_meta_item( 'show_in_rest', false ) ? '1' : '0', + 'meta' => ! empty( $meta ) ? wp_json_encode( $meta ) : '{}', ]; }