@@ -60,7 +60,7 @@ function _wp_connectors_mask_api_key( string $key ): string {
6060 * @param string $provider_id The WP AI client provider ID.
6161 * @return bool|null True if valid, false if invalid, null if unable to determine.
6262 */
63- function _wp_connectors_is_api_key_valid ( string $ key , string $ provider_id ): ?bool {
63+ function _wp_connectors_is_ai_api_key_valid ( string $ key , string $ provider_id ): ?bool {
6464 try {
6565 $ registry = AiClient::defaultRegistry ();
6666
@@ -109,55 +109,129 @@ function _wp_connectors_get_real_api_key( string $option_name, callable $mask_ca
109109}
110110
111111/**
112- * Gets the registered connector provider settings.
112+ * Gets the registered connector settings.
113113 *
114114 * @since 7.0.0
115115 * @access private
116116 *
117- * @return array<string, array{provider: string, label: string, description: string, mask: callable, sanitize: callable}> Provider settings keyed by setting name.
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+ * }
118139 */
119- function _wp_connectors_get_provider_settings (): array {
120- $ providers = array (
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+ ),
121154 '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+ ),
123165 ),
124166 '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+ ),
129177 ),
130178 );
131179
132- $ provider_settings = array ();
133- foreach ( $ providers as $ provider => $ data ) {
134- $ setting_name = "connectors_ai_ {$ provider }_api_key " ;
180+ $ registry = AiClient::defaultRegistry ();
135181
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- );
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+ }
159223 }
160- return $ provider_settings ;
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 ;
161235}
162236
163237/**
@@ -181,10 +255,6 @@ function _wp_connectors_validate_keys_in_rest( WP_REST_Response $response, WP_RE
181255 return $ response ;
182256 }
183257
184- if ( ! class_exists ( '\WordPress\AiClient\AiClient ' ) ) {
185- return $ response ;
186- }
187-
188258 $ fields = $ request ->get_param ( '_fields ' );
189259 if ( ! $ fields ) {
190260 return $ response ;
@@ -201,17 +271,23 @@ function _wp_connectors_validate_keys_in_rest( WP_REST_Response $response, WP_RE
201271 return $ response ;
202272 }
203273
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 ' ];
205281 if ( ! in_array ( $ setting_name , $ requested , true ) ) {
206282 continue ;
207283 }
208284
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 ' );
210286 if ( '' === $ real_key ) {
211287 continue ;
212288 }
213289
214- if ( true !== _wp_connectors_is_api_key_valid ( $ real_key , $ config [ ' provider ' ] ) ) {
290+ if ( true !== _wp_connectors_is_ai_api_key_valid ( $ real_key , $ connector_id ) ) {
215291 $ data [ $ setting_name ] = 'invalid_key ' ;
216292 }
217293 }
@@ -228,27 +304,45 @@ function _wp_connectors_validate_keys_in_rest( WP_REST_Response $response, WP_RE
228304 * @access private
229305 */
230306function _wp_register_default_connector_settings (): void {
231- if ( ! class_exists ( '\WordPress\AiClient\AiClient ' ) ) {
232- return ;
233- }
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+ }
234312
235- foreach ( _wp_connectors_get_provider_settings () as $ setting_name => $ config ) {
313+ $ setting_name = $ auth [ ' setting_name ' ];
236314 register_setting (
237315 'connectors ' ,
238316 $ setting_name ,
239317 array (
240318 '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+ ),
243329 'default ' => '' ,
244330 '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+ },
246340 )
247341 );
248- add_filter ( "option_ {$ setting_name }" , $ config [ ' mask ' ] );
342+ add_filter ( "option_ {$ setting_name }" , ' _wp_connectors_mask_api_key ' );
249343 }
250344}
251- add_action ( 'init ' , '_wp_register_default_connector_settings ' );
345+ add_action ( 'init ' , '_wp_register_default_connector_settings ' , 20 );
252346
253347/**
254348 * Passes stored connector API keys to the WP AI client.
@@ -257,24 +351,68 @@ function _wp_register_default_connector_settings(): void {
257351 * @access private
258352 */
259353function _wp_connectors_pass_default_keys_to_ai_client (): void {
260- if ( ! class_exists ( '\WordPress\AiClient\AiClient ' ) ) {
261- return ;
262- }
263354 try {
264355 $ 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 ' ] ) {
358+ continue ;
359+ }
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 ) ) {
268368 continue ;
269369 }
270370
271371 $ registry ->setProviderRequestAuthentication (
272- $ config [ ' provider ' ] ,
372+ $ connector_id ,
273373 new ApiKeyRequestAuthentication ( $ api_key )
274374 );
275375 }
276376 } catch ( Exception $ e ) {
277377 wp_trigger_error ( __FUNCTION__ , $ e ->getMessage () );
278378 }
279379}
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 ' );
0 commit comments