From 0ec2a8fb98741a68b64b551c1727ff58b8b5e686 Mon Sep 17 00:00:00 2001 From: robelsust Date: Mon, 26 Oct 2026 10:31:07 +0600 Subject: [PATCH 1/2] Connectors: Add has_ai_credentials() helper function to core. Introduces has_ai_credentials() to check if AI credentials have been configured across registered AI provider connectors via database options, environment variables, or PHP constants. Also introduces the wp_has_ai_credentials filter and legacy wpai_has_ai_credentials filter for third-party extensions and backward compatibility. Props therssoftware. Fixes #64924. --- src/wp-includes/connectors.php | 70 +++++++ .../tests/connectors/hasAiCredentials.php | 184 ++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 tests/phpunit/tests/connectors/hasAiCredentials.php diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 88f6a5b964f99..4ce9a886bdf5e 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -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 { + $has_credentials = false; + $connectors = array(); + + if ( wp_supports_ai() ) { + $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> $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> $connectors The registered connectors. + */ + return (bool) apply_filters( 'wpai_has_ai_credentials', $has_credentials, $connectors ); +} + /** * Resolves an AI provider logo file path to a URL. * diff --git a/tests/phpunit/tests/connectors/hasAiCredentials.php b/tests/phpunit/tests/connectors/hasAiCredentials.php new file mode 100644 index 0000000000000..2267d0ad88f5e --- /dev/null +++ b/tests/phpunit/tests/connectors/hasAiCredentials.php @@ -0,0 +1,184 @@ +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' ); + } + + /** + * 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 ); + + $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' ); + $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' ); + + $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 ); + } +} From 23c0c2285195a2a31e49d0e72d89317b1bfd28d2 Mon Sep 17 00:00:00 2001 From: robelsust Date: Tue, 27 Oct 2026 10:55:25 +0600 Subject: [PATCH 2/2] Connectors: Rename to wp_has_ai_credentials() and address review feedback. - Renames has_ai_credentials() to wp_has_ai_credentials(). - Adds pre_wp_has_ai_credentials pre-flight filter allowing short-circuiting. - Removes legacy wpai_has_ai_credentials filter per core convention. - Streamlines test suite tear down and splits provider env var tests into dedicated methods. See #64924. --- src/wp-includes/connectors.php | 30 +-- .../tests/connectors/hasAiCredentials.php | 184 ------------------ .../tests/connectors/wpHasAiCredentials.php | 164 ++++++++++++++++ 3 files changed, 180 insertions(+), 198 deletions(-) delete mode 100644 tests/phpunit/tests/connectors/hasAiCredentials.php create mode 100644 tests/phpunit/tests/connectors/wpHasAiCredentials.php diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 4ce9a886bdf5e..30243e94992d0 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -171,7 +171,21 @@ function wp_get_connectors(): array { * * @return bool True if AI credentials are configured, false otherwise. */ -function has_ai_credentials(): bool { +function wp_has_ai_credentials(): bool { + /** + * Filters whether AI credentials are configured before executing the default check. + * + * Returning a boolean value will short-circuit the default connector credential check. + * + * @since 7.2.0 + * + * @param bool|null $has_credentials Whether AI credentials are configured. Default null. + */ + $pre = apply_filters( 'pre_wp_has_ai_credentials', null ); + if ( null !== $pre ) { + return (bool) $pre; + } + $has_credentials = false; $connectors = array(); @@ -215,19 +229,7 @@ function has_ai_credentials(): bool { * @param bool $has_credentials Whether AI credentials are available. * @param array> $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> $connectors The registered connectors. - */ - return (bool) apply_filters( 'wpai_has_ai_credentials', $has_credentials, $connectors ); + return (bool) apply_filters( 'wp_has_ai_credentials', $has_credentials, $connectors ); } /** diff --git a/tests/phpunit/tests/connectors/hasAiCredentials.php b/tests/phpunit/tests/connectors/hasAiCredentials.php deleted file mode 100644 index 2267d0ad88f5e..0000000000000 --- a/tests/phpunit/tests/connectors/hasAiCredentials.php +++ /dev/null @@ -1,184 +0,0 @@ -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' ); - } - - /** - * 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 ); - - $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' ); - $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' ); - - $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 ); - } -} diff --git a/tests/phpunit/tests/connectors/wpHasAiCredentials.php b/tests/phpunit/tests/connectors/wpHasAiCredentials.php new file mode 100644 index 0000000000000..119743a11c015 --- /dev/null +++ b/tests/phpunit/tests/connectors/wpHasAiCredentials.php @@ -0,0 +1,164 @@ +assertFalse( wp_has_ai_credentials(), 'Should return false when no AI credentials are configured.' ); + } + + /** + * Tests that wp_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( wp_has_ai_credentials(), 'Should return true when an AI provider option exists in the database.' ); + } + + /** + * Tests that wp_has_ai_credentials returns true when OpenAI key is set via environment variable. + * + * @ticket 64924 + */ + public function test_returns_true_when_openai_env_var_is_set() { + putenv( 'OPENAI_API_KEY=sk-test-openai-key' ); + + $this->assertTrue( wp_has_ai_credentials(), 'Should return true when OPENAI_API_KEY environment variable is configured.' ); + } + + /** + * Tests that wp_has_ai_credentials returns true when Anthropic key is set via environment variable. + * + * @ticket 64924 + */ + public function test_returns_true_when_anthropic_env_var_is_set() { + putenv( 'ANTHROPIC_API_KEY=sk-ant-test-key' ); + + $this->assertTrue( wp_has_ai_credentials(), 'Should return true when ANTHROPIC_API_KEY environment variable is configured.' ); + } + + /** + * Tests that wp_has_ai_credentials returns true when Google key is set via environment variable. + * + * @ticket 64924 + */ + public function test_returns_true_when_google_env_var_is_set() { + putenv( 'GOOGLE_API_KEY=test-google-key' ); + + $this->assertTrue( wp_has_ai_credentials(), 'Should return true when GOOGLE_API_KEY environment variable is configured.' ); + } + + /** + * 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( wp_has_ai_credentials(), 'Empty option or environment variable should not count as credentials.' ); + } + + /** + * Tests that non-AI connectors do not cause wp_has_ai_credentials to return true. + * + * @ticket 64924 + */ + public function test_ignores_non_ai_provider_connectors() { + $registry = WP_Connector_Registry::get_instance(); + $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 = wp_has_ai_credentials(); + $registry->unregister( 'test_spam_filter' ); + + $this->assertFalse( $has_ai, 'Non-AI provider connectors should be ignored.' ); + } + + /** + * Tests that wp_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' ); + + $this->assertFalse( wp_has_ai_credentials(), 'Should return false when wp_supports_ai() returns false.' ); + } + + /** + * Tests that the pre_wp_has_ai_credentials filter can short-circuit the credential check. + * + * @ticket 64924 + */ + public function test_pre_wp_has_ai_credentials_filter_can_short_circuit() { + add_filter( 'pre_wp_has_ai_credentials', '__return_true' ); + + $this->assertTrue( wp_has_ai_credentials(), 'Should return true when pre_wp_has_ai_credentials filter returns true.' ); + } + + /** + * 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() { + add_filter( 'wp_has_ai_credentials', '__return_true' ); + + $this->assertTrue( wp_has_ai_credentials(), 'Should return true when wp_has_ai_credentials filter overrides to true.' ); + } + + /** + * 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' ); + + add_filter( 'wp_has_ai_credentials', '__return_false' ); + + $this->assertFalse( wp_has_ai_credentials(), 'Should return false when wp_has_ai_credentials filter overrides to false.' ); + } +}