| [ 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 foreach ( $defaults as $id => $args ) { 388 if ( 'api_key' === $args['authentication']['method'] ) { 389 $sanitized_id = str_replace( '-', '_', $id ); 390 391 $args['authentication']['setting_name'] = "connectors_ai_{$sanitized_id}_api_key"; 392 393 // All AI providers use the {CONSTANT_CASE_ID}_API_KEY naming convention. 394 $constant_case_key = strtoupper( (string) preg_replace( '/([a-z])([A-Z])/', '$1_$2', $sanitized_id ) ) . '_API_KEY'; 395 396 $args['authentication']['constant_name'] = $constant_case_key; 397 $args['authentication']['env_var_name'] = $constant_case_key; 398 } 399 400 $args['plugin']['is_active'] = static function () use ( $ai_registry, $id ): bool { 401 try { 402 return $ai_registry->hasProvider( $id ); 403 } catch ( Exception $e ) { 404 return false; 405 } 406 }; 407 408 $registry->register( $id, $args ); 409 } 410 } 411 412 /** 413 * Masks an API key, showing only the last 4 characters. 414 * 415 * @since 7.0.0 416 * @access private 417 * 418 * @param string $key The API key to mask. 419 * @return string The masked key, e.g. "************fj39". 420 */ 421 function _wp_connectors_mask_api_key( string $key ): string { 422 if ( strlen( $key ) <= 4 ) { 423 return $key; 424 } 425 426 return str_repeat( "\u{2022}", min( strlen( $key ) - 4, 16 ) ) . substr( $key, -4 ); 427 } 428 429 /** 430 * Determines the source of an API key for a given connector. 431 * 432 * Checks in order: environment variable, PHP constant, database. 433 * Environment variable and constant are only checked when their 434 * respective names are provided. 435 * 436 * @since 7.0.0 437 * @access private 438 * 439 * @param string $setting_name The option name for the API key (e.g., 'connectors_spam_filtering_my_plugin_api_key'). 440 * @param string $env_var_name Optional. Environment variable name to check (e.g., 'MY_PLUGIN_API_KEY'). 441 * @param string $constant_name Optional. PHP constant name to check (e.g., 'MY_PLUGIN_API_KEY'). 442 * @return string The key source: 'env', 'constant', 'database', or 'none'. 443 */ 444 function _wp_connectors_get_api_key_source( string $setting_name, string $env_var_name = '', string $constant_name = '' ): string { 445 // Check environment variable first. 446 if ( '' !== $env_var_name ) { 447 $env_value = getenv( $env_var_name ); 448 if ( false !== $env_value && '' !== $env_value ) { 449 return 'env'; 450 } 451 } 452 453 // Check PHP constant. 454 if ( '' !== $constant_name && defined( $constant_name ) ) { 455 $const_value = constant( $constant_name ); 456 if ( is_string( $const_value ) && '' !== $const_value ) { 457 return 'constant'; 458 } 459 } 460 461 // Check database. 462 $db_value = get_option( $setting_name, '' ); 463 if ( '' !== $db_value ) { 464 return 'database'; 465 } 466 467 return 'none'; 468 } 469 470 /** 471 * Parses a `username:password` credentials string. 472 * 473 * Splits on the first colon, matching the HTTP Basic authentication 474 * userinfo format, so passwords may contain colons. 475 * 476 * @since 7.1.0 477 * @access private 478 * 479 * @param string $value The raw credentials string. 480 * @return array{username: string, password: string} Parsed credentials. Both values 481 * are empty when the string is malformed. 482 */ 483 function wp_connectors_parse_application_password_credentials( string $value ): array { 484 $separator = strpos( $value, ':' ); 485 // Trim so surrounding whitespace or a trailing newline (common when the 486 // value comes from a file or `.env`) does not become part of the credentials. 487 $username = false === $separator ? '' : trim( substr( $value, 0, $separator ) ); 488 $password = false === $separator ? '' : trim( substr( $value, $separator + 1 ) ); 489 490 if ( '' === $username || '' === $password ) { 491 return array( 492 'username' => '', 493 'password' => '', 494 ); 495 } 496 497 return array( 498 'username' => $username, 499 'password' => $password, 500 ); 501 } 502 503 /** 504 * Resolves application-password credentials for a connector. 505 * 506 * Checks in order: environment variable, PHP constant, database. The 507 * environment variable and constant are only checked when their respective 508 * names are provided, and must contain the credentials as a single 509 * `username:password` string. A non-empty environment variable or constant 510 * that cannot be parsed as `username:password` is reported with 511 * `_doing_it_wrong()` and ignored, so resolution falls through to the next 512 * source. 513 * 514 * @since 7.1.0 515 * @access private 516 * 517 * @param array $auth The connector's authentication configuration. 518 * @return array{username: string, password: string, source: string} Resolved credentials and 519 * their source: 'env', 'constant', 520 * 'database', or 'none'. 521 */ 522 function wp_connectors_get_application_password_credentials( array $auth ): array { 523 // Check environment variable first. 524 $env_var_name = $auth['env_var_name'] ?? ''; 525 if ( '' !== $env_var_name ) { 526 $env_value = getenv( $env_var_name ); 527 if ( false !== $env_value && '' !== $env_value ) { 528 $credentials = wp_connectors_parse_application_password_credentials( $env_value ); 529 if ( '' !== $credentials['username'] && '' !== $credentials['password'] ) { 530 $credentials['source'] = 'env'; 531 return $credentials; 532 } 533 534 _doing_it_wrong( 535 __FUNCTION__, 536 sprintf( 537 /* translators: %s: Environment variable name. */ 538 __( 'The %s environment variable must contain application password credentials in "username:password" format.' ), 539 esc_html( $env_var_name ) 540 ), 541 '7.1.0' 542 ); 543 } 544 } 545 546 // Check PHP constant. 547 $constant_name = $auth['constant_name'] ?? ''; 548 if ( '' !== $constant_name && defined( $constant_name ) ) { 549 $const_value = constant( $constant_name ); 550 if ( is_string( $const_value ) && '' !== $const_value ) { 551 $credentials = wp_connectors_parse_application_password_credentials( $const_value ); 552 if ( '' !== $credentials['username'] && '' !== $credentials['password'] ) { 553 $credentials['source'] = 'constant'; 554 return $credentials; 555 } 556 557 _doing_it_wrong( 558 __FUNCTION__, 559 sprintf( 560 /* translators: %s: PHP constant name. */ 561 __( 'The %s constant must contain application password credentials in "username:password" format.' ), 562 esc_html( $constant_name ) 563 ), 564 '7.1.0' 565 ); 566 } 567 } 568 569 // Check database. 570 $stored = get_option( $auth['setting_name'] ?? '', array() ); 571 $username = is_array( $stored ) && isset( $stored['username'] ) && is_string( $stored['username'] ) ? $stored['username'] : ''; 572 $password = is_array( $stored ) && isset( $stored['password'] ) && is_string( $stored['password'] ) ? $stored['password'] : ''; 573 574 return array( 575 'username' => $username, 576 'password' => $password, 577 'source' => '' !== $username && '' !== $password ? 'database' : 'none', 578 ); 579 } 580 581 /** 582 * Checks whether an API key is valid for a given provider. 583 * 584 * @since 7.0.0 585 * @access private 586 * 587 * @param string $key The API key to check. 588 * @param string $provider_id The WP AI client provider ID. 589 * @return bool|null True if valid, false if invalid, null if unable to determine. 590 */ 591 function _wp_connectors_is_ai_api_key_valid( string $key, string $provider_id ): ?bool { 592 try { 593 $registry = AiClient::defaultRegistry(); 594 595 if ( ! $registry->hasProvider( $provider_id ) ) { 596 _doing_it_wrong( 597 __FUNCTION__, 598 sprintf( 599 /* translators: %s: AI provider ID. */ 600 __( 'The provider "%s" is not registered in the AI client registry.' ), 601 $provider_id 602 ), 603 '7.0.0' 604 ); 605 return null; 606 } 607 608 $registry->setProviderRequestAuthentication( 609 $provider_id, 610 new ApiKeyRequestAuthentication( $key ) 611 ); 612 613 return $registry->isProviderConfigured( $provider_id ); 614 } catch ( Exception $e ) { 615 wp_trigger_error( __FUNCTION__, $e->getMessage() ); 616 return null; 617 } 618 } 619 620 /** 621 * Sanitizes stored application-password credentials for a connector. 622 * 623 * Credential fields that are missing or not strings keep their currently 624 * stored values, so partial updates cannot silently clear a stored secret. 625 * A password matching the mask that `_wp_connectors_rest_settings_dispatch()` 626 * places in REST responses also keeps the stored password, so a masked 627 * settings response can be submitted back to the endpoint unchanged. 628 * Pass an empty string to clear a field. 629 * If the sanitized username is empty, both fields are discarded so partial 630 * credentials cannot leave an orphaned secret. 631 * 632 * @since 7.1.0 633 * @access private 634 * 635 * @param mixed $value The submitted setting value. 636 * @param string $option The option name being sanitized. Passed explicitly by the 637 * registered sanitize callback; falls back to the current 638 * `sanitize_option_{$option}` filter name when omitted. 639 * @return array{username: string, password: string} Sanitized credentials. 640 */ 641 function wp_connectors_sanitize_application_password_credentials( $value, string $option = '' ): array { 642 if ( ! is_array( $value ) ) { 643 $value = array(); 644 } 645 646 if ( '' === $option ) { 647 $option = str_replace( 'sanitize_option_', '', (string) current_filter() ); 648 } 649 650 $stored = get_option( $option ); 651 if ( ! is_array( $stored ) ) { 652 $stored = array(); 653 } 654 655 $credentials = array(); 656 foreach ( array( 'username', 'password' ) as $field ) { 657 if ( isset( $value[ $field ] ) && is_string( $value[ $field ] ) ) { 658 $credentials[ $field ] = sanitize_text_field( $value[ $field ] ); 659 } else { 660 $credentials[ $field ] = isset( $stored[ $field ] ) && is_string( $stored[ $field ] ) ? $stored[ $field ] : ''; 661 } 662 } 663 664 // A masked password means a client resubmitted a masked REST response. 665 if ( str_repeat( "\u{2022}", 16 ) === $credentials['password'] ) { 666 $credentials['password'] = isset( $stored['password'] ) && is_string( $stored['password'] ) ? $stored['password'] : ''; 667 } 668 669 if ( '' === $credentials['username'] ) { 670 return array( 671 'username' => '', 672 'password' => '', 673 ); 674 } 675 676 return $credentials; 677 } 678 679 /** 680 * Masks and validates connector credentials in REST responses. 681 * 682 * On every `/wp/v2/settings` response, masks connector API key values and the 683 * password field of default application-password credential objects. 684 * 685 * On POST or PUT requests, validates each updated AI provider API key before 686 * masking. If validation fails, the key is reverted to an empty string. 687 * Application password values are masked but not validated. 688 * 689 * @since 7.0.0 690 * @access private 691 * 692 * @param WP_REST_Response $response The response object. 693 * @param WP_REST_Server $server The server instance. 694 * @param WP_REST_Request $request The request object. 695 * @return WP_REST_Response The modified response with masked/validated keys. 696 */ 697 function _wp_connectors_rest_settings_dispatch( WP_REST_Response $response, WP_REST_Server $server, WP_REST_Request $request ): WP_REST_Response { 698 if ( '/wp/v2/settings' !== $request->get_route() ) { 699 return $response; 700 } 701 702 $data = $response->get_data(); 703 if ( ! is_array( $data ) ) { 704 return $response; 705 } 706 707 $is_update = 'POST' === $request->get_method() || 'PUT' === $request->get_method(); 708 709 foreach ( wp_get_connectors() as $connector_id => $connector_data ) { 710 $auth = $connector_data['authentication']; 711 712 if ( 'application_password' === $auth['method'] && ! empty( $auth['setting_name'] ) ) { 713 $setting_name = $auth['setting_name']; 714 if ( array_key_exists( $setting_name, $data ) && is_array( $data[ $setting_name ] ) ) { 715 $password = $data[ $setting_name ]['password'] ?? ''; 716 if ( is_string( $password ) && '' !== $password ) { 717 $data[ $setting_name ]['password'] = str_repeat( "\u{2022}", 16 ); 718 } 719 } 720 continue; 721 } 722 723 if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) { 724 continue; 725 } 726 727 $setting_name = $auth['setting_name']; 728 if ( ! array_key_exists( $setting_name, $data ) ) { 729 continue; 730 } 731 732 $value = $data[ $setting_name ]; 733 734 // On update, validate AI provider keys before masking. 735 // Non-AI connectors accept keys as-is; the service plugin handles its own validation. 736 if ( $is_update && is_string( $value ) && '' !== $value && 'ai_provider' === $connector_data['type'] ) { 737 if ( true !== _wp_connectors_is_ai_api_key_valid( $value, $connector_id ) ) { 738 update_option( $setting_name, '' ); 739 $data[ $setting_name ] = ''; 740 continue; 741 } 742 } 743 744 // Mask the key in the response. 745 if ( is_string( $value ) && '' !== $value ) { 746 $data[ $setting_name ] = _wp_connectors_mask_api_key( $value ); 747 } 748 } 749 750 $response->set_data( $data ); 751 return $response; 752 } 753 add_filter( 'rest_post_dispatch', '_wp_connectors_rest_settings_dispatch', 10, 3 ); 754 755 /** 756 * Registers default connector settings. 757 * 758 * @since 7.0.0 759 * @access private 760 */ 761 function _wp_register_default_connector_settings(): void { 762 $registered_settings = get_registered_settings(); 763 764 foreach ( wp_get_connectors() as $connector_data ) { 765 $auth = $connector_data['authentication']; 766 if ( 'api_key' !== $auth['method'] && 'application_password' !== $auth['method'] ) { 767 continue; 768 } 769 770 if ( empty( $auth['setting_name'] ) || isset( $registered_settings[ $auth['setting_name'] ] ) ) { 771 continue; 772 } 773 $setting_name = $auth['setting_name']; 774 775 if ( ! isset( $connector_data['plugin']['is_active'] ) || ! is_callable( $connector_data['plugin']['is_active'] ) ) { 776 continue; 777 } 778 779 if ( ! call_user_func( $connector_data['plugin']['is_active'] ) ) { 780 continue; 781 } 782 783 if ( 'api_key' === $auth['method'] ) { 784 register_setting( 785 'connectors', 786 $setting_name, 787 array( 788 'type' => 'string', 789 'label' => sprintf( 790 /* translators: %s: Connector name. */ 791 __( '%s API Key' ), 792 $connector_data['name'] 793 ), 794 'description' => sprintf( 795 /* translators: %s: Connector name. */ 796 __( 'API key for the %s connector.' ), 797 $connector_data['name'] 798 ), 799 'default' => '', 800 'show_in_rest' => true, 801 'sanitize_callback' => 'sanitize_text_field', 802 ) 803 ); 804 } elseif ( 'application_password' === $auth['method'] ) { 805 register_setting( 806 'connectors', 807 $setting_name, 808 array( 809 'type' => 'object', 810 'label' => sprintf( 811 /* translators: %s: Connector name. */ 812 __( '%s Credentials' ), 813 $connector_data['name'] 814 ), 815 'description' => sprintf( 816 /* translators: %s: Connector name. */ 817 __( 'Application password credentials for the %s connector.' ), 818 $connector_data['name'] 819 ), 820 'default' => array( 821 'username' => '', 822 'password' => '', 823 ), 824 'show_in_rest' => array( 825 'schema' => array( 826 'type' => 'object', 827 'properties' => array( 828 'username' => array( 829 'type' => 'string', 830 ), 831 'password' => array( 832 'type' => 'string', 833 ), 834 ), 835 'additionalProperties' => false, 836 ), 837 ), 838 'sanitize_callback' => static function ( $value ) use ( $setting_name ) { 839 return wp_connectors_sanitize_application_password_credentials( $value, $setting_name ); 840 }, 841 ) 842 ); 843 } 844 } 845 } 846 add_action( 'init', '_wp_register_default_connector_settings', 20 ); 847 848 /** 849 * Passes stored connector API keys to the WP AI client. 850 * 851 * @since 7.0.0 852 * @access private 853 */ 854 function _wp_connectors_pass_default_keys_to_ai_client(): void { 855 try { 856 $ai_registry = AiClient::defaultRegistry(); 857 foreach ( wp_get_connectors() as $connector_id => $connector_data ) { 858 if ( 'ai_provider' !== $connector_data['type'] ) { 859 continue; 860 } 861 862 $auth = $connector_data['authentication']; 863 if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) { 864 continue; 865 } 866 867 if ( ! $ai_registry->hasProvider( $connector_id ) ) { 868 continue; 869 } 870 871 // Skip if the key is already provided via env var or constant. 872 $key_source = _wp_connectors_get_api_key_source( $auth['setting_name'], $auth['env_var_name'] ?? '', $auth['constant_name'] ?? '' ); 873 if ( 'env' === $key_source || 'constant' === $key_source ) { 874 continue; 875 } 876 877 $api_key = get_option( $auth['setting_name'], '' ); 878 if ( ! is_string( $api_key ) || '' === $api_key ) { 879 continue; 880 } 881 882 $ai_registry->setProviderRequestAuthentication( 883 $connector_id, 884 new ApiKeyRequestAuthentication( $api_key ) 885 ); 886 } 887 } catch ( Exception $e ) { 888 wp_trigger_error( __FUNCTION__, $e->getMessage() ); 889 } 890 } 891 add_action( 'init', '_wp_connectors_pass_default_keys_to_ai_client', 20 ); 892 893 /** 894 * Exposes connector settings to the connectors-wp-admin script module. 895 * 896 * @since 7.0.0 897 * @access private 898 * 899 * @param array<string, mixed> $data Existing script module data. 900 * @return array<string, mixed> Script module data with connectors added. 901 */ 902 function _wp_connectors_get_connector_script_module_data( array $data ): array { 903 $registry = AiClient::defaultRegistry(); 904 905 if ( ! function_exists( 'validate_plugin' ) ) { 906 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 907 } 908 909 $connectors = array(); 910 foreach ( wp_get_connectors() as $connector_id => $connector_data ) { 911 $auth = $connector_data['authentication']; 912 $auth_out = array( 'method' => $auth['method'] ); 913 914 if ( 'api_key' === $auth['method'] ) { 915 $auth_out['settingName'] = $auth['setting_name'] ?? ''; 916 $auth_out['credentialsUrl'] = $auth['credentials_url'] ?? null; 917 $key_source = _wp_connectors_get_api_key_source( $auth['setting_name'] ?? '', $auth['env_var_name'] ?? '', $auth['constant_name'] ?? '' ); 918 $auth_out['keySource'] = $key_source; 919 920 if ( 'ai_provider' === $connector_data['type'] ) { 921 try { 922 $auth_out['isConnected'] = $registry->hasProvider( $connector_id ) && $registry->isProviderConfigured( $connector_id ); 923 } catch ( Exception $e ) { 924 $auth_out['isConnected'] = false; 925 } 926 } else { 927 $auth_out['isConnected'] = 'none' !== $key_source; 928 } 929 } elseif ( 'application_password' === $auth['method'] ) { 930 $credentials = wp_connectors_get_application_password_credentials( $auth ); 931 932 $auth_out['settingName'] = $auth['setting_name'] ?? ''; 933 $auth_out['credentialsUrl'] = $auth['credentials_url'] ?? null; 934 $auth_out['keySource'] = $credentials['source']; 935 $auth_out['isConnected'] = '' !== $credentials['username'] && '' !== $credentials['password']; 936 } 937 938 $connector_out = array( 939 'name' => $connector_data['name'], 940 'description' => $connector_data['description'], 941 'logoUrl' => ! empty( $connector_data['logo_url'] ) ? $connector_data['logo_url'] : null, 942 'type' => $connector_data['type'], 943 'authentication' => $auth_out, 944 ); 945 946 if ( ! empty( $connector_data['plugin']['file'] ) ) { 947 $file = $connector_data['plugin']['file']; 948 $is_activated = (bool) call_user_func( $connector_data['plugin']['is_active'] ); 949 $is_installed = $is_activated || 0 === validate_plugin( $file ); 950 951 $connector_out['plugin'] = array( 952 'file' => $file, 953 'isInstalled' => $is_installed, 954 'isActivated' => $is_activated, 955 ); 956 } 957 958 $connectors[ $connector_id ] = $connector_out; 959 } 960 ksort( $connectors ); 961 $data['connectors'] = $connectors; 962 $data['isFileModDisabled'] = ! wp_is_file_mod_allowed( 'install_plugins' ); 963 return $data; 964 } 965 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 : Fri Jul 24 08:20:19 2026 | Cross-referenced by PHPXref |