Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/wp-includes/connectors.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,76 @@ function wp_get_connectors(): array {
return $registry->get_all_registered();
}

/**
* Checks whether AI credentials are configured.
*
* Checks all registered connectors of type 'ai_provider' to determine if
* valid API key credentials have been configured via an environment variable,
* PHP constant, or database option.
*
* @since 7.2.0
*
* @return bool True if AI credentials are configured, false otherwise.
*/
function has_ai_credentials(): bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefix all the things.

Suggested change
function has_ai_credentials(): bool {
function wp_has_ai_credentials(): bool {

$has_credentials = false;
$connectors = array();

if ( wp_supports_ai() ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any pre-flight filters when getting connectors that need to be accounted for here?

$connectors = wp_get_connectors();

foreach ( $connectors as $connector_data ) {
if ( ! isset( $connector_data['type'] ) || 'ai_provider' !== $connector_data['type'] ) {
continue;
}

$auth = $connector_data['authentication'] ?? null;
if ( ! is_array( $auth ) || ( $auth['method'] ?? '' ) !== 'api_key' ) {
continue;
}

$setting_name = $auth['setting_name'] ?? '';
if ( ! is_string( $setting_name ) || '' === $setting_name ) {
continue;
}

if ( 'none' !== _wp_connectors_get_api_key_source(
$setting_name,
$auth['env_var_name'] ?? '',
$auth['constant_name'] ?? ''
) ) {
$has_credentials = true;
break;
}
}
}

/**
* Filters whether AI credentials are available.
*
* Allows third-party plugins to declare credential availability for
* connectors that do not rely on API key settings or for custom logic.
*
* @since 7.2.0
*
* @param bool $has_credentials Whether AI credentials are available.
* @param array<string, array<string, mixed>> $connectors The registered connectors.
*/
$has_credentials = (bool) apply_filters( 'wp_has_ai_credentials', $has_credentials, $connectors );

/**
* Filters whether AI credentials are available.
*
* Legacy filter for compatibility with the WordPress AI plugin.
*
* @since 7.2.0
*
* @param bool $has_credentials Whether AI credentials are available.
* @param array<string, array<string, mixed>> $connectors The registered connectors.
*/
return (bool) apply_filters( 'wpai_has_ai_credentials', $has_credentials, $connectors );
Comment on lines +219 to +230

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can removed be handled by the plugin:

// Plugin code:

add_filter( 'wp_has_ai_credentials', function ( $has_credentials, $connectors ) {
  return (bool) apply_filters( 'wpai_has_ai_credentials', $has_credentials, $connectors );
}, 10, 2 );

As with Gutenberg, plugin focused code should not end up in WP Core.

}

/**
* Resolves an AI provider logo file path to a URL.
*
Expand Down
184 changes: 184 additions & 0 deletions tests/phpunit/tests/connectors/hasAiCredentials.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<?php
/**
* Tests for has_ai_credentials().
*
* @covers ::has_ai_credentials
*
* @group connectors
* @group ai
*/
class Tests_Connectors_HasAiCredentials extends WP_UnitTestCase {

/**
* Cleans up options, environment variables, and filters after each test.
*/
public function tear_down() {
delete_option( 'connectors_ai_openai_api_key' );
delete_option( 'connectors_ai_google_api_key' );
delete_option( 'connectors_ai_anthropic_api_key' );
Comment on lines +16 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Options are reset by the database transaction in the test suite.

Suggested change
delete_option( 'connectors_ai_openai_api_key' );
delete_option( 'connectors_ai_google_api_key' );
delete_option( 'connectors_ai_anthropic_api_key' );

putenv( 'OPENAI_API_KEY' );
putenv( 'GOOGLE_API_KEY' );
putenv( 'GEMINI_API_KEY' );
putenv( 'ANTHROPIC_API_KEY' );

parent::tear_down();
}

/**
* Tests that has_ai_credentials returns false when no AI credentials are set.
*
* @ticket 64924
*/
public function test_returns_false_when_no_credentials_set() {
$this->assertFalse( has_ai_credentials() );
}

/**
* Tests that has_ai_credentials returns true when an AI provider option is set in the database.
*
* @ticket 64924
*/
public function test_returns_true_when_database_option_is_set() {
update_option( 'connectors_ai_openai_api_key', 'sk-test-database-key' );

$this->assertTrue( has_ai_credentials() );
}

/**
* Tests that has_ai_credentials returns true when an AI provider key is set via environment variable.
*
* @ticket 64924
*/
public function test_returns_true_when_env_var_is_set() {
putenv( 'OPENAI_API_KEY=sk-test-env-key' );

$this->assertTrue( has_ai_credentials() );
}

/**
* Tests that has_ai_credentials returns true for other default providers when configured via env var.
*
* @ticket 64924
*/
public function test_returns_true_when_google_or_anthropic_env_var_is_set() {
putenv( 'ANTHROPIC_API_KEY=sk-ant-test-key' );
$this->assertTrue( has_ai_credentials() );
putenv( 'ANTHROPIC_API_KEY' );

putenv( 'GOOGLE_API_KEY=test-gemini-key' );
$this->assertTrue( has_ai_credentials() );
putenv( 'GOOGLE_API_KEY' );
}

Comment on lines +63 to +72

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd split this in to two tests.

The environment variables are reset in this test suite's tear_down so don't need to be reset here.

/**
* Tests that empty string options or empty env vars do not count as valid credentials.
*
* @ticket 64924
*/
public function test_returns_false_when_empty_string_set() {
update_option( 'connectors_ai_openai_api_key', '' );
putenv( 'OPENAI_API_KEY=' );

$this->assertFalse( has_ai_credentials() );
}

/**
* Tests that non-AI connectors do not cause has_ai_credentials to return true.
*
* @ticket 64924
*/
public function test_ignores_non_ai_provider_connectors() {
$registry = WP_Connector_Registry::get_instance();
$this->assertNotNull( $registry );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests with multiple assertions will require a unique message parameter for each to aid debugging at a later date.


$registry->register(
'test_spam_filter',
array(
'name' => 'Spam Filter',
'type' => 'spam_filter',
'description' => 'Test non-AI connector.',
'authentication' => array(
'method' => 'api_key',
'setting_name' => 'connectors_spam_filter_api_key',
),
)
);

update_option( 'connectors_spam_filter_api_key', 'sk-spam-filter-key' );

$has_ai = has_ai_credentials();

delete_option( 'connectors_spam_filter_api_key' );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed, as above.

$registry->unregister( 'test_spam_filter' );

$this->assertFalse( $has_ai );
}

/**
* Tests that has_ai_credentials returns false when wp_supports_ai() returns false.
*
* @ticket 64924
*/
public function test_returns_false_when_wp_supports_ai_is_false() {
update_option( 'connectors_ai_openai_api_key', 'sk-test-key' );

add_filter( 'wp_supports_ai', '__return_false' );
$has_ai = has_ai_credentials();
remove_filter( 'wp_supports_ai', '__return_false' );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
remove_filter( 'wp_supports_ai', '__return_false' );

The WP Test Suite resets filters between tests.

🔢 This applies to all tests with remove_filter() as part of a tear down.


$this->assertFalse( $has_ai );
}

/**
* Tests that the wp_has_ai_credentials filter can override false to true.
*
* @ticket 64924
*/
public function test_wp_has_ai_credentials_filter_can_override_to_true() {
$filter = static function ( $has_credentials, $connectors ) {
return true;
};

add_filter( 'wp_has_ai_credentials', $filter, 10, 2 );
$result = has_ai_credentials();
remove_filter( 'wp_has_ai_credentials', $filter );

$this->assertTrue( $result );
}

/**
* Tests that the wp_has_ai_credentials filter can override true to false.
*
* @ticket 64924
*/
public function test_wp_has_ai_credentials_filter_can_override_to_false() {
update_option( 'connectors_ai_openai_api_key', 'sk-test-key' );

$filter = static function ( $has_credentials, $connectors ) {
return false;
};

add_filter( 'wp_has_ai_credentials', $filter, 10, 2 );
$result = has_ai_credentials();
remove_filter( 'wp_has_ai_credentials', $filter );

$this->assertFalse( $result );
}

/**
* Tests that the legacy wpai_has_ai_credentials filter works for backward compatibility.
*
* @ticket 64924
*/
public function test_legacy_wpai_has_ai_credentials_filter() {
$filter = static function ( $has_credentials, $connectors ) {
return true;
};

add_filter( 'wpai_has_ai_credentials', $filter, 10, 2 );
$result = has_ai_credentials();
remove_filter( 'wpai_has_ai_credentials', $filter );

$this->assertTrue( $result );
}
}
Loading