Changeset 61985
- Timestamp:
- 03/12/2026 12:05:33 PM (3 weeks ago)
- Location:
- trunk/src/wp-includes
- Files:
-
- 2 edited
-
connectors.php (modified) (12 diffs)
-
default-filters.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/connectors.php
r61983 r61985 340 340 341 341 /** 342 * Determines the source of an API key for a given provider. 343 * 344 * Checks in order: environment variable, PHP constant, database. 345 * Uses the same naming convention as the WP AI Client ProviderRegistry. 346 * 347 * @since 7.0.0 348 * @access private 349 * 350 * @param string $provider_id The provider ID (e.g., 'openai', 'anthropic', 'google'). 351 * @param string $setting_name The option name for the API key (e.g., 'connectors_ai_openai_api_key'). 352 * @return string The key source: 'env', 'constant', 'database', or 'none'. 353 */ 354 function _wp_connectors_get_api_key_source( string $provider_id, string $setting_name ): string { 355 // Convert provider ID to CONSTANT_CASE for env var name. 356 // e.g., 'openai' -> 'OPENAI', 'anthropic' -> 'ANTHROPIC'. 357 $constant_case_id = strtoupper( 358 preg_replace( '/([a-z])([A-Z])/', '$1_$2', str_replace( '-', '_', $provider_id ) ) 359 ); 360 $env_var_name = "{$constant_case_id}_API_KEY"; 361 362 // Check environment variable first. 363 $env_value = getenv( $env_var_name ); 364 if ( false !== $env_value && '' !== $env_value ) { 365 return 'env'; 366 } 367 368 // Check PHP constant. 369 if ( defined( $env_var_name ) ) { 370 $const_value = constant( $env_var_name ); 371 if ( is_string( $const_value ) && '' !== $const_value ) { 372 return 'constant'; 373 } 374 } 375 376 // Check database. 377 $db_value = get_option( $setting_name, '' ); 378 if ( '' !== $db_value ) { 379 return 'database'; 380 } 381 382 return 'none'; 383 } 384 385 /** 342 386 * Checks whether an API key is valid for a given provider. 343 387 * … … 379 423 380 424 /** 381 * Retrieves the real (unmasked) value of a connector API key. 382 * 383 * Temporarily removes the masking filter, reads the option, then re-adds it. 384 * 385 * @since 7.0.0 386 * @access private 387 * 388 * @param string $option_name The option name for the API key. 389 * @param callable $mask_callback The mask filter function. 390 * @return string The real API key value. 391 */ 392 function _wp_connectors_get_real_api_key( string $option_name, callable $mask_callback ): string { 393 remove_filter( "option_{$option_name}", $mask_callback ); 394 $value = get_option( $option_name, '' ); 395 add_filter( "option_{$option_name}", $mask_callback ); 396 return (string) $value; 397 } 398 399 /** 400 * Validates connector API keys in the REST response when explicitly requested. 401 * 402 * Runs on `rest_post_dispatch` for `/wp/v2/settings` requests that include connector 403 * fields via `_fields`. For each requested connector field, it validates the unmasked 404 * key against the provider and replaces the response value with `invalid_key` if 405 * validation fails. 425 * Masks and validates connector API keys in REST responses. 426 * 427 * On every `/wp/v2/settings` response, masks connector API key values so raw 428 * keys are never exposed via the REST API. 429 * 430 * On POST or PUT requests, validates each updated key against the provider 431 * before masking. If validation fails, the key is reverted to an empty string. 406 432 * 407 433 * @since 7.0.0 … … 411 437 * @param WP_REST_Server $server The server instance. 412 438 * @param WP_REST_Request $request The request object. 413 * @return WP_REST_Response The potentially modified response.414 */ 415 function _wp_connectors_ validate_keys_in_rest( WP_REST_Response $response, WP_REST_Server $server, WP_REST_Request $request ): WP_REST_Response {439 * @return WP_REST_Response The modified response with masked/validated keys. 440 */ 441 function _wp_connectors_rest_settings_dispatch( WP_REST_Response $response, WP_REST_Server $server, WP_REST_Request $request ): WP_REST_Response { 416 442 if ( '/wp/v2/settings' !== $request->get_route() ) { 417 443 return $response; 418 }419 420 $fields = $request->get_param( '_fields' );421 if ( ! $fields ) {422 return $response;423 }424 425 if ( is_array( $fields ) ) {426 $requested = $fields;427 } else {428 $requested = array_map( 'trim', explode( ',', $fields ) );429 444 } 430 445 … … 433 448 return $response; 434 449 } 450 451 $is_update = 'POST' === $request->get_method() || 'PUT' === $request->get_method(); 435 452 436 453 foreach ( wp_get_connectors() as $connector_id => $connector_data ) { … … 441 458 442 459 $setting_name = $auth['setting_name']; 443 if ( ! in_array( $setting_name, $requested, true) ) {460 if ( ! array_key_exists( $setting_name, $data ) ) { 444 461 continue; 445 462 } 446 463 447 $real_key = _wp_connectors_get_real_api_key( $setting_name, '_wp_connectors_mask_api_key' ); 448 if ( '' === $real_key ) { 449 continue; 450 } 451 452 if ( true !== _wp_connectors_is_ai_api_key_valid( $real_key, $connector_id ) ) { 453 $data[ $setting_name ] = 'invalid_key'; 464 $value = $data[ $setting_name ]; 465 466 // On update, validate the key before masking. 467 if ( $is_update && is_string( $value ) && '' !== $value ) { 468 if ( true !== _wp_connectors_is_ai_api_key_valid( $value, $connector_id ) ) { 469 update_option( $setting_name, '' ); 470 $data[ $setting_name ] = ''; 471 continue; 472 } 473 } 474 475 // Mask the key in the response. 476 if ( is_string( $value ) && '' !== $value ) { 477 $data[ $setting_name ] = _wp_connectors_mask_api_key( $value ); 454 478 } 455 479 } … … 458 482 return $response; 459 483 } 460 add_filter( 'rest_post_dispatch', '_wp_connectors_ validate_keys_in_rest', 10, 3 );461 462 /** 463 * Registers default connector settings and mask/sanitize filters.484 add_filter( 'rest_post_dispatch', '_wp_connectors_rest_settings_dispatch', 10, 3 ); 485 486 /** 487 * Registers default connector settings. 464 488 * 465 489 * @since 7.0.0 … … 480 504 } 481 505 482 $setting_name = $auth['setting_name'];483 506 register_setting( 484 507 'connectors', 485 $ setting_name,508 $auth['setting_name'], 486 509 array( 487 510 'type' => 'string', … … 498 521 'default' => '', 499 522 'show_in_rest' => true, 500 'sanitize_callback' => static function ( string $value ) use ( $connector_id ): string { 501 $value = sanitize_text_field( $value ); 502 if ( '' === $value ) { 503 return $value; 504 } 505 506 $valid = _wp_connectors_is_ai_api_key_valid( $value, $connector_id ); 507 return true === $valid ? $value : ''; 508 }, 523 'sanitize_callback' => 'sanitize_text_field', 509 524 ) 510 525 ); 511 add_filter( "option_{$setting_name}", '_wp_connectors_mask_api_key' );512 526 } 513 527 } … … 537 551 } 538 552 539 $api_key = _wp_connectors_get_real_api_key( $auth['setting_name'], '_wp_connectors_mask_api_key' ); 553 // Skip if the key is already provided via env var or constant. 554 $key_source = _wp_connectors_get_api_key_source( $connector_id, $auth['setting_name'] ); 555 if ( 'env' === $key_source || 'constant' === $key_source ) { 556 continue; 557 } 558 559 $api_key = get_option( $auth['setting_name'], '' ); 540 560 if ( '' === $api_key ) { 541 561 continue; … … 563 583 */ 564 584 function _wp_connectors_get_connector_script_module_data( array $data ): array { 585 $registry = AiClient::defaultRegistry(); 586 587 // Build a slug-to-file map for plugin installation status. 588 if ( ! function_exists( 'get_plugins' ) ) { 589 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 590 } 591 $plugin_files_by_slug = array(); 592 foreach ( array_keys( get_plugins() ) as $plugin_file ) { 593 $slug = str_contains( $plugin_file, '/' ) ? dirname( $plugin_file ) : str_replace( '.php', '', $plugin_file ); 594 $plugin_files_by_slug[ $slug ] = $plugin_file; 595 } 596 565 597 $connectors = array(); 566 598 foreach ( wp_get_connectors() as $connector_id => $connector_data ) { … … 571 603 $auth_out['settingName'] = $auth['setting_name'] ?? ''; 572 604 $auth_out['credentialsUrl'] = $auth['credentials_url'] ?? null; 605 $auth_out['keySource'] = _wp_connectors_get_api_key_source( $connector_id, $auth['setting_name'] ?? '' ); 606 try { 607 $auth_out['isConnected'] = $registry->hasProvider( $connector_id ) && $registry->isProviderConfigured( $connector_id ); 608 } catch ( Exception $e ) { 609 $auth_out['isConnected'] = false; 610 } 573 611 } 574 612 … … 576 614 'name' => $connector_data['name'], 577 615 'description' => $connector_data['description'], 616 'logoUrl' => ! empty( $connector_data['logo_url'] ) ? $connector_data['logo_url'] : null, 578 617 'type' => $connector_data['type'], 579 618 'authentication' => $auth_out, 580 619 ); 581 620 582 if ( ! empty( $connector_data['plugin'] ) ) { 583 $connector_out['plugin'] = $connector_data['plugin']; 621 if ( ! empty( $connector_data['plugin']['slug'] ) ) { 622 $plugin_slug = $connector_data['plugin']['slug']; 623 $plugin_file = $plugin_files_by_slug[ $plugin_slug ] ?? null; 624 625 $is_installed = null !== $plugin_file; 626 $is_activated = $is_installed && is_plugin_active( $plugin_file ); 627 628 $connector_out['plugin'] = array( 629 'slug' => $plugin_slug, 630 'isInstalled' => $is_installed, 631 'isActivated' => $is_activated, 632 ); 584 633 } 585 634 -
trunk/src/wp-includes/default-filters.php
r61981 r61985 541 541 542 542 // Connectors API. 543 add_action( 'init', '_wp_connectors_init' );543 add_action( 'init', '_wp_connectors_init', 15 ); 544 544 545 545 // Sitemaps actions.
Note: See TracChangeset
for help on using the changeset viewer.