[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> connectors.php (source)

   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', includes
  49   *         credentials_url, setting_name, and optionally constant_name and
  50   *         env_var_name. When 'none', only method is present.
  51   *
  52   *         @type string $method          The authentication method: 'api_key' or 'none'.
  53   *         @type string $credentials_url Optional. URL where users can obtain API credentials.
  54   *         @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.
  57   *     }
  58   *     @type array  $plugin         {
  59   *         Optional. Plugin data for install/activate UI.
  60   *
  61   *         @type string   $file      The plugin's main file path relative to the plugins
  62   *                                   directory (e.g. 'my-plugin/my-plugin.php' or 'hello.php').
  63   *         @type callable $is_active Callback to determine whether the plugin is active. Receives no arguments and must return bool.
  64   *                                   Defaults to `__return_true`.
  65   *     }
  66   * }
  67   * @phpstan-return ?array{
  68   *     name: non-empty-string,
  69   *     description: string,
  70   *     logo_url?: non-empty-string,
  71   *     type: non-empty-string,
  72   *     authentication: array{
  73   *         method: 'api_key'|'none',
  74   *         credentials_url?: non-empty-string,
  75   *         setting_name?: non-empty-string,
  76   *         constant_name?: non-empty-string,
  77   *         env_var_name?: non-empty-string
  78   *     },
  79   *     plugin: array{
  80   *         file?: non-empty-string,
  81   *         is_active: callable(): bool,
  82   *     }
  83   * }
  84   */
  85  function wp_get_connector( string $id ): ?array {
  86      $registry = WP_Connector_Registry::get_instance();
  87      if ( null === $registry ) {
  88          return null;
  89      }
  90  
  91      return $registry->get_registered( $id );
  92  }
  93  
  94  /**
  95   * Retrieves all registered connectors.
  96   *
  97   * @since 7.0.0
  98   *
  99   * @see WP_Connector_Registry::get_all_registered()
 100   *
 101   * @return array {
 102   *     Connector settings keyed by connector ID.
 103   *
 104   *     @type array ...$0 {
 105   *         Data for a single connector.
 106   *
 107   *         @type string      $name           The connector's display name.
 108   *         @type string      $description    The connector's description.
 109   *         @type string      $logo_url       Optional. URL to the connector's logo image.
 110   *         @type string      $type           The connector type, e.g. 'ai_provider'.
 111   *         @type array       $authentication {
 112   *             Authentication configuration. When method is 'api_key', includes
 113   *             credentials_url, setting_name, and optionally constant_name and
 114   *             env_var_name. When 'none', only method is present.
 115   *
 116   *             @type string $method          The authentication method: 'api_key' or 'none'.
 117   *             @type string $credentials_url Optional. URL where users can obtain API credentials.
 118   *             @type string $setting_name    Optional. The setting name for the API key.
 119   *             @type string $constant_name   Optional. PHP constant name for the API key.
 120   *             @type string $env_var_name    Optional. Environment variable name for the API key.
 121   *         }
 122   *         @type array       $plugin         {
 123   *             Optional. Plugin data for install/activate UI.
 124   *
 125   *             @type string   $file      The plugin's main file path relative to the plugins
 126   *                                       directory (e.g. 'my-plugin/my-plugin.php' or 'hello.php').
 127   *             @type callable $is_active Callback to determine whether the plugin is active. Receives no arguments and must return bool.
 128   *                                       Defaults to `__return_true`.
 129   *         }
 130   *     }
 131   * }
 132   * @phpstan-return array<string, array{
 133   *     name: non-empty-string,
 134   *     description: string,
 135   *     logo_url?: non-empty-string,
 136   *     type: non-empty-string,
 137   *     authentication: array{
 138   *         method: 'api_key'|'none',
 139   *         credentials_url?: non-empty-string,
 140   *         setting_name?: non-empty-string,
 141   *         constant_name?: non-empty-string,
 142   *         env_var_name?: non-empty-string
 143   *     },
 144   *     plugin: array{
 145   *         file?: non-empty-string,
 146   *         is_active: callable(): bool,
 147   *     }
 148   * }>
 149   */
 150  function wp_get_connectors(): array {
 151      $registry = WP_Connector_Registry::get_instance();
 152      if ( null === $registry ) {
 153          return array();
 154      }
 155  
 156      return $registry->get_all_registered();
 157  }
 158  
 159  /**
 160   * Resolves an AI provider logo file path to a URL.
 161   *
 162   * Converts an absolute file path to a plugin URL. The path must reside within
 163   * the plugins or must-use plugins directory.
 164   *
 165   * @since 7.0.0
 166   * @access private
 167   *
 168   * @param string $path Absolute path to the logo file.
 169   * @return non-empty-string|null The URL to the logo file, or null if the path is invalid.
 170   */
 171  function _wp_connectors_resolve_ai_provider_logo_url( string $path ): ?string {
 172      if ( ! $path ) {
 173          return null;
 174      }
 175  
 176      $path = wp_normalize_path( $path );
 177  
 178      if ( ! file_exists( $path ) ) {
 179          return null;
 180      }
 181  
 182      $mu_plugin_dir = wp_normalize_path( WPMU_PLUGIN_DIR );
 183      if ( str_starts_with( $path, $mu_plugin_dir . '/' ) ) {
 184          $logo_url = plugins_url( substr( $path, strlen( $mu_plugin_dir ) ), WPMU_PLUGIN_DIR . '/.' );
 185          return $logo_url ? $logo_url : null;
 186      }
 187  
 188      $plugin_dir = wp_normalize_path( WP_PLUGIN_DIR );
 189      if ( str_starts_with( $path, $plugin_dir . '/' ) ) {
 190          $logo_url = plugins_url( substr( $path, strlen( $plugin_dir ) ) );
 191          return $logo_url ? $logo_url : null;
 192      }
 193  
 194      _doing_it_wrong(
 195          __FUNCTION__,
 196          __( 'Provider logo path must be located within the plugins or must-use plugins directory.' ),
 197          '7.0.0'
 198      );
 199  
 200      return null;
 201  }
 202  
 203  /**
 204   * Initializes the connector registry with default connectors and fires the registration action.
 205   *
 206   * Creates the registry instance, registers built-in connectors (which cannot be unhooked),
 207   * and then fires the `wp_connectors_init` action for plugins to register their own connectors.
 208   *
 209   * @since 7.0.0
 210   * @access private
 211   */
 212  function _wp_connectors_init(): void {
 213      $registry = new WP_Connector_Registry();
 214      WP_Connector_Registry::set_instance( $registry );
 215  
 216      // Only register default AI providers if AI support is enabled.
 217      if ( wp_supports_ai() ) {
 218          _wp_connectors_register_default_ai_providers( $registry );
 219      }
 220  
 221      // Non-AI default connectors.
 222      $registry->register(
 223          'akismet',
 224          array(
 225              'name'           => __( 'Akismet Anti-spam' ),
 226              'description'    => __( 'Protect your site from spam.' ),
 227              'type'           => 'spam_filtering',
 228              'plugin'         => array(
 229                  'file'      => 'akismet/akismet.php',
 230                  'is_active' => static function () {
 231                      return defined( 'AKISMET_VERSION' );
 232                  },
 233              ),
 234              'authentication' => array(
 235                  'method'          => 'api_key',
 236                  'credentials_url' => 'https://akismet.com/get/',
 237                  'setting_name'    => 'wordpress_api_key',
 238                  'constant_name'   => 'WPCOM_API_KEY',
 239              ),
 240          )
 241      );
 242  
 243      /**
 244       * Fires when the connector registry is ready for plugins to register connectors.
 245       *
 246       * Built-in connectors and any AI providers auto-discovered from the WP AI Client
 247       * registry have already been registered at this point and cannot be unhooked.
 248       *
 249       * AI provider plugins that register with the WP AI Client do not need to use
 250       * this action — their connectors are created automatically. This action is
 251       * primarily for registering non-AI-provider connectors or overriding metadata
 252       * on existing connectors.
 253       *
 254       * Use `$registry->register()` within this action to add new connectors.
 255       * To override an existing connector, unregister it first, then re-register
 256       * with updated data.
 257       *
 258       * Example — overriding metadata on an auto-discovered connector:
 259       *
 260       *     add_action( 'wp_connectors_init', function ( WP_Connector_Registry $registry ) {
 261       *         if ( $registry->is_registered( 'anthropic' ) ) {
 262       *             $connector = $registry->unregister( 'anthropic' );
 263       *             $connector['description'] = __( 'Custom description for Anthropic.', 'my-plugin' );
 264       *             $registry->register( 'anthropic', $connector );
 265       *         }
 266       *     } );
 267       *
 268       * @since 7.0.0
 269       *
 270       * @param WP_Connector_Registry $registry Connector registry instance.
 271       */
 272      do_action( 'wp_connectors_init', $registry );
 273  }
 274  
 275  /**
 276   * Registers connectors for the built-in AI providers.
 277   *
 278   * @since 7.0.0
 279   * @access private
 280   *
 281   * @param WP_Connector_Registry $registry The connector registry instance.
 282   */
 283  function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $registry ): void {
 284      // Built-in connectors.
 285      $defaults = array(
 286          'anthropic' => array(
 287              'name'           => 'Anthropic',
 288              'description'    => __( 'Text generation with Claude.' ),
 289              'type'           => 'ai_provider',
 290              'plugin'         => array(
 291                  'file' => 'ai-provider-for-anthropic/plugin.php',
 292              ),
 293              'authentication' => array(
 294                  'method'          => 'api_key',
 295                  'credentials_url' => 'https://platform.claude.com/settings/keys',
 296              ),
 297          ),
 298          'google'    => array(
 299              'name'           => 'Google',
 300              'description'    => __( 'Text and image generation with Gemini and Imagen.' ),
 301              'type'           => 'ai_provider',
 302              'plugin'         => array(
 303                  'file' => 'ai-provider-for-google/plugin.php',
 304              ),
 305              'authentication' => array(
 306                  'method'          => 'api_key',
 307                  'credentials_url' => 'https://aistudio.google.com/api-keys',
 308              ),
 309          ),
 310          'openai'    => array(
 311              'name'           => 'OpenAI',
 312              'description'    => __( 'Text and image generation with GPT and Dall-E.' ),
 313              'type'           => 'ai_provider',
 314              'plugin'         => array(
 315                  'file' => 'ai-provider-for-openai/plugin.php',
 316              ),
 317              'authentication' => array(
 318                  'method'          => 'api_key',
 319                  'credentials_url' => 'https://platform.openai.com/api-keys',
 320              ),
 321          ),
 322      );
 323  
 324      // Merge AI Client registry data on top of defaults.
 325      // Registry values (from provider plugins) take precedence over hardcoded fallbacks.
 326      $ai_registry = AiClient::defaultRegistry();
 327  
 328      foreach ( array_filter( $ai_registry->getRegisteredProviderIds() ) as $connector_id ) {
 329          $provider_class_name = $ai_registry->getProviderClassName( $connector_id );
 330          $provider_metadata   = $provider_class_name::metadata();
 331  
 332          $auth_method = $provider_metadata->getAuthenticationMethod();
 333          $is_api_key  = null !== $auth_method && $auth_method->isApiKey();
 334  
 335          if ( $is_api_key ) {
 336              $credentials_url = $provider_metadata->getCredentialsUrl();
 337              $authentication  = array(
 338                  'method' => 'api_key',
 339              );
 340              if ( $credentials_url ) {
 341                  $authentication['credentials_url'] = $credentials_url;
 342              }
 343          } else {
 344              $authentication = array( 'method' => 'none' );
 345          }
 346  
 347          $name        = $provider_metadata->getName();
 348          $description = $provider_metadata->getDescription();
 349          $logo_url    = $provider_metadata->getLogoPath()
 350              ? _wp_connectors_resolve_ai_provider_logo_url( $provider_metadata->getLogoPath() )
 351              : null;
 352  
 353          if ( isset( $defaults[ $connector_id ] ) ) {
 354              // Override fields with non-empty registry values.
 355              if ( $name ) {
 356                  $defaults[ $connector_id ]['name'] = $name;
 357              }
 358              if ( $description ) {
 359                  $defaults[ $connector_id ]['description'] = $description;
 360              }
 361              if ( $logo_url ) {
 362                  $defaults[ $connector_id ]['logo_url'] = $logo_url;
 363              }
 364              // Always update auth method; keep existing credentials_url as fallback.
 365              $defaults[ $connector_id ]['authentication']['method'] = $authentication['method'];
 366              if ( ! empty( $authentication['credentials_url'] ) ) {
 367                  $defaults[ $connector_id ]['authentication']['credentials_url'] = $authentication['credentials_url'];
 368              }
 369          } else {
 370              $defaults[ $connector_id ] = array(
 371                  'name'           => $name ? $name : ucwords( $connector_id ),
 372                  'description'    => $description ? $description : '',
 373                  'type'           => 'ai_provider',
 374                  'authentication' => $authentication,
 375              );
 376              if ( $logo_url ) {
 377                  $defaults[ $connector_id ]['logo_url'] = $logo_url;
 378              }
 379          }
 380      }
 381  
 382      // Register all default connectors directly on the registry.
 383      foreach ( $defaults as $id => $args ) {
 384          if ( 'api_key' === $args['authentication']['method'] ) {
 385              $sanitized_id = str_replace( '-', '_', $id );
 386  
 387              $args['authentication']['setting_name'] = "connectors_ai_{$sanitized_id}_api_key";
 388  
 389              // All AI providers use the {CONSTANT_CASE_ID}_API_KEY naming convention.
 390              $constant_case_key = strtoupper( (string) preg_replace( '/([a-z])([A-Z])/', '$1_$2', $sanitized_id ) ) . '_API_KEY';
 391  
 392              $args['authentication']['constant_name'] = $constant_case_key;
 393              $args['authentication']['env_var_name']  = $constant_case_key;
 394          }
 395  
 396          $args['plugin']['is_active'] = static function () use ( $ai_registry, $id ): bool {
 397              try {
 398                  return $ai_registry->hasProvider( $id );
 399              } catch ( Exception $e ) {
 400                  return false;
 401              }
 402          };
 403  
 404          $registry->register( $id, $args );
 405      }
 406  }
 407  
 408  /**
 409   * Masks an API key, showing only the last 4 characters.
 410   *
 411   * @since 7.0.0
 412   * @access private
 413   *
 414   * @param string $key The API key to mask.
 415   * @return string The masked key, e.g. "************fj39".
 416   */
 417  function _wp_connectors_mask_api_key( string $key ): string {
 418      if ( strlen( $key ) <= 4 ) {
 419          return $key;
 420      }
 421  
 422      return str_repeat( "\u{2022}", min( strlen( $key ) - 4, 16 ) ) . substr( $key, -4 );
 423  }
 424  
 425  /**
 426   * Determines the source of an API key for a given connector.
 427   *
 428   * Checks in order: environment variable, PHP constant, database.
 429   * Environment variable and constant are only checked when their
 430   * respective names are provided.
 431   *
 432   * @since 7.0.0
 433   * @access private
 434   *
 435   * @param string $setting_name  The option name for the API key (e.g., 'connectors_spam_filtering_my_plugin_api_key').
 436   * @param string $env_var_name  Optional. Environment variable name to check (e.g., 'MY_PLUGIN_API_KEY').
 437   * @param string $constant_name Optional. PHP constant name to check (e.g., 'MY_PLUGIN_API_KEY').
 438   * @return string The key source: 'env', 'constant', 'database', or 'none'.
 439   */
 440  function _wp_connectors_get_api_key_source( string $setting_name, string $env_var_name = '', string $constant_name = '' ): string {
 441      // Check environment variable first.
 442      if ( '' !== $env_var_name ) {
 443          $env_value = getenv( $env_var_name );
 444          if ( false !== $env_value && '' !== $env_value ) {
 445              return 'env';
 446          }
 447      }
 448  
 449      // Check PHP constant.
 450      if ( '' !== $constant_name && defined( $constant_name ) ) {
 451          $const_value = constant( $constant_name );
 452          if ( is_string( $const_value ) && '' !== $const_value ) {
 453              return 'constant';
 454          }
 455      }
 456  
 457      // Check database.
 458      $db_value = get_option( $setting_name, '' );
 459      if ( '' !== $db_value ) {
 460          return 'database';
 461      }
 462  
 463      return 'none';
 464  }
 465  
 466  /**
 467   * Checks whether an API key is valid for a given provider.
 468   *
 469   * @since 7.0.0
 470   * @access private
 471   *
 472   * @param string $key         The API key to check.
 473   * @param string $provider_id The WP AI client provider ID.
 474   * @return bool|null True if valid, false if invalid, null if unable to determine.
 475   */
 476  function _wp_connectors_is_ai_api_key_valid( string $key, string $provider_id ): ?bool {
 477      try {
 478          $registry = AiClient::defaultRegistry();
 479  
 480          if ( ! $registry->hasProvider( $provider_id ) ) {
 481              _doing_it_wrong(
 482                  __FUNCTION__,
 483                  sprintf(
 484                      /* translators: %s: AI provider ID. */
 485                      __( 'The provider "%s" is not registered in the AI client registry.' ),
 486                      $provider_id
 487                  ),
 488                  '7.0.0'
 489              );
 490              return null;
 491          }
 492  
 493          $registry->setProviderRequestAuthentication(
 494              $provider_id,
 495              new ApiKeyRequestAuthentication( $key )
 496          );
 497  
 498          return $registry->isProviderConfigured( $provider_id );
 499      } catch ( Exception $e ) {
 500          wp_trigger_error( __FUNCTION__, $e->getMessage() );
 501          return null;
 502      }
 503  }
 504  
 505  /**
 506   * Masks and validates connector API keys in REST responses.
 507   *
 508   * On every `/wp/v2/settings` response, masks connector API key values so raw
 509   * keys are never exposed via the REST API.
 510   *
 511   * On POST or PUT requests, validates each updated key against the provider
 512   * before masking. If validation fails, the key is reverted to an empty string.
 513   *
 514   * @since 7.0.0
 515   * @access private
 516   *
 517   * @param WP_REST_Response $response The response object.
 518   * @param WP_REST_Server   $server   The server instance.
 519   * @param WP_REST_Request  $request  The request object.
 520   * @return WP_REST_Response The modified response with masked/validated keys.
 521   */
 522  function _wp_connectors_rest_settings_dispatch( WP_REST_Response $response, WP_REST_Server $server, WP_REST_Request $request ): WP_REST_Response {
 523      if ( '/wp/v2/settings' !== $request->get_route() ) {
 524          return $response;
 525      }
 526  
 527      $data = $response->get_data();
 528      if ( ! is_array( $data ) ) {
 529          return $response;
 530      }
 531  
 532      $is_update = 'POST' === $request->get_method() || 'PUT' === $request->get_method();
 533  
 534      foreach ( wp_get_connectors() as $connector_id => $connector_data ) {
 535          $auth = $connector_data['authentication'];
 536          if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) {
 537              continue;
 538          }
 539  
 540          $setting_name = $auth['setting_name'];
 541          if ( ! array_key_exists( $setting_name, $data ) ) {
 542              continue;
 543          }
 544  
 545          $value = $data[ $setting_name ];
 546  
 547          // On update, validate AI provider keys before masking.
 548          // Non-AI connectors accept keys as-is; the service plugin handles its own validation.
 549          if ( $is_update && is_string( $value ) && '' !== $value && 'ai_provider' === $connector_data['type'] ) {
 550              if ( true !== _wp_connectors_is_ai_api_key_valid( $value, $connector_id ) ) {
 551                  update_option( $setting_name, '' );
 552                  $data[ $setting_name ] = '';
 553                  continue;
 554              }
 555          }
 556  
 557          // Mask the key in the response.
 558          if ( is_string( $value ) && '' !== $value ) {
 559              $data[ $setting_name ] = _wp_connectors_mask_api_key( $value );
 560          }
 561      }
 562  
 563      $response->set_data( $data );
 564      return $response;
 565  }
 566  add_filter( 'rest_post_dispatch', '_wp_connectors_rest_settings_dispatch', 10, 3 );
 567  
 568  /**
 569   * Registers default connector settings.
 570   *
 571   * @since 7.0.0
 572   * @access private
 573   */
 574  function _wp_register_default_connector_settings(): void {
 575      $registered_settings = get_registered_settings();
 576  
 577      foreach ( wp_get_connectors() as $connector_data ) {
 578          $auth = $connector_data['authentication'];
 579          if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) {
 580              continue;
 581          }
 582  
 583          // Skip if the setting is already registered (e.g. by an owning plugin).
 584          if ( isset( $registered_settings[ $auth['setting_name'] ] ) ) {
 585              continue;
 586          }
 587  
 588          if ( ! isset( $connector_data['plugin']['is_active'] ) || ! is_callable( $connector_data['plugin']['is_active'] ) ) {
 589              continue;
 590          }
 591  
 592          if ( ! call_user_func( $connector_data['plugin']['is_active'] ) ) {
 593              continue;
 594          }
 595  
 596          register_setting(
 597              'connectors',
 598              $auth['setting_name'],
 599              array(
 600                  'type'              => 'string',
 601                  'label'             => sprintf(
 602                      /* translators: %s: Connector name. */
 603                      __( '%s API Key' ),
 604                      $connector_data['name']
 605                  ),
 606                  'description'       => sprintf(
 607                      /* translators: %s: Connector name. */
 608                      __( 'API key for the %s connector.' ),
 609                      $connector_data['name']
 610                  ),
 611                  'default'           => '',
 612                  'show_in_rest'      => true,
 613                  'sanitize_callback' => 'sanitize_text_field',
 614              )
 615          );
 616      }
 617  }
 618  add_action( 'init', '_wp_register_default_connector_settings', 20 );
 619  
 620  /**
 621   * Passes stored connector API keys to the WP AI client.
 622   *
 623   * @since 7.0.0
 624   * @access private
 625   */
 626  function _wp_connectors_pass_default_keys_to_ai_client(): void {
 627      try {
 628          $ai_registry = AiClient::defaultRegistry();
 629          foreach ( wp_get_connectors() as $connector_id => $connector_data ) {
 630              if ( 'ai_provider' !== $connector_data['type'] ) {
 631                  continue;
 632              }
 633  
 634              $auth = $connector_data['authentication'];
 635              if ( 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) {
 636                  continue;
 637              }
 638  
 639              if ( ! $ai_registry->hasProvider( $connector_id ) ) {
 640                  continue;
 641              }
 642  
 643              // Skip if the key is already provided via env var or constant.
 644              $key_source = _wp_connectors_get_api_key_source( $auth['setting_name'], $auth['env_var_name'] ?? '', $auth['constant_name'] ?? '' );
 645              if ( 'env' === $key_source || 'constant' === $key_source ) {
 646                  continue;
 647              }
 648  
 649              $api_key = get_option( $auth['setting_name'], '' );
 650              if ( ! is_string( $api_key ) || '' === $api_key ) {
 651                  continue;
 652              }
 653  
 654              $ai_registry->setProviderRequestAuthentication(
 655                  $connector_id,
 656                  new ApiKeyRequestAuthentication( $api_key )
 657              );
 658          }
 659      } catch ( Exception $e ) {
 660          wp_trigger_error( __FUNCTION__, $e->getMessage() );
 661      }
 662  }
 663  add_action( 'init', '_wp_connectors_pass_default_keys_to_ai_client', 20 );
 664  
 665  /**
 666   * Exposes connector settings to the connectors-wp-admin script module.
 667   *
 668   * @since 7.0.0
 669   * @access private
 670   *
 671   * @param array<string, mixed> $data Existing script module data.
 672   * @return array<string, mixed> Script module data with connectors added.
 673   */
 674  function _wp_connectors_get_connector_script_module_data( array $data ): array {
 675      $registry = AiClient::defaultRegistry();
 676  
 677      if ( ! function_exists( 'validate_plugin' ) ) {
 678          require_once  ABSPATH . 'wp-admin/includes/plugin.php';
 679      }
 680  
 681      $connectors = array();
 682      foreach ( wp_get_connectors() as $connector_id => $connector_data ) {
 683          $auth     = $connector_data['authentication'];
 684          $auth_out = array( 'method' => $auth['method'] );
 685  
 686          if ( 'api_key' === $auth['method'] ) {
 687              $auth_out['settingName']    = $auth['setting_name'] ?? '';
 688              $auth_out['credentialsUrl'] = $auth['credentials_url'] ?? null;
 689              $key_source                 = _wp_connectors_get_api_key_source( $auth['setting_name'] ?? '', $auth['env_var_name'] ?? '', $auth['constant_name'] ?? '' );
 690              $auth_out['keySource']      = $key_source;
 691  
 692              if ( 'ai_provider' === $connector_data['type'] ) {
 693                  try {
 694                      $auth_out['isConnected'] = $registry->hasProvider( $connector_id ) && $registry->isProviderConfigured( $connector_id );
 695                  } catch ( Exception $e ) {
 696                      $auth_out['isConnected'] = false;
 697                  }
 698              } else {
 699                  $auth_out['isConnected'] = 'none' !== $key_source;
 700              }
 701          }
 702  
 703          $connector_out = array(
 704              'name'           => $connector_data['name'],
 705              'description'    => $connector_data['description'],
 706              'logoUrl'        => ! empty( $connector_data['logo_url'] ) ? $connector_data['logo_url'] : null,
 707              'type'           => $connector_data['type'],
 708              'authentication' => $auth_out,
 709          );
 710  
 711          if ( ! empty( $connector_data['plugin']['file'] ) ) {
 712              $file         = $connector_data['plugin']['file'];
 713              $is_activated = (bool) call_user_func( $connector_data['plugin']['is_active'] );
 714              $is_installed = $is_activated || 0 === validate_plugin( $file );
 715  
 716              $connector_out['plugin'] = array(
 717                  'file'        => $file,
 718                  'isInstalled' => $is_installed,
 719                  'isActivated' => $is_activated,
 720              );
 721          }
 722  
 723          $connectors[ $connector_id ] = $connector_out;
 724      }
 725      ksort( $connectors );
 726      $data['connectors']        = $connectors;
 727      $data['isFileModDisabled'] = ! wp_is_file_mod_allowed( 'install_plugins' );
 728      return $data;
 729  }
 730  add_filter( 'script_module_data_options-connectors-wp-admin', '_wp_connectors_get_connector_script_module_data' );


Generated : Sat Jun 13 09:38:55 2026 Cross-referenced by PHPXref