diff --git a/src/wp-includes/class-wp-connector-registry.php b/src/wp-includes/class-wp-connector-registry.php index fbf35ad73e21d..77e9a0df87d13 100644 --- a/src/wp-includes/class-wp-connector-registry.php +++ b/src/wp-includes/class-wp-connector-registry.php @@ -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 { diff --git a/src/wp-includes/connectors.php b/src/wp-includes/connectors.php index 5916c44158c18..a54dd8029758f 100644 --- a/src/wp-includes/connectors.php +++ b/src/wp-includes/connectors.php @@ -64,7 +64,7 @@ function wp_is_connector_registered( string $id ): bool { * } * @phpstan-return ?array{ * name: non-empty-string, - * description: non-empty-string, + * description: string, * logo_url?: non-empty-string, * type: non-empty-string, * authentication: array{ @@ -75,7 +75,8 @@ function wp_is_connector_registered( string $id ): bool { * env_var_name?: non-empty-string * }, * plugin?: array{ - * file: non-empty-string + * file?: non-empty-string, + * is_active: callable(): bool, * } * } */ @@ -126,7 +127,7 @@ function wp_get_connector( string $id ): ?array { * } * @phpstan-return array */ @@ -160,7 +162,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 +177,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 +321,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 +331,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 +368,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 +380,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'; - - if ( ! isset( $args['authentication']['constant_name'] ) ) { - $args['authentication']['constant_name'] = $constant_case_key; - } + $constant_case_key = strtoupper( (string) preg_replace( '/([a-z])([A-Z])/', '$1_$2', $sanitized_id ) ) . '_API_KEY'; - if ( ! isset( $args['authentication']['env_var_name'] ) ) { - $args['authentication']['env_var_name'] = $constant_case_key; - } - } + $args['authentication']['constant_name'] = $constant_case_key; + $args['authentication']['env_var_name'] = $constant_case_key; } - 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; - } - }; - } + $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 +643,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 +670,10 @@ 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( '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 +707,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,