-
Notifications
You must be signed in to change notification settings - Fork 50
Add initial core abilities for WordPress 6.9 #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cfff125
1957efb
299ecc9
7f928ef
19f665a
033f653
dbf97f7
28b6040
93e85df
53d37fb
6dced81
4dce755
00fa5c1
afd83c9
766490b
8f09665
b7b69a1
73a2092
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,317 @@ | ||
| <?php | ||
| /** | ||
| * Core Abilities registration. | ||
| * | ||
| * @package WordPress | ||
| * @subpackage Abilities_API | ||
| * @since 0.3.0 | ||
| */ | ||
|
|
||
| declare( strict_types = 1 ); | ||
|
|
||
| /** | ||
| * Registers the default core abilities that ship with the Abilities API. | ||
| * | ||
| * @since 0.3.0 | ||
| */ | ||
| // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Core class intended for WordPress core. | ||
| final class WP_Core_Abilities { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the value in putting these in a class? It feels like this could all be done in more straightforward functions.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be perfectly fine to introduce two functions or static methods and attach them inside add_filter( 'wp_abilities_api_categories_init', 'register_core_ability_categories' );
add_filter( 'wp_abilities_api_init', 'register_core_abilities' ); |
||
| /** | ||
| * Category slugs for core abilities. | ||
| * | ||
| * @since 0.3.0 | ||
| */ | ||
| public const CATEGORY_SITE = 'site'; | ||
| public const CATEGORY_USER = 'user'; | ||
Jameswlepage marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /** | ||
| * Registers the core abilities categories. | ||
| * | ||
| * @since 0.3.0 | ||
| * | ||
| * @return void | ||
| */ | ||
| public static function register_category(): void { | ||
| // Site-related capabilities | ||
| wp_register_ability_category( | ||
| self::CATEGORY_SITE, | ||
| array( | ||
| 'label' => __( 'Site' ), | ||
| 'description' => __( 'Abilities that retrieve or modify site information and settings.' ), | ||
| ) | ||
| ); | ||
|
|
||
| // User-related capabilities | ||
| wp_register_ability_category( | ||
| self::CATEGORY_USER, | ||
| array( | ||
| 'label' => __( 'User' ), | ||
| 'description' => __( 'Abilities that retrieve or modify user information and settings.' ), | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Registers the default core abilities. | ||
| * | ||
| * @since 0.3.0 | ||
| * | ||
| * @return void | ||
| */ | ||
| public static function register(): void { | ||
| self::register_get_site_info(); | ||
| self::register_get_user_info(); | ||
| self::register_get_environment_info(); | ||
| } | ||
|
|
||
| /** | ||
| * Registers the `core/get-site-info` ability. | ||
| * | ||
| * @since 0.3.0 | ||
| * | ||
| * @return void | ||
| */ | ||
| protected static function register_get_site_info(): void { | ||
| $fields = array( | ||
| 'name', | ||
| 'description', | ||
| 'url', | ||
| 'wpurl', | ||
| 'admin_email', | ||
| 'charset', | ||
| 'language', | ||
| 'version', | ||
| ); | ||
|
|
||
| wp_register_ability( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to be duplicating the site settings REST endpoint. What is the value of exposing this here when there is already a way to expose this information.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great Q! If you're asking from a more philosophical POV ("why do Abilities even exist"), then the answer is similar to "What is the value of having REST endpoints when there's already a php function and ajax?". The tl;dr goal of the Abilities API is to create a sort of "Generic API" abstraction + registry, the idea being that instead of needed to reregister/expose the same functionality to various last-mile APIs (REST, admin-ajax, graphql, PHP in theme/plugins, MCP/A2A, Command Palette, and whatever future tech integrations that come up with). It's standardized and reliable, like hooks but for functionality, so all anybody needs is a generic adapter and it'l work without any domain knowledge of the ability itself. In fact, I'd hope and expect to see the internals of many of our existing REST endpoints updated to internally use the Abilities API in 7.0 and beyond. If you're asking from a more practical POV (e.g. regarding the shape of the input/output schemas), I think we're open to evolution here. I believe we want something that it generic to make sense in multiple context, and not just a single one of either REST, MCP, or Command Palette. |
||
| 'core/get-site-info', | ||
| array( | ||
| 'label' => __( 'Get Site Information' ), | ||
| 'description' => __( 'Returns site information configured in WordPress. By default returns all fields, or optionally a filtered subset.' ), | ||
| 'category' => self::CATEGORY_SITE, | ||
| 'input_schema' => array( | ||
| 'type' => 'object', | ||
gziolo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 'properties' => array( | ||
| 'fields' => array( | ||
| 'type' => 'array', | ||
| 'items' => array( | ||
| 'type' => 'string', | ||
| 'enum' => $fields, | ||
| ), | ||
| 'description' => __( 'Optional: Limit response to specific fields. If omitted, all fields are returned.' ), | ||
| ), | ||
| ), | ||
| 'additionalProperties' => false, | ||
| 'default' => array(), | ||
| ), | ||
| 'output_schema' => array( | ||
| 'type' => 'object', | ||
| 'properties' => array( | ||
| 'name' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The site title.' ), | ||
| ), | ||
| 'description' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The site tagline.' ), | ||
| ), | ||
| 'url' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The site home URL.' ), | ||
| ), | ||
| 'wpurl' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The WordPress installation URL.' ), | ||
| ), | ||
| 'admin_email' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The site administrator email address.' ), | ||
| ), | ||
| 'charset' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The site character encoding.' ), | ||
| ), | ||
| 'language' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The site language locale code.' ), | ||
| ), | ||
| 'version' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The WordPress version.' ), | ||
| ), | ||
| ), | ||
| 'additionalProperties' => false, | ||
| ), | ||
| 'execute_callback' => static function ( $input = array() ): array { | ||
| $input = is_array( $input ) ? $input : array(); | ||
| $all_fields = array( 'name', 'description', 'url', 'wpurl', 'admin_email', 'charset', 'language', 'version' ); | ||
| $requested_fields = ! empty( $input['fields'] ) ? $input['fields'] : $all_fields; | ||
|
|
||
| $result = array(); | ||
| foreach ( $requested_fields as $field ) { | ||
| $result[ $field ] = get_bloginfo( $field ); | ||
| } | ||
|
|
||
| return $result; | ||
| }, | ||
| 'permission_callback' => static function (): bool { | ||
| return current_user_can( 'manage_options' ); | ||
| }, | ||
| 'meta' => array( | ||
| 'annotations' => array( | ||
| 'readonly' => true, | ||
| 'destructive' => false, | ||
| 'idempotent' => true, | ||
| ), | ||
| 'show_in_rest' => true, | ||
| ), | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Registers the `core/get-user-info` ability. | ||
| * | ||
| * @since 0.3.0 | ||
| * | ||
| * @return void | ||
| */ | ||
| protected static function register_get_user_info(): void { | ||
| wp_register_ability( | ||
| 'core/get-user-info', | ||
| array( | ||
| 'label' => __( 'Get User Information' ), | ||
| 'description' => __( 'Returns basic profile details for the current authenticated user to support personalization, auditing, and access-aware behavior.' ), | ||
| 'category' => self::CATEGORY_USER, | ||
| 'output_schema' => array( | ||
| 'type' => 'object', | ||
| 'required' => array( 'id', 'display_name', 'user_nicename', 'user_login', 'roles', 'locale' ), | ||
| 'properties' => array( | ||
| 'id' => array( | ||
| 'type' => 'integer', | ||
| 'description' => __( 'The user ID.' ), | ||
| ), | ||
| 'display_name' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The display name of the user.' ), | ||
| ), | ||
| 'user_nicename' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The URL-friendly name for the user.' ), | ||
| ), | ||
| 'user_login' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The login username for the user.' ), | ||
| ), | ||
| 'roles' => array( | ||
| 'type' => 'array', | ||
| 'description' => __( 'The roles assigned to the user.' ), | ||
| 'items' => array( | ||
| 'type' => 'string', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Roles is an array, not a string. See: https://core.trac.wordpress.org/browser/trunk/src/wp-includes/class-wp-user.php#L82
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @aaronjorbin, what we have seems correct, roles is of type array with each item being a string. Feel free to correct me if I'm missing anything. |
||
| ), | ||
| ), | ||
| 'locale' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The locale string for the user, such as en_US.' ), | ||
| ), | ||
| ), | ||
| 'additionalProperties' => false, | ||
| ), | ||
| 'execute_callback' => static function (): array { | ||
| $current_user = wp_get_current_user(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is going to be interesting to see how it plays out in practice with AI clients that use some access tokens. It might happen that this will report back as an agent user 😄
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Related: #108 (comment) Switching to |
||
|
|
||
| return array( | ||
| 'id' => $current_user->ID, | ||
| 'display_name' => $current_user->display_name, | ||
| 'user_nicename' => $current_user->user_nicename, | ||
| 'user_login' => $current_user->user_login, | ||
| 'roles' => $current_user->roles, | ||
| 'locale' => get_user_locale( $current_user ), | ||
| ); | ||
| }, | ||
| 'permission_callback' => static function (): bool { | ||
| return is_user_logged_in(); | ||
| }, | ||
| 'meta' => array( | ||
| 'annotations' => array( | ||
| 'readonly' => true, | ||
| 'destructive' => false, | ||
| 'idempotent' => true, | ||
| ), | ||
| 'show_in_rest' => false, | ||
| ), | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Registers the `core/get-environment-info` ability. | ||
| * | ||
| * @since 0.3.0 | ||
| * | ||
| * @return void | ||
| */ | ||
| protected static function register_get_environment_info(): void { | ||
| wp_register_ability( | ||
| 'core/get-environment-info', | ||
| array( | ||
| 'label' => __( 'Get Environment Info' ), | ||
| 'description' => __( 'Returns core details about the site\'s runtime context for diagnostics and compatibility (environment, PHP runtime, database server info, WordPress version).' ), | ||
| 'category' => self::CATEGORY_SITE, | ||
| 'output_schema' => array( | ||
| 'type' => 'object', | ||
| 'required' => array( 'environment', 'php_version', 'db_server_info', 'wp_version' ), | ||
| 'properties' => array( | ||
| 'environment' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The site\'s runtime environment classification (e.g., production, staging, development).' ), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can use examples here, too.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can only be one of four values. I think it's valueable to make that clearer.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 'examples' => array( 'production', 'staging', 'development', 'local' ), | ||
| ), | ||
| 'php_version' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The PHP runtime version executing WordPress.' ), | ||
| ), | ||
| 'db_server_info' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The database server vendor and version string reported by the driver.' ), | ||
| 'examples' => array( '8.0.34', '10.11.6-MariaDB' ), | ||
| ), | ||
| 'wp_version' => array( | ||
| 'type' => 'string', | ||
| 'description' => __( 'The WordPress core version running on this site.' ), | ||
| ), | ||
| ), | ||
| 'additionalProperties' => false, | ||
| ), | ||
| 'execute_callback' => static function (): array { | ||
| global $wpdb; | ||
|
|
||
| $env = wp_get_environment_type(); | ||
| $php_version = phpversion(); | ||
| $db_server_info = ''; | ||
| if ( isset( $wpdb ) && is_object( $wpdb ) && method_exists( $wpdb, 'db_server_info' ) ) { | ||
| $db_server_info = $wpdb->db_server_info() ?? ''; | ||
| } | ||
| $wp_version = get_bloginfo( 'version' ); | ||
|
|
||
| return array( | ||
| 'environment' => $env, | ||
| 'php_version' => $php_version, | ||
| 'db_server_info' => $db_server_info, | ||
| 'wp_version' => $wp_version, | ||
| ); | ||
| }, | ||
| 'permission_callback' => static function (): bool { | ||
| return current_user_can( 'manage_options' ); | ||
| }, | ||
| 'meta' => array( | ||
| 'annotations' => array( | ||
| 'readonly' => true, | ||
| 'destructive' => false, | ||
| 'idempotent' => true, | ||
| ), | ||
| 'show_in_rest' => true, | ||
| ), | ||
| ) | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran out of time to include that in the patch against WordPress
trunk. Is there anyone willing to handle the bug fix PR in core just for this change? 😄There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WordPress/wordpress-develop#10395 - started working on the patch. Unit tests are still missing at the time of writing.