From d1dae91172883870f399b4b6632595c2ac927e1f Mon Sep 17 00:00:00 2001 From: Prabhat Mishra Date: Thu, 25 Sep 2025 17:29:15 +0530 Subject: [PATCH 01/10] Show plugin cards when external requests are disabled Fixes #2189 where the Performance settings page would be empty when external HTTP requests are blocked. Now we read plugin info directly from installed plugin headers as a fallback, so users can still see and manage their performance plugins even without internet access. Added local plugin detection that scans for installed performance plugins and displays them with a (local) indicator. --- .../includes/admin/local-plugin-fallback.php | 195 ++++++++++++++++++ .../includes/admin/plugins.php | 56 +++++ plugins/performance-lab/load.php | 1 + .../admin/test-local-plugin-fallback.php | 117 +++++++++++ 4 files changed, 369 insertions(+) create mode 100644 plugins/performance-lab/includes/admin/local-plugin-fallback.php create mode 100644 plugins/performance-lab/tests/includes/admin/test-local-plugin-fallback.php diff --git a/plugins/performance-lab/includes/admin/local-plugin-fallback.php b/plugins/performance-lab/includes/admin/local-plugin-fallback.php new file mode 100644 index 0000000000..8afc70b7b9 --- /dev/null +++ b/plugins/performance-lab/includes/admin/local-plugin-fallback.php @@ -0,0 +1,195 @@ + Local plugin data keyed by slug. + */ +function perflab_get_local_plugin_fallback_data( array $plugin_slugs ): array { + // Ensure we have access to plugin functions. + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $local_plugins = get_plugins(); + $fallback_data = array(); + + foreach ( $plugin_slugs as $plugin_slug ) { + // Look for plugin files that match this slug. + $plugin_file = perflab_find_local_plugin_file( $local_plugins, $plugin_slug ); + + if ( ! $plugin_file ) { + continue; // Plugin not installed locally. + } + + $plugin_headers = $local_plugins[ $plugin_file ]; + $is_active = is_plugin_active( $plugin_file ); + + // Build normalized plugin data similar to WordPress.org API response. + $fallback_data[ $plugin_slug ] = array( + 'name' => $plugin_headers['Name'] ?? $plugin_slug, + 'slug' => $plugin_slug, + 'short_description' => perflab_sanitize_plugin_description( $plugin_headers['Description'] ?? '' ), + 'requires' => $plugin_headers['RequiresWP'] ?? false, + 'requires_php' => $plugin_headers['RequiresPHP'] ?? false, + 'requires_plugins' => perflab_parse_requires_plugins( $plugin_headers ), + 'version' => $plugin_headers['Version'] ?? '0.0.0', + 'fallback_local' => true, // Flag to identify this as local fallback data. + 'is_installed' => true, + 'is_active' => $is_active, + ); + } + + return $fallback_data; +} + +/** + * Finds the plugin file for a given slug among installed plugins. + * + * @since 4.0.1 + * + * @param array> $local_plugins Array from get_plugins(). + * @param string $plugin_slug Plugin slug to find. + * @return string|false Plugin file path relative to plugins directory, or false if not found. + */ +function perflab_find_local_plugin_file( array $local_plugins, string $plugin_slug ) { + foreach ( $local_plugins as $plugin_file => $plugin_data ) { + // Extract directory name from plugin file path. + $plugin_dir = strtok( $plugin_file, '/' ); + + if ( $plugin_dir === $plugin_slug ) { + return $plugin_file; + } + } + + return false; +} + +/** + * Sanitizes and truncates plugin description for display. + * + * @since 4.0.1 + * + * @param string $description Raw plugin description. + * @return string Sanitized and truncated description. + */ +function perflab_sanitize_plugin_description( string $description ): string { + if ( empty( $description ) ) { + return ''; + } + + // Strip all HTML tags and decode entities. + $description = wp_strip_all_tags( $description ); + $description = html_entity_decode( $description, ENT_QUOTES, 'UTF-8' ); + + // Truncate to reasonable length for short description. + if ( mb_strlen( $description ) > 200 ) { + $description = mb_substr( $description, 0, 200 ) . '...'; + } + + return trim( $description ); +} + +/** + * Parses the requires_plugins from plugin headers. + * + * This attempts to extract required plugins from various possible header formats. + * + * @since 4.0.1 + * + * @param array $plugin_headers Plugin headers array. + * @return string[] Array of required plugin slugs. + */ +function perflab_parse_requires_plugins( array $plugin_headers ): array { + $requires_plugins = array(); + + // Check for RequiresPlugins header (WordPress 6.5+). + if ( ! empty( $plugin_headers['RequiresPlugins'] ) ) { + $plugins = array_map( 'trim', explode( ',', $plugin_headers['RequiresPlugins'] ) ); + $requires_plugins = array_merge( $requires_plugins, $plugins ); + } + + // For known Performance Lab plugins, add their specific dependencies. + $plugin_name = $plugin_headers['Name'] ?? ''; + + // Embed Optimizer has a soft dependency on Optimization Detective. + if ( false !== strpos( $plugin_name, 'Embed Optimizer' ) ) { + $requires_plugins[] = 'optimization-detective'; + } + + // Image Prioritizer has a soft dependency on Optimization Detective. + if ( false !== strpos( $plugin_name, 'Image Prioritizer' ) ) { + $requires_plugins[] = 'optimization-detective'; + } + + return array_unique( array_filter( $requires_plugins ) ); +} + +/** + * Checks if external requests are blocked or likely to fail. + * + * @since 4.0.1 + * + * @return bool True if external requests are blocked or should be avoided. + */ +function perflab_are_external_requests_blocked(): bool { + // Check if external requests are explicitly blocked. + if ( defined( 'WP_HTTP_BLOCK_EXTERNAL' ) && WP_HTTP_BLOCK_EXTERNAL ) { + // Check if wordpress.org is in the allowed hosts. + $allowed_hosts = defined( 'WP_ACCESSIBLE_HOSTS' ) ? WP_ACCESSIBLE_HOSTS : ''; + if ( empty( $allowed_hosts ) || false === strpos( $allowed_hosts, 'wordpress.org' ) ) { + return true; + } + } + + return false; +} + +/** + * Gets plugin settings URL for a given plugin slug. + * + * This attempts to determine the settings URL for locally installed performance plugins. + * + * @since 4.0.1 + * + * @param string $plugin_slug Plugin slug. + * @return string|null Settings URL or null if not available. + */ +function perflab_get_local_plugin_settings_url( string $plugin_slug ): ?string { + // Use existing function if available (it should be). + if ( function_exists( 'perflab_get_plugin_settings_url' ) ) { + return perflab_get_plugin_settings_url( $plugin_slug ); + } + + // Fallback for common patterns. + $settings_patterns = array( + 'webp-uploads' => admin_url( 'options-media.php' ), + 'dominant-color-images' => admin_url( 'options-media.php' ), + 'speculation-rules' => admin_url( 'options-general.php?page=speculation-rules' ), + 'performant-translations' => admin_url( 'options-general.php?page=performant-translations' ), + ); + + return $settings_patterns[ $plugin_slug ] ?? null; +} diff --git a/plugins/performance-lab/includes/admin/plugins.php b/plugins/performance-lab/includes/admin/plugins.php index ce0fa87eab..d0ae6787f8 100644 --- a/plugins/performance-lab/includes/admin/plugins.php +++ b/plugins/performance-lab/includes/admin/plugins.php @@ -15,7 +15,10 @@ /** * Gets plugin info for the given plugin slug from WordPress.org. * + * Falls back to local plugin data when external requests are disabled or fail. + * * @since 2.8.0 + * @since 4.0.1 Added fallback to local plugin data when external requests fail. * * @param string $plugin_slug The string identifier for the plugin in questions slug. * @return array{name: string, slug: string, short_description: string, requires: string|false, requires_php: string|false, requires_plugins: string[], version: string}|WP_Error Array of plugin data or WP_Error if failed. @@ -35,6 +38,21 @@ function perflab_query_plugin_info( string $plugin_slug ) { return $plugins[ $plugin_slug ]; // Return cached plugin info if found. } + // Check if external requests are blocked and if we should use local fallback. + $should_use_fallback = perflab_are_external_requests_blocked(); + + if ( $should_use_fallback ) { + // Try to get local plugin data instead. + $local_data = perflab_get_local_plugin_fallback_data( array( $plugin_slug ) ); + if ( isset( $local_data[ $plugin_slug ] ) ) { + // Cache the local data with a shorter expiration. + $cached_plugins = is_array( $plugins ) ? $plugins : array(); + $cached_plugins[ $plugin_slug ] = $local_data[ $plugin_slug ]; + set_transient( $transient_key, $cached_plugins, 5 * MINUTE_IN_SECONDS ); + return $local_data[ $plugin_slug ]; + } + } + $fields = array( 'name', 'slug', @@ -60,6 +78,19 @@ function perflab_query_plugin_info( string $plugin_slug ) { $plugins = array(); if ( is_wp_error( $response ) ) { + // Try local fallback first before giving up. + $local_fallback_data = perflab_get_local_plugin_fallback_data( perflab_get_standalone_plugins() ); + + if ( ! empty( $local_fallback_data ) ) { + // We have some local plugins to show, cache them. + set_transient( $transient_key, $local_fallback_data, 5 * MINUTE_IN_SECONDS ); + + if ( isset( $local_fallback_data[ $plugin_slug ] ) ) { + return $local_fallback_data[ $plugin_slug ]; + } + } + + // No local fallback available, store error. $plugins[ $plugin_slug ] = array( 'error' => array( 'code' => 'api_error', @@ -77,6 +108,19 @@ function perflab_query_plugin_info( string $plugin_slug ) { $has_errors = true; } elseif ( ! is_object( $response ) || ! property_exists( $response, 'plugins' ) ) { + // Try local fallback first before giving up. + $local_fallback_data = perflab_get_local_plugin_fallback_data( perflab_get_standalone_plugins() ); + + if ( ! empty( $local_fallback_data ) ) { + // We have some local plugins to show, cache them. + set_transient( $transient_key, $local_fallback_data, 5 * MINUTE_IN_SECONDS ); + + if ( isset( $local_fallback_data[ $plugin_slug ] ) ) { + return $local_fallback_data[ $plugin_slug ]; + } + } + + // No local fallback available, store error. $plugins[ $plugin_slug ] = array( 'error' => array( 'code' => 'no_plugins', @@ -593,6 +637,13 @@ function perflab_render_plugin_card( array $plugin_data ): void { if ( null !== $settings_url ) { /* translators: %s is the settings URL */ $action_links[] = sprintf( '%s', esc_url( $settings_url ), esc_html__( 'Settings', 'performance-lab' ) ); + } elseif ( ! empty( $plugin_data['fallback_local'] ) ) { + // Try local fallback settings URL for locally installed plugins. + $local_settings_url = perflab_get_local_plugin_settings_url( $plugin_data['slug'] ); + if ( null !== $local_settings_url ) { + /* translators: %s is the settings URL */ + $action_links[] = sprintf( '%s', esc_url( $local_settings_url ), esc_html__( 'Settings', 'performance-lab' ) ); + } } } ?> @@ -668,6 +719,11 @@ function perflab_render_plugin_card( array $plugin_data ): void { + + + + +