Connectors: Refine plugin install check and PHPStan types.

Replace `file_exists()` with `validate_plugin()` in the connectors script module data, and refine PHPStan type definitions in the connectors code to better reflect the actual shape of registered connector data.

Follow-up to [62288].

Props jorgefilipecosta, mukesh27, peterwilsoncc, westonruter, wildworks.
See #65020.
Built from https://develop.svn.wordpress.org/trunk@62332


git-svn-id: http://core.svn.wordpress.org/trunk@61613 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
wildworks
2026-05-08 10:39:43 +00:00
parent 034da2217a
commit 5e62b3f06c
3 changed files with 68 additions and 47 deletions
+21 -5
View File
@@ -29,7 +29,7 @@
*
* @phpstan-type Connector array{
* name: non-empty-string,
* description: non-empty-string,
* description: string,
* logo_url?: non-empty-string,
* type: non-empty-string,
* authentication: array{
@@ -39,9 +39,9 @@
* constant_name?: non-empty-string,
* env_var_name?: non-empty-string
* },
* plugin?: array{
* file: non-empty-string,
* is_active?: callable(): bool
* plugin: array{
* file?: non-empty-string,
* is_active: callable(): bool
* }
* }
*/
@@ -120,7 +120,23 @@ final class WP_Connector_Registry {
* }
* @return array|null The registered connector data on success, null on failure.
*
* @phpstan-param Connector $args
* @phpstan-param array{
* name: non-empty-string,
* description?: string,
* logo_url?: non-empty-string,
* type: non-empty-string,
* authentication: array{
* method: 'api_key'|'none',
* credentials_url?: non-empty-string,
* setting_name?: non-empty-string,
* constant_name?: non-empty-string,
* env_var_name?: non-empty-string
* },
* plugin?: array{
* file?: non-empty-string,
* is_active?: callable(): bool
* }
* } $args
* @phpstan-return Connector|null
*/
public function register( string $id, array $args ): ?array {
+46 -41
View File
@@ -58,13 +58,15 @@ function wp_is_connector_registered( string $id ): bool {
* @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 Callback to determine whether the plugin is active. Receives no arguments and must return bool.
* Defaults to `__return_true`.
* }
* }
* @phpstan-return ?array{
* name: non-empty-string,
* description: non-empty-string,
* description: string,
* logo_url?: non-empty-string,
* type: non-empty-string,
* authentication: array{
@@ -74,8 +76,9 @@ function wp_is_connector_registered( string $id ): bool {
* constant_name?: non-empty-string,
* env_var_name?: non-empty-string
* },
* plugin?: array{
* file: non-empty-string
* plugin: array{
* file?: non-empty-string,
* is_active: callable(): bool,
* }
* }
*/
@@ -119,14 +122,16 @@ function wp_get_connector( string $id ): ?array {
* @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 Callback to determine whether the plugin is active. Receives no arguments and must return bool.
* Defaults to `__return_true`.
* }
* }
* }
* @phpstan-return array<string, array{
* name: non-empty-string,
* description: non-empty-string,
* description: string,
* logo_url?: non-empty-string,
* type: non-empty-string,
* authentication: array{
@@ -136,8 +141,9 @@ function wp_get_connector( string $id ): ?array {
* constant_name?: non-empty-string,
* env_var_name?: non-empty-string
* },
* plugin?: array{
* file: non-empty-string
* plugin: array{
* file?: non-empty-string,
* is_active: callable(): bool,
* }
* }>
*/
@@ -160,7 +166,7 @@ function wp_get_connectors(): array {
* @access private
*
* @param string $path Absolute path to the logo file.
* @return string|null The URL to the logo file, or null if the path is invalid.
* @return non-empty-string|null The URL to the logo file, or null if the path is invalid.
*/
function _wp_connectors_resolve_ai_provider_logo_url( string $path ): ?string {
if ( ! $path ) {
@@ -175,12 +181,14 @@ function _wp_connectors_resolve_ai_provider_logo_url( string $path ): ?string {
$mu_plugin_dir = wp_normalize_path( WPMU_PLUGIN_DIR );
if ( str_starts_with( $path, $mu_plugin_dir . '/' ) ) {
return plugins_url( substr( $path, strlen( $mu_plugin_dir ) ), WPMU_PLUGIN_DIR . '/.' );
$logo_url = plugins_url( substr( $path, strlen( $mu_plugin_dir ) ), WPMU_PLUGIN_DIR . '/.' );
return $logo_url ? $logo_url : null;
}
$plugin_dir = wp_normalize_path( WP_PLUGIN_DIR );
if ( str_starts_with( $path, $plugin_dir . '/' ) ) {
return plugins_url( substr( $path, strlen( $plugin_dir ) ) );
$logo_url = plugins_url( substr( $path, strlen( $plugin_dir ) ) );
return $logo_url ? $logo_url : null;
}
_doing_it_wrong(
@@ -317,7 +325,7 @@ function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $re
// Registry values (from provider plugins) take precedence over hardcoded fallbacks.
$ai_registry = AiClient::defaultRegistry();
foreach ( $ai_registry->getRegisteredProviderIds() as $connector_id ) {
foreach ( array_filter( $ai_registry->getRegisteredProviderIds() ) as $connector_id ) {
$provider_class_name = $ai_registry->getProviderClassName( $connector_id );
$provider_metadata = $provider_class_name::metadata();
@@ -327,9 +335,11 @@ function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $re
if ( $is_api_key ) {
$credentials_url = $provider_metadata->getCredentialsUrl();
$authentication = array(
'method' => 'api_key',
'credentials_url' => $credentials_url ? $credentials_url : null,
'method' => 'api_key',
);
if ( $credentials_url ) {
$authentication['credentials_url'] = $credentials_url;
}
} else {
$authentication = array( 'method' => 'none' );
}
@@ -362,8 +372,10 @@ function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $re
'description' => $description ? $description : '',
'type' => 'ai_provider',
'authentication' => $authentication,
'logo_url' => $logo_url,
);
if ( $logo_url ) {
$defaults[ $connector_id ]['logo_url'] = $logo_url;
}
}
}
@@ -372,33 +384,22 @@ function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $re
if ( 'api_key' === $args['authentication']['method'] ) {
$sanitized_id = str_replace( '-', '_', $id );
if ( ! isset( $args['authentication']['setting_name'] ) ) {
$args['authentication']['setting_name'] = "connectors_ai_{$sanitized_id}_api_key";
}
$args['authentication']['setting_name'] = "connectors_ai_{$sanitized_id}_api_key";
// All AI providers use the {CONSTANT_CASE_ID}_API_KEY naming convention.
if ( ! isset( $args['authentication']['constant_name'] ) || ! isset( $args['authentication']['env_var_name'] ) ) {
$constant_case_key = strtoupper( preg_replace( '/([a-z])([A-Z])/', '$1_$2', $sanitized_id ) ) . '_API_KEY';
$constant_case_key = strtoupper( (string) preg_replace( '/([a-z])([A-Z])/', '$1_$2', $sanitized_id ) ) . '_API_KEY';
if ( ! isset( $args['authentication']['constant_name'] ) ) {
$args['authentication']['constant_name'] = $constant_case_key;
}
$args['authentication']['constant_name'] = $constant_case_key;
$args['authentication']['env_var_name'] = $constant_case_key;
}
if ( ! isset( $args['authentication']['env_var_name'] ) ) {
$args['authentication']['env_var_name'] = $constant_case_key;
}
$args['plugin']['is_active'] = static function () use ( $ai_registry, $id ): bool {
try {
return $ai_registry->hasProvider( $id );
} catch ( Exception $e ) {
return false;
}
}
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 );
}
@@ -646,7 +647,7 @@ function _wp_connectors_pass_default_keys_to_ai_client(): void {
}
$api_key = get_option( $auth['setting_name'], '' );
if ( '' === $api_key ) {
if ( ! is_string( $api_key ) || '' === $api_key ) {
continue;
}
@@ -673,6 +674,10 @@ add_action( 'init', '_wp_connectors_pass_default_keys_to_ai_client', 20 );
function _wp_connectors_get_connector_script_module_data( array $data ): array {
$registry = AiClient::defaultRegistry();
if ( ! function_exists( 'validate_plugin' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$connectors = array();
foreach ( wp_get_connectors() as $connector_id => $connector_data ) {
$auth = $connector_data['authentication'];
@@ -706,7 +711,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_normalize_path( WP_PLUGIN_DIR . '/' . $file ) );
$is_installed = $is_activated || 0 === validate_plugin( $file );
$connector_out['plugin'] = array(
'file' => $file,
+1 -1
View File
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
$wp_version = '7.1-alpha-62328';
$wp_version = '7.1-alpha-62332';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.