From 2686f3dd663583dd7b189f7c2633b48b0f13cd10 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Mon, 13 Apr 2026 23:12:54 +0530 Subject: [PATCH 1/5] Connectors: Add is_active callback support to plugin registration Backport of WordPress/gutenberg#76994 --- .../class-wp-connector-registry.php | 19 ++++++++++++++++++- src/wp-includes/connectors.php | 18 ++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/class-wp-connector-registry.php b/src/wp-includes/class-wp-connector-registry.php index 9fe51be96aa8e..62baac907b9fd 100644 --- a/src/wp-includes/class-wp-connector-registry.php +++ b/src/wp-includes/class-wp-connector-registry.php @@ -40,7 +40,8 @@ * env_var_name?: non-empty-string * }, * plugin?: array{ - * file: non-empty-string + * file: non-empty-string, + * is_active?: callable(): bool * } * } */ @@ -111,6 +112,8 @@ final class WP_Connector_Registry { * * @type string $file The plugin's main file path relative to the plugins * directory (e.g. 'my-plugin/my-plugin.php' or 'hello.php'). + * @type callable $is_active Optional callback to determine whether the plugin + * is active. Receives no arguments and must return bool. * } * } * @return array|null The registered connector data on success, null on failure. @@ -245,6 +248,20 @@ public function register( string $id, array $args ): ?array { if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) && ! empty( $args['plugin']['file'] ) ) { $connector['plugin'] = array( 'file' => $args['plugin']['file'] ); + + if ( isset( $args['plugin']['is_active'] ) ) { + if ( ! is_callable( $args['plugin']['is_active'] ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Connector ID. */ + sprintf( __( 'Connector "%s" plugin is_active must be callable.' ), esc_html( $id ) ), + '7.0.0' + ); + return null; + } + + $connector['plugin']['is_active'] = $args['plugin']['is_active']; + } } $this->registered_connectors[ $id ] = $connector; diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 63e018074fd58..6cfb8e615bc43 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -674,8 +674,22 @@ function _wp_connectors_get_connector_script_module_data( array $data ): array { if ( ! empty( $connector_data['plugin']['file'] ) ) { $file = $connector_data['plugin']['file']; - $is_installed = file_exists( wp_normalize_path( WP_PLUGIN_DIR . '/' . $file ) ); - $is_activated = $is_installed && is_plugin_active( $file ); + $is_installed = false; + $is_activated = false; + + if ( ! empty( $connector_data['plugin']['is_active'] ) && is_callable( $connector_data['plugin']['is_active'] ) ) { + $is_activated = (bool) call_user_func( $connector_data['plugin']['is_active'] ); + } + + if ( ! $is_activated ) { + $is_activated = is_plugin_active( $file ); + } + + if ( $is_activated ) { + $is_installed = true; + } else { + $is_installed = file_exists( WP_PLUGIN_DIR . '/' . $file ); + } $connector_out['plugin'] = array( 'file' => $file, From a83cd4e368b1f3636136768391df57ed3bc07315 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Tue, 21 Apr 2026 15:24:58 +0530 Subject: [PATCH 2/5] refactor(connectors): simplify plugin status checks --- src/wp-includes/class-wp-connector-registry.php | 4 ++-- src/wp-includes/connectors.php | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/class-wp-connector-registry.php b/src/wp-includes/class-wp-connector-registry.php index 62baac907b9fd..8cb1ebb4fa6cb 100644 --- a/src/wp-includes/class-wp-connector-registry.php +++ b/src/wp-includes/class-wp-connector-registry.php @@ -110,8 +110,8 @@ final class WP_Connector_Registry { * @type array $plugin { * Optional. Plugin data for install/activate UI. * - * @type string $file The plugin's main file path relative to the plugins - * directory (e.g. 'my-plugin/my-plugin.php' or 'hello.php'). + * @type string $file The plugin's main file path relative to the plugins + * directory (e.g. 'my-plugin/my-plugin.php' or 'hello.php'). * @type callable $is_active Optional callback to determine whether the plugin * is active. Receives no arguments and must return bool. * } diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 6cfb8e615bc43..c5d46c7637faa 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -685,11 +685,7 @@ function _wp_connectors_get_connector_script_module_data( array $data ): array { $is_activated = is_plugin_active( $file ); } - if ( $is_activated ) { - $is_installed = true; - } else { - $is_installed = file_exists( WP_PLUGIN_DIR . '/' . $file ); - } + $is_installed = $is_activated || file_exists( WP_PLUGIN_DIR . '/' . $file ); $connector_out['plugin'] = array( 'file' => $file, From 6d0a4b0e61c344689b6c7dd8749240d6c5d25d05 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Tue, 21 Apr 2026 15:33:22 +0530 Subject: [PATCH 3/5] docs(connectors): Align the plugin docblock indentation --- src/wp-includes/class-wp-connector-registry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-connector-registry.php b/src/wp-includes/class-wp-connector-registry.php index 8cb1ebb4fa6cb..c35832ca54a48 100644 --- a/src/wp-includes/class-wp-connector-registry.php +++ b/src/wp-includes/class-wp-connector-registry.php @@ -110,7 +110,7 @@ final class WP_Connector_Registry { * @type array $plugin { * Optional. Plugin data for install/activate UI. * - * @type string $file The plugin's main file path relative to the plugins + * @type string $file The plugin's main file path relative to the plugins * directory (e.g. 'my-plugin/my-plugin.php' or 'hello.php'). * @type callable $is_active Optional callback to determine whether the plugin * is active. Receives no arguments and must return bool. From 31eb00b540f7951ea1943d12490b7515d8afc83e Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Mon, 27 Apr 2026 17:18:57 +0530 Subject: [PATCH 4/5] Connector Registry: Add default `is_active` callback and enhance tests for plugin registration --- .../class-wp-connector-registry.php | 3 ++ src/wp-includes/connectors.php | 16 +----- .../tests/connectors/wpConnectorRegistry.php | 50 ++++++++++++++++++- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/wp-includes/class-wp-connector-registry.php b/src/wp-includes/class-wp-connector-registry.php index c35832ca54a48..5191f449942dd 100644 --- a/src/wp-includes/class-wp-connector-registry.php +++ b/src/wp-includes/class-wp-connector-registry.php @@ -114,6 +114,7 @@ final class WP_Connector_Registry { * directory (e.g. 'my-plugin/my-plugin.php' or 'hello.php'). * @type callable $is_active Optional callback to determine whether the plugin * is active. Receives no arguments and must return bool. + * Defaults to `__return_true`. * } * } * @return array|null The registered connector data on success, null on failure. @@ -261,6 +262,8 @@ public function register( string $id, array $args ): ?array { } $connector['plugin']['is_active'] = $args['plugin']['is_active']; + } else { + $connector['plugin']['is_active'] = '__return_true'; } } diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index c5d46c7637faa..00807caa68062 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -638,10 +638,6 @@ function _wp_connectors_pass_default_keys_to_ai_client(): void { function _wp_connectors_get_connector_script_module_data( array $data ): array { $registry = AiClient::defaultRegistry(); - if ( ! function_exists( 'is_plugin_active' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - $connectors = array(); foreach ( wp_get_connectors() as $connector_id => $connector_data ) { $auth = $connector_data['authentication']; @@ -674,17 +670,7 @@ function _wp_connectors_get_connector_script_module_data( array $data ): array { if ( ! empty( $connector_data['plugin']['file'] ) ) { $file = $connector_data['plugin']['file']; - $is_installed = false; - $is_activated = false; - - if ( ! empty( $connector_data['plugin']['is_active'] ) && is_callable( $connector_data['plugin']['is_active'] ) ) { - $is_activated = (bool) call_user_func( $connector_data['plugin']['is_active'] ); - } - - if ( ! $is_activated ) { - $is_activated = is_plugin_active( $file ); - } - + $is_activated = (bool) call_user_func( $connector_data['plugin']['is_active'] ); $is_installed = $is_activated || file_exists( WP_PLUGIN_DIR . '/' . $file ); $connector_out['plugin'] = array( diff --git a/tests/phpunit/tests/connectors/wpConnectorRegistry.php b/tests/phpunit/tests/connectors/wpConnectorRegistry.php index d1a46dc0981fe..0a2ce90d30712 100644 --- a/tests/phpunit/tests/connectors/wpConnectorRegistry.php +++ b/tests/phpunit/tests/connectors/wpConnectorRegistry.php @@ -299,7 +299,55 @@ public function test_register_includes_plugin_data() { $result = $this->registry->register( 'with-plugin', $args ); $this->assertArrayHasKey( 'plugin', $result ); - $this->assertSame( array( 'file' => 'my-plugin/my-plugin.php' ), $result['plugin'] ); + $this->assertSame( 'my-plugin/my-plugin.php', $result['plugin']['file'] ); + } + + /** + * @ticket 65020 + */ + public function test_register_stores_plugin_is_active_callback() { + $args = self::$default_args; + $args['plugin'] = array( + 'file' => 'my-plugin/my-plugin.php', + 'is_active' => '__return_true', + ); + + $result = $this->registry->register( 'with-callback', $args ); + + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'is_active', $result['plugin'] ); + $this->assertIsCallable( $result['plugin']['is_active'] ); + } + + /** + * @ticket 65020 + */ + public function test_register_rejects_non_callable_plugin_is_active() { + $this->setExpectedIncorrectUsage( 'WP_Connector_Registry::register' ); + + $args = self::$default_args; + $args['plugin'] = array( + 'file' => 'my-plugin/my-plugin.php', + 'is_active' => 'not_a_real_function_name', + ); + + $result = $this->registry->register( 'bad-callback', $args ); + + $this->assertNull( $result ); + } + + /** + * @ticket 65020 + */ + public function test_register_defaults_plugin_is_active_to_return_true() { + $args = self::$default_args; + $args['plugin'] = array( 'file' => 'my-plugin/my-plugin.php' ); + + $result = $this->registry->register( 'default-callback', $args ); + + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'is_active', $result['plugin'] ); + $this->assertSame( '__return_true', $result['plugin']['is_active'] ); } /** From e87d603582136ca9946aef473110045eb444c992 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 30 Apr 2026 12:34:37 +0100 Subject: [PATCH 5/5] Apply feedback and pass a correct is_active hook for the ai connectors --- .../class-wp-connector-registry.php | 19 +++++++++++++------ src/wp-includes/connectors.php | 13 ++++++++++++- .../tests/connectors/wpConnectorRegistry.php | 6 ++++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/class-wp-connector-registry.php b/src/wp-includes/class-wp-connector-registry.php index 5191f449942dd..fbf35ad73e21d 100644 --- a/src/wp-includes/class-wp-connector-registry.php +++ b/src/wp-includes/class-wp-connector-registry.php @@ -110,8 +110,9 @@ final class WP_Connector_Registry { * @type array $plugin { * Optional. Plugin data for install/activate UI. * - * @type string $file The plugin's main file path relative to the plugins - * directory (e.g. 'my-plugin/my-plugin.php' or 'hello.php'). + * @type string $file Optional. The plugin's main file path relative to the + * plugins directory (e.g. 'my-plugin/my-plugin.php' or + * 'hello.php'). * @type callable $is_active Optional callback to determine whether the plugin * is active. Receives no arguments and must return bool. * Defaults to `__return_true`. @@ -247,8 +248,12 @@ public function register( string $id, array $args ): ?array { } } - if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) && ! empty( $args['plugin']['file'] ) ) { - $connector['plugin'] = array( 'file' => $args['plugin']['file'] ); + $connector['plugin'] = array(); + + if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) ) { + if ( ! empty( $args['plugin']['file'] ) ) { + $connector['plugin']['file'] = $args['plugin']['file']; + } if ( isset( $args['plugin']['is_active'] ) ) { if ( ! is_callable( $args['plugin']['is_active'] ) ) { @@ -262,11 +267,13 @@ public function register( string $id, array $args ): ?array { } $connector['plugin']['is_active'] = $args['plugin']['is_active']; - } else { - $connector['plugin']['is_active'] = '__return_true'; } } + if ( ! isset( $connector['plugin']['is_active'] ) ) { + $connector['plugin']['is_active'] = '__return_true'; + } + $this->registered_connectors[ $id ] = $connector; return $connector; } diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 00807caa68062..9e7db2cef8f68 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -367,6 +367,17 @@ function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $re } } } + + if ( ! isset( $args['plugin']['is_active'] ) ) { + $args['plugin']['is_active'] = static function () use ( $ai_registry, $id ): bool { + try { + return $ai_registry->hasProvider( $id ); + } catch ( Exception $e ) { + return false; + } + }; + } + $registry->register( $id, $args ); } } @@ -671,7 +682,7 @@ function _wp_connectors_get_connector_script_module_data( array $data ): array { if ( ! empty( $connector_data['plugin']['file'] ) ) { $file = $connector_data['plugin']['file']; $is_activated = (bool) call_user_func( $connector_data['plugin']['is_active'] ); - $is_installed = $is_activated || file_exists( WP_PLUGIN_DIR . '/' . $file ); + $is_installed = $is_activated || file_exists( wp_normalize_path( WP_PLUGIN_DIR . '/' . $file ) ); $connector_out['plugin'] = array( 'file' => $file, diff --git a/tests/phpunit/tests/connectors/wpConnectorRegistry.php b/tests/phpunit/tests/connectors/wpConnectorRegistry.php index 0a2ce90d30712..47e12eb7fd6fd 100644 --- a/tests/phpunit/tests/connectors/wpConnectorRegistry.php +++ b/tests/phpunit/tests/connectors/wpConnectorRegistry.php @@ -353,10 +353,12 @@ public function test_register_defaults_plugin_is_active_to_return_true() { /** * @ticket 64791 */ - public function test_register_omits_plugin_when_not_provided() { + public function test_register_defaults_plugin_when_not_provided() { $result = $this->registry->register( 'no-plugin', self::$default_args ); - $this->assertArrayNotHasKey( 'plugin', $result ); + $this->assertArrayHasKey( 'plugin', $result ); + $this->assertArrayNotHasKey( 'file', $result['plugin'] ); + $this->assertSame( '__return_true', $result['plugin']['is_active'] ); } /**