diff --git a/src/wp-includes/abilities.php b/src/wp-includes/abilities.php index 4c6db1ed830e0..132ca6fc673a3 100644 --- a/src/wp-includes/abilities.php +++ b/src/wp-includes/abilities.php @@ -9,6 +9,8 @@ declare( strict_types = 1 ); +require_once __DIR__ . '/abilities/class-wp-settings-abilities.php'; + /** * Registers the core ability categories. * @@ -257,4 +259,6 @@ function wp_register_core_abilities(): void { ), ) ); + + WP_Settings_Abilities::register(); } diff --git a/src/wp-includes/abilities/class-wp-settings-abilities.php b/src/wp-includes/abilities/class-wp-settings-abilities.php new file mode 100644 index 0000000000000..5af7fa48450ee --- /dev/null +++ b/src/wp-includes/abilities/class-wp-settings-abilities.php @@ -0,0 +1,343 @@ + args for allowed settings. + */ + private static function get_allowed_settings(): array { + $settings = array(); + + foreach ( get_registered_settings() as $option_name => $args ) { + if ( ! empty( $args['show_in_abilities'] ) ) { + $settings[ $option_name ] = $args; + } + } + + return $settings; + } + + /** + * Gets unique setting groups that have show_in_abilities enabled. + * + * @since 7.0.0 + * + * @return string[] List of unique group names. + */ + private static function get_available_groups(): array { + $groups = array(); + + foreach ( self::get_allowed_settings() as $args ) { + $group = $args['group'] ?? 'general'; + if ( ! in_array( $group, $groups, true ) ) { + $groups[] = $group; + } + } + + sort( $groups ); + + return $groups; + } + + /** + * Gets unique setting slugs that have show_in_abilities enabled. + * + * @since 7.0.0 + * + * @return string[] List of unique setting slugs. + */ + private static function get_available_slugs(): array { + $slugs = array(); + + foreach ( self::get_allowed_settings() as $option_name => $args ) { + $slugs[] = $option_name; + } + + sort( $slugs ); + + return $slugs; + } + + /** + * Builds a rich output schema from registered settings metadata. + * + * Creates a JSON Schema that documents each setting group and its settings + * with their types, titles, descriptions, defaults, and any additional + * schema properties from show_in_rest. + * + * @since 7.0.0 + * + * @return array JSON Schema for the output. + */ + private static function build_output_schema(): array { + $group_properties = array(); + + foreach ( self::get_allowed_settings() as $option_name => $args ) { + $group = $args['group'] ?? 'general'; + + $setting_schema = array( + 'type' => $args['type'] ?? 'string', + ); + + if ( ! empty( $args['label'] ) ) { + $setting_schema['title'] = $args['label']; + } + + if ( ! empty( $args['description'] ) ) { + $setting_schema['description'] = $args['description']; + } elseif ( ! empty( $args['label'] ) ) { + $setting_schema['description'] = $args['label']; + } + + if ( ! isset( $group_properties[ $group ] ) ) { + $group_properties[ $group ] = array( + 'type' => 'object', + 'properties' => array(), + 'additionalProperties' => false, + ); + } + + $group_properties[ $group ]['properties'][ $option_name ] = $setting_schema; + } + + ksort( $group_properties ); + + return array( + 'type' => 'object', + 'description' => __( 'Settings grouped by registration group. Each group contains settings with their current values.' ), + 'properties' => $group_properties, + 'additionalProperties' => false, + ); + } + + /** + * Registers the core/get-settings ability. + * + * @since 7.0.0 + * + * @return void + */ + private static function register_get_settings(): void { + wp_register_ability( + 'core/get-settings', + array( + 'label' => __( 'Get Settings' ), + 'description' => __( 'Returns registered WordPress settings grouped by their registration group. Returns key-value pairs per setting.' ), + 'category' => 'site', + 'input_schema' => array( + 'default' => (object) array(), + 'oneOf' => array( + // Branch 1: No filter (empty object). + array( + 'type' => 'object', + 'additionalProperties' => false, + 'maxProperties' => 0, + ), + // Branch 2: Filter by group only. + array( + 'type' => 'object', + 'properties' => array( + 'group' => array( + 'type' => 'string', + 'description' => __( 'Filter settings by group name.' ), + 'enum' => self::$available_groups, + ), + ), + 'required' => array( 'group' ), + 'additionalProperties' => false, + ), + // Branch 3: Filter by slugs only. + array( + 'type' => 'object', + 'properties' => array( + 'slugs' => array( + 'type' => 'array', + 'description' => __( 'Filter settings by specific setting slugs.' ), + 'items' => array( + 'type' => 'string', + 'enum' => self::$available_slugs, + ), + ), + ), + 'required' => array( 'slugs' ), + 'additionalProperties' => false, + ), + ), + ), + 'output_schema' => self::$output_schema, + 'execute_callback' => array( __CLASS__, 'execute_get_settings' ), + 'permission_callback' => array( __CLASS__, 'check_manage_options' ), + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + } + + /** + * Permission callback for settings abilities. + * + * @since 7.0.0 + * + * @return bool True if the current user can manage options, false otherwise. + */ + public static function check_manage_options(): bool { + return current_user_can( 'manage_options' ); + } + + /** + * Execute callback for core/get-settings ability. + * + * Retrieves all registered settings that are exposed through the Abilities API, + * grouped by their registration group. + * + * @since 7.0.0 + * + * @param array $input { + * Optional. Input parameters. + * + * @type string $group Optional. Filter settings by group name. Cannot be used with slugs. + * @type string[] $slugs Optional. Filter settings by specific setting slugs. Cannot be used with group. + * } + * @return array Settings grouped by registration group. + */ + public static function execute_get_settings( $input = array() ): array { + $input = is_array( $input ) ? $input : array(); + $filter_group = ! empty( $input['group'] ) ? $input['group'] : null; + $filter_slugs = ! empty( $input['slugs'] ) ? $input['slugs'] : null; + + $settings_by_group = array(); + + foreach ( self::get_allowed_settings() as $option_name => $args ) { + $group = $args['group'] ?? 'general'; + + if ( $filter_group && $group !== $filter_group ) { + continue; + } + + if ( $filter_slugs && ! in_array( $option_name, $filter_slugs, true ) ) { + continue; + } + + $default = $args['default'] ?? null; + + $value = get_option( $option_name, $default ); + $value = self::cast_value( $value, $args['type'] ?? 'string' ); + + if ( ! isset( $settings_by_group[ $group ] ) ) { + $settings_by_group[ $group ] = array(); + } + + $settings_by_group[ $group ][ $option_name ] = $value; + } + + ksort( $settings_by_group ); + + return $settings_by_group; + } + + /** + * Casts a value to the appropriate type based on the setting's registered type. + * + * @since 7.0.0 + * + * @param mixed $value The value to cast. + * @param string $type The registered type (string, boolean, integer, number, array, object). + * @return string|bool|int|float|array The cast value. + */ + private static function cast_value( $value, string $type ) { + switch ( $type ) { + case 'boolean': + return (bool) $value; + case 'integer': + return (int) $value; + case 'number': + return (float) $value; + case 'array': + case 'object': + return is_array( $value ) ? $value : array(); + case 'string': + default: + return (string) $value; + } + } +} diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index 7979c119a986f..8a9a2c3c89ece 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -2743,12 +2743,13 @@ function register_initial_settings() { 'general', 'blogname', array( - 'show_in_rest' => array( + 'show_in_rest' => array( 'name' => 'title', ), - 'type' => 'string', - 'label' => __( 'Title' ), - 'description' => __( 'Site title.' ), + 'show_in_abilities' => true, + 'type' => 'string', + 'label' => __( 'Title' ), + 'description' => __( 'Site title.' ), ) ); @@ -2756,12 +2757,13 @@ function register_initial_settings() { 'general', 'blogdescription', array( - 'show_in_rest' => array( + 'show_in_rest' => array( 'name' => 'description', ), - 'type' => 'string', - 'label' => __( 'Tagline' ), - 'description' => __( 'Site tagline.' ), + 'show_in_abilities' => true, + 'type' => 'string', + 'label' => __( 'Tagline' ), + 'description' => __( 'Site tagline.' ), ) ); @@ -2770,14 +2772,15 @@ function register_initial_settings() { 'general', 'siteurl', array( - 'show_in_rest' => array( + 'show_in_rest' => array( 'name' => 'url', 'schema' => array( 'format' => 'uri', ), ), - 'type' => 'string', - 'description' => __( 'Site URL.' ), + 'show_in_abilities' => true, + 'type' => 'string', + 'description' => __( 'Site URL.' ), ) ); } @@ -2787,14 +2790,15 @@ function register_initial_settings() { 'general', 'admin_email', array( - 'show_in_rest' => array( + 'show_in_rest' => array( 'name' => 'email', 'schema' => array( 'format' => 'email', ), ), - 'type' => 'string', - 'description' => __( 'This address is used for admin purposes, like new user notification.' ), + 'show_in_abilities' => true, + 'type' => 'string', + 'description' => __( 'This address is used for admin purposes, like new user notification.' ), ) ); } @@ -2803,11 +2807,12 @@ function register_initial_settings() { 'general', 'timezone_string', array( - 'show_in_rest' => array( + 'show_in_rest' => array( 'name' => 'timezone', ), - 'type' => 'string', - 'description' => __( 'A city in the same timezone as you.' ), + 'show_in_abilities' => true, + 'type' => 'string', + 'description' => __( 'A city in the same timezone as you.' ), ) ); @@ -2815,9 +2820,10 @@ function register_initial_settings() { 'general', 'date_format', array( - 'show_in_rest' => true, - 'type' => 'string', - 'description' => __( 'A date format for all date strings.' ), + 'show_in_rest' => true, + 'show_in_abilities' => true, + 'type' => 'string', + 'description' => __( 'A date format for all date strings.' ), ) ); @@ -2825,9 +2831,10 @@ function register_initial_settings() { 'general', 'time_format', array( - 'show_in_rest' => true, - 'type' => 'string', - 'description' => __( 'A time format for all time strings.' ), + 'show_in_rest' => true, + 'show_in_abilities' => true, + 'type' => 'string', + 'description' => __( 'A time format for all time strings.' ), ) ); @@ -2835,9 +2842,10 @@ function register_initial_settings() { 'general', 'start_of_week', array( - 'show_in_rest' => true, - 'type' => 'integer', - 'description' => __( 'A day number of the week that the week should start on.' ), + 'show_in_rest' => true, + 'show_in_abilities' => true, + 'type' => 'integer', + 'description' => __( 'A day number of the week that the week should start on.' ), ) ); @@ -2845,12 +2853,13 @@ function register_initial_settings() { 'general', 'WPLANG', array( - 'show_in_rest' => array( + 'show_in_rest' => array( 'name' => 'language', ), - 'type' => 'string', - 'description' => __( 'WordPress locale code.' ), - 'default' => 'en_US', + 'show_in_abilities' => true, + 'type' => 'string', + 'description' => __( 'WordPress locale code.' ), + 'default' => 'en_US', ) ); @@ -2858,10 +2867,11 @@ function register_initial_settings() { 'writing', 'use_smilies', array( - 'show_in_rest' => true, - 'type' => 'boolean', - 'description' => __( 'Convert emoticons like :-) and :-P to graphics on display.' ), - 'default' => true, + 'show_in_rest' => true, + 'show_in_abilities' => true, + 'type' => 'boolean', + 'description' => __( 'Convert emoticons like :-) and :-P to graphics on display.' ), + 'default' => true, ) ); @@ -2869,9 +2879,10 @@ function register_initial_settings() { 'writing', 'default_category', array( - 'show_in_rest' => true, - 'type' => 'integer', - 'description' => __( 'Default post category.' ), + 'show_in_rest' => true, + 'show_in_abilities' => true, + 'type' => 'integer', + 'description' => __( 'Default post category.' ), ) ); @@ -2879,9 +2890,10 @@ function register_initial_settings() { 'writing', 'default_post_format', array( - 'show_in_rest' => true, - 'type' => 'string', - 'description' => __( 'Default post format.' ), + 'show_in_rest' => true, + 'show_in_abilities' => true, + 'type' => 'string', + 'description' => __( 'Default post format.' ), ) ); @@ -2889,11 +2901,12 @@ function register_initial_settings() { 'reading', 'posts_per_page', array( - 'show_in_rest' => true, - 'type' => 'integer', - 'label' => __( 'Maximum posts per page' ), - 'description' => __( 'Blog pages show at most.' ), - 'default' => 10, + 'show_in_rest' => true, + 'show_in_abilities' => true, + 'type' => 'integer', + 'label' => __( 'Maximum posts per page' ), + 'description' => __( 'Blog pages show at most.' ), + 'default' => 10, ) ); @@ -2901,10 +2914,11 @@ function register_initial_settings() { 'reading', 'show_on_front', array( - 'show_in_rest' => true, - 'type' => 'string', - 'label' => __( 'Show on front' ), - 'description' => __( 'What to show on the front page' ), + 'show_in_rest' => true, + 'show_in_abilities' => true, + 'type' => 'string', + 'label' => __( 'Show on front' ), + 'description' => __( 'What to show on the front page' ), ) ); @@ -2912,10 +2926,11 @@ function register_initial_settings() { 'reading', 'page_on_front', array( - 'show_in_rest' => true, - 'type' => 'integer', - 'label' => __( 'Page on front' ), - 'description' => __( 'The ID of the page that should be displayed on the front page' ), + 'show_in_rest' => true, + 'show_in_abilities' => true, + 'type' => 'integer', + 'label' => __( 'Page on front' ), + 'description' => __( 'The ID of the page that should be displayed on the front page' ), ) ); @@ -2923,9 +2938,10 @@ function register_initial_settings() { 'reading', 'page_for_posts', array( - 'show_in_rest' => true, - 'type' => 'integer', - 'description' => __( 'The ID of the page that should display the latest posts' ), + 'show_in_rest' => true, + 'show_in_abilities' => true, + 'type' => 'integer', + 'description' => __( 'The ID of the page that should display the latest posts' ), ) ); @@ -2933,13 +2949,14 @@ function register_initial_settings() { 'discussion', 'default_ping_status', array( - 'show_in_rest' => array( + 'show_in_rest' => array( 'schema' => array( 'enum' => array( 'open', 'closed' ), ), ), - 'type' => 'string', - 'description' => __( 'Allow link notifications from other blogs (pingbacks and trackbacks) on new articles.' ), + 'show_in_abilities' => true, + 'type' => 'string', + 'description' => __( 'Allow link notifications from other blogs (pingbacks and trackbacks) on new articles.' ), ) ); @@ -2947,14 +2964,15 @@ function register_initial_settings() { 'discussion', 'default_comment_status', array( - 'show_in_rest' => array( + 'show_in_rest' => array( 'schema' => array( 'enum' => array( 'open', 'closed' ), ), ), - 'type' => 'string', - 'label' => __( 'Allow comments on new posts' ), - 'description' => __( 'Allow people to submit comments on new posts.' ), + 'show_in_abilities' => true, + 'type' => 'string', + 'label' => __( 'Allow comments on new posts' ), + 'description' => __( 'Allow people to submit comments on new posts.' ), ) ); } @@ -2985,10 +3003,12 @@ function register_initial_settings() { * @type string $label A label of the data attached to this setting. * @type string $description A description of the data attached to this setting. * @type callable $sanitize_callback A callback function that sanitizes the option's value. - * @type bool|array $show_in_rest Whether data associated with this setting should be included in the REST API. - * When registering complex settings, this argument may optionally be an - * array with a 'schema' key. - * @type mixed $default Default value when calling `get_option()`. + * @type bool|array $show_in_rest Whether data associated with this setting should be included in the REST API. + * When registering complex settings, this argument may optionally be an + * array with a 'schema' key. + * @type bool $show_in_abilities Whether this setting should be exposed through the Abilities API. + * Default false. + * @type mixed $default Default value when calling `get_option()`. * } */ function register_setting( $option_group, $option_name, $args = array() ) { @@ -3001,12 +3021,13 @@ function register_setting( $option_group, $option_name, $args = array() ) { $GLOBALS['new_whitelist_options'] = &$new_allowed_options; $defaults = array( - 'type' => 'string', - 'group' => $option_group, - 'label' => '', - 'description' => '', - 'sanitize_callback' => null, - 'show_in_rest' => false, + 'type' => 'string', + 'group' => $option_group, + 'label' => '', + 'description' => '', + 'sanitize_callback' => null, + 'show_in_rest' => false, + 'show_in_abilities' => false, ); // Back-compat: old sanitize callback is added. diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesSettingsController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesSettingsController.php new file mode 100644 index 0000000000000..198c0c3b8bc69 --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesSettingsController.php @@ -0,0 +1,356 @@ +user->create( array( 'role' => 'administrator' ) ); + self::$subscriber_id = self::factory()->user->create( array( 'role' => 'subscriber' ) ); + + // Register initial settings first so abilities can build schemas. + register_initial_settings(); + + // Ensure core abilities are registered for these tests. + remove_action( 'wp_abilities_api_categories_init', '_unhook_core_ability_categories_registration', 1 ); + remove_action( 'wp_abilities_api_init', '_unhook_core_abilities_registration', 1 ); + + add_action( 'wp_abilities_api_categories_init', 'wp_register_core_ability_categories' ); + add_action( 'wp_abilities_api_init', 'wp_register_core_abilities' ); + do_action( 'wp_abilities_api_categories_init' ); + do_action( 'wp_abilities_api_init' ); + } + + /** + * Tear down after class. + */ + public static function tear_down_after_class(): void { + // Re-add the unhook functions for subsequent tests. + add_action( 'wp_abilities_api_categories_init', '_unhook_core_ability_categories_registration', 1 ); + add_action( 'wp_abilities_api_init', '_unhook_core_abilities_registration', 1 ); + + // Remove the core abilities and their categories. + foreach ( wp_get_abilities() as $ability ) { + wp_unregister_ability( $ability->get_name() ); + } + foreach ( wp_get_ability_categories() as $ability_category ) { + wp_unregister_ability_category( $ability_category->get_slug() ); + } + + parent::tear_down_after_class(); + } + + /** + * Set up before each test. + */ + public function set_up(): void { + parent::set_up(); + + global $wp_rest_server; + $wp_rest_server = new WP_REST_Server(); + $this->server = $wp_rest_server; + + do_action( 'rest_api_init' ); + + wp_set_current_user( self::$admin_id ); + } + + /** + * Tear down after each test. + */ + public function tear_down(): void { + global $wp_rest_server; + $wp_rest_server = null; + + parent::tear_down(); + } + + /** + * Tests that unauthenticated users cannot access the get-settings ability. + * + * @ticket 64605 + */ + public function test_core_get_settings_requires_authentication(): void { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/core/get-settings/run' ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 401, $response->get_status() ); + } + + /** + * Tests that subscribers cannot access the get-settings ability. + * + * @ticket 64605 + */ + public function test_core_get_settings_requires_manage_options_capability(): void { + wp_set_current_user( self::$subscriber_id ); + + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/core/get-settings/run' ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 403, $response->get_status() ); + } + + /** + * Tests that administrators can access the get-settings ability. + * + * @ticket 64605 + */ + public function test_core_get_settings_allows_administrators(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/core/get-settings/run' ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + } + + /** + * Tests that the get-settings ability returns settings grouped by registration group. + * + * @ticket 64605 + */ + public function test_core_get_settings_returns_grouped_settings(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/core/get-settings/run' ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + + $this->assertIsArray( $data ); + $this->assertArrayHasKey( 'general', $data ); + $this->assertArrayHasKey( 'blogname', $data['general'] ); + $this->assertArrayHasKey( 'blogdescription', $data['general'] ); + } + + /** + * Tests that the get-settings ability can filter by group. + * + * @ticket 64605 + */ + public function test_core_get_settings_filters_by_group(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/core/get-settings/run' ); + $request->set_query_params( + array( + 'input' => array( + 'group' => 'general', + ), + ) + ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + + $this->assertIsArray( $data ); + $this->assertCount( 1, $data ); + $this->assertArrayHasKey( 'general', $data ); + } + + /** + * Tests that the get-settings ability can filter by specific slugs. + * + * @ticket 64605 + */ + public function test_core_get_settings_filters_by_slugs(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/core/get-settings/run' ); + $request->set_query_params( + array( + 'input' => array( + 'slugs' => array( 'blogname', 'blogdescription' ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + + $this->assertIsArray( $data ); + $this->assertArrayHasKey( 'general', $data ); + $this->assertCount( 2, $data['general'] ); + $this->assertArrayHasKey( 'blogname', $data['general'] ); + $this->assertArrayHasKey( 'blogdescription', $data['general'] ); + } + + /** + * Tests that settings without show_in_abilities are excluded. + * + * @ticket 64605 + */ + public function test_core_get_settings_excludes_settings_without_show_in_abilities(): void { + register_setting( + 'general', + 'test_setting_excluded', + array( + 'type' => 'string', + 'default' => 'test_value', + 'show_in_abilities' => false, + ) + ); + update_option( 'test_setting_excluded', 'test_value' ); + + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/core/get-settings/run' ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + + $this->assertArrayNotHasKey( 'test_setting_excluded', $data['general'] ?? array() ); + + unregister_setting( 'general', 'test_setting_excluded' ); + delete_option( 'test_setting_excluded' ); + } + + /** + * Tests that core settings with show_in_abilities are included. + * + * @ticket 64605 + */ + public function test_core_get_settings_includes_settings_with_show_in_abilities(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/core/get-settings/run' ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + + // blogname has show_in_abilities => true in register_initial_settings(). + $this->assertArrayHasKey( 'general', $data ); + $this->assertArrayHasKey( 'blogname', $data['general'] ); + + // use_smilies has show_in_abilities => true. + $this->assertArrayHasKey( 'writing', $data ); + $this->assertArrayHasKey( 'use_smilies', $data['writing'] ); + } + + /** + * Tests that boolean settings are cast to actual booleans. + * + * @ticket 64605 + */ + public function test_core_get_settings_casts_boolean_values(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/core/get-settings/run' ); + $request->set_query_params( + array( + 'input' => array( + 'slugs' => array( 'use_smilies' ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + + $this->assertArrayHasKey( 'writing', $data ); + $this->assertArrayHasKey( 'use_smilies', $data['writing'] ); + $this->assertIsBool( $data['writing']['use_smilies'] ); + } + + /** + * Tests that integer settings are cast to actual integers. + * + * @ticket 64605 + */ + public function test_core_get_settings_casts_integer_values(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/core/get-settings/run' ); + $request->set_query_params( + array( + 'input' => array( + 'slugs' => array( 'start_of_week' ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + + $this->assertArrayHasKey( 'general', $data ); + $this->assertArrayHasKey( 'start_of_week', $data['general'] ); + $this->assertIsInt( $data['general']['start_of_week'] ); + } + + /** + * Tests that the get-settings ability requires GET method (read-only). + * + * @ticket 64605 + */ + public function test_core_get_settings_requires_get_method(): void { + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/core/get-settings/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( array( 'input' => array() ) ) ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 405, $response->get_status() ); + + $data = $response->get_data(); + $this->assertSame( 'rest_ability_invalid_method', $data['code'] ); + } + + /** + * Tests that the get-settings ability returns correct values. + * + * @ticket 64605 + */ + public function test_core_get_settings_returns_correct_values(): void { + update_option( 'blogname', 'Test Site Name' ); + + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/core/get-settings/run' ); + $request->set_query_params( + array( + 'input' => array( + 'slugs' => array( 'blogname' ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + + $this->assertSame( 'Test Site Name', $data['general']['blogname'] ); + } +}