PluginProbe
Gutenberg / 22.7.0
Gutenberg v22.7.0
24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 All 403 releases
gutenberg / lib / experimental / connectors / default-connectors.php

default-connectors.php in Gutenberg 22.7.0, at lib/experimental/connectors/default-connectors.php

401 lines 12.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Default connectors backend logic.
4 *
5 * @package gutenberg
6 */
7
8 /**
9 * Masks an API key, showing only the last 4 characters.
10 *
11 * @access private
12 *
13 * @param string $key The API key to mask.
14 * @return string The masked key, e.g. "************fj39".
15 */
16 function _gutenberg_mask_api_key( string $key ): string {
17 if ( strlen( $key ) <= 4 ) {
18 return $key;
19 }
20
21 return str_repeat( "\u{2022}", min( strlen( $key ) - 4, 16 ) ) . substr( $key, -4 );
22 }
23
24 /**
25 * Checks whether an API key is valid for a given provider.
26 *
27 * @access private
28 *
29 * @param string $key The API key to check.
30 * @param string $provider_id The WP AI client provider ID.
31 * @return bool|null True if valid, false if invalid, null if unable to determine.
32 */
33 function _gutenberg_is_ai_api_key_valid( string $key, string $provider_id ): ?bool {
34 try {
35 $registry = \WordPress\AiClient\AiClient::defaultRegistry();
36
37 if ( ! $registry->hasProvider( $provider_id ) ) {
38 _doing_it_wrong(
39 __FUNCTION__,
40 sprintf(
41 /* translators: %s: AI provider ID. */
42 __( 'The provider "%s" is not registered in the AI client registry.', 'gutenberg' ),
43 $provider_id
44 ),
45 '7.0.0'
46 );
47 return null;
48 }
49
50 $registry->setProviderRequestAuthentication(
51 $provider_id,
52 new \WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication( $key )
53 );
54
55 return $registry->isProviderConfigured( $provider_id );
56 } catch ( Exception $e ) {
57 wp_trigger_error( __FUNCTION__, $e->getMessage() );
58 return null;
59 }
60 }
61
62 /**
63 * Retrieves the real (unmasked) value of a connector API key.
64 *
65 * Temporarily removes the masking filter, reads the option, then re-adds it.
66 *
67 * @access private
68 *
69 * @param string $option_name The option name for the API key.
70 * @param callable $mask_callback The mask filter function.
71 * @return string The real API key value.
72 */
73 function _gutenberg_get_real_api_key( string $option_name, callable $mask_callback ): string {
74 remove_filter( "option_{$option_name}", $mask_callback );
75 $value = get_option( $option_name, '' );
76 add_filter( "option_{$option_name}", $mask_callback );
77 return (string) $value;
78 }
79
80 /**
81 * Gets the registered connector settings.
82 *
83 * @access private
84 *
85 * @return array {
86 * Connector settings keyed by connector ID.
87 *
88 * @type array ...$0 {
89 * Data for a single connector.
90 *
91 * @type string $name The connector's display name.
92 * @type string $description The connector's description.
93 * @type string $type The connector type. Currently, only 'ai_provider' is supported.
94 * @type array $plugin Optional. Plugin data for install/activate UI.
95 * @type string $slug The WordPress.org plugin slug.
96 * }
97 * @type array $authentication {
98 * Authentication configuration. When method is 'api_key', includes
99 * credentials_url and setting_name. When 'none', only method is present.
100 *
101 * @type string $method The authentication method: 'api_key' or 'none'.
102 * @type string|null $credentials_url Optional. URL where users can obtain API credentials.
103 * @type string $setting_name Optional. The setting name for the API key.
104 * }
105 * }
106 * }
107 */
108 function _gutenberg_get_connector_settings(): array {
109 $connectors = array(
110 'google' => array(
111 'name' => 'Google',
112 'description' => __( 'Text and image generation with Gemini and Imagen.', 'gutenberg' ),
113 'type' => 'ai_provider',
114 'plugin' => array(
115 'slug' => 'ai-provider-for-google',
116 ),
117 'authentication' => array(
118 'method' => 'api_key',
119 'credentials_url' => 'https://aistudio.google.com/api-keys',
120 ),
121 ),
122 'openai' => array(
123 'name' => 'OpenAI',
124 'description' => __( 'Text and image generation with GPT and Dall-E.', 'gutenberg' ),
125 'type' => 'ai_provider',
126 'plugin' => array(
127 'slug' => 'ai-provider-for-openai',
128 ),
129 'authentication' => array(
130 'method' => 'api_key',
131 'credentials_url' => 'https://platform.openai.com/api-keys',
132 ),
133 ),
134 'anthropic' => array(
135 'name' => 'Anthropic',
136 'description' => __( 'Text generation with Claude.', 'gutenberg' ),
137 'type' => 'ai_provider',
138 'plugin' => array(
139 'slug' => 'ai-provider-for-anthropic',
140 ),
141 'authentication' => array(
142 'method' => 'api_key',
143 'credentials_url' => 'https://platform.claude.com/settings/keys',
144 ),
145 ),
146 );
147
148 $registry = \WordPress\AiClient\AiClient::defaultRegistry();
149
150 foreach ( $registry->getRegisteredProviderIds() as $connector_id ) {
151 $provider_class = $registry->getProviderClassName( $connector_id );
152 $metadata = $provider_class::metadata();
153
154 $auth_method = $metadata->getAuthenticationMethod();
155 $is_api_key = null !== $auth_method && $auth_method->isApiKey();
156
157 if ( $is_api_key ) {
158 $credentials_url = $metadata->getCredentialsUrl();
159 $authentication = array(
160 'method' => 'api_key',
161 'credentials_url' => $credentials_url ? $credentials_url : null,
162 );
163 } else {
164 $authentication = array( 'method' => 'none' );
165 }
166
167 $name = $metadata->getName();
168 $description = method_exists( $metadata, 'getDescription' ) ? $metadata->getDescription() : null;
169
170 if ( isset( $connectors[ $connector_id ] ) ) {
171 // Override fields with non-empty registry values.
172 if ( $name ) {
173 $connectors[ $connector_id ]['name'] = $name;
174 }
175 if ( $description ) {
176 $connectors[ $connector_id ]['description'] = $description;
177 }
178 // Always update auth method; keep existing credentials_url as fallback.
179 $connectors[ $connector_id ]['authentication']['method'] = $authentication['method'];
180 if ( ! empty( $authentication['credentials_url'] ) ) {
181 $connectors[ $connector_id ]['authentication']['credentials_url'] = $authentication['credentials_url'];
182 }
183 } else {
184 $connectors[ $connector_id ] = array(
185 'name' => $name ? $name : ucwords( $connector_id ),
186 'description' => $description ? $description : '',
187 'type' => 'ai_provider',
188 'authentication' => $authentication,
189 );
190 }
191 }
192
193 // Add setting_name for connectors that use API key authentication.
194 foreach ( $connectors as $connector_id => $connector ) {
195 if ( 'api_key' === $connector['authentication']['method'] ) {
196 $connectors[ $connector_id ]['authentication']['setting_name'] = "connectors_ai_{$connector_id}_api_key";
197 }
198 }
199
200 return $connectors;
201 }
202
203 /**
204 * Validates connector API keys in the REST response when explicitly requested.
205 *
206 * Runs on `rest_post_dispatch` for `/wp/v2/settings` requests that include connector
207 * fields via `_fields`. For each requested connector field, it validates the unmasked
208 * key against the provider and replaces the response value with `invalid_key` if
209 * validation fails.
210 *
211 * @access private
212 *
213 * @param WP_REST_Response $response The response object.
214 * @param WP_REST_Server $server The server instance.
215 * @param WP_REST_Request $request The request object.
216 * @return WP_REST_Response The potentially modified response.
217 */
218 function _gutenberg_validate_connector_keys_in_rest( WP_REST_Response $response, WP_REST_Server $server, WP_REST_Request $request ): WP_REST_Response {
219 if ( '/wp/v2/settings' !== $request->get_route() ) {
220 return $response;
221 }
222
223 if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) {
224 return $response;
225 }
226
227 $fields = $request->get_param( '_fields' );
228 if ( ! $fields ) {
229 return $response;
230 }
231
232 if ( is_array( $fields ) ) {
233 $requested = $fields;
234 } else {
235 $requested = array_map( 'trim', explode( ',', $fields ) );
236 }
237
238 $data = $response->get_data();
239 if ( ! is_array( $data ) ) {
240 return $response;
241 }
242
243 foreach ( _gutenberg_get_connector_settings() as $connector_id => $connector_data ) {
244 $auth = $connector_data['authentication'];
245 if ( 'ai_provider' !== $connector_data['type'] || 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) {
246 continue;
247 }
248
249 $setting_name = $auth['setting_name'];
250 if ( ! in_array( $setting_name, $requested, true ) ) {
251 continue;
252 }
253
254 $real_key = _gutenberg_get_real_api_key( $setting_name, '_gutenberg_mask_api_key' );
255 if ( '' === $real_key ) {
256 continue;
257 }
258
259 if ( true !== _gutenberg_is_ai_api_key_valid( $real_key, $connector_id ) ) {
260 $data[ $setting_name ] = 'invalid_key';
261 }
262 }
263
264 $response->set_data( $data );
265 return $response;
266 }
267 remove_filter( 'rest_post_dispatch', '_wp_connectors_validate_keys_in_rest', 10 );
268 add_filter( 'rest_post_dispatch', '_gutenberg_validate_connector_keys_in_rest', 10, 3 );
269
270 /**
271 * Registers default connector settings and mask/sanitize filters.
272 *
273 * @access private
274 */
275 function _gutenberg_register_default_connector_settings(): void {
276 if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) {
277 return;
278 }
279
280 foreach ( _gutenberg_get_connector_settings() as $connector_id => $connector_data ) {
281 $auth = $connector_data['authentication'];
282 if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) {
283 continue;
284 }
285
286 $setting_name = $auth['setting_name'];
287 register_setting(
288 'connectors',
289 $setting_name,
290 array(
291 'type' => 'string',
292 'label' => sprintf(
293 /* translators: %s: AI provider name. */
294 __( '%s API Key', 'gutenberg' ),
295 $connector_data['name']
296 ),
297 'description' => sprintf(
298 /* translators: %s: AI provider name. */
299 __( 'API key for the %s AI provider.', 'gutenberg' ),
300 $connector_data['name']
301 ),
302 'default' => '',
303 'show_in_rest' => true,
304 'sanitize_callback' => static function ( string $value ) use ( $connector_id ): string {
305 $value = sanitize_text_field( $value );
306 if ( '' === $value ) {
307 return $value;
308 }
309
310 $valid = _gutenberg_is_ai_api_key_valid( $value, $connector_id );
311 return true === $valid ? $value : '';
312 },
313 )
314 );
315 add_filter( "option_{$setting_name}", '_gutenberg_mask_api_key' );
316 }
317 }
318 remove_action( 'init', '_wp_register_default_connector_settings' );
319 add_action( 'init', '_gutenberg_register_default_connector_settings' );
320
321 /**
322 * Passes stored connector API keys to the WP AI client.
323 *
324 * @access private
325 */
326 function _gutenberg_pass_default_connector_keys_to_ai_client(): void {
327 if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) {
328 return;
329 }
330
331 try {
332 $registry = \WordPress\AiClient\AiClient::defaultRegistry();
333 foreach ( _gutenberg_get_connector_settings() as $connector_id => $connector_data ) {
334 if ( 'ai_provider' !== $connector_data['type'] ) {
335 continue;
336 }
337
338 $auth = $connector_data['authentication'];
339 if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) {
340 continue;
341 }
342
343 $api_key = _gutenberg_get_real_api_key( $auth['setting_name'], '_gutenberg_mask_api_key' );
344 if ( '' === $api_key || ! $registry->hasProvider( $connector_id ) ) {
345 continue;
346 }
347
348 $registry->setProviderRequestAuthentication(
349 $connector_id,
350 new \WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication( $api_key )
351 );
352 }
353 } catch ( Exception $e ) {
354 wp_trigger_error( __FUNCTION__, $e->getMessage() );
355 }
356 }
357 remove_action( 'init', '_wp_connectors_pass_default_keys_to_ai_client' );
358 add_action( 'init', '_gutenberg_pass_default_connector_keys_to_ai_client' );
359
360 /**
361 * Exposes connector settings to the connectors-wp-admin script module.
362 *
363 * @access private
364 *
365 * @param array $data Existing script module data.
366 * @return array Script module data with connectors added.
367 */
368 function _gutenberg_get_connector_script_module_data( array $data ): array {
369 if ( ! class_exists( '\WordPress\AiClient\AiClient' ) ) {
370 return $data;
371 }
372
373 $connectors = array();
374 foreach ( _gutenberg_get_connector_settings() as $connector_id => $connector_data ) {
375 $auth = $connector_data['authentication'];
376 $auth_out = array( 'method' => $auth['method'] );
377
378 if ( 'api_key' === $auth['method'] ) {
379 $auth_out['settingName'] = $auth['setting_name'] ?? '';
380 $auth_out['credentialsUrl'] = $auth['credentials_url'] ?? null;
381 }
382
383 $connector_out = array(
384 'name' => $connector_data['name'],
385 'description' => $connector_data['description'],
386 'type' => $connector_data['type'],
387 'authentication' => $auth_out,
388 );
389
390 if ( ! empty( $connector_data['plugin'] ) ) {
391 $connector_out['plugin'] = $connector_data['plugin'];
392 }
393
394 $connectors[ $connector_id ] = $connector_out;
395 }
396 $data['connectors'] = $connectors;
397 return $data;
398 }
399 remove_filter( 'script_module_data_connectors-wp-admin', '_wp_connectors_get_connector_script_module_data' );
400 add_filter( 'script_module_data_connectors-wp-admin', '_gutenberg_get_connector_script_module_data' );
401