Skip to content
51 changes: 48 additions & 3 deletions src/wp-includes/connectors.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,43 @@ function _wp_connectors_get_connector_settings(): array {

ksort( $connectors );

// Add setting_name for connectors that use API key authentication.
/**
* Filters the registered connector settings.
*
* Allows plugins to add, modify, or remove connectors displayed
* on the Connectors screen. Runs after built-in and AI Client
* registry providers are collected, but before `setting_name` is
* generated for API-key connectors.
*
* @since 7.0.0
*
* @param array $connectors {
* Connector settings keyed by connector ID.
*
* @type array ...$0 {
* Data for a single connector.
*
* @type string $name The connector's display name.
* @type string $description The connector's description.
* @type string $type The connector type. Currently, only 'ai_provider' is supported.
* @type array $plugin Optional. Plugin data for install/activate UI.
* @type string $slug The WordPress.org plugin slug.
* }
* @type array $authentication {
* Authentication configuration. When method is 'api_key', includes
* credentials_url. When 'none', only method is present.
*
* @type string $method The authentication method: 'api_key' or 'none'.
* @type string|null $credentials_url Optional. URL where users can obtain API credentials.
* }
* }
* }
*/
$connectors = apply_filters( 'wp_connectors_settings', $connectors );

// Add setting_name for AI provider connectors that use API key authentication.
foreach ( $connectors as $connector_id => $connector ) {
if ( 'api_key' === $connector['authentication']['method'] ) {
if ( 'ai_provider' === $connector['type'] && 'api_key' === $connector['authentication']['method'] ) {
$connectors[ $connector_id ]['authentication']['setting_name'] = "connectors_ai_{$connector_id}_api_key";
}
}
Expand Down Expand Up @@ -282,12 +316,19 @@ function _wp_connectors_validate_keys_in_rest( WP_REST_Response $response, WP_RE
* @access private
*/
function _wp_register_default_connector_settings(): void {
$registry = AiClient::defaultRegistry();

foreach ( _wp_connectors_get_connector_settings() as $connector_id => $connector_data ) {
$auth = $connector_data['authentication'];
if ( 'ai_provider' !== $connector_data['type'] || 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) {
continue;
}

// Skip registering the setting if the provider is not in the registry.
if ( ! $registry->hasProvider( $connector_id ) ) {
continue;
}

$setting_name = $auth['setting_name'];
register_setting(
'connectors',
Expand Down Expand Up @@ -341,8 +382,12 @@ function _wp_connectors_pass_default_keys_to_ai_client(): void {
continue;
}

if ( ! $registry->hasProvider( $connector_id ) ) {
continue;
}

$api_key = _wp_connectors_get_real_api_key( $auth['setting_name'], '_wp_connectors_mask_api_key' );
if ( '' === $api_key || ! $registry->hasProvider( $connector_id ) ) {
if ( '' === $api_key ) {
continue;
}

Expand Down
138 changes: 138 additions & 0 deletions tests/phpunit/tests/connectors/wpConnectorsGetConnectorSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,142 @@ public function test_includes_registered_provider_from_registry() {
$this->assertNull( $mock['authentication']['credentials_url'] );
$this->assertSame( 'connectors_ai_mock_connectors_test_api_key', $mock['authentication']['setting_name'] );
}

/**
* @ticket 64791
*/
public function test_filter_can_add_new_connector() {
$callback = static function ( $connectors ) {
$connectors['my_email_service'] = array(
'name' => 'My Email Service',
'description' => 'Send transactional emails.',
'type' => 'email_service',
'authentication' => array( 'method' => 'none' ),
);
return $connectors;
};
add_filter( 'wp_connectors_settings', $callback );

$connectors = _wp_connectors_get_connector_settings();
remove_filter( 'wp_connectors_settings', $callback );

$this->assertArrayHasKey( 'my_email_service', $connectors );
$this->assertSame( 'My Email Service', $connectors['my_email_service']['name'] );
$this->assertSame( 'email_service', $connectors['my_email_service']['type'] );
$this->assertSame( 'none', $connectors['my_email_service']['authentication']['method'] );
}

/**
* @ticket 64791
*/
public function test_filter_can_modify_existing_connector() {
$callback = static function ( $connectors ) {
$connectors['google']['description'] = 'Custom description for Google.';
return $connectors;
};
add_filter( 'wp_connectors_settings', $callback );

$connectors = _wp_connectors_get_connector_settings();
remove_filter( 'wp_connectors_settings', $callback );

$this->assertSame( 'Custom description for Google.', $connectors['google']['description'] );
}

/**
* @ticket 64791
*/
public function test_filter_can_remove_connector() {
$callback = static function ( $connectors ) {
unset( $connectors['openai'] );
return $connectors;
};
add_filter( 'wp_connectors_settings', $callback );

$connectors = _wp_connectors_get_connector_settings();
remove_filter( 'wp_connectors_settings', $callback );

$this->assertArrayNotHasKey( 'openai', $connectors );
// Other connectors remain.
$this->assertArrayHasKey( 'google', $connectors );
$this->assertArrayHasKey( 'anthropic', $connectors );
}

/**
* @ticket 64791
*/
public function test_filter_added_api_key_connector_gets_setting_name() {
$callback = static function ( $connectors ) {
$connectors['custom_ai'] = array(
'name' => 'Custom AI',
'description' => 'A custom AI provider.',
'type' => 'ai_provider',
'authentication' => array(
'method' => 'api_key',
'credentials_url' => 'https://example.com/keys',
),
);
return $connectors;
};
add_filter( 'wp_connectors_settings', $callback );

$connectors = _wp_connectors_get_connector_settings();
remove_filter( 'wp_connectors_settings', $callback );

$this->assertArrayHasKey( 'custom_ai', $connectors );
$this->assertSame(
'connectors_ai_custom_ai_api_key',
$connectors['custom_ai']['authentication']['setting_name'],
'Connectors added via the filter with api_key auth should receive a setting_name automatically.'
);
}

/**
* @ticket 64791
*/
public function test_filter_added_non_ai_api_key_connector_does_not_get_setting_name() {
$callback = static function ( $connectors ) {
$connectors['my_crm'] = array(
'name' => 'My CRM',
'description' => 'CRM integration.',
'type' => 'crm',
'authentication' => array(
'method' => 'api_key',
'credentials_url' => 'https://example.com/crm-keys',
),
);
return $connectors;
};
add_filter( 'wp_connectors_settings', $callback );

$connectors = _wp_connectors_get_connector_settings();
remove_filter( 'wp_connectors_settings', $callback );

$this->assertArrayHasKey( 'my_crm', $connectors );
$this->assertArrayNotHasKey(
'setting_name',
$connectors['my_crm']['authentication'],
'Non-AI connectors should not receive an auto-generated setting_name.'
);
}

/**
* @ticket 64791
*/
public function test_filter_receives_all_default_connectors() {
$received = null;

$callback = static function ( $connectors ) use ( &$received ) {
$received = $connectors;
return $connectors;
};
add_filter( 'wp_connectors_settings', $callback );

_wp_connectors_get_connector_settings();
remove_filter( 'wp_connectors_settings', $callback );

$this->assertArrayHasKey( 'google', $received );
$this->assertArrayHasKey( 'openai', $received );
$this->assertArrayHasKey( 'anthropic', $received );
$this->assertArrayHasKey( 'mock_connectors_test', $received );
}
}
4 changes: 0 additions & 4 deletions tests/phpunit/tests/rest-api/rest-settings-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,6 @@ public function test_get_items() {
'default_comment_status',
'site_icon', // Registered in wp-includes/blocks/site-logo.php
'wp_enable_real_time_collaboration',
// Connectors API keys are registered in _wp_register_default_connector_settings() in wp-includes/connectors.php.
'connectors_ai_anthropic_api_key',
'connectors_ai_google_api_key',
'connectors_ai_openai_api_key',
);

if ( ! is_multisite() ) {
Expand Down
21 changes: 0 additions & 21 deletions tests/qunit/fixtures/wp-api-generated.js
Original file line number Diff line number Diff line change
Expand Up @@ -11066,24 +11066,6 @@ mockedApiResponse.Schema = {
"PATCH"
],
"args": {
"connectors_ai_anthropic_api_key": {
"title": "Anthropic API Key",
"description": "API key for the Anthropic AI provider.",
"type": "string",
"required": false
},
"connectors_ai_google_api_key": {
"title": "Google API Key",
"description": "API key for the Google AI provider.",
"type": "string",
"required": false
},
"connectors_ai_openai_api_key": {
"title": "OpenAI API Key",
"description": "API key for the OpenAI AI provider.",
"type": "string",
"required": false
},
"title": {
"title": "Title",
"description": "Site title.",
Expand Down Expand Up @@ -14762,9 +14744,6 @@ mockedApiResponse.CommentModel = {
};

mockedApiResponse.settings = {
"connectors_ai_anthropic_api_key": "",
"connectors_ai_google_api_key": "",
"connectors_ai_openai_api_key": "",
"title": "Test Blog",
"description": "",
"url": "http://example.org",
Expand Down
Loading