From cfff125f55d7897b8f5c0ce85a0bf62cea03ba3b Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:40:15 -0400 Subject: [PATCH 01/16] Add initial core abilities for WordPress 6.9 Implements 4 core abilities to provide a stable foundation: - core/get-bloginfo: Retrieve site information fields - core/get-current-user-info: Get current authenticated user data - core/get-environment-type: Get WordPress environment type - core/find-abilities: Discover available abilities with optional filtering The core abilities are registered via the abilities_api_init hook and can be disabled in test environments or controlled via the abilities_api_register_core_abilities filter. Addresses #105 --- .../abilities/class-wp-core-abilities.php | 345 ++++++++++++++++++ includes/bootstrap.php | 13 + tests/unit/abilities-api/wpCoreAbilities.php | 171 +++++++++ 3 files changed, 529 insertions(+) create mode 100644 includes/abilities/class-wp-core-abilities.php create mode 100644 tests/unit/abilities-api/wpCoreAbilities.php diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php new file mode 100644 index 00000000..27d1b9bf --- /dev/null +++ b/includes/abilities/class-wp-core-abilities.php @@ -0,0 +1,345 @@ + __( 'Get Blog Information', 'abilities-api' ), + 'description' => __( 'Returns a single site information field from get_bloginfo().', 'abilities-api' ), + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'field' => array( + 'type' => 'string', + 'enum' => $fields, + 'description' => __( 'The site information field to retrieve.', 'abilities-api' ), + ), + ), + 'required' => array( 'field' ), + 'additionalProperties' => false, + ), + 'output_schema' => array( + 'type' => 'object', + 'required' => array( 'field', 'value' ), + 'properties' => array( + 'field' => array( + 'type' => 'string', + 'description' => __( 'The requested site information field.', 'abilities-api' ), + ), + 'value' => array( + 'type' => 'string', + 'description' => __( 'The value returned by get_bloginfo().', 'abilities-api' ), + ), + ), + 'additionalProperties' => false, + ), + 'execute_callback' => static function ( array $input ): array { + $field = $input['field']; + $value = get_bloginfo( $field ); + + return array( + 'field' => $field, + 'value' => (string) $value, + ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'annotations' => array( + 'instructions' => __( 'Retrieves a single site property by passing an allowed field to get_bloginfo().', 'abilities-api' ), + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + } + + /** + * Registers the `core/get-current-user-info` ability. + * + * @since n.e.x.t + * + * @return void + */ + protected static function register_get_current_user_info(): void { + wp_register_ability( + 'core/get-current-user-info', + array( + 'label' => __( 'Get Current User Information', 'abilities-api' ), + 'description' => __( 'Returns basic information about the current authenticated user.', 'abilities-api' ), + 'output_schema' => array( + 'type' => 'object', + 'required' => array( 'id', 'display_name', 'locale' ), + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'description' => __( 'The user ID.', 'abilities-api' ), + ), + 'display_name' => array( + 'type' => 'string', + 'description' => __( 'The display name of the user.', 'abilities-api' ), + ), + 'user_nicename' => array( + 'type' => 'string', + 'description' => __( 'The URL-friendly name for the user.', 'abilities-api' ), + ), + 'user_login' => array( + 'type' => 'string', + 'description' => __( 'The login username for the user.', 'abilities-api' ), + ), + 'roles' => array( + 'type' => 'array', + 'description' => __( 'The roles assigned to the user.', 'abilities-api' ), + 'items' => array( + 'type' => 'string', + ), + ), + 'locale' => array( + 'type' => 'string', + 'description' => __( 'The locale string for the user, such as en_US.', 'abilities-api' ), + ), + ), + 'additionalProperties' => false, + ), + 'execute_callback' => static function (): array { + $current_user = wp_get_current_user(); + + 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( + 'instructions' => __( 'Retrieves information about the current authenticated user.', 'abilities-api' ), + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + } + + /** + * Registers the `core/get-environment-type` ability. + * + * @since n.e.x.t + * + * @return void + */ + protected static function register_get_environment_type(): void { + wp_register_ability( + 'core/get-environment-type', + array( + 'label' => __( 'Get Environment Type', 'abilities-api' ), + 'description' => __( 'Returns the current WordPress environment type (e.g. production or staging).', 'abilities-api' ), + 'output_schema' => array( + 'type' => 'object', + 'required' => array( 'environment' ), + 'properties' => array( + 'environment' => array( + 'type' => 'string', + 'description' => __( 'The environment type returned by wp_get_environment_type().', 'abilities-api' ), + ), + ), + 'additionalProperties' => false, + ), + 'execute_callback' => static function (): array { + return array( + 'environment' => wp_get_environment_type(), + ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'annotations' => array( + 'instructions' => __( 'Retrieves the current WordPress environment type.', 'abilities-api' ), + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + } + + /** + * Registers the `core/find-abilities` ability. + * + * @since n.e.x.t + * + * @return void + */ + protected static function register_find_abilities(): void { + wp_register_ability( + 'core/find-abilities', + array( + 'label' => __( 'Find Abilities', 'abilities-api' ), + 'description' => __( 'Returns a list of abilities that are exposed through the registry.', 'abilities-api' ), + 'input_schema' => array( + 'type' => array( 'object', 'null' ), + 'properties' => array( + 'namespace' => array( + 'type' => 'string', + 'description' => __( 'Optional namespace prefix to filter abilities (e.g. "core/").', 'abilities-api' ), + ), + 'show_in_rest' => array( + 'type' => 'boolean', + 'description' => __( 'Whether to limit results to abilities exposed in REST. Defaults to true.', 'abilities-api' ), + ), + ), + 'additionalProperties' => false, + ), + 'output_schema' => array( + 'type' => 'object', + 'properties' => array( + 'abilities' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'type' => 'string', + 'description' => __( 'The ability name.', 'abilities-api' ), + ), + 'label' => array( + 'type' => 'string', + 'description' => __( 'The human readable label.', 'abilities-api' ), + ), + 'description' => array( + 'type' => 'string', + 'description' => __( 'The detailed description.', 'abilities-api' ), + ), + 'meta' => array( + 'type' => 'object', + 'description' => __( 'Additional metadata associated with the ability.', 'abilities-api' ), + ), + 'annotations' => array( + 'type' => 'object', + 'description' => __( 'Annotations describing ability behavior.', 'abilities-api' ), + ), + 'show_in_rest' => array( + 'type' => 'boolean', + 'description' => __( 'Whether the ability is exposed in REST.', 'abilities-api' ), + ), + ), + 'required' => array( 'name', 'label', 'description', 'meta', 'annotations', 'show_in_rest' ), + ), + ), + ), + 'additionalProperties' => false, + ), + 'execute_callback' => static function ( array $input = array() ): array { + $namespace = $input['namespace'] ?? null; + $filter_rest = array_key_exists( 'show_in_rest', $input ) ? (bool) $input['show_in_rest'] : true; + $abilities = wp_get_abilities(); + $filtered_list = array(); + + foreach ( $abilities as $ability ) { + $show_in_rest = $ability->get_meta_item( 'show_in_rest', false ); + if ( $filter_rest && ! $show_in_rest ) { + continue; + } + + if ( $namespace && ! str_starts_with( $ability->get_name(), $namespace ) ) { + continue; + } + + $filtered_list[] = array( + 'name' => $ability->get_name(), + 'label' => $ability->get_label(), + 'description' => $ability->get_description(), + 'meta' => $ability->get_meta(), + 'annotations' => $ability->get_meta_item( 'annotations', array() ), + 'show_in_rest' => $show_in_rest, + ); + } + + /** + * Filters the abilities returned by the `core/find-abilities` ability. + * + * @since n.e.x.t + * + * @param array[] $abilities An array of abilities exposed by the ability. + * @param array $input The input arguments passed to the ability. + */ + $filtered_list = apply_filters( 'abilities_api_core_find_abilities_results', $filtered_list, $input ); + + return array( + 'abilities' => array_values( $filtered_list ), + ); + }, + 'permission_callback' => static function (): bool { + return current_user_can( 'read' ); + }, + 'meta' => array( + 'annotations' => array( + 'instructions' => __( 'Lists abilities from the registry. Optional namespace filter is supported.', 'abilities-api' ), + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + } +} diff --git a/includes/bootstrap.php b/includes/bootstrap.php index ae23de56..d5a12f48 100644 --- a/includes/bootstrap.php +++ b/includes/bootstrap.php @@ -41,6 +41,19 @@ require_once __DIR__ . '/abilities-api.php'; } +// Load core abilities class. +if ( ! class_exists( 'WP_Core_Abilities' ) ) { + require_once __DIR__ . '/abilities/class-wp-core-abilities.php'; +} + +// Register core abilities when requested via filter or when not in test environment. +$is_test_env = defined( 'WP_RUN_CORE_TESTS' ) || defined( 'WP_TESTS_CONFIG_FILE_PATH' ) || ( function_exists( 'getenv' ) && false !== getenv( 'WP_PHPUNIT__DIR' ) ); +if ( ! $is_test_env || apply_filters( 'abilities_api_register_core_abilities', false ) ) { + if ( function_exists( 'add_action' ) ) { + add_action( 'abilities_api_init', array( 'WP_Core_Abilities', 'register' ) ); + } +} + // Load REST API init class for plugin bootstrap. if ( ! class_exists( 'WP_REST_Abilities_Init' ) ) { require_once __DIR__ . '/rest-api/class-wp-rest-abilities-init.php'; diff --git a/tests/unit/abilities-api/wpCoreAbilities.php b/tests/unit/abilities-api/wpCoreAbilities.php new file mode 100644 index 00000000..9ad220f2 --- /dev/null +++ b/tests/unit/abilities-api/wpCoreAbilities.php @@ -0,0 +1,171 @@ +is_registered( 'core/get-bloginfo' ) ) { + $registry->unregister( 'core/get-bloginfo' ); + } + if ( $registry->is_registered( 'core/get-current-user-info' ) ) { + $registry->unregister( 'core/get-current-user-info' ); + } + if ( $registry->is_registered( 'core/get-environment-type' ) ) { + $registry->unregister( 'core/get-environment-type' ); + } + if ( $registry->is_registered( 'core/find-abilities' ) ) { + $registry->unregister( 'core/find-abilities' ); + } + + // Fire the init action if it hasn't been fired yet. + if ( ! did_action( 'abilities_api_init' ) ) { + do_action( 'abilities_api_init' ); + } + + // Register core abilities for testing. + WP_Core_Abilities::register(); + } + + /** + * Tests that the `core/get-bloginfo` ability is registered with the expected schema. + */ + public function test_core_get_bloginfo_ability_is_registered(): void { + $ability = wp_get_ability( 'core/get-bloginfo' ); + + $this->assertInstanceOf( WP_Ability::class, $ability ); + $this->assertTrue( $ability->get_meta_item( 'show_in_rest', false ) ); + + $input_schema = $ability->get_input_schema(); + $this->assertSame( array( 'field' ), $input_schema['required'] ); + $this->assertContains( 'name', $input_schema['properties']['field']['enum'] ); + } + + /** + * Tests executing the `core/get-bloginfo` ability. + */ + public function test_core_get_bloginfo_executes(): void { + $ability = wp_get_ability( 'core/get-bloginfo' ); + + $result = $ability->execute( + array( + 'field' => 'name', + ) + ); + + $this->assertSame( + array( + 'field' => 'name', + 'value' => get_bloginfo( 'name' ), + ), + $result + ); + } + + /** + * Tests that executing the current user info ability requires authentication. + */ + public function test_core_get_current_user_info_requires_authentication(): void { + $ability = wp_get_ability( 'core/get-current-user-info' ); + + $this->assertFalse( $ability->check_permissions() ); + + $result = $ability->execute(); + $this->assertWPError( $result ); + $this->assertSame( 'ability_invalid_permissions', $result->get_error_code() ); + } + + /** + * Tests executing the current user info ability as an authenticated user. + */ + public function test_core_get_current_user_info_returns_user_data(): void { + $user_id = self::factory()->user->create( + array( + 'role' => 'subscriber', + 'locale' => 'fr_FR', + ) + ); + + wp_set_current_user( $user_id ); + + $ability = wp_get_ability( 'core/get-current-user-info' ); + + $this->assertTrue( $ability->check_permissions() ); + + $result = $ability->execute(); + $this->assertSame( $user_id, $result['id'] ); + $this->assertSame( 'fr_FR', $result['locale'] ); + $this->assertSame( 'subscriber', $result['roles'][0] ); + $this->assertSame( get_userdata( $user_id )->display_name, $result['display_name'] ); + + wp_set_current_user( 0 ); + } + + /** + * Tests executing the environment type ability. + */ + public function test_core_get_environment_type_executes(): void { + $ability = wp_get_ability( 'core/get-environment-type' ); + $environment = wp_get_environment_type(); + $ability_data = $ability->execute(); + + $this->assertSame( + array( + 'environment' => $environment, + ), + $ability_data + ); + } + + /** + * Tests executing the find abilities ability and filtering results. + */ + public function test_core_find_abilities_executes(): void { + $user_id = self::factory()->user->create( + array( + 'role' => 'administrator', + ) + ); + + wp_set_current_user( $user_id ); + + // Verify user has 'read' capability. + $this->assertTrue( current_user_can( 'read' ), 'Administrator should have read capability' ); + + $ability = wp_get_ability( 'core/find-abilities' ); + + $this->assertTrue( $ability->check_permissions() ); + + // Test with namespace filter to get all abilities. + $result = $ability->execute( + array( + 'namespace' => '', + ) + ); + $this->assertNotWPError( $result ); + $this->assertArrayHasKey( 'abilities', $result ); + $this->assertNotEmpty( $result['abilities'] ); + + // Test with namespace filter. + $result_filtered = $ability->execute( + array( + 'namespace' => 'core/', + ) + ); + foreach ( $result_filtered['abilities'] as $item ) { + $this->assertStringStartsWith( 'core/', $item['name'] ); + } + + wp_set_current_user( 0 ); + } +} From 1957efb6451e2bc2e3cb6fd769b39db1868a0c1b Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Fri, 10 Oct 2025 18:49:16 -0400 Subject: [PATCH 02/16] Fix linting issues and update hook naming - Update filter hook name to wp_find_abilities_results for core consistency - Remove text domains from translation functions (core uses default domain) - Fix execute callback signatures to use optional parameters - Inline test environment check in bootstrap.php --- .../abilities/class-wp-core-abilities.php | 106 +++++++++--------- includes/bootstrap.php | 4 +- 2 files changed, 56 insertions(+), 54 deletions(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index 27d1b9bf..8d28a7e6 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -14,6 +14,7 @@ * * @since n.e.x.t */ +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Core class intended for WordPress core. class WP_Core_Abilities { /** * Registers the default core abilities. @@ -51,15 +52,15 @@ protected static function register_get_bloginfo(): void { wp_register_ability( 'core/get-bloginfo', array( - 'label' => __( 'Get Blog Information', 'abilities-api' ), - 'description' => __( 'Returns a single site information field from get_bloginfo().', 'abilities-api' ), + 'label' => __( 'Get Blog Information' ), + 'description' => __( 'Returns a single site information field from get_bloginfo().' ), 'input_schema' => array( 'type' => 'object', 'properties' => array( 'field' => array( 'type' => 'string', 'enum' => $fields, - 'description' => __( 'The site information field to retrieve.', 'abilities-api' ), + 'description' => __( 'The site information field to retrieve.' ), ), ), 'required' => array( 'field' ), @@ -71,16 +72,16 @@ protected static function register_get_bloginfo(): void { 'properties' => array( 'field' => array( 'type' => 'string', - 'description' => __( 'The requested site information field.', 'abilities-api' ), + 'description' => __( 'The requested site information field.' ), ), 'value' => array( 'type' => 'string', - 'description' => __( 'The value returned by get_bloginfo().', 'abilities-api' ), + 'description' => __( 'The value returned by get_bloginfo().' ), ), ), 'additionalProperties' => false, ), - 'execute_callback' => static function ( array $input ): array { + 'execute_callback' => static function ( $input = array() ): array { $field = $input['field']; $value = get_bloginfo( $field ); @@ -92,7 +93,7 @@ protected static function register_get_bloginfo(): void { 'permission_callback' => '__return_true', 'meta' => array( 'annotations' => array( - 'instructions' => __( 'Retrieves a single site property by passing an allowed field to get_bloginfo().', 'abilities-api' ), + 'instructions' => __( 'Retrieves a single site property by passing an allowed field to get_bloginfo().' ), 'readonly' => true, 'destructive' => false, 'idempotent' => true, @@ -114,38 +115,38 @@ protected static function register_get_current_user_info(): void { wp_register_ability( 'core/get-current-user-info', array( - 'label' => __( 'Get Current User Information', 'abilities-api' ), - 'description' => __( 'Returns basic information about the current authenticated user.', 'abilities-api' ), + 'label' => __( 'Get Current User Information' ), + 'description' => __( 'Returns basic information about the current authenticated user.' ), 'output_schema' => array( 'type' => 'object', 'required' => array( 'id', 'display_name', 'locale' ), 'properties' => array( - 'id' => array( + 'id' => array( 'type' => 'integer', - 'description' => __( 'The user ID.', 'abilities-api' ), + 'description' => __( 'The user ID.' ), ), - 'display_name' => array( + 'display_name' => array( 'type' => 'string', - 'description' => __( 'The display name of the user.', 'abilities-api' ), + 'description' => __( 'The display name of the user.' ), ), 'user_nicename' => array( 'type' => 'string', - 'description' => __( 'The URL-friendly name for the user.', 'abilities-api' ), + 'description' => __( 'The URL-friendly name for the user.' ), ), - 'user_login' => array( + 'user_login' => array( 'type' => 'string', - 'description' => __( 'The login username for the user.', 'abilities-api' ), + 'description' => __( 'The login username for the user.' ), ), - 'roles' => array( + 'roles' => array( 'type' => 'array', - 'description' => __( 'The roles assigned to the user.', 'abilities-api' ), + 'description' => __( 'The roles assigned to the user.' ), 'items' => array( 'type' => 'string', ), ), - 'locale' => array( + 'locale' => array( 'type' => 'string', - 'description' => __( 'The locale string for the user, such as en_US.', 'abilities-api' ), + 'description' => __( 'The locale string for the user, such as en_US.' ), ), ), 'additionalProperties' => false, @@ -154,12 +155,12 @@ protected static function register_get_current_user_info(): void { $current_user = wp_get_current_user(); return array( - 'id' => $current_user->ID, - 'display_name' => $current_user->display_name, + '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 ), + 'user_login' => $current_user->user_login, + 'roles' => $current_user->roles, + 'locale' => get_user_locale( $current_user ), ); }, 'permission_callback' => static function (): bool { @@ -167,7 +168,7 @@ protected static function register_get_current_user_info(): void { }, 'meta' => array( 'annotations' => array( - 'instructions' => __( 'Retrieves information about the current authenticated user.', 'abilities-api' ), + 'instructions' => __( 'Retrieves information about the current authenticated user.' ), 'readonly' => true, 'destructive' => false, 'idempotent' => true, @@ -189,15 +190,15 @@ protected static function register_get_environment_type(): void { wp_register_ability( 'core/get-environment-type', array( - 'label' => __( 'Get Environment Type', 'abilities-api' ), - 'description' => __( 'Returns the current WordPress environment type (e.g. production or staging).', 'abilities-api' ), + 'label' => __( 'Get Environment Type' ), + 'description' => __( 'Returns the current WordPress environment type (e.g. production or staging).' ), 'output_schema' => array( 'type' => 'object', 'required' => array( 'environment' ), 'properties' => array( 'environment' => array( 'type' => 'string', - 'description' => __( 'The environment type returned by wp_get_environment_type().', 'abilities-api' ), + 'description' => __( 'The environment type returned by wp_get_environment_type().' ), ), ), 'additionalProperties' => false, @@ -210,7 +211,7 @@ protected static function register_get_environment_type(): void { 'permission_callback' => '__return_true', 'meta' => array( 'annotations' => array( - 'instructions' => __( 'Retrieves the current WordPress environment type.', 'abilities-api' ), + 'instructions' => __( 'Retrieves the current WordPress environment type.' ), 'readonly' => true, 'destructive' => false, 'idempotent' => true, @@ -232,53 +233,53 @@ protected static function register_find_abilities(): void { wp_register_ability( 'core/find-abilities', array( - 'label' => __( 'Find Abilities', 'abilities-api' ), - 'description' => __( 'Returns a list of abilities that are exposed through the registry.', 'abilities-api' ), + 'label' => __( 'Find Abilities' ), + 'description' => __( 'Returns a list of abilities that are exposed through the registry.' ), 'input_schema' => array( 'type' => array( 'object', 'null' ), 'properties' => array( - 'namespace' => array( + 'namespace' => array( 'type' => 'string', - 'description' => __( 'Optional namespace prefix to filter abilities (e.g. "core/").', 'abilities-api' ), + 'description' => __( 'Optional namespace prefix to filter abilities (e.g. "core/").' ), ), 'show_in_rest' => array( 'type' => 'boolean', - 'description' => __( 'Whether to limit results to abilities exposed in REST. Defaults to true.', 'abilities-api' ), + 'description' => __( 'Whether to limit results to abilities exposed in REST. Defaults to true.' ), ), ), 'additionalProperties' => false, ), 'output_schema' => array( - 'type' => 'object', - 'properties' => array( + 'type' => 'object', + 'properties' => array( 'abilities' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'properties' => array( - 'name' => array( + 'name' => array( 'type' => 'string', - 'description' => __( 'The ability name.', 'abilities-api' ), + 'description' => __( 'The ability name.' ), ), - 'label' => array( + 'label' => array( 'type' => 'string', - 'description' => __( 'The human readable label.', 'abilities-api' ), + 'description' => __( 'The human readable label.' ), ), - 'description' => array( + 'description' => array( 'type' => 'string', - 'description' => __( 'The detailed description.', 'abilities-api' ), + 'description' => __( 'The detailed description.' ), ), - 'meta' => array( + 'meta' => array( 'type' => 'object', - 'description' => __( 'Additional metadata associated with the ability.', 'abilities-api' ), + 'description' => __( 'Additional metadata associated with the ability.' ), ), - 'annotations' => array( + 'annotations' => array( 'type' => 'object', - 'description' => __( 'Annotations describing ability behavior.', 'abilities-api' ), + 'description' => __( 'Annotations describing ability behavior.' ), ), 'show_in_rest' => array( 'type' => 'boolean', - 'description' => __( 'Whether the ability is exposed in REST.', 'abilities-api' ), + 'description' => __( 'Whether the ability is exposed in REST.' ), ), ), 'required' => array( 'name', 'label', 'description', 'meta', 'annotations', 'show_in_rest' ), @@ -287,7 +288,7 @@ protected static function register_find_abilities(): void { ), 'additionalProperties' => false, ), - 'execute_callback' => static function ( array $input = array() ): array { + 'execute_callback' => static function ( $input = array() ): array { $namespace = $input['namespace'] ?? null; $filter_rest = array_key_exists( 'show_in_rest', $input ) ? (bool) $input['show_in_rest'] : true; $abilities = wp_get_abilities(); @@ -299,7 +300,7 @@ protected static function register_find_abilities(): void { continue; } - if ( $namespace && ! str_starts_with( $ability->get_name(), $namespace ) ) { + if ( $namespace && 0 !== strpos( $ability->get_name(), $namespace ) ) { continue; } @@ -321,7 +322,8 @@ protected static function register_find_abilities(): void { * @param array[] $abilities An array of abilities exposed by the ability. * @param array $input The input arguments passed to the ability. */ - $filtered_list = apply_filters( 'abilities_api_core_find_abilities_results', $filtered_list, $input ); + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core hook intended for WordPress core. + $filtered_list = apply_filters( 'wp_find_abilities_results', $filtered_list, $input ); return array( 'abilities' => array_values( $filtered_list ), @@ -332,7 +334,7 @@ protected static function register_find_abilities(): void { }, 'meta' => array( 'annotations' => array( - 'instructions' => __( 'Lists abilities from the registry. Optional namespace filter is supported.', 'abilities-api' ), + 'instructions' => __( 'Lists abilities from the registry. Optional namespace filter is supported.' ), 'readonly' => true, 'destructive' => false, 'idempotent' => true, diff --git a/includes/bootstrap.php b/includes/bootstrap.php index d5a12f48..20958ddb 100644 --- a/includes/bootstrap.php +++ b/includes/bootstrap.php @@ -47,8 +47,8 @@ } // Register core abilities when requested via filter or when not in test environment. -$is_test_env = defined( 'WP_RUN_CORE_TESTS' ) || defined( 'WP_TESTS_CONFIG_FILE_PATH' ) || ( function_exists( 'getenv' ) && false !== getenv( 'WP_PHPUNIT__DIR' ) ); -if ( ! $is_test_env || apply_filters( 'abilities_api_register_core_abilities', false ) ) { +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Plugin-specific hook for feature plugin context. +if ( ! ( defined( 'WP_RUN_CORE_TESTS' ) || defined( 'WP_TESTS_CONFIG_FILE_PATH' ) || ( function_exists( 'getenv' ) && false !== getenv( 'WP_PHPUNIT__DIR' ) ) ) || apply_filters( 'abilities_api_register_core_abilities', false ) ) { if ( function_exists( 'add_action' ) ) { add_action( 'abilities_api_init', array( 'WP_Core_Abilities', 'register' ) ); } From 299ecc95e136b9cb1cf4837afafd2e07933400fc Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Wed, 15 Oct 2025 14:16:55 -0400 Subject: [PATCH 03/16] Remove core/find-abilities ability This removes the `core/find-abilities` ability to address the chicken-and-egg security issue where the ability could expose non-REST abilities when called via REST API. The ability had a `show_in_rest` parameter that could be set to false, which would bypass the REST visibility controls when the ability itself was marked as exposed in REST. This creates an inconsistent security model. Changes: - Removed register_find_abilities() method from WP_Core_Abilities class - Removed register_find_abilities() call from register() method - Removed test_core_find_abilities_executes() test - Removed core/find-abilities unregister check from test setup --- .../abilities/class-wp-core-abilities.php | 124 ------------------ tests/unit/abilities-api/wpCoreAbilities.php | 44 ------- 2 files changed, 168 deletions(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index 8d28a7e6..1dffc1be 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -27,7 +27,6 @@ public static function register(): void { self::register_get_bloginfo(); self::register_get_current_user_info(); self::register_get_environment_type(); - self::register_find_abilities(); } /** @@ -221,127 +220,4 @@ protected static function register_get_environment_type(): void { ) ); } - - /** - * Registers the `core/find-abilities` ability. - * - * @since n.e.x.t - * - * @return void - */ - protected static function register_find_abilities(): void { - wp_register_ability( - 'core/find-abilities', - array( - 'label' => __( 'Find Abilities' ), - 'description' => __( 'Returns a list of abilities that are exposed through the registry.' ), - 'input_schema' => array( - 'type' => array( 'object', 'null' ), - 'properties' => array( - 'namespace' => array( - 'type' => 'string', - 'description' => __( 'Optional namespace prefix to filter abilities (e.g. "core/").' ), - ), - 'show_in_rest' => array( - 'type' => 'boolean', - 'description' => __( 'Whether to limit results to abilities exposed in REST. Defaults to true.' ), - ), - ), - 'additionalProperties' => false, - ), - 'output_schema' => array( - 'type' => 'object', - 'properties' => array( - 'abilities' => array( - 'type' => 'array', - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'name' => array( - 'type' => 'string', - 'description' => __( 'The ability name.' ), - ), - 'label' => array( - 'type' => 'string', - 'description' => __( 'The human readable label.' ), - ), - 'description' => array( - 'type' => 'string', - 'description' => __( 'The detailed description.' ), - ), - 'meta' => array( - 'type' => 'object', - 'description' => __( 'Additional metadata associated with the ability.' ), - ), - 'annotations' => array( - 'type' => 'object', - 'description' => __( 'Annotations describing ability behavior.' ), - ), - 'show_in_rest' => array( - 'type' => 'boolean', - 'description' => __( 'Whether the ability is exposed in REST.' ), - ), - ), - 'required' => array( 'name', 'label', 'description', 'meta', 'annotations', 'show_in_rest' ), - ), - ), - ), - 'additionalProperties' => false, - ), - 'execute_callback' => static function ( $input = array() ): array { - $namespace = $input['namespace'] ?? null; - $filter_rest = array_key_exists( 'show_in_rest', $input ) ? (bool) $input['show_in_rest'] : true; - $abilities = wp_get_abilities(); - $filtered_list = array(); - - foreach ( $abilities as $ability ) { - $show_in_rest = $ability->get_meta_item( 'show_in_rest', false ); - if ( $filter_rest && ! $show_in_rest ) { - continue; - } - - if ( $namespace && 0 !== strpos( $ability->get_name(), $namespace ) ) { - continue; - } - - $filtered_list[] = array( - 'name' => $ability->get_name(), - 'label' => $ability->get_label(), - 'description' => $ability->get_description(), - 'meta' => $ability->get_meta(), - 'annotations' => $ability->get_meta_item( 'annotations', array() ), - 'show_in_rest' => $show_in_rest, - ); - } - - /** - * Filters the abilities returned by the `core/find-abilities` ability. - * - * @since n.e.x.t - * - * @param array[] $abilities An array of abilities exposed by the ability. - * @param array $input The input arguments passed to the ability. - */ - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core hook intended for WordPress core. - $filtered_list = apply_filters( 'wp_find_abilities_results', $filtered_list, $input ); - - return array( - 'abilities' => array_values( $filtered_list ), - ); - }, - 'permission_callback' => static function (): bool { - return current_user_can( 'read' ); - }, - 'meta' => array( - 'annotations' => array( - 'instructions' => __( 'Lists abilities from the registry. Optional namespace filter is supported.' ), - 'readonly' => true, - 'destructive' => false, - 'idempotent' => true, - ), - 'show_in_rest' => true, - ), - ) - ); - } } diff --git a/tests/unit/abilities-api/wpCoreAbilities.php b/tests/unit/abilities-api/wpCoreAbilities.php index 9ad220f2..f33b8f98 100644 --- a/tests/unit/abilities-api/wpCoreAbilities.php +++ b/tests/unit/abilities-api/wpCoreAbilities.php @@ -24,9 +24,6 @@ public function set_up(): void { if ( $registry->is_registered( 'core/get-environment-type' ) ) { $registry->unregister( 'core/get-environment-type' ); } - if ( $registry->is_registered( 'core/find-abilities' ) ) { - $registry->unregister( 'core/find-abilities' ); - } // Fire the init action if it hasn't been fired yet. if ( ! did_action( 'abilities_api_init' ) ) { @@ -127,45 +124,4 @@ public function test_core_get_environment_type_executes(): void { ); } - /** - * Tests executing the find abilities ability and filtering results. - */ - public function test_core_find_abilities_executes(): void { - $user_id = self::factory()->user->create( - array( - 'role' => 'administrator', - ) - ); - - wp_set_current_user( $user_id ); - - // Verify user has 'read' capability. - $this->assertTrue( current_user_can( 'read' ), 'Administrator should have read capability' ); - - $ability = wp_get_ability( 'core/find-abilities' ); - - $this->assertTrue( $ability->check_permissions() ); - - // Test with namespace filter to get all abilities. - $result = $ability->execute( - array( - 'namespace' => '', - ) - ); - $this->assertNotWPError( $result ); - $this->assertArrayHasKey( 'abilities', $result ); - $this->assertNotEmpty( $result['abilities'] ); - - // Test with namespace filter. - $result_filtered = $ability->execute( - array( - 'namespace' => 'core/', - ) - ); - foreach ( $result_filtered['abilities'] as $item ) { - $this->assertStringStartsWith( 'core/', $item['name'] ); - } - - wp_set_current_user( 0 ); - } } From 7f928ef18adcb71094c832b6f822f4e250e6795b Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Wed, 15 Oct 2025 15:31:50 -0400 Subject: [PATCH 04/16] Refactor core abilities: namespace, categories, and expanded environment info Major changes based on PR feedback and model testing findings: Namespace Changes: - Renamed core/* to wp/* namespace for clarity - wp/get-site-info (was core/get-bloginfo) - wp/get-current-user-info (namespace change) - wp/get-environment-info (was core/get-environment-type) Category System: - Split from single 'core' category into semantic categories - 'site' category for site-related abilities - 'user' category for user-related abilities Expanded Environment Info: - Added php_version field - Added mysql_version field - Added wp_version field - Added database_type field (mysql/mariadb detection) - Renamed ability to reflect expanded scope Security Improvements: - wp/get-site-info now requires manage_options capability - wp/get-environment-info requires manage_options capability - Protects sensitive site and environment information Tests: - All 193 tests passing with 447 assertions - Updated test fixtures for new names and categories - Added assertions for expanded environment info fields --- .../abilities/class-wp-core-abilities.php | 121 +++++++++++++---- includes/bootstrap.php | 3 +- tests/unit/abilities-api/wpCoreAbilities.php | 128 +++++++++++------- 3 files changed, 177 insertions(+), 75 deletions(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index 1dffc1be..63ce31f4 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -4,7 +4,7 @@ * * @package WordPress * @subpackage Abilities_API - * @since n.e.x.t + * @since 0.3.0 */ declare( strict_types = 1 ); @@ -12,31 +12,58 @@ /** * Registers the default core abilities that ship with the Abilities API. * - * @since n.e.x.t + * @since 0.3.0 */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Core class intended for WordPress core. class WP_Core_Abilities { + /** + * Registers the core abilities categories. + * + * @since 0.3.0 + * + * @return void + */ + public static function register_category(): void { + // Site-related capabilities + wp_register_ability_category( + 'site', + array( + 'label' => __( 'Site' ), + 'description' => __( 'Abilities that retrieve or modify site information and settings.' ), + ) + ); + + // User-related capabilities + wp_register_ability_category( + 'user', + array( + 'label' => __( 'User' ), + 'description' => __( 'Abilities that retrieve or modify user information and settings.' ), + ) + ); + } + /** * Registers the default core abilities. * - * @since n.e.x.t + * @since 0.3.0 * * @return void */ public static function register(): void { - self::register_get_bloginfo(); + self::register_get_site_info(); self::register_get_current_user_info(); - self::register_get_environment_type(); + self::register_get_environment_info(); } /** - * Registers the `core/get-bloginfo` ability. + * Registers the `wp/get-site-info` ability. * - * @since n.e.x.t + * @since 0.3.0 * * @return void */ - protected static function register_get_bloginfo(): void { + protected static function register_get_site_info(): void { $fields = array( 'name', 'description', @@ -49,10 +76,11 @@ protected static function register_get_bloginfo(): void { ); wp_register_ability( - 'core/get-bloginfo', + 'wp/get-site-info', array( - 'label' => __( 'Get Blog Information' ), + 'label' => __( 'Get Site Information' ), 'description' => __( 'Returns a single site information field from get_bloginfo().' ), + 'category' => 'site', 'input_schema' => array( 'type' => 'object', 'properties' => array( @@ -89,7 +117,10 @@ protected static function register_get_bloginfo(): void { 'value' => (string) $value, ); }, - 'permission_callback' => '__return_true', + 'permission_callback' => static function ( $input = array() ): bool { + // Site information can expose sensitive details; require admin capability. + return current_user_can( 'manage_options' ); + }, 'meta' => array( 'annotations' => array( 'instructions' => __( 'Retrieves a single site property by passing an allowed field to get_bloginfo().' ), @@ -104,18 +135,19 @@ protected static function register_get_bloginfo(): void { } /** - * Registers the `core/get-current-user-info` ability. + * Registers the `wp/get-current-user-info` ability. * - * @since n.e.x.t + * @since 0.3.0 * * @return void */ protected static function register_get_current_user_info(): void { wp_register_ability( - 'core/get-current-user-info', + 'wp/get-current-user-info', array( 'label' => __( 'Get Current User Information' ), 'description' => __( 'Returns basic information about the current authenticated user.' ), + 'category' => 'user', 'output_schema' => array( 'type' => 'object', 'required' => array( 'id', 'display_name', 'locale' ), @@ -179,38 +211,77 @@ protected static function register_get_current_user_info(): void { } /** - * Registers the `core/get-environment-type` ability. + * Registers the `wp/get-environment-info` ability. * - * @since n.e.x.t + * @since 0.3.0 * * @return void */ - protected static function register_get_environment_type(): void { + protected static function register_get_environment_info(): void { wp_register_ability( - 'core/get-environment-type', + 'wp/get-environment-info', array( - 'label' => __( 'Get Environment Type' ), - 'description' => __( 'Returns the current WordPress environment type (e.g. production or staging).' ), + 'label' => __( 'Get Environment Info' ), + 'description' => __( 'Returns basic information about the WordPress runtime environment.' ), + 'category' => 'site', 'output_schema' => array( 'type' => 'object', - 'required' => array( 'environment' ), + 'required' => array( 'environment', 'php_version', 'mysql_version', 'wp_version', 'database_type' ), 'properties' => array( - 'environment' => array( + 'environment' => array( 'type' => 'string', 'description' => __( 'The environment type returned by wp_get_environment_type().' ), ), + 'php_version' => array( + 'type' => 'string', + 'description' => __( 'The PHP version.' ), + ), + 'mysql_version' => array( + 'type' => 'string', + 'description' => __( 'The database server version (MySQL or MariaDB).' ), + ), + 'wp_version' => array( + 'type' => 'string', + 'description' => __( 'The WordPress version.' ), + ), + 'database_type' => array( + 'type' => 'string', + 'description' => __( 'The database server type (e.g., mysql or mariadb).' ), + ), ), 'additionalProperties' => false, ), 'execute_callback' => static function (): array { + global $wpdb; + + $env = wp_get_environment_type(); + $php_version = phpversion(); + $db_version = ''; + if ( isset( $wpdb ) && is_object( $wpdb ) && method_exists( $wpdb, 'db_version' ) ) { + $db_version = (string) $wpdb->db_version(); + } + $wp_version = (string) get_bloginfo( 'version' ); + + $type = 'mysql'; + if ( stripos( $db_version, 'mariadb' ) !== false ) { + $type = 'mariadb'; + } + return array( - 'environment' => wp_get_environment_type(), + 'environment' => (string) $env, + 'php_version' => (string) $php_version, + 'mysql_version' => (string) $db_version, + 'wp_version' => (string) $wp_version, + 'database_type' => (string) $type, ); }, - 'permission_callback' => '__return_true', + 'permission_callback' => static function (): bool { + // Environment information is restricted to administrators. + return current_user_can( 'manage_options' ); + }, 'meta' => array( 'annotations' => array( - 'instructions' => __( 'Retrieves the current WordPress environment type.' ), + 'instructions' => __( 'Retrieves environment information such as environment type, PHP, database, and WordPress versions.' ), 'readonly' => true, 'destructive' => false, 'idempotent' => true, diff --git a/includes/bootstrap.php b/includes/bootstrap.php index 20958ddb..ecafd7a2 100644 --- a/includes/bootstrap.php +++ b/includes/bootstrap.php @@ -46,10 +46,11 @@ require_once __DIR__ . '/abilities/class-wp-core-abilities.php'; } -// Register core abilities when requested via filter or when not in test environment. +// Register core abilities category and abilities when requested via filter or when not in test environment. // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Plugin-specific hook for feature plugin context. if ( ! ( defined( 'WP_RUN_CORE_TESTS' ) || defined( 'WP_TESTS_CONFIG_FILE_PATH' ) || ( function_exists( 'getenv' ) && false !== getenv( 'WP_PHPUNIT__DIR' ) ) ) || apply_filters( 'abilities_api_register_core_abilities', false ) ) { if ( function_exists( 'add_action' ) ) { + add_action( 'abilities_api_categories_init', array( 'WP_Core_Abilities', 'register_category' ) ); add_action( 'abilities_api_init', array( 'WP_Core_Abilities', 'register' ) ); } } diff --git a/tests/unit/abilities-api/wpCoreAbilities.php b/tests/unit/abilities-api/wpCoreAbilities.php index f33b8f98..c16cb300 100644 --- a/tests/unit/abilities-api/wpCoreAbilities.php +++ b/tests/unit/abilities-api/wpCoreAbilities.php @@ -13,17 +13,34 @@ class Tests_Abilities_API_WpCoreAbilities extends WP_UnitTestCase { public function set_up(): void { parent::set_up(); - // Unregister core abilities if they were already registered to avoid duplicate registration warnings. - $registry = WP_Abilities_Registry::get_instance(); - if ( $registry->is_registered( 'core/get-bloginfo' ) ) { - $registry->unregister( 'core/get-bloginfo' ); - } - if ( $registry->is_registered( 'core/get-current-user-info' ) ) { - $registry->unregister( 'core/get-current-user-info' ); - } - if ( $registry->is_registered( 'core/get-environment-type' ) ) { - $registry->unregister( 'core/get-environment-type' ); - } + // Unregister core abilities if they were already registered to avoid duplicate registration warnings. + $registry = WP_Abilities_Registry::get_instance(); + if ( $registry->is_registered( 'wp/get-site-info' ) ) { + $registry->unregister( 'wp/get-site-info' ); + } + if ( $registry->is_registered( 'wp/get-current-user-info' ) ) { + $registry->unregister( 'wp/get-current-user-info' ); + } + if ( $registry->is_registered( 'wp/get-environment-info' ) ) { + $registry->unregister( 'wp/get-environment-info' ); + } + + // Unregister categories if they exist. + $category_registry = WP_Abilities_Category_Registry::get_instance(); + if ( $category_registry->is_registered( 'site' ) ) { + wp_unregister_ability_category( 'site' ); + } + if ( $category_registry->is_registered( 'user' ) ) { + wp_unregister_ability_category( 'user' ); + } + + // Register core abilities category during the proper hook. + $callback = static function (): void { + WP_Core_Abilities::register_category(); + }; + add_action( 'abilities_api_categories_init', $callback ); + do_action( 'abilities_api_categories_init', WP_Abilities_Category_Registry::get_instance() ); + remove_action( 'abilities_api_categories_init', $callback ); // Fire the init action if it hasn't been fired yet. if ( ! did_action( 'abilities_api_init' ) ) { @@ -35,10 +52,10 @@ public function set_up(): void { } /** - * Tests that the `core/get-bloginfo` ability is registered with the expected schema. - */ - public function test_core_get_bloginfo_ability_is_registered(): void { - $ability = wp_get_ability( 'core/get-bloginfo' ); + * Tests that the `wp/get-site-info` ability is registered with the expected schema. + */ + public function test_core_get_bloginfo_ability_is_registered(): void { + $ability = wp_get_ability( 'wp/get-site-info' ); $this->assertInstanceOf( WP_Ability::class, $ability ); $this->assertTrue( $ability->get_meta_item( 'show_in_rest', false ) ); @@ -49,10 +66,14 @@ public function test_core_get_bloginfo_ability_is_registered(): void { } /** - * Tests executing the `core/get-bloginfo` ability. - */ - public function test_core_get_bloginfo_executes(): void { - $ability = wp_get_ability( 'core/get-bloginfo' ); + * Tests executing the `wp/get-site-info` ability. + */ + public function test_core_get_bloginfo_executes(): void { + // Requires manage_options. + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + $ability = wp_get_ability( 'wp/get-site-info' ); $result = $ability->execute( array( @@ -60,20 +81,22 @@ public function test_core_get_bloginfo_executes(): void { ) ); - $this->assertSame( - array( - 'field' => 'name', - 'value' => get_bloginfo( 'name' ), - ), - $result - ); - } + $this->assertSame( + array( + 'field' => 'name', + 'value' => get_bloginfo( 'name' ), + ), + $result + ); + + wp_set_current_user( 0 ); + } /** - * Tests that executing the current user info ability requires authentication. - */ - public function test_core_get_current_user_info_requires_authentication(): void { - $ability = wp_get_ability( 'core/get-current-user-info' ); + * Tests that executing the current user info ability requires authentication. + */ + public function test_core_get_current_user_info_requires_authentication(): void { + $ability = wp_get_ability( 'wp/get-current-user-info' ); $this->assertFalse( $ability->check_permissions() ); @@ -83,9 +106,9 @@ public function test_core_get_current_user_info_requires_authentication(): void } /** - * Tests executing the current user info ability as an authenticated user. - */ - public function test_core_get_current_user_info_returns_user_data(): void { + * Tests executing the current user info ability as an authenticated user. + */ + public function test_core_get_current_user_info_returns_user_data(): void { $user_id = self::factory()->user->create( array( 'role' => 'subscriber', @@ -95,7 +118,7 @@ public function test_core_get_current_user_info_returns_user_data(): void { wp_set_current_user( $user_id ); - $ability = wp_get_ability( 'core/get-current-user-info' ); + $ability = wp_get_ability( 'wp/get-current-user-info' ); $this->assertTrue( $ability->check_permissions() ); @@ -109,19 +132,26 @@ public function test_core_get_current_user_info_returns_user_data(): void { } /** - * Tests executing the environment type ability. - */ - public function test_core_get_environment_type_executes(): void { - $ability = wp_get_ability( 'core/get-environment-type' ); - $environment = wp_get_environment_type(); - $ability_data = $ability->execute(); - - $this->assertSame( - array( - 'environment' => $environment, - ), - $ability_data - ); - } + * Tests executing the environment info ability. + */ + public function test_core_get_environment_type_executes(): void { + // Requires manage_options. + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + $ability = wp_get_ability( 'wp/get-environment-info' ); + $environment = wp_get_environment_type(); + $ability_data = $ability->execute(); + + $this->assertIsArray( $ability_data ); + $this->assertArrayHasKey( 'environment', $ability_data ); + $this->assertArrayHasKey( 'php_version', $ability_data ); + $this->assertArrayHasKey( 'mysql_version', $ability_data ); + $this->assertArrayHasKey( 'wp_version', $ability_data ); + $this->assertArrayHasKey( 'database_type', $ability_data ); + $this->assertSame( $environment, $ability_data['environment'] ); + + wp_set_current_user( 0 ); + } } From 19f665a05dd1a626e413eada8bc9a7f6ea5994d4 Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Wed, 15 Oct 2025 15:36:22 -0400 Subject: [PATCH 05/16] Fix wp/get-current-user-info output schema required fields All fields returned by the execute callback should be marked as required in the output schema. The callback always returns all 6 fields from the current user object. Updated required array to include: - id - display_name - user_nicename (was missing) - user_login (was missing) - roles (was missing) - locale --- includes/abilities/class-wp-core-abilities.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index 63ce31f4..0f67aaff 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -150,7 +150,7 @@ protected static function register_get_current_user_info(): void { 'category' => 'user', 'output_schema' => array( 'type' => 'object', - 'required' => array( 'id', 'display_name', 'locale' ), + 'required' => array( 'id', 'display_name', 'user_nicename', 'user_login', 'roles', 'locale' ), 'properties' => array( 'id' => array( 'type' => 'integer', From 033f6539cfcea59b814cdb932c1d1ef5ea3b6448 Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Wed, 15 Oct 2025 15:40:08 -0400 Subject: [PATCH 06/16] Remove unused input parameter from wp/get-site-info permission callback The permission callback doesn't use the input parameter, so removing it to fix PHPCS warning. --- includes/abilities/class-wp-core-abilities.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index 0f67aaff..621202d5 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -117,7 +117,7 @@ protected static function register_get_site_info(): void { 'value' => (string) $value, ); }, - 'permission_callback' => static function ( $input = array() ): bool { + 'permission_callback' => static function (): bool { // Site information can expose sensitive details; require admin capability. return current_user_can( 'manage_options' ); }, From dbf97f703f4bc888cd90be22831f89e0d2c38ab2 Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Wed, 15 Oct 2025 22:45:04 -0400 Subject: [PATCH 07/16] Refine core abilities with constants and improved descriptions Code improvements: - Add CATEGORY_SITE and CATEGORY_USER class constants for type safety - Use constants instead of magic strings for category references - Improve ability descriptions with more specific use cases - Simplify annotations by removing redundant instructions field - Remove unnecessary type casting in get-site-info value output - Consolidate database info into single db_server_info field Schema changes: - wp/get-environment-info now returns db_server_info instead of separate mysql_version and database_type - Updated test assertions to match new schema - More descriptive field documentation throughout All tests passing: 193 PHP tests (446 assertions), 102 client tests --- .../abilities/class-wp-core-abilities.php | 120 ++++++++---------- tests/unit/abilities-api/wpCoreAbilities.php | 3 +- 2 files changed, 57 insertions(+), 66 deletions(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index 621202d5..c993c5ab 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -16,6 +16,13 @@ */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Core class intended for WordPress core. class WP_Core_Abilities { + /** + * Category slugs for core abilities. + * + * @since 0.3.0 + */ + public const CATEGORY_SITE = 'site'; + public const CATEGORY_USER = 'user'; /** * Registers the core abilities categories. * @@ -25,22 +32,22 @@ class WP_Core_Abilities { */ public static function register_category(): void { // Site-related capabilities - wp_register_ability_category( - 'site', - array( - 'label' => __( 'Site' ), - 'description' => __( 'Abilities that retrieve or modify site information and settings.' ), - ) - ); + 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( - 'user', - array( - 'label' => __( 'User' ), - 'description' => __( 'Abilities that retrieve or modify user information and settings.' ), - ) - ); + wp_register_ability_category( + self::CATEGORY_USER, + array( + 'label' => __( 'User' ), + 'description' => __( 'Abilities that retrieve or modify user information and settings.' ), + ) + ); } /** @@ -79,8 +86,8 @@ protected static function register_get_site_info(): void { 'wp/get-site-info', array( 'label' => __( 'Get Site Information' ), - 'description' => __( 'Returns a single site information field from get_bloginfo().' ), - 'category' => 'site', + 'description' => __( 'Returns a single site information field configured in WordPress (e.g., site name, URL, version) for display or diagnostics.' ), + 'category' => self::CATEGORY_SITE, 'input_schema' => array( 'type' => 'object', 'properties' => array( @@ -103,7 +110,7 @@ protected static function register_get_site_info(): void { ), 'value' => array( 'type' => 'string', - 'description' => __( 'The value returned by get_bloginfo().' ), + 'description' => __( 'The string value of the requested site information field.' ), ), ), 'additionalProperties' => false, @@ -114,19 +121,17 @@ protected static function register_get_site_info(): void { return array( 'field' => $field, - 'value' => (string) $value, + 'value' => $value, ); }, 'permission_callback' => static function (): bool { - // Site information can expose sensitive details; require admin capability. return current_user_can( 'manage_options' ); }, 'meta' => array( 'annotations' => array( - 'instructions' => __( 'Retrieves a single site property by passing an allowed field to get_bloginfo().' ), - 'readonly' => true, - 'destructive' => false, - 'idempotent' => true, + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, ), 'show_in_rest' => true, ), @@ -146,8 +151,8 @@ protected static function register_get_current_user_info(): void { 'wp/get-current-user-info', array( 'label' => __( 'Get Current User Information' ), - 'description' => __( 'Returns basic information about the current authenticated user.' ), - 'category' => 'user', + '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' ), @@ -199,12 +204,11 @@ protected static function register_get_current_user_info(): void { }, 'meta' => array( 'annotations' => array( - 'instructions' => __( 'Retrieves information about the current authenticated user.' ), - 'readonly' => true, - 'destructive' => false, - 'idempotent' => true, + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, ), - 'show_in_rest' => true, + 'show_in_rest' => false, ), ) ); @@ -222,31 +226,27 @@ protected static function register_get_environment_info(): void { 'wp/get-environment-info', array( 'label' => __( 'Get Environment Info' ), - 'description' => __( 'Returns basic information about the WordPress runtime environment.' ), - 'category' => 'site', + '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', 'mysql_version', 'wp_version', 'database_type' ), + 'required' => array( 'environment', 'php_version', 'db_server_info', 'wp_version' ), 'properties' => array( - 'environment' => array( + 'environment' => array( 'type' => 'string', - 'description' => __( 'The environment type returned by wp_get_environment_type().' ), + 'description' => __( 'The site\'s runtime environment classification (e.g., production, staging, development).' ), ), - 'php_version' => array( + 'php_version' => array( 'type' => 'string', - 'description' => __( 'The PHP version.' ), + 'description' => __( 'The PHP runtime version executing WordPress.' ), ), - 'mysql_version' => array( + 'db_server_info' => array( 'type' => 'string', - 'description' => __( 'The database server version (MySQL or MariaDB).' ), + 'description' => __( 'The database server vendor and version string reported by the driver (e.g., “8.0.34”, “10.11.6-MariaDB”).' ), ), - 'wp_version' => array( + 'wp_version' => array( 'type' => 'string', - 'description' => __( 'The WordPress version.' ), - ), - 'database_type' => array( - 'type' => 'string', - 'description' => __( 'The database server type (e.g., mysql or mariadb).' ), + 'description' => __( 'The WordPress core version running on this site.' ), ), ), 'additionalProperties' => false, @@ -256,35 +256,27 @@ protected static function register_get_environment_info(): void { $env = wp_get_environment_type(); $php_version = phpversion(); - $db_version = ''; - if ( isset( $wpdb ) && is_object( $wpdb ) && method_exists( $wpdb, 'db_version' ) ) { - $db_version = (string) $wpdb->db_version(); - } - $wp_version = (string) get_bloginfo( 'version' ); - - $type = 'mysql'; - if ( stripos( $db_version, 'mariadb' ) !== false ) { - $type = 'mariadb'; + $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' => (string) $env, - 'php_version' => (string) $php_version, - 'mysql_version' => (string) $db_version, - 'wp_version' => (string) $wp_version, - 'database_type' => (string) $type, + 'environment' => $env, + 'php_version' => $php_version, + 'db_server_info' => $db_server_info, + 'wp_version' => $wp_version, ); }, 'permission_callback' => static function (): bool { - // Environment information is restricted to administrators. return current_user_can( 'manage_options' ); }, 'meta' => array( 'annotations' => array( - 'instructions' => __( 'Retrieves environment information such as environment type, PHP, database, and WordPress versions.' ), - 'readonly' => true, - 'destructive' => false, - 'idempotent' => true, + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, ), 'show_in_rest' => true, ), diff --git a/tests/unit/abilities-api/wpCoreAbilities.php b/tests/unit/abilities-api/wpCoreAbilities.php index c16cb300..1be0a75f 100644 --- a/tests/unit/abilities-api/wpCoreAbilities.php +++ b/tests/unit/abilities-api/wpCoreAbilities.php @@ -146,9 +146,8 @@ public function test_core_get_environment_type_executes(): void { $this->assertIsArray( $ability_data ); $this->assertArrayHasKey( 'environment', $ability_data ); $this->assertArrayHasKey( 'php_version', $ability_data ); - $this->assertArrayHasKey( 'mysql_version', $ability_data ); + $this->assertArrayHasKey( 'db_server_info', $ability_data ); $this->assertArrayHasKey( 'wp_version', $ability_data ); - $this->assertArrayHasKey( 'database_type', $ability_data ); $this->assertSame( $environment, $ability_data['environment'] ); wp_set_current_user( 0 ); From 28b604014708fe923120a20fd7c2799ec41cbf42 Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Thu, 16 Oct 2025 00:32:35 -0400 Subject: [PATCH 08/16] Update includes/abilities/class-wp-core-abilities.php Co-authored-by: Jason Adams --- includes/abilities/class-wp-core-abilities.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index c993c5ab..eebd142a 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -242,7 +242,8 @@ protected static function register_get_environment_info(): void { ), 'db_server_info' => array( 'type' => 'string', - 'description' => __( 'The database server vendor and version string reported by the driver (e.g., “8.0.34”, “10.11.6-MariaDB”).' ), + 'description' => __( 'The database server vendor and version string reported by the driver.' ), + 'examples' => ['8.0.34', '10.11.6-MariaDB'] ), 'wp_version' => array( 'type' => 'string', From 93e85dff54b38003af2d314ff6ffeb0597d9b977 Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Thu, 16 Oct 2025 00:39:27 -0400 Subject: [PATCH 09/16] Fix code style and add environment examples in WP_Core_Abilities - Add example values for environment field in site info schema - Convert short array syntax to WordPress coding standards - Fix spacing alignment for consistency --- includes/abilities/class-wp-core-abilities.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index eebd142a..da1f81a2 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -235,6 +235,7 @@ protected static function register_get_environment_info(): void { 'environment' => array( 'type' => 'string', 'description' => __( 'The site\'s runtime environment classification (e.g., production, staging, development).' ), + 'examples' => array( 'production', 'staging', 'development', 'local' ), ), 'php_version' => array( 'type' => 'string', @@ -243,7 +244,7 @@ protected static function register_get_environment_info(): void { 'db_server_info' => array( 'type' => 'string', 'description' => __( 'The database server vendor and version string reported by the driver.' ), - 'examples' => ['8.0.34', '10.11.6-MariaDB'] + 'examples' => array( '8.0.34', '10.11.6-MariaDB' ), ), 'wp_version' => array( 'type' => 'string', From 4dce755a15694820701053d5a8a8cc4c40fb8061 Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:08:14 -0400 Subject: [PATCH 10/16] Apply suggestions from code review Co-authored-by: Dovid Levine --- .../abilities/class-wp-core-abilities.php | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index da1f81a2..d2bd9ccb 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -15,7 +15,7 @@ * @since 0.3.0 */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Core class intended for WordPress core. -class WP_Core_Abilities { +final class WP_Core_Abilities { /** * Category slugs for core abilities. * @@ -32,28 +32,28 @@ class WP_Core_Abilities { */ 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.' ), - ) - ); + 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.' ), - ) - ); + 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 + * @since 0.3.0 * * @return void */ From 00fa5c16f88ed52dd627e336a7ad702d534793a2 Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:28:00 -0400 Subject: [PATCH 11/16] Changed namespace back to core/ and not wp/ to align w/ GB --- .../abilities/class-wp-core-abilities.php | 14 +++++----- tests/unit/abilities-api/wpCoreAbilities.php | 26 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index d2bd9ccb..3f879a46 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -53,7 +53,7 @@ public static function register_category(): void { /** * Registers the default core abilities. * - * @since 0.3.0 + * @since 0.3.0 * * @return void */ @@ -64,7 +64,7 @@ public static function register(): void { } /** - * Registers the `wp/get-site-info` ability. + * Registers the `core/get-site-info` ability. * * @since 0.3.0 * @@ -83,7 +83,7 @@ protected static function register_get_site_info(): void { ); wp_register_ability( - 'wp/get-site-info', + 'core/get-site-info', array( 'label' => __( 'Get Site Information' ), 'description' => __( 'Returns a single site information field configured in WordPress (e.g., site name, URL, version) for display or diagnostics.' ), @@ -140,7 +140,7 @@ protected static function register_get_site_info(): void { } /** - * Registers the `wp/get-current-user-info` ability. + * Registers the `core/get-current-user-info` ability. * * @since 0.3.0 * @@ -148,7 +148,7 @@ protected static function register_get_site_info(): void { */ protected static function register_get_current_user_info(): void { wp_register_ability( - 'wp/get-current-user-info', + 'core/get-current-user-info', array( 'label' => __( 'Get Current User Information' ), 'description' => __( 'Returns basic profile details for the current authenticated user to support personalization, auditing, and access-aware behavior.' ), @@ -215,7 +215,7 @@ protected static function register_get_current_user_info(): void { } /** - * Registers the `wp/get-environment-info` ability. + * Registers the `core/get-environment-info` ability. * * @since 0.3.0 * @@ -223,7 +223,7 @@ protected static function register_get_current_user_info(): void { */ protected static function register_get_environment_info(): void { wp_register_ability( - 'wp/get-environment-info', + '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).' ), diff --git a/tests/unit/abilities-api/wpCoreAbilities.php b/tests/unit/abilities-api/wpCoreAbilities.php index 1be0a75f..c403cf67 100644 --- a/tests/unit/abilities-api/wpCoreAbilities.php +++ b/tests/unit/abilities-api/wpCoreAbilities.php @@ -15,14 +15,14 @@ public function set_up(): void { // Unregister core abilities if they were already registered to avoid duplicate registration warnings. $registry = WP_Abilities_Registry::get_instance(); - if ( $registry->is_registered( 'wp/get-site-info' ) ) { - $registry->unregister( 'wp/get-site-info' ); + if ( $registry->is_registered( 'core/get-site-info' ) ) { + $registry->unregister( 'core/get-site-info' ); } - if ( $registry->is_registered( 'wp/get-current-user-info' ) ) { - $registry->unregister( 'wp/get-current-user-info' ); + if ( $registry->is_registered( 'core/get-current-user-info' ) ) { + $registry->unregister( 'core/get-current-user-info' ); } - if ( $registry->is_registered( 'wp/get-environment-info' ) ) { - $registry->unregister( 'wp/get-environment-info' ); + if ( $registry->is_registered( 'core/get-environment-info' ) ) { + $registry->unregister( 'core/get-environment-info' ); } // Unregister categories if they exist. @@ -52,10 +52,10 @@ public function set_up(): void { } /** - * Tests that the `wp/get-site-info` ability is registered with the expected schema. + * Tests that the `core/get-site-info` ability is registered with the expected schema. */ public function test_core_get_bloginfo_ability_is_registered(): void { - $ability = wp_get_ability( 'wp/get-site-info' ); + $ability = wp_get_ability( 'core/get-site-info' ); $this->assertInstanceOf( WP_Ability::class, $ability ); $this->assertTrue( $ability->get_meta_item( 'show_in_rest', false ) ); @@ -66,14 +66,14 @@ public function test_core_get_bloginfo_ability_is_registered(): void { } /** - * Tests executing the `wp/get-site-info` ability. + * Tests executing the `core/get-site-info` ability. */ public function test_core_get_bloginfo_executes(): void { // Requires manage_options. $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); wp_set_current_user( $admin_id ); - $ability = wp_get_ability( 'wp/get-site-info' ); + $ability = wp_get_ability( 'core/get-site-info' ); $result = $ability->execute( array( @@ -96,7 +96,7 @@ public function test_core_get_bloginfo_executes(): void { * Tests that executing the current user info ability requires authentication. */ public function test_core_get_current_user_info_requires_authentication(): void { - $ability = wp_get_ability( 'wp/get-current-user-info' ); + $ability = wp_get_ability( 'core/get-current-user-info' ); $this->assertFalse( $ability->check_permissions() ); @@ -118,7 +118,7 @@ public function test_core_get_current_user_info_returns_user_data(): void { wp_set_current_user( $user_id ); - $ability = wp_get_ability( 'wp/get-current-user-info' ); + $ability = wp_get_ability( 'core/get-current-user-info' ); $this->assertTrue( $ability->check_permissions() ); @@ -139,7 +139,7 @@ public function test_core_get_environment_type_executes(): void { $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); wp_set_current_user( $admin_id ); - $ability = wp_get_ability( 'wp/get-environment-info' ); + $ability = wp_get_ability( 'core/get-environment-info' ); $environment = wp_get_environment_type(); $ability_data = $ability->execute(); From afd83c96af6f7309e0f0c217c916160292451d9d Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:40:28 -0400 Subject: [PATCH 12/16] Renaming Get Current User info to Get User Info to allow for flexibility in the future --- includes/abilities/class-wp-core-abilities.php | 6 +++--- tests/unit/abilities-api/wpCoreAbilities.php | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index 3f879a46..62e3cb4d 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -140,7 +140,7 @@ protected static function register_get_site_info(): void { } /** - * Registers the `core/get-current-user-info` ability. + * Registers the `core/get-user-info` ability. * * @since 0.3.0 * @@ -148,9 +148,9 @@ protected static function register_get_site_info(): void { */ protected static function register_get_current_user_info(): void { wp_register_ability( - 'core/get-current-user-info', + 'core/get-user-info', array( - 'label' => __( 'Get Current User Information' ), + '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( diff --git a/tests/unit/abilities-api/wpCoreAbilities.php b/tests/unit/abilities-api/wpCoreAbilities.php index c403cf67..0a866a85 100644 --- a/tests/unit/abilities-api/wpCoreAbilities.php +++ b/tests/unit/abilities-api/wpCoreAbilities.php @@ -18,8 +18,8 @@ public function set_up(): void { if ( $registry->is_registered( 'core/get-site-info' ) ) { $registry->unregister( 'core/get-site-info' ); } - if ( $registry->is_registered( 'core/get-current-user-info' ) ) { - $registry->unregister( 'core/get-current-user-info' ); + if ( $registry->is_registered( 'core/get-user-info' ) ) { + $registry->unregister( 'core/get-user-info' ); } if ( $registry->is_registered( 'core/get-environment-info' ) ) { $registry->unregister( 'core/get-environment-info' ); @@ -96,7 +96,7 @@ public function test_core_get_bloginfo_executes(): void { * Tests that executing the current user info ability requires authentication. */ public function test_core_get_current_user_info_requires_authentication(): void { - $ability = wp_get_ability( 'core/get-current-user-info' ); + $ability = wp_get_ability( 'core/get-user-info' ); $this->assertFalse( $ability->check_permissions() ); @@ -118,7 +118,7 @@ public function test_core_get_current_user_info_returns_user_data(): void { wp_set_current_user( $user_id ); - $ability = wp_get_ability( 'core/get-current-user-info' ); + $ability = wp_get_ability( 'core/get-user-info' ); $this->assertTrue( $ability->check_permissions() ); From 766490bd7bb0a6654d99aabb4dc9f04b08b1c2f3 Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:45:16 -0400 Subject: [PATCH 13/16] Update function name for user info too --- includes/abilities/class-wp-core-abilities.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index 62e3cb4d..7c562edf 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -59,7 +59,7 @@ public static function register_category(): void { */ public static function register(): void { self::register_get_site_info(); - self::register_get_current_user_info(); + self::register_get_user_info(); self::register_get_environment_info(); } @@ -146,7 +146,7 @@ protected static function register_get_site_info(): void { * * @return void */ - protected static function register_get_current_user_info(): void { + protected static function register_get_user_info(): void { wp_register_ability( 'core/get-user-info', array( From 8f09665ff034eb0c634977894df47de73574c2b2 Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:56:30 -0400 Subject: [PATCH 14/16] Rewrite Site Info ability to return all fields by default w/ self documenting schema + tests --- .../abilities/class-wp-core-abilities.php | 61 +++++++++++++------ tests/unit/abilities-api/wpCoreAbilities.php | 44 +++++++++---- 2 files changed, 76 insertions(+), 29 deletions(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index 7c562edf..30225db2 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -86,43 +86,70 @@ protected static function register_get_site_info(): void { 'core/get-site-info', array( 'label' => __( 'Get Site Information' ), - 'description' => __( 'Returns a single site information field configured in WordPress (e.g., site name, URL, version) for display or diagnostics.' ), + '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', 'properties' => array( - 'field' => array( - 'type' => 'string', - 'enum' => $fields, - 'description' => __( 'The site information field to retrieve.' ), + 'fields' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'enum' => $fields, + ), + 'description' => __( 'Optional: Limit response to specific fields. If omitted, all fields are returned.' ), ), ), - 'required' => array( 'field' ), 'additionalProperties' => false, ), 'output_schema' => array( 'type' => 'object', - 'required' => array( 'field', 'value' ), 'properties' => array( - 'field' => 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 requested site information field.' ), + 'description' => __( 'The WordPress installation URL.' ), ), - 'value' => array( + 'admin_email' => array( 'type' => 'string', - 'description' => __( 'The string value of the requested site information field.' ), + '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 { - $field = $input['field']; - $value = get_bloginfo( $field ); + $all_fields = array( 'name', 'description', 'url', 'wpurl', 'admin_email', 'charset', 'language', 'version' ); + $requested_fields = ! empty( $input['fields'] ) ? $input['fields'] : $all_fields; - return array( - 'field' => $field, - 'value' => $value, - ); + $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' ); diff --git a/tests/unit/abilities-api/wpCoreAbilities.php b/tests/unit/abilities-api/wpCoreAbilities.php index 0a866a85..a3d87d04 100644 --- a/tests/unit/abilities-api/wpCoreAbilities.php +++ b/tests/unit/abilities-api/wpCoreAbilities.php @@ -60,13 +60,22 @@ public function test_core_get_bloginfo_ability_is_registered(): void { $this->assertInstanceOf( WP_Ability::class, $ability ); $this->assertTrue( $ability->get_meta_item( 'show_in_rest', false ) ); - $input_schema = $ability->get_input_schema(); - $this->assertSame( array( 'field' ), $input_schema['required'] ); - $this->assertContains( 'name', $input_schema['properties']['field']['enum'] ); + $input_schema = $ability->get_input_schema(); + $output_schema = $ability->get_output_schema(); + + // Input schema should have optional fields array. + $this->assertArrayHasKey( 'fields', $input_schema['properties'] ); + $this->assertSame( 'array', $input_schema['properties']['fields']['type'] ); + $this->assertContains( 'name', $input_schema['properties']['fields']['items']['enum'] ); + + // Output schema should have all fields documented. + $this->assertArrayHasKey( 'name', $output_schema['properties'] ); + $this->assertArrayHasKey( 'url', $output_schema['properties'] ); + $this->assertArrayHasKey( 'version', $output_schema['properties'] ); } /** - * Tests executing the `core/get-site-info` ability. + * Tests executing the `core/get-site-info` ability returns all fields by default. */ public function test_core_get_bloginfo_executes(): void { // Requires manage_options. @@ -75,19 +84,30 @@ public function test_core_get_bloginfo_executes(): void { $ability = wp_get_ability( 'core/get-site-info' ); + // Test without fields parameter - should return all fields. + $result = $ability->execute( array() ); + + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'name', $result ); + $this->assertArrayHasKey( 'description', $result ); + $this->assertArrayHasKey( 'url', $result ); + $this->assertArrayHasKey( 'version', $result ); + $this->assertSame( get_bloginfo( 'name' ), $result['name'] ); + + // Test with fields parameter - should return only requested fields. $result = $ability->execute( array( - 'field' => 'name', + 'fields' => array( 'name', 'url' ), ) ); - $this->assertSame( - array( - 'field' => 'name', - 'value' => get_bloginfo( 'name' ), - ), - $result - ); + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'name', $result ); + $this->assertArrayHasKey( 'url', $result ); + $this->assertArrayNotHasKey( 'description', $result ); + $this->assertArrayNotHasKey( 'version', $result ); + $this->assertSame( get_bloginfo( 'name' ), $result['name'] ); + $this->assertSame( get_bloginfo( 'url' ), $result['url'] ); wp_set_current_user( 0 ); } From b7b69a184b45f6faca65c407af4565466e9f88b2 Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:16:46 -0400 Subject: [PATCH 15/16] Normalize ability inputs with schema defaults for empty REST calls --- includes/abilities-api/class-wp-ability.php | 23 +++++++++++++++++++ .../abilities/class-wp-core-abilities.php | 5 +++- ...class-wp-rest-abilities-run-controller.php | 4 ++-- tests/unit/abilities-api/wpCoreAbilities.php | 6 ++++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/includes/abilities-api/class-wp-ability.php b/includes/abilities-api/class-wp-ability.php index e31ce5ba..84181db5 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -320,6 +320,27 @@ public function get_input_schema(): array { return $this->input_schema; } + /** + * Applies the defined input default when no input is provided. + * + * @since 0.4.0 + * + * @param mixed $input Optional. The raw input provided for the ability. Default `null`. + * @return mixed The input with the schema default applied when available. + */ + public function normalize_input( $input = null ) { + if ( null !== $input ) { + return $input; + } + + $input_schema = $this->get_input_schema(); + if ( ! empty( $input_schema ) && array_key_exists( 'default', $input_schema ) ) { + return $input_schema['default']; + } + + return null; + } + /** * Retrieves the output schema for the ability. * @@ -436,6 +457,7 @@ protected function invoke_callback( callable $callback, $input = null ) { * @return bool|\WP_Error Whether the ability has the necessary permission. */ public function check_permissions( $input = null ) { + $input = $this->normalize_input( $input ); $is_valid = $this->validate_input( $input ); if ( is_wp_error( $is_valid ) ) { return $is_valid; @@ -522,6 +544,7 @@ protected function validate_output( $output ) { * @return mixed|\WP_Error The result of the ability execution, or WP_Error on failure. */ public function execute( $input = null ) { + $input = $this->normalize_input( $input ); $has_permissions = $this->check_permissions( $input ); if ( true !== $has_permissions ) { if ( is_wp_error( $has_permissions ) ) { diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index 30225db2..5fee4dfe 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -101,6 +101,7 @@ protected static function register_get_site_info(): void { ), ), 'additionalProperties' => false, + 'default' => array(), ), 'output_schema' => array( 'type' => 'object', @@ -141,6 +142,7 @@ protected static function register_get_site_info(): void { '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; @@ -151,7 +153,8 @@ protected static function register_get_site_info(): void { return $result; }, - 'permission_callback' => static function (): bool { + 'permission_callback' => static function ( $input = null ): bool { + unset( $input ); return current_user_can( 'manage_options' ); }, 'meta' => array( diff --git a/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php b/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php index 5fdbfea4..a1c663a0 100644 --- a/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php +++ b/includes/rest-api/endpoints/class-wp-rest-abilities-run-controller.php @@ -132,7 +132,7 @@ public function run_ability( $request ) { ); } - $input = $this->get_input_from_request( $request ); + $input = $ability->normalize_input( $this->get_input_from_request( $request ) ); $result = $ability->execute( $input ); if ( is_wp_error( $result ) ) { if ( 'ability_invalid_input' === $result->get_error_code() ) { @@ -162,7 +162,7 @@ public function run_ability_permissions_check( $request ) { ); } - $input = $this->get_input_from_request( $request ); + $input = $ability->normalize_input( $this->get_input_from_request( $request ) ); if ( ! $ability->check_permissions( $input ) ) { return new \WP_Error( 'rest_ability_cannot_execute', diff --git a/tests/unit/abilities-api/wpCoreAbilities.php b/tests/unit/abilities-api/wpCoreAbilities.php index a3d87d04..16203a77 100644 --- a/tests/unit/abilities-api/wpCoreAbilities.php +++ b/tests/unit/abilities-api/wpCoreAbilities.php @@ -63,6 +63,10 @@ public function test_core_get_bloginfo_ability_is_registered(): void { $input_schema = $ability->get_input_schema(); $output_schema = $ability->get_output_schema(); + $this->assertSame( 'object', $input_schema['type'] ); + $this->assertArrayHasKey( 'default', $input_schema ); + $this->assertSame( array(), $input_schema['default'] ); + // Input schema should have optional fields array. $this->assertArrayHasKey( 'fields', $input_schema['properties'] ); $this->assertSame( 'array', $input_schema['properties']['fields']['type'] ); @@ -85,7 +89,7 @@ public function test_core_get_bloginfo_executes(): void { $ability = wp_get_ability( 'core/get-site-info' ); // Test without fields parameter - should return all fields. - $result = $ability->execute( array() ); + $result = $ability->execute(); $this->assertIsArray( $result ); $this->assertArrayHasKey( 'name', $result ); From 73a2092b5ed9ab24ada74dc114ade8a4e335b3e6 Mon Sep 17 00:00:00 2001 From: James LePage <36246732+Jameswlepage@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:26:57 -0400 Subject: [PATCH 16/16] drop input var on site permission callback --- includes/abilities/class-wp-core-abilities.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/abilities/class-wp-core-abilities.php b/includes/abilities/class-wp-core-abilities.php index 5fee4dfe..66635acd 100644 --- a/includes/abilities/class-wp-core-abilities.php +++ b/includes/abilities/class-wp-core-abilities.php @@ -153,8 +153,7 @@ protected static function register_get_site_info(): void { return $result; }, - 'permission_callback' => static function ( $input = null ): bool { - unset( $input ); + 'permission_callback' => static function (): bool { return current_user_can( 'manage_options' ); }, 'meta' => array(