| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Connectors API. 4 * 5 * @package WordPress 6 * @subpackage Connectors 7 * @since 7.0.0 8 */ 9 10 use WordPress\AiClient\AiClient; 11 use WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication; 12 13 /** 14 * Checks if a connector is registered. 15 * 16 * @since 7.0.0 17 * 18 * @see WP_Connector_Registry::is_registered() 19 * 20 * @param string $id The connector identifier. 21 * @return bool True if the connector is registered, false otherwise. 22 */ 23 function wp_is_connector_registered( string $id ): bool { 24 $registry = WP_Connector_Registry::get_instance(); 25 if ( null === $registry ) { 26 return false; 27 } 28 29 return $registry->is_registered( $id ); 30 } 31 32 /** 33 * Retrieves a registered connector. 34 * 35 * @since 7.0.0 36 * 37 * @see WP_Connector_Registry::get_registered() 38 * 39 * @param string $id The connector identifier. 40 * @return array|null { 41 * Connector data, or null if not registered. 42 * 43 * @type string $name The connector's display name. 44 * @type string $description The connector's description. 45 * @type string $logo_url Optional. URL to the connector's logo image. 46 * @type string $type The connector type, e.g. 'ai_provider'. 47 * @type array $authentication { 48 * Authentication configuration. When method is 'api_key' or 49 * 'application_password', includes credentials_url, setting_name, and 50 * optionally constant_name and env_var_name. When 'none', only method 51 * is present. 52 * 53 * @type string $method The authentication method: 'api_key', 54 * 'application_password', or 'none'. 55 * @type string $credentials_url Optional. URL where users can obtain API credentials. 56 * @type string $setting_name Optional. The setting name for the API key or application-password credentials. 57 * @type string $constant_name Optional. PHP constant name for the API key or application-password credentials. 58 * @type string $env_var_name Optional. Environment variable name for the API key or application-password credentials. 59 * } 60 * @type array $plugin { 61 * Optional. Plugin data for install/activate UI. 62 * 63 * @type string $file The plugin's main file path relative to the plugins 64 * directory (e.g. 'my-plugin/my-plugin.php' or 'hello.php'). 65 * @type callable $is_active Callback to determine whether the plugin is active. Receives no arguments and must return bool. 66 * Defaults to `__return_true`. 67 * } 68 * } 69 * @phpstan-return ?array{ 70 * name: non-empty-string, 71 * description: string, 72 * logo_url?: non-empty-string, 73 * type: non-empty-string, 74 * authentication: array{ 75 * method: 'api_key'|'application_password'|'none', 76 * credentials_url?: non-empty-string, 77 * setting_name?: non-empty-string, 78 * constant_name?: non-empty-string, 79 * env_var_name?: non-empty-string 80 * }, 81 * plugin: array{ 82 * file?: non-empty-string, 83 * is_active: callable(): bool, 84 * } 85 * } 86 */ 87 function wp_get_connector( string $id ): ?array { 88 $registry = WP_Connector_Registry::get_instance(); 89 if ( null === $registry ) { 90 return null; 91 } 92 93 return $registry->get_registered( $id ); 94 } 95 96 /** 97 * Retrieves all registered connectors. 98 * 99 * @since 7.0.0 100 * 101 * @see WP_Connector_Registry::get_all_registered() 102 * 103 * @return array { 104 * Connector settings keyed by connector ID. 105 * 106 * @type array ...$0 { 107 * Data for a single connector. 108 * 109 * @type string $name The connector's display name. 110 * @type string $description The connector's description. 111 * @type string $logo_url Optional. URL to the connector's logo image. 112 * @type string $type The connector type, e.g. 'ai_provider'. 113 * @type array $authentication { 114 * Authentication configuration. When method is 'api_key' or 115 * 'application_password', includes credentials_url, setting_name, 116 * and optionally constant_name and env_var_name. When 'none', only 117 * method is present. 118 * 119 * @type string $method The authentication method: 'api_key', 120 * 'application_password', or 'none'. 121 * @type string $credentials_url Optional. URL where users can obtain API credentials. 122 * @type string $setting_name Optional. The setting name for the API key or application-password credentials. 123 * @type string $constant_name Optional. PHP constant name for the API key or application-password credentials. 124 * @type string $env_var_name Optional. Environment variable name for the API key or application-password credentials. 125 * } 126 * @type array $plugin { 127 * Optional. Plugin data for install/activate UI. 128 * 129 * @type string $file The plugin's main file path relative to the plugins 130 * directory (e.g. 'my-plugin/my-plugin.php' or 'hello.php'). 131 * @type callable $is_active Callback to determine whether the plugin is active. Receives no arguments and must return bool. 132 * Defaults to `__return_true`. 133 * } 134 * } 135 * } 136 * @phpstan-return array<string, array{ 137 * name: non-empty-string, 138 * description: string, 139 * logo_url?: non-empty-string, 140 * type: non-empty-string, 141 * authentication: array{ 142 * method: 'api_key'|'application_password'|'none', 143 * credentials_url?: non-empty-string, 144 * setting_name?: non-empty-string, 145 * constant_name?: non-empty-string, 146 * env_var_name?: non-empty-string 147 * }, 148 * plugin: array{ 149 * file?: non-empty-string, 150 * is_active: callable(): bool, 151 * } 152 * }> 153 */ 154 function wp_get_connectors(): array { 155 $registry = WP_Connector_Registry::get_instance(); 156 if ( null === $registry ) { 157 return array(); 158 } 159 160 return $registry->get_all_registered(); 161 } 162 163 /** 164 * Resolves an AI provider logo file path to a URL. 165 * 166 * Converts an absolute file path to a plugin URL. The path must reside within 167 * the plugins or must-use plugins directory. 168 * 169 * @since 7.0.0 170 * @access private 171 * 172 * @param string $path Absolute path to the logo file. 173 * @return non-empty-string|null The URL to the logo file, or null if the path is invalid. 174 */ 175 function _wp_connectors_resolve_ai_provider_logo_url( string $path ): ?string { 176 if ( ! $path ) { 177 return null; 178 } 179 180 $path = wp_normalize_path( $path ); 181 182 if ( ! file_exists( $path ) ) { 183 return null; 184 } 185 186 $mu_plugin_dir = wp_normalize_path( WPMU_PLUGIN_DIR ); 187 if ( str_starts_with( $path, $mu_plugin_dir . '/' ) ) { 188 $logo_url = plugins_url( substr( $path, strlen( $mu_plugin_dir ) ), WPMU_PLUGIN_DIR . '/.' ); 189 return $logo_url ? $logo_url : null; 190 } 191 192 $plugin_dir = wp_normalize_path( WP_PLUGIN_DIR ); 193 if ( str_starts_with( $path, $plugin_dir . '/' ) ) { 194 $logo_url = plugins_url( substr( $path, strlen( $plugin_dir ) ) ); 195 return $logo_url ? $logo_url : null; 196 } 197 198 _doing_it_wrong( 199 __FUNCTION__, 200 __( 'Provider logo path must be located within the plugins or must-use plugins directory.' ), 201 '7.0.0' 202 ); 203 204 return null; 205 } 206 207 /** 208 * Initializes the connector registry with default connectors and fires the registration action. 209 * 210 * Creates the registry instance, registers built-in connectors (which cannot be unhooked), 211 * and then fires the `wp_connectors_init` action for plugins to register their own connectors. 212 * 213 * @since 7.0.0 214 * @access private 215 */ 216 function _wp_connectors_init(): void { 217 $registry = new WP_Connector_Registry(); 218 WP_Connector_Registry::set_instance( $registry ); 219 220 // Only register default AI providers if AI support is enabled. 221 if ( wp_supports_ai() ) { 222 _wp_connectors_register_default_ai_providers( $registry ); 223 } 224 225 // Non-AI default connectors. 226 $registry->register( 227 'akismet', 228 array( 229 'name' => __( 'Akismet Anti-spam' ), 230 'description' => __( 'Protect your site from spam.' ), 231 'type' => 'spam_filtering', 232 'plugin' => array( 233 'file' => 'akismet/akismet.php', 234 'is_active' => static function () { 235 return defined( 'AKISMET_VERSION' ); 236 }, 237 ), 238 'authentication' => array( 239 'method' => 'api_key', 240 'credentials_url' => 'https://akismet.com/get/', 241 'setting_name' => 'wordpress_api_key', 242 'constant_name' => 'WPCOM_API_KEY', 243 ), 244 ) 245 ); 246 247 /** 248 * Fires when the connector registry is ready for plugins to register connectors. 249 * 250 * Built-in connectors and any AI providers auto-discovered from the WP AI Client 251 * registry have already been registered at this point and cannot be unhooked. 252 * 253 * AI provider plugins that register with the WP AI Client do not need to use 254 * this action — their connectors are created automatically. This action is 255 * primarily for registering non-AI-provider connectors or overriding metadata 256 * on existing connectors. 257 * 258 * Use `$registry->register()` within this action to add new connectors. 259 * To override an existing connector, unregister it first, then re-register 260 * with updated data. 261 * 262 * Example — overriding metadata on an auto-discovered connector: 263 * 264 * add_action( 'wp_connectors_init', function ( WP_Connector_Registry $registry ) { 265 * if ( $registry->is_registered( 'anthropic' ) ) { 266 * $connector = $registry->unregister( 'anthropic' ); 267 * $connector['description'] = __( 'Custom description for Anthropic.', 'my-plugin' ); 268 * $registry->register( 'anthropic', $connector ); 269 * } 270 * } ); 271 * 272 * @since 7.0.0 273 * 274 * @param WP_Connector_Registry $registry Connector registry instance. 275 */ 276 do_action( 'wp_connectors_init', $registry ); 277 } 278 279 /** 280 * Registers connectors for the built-in AI providers. 281 * 282 * @since 7.0.0 283 * @access private 284 * 285 * @param WP_Connector_Registry $registry The connector registry instance. 286 */ 287 function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $registry ): void { 288 // Built-in connectors. 289 $defaults = array( 290 'anthropic' => array( 291 'name' => 'Anthropic', 292 'description' => __( 'Text generation with Claude.' ), 293 'type' => 'ai_provider', 294 'plugin' => array( 295 'file' => 'ai-provider-for-anthropic/plugin.php', 296 ), 297 'authentication' => array( 298 'method' => 'api_key', 299 'credentials_url' => 'https://platform.claude.com/settings/keys', 300 ), 301 ), 302 'google' => array( 303 'name' => 'Google', 304 'description' => __( 'Text and image generation with Gemini and Imagen.' ), 305 'type' => 'ai_provider', 306 'plugin' => array( 307 'file' => 'ai-provider-for-google/plugin.php', 308 ), 309 'authentication' => array( 310 'method' => 'api_key', 311 'credentials_url' => 'https://aistudio.google.com/api-keys', 312 ), 313 ), 314 'openai' => array( 315 'name' => 'OpenAI', 316 'description' => __( 'Text and image generation with GPT and Dall-E.' ), 317 'type' => 'ai_provider', 318 'plugin' => array( 319 'file' => 'ai-provider-for-openai/plugin.php', 320 ), 321 'authentication' => array( 322 'method' => 'api_key', 323 'credentials_url' => 'https://platform.openai.com/api-keys', 324 ), 325 ), 326 ); 327 328 // Merge AI Client registry data on top of defaults. 329 // Registry values (from provider plugins) take precedence over hardcoded fallbacks. 330 $ai_registry = AiClient::defaultRegistry(); 331 332 foreach ( array_filter( $ai_registry->getRegisteredProviderIds() ) as $connector_id ) { 333 $provider_class_name = $ai_registry->getProviderClassName( $connector_id ); 334 $provider_metadata = $provider_class_name::metadata(); 335 336 $auth_method = $provider_metadata->getAuthenticationMethod(); 337 $is_api_key = null !== $auth_method && $auth_method->isApiKey(); 338 339 if ( $is_api_key ) { 340 $credentials_url = $provider_metadata->getCredentialsUrl(); 341 $authentication = array( 342 'method' => 'api_key', 343 ); 344 if ( $credentials_url ) { 345 $authentication['credentials_url'] = $credentials_url; 346 } 347 } else { 348 $authentication = array( 'method' => 'none' ); 349 } 350 351 $name = $provider_metadata->getName(); 352 $description = $provider_metadata->getDescription(); 353 $logo_url = $provider_metadata->getLogoPath() 354 ? _wp_connectors_resolve_ai_provider_logo_url( $provider_metadata->getLogoPath() ) 355 : null; 356 357 if ( isset( $defaults[ $connector_id ] ) ) { 358 // Override fields with non-empty registry values. 359 if ( $name ) { 360 $defaults[ $connector_id ]['name'] = $name; 361 } 362 if ( $description ) { 363 $defaults[ $connector_id ]['description'] = $description; 364 } 365 if ( $logo_url ) { 366 $defaults[ $connector_id ]['logo_url'] = $logo_url; 367 } 368 // Always update auth method; keep existing credentials_url as fallback. 369 $defaults[ $connector_id ]['authentication']['method'] = $authentication['method']; 370 if ( ! empty( $authentication['credentials_url'] ) ) { 371 $defaults[ $connector_id ]['authentication']['credentials_url'] = $authentication['credentials_url']; 372 } 373 } else { 374 $defaults[ $connector_id ] = array( 375 'name' => $name ? $name : ucwords( $connector_id ), 376 'description' => $description ? $description : '', 377 'type' => 'ai_provider', 378 'authentication' => $authentication, 379 ); 380 if ( $logo_url ) { 381 $defaults[ $connector_id ]['logo_url'] = $logo_url; 382 } 383 } 384 } 385 386 // Register all default connectors directly on the registry. 387 /** @var lowercase-string&non-empty-string $id AI Client provider IDs are validated as lowercase by ProviderMetadata. */ 388 foreach ( $defaults as $id => $args ) { 389 if ( 'api_key' === $args['authentication']['method'] ) { 390 $sanitized_id = str_replace( '-', '_', $id ); 391 392 $args['authentication']['setting_name'] = "connectors_ai_{$sanitized_id}_api_key"; 393 394 // All AI providers use the {CONSTANT_CASE_ID}_API_KEY naming convention. 395 $constant_case_key = strtoupper( (string) preg_replace( '/([a-z])([A-Z])/', '$1_$2', $sanitized_id ) ) . '_API_KEY'; 396 397 $args['authentication']['constant_name'] = $constant_case_key; 398 $args['authentication']['env_var_name'] = $constant_case_key; 399 } 400 401 $args['plugin']['is_active'] = static function () use ( $ai_registry, $id ): bool { 402 try { 403 return $ai_registry->hasProvider( $id ); 404 } catch ( Exception $e ) { 405 return false; 406 } 407 }; 408 409 $registry->register( $id, $args ); 410 } 411 } 412 413 /** 414 * Masks an API key, showing only the last 4 characters. 415 * 416 * @since 7.0.0 417 * @access private 418 * 419 * @param string $key The API key to mask. 420 * @return string The masked key, e.g. "************fj39". 421 */ 422 function _wp_connectors_mask_api_key( string $key ): string { 423 if ( strlen( $key ) <= 4 ) { 424 return $key; 425 } 426 427 return str_repeat( "\u{2022}", min( strlen( $key ) - 4, 16 ) ) . substr( $key, -4 ); 428 } 429 430 /** 431 * Determines the source of an API key for a given connector. 432 * 433 * Checks in order: environment variable, PHP constant, database. 434 * Environment variable and constant are only checked when their 435 * respective names are provided. 436 * 437 * @since 7.0.0 438 * @access private 439 * 440 * @param string $setting_name The option name for the API key (e.g., 'connectors_spam_filtering_my_plugin_api_key'). 441 * @param string $env_var_name Optional. Environment variable name to check (e.g., 'MY_PLUGIN_API_KEY'). 442 * @param string $constant_name Optional. PHP constant name to check (e.g., 'MY_PLUGIN_API_KEY'). 443 * @return string The key source: 'env', 'constant', 'database', or 'none'. 444 */ 445 function _wp_connectors_get_api_key_source( string $setting_name, string $env_var_name = '', string $constant_name = '' ): string { 446 // Check environment variable first. 447 if ( '' !== $env_var_name ) { 448 $env_value = getenv( $env_var_name ); 449 if ( false !== $env_value && '' !== $env_value ) { 450 return 'env'; 451 } 452 } 453 454 // Check PHP constant. 455 if ( '' !== $constant_name && defined( $constant_name ) ) { 456 $const_value = constant( $constant_name ); 457 if ( is_string( $const_value ) && '' !== $const_value ) { 458 return 'constant'; 459 } 460 } 461 462 // Check database. 463 $db_value = get_option( $setting_name, '' ); 464 if ( '' !== $db_value ) { 465 return 'database'; 466 } 467 468 return 'none'; 469 } 470 471 /** 472 * Parses a `username:password` credentials string. 473 * 474 * Splits on the first colon, matching the HTTP Basic authentication 475 * userinfo format, so passwords may contain colons. 476 * 477 * @since 7.1.0 478 * @access private 479 * 480 * @param string $value The raw credentials string. 481 * @return array{username: string, password: string} Parsed credentials. Both values 482 * are empty when the string is malformed. 483 */ 484 function wp_connectors_parse_application_password_credentials( string $value ): array { 485 $separator = strpos( $value, ':' ); 486 // Trim so surrounding whitespace or a trailing newline (common when the 487 // value comes from a file or `.env`) does not become part of the credentials. 488 $username = false === $separator ? '' : trim( substr( $value, 0, $separator ) ); 489 $password = false === $separator ? '' : trim( substr( $value, $separator + 1 ) ); 490 491 if ( '' === $username || '' === $password ) { 492 return array( 493 'username' => '', 494 'password' => '', 495 ); 496 } 497 498 return array( 499 'username' => $username, 500 'password' => $password, 501 ); 502 } 503 504 /** 505 * Resolves application-password credentials for a connector. 506 * 507 * Checks in order: environment variable, PHP constant, database. The 508 * environment variable and constant are only checked when their respective 509 * names are provided, and must contain the credentials as a single 510 * `username:password` string. A non-empty environment variable or constant 511 * that cannot be parsed as `username:password` is reported with 512 * `_doing_it_wrong()` and ignored, so resolution falls through to the next 513 * source. 514 * 515 * @since 7.1.0 516 * @access private 517 * 518 * @param array $auth The connector's authentication configuration. 519 * @return array{username: string, password: string, source: string} Resolved credentials and 520 * their source: 'env', 'constant', 521 * 'database', or 'none'. 522 */ 523 function wp_connectors_get_application_password_credentials( array $auth ): array { 524 // Check environment variable first. 525 $env_var_name = $auth['env_var_name'] ?? ''; 526 if ( '' !== $env_var_name ) { 527 $env_value = getenv( $env_var_name ); 528 if ( false !== $env_value && '' !== $env_value ) { 529 $credentials = wp_connectors_parse_application_password_credentials( $env_value ); 530 if ( '' !== $credentials['username'] && '' !== $credentials['password'] ) { 531 $credentials['source'] = 'env'; 532 return $credentials; 533 } 534 535 _doing_it_wrong( 536 __FUNCTION__, 537 sprintf( 538 /* translators: %s: Environment variable name. */ 539 __( 'The %s environment variable must contain application password credentials in "username:password" format.' ), 540 esc_html( $env_var_name ) 541 ), 542 '7.1.0' 543 ); 544 } 545 } 546 547 // Check PHP constant. 548 $constant_name = $auth['constant_name'] ?? ''; 549 if ( '' !== $constant_name && defined( $constant_name ) ) { 550 $const_value = constant( $constant_name ); 551 if ( is_string( $const_value ) && '' !== $const_value ) { 552 $credentials = wp_connectors_parse_application_password_credentials( $const_value ); 553 if ( '' !== $credentials['username'] && '' !== $credentials['password'] ) { 554 $credentials['source'] = 'constant'; 555 return $credentials; 556 } 557 558 _doing_it_wrong( 559 __FUNCTION__, 560 sprintf( 561 /* translators: %s: PHP constant name. */ 562 __( 'The %s constant must contain application password credentials in "username:password" format.' ), 563 esc_html( $constant_name ) 564 ), 565 '7.1.0' 566 ); 567 } 568 } 569 570 // Check database. 571 $stored = get_option( $auth['setting_name'] ?? '', array() ); 572 $username = is_array( $stored ) && isset( $stored['username'] ) && is_string( $stored['username'] ) ? $stored['username'] : ''; 573 $password = is_array( $stored ) && isset( $stored['password'] ) && is_string( $stored['password'] ) ? $stored['password'] : ''; 574 575 return array( 576 'username' => $username, 577 'password' => $password, 578 'source' => '' !== $username && '' !== $password ? 'database' : 'none', 579 ); 580 } 581 582 /** 583 * Checks whether an API key is valid for a given provider. 584 * 585 * @since 7.0.0 586 * @access private 587 * 588 * @param string $key The API key to check. 589 * @param string $provider_id The WP AI client provider ID. 590 * @return bool|null True if valid, false if invalid, null if unable to determine. 591 */ 592 function _wp_connectors_is_ai_api_key_valid( string $key, string $provider_id ): ?bool { 593 try { 594 $registry = AiClient::defaultRegistry(); 595 596 if ( ! $registry->hasProvider( $provider_id ) ) { 597 _doing_it_wrong( 598 __FUNCTION__, 599 sprintf( 600 /* translators: %s: AI provider ID. */ 601 __( 'The provider "%s" is not registered in the AI client registry.' ), 602 $provider_id 603 ), 604 '7.0.0' 605 ); 606 return null; 607 } 608 609 $registry->setProviderRequestAuthentication( 610 $provider_id, 611 new ApiKeyRequestAuthentication( $key ) 612 ); 613 614 return $registry->isProviderConfigured( $provider_id ); 615 } catch ( Exception $e ) { 616 wp_trigger_error( __FUNCTION__, $e->getMessage() ); 617 return null; 618 } 619 } 620 621 /** 622 * Sanitizes stored application-password credentials for a connector. 623 * 624 * Credential fields that are missing or not strings keep their currently 625 * stored values, so partial updates cannot silently clear a stored secret. 626 * A password matching the mask that `_wp_connectors_rest_settings_dispatch()` 627 * places in REST responses also keeps the stored password, so a masked 628 * settings response can be submitted back to the endpoint unchanged. 629 * Pass an empty string to clear a field. 630 * If the sanitized username is empty, both fields are discarded so partial 631 * credentials cannot leave an orphaned secret. 632 * 633 * @since 7.1.0 634 * @access private 635 * 636 * @param mixed $value The submitted setting value. 637 * @param string $option The option name being sanitized. Passed explicitly by the 638 * registered sanitize callback; falls back to the current 639 * `sanitize_option_{$option}` filter name when omitted. 640 * @return array{username: string, password: string} Sanitized credentials. 641 */ 642 function wp_connectors_sanitize_application_password_credentials( $value, string $option = '' ): array { 643 if ( ! is_array( $value ) ) { 644 $value = array(); 645 } 646 647 if ( '' === $option ) { 648 $option = str_replace( 'sanitize_option_', '', (string) current_filter() ); 649 } 650 651 $stored = get_option( $option ); 652 if ( ! is_array( $stored ) ) { 653 $stored = array(); 654 } 655 656 $credentials = array(); 657 foreach ( array( 'username', 'password' ) as $field ) { 658 if ( isset( $value[ $field ] ) && is_string( $value[ $field ] ) ) { 659 $credentials[ $field ] = sanitize_text_field( $value[ $field ] ); 660 } else { 661 $credentials[ $field ] = isset( $stored[ $field ] ) && is_string( $stored[ $field ] ) ? $stored[ $field ] : ''; 662 } 663 } 664 665 // A masked password means a client resubmitted a masked REST response. 666 if ( str_repeat( "\u{2022}", 16 ) === $credentials['password'] ) { 667 $credentials['password'] = isset( $stored['password'] ) && is_string( $stored['password'] ) ? $stored['password'] : ''; 668 } 669 670 if ( '' === $credentials['username'] ) { 671 return array( 672 'username' => '', 673 'password' => '', 674 ); 675 } 676 677 return $credentials; 678 } 679 680 /** 681 * Masks and validates connector credentials in REST responses. 682 * 683 * On every `/wp/v2/settings` response, masks connector API key values and the 684 * password field of default application-password credential objects. 685 * 686 * On POST or PUT requests, validates each updated AI provider API key before 687 * masking. If validation fails, the key is reverted to an empty string. 688 * Application password values are masked but not validated. 689 * 690 * @since 7.0.0 691 * @access private 692 * 693 * @param WP_REST_Response $response The response object. 694 * @param WP_REST_Server $server The server instance. 695 * @param WP_REST_Request $request The request object. 696 * @return WP_REST_Response The modified response with masked/validated keys. 697 */ 698 function _wp_connectors_rest_settings_dispatch( WP_REST_Response $response, WP_REST_Server $server, WP_REST_Request $request ): WP_REST_Response { 699 // Match the route the way WP_REST_Server::match_request_to_handler() does, ignoring case. 700 if ( ! preg_match( '@^/wp/v2/settings$@i', $request->get_route() ) ) { 701 return $response; 702 } 703 704 $data = $response->get_data(); 705 if ( ! is_array( $data ) ) { 706 return $response; 707 } 708 709 $is_update = 'POST' === $request->get_method() || 'PUT' === $request->get_method(); 710 711 foreach ( wp_get_connectors() as $connector_id => $connector_data ) { 712 $auth = $connector_data['authentication']; 713 714 if ( 'application_password' === $auth['method'] && ! empty( $auth['setting_name'] ) ) { 715 $setting_name = $auth['setting_name']; 716 if ( array_key_exists( $setting_name, $data ) && is_array( $data[ $setting_name ] ) ) { 717 $password = $data[ $setting_name ]['password'] ?? ''; 718 if ( is_string( $password ) && '' !== $password ) { 719 $data[ $setting_name ]['password'] = str_repeat( "\u{2022}", 16 ); 720 } 721 } 722 continue; 723 } 724 725 if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) { 726 continue; 727 } 728 729 $setting_name = $auth['setting_name']; 730 if ( ! array_key_exists( $setting_name, $data ) ) { 731 continue; 732 } 733 734 $value = $data[ $setting_name ]; 735 736 // On update, validate AI provider keys submitted in the request before masking. 737 // Non-AI connectors accept keys as-is; the service plugin handles its own validation. 738 if ( $is_update 739 && $request->has_param( $setting_name ) 740 && is_string( $value ) && '' !== $value 741 && 'ai_provider' === $connector_data['type'] 742 ) { 743 if ( true !== _wp_connectors_is_ai_api_key_valid( $value, $connector_id ) ) { 744 update_option( $setting_name, '' ); 745 $data[ $setting_name ] = ''; 746 continue; 747 } 748 } 749 750 // Mask the key in the response. 751 if ( is_string( $value ) && '' !== $value ) { 752 $data[ $setting_name ] = _wp_connectors_mask_api_key( $value ); 753 } 754 } 755 756 $response->set_data( $data ); 757 return $response; 758 } 759 add_filter( 'rest_post_dispatch', '_wp_connectors_rest_settings_dispatch', 10, 3 ); 760 761 /** 762 * Registers default connector settings. 763 * 764 * @since 7.0.0 765 * @access private 766 */ 767 function _wp_register_default_connector_settings(): void { 768 $registered_settings = get_registered_settings(); 769 770 foreach ( wp_get_connectors() as $connector_data ) { 771 $auth = $connector_data['authentication']; 772 if ( 'api_key' !== $auth['method'] && 'application_password' !== $auth['method'] ) { 773 continue; 774 } 775 776 if ( empty( $auth['setting_name'] ) || isset( $registered_settings[ $auth['setting_name'] ] ) ) { 777 continue; 778 } 779 $setting_name = $auth['setting_name']; 780 781 if ( ! isset( $connector_data['plugin']['is_active'] ) || ! is_callable( $connector_data['plugin']['is_active'] ) ) { 782 continue; 783 } 784 785 if ( ! call_user_func( $connector_data['plugin']['is_active'] ) ) { 786 continue; 787 } 788 789 if ( 'api_key' === $auth['method'] ) { 790 register_setting( 791 'connectors', 792 $setting_name, 793 array( 794 'type' => 'string', 795 'label' => sprintf( 796 /* translators: %s: Connector name. */ 797 __( '%s API Key' ), 798 $connector_data['name'] 799 ), 800 'description' => sprintf( 801 /* translators: %s: Connector name. */ 802 __( 'API key for the %s connector.' ), 803 $connector_data['name'] 804 ), 805 'default' => '', 806 'show_in_rest' => true, 807 'sanitize_callback' => 'sanitize_text_field', 808 ) 809 ); 810 } elseif ( 'application_password' === $auth['method'] ) { 811 register_setting( 812 'connectors', 813 $setting_name, 814 array( 815 'type' => 'object', 816 'label' => sprintf( 817 /* translators: %s: Connector name. */ 818 __( '%s Credentials' ), 819 $connector_data['name'] 820 ), 821 'description' => sprintf( 822 /* translators: %s: Connector name. */ 823 __( 'Application password credentials for the %s connector.' ), 824 $connector_data['name'] 825 ), 826 'default' => array( 827 'username' => '', 828 'password' => '', 829 ), 830 'show_in_rest' => array( 831 'schema' => array( 832 'type' => 'object', 833 'properties' => array( 834 'username' => array( 835 'type' => 'string', 836 ), 837 'password' => array( 838 'type' => 'string', 839 ), 840 ), 841 'additionalProperties' => false, 842 ), 843 ), 844 'sanitize_callback' => static function ( $value ) use ( $setting_name ) { 845 return wp_connectors_sanitize_application_password_credentials( $value, $setting_name ); 846 }, 847 ) 848 ); 849 } 850 } 851 } 852 add_action( 'init', '_wp_register_default_connector_settings', 20 ); 853 854 /** 855 * Passes stored connector API keys to the WP AI client. 856 * 857 * @since 7.0.0 858 * @access private 859 */ 860 function _wp_connectors_pass_default_keys_to_ai_client(): void { 861 try { 862 $ai_registry = AiClient::defaultRegistry(); 863 foreach ( wp_get_connectors() as $connector_id => $connector_data ) { 864 if ( 'ai_provider' !== $connector_data['type'] ) { 865 continue; 866 } 867 868 $auth = $connector_data['authentication']; 869 if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) { 870 continue; 871 } 872 873 if ( ! $ai_registry->hasProvider( $connector_id ) ) { 874 continue; 875 } 876 877 // Skip if the key is already provided via env var or constant. 878 $key_source = _wp_connectors_get_api_key_source( $auth['setting_name'], $auth['env_var_name'] ?? '', $auth['constant_name'] ?? '' ); 879 if ( 'env' === $key_source || 'constant' === $key_source ) { 880 continue; 881 } 882 883 $api_key = get_option( $auth['setting_name'], '' ); 884 if ( ! is_string( $api_key ) || '' === $api_key ) { 885 continue; 886 } 887 888 $ai_registry->setProviderRequestAuthentication( 889 $connector_id, 890 new ApiKeyRequestAuthentication( $api_key ) 891 ); 892 } 893 } catch ( Exception $e ) { 894 wp_trigger_error( __FUNCTION__, $e->getMessage() ); 895 } 896 } 897 add_action( 'init', '_wp_connectors_pass_default_keys_to_ai_client', 20 ); 898 899 /** 900 * Exposes connector settings to the connectors-wp-admin script module. 901 * 902 * @since 7.0.0 903 * @access private 904 * 905 * @param array<string, mixed> $data Existing script module data. 906 * @return array<string, mixed> Script module data with connectors added. 907 */ 908 function _wp_connectors_get_connector_script_module_data( array $data ): array { 909 $registry = AiClient::defaultRegistry(); 910 911 if ( ! function_exists( 'validate_plugin' ) ) { 912 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 913 } 914 915 $connectors = array(); 916 foreach ( wp_get_connectors() as $connector_id => $connector_data ) { 917 $auth = $connector_data['authentication']; 918 $auth_out = array( 'method' => $auth['method'] ); 919 920 if ( 'api_key' === $auth['method'] ) { 921 $auth_out['settingName'] = $auth['setting_name'] ?? ''; 922 $auth_out['credentialsUrl'] = $auth['credentials_url'] ?? null; 923 $key_source = _wp_connectors_get_api_key_source( $auth['setting_name'] ?? '', $auth['env_var_name'] ?? '', $auth['constant_name'] ?? '' ); 924 $auth_out['keySource'] = $key_source; 925 926 if ( 'ai_provider' === $connector_data['type'] ) { 927 try { 928 $auth_out['isConnected'] = $registry->hasProvider( $connector_id ) && $registry->isProviderConfigured( $connector_id ); 929 } catch ( Exception $e ) { 930 $auth_out['isConnected'] = false; 931 } 932 } else { 933 $auth_out['isConnected'] = 'none' !== $key_source; 934 } 935 } elseif ( 'application_password' === $auth['method'] ) { 936 $credentials = wp_connectors_get_application_password_credentials( $auth ); 937 938 $auth_out['settingName'] = $auth['setting_name'] ?? ''; 939 $auth_out['credentialsUrl'] = $auth['credentials_url'] ?? null; 940 $auth_out['keySource'] = $credentials['source']; 941 $auth_out['isConnected'] = '' !== $credentials['username'] && '' !== $credentials['password']; 942 } 943 944 $connector_out = array( 945 'name' => $connector_data['name'], 946 'description' => $connector_data['description'], 947 'logoUrl' => ! empty( $connector_data['logo_url'] ) ? $connector_data['logo_url'] : null, 948 'type' => $connector_data['type'], 949 'authentication' => $auth_out, 950 ); 951 952 if ( ! empty( $connector_data['plugin']['file'] ) ) { 953 $file = $connector_data['plugin']['file']; 954 $is_activated = (bool) call_user_func( $connector_data['plugin']['is_active'] ); 955 $is_installed = $is_activated || 0 === validate_plugin( $file ); 956 957 $connector_out['plugin'] = array( 958 'file' => $file, 959 'isInstalled' => $is_installed, 960 'isActivated' => $is_activated, 961 ); 962 } 963 964 $connectors[ $connector_id ] = $connector_out; 965 } 966 ksort( $connectors ); 967 $data['connectors'] = $connectors; 968 $data['isFileModDisabled'] = ! wp_is_file_mod_allowed( 'install_plugins' ); 969 return $data; 970 } 971 add_filter( 'script_module_data_options-connectors-wp-admin', '_wp_connectors_get_connector_script_module_data' );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sun Nov 1 08:20:35 2026 | Cross-referenced by PHPXref |