Make WordPress Core


Ignore:
Timestamp:
04/01/2026 04:37:48 PM (4 months ago)
Author:
jorgefilipecosta
Message:

Connectors: Fix and generalize the API for custom connector types.

Validate setting_name, constant_name, and env_var_name in connector
registration — reject invalid values with _doing_it_wrong() instead of
silently falling back.
Change the auto-generated setting_name pattern from
connectors_ai_{$id}_api_key to connectors_{$type}_{$id}_api_key so it
works for any connector type. Built-in AI providers infer their names using
the existing connectors_ai_{$id}_api_key convention, preserving backward
compatibility.
Add constant_name and env_var_name as optional authentication fields,
allowing connectors to declare explicit PHP constant and environment
variable names for API key lookup. AI providers auto-generate these using
the {CONSTANT_CASE_ID}_API_KEY convention.
Refactor _wp_connectors_get_api_key_source() to accept explicit
env_var_name and constant_name parameters instead of deriving them from
the provider ID. Environment variable and constant checks are skipped when
not provided.
Generalize REST dispatch, settings registration, and script module data to
work with all connector types, not just ai_provider. Settings
registration skips already-registered settings. Non-AI connectors determine
isConnected based on key source.
Replace isInstalled with pluginFile in script module data output to fix
plugin entity ID resolution on the frontend.
Update PHPDoc to reflect current behavior — widen type from literal
'ai_provider' to non-empty-string, document new authentication fields,
and use Anthropic examples throughout.

Props gziolo, jorgefilipecosta.
Fixes #64957.

Location:
branches/7.0
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • branches/7.0

  • branches/7.0/src/wp-includes/connectors.php

    r62067 r62194  
    4444 *     @type string $description    The connector's description.
    4545 *     @type string $logo_url       Optional. URL to the connector's logo image.
    46  *     @type string $type           The connector type. Currently, only 'ai_provider' is supported.
     46 *     @type string $type           The connector type, e.g. 'ai_provider'.
    4747 *     @type array  $authentication {
    4848 *         Authentication configuration. When method is 'api_key', includes
    49  *         credentials_url and setting_name. When 'none', only method is present.
     49 *         credentials_url, setting_name, and optionally constant_name and
     50 *         env_var_name. When 'none', only method is present.
    5051 *
    5152 *         @type string $method          The authentication method: 'api_key' or 'none'.
    5253 *         @type string $credentials_url Optional. URL where users can obtain API credentials.
    5354 *         @type string $setting_name    Optional. The setting name for the API key.
     55 *         @type string $constant_name   Optional. PHP constant name for the API key.
     56 *         @type string $env_var_name    Optional. Environment variable name for the API key.
    5457 *     }
    5558 *     @type array  $plugin         {
     
    6366 *     description: non-empty-string,
    6467 *     logo_url?: non-empty-string,
    65  *     type: 'ai_provider',
     68 *     type: non-empty-string,
    6669 *     authentication: array{
    6770 *         method: 'api_key'|'none',
    6871 *         credentials_url?: non-empty-string,
    69  *         setting_name?: non-empty-string
     72 *         setting_name?: non-empty-string,
     73 *         constant_name?: non-empty-string,
     74 *         env_var_name?: non-empty-string
    7075 *     },
    7176 *     plugin?: array{
     
    99104 *         @type string      $description    The connector's description.
    100105 *         @type string      $logo_url       Optional. URL to the connector's logo image.
    101  *         @type string      $type           The connector type. Currently, only 'ai_provider' is supported.
     106 *         @type string      $type           The connector type, e.g. 'ai_provider'.
    102107 *         @type array       $authentication {
    103108 *             Authentication configuration. When method is 'api_key', includes
    104  *             credentials_url and setting_name. When 'none', only method is present.
     109 *             credentials_url, setting_name, and optionally constant_name and
     110 *             env_var_name. When 'none', only method is present.
    105111 *
    106112 *             @type string $method          The authentication method: 'api_key' or 'none'.
    107113 *             @type string $credentials_url Optional. URL where users can obtain API credentials.
    108114 *             @type string $setting_name    Optional. The setting name for the API key.
     115 *             @type string $constant_name   Optional. PHP constant name for the API key.
     116 *             @type string $env_var_name    Optional. Environment variable name for the API key.
    109117 *         }
    110118 *         @type array       $plugin         {
     
    119127 *     description: non-empty-string,
    120128 *     logo_url?: non-empty-string,
    121  *     type: 'ai_provider',
     129 *     type: non-empty-string,
    122130 *     authentication: array{
    123131 *         method: 'api_key'|'none',
    124132 *         credentials_url?: non-empty-string,
    125  *         setting_name?: non-empty-string
     133 *         setting_name?: non-empty-string,
     134 *         constant_name?: non-empty-string,
     135 *         env_var_name?: non-empty-string
    126136 *     },
    127137 *     plugin?: array{
     
    217227         *
    218228         *     add_action( 'wp_connectors_init', function ( WP_Connector_Registry $registry ) {
    219          *         if ( $registry->is_registered( 'openai' ) ) {
    220          *             $connector = $registry->unregister( 'openai' );
    221          *             $connector['description'] = __( 'Custom description for OpenAI.', 'my-plugin' );
    222          *             $registry->register( 'openai', $connector );
     229         *         if ( $registry->is_registered( 'anthropic' ) ) {
     230         *             $connector = $registry->unregister( 'anthropic' );
     231         *             $connector['description'] = __( 'Custom description for Anthropic.', 'my-plugin' );
     232         *             $registry->register( 'anthropic', $connector );
    223233         *         }
    224234         *     } );
     
    336346        // Register all default connectors directly on the registry.
    337347        foreach ( $defaults as $id => $args ) {
     348                if ( 'api_key' === $args['authentication']['method'] ) {
     349                        $sanitized_id = str_replace( '-', '_', $id );
     350
     351                        if ( ! isset( $args['authentication']['setting_name'] ) ) {
     352                                $args['authentication']['setting_name'] = "connectors_ai_{$sanitized_id}_api_key";
     353                        }
     354
     355                        // All AI providers use the {CONSTANT_CASE_ID}_API_KEY naming convention.
     356                        if ( ! isset( $args['authentication']['constant_name'] ) || ! isset( $args['authentication']['env_var_name'] ) ) {
     357                                $constant_case_key = strtoupper( preg_replace( '/([a-z])([A-Z])/', '$1_$2', $sanitized_id ) ) . '_API_KEY';
     358
     359                                if ( ! isset( $args['authentication']['constant_name'] ) ) {
     360                                        $args['authentication']['constant_name'] = $constant_case_key;
     361                                }
     362
     363                                if ( ! isset( $args['authentication']['env_var_name'] ) ) {
     364                                        $args['authentication']['env_var_name'] = $constant_case_key;
     365                                }
     366                        }
     367                }
    338368                $registry->register( $id, $args );
    339369        }
     
    358388
    359389/**
    360  * Determines the source of an API key for a given provider.
     390 * Determines the source of an API key for a given connector.
    361391 *
    362392 * Checks in order: environment variable, PHP constant, database.
    363  * Uses the same naming convention as the WP AI Client ProviderRegistry.
    364  *
    365  * @since 7.0.0
    366  * @access private
    367  *
    368  * @param string $provider_id  The provider ID (e.g., 'openai', 'anthropic', 'google').
    369  * @param string $setting_name The option name for the API key (e.g., 'connectors_ai_openai_api_key').
     393 * Environment variable and constant are only checked when their
     394 * respective names are provided.
     395 *
     396 * @since 7.0.0
     397 * @access private
     398 *
     399 * @param string $setting_name  The option name for the API key (e.g., 'connectors_spam_filtering_akismet_api_key').
     400 * @param string $env_var_name  Optional. Environment variable name to check (e.g., 'AKISMET_API_KEY').
     401 * @param string $constant_name Optional. PHP constant name to check (e.g., 'AKISMET_API_KEY').
    370402 * @return string The key source: 'env', 'constant', 'database', or 'none'.
    371403 */
    372 function _wp_connectors_get_api_key_source( string $provider_id, string $setting_name ): string {
    373         // Convert provider ID to CONSTANT_CASE for env var name.
    374         // e.g., 'openai' -> 'OPENAI', 'anthropic' -> 'ANTHROPIC'.
    375         $constant_case_id = strtoupper(
    376                 preg_replace( '/([a-z])([A-Z])/', '$1_$2', str_replace( '-', '_', $provider_id ) )
    377         );
    378         $env_var_name     = "{$constant_case_id}_API_KEY";
    379 
     404function _wp_connectors_get_api_key_source( string $setting_name, string $env_var_name = '', string $constant_name = '' ): string {
    380405        // Check environment variable first.
    381         $env_value = getenv( $env_var_name );
    382         if ( false !== $env_value && '' !== $env_value ) {
    383                 return 'env';
     406        if ( '' !== $env_var_name ) {
     407                $env_value = getenv( $env_var_name );
     408                if ( false !== $env_value && '' !== $env_value ) {
     409                        return 'env';
     410                }
    384411        }
    385412
    386413        // Check PHP constant.
    387         if ( defined( $env_var_name ) ) {
    388                 $const_value = constant( $env_var_name );
     414        if ( '' !== $constant_name && defined( $constant_name ) ) {
     415                $const_value = constant( $constant_name );
    389416                if ( is_string( $const_value ) && '' !== $const_value ) {
    390417                        return 'constant';
     
    471498        foreach ( wp_get_connectors() as $connector_id => $connector_data ) {
    472499                $auth = $connector_data['authentication'];
    473                 if ( 'ai_provider' !== $connector_data['type'] || 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) {
     500                if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) {
    474501                        continue;
    475502                }
     
    482509                $value = $data[ $setting_name ];
    483510
    484                 // On update, validate the key before masking.
    485                 if ( $is_update && is_string( $value ) && '' !== $value ) {
     511                // On update, validate AI provider keys before masking.
     512                // Non-AI connectors accept keys as-is; the service plugin handles its own validation.
     513                if ( $is_update && is_string( $value ) && '' !== $value && 'ai_provider' === $connector_data['type'] ) {
    486514                        if ( true !== _wp_connectors_is_ai_api_key_valid( $value, $connector_id ) ) {
    487515                                update_option( $setting_name, '' );
     
    509537 */
    510538function _wp_register_default_connector_settings(): void {
    511         $ai_registry = AiClient::defaultRegistry();
     539        $ai_registry         = AiClient::defaultRegistry();
     540        $registered_settings = get_registered_settings();
    512541
    513542        foreach ( wp_get_connectors() as $connector_id => $connector_data ) {
    514543                $auth = $connector_data['authentication'];
    515                 if ( 'ai_provider' !== $connector_data['type'] || 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) {
     544                if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) {
    516545                        continue;
    517546                }
    518547
    519                 // Skip registering the setting if the provider is not in the registry.
    520                 if ( ! $ai_registry->hasProvider( $connector_id ) ) {
     548                // Skip if the setting is already registered (e.g. by an owning plugin).
     549                if ( isset( $registered_settings[ $auth['setting_name'] ] ) ) {
     550                        continue;
     551                }
     552
     553                // For AI providers, skip if the provider is not in the AI Client registry.
     554                if ( 'ai_provider' === $connector_data['type'] && ! $ai_registry->hasProvider( $connector_id ) ) {
    521555                        continue;
    522556                }
     
    528562                                'type'              => 'string',
    529563                                'label'             => sprintf(
    530                                         /* translators: %s: AI provider name. */
     564                                        /* translators: %s: Connector name. */
    531565                                        __( '%s API Key' ),
    532566                                        $connector_data['name']
    533567                                ),
    534568                                'description'       => sprintf(
    535                                         /* translators: %s: AI provider name. */
    536                                         __( 'API key for the %s AI provider.' ),
     569                                        /* translators: %s: Connector name. */
     570                                        __( 'API key for the %s connector.' ),
    537571                                        $connector_data['name']
    538572                                ),
     
    570604
    571605                        // Skip if the key is already provided via env var or constant.
    572                         $key_source = _wp_connectors_get_api_key_source( $connector_id, $auth['setting_name'] );
     606                        $key_source = _wp_connectors_get_api_key_source( $auth['setting_name'], $auth['env_var_name'] ?? '', $auth['constant_name'] ?? '' );
    573607                        if ( 'env' === $key_source || 'constant' === $key_source ) {
    574608                                continue;
     
    621655                        $auth_out['settingName']    = $auth['setting_name'] ?? '';
    622656                        $auth_out['credentialsUrl'] = $auth['credentials_url'] ?? null;
    623                         $auth_out['keySource']      = _wp_connectors_get_api_key_source( $connector_id, $auth['setting_name'] ?? '' );
    624                         try {
    625                                 $auth_out['isConnected'] = $registry->hasProvider( $connector_id ) && $registry->isProviderConfigured( $connector_id );
    626                         } catch ( Exception $e ) {
    627                                 $auth_out['isConnected'] = false;
     657                        $key_source                 = _wp_connectors_get_api_key_source( $auth['setting_name'] ?? '', $auth['env_var_name'] ?? '', $auth['constant_name'] ?? '' );
     658                        $auth_out['keySource']      = $key_source;
     659
     660                        if ( 'ai_provider' === $connector_data['type'] ) {
     661                                try {
     662                                        $auth_out['isConnected'] = $registry->hasProvider( $connector_id ) && $registry->isProviderConfigured( $connector_id );
     663                                } catch ( Exception $e ) {
     664                                        $auth_out['isConnected'] = false;
     665                                }
     666                        } else {
     667                                $auth_out['isConnected'] = 'none' !== $key_source;
    628668                        }
    629669                }
     
    646686                        $connector_out['plugin'] = array(
    647687                                'slug'        => $plugin_slug,
    648                                 'isInstalled' => $is_installed,
     688                                'pluginFile'  => $is_installed
     689                                        ? ( str_ends_with( $plugin_file, '.php' ) ? substr( $plugin_file, 0, -4 ) : $plugin_file )
     690                                        : null,
    649691                                'isActivated' => $is_activated,
    650692                        );
Note: See TracChangeset for help on using the changeset viewer.