Skip to content
101 changes: 66 additions & 35 deletions src/wp-includes/abilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,35 +46,43 @@ function wp_register_core_abilities(): void {
$site_info_properties = array(
'name' => array(
'type' => 'string',
'title' => __( 'Site Title' ),
'description' => __( 'The site title.' ),
),
'description' => array(
'type' => 'string',
'title' => __( 'Tagline' ),
'description' => __( 'The site tagline.' ),
),
'url' => array(
'type' => 'string',
'description' => __( 'The site home URL.' ),
'title' => __( 'Site Address (URL)' ),
'description' => __( 'The public URL where visitors access the site. May differ from the WordPress installation URL.' ),
),
'wpurl' => array(
'type' => 'string',
'description' => __( 'The WordPress installation URL.' ),
'title' => __( 'WordPress Address (URL)' ),
'description' => __( 'The URL where WordPress core files are served. May differ from the public site URL.' ),
),
'admin_email' => array(
'type' => 'string',
'title' => __( 'Administration Email Address' ),
'description' => __( 'The site administrator email address.' ),
),
'charset' => array(
'type' => 'string',
'title' => __( 'Site Charset' ),
'description' => __( 'The site character encoding.' ),
),
'language' => array(
'type' => 'string',
'description' => __( 'The site language locale code.' ),
'title' => __( 'Site Language' ),
'description' => __( 'The site locale in dash form (e.g. en-US).' ),
),
'version' => array(
'type' => 'string',
'description' => __( 'The WordPress version.' ),
'title' => __( 'WordPress Version' ),
'description' => __( 'The WordPress core version running on this site.' ),
),
);
$site_info_fields = array_keys( $site_info_properties );
Expand Down Expand Up @@ -138,7 +146,7 @@ function wp_register_core_abilities(): void {
'id' => array(
'type' => 'integer',
'title' => __( 'User ID' ),
'description' => __( 'Unique numeric identifier for the user.' ),
'description' => __( 'Unique identifier for the user.' ),
),
'display_name' => array(
'type' => 'string',
Expand Down Expand Up @@ -186,7 +194,7 @@ function wp_register_core_abilities(): void {
'description' => array(
'type' => 'string',
'title' => __( 'Biographical Info' ),
'description' => __( 'User-authored biography, often shown on author pages.' ),
'description' => __( 'User-authored biography. May be empty.' ),
),
'user_url' => array(
'type' => 'string',
Expand Down Expand Up @@ -253,58 +261,81 @@ function wp_register_core_abilities(): void {
'destructive' => false,
'idempotent' => true,
),
'show_in_rest' => false,
'show_in_rest' => true,
),
)
);

$environment_info_properties = array(
'environment' => array(
'type' => 'string',
'title' => __( 'Environment Type' ),
'description' => __( 'The site\'s runtime environment classification.' ),
'enum' => array( 'production', 'staging', 'development', 'local' ),
),
'php_version' => array(
'type' => 'string',
'title' => __( 'PHP Version' ),
'description' => __( 'The PHP runtime version executing WordPress.' ),
),
'db_server_info' => array(
'type' => 'string',
'title' => __( 'Database Server Info' ),
'description' => __( 'The database server vendor and version string reported by the driver.' ),
),
'wp_version' => array(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that the WordPress version is included in two site abilities. I’ve aligned the title and description to reflect this.

'type' => 'string',
'title' => __( 'WordPress Version' ),
'description' => __( 'The WordPress core version running on this site.' ),
),
);
$environment_info_fields = array_keys( $environment_info_properties );

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).' ),
'description' => __( 'Returns core details about the site\'s runtime context for diagnostics and compatibility (environment, PHP runtime, database server info, WordPress version). By default returns all fields, or optionally a filtered subset.' ),
'category' => $category_site,
'output_schema' => array(
'input_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.' ),
),
'wp_version' => array(
'type' => 'string',
'description' => __( 'The WordPress core version running on this site.' ),
'fields' => array(
'type' => 'array',
'items' => array(
'type' => 'string',
'enum' => $environment_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' => $environment_info_properties,
'additionalProperties' => false,
),
'execute_callback' => static function (): array {
'execute_callback' => static function ( $input = array() ) use ( $environment_info_fields ): array {
global $wpdb;

$env = wp_get_environment_type();
$php_version = phpversion();
$db_server_info = '';
$input = is_array( $input ) ? $input : array();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$input = is_array( $input ) ? $input : array();
/** @var array{ fields?: string[] } $input */
$input = is_array( $input ) ? $input : array();

This addresses the following PHPStan issue:

  338    Parameter #1 $input of function array_flip expects array<int|string>, mixed given.
         🪪  argument.type

$requested_fields = ! empty( $input['fields'] ) ? $input['fields'] : $environment_info_fields;

$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,
$all = array(
'environment' => wp_get_environment_type(),
'php_version' => phpversion(),
'db_server_info' => $db_server_info,
'wp_version' => $wp_version,
'wp_version' => get_bloginfo( 'version' ),
);

return array_intersect_key( $all, array_flip( $requested_fields ) );
},
'permission_callback' => static function (): bool {
return current_user_can( 'manage_options' );
Expand Down
106 changes: 92 additions & 14 deletions tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,18 @@ public function test_core_get_site_info_ability_is_registered(): void {
$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'] );
$expected_fields = array( 'name', 'description', 'url', 'wpurl', 'admin_email', 'charset', 'language', 'version' );

$this->assertSame( $expected_fields, $input_schema['properties']['fields']['items']['enum'] );
$this->assertSame( $expected_fields, array_keys( $output_schema['properties'] ) );

foreach ( $expected_fields as $field ) {
$this->assertArrayHasKey( 'title', $output_schema['properties'][ $field ] );
$this->assertArrayHasKey( 'description', $output_schema['properties'][ $field ] );
}
}

/**
Expand Down Expand Up @@ -200,25 +203,23 @@ public function test_core_get_user_info_ability_is_registered(): void {
$ability = wp_get_ability( 'core/get-user-info' );

$this->assertInstanceOf( WP_Ability::class, $ability );
$this->assertTrue( $ability->get_meta_item( 'show_in_rest', false ) );

$input_schema = $ability->get_input_schema();
$output_schema = $ability->get_output_schema();

// Input schema should expose an optional `fields` array with an enum of valid field names.
$this->assertSame( 'object', $input_schema['type'] );
$this->assertArrayHasKey( 'default', $input_schema );
$this->assertSame( array(), $input_schema['default'] );
$this->assertArrayHasKey( 'fields', $input_schema['properties'] );
$this->assertSame( 'array', $input_schema['properties']['fields']['type'] );

$enum = $input_schema['properties']['fields']['items']['enum'];
foreach ( array( 'id', 'display_name', 'first_name', 'last_name', 'nickname', 'description', 'user_url' ) as $field ) {
$this->assertContains( $field, $enum );
}
$expected_fields = array( 'id', 'display_name', 'user_nicename', 'user_login', 'roles', 'locale', 'first_name', 'last_name', 'nickname', 'description', 'user_url' );

$this->assertSame( $expected_fields, $input_schema['properties']['fields']['items']['enum'] );
$this->assertSame( $expected_fields, array_keys( $output_schema['properties'] ) );

// Output schema should document the original and new profile fields with title + description.
foreach ( array( 'id', 'display_name', 'first_name', 'last_name', 'nickname', 'description', 'user_url' ) as $field ) {
$this->assertArrayHasKey( $field, $output_schema['properties'] );
foreach ( $expected_fields as $field ) {
$this->assertArrayHasKey( 'title', $output_schema['properties'][ $field ] );
$this->assertArrayHasKey( 'description', $output_schema['properties'][ $field ] );
}
Expand Down Expand Up @@ -298,6 +299,83 @@ public function test_core_get_environment_info_executes(): void {
$this->assertSame( $environment, $ability_data['environment'] );
}

/**
* Tests that the `core/get-environment-info` ability is registered with the expected schema.
*
* @ticket 65355
*/
public function test_core_get_environment_info_ability_is_registered(): void {
$ability = wp_get_ability( 'core/get-environment-info' );

$this->assertInstanceOf( WP_Ability::class, $ability );
$this->assertTrue( $ability->get_meta_item( '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'] );
$this->assertArrayHasKey( 'fields', $input_schema['properties'] );
$this->assertSame( 'array', $input_schema['properties']['fields']['type'] );

$expected_fields = array( 'environment', 'php_version', 'db_server_info', 'wp_version' );

$this->assertSame( $expected_fields, $input_schema['properties']['fields']['items']['enum'] );
$this->assertSame( $expected_fields, array_keys( $output_schema['properties'] ) );

foreach ( $expected_fields as $field ) {
$this->assertArrayHasKey( 'title', $output_schema['properties'][ $field ] );
$this->assertArrayHasKey( 'description', $output_schema['properties'][ $field ] );
}
}

/**
* Tests that the `core/get-environment-info` ability filters its output by the `fields` input parameter.
*
* @ticket 65355
*/
public function test_core_get_environment_info_filters_fields(): void {
$admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
wp_set_current_user( $admin_id );

$ability = wp_get_ability( 'core/get-environment-info' );

$result = $ability->execute(
array(
'fields' => array( 'environment', 'wp_version' ),
)
);

$this->assertIsArray( $result );
$this->assertCount( 2, $result );
$this->assertArrayHasKey( 'environment', $result );
$this->assertArrayHasKey( 'wp_version', $result );
$this->assertArrayNotHasKey( 'php_version', $result );
$this->assertArrayNotHasKey( 'db_server_info', $result );
}

/**
* Tests that the `core/get-environment-info` ability rejects unknown field names via schema validation.
*
* @ticket 65355
*/
public function test_core_get_environment_info_rejects_invalid_fields(): void {
$admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
wp_set_current_user( $admin_id );

$ability = wp_get_ability( 'core/get-environment-info' );

$result = $ability->execute(
array(
'fields' => array( 'environment', 'not_a_real_field' ),
)
);

$this->assertWPError( $result );
$this->assertSame( 'ability_invalid_input', $result->get_error_code() );
}

/**
* Tests that all core ability schemas only use valid JSON Schema keywords.
*
Expand Down
Loading