From a931561069f7c8671b8f3a176c539041b2f8edf1 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 26 Feb 2026 00:28:06 +0000 Subject: [PATCH 01/14] Backport connectors screen --- package.json | 2 +- src/wp-includes/connectors.php | 369 +++++++++++++++++++++++++++++++++ src/wp-settings.php | 1 + 3 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 src/wp-includes/connectors.php diff --git a/package.json b/package.json index 745ec98ab6b58..9109076de4b1d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "ref": "23b566c72e9c4a36219ef5d6e62890f05551f6cb" + "ref": "336a47b80b566256ce5035cae56b2ab16f583dad" }, "engines": { "node": ">=20.10.0", diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php new file mode 100644 index 0000000000000..d67bafc98c43f --- /dev/null +++ b/src/wp-includes/connectors.php @@ -0,0 +1,369 @@ +hasProvider( $provider_id ) ) { + return null; + } + + $registry->setProviderRequestAuthentication( + $provider_id, + new ApiKeyRequestAuthentication( $key ) + ); + + return $registry->isProviderConfigured( $provider_id ); + } catch ( \Error $e ) { + return null; + } +} + +/** + * Sets API key authentication for a provider in the WP AI Client registry. + * + * @since 7.0.0 + * @access private + * + * @param string $key The API key. + * @param string $provider_id The WP AI client provider ID. + */ +function _wp_connectors_set_provider_api_key( string $key, string $provider_id ): void { + try { + $registry = AiClient::defaultRegistry(); + + if ( ! $registry->hasProvider( $provider_id ) ) { + return; + } + + $registry->setProviderRequestAuthentication( + $provider_id, + new ApiKeyRequestAuthentication( $key ) + ); + } catch ( \Error $e ) { + // WP AI Client not available. + } +} + +/** + * Retrieves the real (unmasked) value of a connector API key. + * + * Temporarily removes the masking filter, reads the option, then re-adds it. + * + * @since 7.0.0 + * @access private + * + * @param string $option_name The option name for the API key. + * @param callable $mask_callback The mask filter function. + * @return string The real API key value. + */ +function _wp_connectors_get_real_api_key( string $option_name, callable $mask_callback ): string { + remove_filter( "option_{$option_name}", $mask_callback ); + $value = get_option( $option_name, '' ); + add_filter( "option_{$option_name}", $mask_callback ); + return (string) $value; +} + +/** + * Masks the Gemini API key on read. + * + * @since 7.0.0 + * @access private + * + * @param string $value The raw option value. + * @return string Masked key or empty string. + */ +function _wp_connectors_mask_gemini_api_key( string $value ): string { + if ( '' === $value ) { + return $value; + } + + return _wp_connectors_mask_api_key( $value ); +} + +/** + * Sanitizes and validates the Gemini API key before saving. + * + * @since 7.0.0 + * @access private + * + * @param string $value The new value. + * @return string The sanitized value, or empty string if the key is not valid. + */ +function _wp_connectors_sanitize_gemini_api_key( string $value ): string { + $value = sanitize_text_field( $value ); + if ( '' === $value ) { + return $value; + } + + $valid = _wp_connectors_is_api_key_valid( $value, 'google' ); + return true === $valid ? $value : ''; +} + +/** + * Masks the OpenAI API key on read. + * + * @since 7.0.0 + * @access private + * + * @param string $value The raw option value. + * @return string Masked key or empty string. + */ +function _wp_connectors_mask_openai_api_key( string $value ): string { + if ( '' === $value ) { + return $value; + } + + return _wp_connectors_mask_api_key( $value ); +} + +/** + * Sanitizes and validates the OpenAI API key before saving. + * + * @since 7.0.0 + * @access private + * + * @param string $value The new value. + * @return string The sanitized value, or empty string if the key is not valid. + */ +function _wp_connectors_sanitize_openai_api_key( string $value ): string { + $value = sanitize_text_field( $value ); + if ( '' === $value ) { + return $value; + } + + $valid = _wp_connectors_is_api_key_valid( $value, 'openai' ); + return true === $valid ? $value : ''; +} + +/** + * Masks the Anthropic API key on read. + * + * @since 7.0.0 + * @access private + * + * @param string $value The raw option value. + * @return string Masked key or empty string. + */ +function _wp_connectors_mask_anthropic_api_key( string $value ): string { + if ( '' === $value ) { + return $value; + } + + return _wp_connectors_mask_api_key( $value ); +} + +/** + * Sanitizes and validates the Anthropic API key before saving. + * + * @since 7.0.0 + * @access private + * + * @param string $value The new value. + * @return string The sanitized value, or empty string if the key is not valid. + */ +function _wp_connectors_sanitize_anthropic_api_key( string $value ): string { + $value = sanitize_text_field( $value ); + if ( '' === $value ) { + return $value; + } + + $valid = _wp_connectors_is_api_key_valid( $value, 'anthropic' ); + return true === $valid ? $value : ''; +} + +/** + * Gets the provider connectors. + * + * @since 7.0.0 + * @access private + * + * @return array Connectors. + */ +function _wp_connectors_get_connectors(): array { + return array( + 'connectors_gemini_api_key' => array( + 'provider' => 'google', + 'mask' => '_wp_connectors_mask_gemini_api_key', + 'sanitize' => '_wp_connectors_sanitize_gemini_api_key', + ), + 'connectors_openai_api_key' => array( + 'provider' => 'openai', + 'mask' => '_wp_connectors_mask_openai_api_key', + 'sanitize' => '_wp_connectors_sanitize_openai_api_key', + ), + 'connectors_anthropic_api_key' => array( + 'provider' => 'anthropic', + 'mask' => '_wp_connectors_mask_anthropic_api_key', + 'sanitize' => '_wp_connectors_sanitize_anthropic_api_key', + ), + ); +} + +/** + * Validates connector API keys in the REST response when explicitly requested. + * + * Runs on `rest_post_dispatch` for `/wp/v2/settings` requests that include connector + * fields via `_fields`. For each requested connector field, it validates the unmasked + * key against the provider and replaces the response value with `invalid_key` if + * validation fails. + * + * @since 7.0.0 + * @access private + * + * @param WP_REST_Response $response The response object. + * @param WP_REST_Server $server The server instance. + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response The potentially modified response. + */ +function _wp_connectors_validate_keys_in_rest( WP_REST_Response $response, WP_REST_Server $server, WP_REST_Request $request ): WP_REST_Response { + if ( '/wp/v2/settings' !== $request->get_route() ) { + return $response; + } + + if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { + return $response; + } + + $fields = $request->get_param( '_fields' ); + if ( ! $fields ) { + return $response; + } + + if ( is_array( $fields ) ) { + $requested = $fields; + } else { + $requested = array_map( 'trim', explode( ',', $fields ) ); + } + + $data = $response->get_data(); + if ( ! is_array( $data ) ) { + return $response; + } + + foreach ( _wp_connectors_get_connectors() as $option_name => $config ) { + if ( ! in_array( $option_name, $requested, true ) ) { + continue; + } + + $real_key = _wp_connectors_get_real_api_key( $option_name, $config['mask'] ); + if ( '' === $real_key ) { + continue; + } + + if ( true !== _wp_connectors_is_api_key_valid( $real_key, $config['provider'] ) ) { + $data[ $option_name ] = 'invalid_key'; + } + } + + $response->set_data( $data ); + return $response; +} +add_filter( 'rest_post_dispatch', '_wp_connectors_validate_keys_in_rest', 10, 3 ); + +/** + * Registers default connector settings and mask/sanitize filters. + * + * @since 7.0.0 + * @access private + */ +function _wp_register_default_connector_settings(): void { + if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { + return; + } + + foreach ( _wp_connectors_get_connectors() as $option_name => $config ) { + register_setting( + 'connectors', + $option_name, + array( + 'type' => 'string', + 'default' => '', + 'show_in_rest' => true, + 'sanitize_callback' => $config['sanitize'], + ) + ); + add_filter( "option_{$option_name}", $config['mask'] ); + } +} +add_action( 'init', '_wp_register_default_connector_settings' ); + +/** + * Passes stored connector API keys to the WP AI client. + * + * @since 7.0.0 + * @access private + */ +function _wp_connectors_pass_default_keys_to_ai_client(): void { + if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { + return; + } + + foreach ( _wp_connectors_get_connectors() as $option_name => $config ) { + $api_key = _wp_connectors_get_real_api_key( $option_name, $config['mask'] ); + if ( '' !== $api_key ) { + _wp_connectors_set_provider_api_key( $api_key, $config['provider'] ); + } + } +} +add_action( 'init', '_wp_connectors_pass_default_keys_to_ai_client' ); diff --git a/src/wp-settings.php b/src/wp-settings.php index 90741401e800c..023cdccd5ecc9 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -294,6 +294,7 @@ require ABSPATH . WPINC . '/ai-client/class-wp-ai-client-ability-function-resolver.php'; require ABSPATH . WPINC . '/ai-client/class-wp-ai-client-prompt-builder.php'; require ABSPATH . WPINC . '/ai-client.php'; +require ABSPATH . WPINC . '/connectors.php'; require ABSPATH . WPINC . '/class-wp-icons-registry.php'; require ABSPATH . WPINC . '/widgets.php'; require ABSPATH . WPINC . '/class-wp-widget.php'; From 409f90ff6fc46544227211a01806ef611ff109d5 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 26 Feb 2026 00:53:26 +0000 Subject: [PATCH 02/14] unit test updates --- tests/phpunit/tests/rest-api/rest-settings-controller.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-settings-controller.php b/tests/phpunit/tests/rest-api/rest-settings-controller.php index dd79885d2b16d..8b09fe3a3eb83 100644 --- a/tests/phpunit/tests/rest-api/rest-settings-controller.php +++ b/tests/phpunit/tests/rest-api/rest-settings-controller.php @@ -120,6 +120,10 @@ public function test_get_items() { 'default_comment_status', 'site_icon', // Registered in wp-includes/blocks/site-logo.php 'wp_enable_real_time_collaboration', + // Connectors API keys are registered in _wp_register_default_connector_settings() in wp-includes/connectors.php. + 'connectors_anthropic_api_key', + 'connectors_gemini_api_key', + 'connectors_openai_api_key' ); if ( ! is_multisite() ) { From e26814de007698d3925cc6b2ae876117ab42e3c1 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 26 Feb 2026 00:56:15 +0000 Subject: [PATCH 03/14] lint fix --- tests/phpunit/tests/rest-api/rest-settings-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/rest-api/rest-settings-controller.php b/tests/phpunit/tests/rest-api/rest-settings-controller.php index 8b09fe3a3eb83..7d3250e364941 100644 --- a/tests/phpunit/tests/rest-api/rest-settings-controller.php +++ b/tests/phpunit/tests/rest-api/rest-settings-controller.php @@ -123,7 +123,7 @@ public function test_get_items() { // Connectors API keys are registered in _wp_register_default_connector_settings() in wp-includes/connectors.php. 'connectors_anthropic_api_key', 'connectors_gemini_api_key', - 'connectors_openai_api_key' + 'connectors_openai_api_key', ); if ( ! is_multisite() ) { From 81a410090867eaf1ab81aef58f8c379eb2aa183b Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 26 Feb 2026 01:26:20 +0000 Subject: [PATCH 04/14] Tests: Update REST API JS fixture for connectors settings --- tests/qunit/fixtures/wp-api-generated.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index e1a8ffc96995f..95dd8bc4935d4 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -11064,6 +11064,24 @@ mockedApiResponse.Schema = { "PATCH" ], "args": { + "connectors_gemini_api_key": { + "title": "", + "description": "", + "type": "string", + "required": false + }, + "connectors_openai_api_key": { + "title": "", + "description": "", + "type": "string", + "required": false + }, + "connectors_anthropic_api_key": { + "title": "", + "description": "", + "type": "string", + "required": false + }, "title": { "title": "Title", "description": "Site title.", @@ -14634,6 +14652,9 @@ mockedApiResponse.CommentModel = { }; mockedApiResponse.settings = { + "connectors_gemini_api_key": "", + "connectors_openai_api_key": "", + "connectors_anthropic_api_key": "", "title": "Test Blog", "description": "", "url": "http://example.org", From 31ab71c55547a94cd051a1e51b4602254473b1a3 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 26 Feb 2026 09:39:12 +0100 Subject: [PATCH 05/14] Connectors: Improve error handling in AI client integration. - Change catch blocks from `\Error` to `Exception` (global namespace). - Add `_doing_it_wrong` when an unregistered provider ID is passed. - Add `wp_trigger_error` to surface exception messages in catch blocks. - Change `_wp_connectors_set_provider_api_key` to return `bool`. Co-Authored-By: Claude Opus 4.6 --- src/wp-includes/connectors.php | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index d67bafc98c43f..8d422d244a162 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -65,6 +65,15 @@ function _wp_connectors_is_api_key_valid( string $key, string $provider_id ): ?b $registry = AiClient::defaultRegistry(); if ( ! $registry->hasProvider( $provider_id ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: AI provider ID. */ + __( 'The provider "%s" is not registered in the AI client registry.' ), + $provider_id + ), + '7.0.0' + ); return null; } @@ -74,7 +83,8 @@ function _wp_connectors_is_api_key_valid( string $key, string $provider_id ): ?b ); return $registry->isProviderConfigured( $provider_id ); - } catch ( \Error $e ) { + } catch ( Exception $e ) { + wp_trigger_error( __FUNCTION__, $e->getMessage() ); return null; } } @@ -87,21 +97,34 @@ function _wp_connectors_is_api_key_valid( string $key, string $provider_id ): ?b * * @param string $key The API key. * @param string $provider_id The WP AI client provider ID. + * @return bool True if the key was set successfully, false otherwise. */ -function _wp_connectors_set_provider_api_key( string $key, string $provider_id ): void { +function _wp_connectors_set_provider_api_key( string $key, string $provider_id ): bool { try { $registry = AiClient::defaultRegistry(); if ( ! $registry->hasProvider( $provider_id ) ) { - return; + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: AI provider ID. */ + __( 'The provider "%s" is not registered in the AI client registry.' ), + $provider_id + ), + '7.0.0' + ); + return false; } $registry->setProviderRequestAuthentication( $provider_id, new ApiKeyRequestAuthentication( $key ) ); - } catch ( \Error $e ) { - // WP AI Client not available. + + return true; + } catch ( Exception $e ) { + wp_trigger_error( __FUNCTION__, $e->getMessage() ); + return false; } } From 734a6b3ffd654eb1cd7b6066e2461a7416e7f902 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 26 Feb 2026 10:26:20 +0100 Subject: [PATCH 06/14] Connectors: Remove per-provider boilerplate in favor of shared callbacks. Replace six identical per-provider mask/sanitize wrapper functions with `_wp_connectors_mask_api_key` used directly and sanitize closures built from a provider map in `_wp_connectors_get_connectors()`. Co-Authored-By: Claude Opus 4.6 --- src/wp-includes/connectors.php | 146 +++++---------------------------- 1 file changed, 22 insertions(+), 124 deletions(-) diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 8d422d244a162..7dce768d8129c 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -147,114 +147,6 @@ function _wp_connectors_get_real_api_key( string $option_name, callable $mask_ca return (string) $value; } -/** - * Masks the Gemini API key on read. - * - * @since 7.0.0 - * @access private - * - * @param string $value The raw option value. - * @return string Masked key or empty string. - */ -function _wp_connectors_mask_gemini_api_key( string $value ): string { - if ( '' === $value ) { - return $value; - } - - return _wp_connectors_mask_api_key( $value ); -} - -/** - * Sanitizes and validates the Gemini API key before saving. - * - * @since 7.0.0 - * @access private - * - * @param string $value The new value. - * @return string The sanitized value, or empty string if the key is not valid. - */ -function _wp_connectors_sanitize_gemini_api_key( string $value ): string { - $value = sanitize_text_field( $value ); - if ( '' === $value ) { - return $value; - } - - $valid = _wp_connectors_is_api_key_valid( $value, 'google' ); - return true === $valid ? $value : ''; -} - -/** - * Masks the OpenAI API key on read. - * - * @since 7.0.0 - * @access private - * - * @param string $value The raw option value. - * @return string Masked key or empty string. - */ -function _wp_connectors_mask_openai_api_key( string $value ): string { - if ( '' === $value ) { - return $value; - } - - return _wp_connectors_mask_api_key( $value ); -} - -/** - * Sanitizes and validates the OpenAI API key before saving. - * - * @since 7.0.0 - * @access private - * - * @param string $value The new value. - * @return string The sanitized value, or empty string if the key is not valid. - */ -function _wp_connectors_sanitize_openai_api_key( string $value ): string { - $value = sanitize_text_field( $value ); - if ( '' === $value ) { - return $value; - } - - $valid = _wp_connectors_is_api_key_valid( $value, 'openai' ); - return true === $valid ? $value : ''; -} - -/** - * Masks the Anthropic API key on read. - * - * @since 7.0.0 - * @access private - * - * @param string $value The raw option value. - * @return string Masked key or empty string. - */ -function _wp_connectors_mask_anthropic_api_key( string $value ): string { - if ( '' === $value ) { - return $value; - } - - return _wp_connectors_mask_api_key( $value ); -} - -/** - * Sanitizes and validates the Anthropic API key before saving. - * - * @since 7.0.0 - * @access private - * - * @param string $value The new value. - * @return string The sanitized value, or empty string if the key is not valid. - */ -function _wp_connectors_sanitize_anthropic_api_key( string $value ): string { - $value = sanitize_text_field( $value ); - if ( '' === $value ) { - return $value; - } - - $valid = _wp_connectors_is_api_key_valid( $value, 'anthropic' ); - return true === $valid ? $value : ''; -} - /** * Gets the provider connectors. * @@ -264,23 +156,29 @@ function _wp_connectors_sanitize_anthropic_api_key( string $value ): string { * @return array Connectors. */ function _wp_connectors_get_connectors(): array { - return array( - 'connectors_gemini_api_key' => array( - 'provider' => 'google', - 'mask' => '_wp_connectors_mask_gemini_api_key', - 'sanitize' => '_wp_connectors_sanitize_gemini_api_key', - ), - 'connectors_openai_api_key' => array( - 'provider' => 'openai', - 'mask' => '_wp_connectors_mask_openai_api_key', - 'sanitize' => '_wp_connectors_sanitize_openai_api_key', - ), - 'connectors_anthropic_api_key' => array( - 'provider' => 'anthropic', - 'mask' => '_wp_connectors_mask_anthropic_api_key', - 'sanitize' => '_wp_connectors_sanitize_anthropic_api_key', - ), + $providers = array( + 'google' => 'connectors_gemini_api_key', + 'openai' => 'connectors_openai_api_key', + 'anthropic' => 'connectors_anthropic_api_key', ); + + $connectors = array(); + foreach ( $providers as $provider => $option_name ) { + $connectors[ $option_name ] = array( + 'provider' => $provider, + 'mask' => '_wp_connectors_mask_api_key', + 'sanitize' => static function ( string $value ) use ( $provider ): string { + $value = sanitize_text_field( $value ); + if ( '' === $value ) { + return $value; + } + + $valid = _wp_connectors_is_api_key_valid( $value, $provider ); + return true === $valid ? $value : ''; + }, + ); + } + return $connectors; } /** From a9539cfcbbc7332a58cfeb1d07bd7336e58a145d Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 26 Feb 2026 11:00:54 +0100 Subject: [PATCH 07/14] Connectors: Add label and description to provider settings and reduce boilerplate. Rename `_wp_connectors_get_connectors` to `_wp_connectors_get_provider_settings` and dynamically assemble setting names, labels, and descriptions from a minimal provider slug/name map. Add `label` and `description` to `register_setting` calls so they appear in the REST API schema. Co-Authored-By: Claude Opus 4.6 --- src/wp-includes/connectors.php | 67 ++++++++++++++++-------- tests/qunit/fixtures/wp-api-generated.js | 12 ++--- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 7dce768d8129c..b7c63de18aba9 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -148,26 +148,47 @@ function _wp_connectors_get_real_api_key( string $option_name, callable $mask_ca } /** - * Gets the provider connectors. + * Gets the registered connector provider settings. * * @since 7.0.0 * @access private * - * @return array Connectors. + * @return array Provider settings keyed by setting name. */ -function _wp_connectors_get_connectors(): array { +function _wp_connectors_get_provider_settings(): array { $providers = array( - 'google' => 'connectors_gemini_api_key', - 'openai' => 'connectors_openai_api_key', - 'anthropic' => 'connectors_anthropic_api_key', + 'google' => array( + 'slug' => 'gemini', + 'name' => 'Gemini', + ), + 'openai' => array( + 'slug' => 'openai', + 'name' => 'OpenAI', + ), + 'anthropic' => array( + 'slug' => 'anthropic', + 'name' => 'Anthropic', + ), ); - $connectors = array(); - foreach ( $providers as $provider => $option_name ) { - $connectors[ $option_name ] = array( - 'provider' => $provider, - 'mask' => '_wp_connectors_mask_api_key', - 'sanitize' => static function ( string $value ) use ( $provider ): string { + $provider_settings = array(); + foreach ( $providers as $provider => $data ) { + $setting_name = "connectors_{$data['slug']}_api_key"; + + $provider_settings[ $setting_name ] = array( + 'provider' => $provider, + 'label' => sprintf( + /* translators: %s: AI provider name. */ + __( '%s API Key' ), + $data['name'] + ), + 'description' => sprintf( + /* translators: %s: AI provider name. */ + __( 'API key for the %s AI provider.' ), + $data['name'] + ), + 'mask' => '_wp_connectors_mask_api_key', + 'sanitize' => static function ( string $value ) use ( $provider ): string { $value = sanitize_text_field( $value ); if ( '' === $value ) { return $value; @@ -178,7 +199,7 @@ function _wp_connectors_get_connectors(): array { }, ); } - return $connectors; + return $provider_settings; } /** @@ -222,18 +243,18 @@ function _wp_connectors_validate_keys_in_rest( WP_REST_Response $response, WP_RE return $response; } - foreach ( _wp_connectors_get_connectors() as $option_name => $config ) { - if ( ! in_array( $option_name, $requested, true ) ) { + foreach ( _wp_connectors_get_provider_settings() as $setting_name => $config ) { + if ( ! in_array( $setting_name, $requested, true ) ) { continue; } - $real_key = _wp_connectors_get_real_api_key( $option_name, $config['mask'] ); + $real_key = _wp_connectors_get_real_api_key( $setting_name, $config['mask'] ); if ( '' === $real_key ) { continue; } if ( true !== _wp_connectors_is_api_key_valid( $real_key, $config['provider'] ) ) { - $data[ $option_name ] = 'invalid_key'; + $data[ $setting_name ] = 'invalid_key'; } } @@ -253,18 +274,20 @@ function _wp_register_default_connector_settings(): void { return; } - foreach ( _wp_connectors_get_connectors() as $option_name => $config ) { + foreach ( _wp_connectors_get_provider_settings() as $setting_name => $config ) { register_setting( 'connectors', - $option_name, + $setting_name, array( 'type' => 'string', + 'label' => $config['label'], + 'description' => $config['description'], 'default' => '', 'show_in_rest' => true, 'sanitize_callback' => $config['sanitize'], ) ); - add_filter( "option_{$option_name}", $config['mask'] ); + add_filter( "option_{$setting_name}", $config['mask'] ); } } add_action( 'init', '_wp_register_default_connector_settings' ); @@ -280,8 +303,8 @@ function _wp_connectors_pass_default_keys_to_ai_client(): void { return; } - foreach ( _wp_connectors_get_connectors() as $option_name => $config ) { - $api_key = _wp_connectors_get_real_api_key( $option_name, $config['mask'] ); + foreach ( _wp_connectors_get_provider_settings() as $setting_name => $config ) { + $api_key = _wp_connectors_get_real_api_key( $setting_name, $config['mask'] ); if ( '' !== $api_key ) { _wp_connectors_set_provider_api_key( $api_key, $config['provider'] ); } diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 95dd8bc4935d4..ca563c1146ddc 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -11065,20 +11065,20 @@ mockedApiResponse.Schema = { ], "args": { "connectors_gemini_api_key": { - "title": "", - "description": "", + "title": "Gemini API Key", + "description": "API key for the Gemini AI provider.", "type": "string", "required": false }, "connectors_openai_api_key": { - "title": "", - "description": "", + "title": "OpenAI API Key", + "description": "API key for the OpenAI AI provider.", "type": "string", "required": false }, "connectors_anthropic_api_key": { - "title": "", - "description": "", + "title": "Anthropic API Key", + "description": "API key for the Anthropic AI provider.", "type": "string", "required": false }, From 99202930aa279ae3ac083ebb459ab1457d9ed738 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 26 Feb 2026 12:17:02 +0100 Subject: [PATCH 08/14] Tests: Add unit tests for _wp_connectors_get_provider_settings(). Co-Authored-By: Claude Opus 4.6 --- .../wpConnectorsGetProviderSettings.php | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/phpunit/tests/connectors/wpConnectorsGetProviderSettings.php diff --git a/tests/phpunit/tests/connectors/wpConnectorsGetProviderSettings.php b/tests/phpunit/tests/connectors/wpConnectorsGetProviderSettings.php new file mode 100644 index 0000000000000..5f753f94475b0 --- /dev/null +++ b/tests/phpunit/tests/connectors/wpConnectorsGetProviderSettings.php @@ -0,0 +1,46 @@ +assertArrayHasKey( 'connectors_gemini_api_key', $settings ); + $this->assertArrayHasKey( 'connectors_openai_api_key', $settings ); + $this->assertArrayHasKey( 'connectors_anthropic_api_key', $settings ); + $this->assertCount( 3, $settings ); + } + + /** + * @ticket 64730 + */ + public function test_each_setting_has_required_fields() { + $settings = _wp_connectors_get_provider_settings(); + $required_keys = array( 'provider', 'label', 'description', 'mask', 'sanitize' ); + + foreach ( $settings as $setting_name => $config ) { + foreach ( $required_keys as $key ) { + $this->assertArrayHasKey( $key, $config, "Setting '{$setting_name}' is missing '{$key}'." ); + } + } + } + + /** + * @ticket 64730 + */ + public function test_provider_values_match_expected() { + $settings = _wp_connectors_get_provider_settings(); + + $this->assertSame( 'google', $settings['connectors_gemini_api_key']['provider'] ); + $this->assertSame( 'openai', $settings['connectors_openai_api_key']['provider'] ); + $this->assertSame( 'anthropic', $settings['connectors_anthropic_api_key']['provider'] ); + } +} From 06256246cc1f3b8bc54d454f4b7af2e1659dfa22 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 26 Feb 2026 12:23:37 +0100 Subject: [PATCH 09/14] Tests: Add unit tests for _wp_connectors_mask_api_key(). Co-Authored-By: Claude Opus 4.6 --- .../connectors/wpConnectorsMaskApiKey.php | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/phpunit/tests/connectors/wpConnectorsMaskApiKey.php diff --git a/tests/phpunit/tests/connectors/wpConnectorsMaskApiKey.php b/tests/phpunit/tests/connectors/wpConnectorsMaskApiKey.php new file mode 100644 index 0000000000000..1d4fd9a3eb908 --- /dev/null +++ b/tests/phpunit/tests/connectors/wpConnectorsMaskApiKey.php @@ -0,0 +1,44 @@ +assertSame( $expected, _wp_connectors_mask_api_key( $input ) ); + } + + /** + * Data provider. + * + * @return array[] Test parameters { + * @type string $input API key to mask. + * @type string $expected Expected masked result. + * } + */ + public function data_mask_api_key(): array { + $bullet = "\u{2022}"; + + return array( + 'empty string' => array( '', '' ), + '1 char' => array( 'a', 'a' ), + '4 chars (boundary)' => array( 'abcd', 'abcd' ), + '5 chars (1 bullet + last 4)' => array( 'abcde', $bullet . 'bcde' ), + '20 chars (cap at 16 bullets)' => array( '12345678901234567890', str_repeat( $bullet, 16 ) . '7890' ), + '30 chars (cap at 16 bullets)' => array( str_repeat( 'x', 30 ), str_repeat( $bullet, 16 ) . 'xxxx' ), + ); + } +} From 4c380a96775f5e362fea4ecb20083ea9886bbe46 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 26 Feb 2026 12:39:06 +0100 Subject: [PATCH 10/14] Tests: Add unit tests for _wp_connectors_is_api_key_valid(). Introduces a reusable mock provider trait for registering a controllable test provider in the AI Client registry. Co-Authored-By: Claude Opus 4.6 --- .../wp-ai-client-mock-provider-trait.php | 172 ++++++++++++++++++ .../connectors/wpConnectorsIsApiKeyValid.php | 69 +++++++ 2 files changed, 241 insertions(+) create mode 100644 tests/phpunit/includes/wp-ai-client-mock-provider-trait.php create mode 100644 tests/phpunit/tests/connectors/wpConnectorsIsApiKeyValid.php diff --git a/tests/phpunit/includes/wp-ai-client-mock-provider-trait.php b/tests/phpunit/includes/wp-ai-client-mock-provider-trait.php new file mode 100644 index 0000000000000..b32b1d4083d2c --- /dev/null +++ b/tests/phpunit/includes/wp-ai-client-mock-provider-trait.php @@ -0,0 +1,172 @@ +hasProvider( 'mock_connectors_test' ) ) { + $registry->registerProvider( Mock_Connectors_Test_Provider::class ); + } + } + + /** + * Sets whether the mock provider reports as configured. + * + * @param bool $is_configured Whether the provider should be configured. + */ + private static function set_mock_provider_configured( bool $is_configured ): void { + Mock_Connectors_Test_Provider_Availability::$is_configured = $is_configured; + } +} diff --git a/tests/phpunit/tests/connectors/wpConnectorsIsApiKeyValid.php b/tests/phpunit/tests/connectors/wpConnectorsIsApiKeyValid.php new file mode 100644 index 0000000000000..79d4129be5103 --- /dev/null +++ b/tests/phpunit/tests/connectors/wpConnectorsIsApiKeyValid.php @@ -0,0 +1,69 @@ +setExpectedIncorrectUsage( '_wp_connectors_is_api_key_valid' ); + + $result = _wp_connectors_is_api_key_valid( 'test-key', 'nonexistent_provider' ); + + $this->assertNull( $result ); + } + + /** + * Tests that a registered and configured provider returns true. + * + * @ticket 64730 + */ + public function test_configured_provider_returns_true() { + self::set_mock_provider_configured( true ); + + $result = _wp_connectors_is_api_key_valid( 'test-key', 'mock_connectors_test' ); + + $this->assertTrue( $result ); + } + + /** + * Tests that a registered but unconfigured provider returns false. + * + * @ticket 64730 + */ + public function test_unconfigured_provider_returns_false() { + self::set_mock_provider_configured( false ); + + $result = _wp_connectors_is_api_key_valid( 'test-key', 'mock_connectors_test' ); + + $this->assertFalse( $result ); + } +} From 21201c5904a1305fd522d249ee02dd0183bbdf03 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 26 Feb 2026 12:44:11 +0100 Subject: [PATCH 11/14] Tests: Fix PHPCS warnings in connector test files. Uses snake_case parameter names in interface implementations and fixes array alignment in data provider. Co-Authored-By: Claude Opus 4.6 --- .../wp-ai-client-mock-provider-trait.php | 16 ++++++++-------- .../tests/connectors/wpConnectorsMaskApiKey.php | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/phpunit/includes/wp-ai-client-mock-provider-trait.php b/tests/phpunit/includes/wp-ai-client-mock-provider-trait.php index b32b1d4083d2c..326ead2ed771e 100644 --- a/tests/phpunit/includes/wp-ai-client-mock-provider-trait.php +++ b/tests/phpunit/includes/wp-ai-client-mock-provider-trait.php @@ -59,20 +59,20 @@ public function listModelMetadata(): array { /** * Checks if a model exists. * - * @param string $modelId The model ID. + * @param string $model_id The model ID. * @return bool Always false. */ - public function hasModelMetadata( string $modelId ): bool { + public function hasModelMetadata( string $model_id ): bool { return false; } /** * Gets model metadata. * - * @param string $modelId The model ID. + * @param string $model_id The model ID. * @throws \InvalidArgumentException Always, as no models are available. */ - public function getModelMetadata( string $modelId ): ModelMetadata { + public function getModelMetadata( string $model_id ): ModelMetadata { throw new \InvalidArgumentException( 'No models available.' ); } } @@ -125,13 +125,13 @@ protected static function createModelMetadataDirectory(): ModelMetadataDirectory /** * Creates a model instance. * - * @param ModelMetadata $modelMetadata The model metadata. - * @param ProviderMetadata $providerMetadata The provider metadata. + * @param ModelMetadata $model_metadata The model metadata. + * @param ProviderMetadata $provider_metadata The provider metadata. * @throws \RuntimeException Always, as model creation is not needed for these tests. */ protected static function createModel( - ModelMetadata $modelMetadata, - ProviderMetadata $providerMetadata + ModelMetadata $model_metadata, + ProviderMetadata $provider_metadata ): ModelInterface { throw new \RuntimeException( 'Not implemented.' ); } diff --git a/tests/phpunit/tests/connectors/wpConnectorsMaskApiKey.php b/tests/phpunit/tests/connectors/wpConnectorsMaskApiKey.php index 1d4fd9a3eb908..77f63f4b53ede 100644 --- a/tests/phpunit/tests/connectors/wpConnectorsMaskApiKey.php +++ b/tests/phpunit/tests/connectors/wpConnectorsMaskApiKey.php @@ -33,12 +33,12 @@ public function data_mask_api_key(): array { $bullet = "\u{2022}"; return array( - 'empty string' => array( '', '' ), - '1 char' => array( 'a', 'a' ), - '4 chars (boundary)' => array( 'abcd', 'abcd' ), - '5 chars (1 bullet + last 4)' => array( 'abcde', $bullet . 'bcde' ), - '20 chars (cap at 16 bullets)' => array( '12345678901234567890', str_repeat( $bullet, 16 ) . '7890' ), - '30 chars (cap at 16 bullets)' => array( str_repeat( 'x', 30 ), str_repeat( $bullet, 16 ) . 'xxxx' ), + 'empty string' => array( '', '' ), + '1 char' => array( 'a', 'a' ), + '4 chars (boundary)' => array( 'abcd', 'abcd' ), + '5 chars (1 bullet + last 4)' => array( 'abcde', $bullet . 'bcde' ), + '20 chars (cap at 16 bullets)' => array( '12345678901234567890', str_repeat( $bullet, 16 ) . '7890' ), + '30 chars (cap at 16 bullets)' => array( str_repeat( 'x', 30 ), str_repeat( $bullet, 16 ) . 'xxxx' ), ); } } From 0b6f6c13c382d80082b68b2efcc3bc9fe56ef966 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 26 Feb 2026 11:53:08 +0000 Subject: [PATCH 12/14] update setting names --- package.json | 2 +- src/wp-includes/connectors.php | 7 ++----- .../wpConnectorsGetProviderSettings.php | 12 ++++++------ .../tests/rest-api/rest-settings-controller.php | 6 +++--- tests/qunit/fixtures/wp-api-generated.js | 16 ++++++++-------- 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 9109076de4b1d..3598caec635a7 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "ref": "336a47b80b566256ce5035cae56b2ab16f583dad" + "ref": "4aa81f968a38960a7a3ee32f557a366f285dc08f" }, "engines": { "node": ">=20.10.0", diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index b7c63de18aba9..1d84cd700923f 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -158,22 +158,19 @@ function _wp_connectors_get_real_api_key( string $option_name, callable $mask_ca function _wp_connectors_get_provider_settings(): array { $providers = array( 'google' => array( - 'slug' => 'gemini', - 'name' => 'Gemini', + 'name' => 'Google', ), 'openai' => array( - 'slug' => 'openai', 'name' => 'OpenAI', ), 'anthropic' => array( - 'slug' => 'anthropic', 'name' => 'Anthropic', ), ); $provider_settings = array(); foreach ( $providers as $provider => $data ) { - $setting_name = "connectors_{$data['slug']}_api_key"; + $setting_name = "connectors_ai_{$provider}_api_key"; $provider_settings[ $setting_name ] = array( 'provider' => $provider, diff --git a/tests/phpunit/tests/connectors/wpConnectorsGetProviderSettings.php b/tests/phpunit/tests/connectors/wpConnectorsGetProviderSettings.php index 5f753f94475b0..8d49391d34d38 100644 --- a/tests/phpunit/tests/connectors/wpConnectorsGetProviderSettings.php +++ b/tests/phpunit/tests/connectors/wpConnectorsGetProviderSettings.php @@ -13,9 +13,9 @@ class Tests_Connectors_WpConnectorsGetProviderSettings extends WP_UnitTestCase { public function test_returns_expected_provider_keys() { $settings = _wp_connectors_get_provider_settings(); - $this->assertArrayHasKey( 'connectors_gemini_api_key', $settings ); - $this->assertArrayHasKey( 'connectors_openai_api_key', $settings ); - $this->assertArrayHasKey( 'connectors_anthropic_api_key', $settings ); + $this->assertArrayHasKey( 'connectors_ai_google_api_key', $settings ); + $this->assertArrayHasKey( 'connectors_ai_openai_api_key', $settings ); + $this->assertArrayHasKey( 'connectors_ai_anthropic_api_key', $settings ); $this->assertCount( 3, $settings ); } @@ -39,8 +39,8 @@ public function test_each_setting_has_required_fields() { public function test_provider_values_match_expected() { $settings = _wp_connectors_get_provider_settings(); - $this->assertSame( 'google', $settings['connectors_gemini_api_key']['provider'] ); - $this->assertSame( 'openai', $settings['connectors_openai_api_key']['provider'] ); - $this->assertSame( 'anthropic', $settings['connectors_anthropic_api_key']['provider'] ); + $this->assertSame( 'google', $settings['connectors_ai_google_api_key']['provider'] ); + $this->assertSame( 'openai', $settings['connectors_ai_openai_api_key']['provider'] ); + $this->assertSame( 'anthropic', $settings['connectors_ai_anthropic_api_key']['provider'] ); } } diff --git a/tests/phpunit/tests/rest-api/rest-settings-controller.php b/tests/phpunit/tests/rest-api/rest-settings-controller.php index 7d3250e364941..ef9e72e6a6724 100644 --- a/tests/phpunit/tests/rest-api/rest-settings-controller.php +++ b/tests/phpunit/tests/rest-api/rest-settings-controller.php @@ -121,9 +121,9 @@ public function test_get_items() { 'site_icon', // Registered in wp-includes/blocks/site-logo.php 'wp_enable_real_time_collaboration', // Connectors API keys are registered in _wp_register_default_connector_settings() in wp-includes/connectors.php. - 'connectors_anthropic_api_key', - 'connectors_gemini_api_key', - 'connectors_openai_api_key', + 'connectors_ai_anthropic_api_key', + 'connectors_ai_google_api_key', + 'connectors_ai_openai_api_key', ); if ( ! is_multisite() ) { diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index ca563c1146ddc..ef59ce5ca5074 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -11064,19 +11064,19 @@ mockedApiResponse.Schema = { "PATCH" ], "args": { - "connectors_gemini_api_key": { - "title": "Gemini API Key", - "description": "API key for the Gemini AI provider.", + "connectors_ai_google_api_key": { + "title": "Google API Key", + "description": "API key for the Google AI provider.", "type": "string", "required": false }, - "connectors_openai_api_key": { + "connectors_ai_openai_api_key": { "title": "OpenAI API Key", "description": "API key for the OpenAI AI provider.", "type": "string", "required": false }, - "connectors_anthropic_api_key": { + "connectors_ai_anthropic_api_key": { "title": "Anthropic API Key", "description": "API key for the Anthropic AI provider.", "type": "string", @@ -14652,9 +14652,9 @@ mockedApiResponse.CommentModel = { }; mockedApiResponse.settings = { - "connectors_gemini_api_key": "", - "connectors_openai_api_key": "", - "connectors_anthropic_api_key": "", + "connectors_ai_google_api_key": "", + "connectors_ai_openai_api_key": "", + "connectors_ai_anthropic_api_key": "", "title": "Test Blog", "description": "", "url": "http://example.org", From b9eccfe2ca488827d696afc10bf4b290a24d01fa Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 26 Feb 2026 12:24:31 +0000 Subject: [PATCH 13/14] inline and simplify _wp_connectors_set_provider_api_key --- src/wp-includes/connectors.php | 56 ++++++++-------------------------- 1 file changed, 13 insertions(+), 43 deletions(-) diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 1d84cd700923f..85a92e31f98df 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -89,45 +89,6 @@ function _wp_connectors_is_api_key_valid( string $key, string $provider_id ): ?b } } -/** - * Sets API key authentication for a provider in the WP AI Client registry. - * - * @since 7.0.0 - * @access private - * - * @param string $key The API key. - * @param string $provider_id The WP AI client provider ID. - * @return bool True if the key was set successfully, false otherwise. - */ -function _wp_connectors_set_provider_api_key( string $key, string $provider_id ): bool { - try { - $registry = AiClient::defaultRegistry(); - - if ( ! $registry->hasProvider( $provider_id ) ) { - _doing_it_wrong( - __FUNCTION__, - sprintf( - /* translators: %s: AI provider ID. */ - __( 'The provider "%s" is not registered in the AI client registry.' ), - $provider_id - ), - '7.0.0' - ); - return false; - } - - $registry->setProviderRequestAuthentication( - $provider_id, - new ApiKeyRequestAuthentication( $key ) - ); - - return true; - } catch ( Exception $e ) { - wp_trigger_error( __FUNCTION__, $e->getMessage() ); - return false; - } -} - /** * Retrieves the real (unmasked) value of a connector API key. * @@ -299,12 +260,21 @@ function _wp_connectors_pass_default_keys_to_ai_client(): void { if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { return; } + try { + $registry = AiClient::defaultRegistry(); + foreach ( _wp_connectors_get_provider_settings() as $setting_name => $config ) { + $api_key = _wp_connectors_get_real_api_key( $setting_name, $config['mask'] ); + if ( '' === $api_key || ! $registry->hasProvider( $config['provider'] ) ) { + continue; + } - foreach ( _wp_connectors_get_provider_settings() as $setting_name => $config ) { - $api_key = _wp_connectors_get_real_api_key( $setting_name, $config['mask'] ); - if ( '' !== $api_key ) { - _wp_connectors_set_provider_api_key( $api_key, $config['provider'] ); + $registry->setProviderRequestAuthentication( + $config['provider'], + new ApiKeyRequestAuthentication( $api_key ) + ); } + } catch ( Exception $e ) { + wp_trigger_error( __FUNCTION__, $e->getMessage() ); } } add_action( 'init', '_wp_connectors_pass_default_keys_to_ai_client' ); From 52fbdca2454078f873da5a73d2f6014b5cf9ab20 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 26 Feb 2026 12:53:01 +0000 Subject: [PATCH 14/14] revert the gutenberg update temporary test change --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3598caec635a7..745ec98ab6b58 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "ref": "4aa81f968a38960a7a3ee32f557a366f285dc08f" + "ref": "23b566c72e9c4a36219ef5d6e62890f05551f6cb" }, "engines": { "node": ">=20.10.0",