Changeset 61824
- Timestamp:
- 03/04/2026 12:48:48 PM (9 days ago)
- Location:
- trunk
- Files:
-
- 1 added
- 1 deleted
- 5 edited
-
src/wp-includes/connectors.php (modified) (8 diffs)
-
tests/phpunit/includes/functions.php (modified) (1 diff)
-
tests/phpunit/includes/wp-ai-client-mock-provider-trait.php (modified) (1 diff)
-
tests/phpunit/tests/connectors/wpConnectorsGetConnectorSettings.php (added)
-
tests/phpunit/tests/connectors/wpConnectorsGetProviderSettings.php (deleted)
-
tests/phpunit/tests/connectors/wpConnectorsIsApiKeyValid.php (modified) (4 diffs)
-
tests/qunit/fixtures/wp-api-generated.js (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/connectors.php
r61749 r61824 61 61 * @return bool|null True if valid, false if invalid, null if unable to determine. 62 62 */ 63 function _wp_connectors_is_a pi_key_valid( string $key, string $provider_id ): ?bool {63 function _wp_connectors_is_ai_api_key_valid( string $key, string $provider_id ): ?bool { 64 64 try { 65 65 $registry = AiClient::defaultRegistry(); … … 110 110 111 111 /** 112 * Gets the registered connector provider settings. 113 * 114 * @since 7.0.0 115 * @access private 116 * 117 * @return array<string, array{provider: string, label: string, description: string, mask: callable, sanitize: callable}> Provider settings keyed by setting name. 118 */ 119 function _wp_connectors_get_provider_settings(): array { 120 $providers = array( 112 * Gets the registered connector settings. 113 * 114 * @since 7.0.0 115 * @access private 116 * 117 * @return array { 118 * Connector settings keyed by connector ID. 119 * 120 * @type array ...$0 { 121 * Data for a single connector. 122 * 123 * @type string $name The connector's display name. 124 * @type string $description The connector's description. 125 * @type string $type The connector type. Currently, only 'ai_provider' is supported. 126 * @type array $plugin Optional. Plugin data for install/activate UI. 127 * @type string $slug The WordPress.org plugin slug. 128 * } 129 * @type array $authentication { 130 * Authentication configuration. When method is 'api_key', includes 131 * credentials_url and setting_name. When 'none', only method is present. 132 * 133 * @type string $method The authentication method: 'api_key' or 'none'. 134 * @type string|null $credentials_url Optional. URL where users can obtain API credentials. 135 * @type string $setting_name Optional. The setting name for the API key. 136 * } 137 * } 138 * } 139 */ 140 function _wp_connectors_get_connector_settings(): array { 141 $connectors = array( 142 'anthropic' => array( 143 'name' => 'Anthropic', 144 'description' => __( 'Text generation with Claude.' ), 145 'type' => 'ai_provider', 146 'plugin' => array( 147 'slug' => 'ai-provider-for-anthropic', 148 ), 149 'authentication' => array( 150 'method' => 'api_key', 151 'credentials_url' => 'https://platform.claude.com/settings/keys', 152 ), 153 ), 121 154 'google' => array( 122 'name' => 'Google', 155 'name' => 'Google', 156 'description' => __( 'Text and image generation with Gemini and Imagen.' ), 157 'type' => 'ai_provider', 158 'plugin' => array( 159 'slug' => 'ai-provider-for-google', 160 ), 161 'authentication' => array( 162 'method' => 'api_key', 163 'credentials_url' => 'https://aistudio.google.com/api-keys', 164 ), 123 165 ), 124 166 'openai' => array( 125 'name' => 'OpenAI', 126 ), 127 'anthropic' => array( 128 'name' => 'Anthropic', 167 'name' => 'OpenAI', 168 'description' => __( 'Text and image generation with GPT and Dall-E.' ), 169 'type' => 'ai_provider', 170 'plugin' => array( 171 'slug' => 'ai-provider-for-openai', 172 ), 173 'authentication' => array( 174 'method' => 'api_key', 175 'credentials_url' => 'https://platform.openai.com/api-keys', 176 ), 129 177 ), 130 178 ); 131 179 132 $provider_settings = array(); 133 foreach ( $providers as $provider => $data ) { 134 $setting_name = "connectors_ai_{$provider}_api_key"; 135 136 $provider_settings[ $setting_name ] = array( 137 'provider' => $provider, 138 'label' => sprintf( 139 /* translators: %s: AI provider name. */ 140 __( '%s API Key' ), 141 $data['name'] 142 ), 143 'description' => sprintf( 144 /* translators: %s: AI provider name. */ 145 __( 'API key for the %s AI provider.' ), 146 $data['name'] 147 ), 148 'mask' => '_wp_connectors_mask_api_key', 149 'sanitize' => static function ( string $value ) use ( $provider ): string { 150 $value = sanitize_text_field( $value ); 151 if ( '' === $value ) { 152 return $value; 153 } 154 155 $valid = _wp_connectors_is_api_key_valid( $value, $provider ); 156 return true === $valid ? $value : ''; 157 }, 158 ); 159 } 160 return $provider_settings; 180 $registry = AiClient::defaultRegistry(); 181 182 foreach ( $registry->getRegisteredProviderIds() as $connector_id ) { 183 $provider_class_name = $registry->getProviderClassName( $connector_id ); 184 $provider_metadata = $provider_class_name::metadata(); 185 186 $auth_method = $provider_metadata->getAuthenticationMethod(); 187 $is_api_key = null !== $auth_method && $auth_method->isApiKey(); 188 189 if ( $is_api_key ) { 190 $credentials_url = $provider_metadata->getCredentialsUrl(); 191 $authentication = array( 192 'method' => 'api_key', 193 'credentials_url' => $credentials_url ? $credentials_url : null, 194 ); 195 } else { 196 $authentication = array( 'method' => 'none' ); 197 } 198 199 $name = $provider_metadata->getName(); 200 $description = $provider_metadata->getDescription(); 201 202 if ( isset( $connectors[ $connector_id ] ) ) { 203 // Override fields with non-empty registry values. 204 if ( $name ) { 205 $connectors[ $connector_id ]['name'] = $name; 206 } 207 if ( $description ) { 208 $connectors[ $connector_id ]['description'] = $description; 209 } 210 // Always update auth method; keep existing credentials_url as fallback. 211 $connectors[ $connector_id ]['authentication']['method'] = $authentication['method']; 212 if ( ! empty( $authentication['credentials_url'] ) ) { 213 $connectors[ $connector_id ]['authentication']['credentials_url'] = $authentication['credentials_url']; 214 } 215 } else { 216 $connectors[ $connector_id ] = array( 217 'name' => $name ? $name : ucwords( $connector_id ), 218 'description' => $description ? $description : '', 219 'type' => 'ai_provider', 220 'authentication' => $authentication, 221 ); 222 } 223 } 224 225 ksort( $connectors ); 226 227 // Add setting_name for connectors that use API key authentication. 228 foreach ( $connectors as $connector_id => $connector ) { 229 if ( 'api_key' === $connector['authentication']['method'] ) { 230 $connectors[ $connector_id ]['authentication']['setting_name'] = "connectors_ai_{$connector_id}_api_key"; 231 } 232 } 233 234 return $connectors; 161 235 } 162 236 … … 182 256 } 183 257 184 if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) {185 return $response;186 }187 188 258 $fields = $request->get_param( '_fields' ); 189 259 if ( ! $fields ) { … … 202 272 } 203 273 204 foreach ( _wp_connectors_get_provider_settings() as $setting_name => $config ) { 274 foreach ( _wp_connectors_get_connector_settings() as $connector_id => $connector_data ) { 275 $auth = $connector_data['authentication']; 276 if ( 'ai_provider' !== $connector_data['type'] || 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) { 277 continue; 278 } 279 280 $setting_name = $auth['setting_name']; 205 281 if ( ! in_array( $setting_name, $requested, true ) ) { 206 282 continue; 207 283 } 208 284 209 $real_key = _wp_connectors_get_real_api_key( $setting_name, $config['mask']);285 $real_key = _wp_connectors_get_real_api_key( $setting_name, '_wp_connectors_mask_api_key' ); 210 286 if ( '' === $real_key ) { 211 287 continue; 212 288 } 213 289 214 if ( true !== _wp_connectors_is_a pi_key_valid( $real_key, $config['provider']) ) {290 if ( true !== _wp_connectors_is_ai_api_key_valid( $real_key, $connector_id ) ) { 215 291 $data[ $setting_name ] = 'invalid_key'; 216 292 } … … 229 305 */ 230 306 function _wp_register_default_connector_settings(): void { 231 if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) { 232 return; 233 } 234 235 foreach ( _wp_connectors_get_provider_settings() as $setting_name => $config ) { 307 foreach ( _wp_connectors_get_connector_settings() as $connector_id => $connector_data ) { 308 $auth = $connector_data['authentication']; 309 if ( 'ai_provider' !== $connector_data['type'] || 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) { 310 continue; 311 } 312 313 $setting_name = $auth['setting_name']; 236 314 register_setting( 237 315 'connectors', … … 239 317 array( 240 318 'type' => 'string', 241 'label' => $config['label'], 242 'description' => $config['description'], 319 'label' => sprintf( 320 /* translators: %s: AI provider name. */ 321 __( '%s API Key' ), 322 $connector_data['name'] 323 ), 324 'description' => sprintf( 325 /* translators: %s: AI provider name. */ 326 __( 'API key for the %s AI provider.' ), 327 $connector_data['name'] 328 ), 243 329 'default' => '', 244 330 'show_in_rest' => true, 245 'sanitize_callback' => $config['sanitize'], 331 'sanitize_callback' => static function ( string $value ) use ( $connector_id ): string { 332 $value = sanitize_text_field( $value ); 333 if ( '' === $value ) { 334 return $value; 335 } 336 337 $valid = _wp_connectors_is_ai_api_key_valid( $value, $connector_id ); 338 return true === $valid ? $value : ''; 339 }, 246 340 ) 247 341 ); 248 add_filter( "option_{$setting_name}", $config['mask']);249 } 250 } 251 add_action( 'init', '_wp_register_default_connector_settings' );342 add_filter( "option_{$setting_name}", '_wp_connectors_mask_api_key' ); 343 } 344 } 345 add_action( 'init', '_wp_register_default_connector_settings', 20 ); 252 346 253 347 /** … … 258 352 */ 259 353 function _wp_connectors_pass_default_keys_to_ai_client(): void { 260 if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) {261 return;262 }263 354 try { 264 355 $registry = AiClient::defaultRegistry(); 265 foreach ( _wp_connectors_get_provider_settings() as $setting_name => $config ) { 266 $api_key = _wp_connectors_get_real_api_key( $setting_name, $config['mask'] ); 267 if ( '' === $api_key || ! $registry->hasProvider( $config['provider'] ) ) { 356 foreach ( _wp_connectors_get_connector_settings() as $connector_id => $connector_data ) { 357 if ( 'ai_provider' !== $connector_data['type'] ) { 268 358 continue; 269 359 } 270 360 361 $auth = $connector_data['authentication']; 362 if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) { 363 continue; 364 } 365 366 $api_key = _wp_connectors_get_real_api_key( $auth['setting_name'], '_wp_connectors_mask_api_key' ); 367 if ( '' === $api_key || ! $registry->hasProvider( $connector_id ) ) { 368 continue; 369 } 370 271 371 $registry->setProviderRequestAuthentication( 272 $con fig['provider'],372 $connector_id, 273 373 new ApiKeyRequestAuthentication( $api_key ) 274 374 ); … … 278 378 } 279 379 } 280 add_action( 'init', '_wp_connectors_pass_default_keys_to_ai_client' ); 380 add_action( 'init', '_wp_connectors_pass_default_keys_to_ai_client', 20 ); 381 382 /** 383 * Exposes connector settings to the connectors-wp-admin script module. 384 * 385 * @since 7.0.0 386 * @access private 387 * 388 * @param array $data Existing script module data. 389 * @return array Script module data with connectors added. 390 */ 391 function _wp_connectors_get_connector_script_module_data( array $data ): array { 392 $connectors = array(); 393 foreach ( _wp_connectors_get_connector_settings() as $connector_id => $connector_data ) { 394 $auth = $connector_data['authentication']; 395 $auth_out = array( 'method' => $auth['method'] ); 396 397 if ( 'api_key' === $auth['method'] ) { 398 $auth_out['settingName'] = $auth['setting_name'] ?? ''; 399 $auth_out['credentialsUrl'] = $auth['credentials_url'] ?? null; 400 } 401 402 $connector_out = array( 403 'name' => $connector_data['name'], 404 'description' => $connector_data['description'], 405 'type' => $connector_data['type'], 406 'authentication' => $auth_out, 407 ); 408 409 if ( ! empty( $connector_data['plugin'] ) ) { 410 $connector_out['plugin'] = $connector_data['plugin']; 411 } 412 413 $connectors[ $connector_id ] = $connector_out; 414 } 415 $data['connectors'] = $connectors; 416 return $data; 417 } 418 add_filter( 'script_module_data_connectors-wp-admin', '_wp_connectors_get_connector_script_module_data' ); -
trunk/tests/phpunit/includes/functions.php
r61083 r61824 377 377 378 378 /** 379 * After the init action has been run once, trying to re-register connector settings can cause 380 * duplicate registrations. To avoid this, unhook the connector registration functions. 381 * 382 * @since 7.0.0 383 */ 384 function _unhook_connector_registration() { 385 remove_action( 'init', '_wp_register_default_connector_settings', 20 ); 386 remove_action( 'init', '_wp_connectors_pass_default_keys_to_ai_client', 20 ); 387 } 388 tests_add_filter( 'init', '_unhook_connector_registration', 1000 ); 389 390 /** 379 391 * Before the abilities API categories init action runs, unhook the core ability 380 392 * categories registration function to prevent core categories from being registered -
trunk/tests/phpunit/includes/wp-ai-client-mock-provider-trait.php
r61749 r61824 170 170 Mock_Connectors_Test_Provider_Availability::$is_configured = $is_configured; 171 171 } 172 173 /** 174 * Unregisters the mock provider's connector setting. 175 * 176 * Reverses the side effect of _wp_register_default_connector_settings() 177 * for the mock provider so that subsequent test classes start with a clean slate. 178 * Must be called from tear_down_after_class() after running tests. 179 */ 180 private static function unregister_mock_connector_setting(): void { 181 $setting_name = 'connectors_ai_mock_connectors_test_api_key'; 182 unregister_setting( 'connectors', $setting_name ); 183 remove_filter( "option_{$setting_name}", '_wp_connectors_mask_api_key' ); 184 } 172 185 } -
trunk/tests/phpunit/tests/connectors/wpConnectorsIsApiKeyValid.php
r61749 r61824 4 4 5 5 /** 6 * Tests for _wp_connectors_is_a pi_key_valid().6 * Tests for _wp_connectors_is_ai_api_key_valid(). 7 7 * 8 8 * @group connectors 9 * @covers ::_wp_connectors_is_a pi_key_valid9 * @covers ::_wp_connectors_is_ai_api_key_valid 10 10 */ 11 11 class Tests_Connectors_WpConnectorsIsApiKeyValid extends WP_UnitTestCase { … … 35 35 */ 36 36 public function test_unregistered_provider_returns_null() { 37 $this->setExpectedIncorrectUsage( '_wp_connectors_is_a pi_key_valid' );37 $this->setExpectedIncorrectUsage( '_wp_connectors_is_ai_api_key_valid' ); 38 38 39 $result = _wp_connectors_is_a pi_key_valid( 'test-key', 'nonexistent_provider' );39 $result = _wp_connectors_is_ai_api_key_valid( 'test-key', 'nonexistent_provider' ); 40 40 41 41 $this->assertNull( $result ); … … 50 50 self::set_mock_provider_configured( true ); 51 51 52 $result = _wp_connectors_is_a pi_key_valid( 'test-key', 'mock_connectors_test' );52 $result = _wp_connectors_is_ai_api_key_valid( 'test-key', 'mock_connectors_test' ); 53 53 54 54 $this->assertTrue( $result ); … … 63 63 self::set_mock_provider_configured( false ); 64 64 65 $result = _wp_connectors_is_a pi_key_valid( 'test-key', 'mock_connectors_test' );65 $result = _wp_connectors_is_ai_api_key_valid( 'test-key', 'mock_connectors_test' ); 66 66 67 67 $this->assertFalse( $result ); -
trunk/tests/qunit/fixtures/wp-api-generated.js
r61809 r61824 11066 11066 ], 11067 11067 "args": { 11068 "connectors_ai_anthropic_api_key": { 11069 "title": "Anthropic API Key", 11070 "description": "API key for the Anthropic AI provider.", 11071 "type": "string", 11072 "required": false 11073 }, 11068 11074 "connectors_ai_google_api_key": { 11069 11075 "title": "Google API Key", … … 11075 11081 "title": "OpenAI API Key", 11076 11082 "description": "API key for the OpenAI AI provider.", 11077 "type": "string",11078 "required": false11079 },11080 "connectors_ai_anthropic_api_key": {11081 "title": "Anthropic API Key",11082 "description": "API key for the Anthropic AI provider.",11083 11083 "type": "string", 11084 11084 "required": false … … 14654 14654 14655 14655 mockedApiResponse.settings = { 14656 "connectors_ai_anthropic_api_key": "", 14656 14657 "connectors_ai_google_api_key": "", 14657 14658 "connectors_ai_openai_api_key": "", 14658 "connectors_ai_anthropic_api_key": "",14659 14659 "title": "Test Blog", 14660 14660 "description": "",
Note: See TracChangeset
for help on using the changeset viewer.