diff --git a/src/wp-includes/abilities/wp-core-abilities.php b/src/wp-includes/abilities/wp-core-abilities.php new file mode 100644 index 0000000000000..610cf68f27c69 --- /dev/null +++ b/src/wp-includes/abilities/wp-core-abilities.php @@ -0,0 +1,270 @@ + __( 'Site' ), + 'description' => __( 'Abilities that retrieve or modify site information and settings.' ), + ) + ); + + wp_register_ability_category( + 'user', + array( + 'label' => __( 'User' ), + 'description' => __( 'Abilities that retrieve or modify user information and settings.' ), + ) + ); +} + +/** + * Registers the default core abilities. + * + * @since 6.9.0 + * + * @return void + */ +function wp_register_core_abilities(): void { + $category_site = 'site'; + $category_user = 'user'; + + $site_info_fields = array( + 'name', + 'description', + 'url', + 'wpurl', + 'admin_email', + 'charset', + 'language', + 'version', + ); + + wp_register_ability( + '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' => $category_site, + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'fields' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'enum' => $site_info_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() ) use ( $site_info_fields ): array { + $input = is_array( $input ) ? $input : array(); + $requested_fields = ! empty( $input['fields'] ) ? $input['fields'] : $site_info_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, + ), + ) + ); + + 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' => $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', + ), + ), + '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(); + + 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, + ), + ) + ); + + 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' => $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 (can be one of these: production, staging, development, local).' ), + 'enum' => 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 ( 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, + ), + ) + ); +} diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index ba2776506395c..4a386eb53cb65 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -532,6 +532,10 @@ add_action( 'rest_api_init', 'create_initial_rest_routes', 99 ); add_action( 'parse_request', 'rest_api_loaded' ); +// Abilities API. +add_action( 'wp_abilities_api_categories_init', 'wp_register_core_ability_categories' ); +add_action( 'wp_abilities_api_init', 'wp_register_core_abilities' ); + // Sitemaps actions. add_action( 'init', 'wp_sitemaps_get_server' ); diff --git a/src/wp-settings.php b/src/wp-settings.php index 256a0f6791427..471c22b0fd7df 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -290,6 +290,7 @@ require ABSPATH . WPINC . '/abilities-api/class-wp-ability.php'; require ABSPATH . WPINC . '/abilities-api/class-wp-abilities-registry.php'; require ABSPATH . WPINC . '/abilities-api.php'; +require ABSPATH . WPINC . '/abilities/wp-core-abilities.php'; require ABSPATH . WPINC . '/rest-api.php'; require ABSPATH . WPINC . '/rest-api/class-wp-rest-server.php'; require ABSPATH . WPINC . '/rest-api/class-wp-rest-response.php'; diff --git a/tests/phpunit/includes/functions.php b/tests/phpunit/includes/functions.php index 898f1214ab15a..c0e78f5dd3787 100644 --- a/tests/phpunit/includes/functions.php +++ b/tests/phpunit/includes/functions.php @@ -364,3 +364,26 @@ function _unhook_font_registration() { remove_action( 'init', '_wp_register_default_font_collections' ); } tests_add_filter( 'init', '_unhook_font_registration', 1000 ); + +/** + * Before the abilities API categories init action runs, unhook the core ability + * categories registration function to prevent core categories from being registered + * during tests. + * + * @since 6.9.0 + */ +function _unhook_core_ability_categories_registration() { + remove_action( 'wp_abilities_api_categories_init', 'wp_register_core_ability_categories' ); +} +tests_add_filter( 'wp_abilities_api_categories_init', '_unhook_core_ability_categories_registration', 1 ); + +/** + * Before the abilities API init action runs, unhook the core abilities + * registration function to prevent core abilities from being registered during tests. + * + * @since 6.9.0 + */ +function _unhook_core_abilities_registration() { + remove_action( 'wp_abilities_api_init', 'wp_register_core_abilities' ); +} +tests_add_filter( 'wp_abilities_api_init', '_unhook_core_abilities_registration', 1 ); diff --git a/tests/phpunit/tests/abilities-api/wpCoreAbilities.php b/tests/phpunit/tests/abilities-api/wpCoreAbilities.php new file mode 100644 index 0000000000000..f87361f0e77f2 --- /dev/null +++ b/tests/phpunit/tests/abilities-api/wpCoreAbilities.php @@ -0,0 +1,157 @@ +assertInstanceOf( WP_Ability::class, $ability ); + $this->assertTrue( $ability->get_meta_item( 'show_in_rest', false ) ); + + $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'] ); + $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 returns all fields by default. + * @ticket 64146 + */ + public function test_core_get_site_info_executes(): void { + // Requires manage_options. + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + $ability = wp_get_ability( 'core/get-site-info' ); + + // Test without fields parameter - should return all fields. + $result = $ability->execute(); + + $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( + 'fields' => array( 'name', 'url' ), + ) + ); + + $this->assertIsArray( $result ); + $this->assertCount( 2, $result ); + $this->assertSame( get_bloginfo( 'name' ), $result['name'] ); + $this->assertSame( get_bloginfo( 'url' ), $result['url'] ); + } + + /** + * Tests that executing the current user info ability requires authentication. + * @ticket 64146 + */ + public function test_core_get_current_user_info_requires_authentication(): void { + $ability = wp_get_ability( 'core/get-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. + * @ticket 64146 + */ + 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-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'] ); + } + + /** + * Tests executing the environment info ability. + * @ticket 64146 + */ + public function test_core_get_environment_info_executes(): void { + // Requires manage_options. + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + $ability = wp_get_ability( 'core/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( 'db_server_info', $ability_data ); + $this->assertArrayHasKey( 'wp_version', $ability_data ); + $this->assertSame( $environment, $ability_data['environment'] ); + } +}