Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions plugins/performance-lab/includes/admin/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
}
// @codeCoverageIgnoreEnd

// Load local plugin fallback functionality.
require_once __DIR__ . '/local-plugin-fallback.php';

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? It's already being loaded below.

Suggested change
// Load local plugin fallback functionality.
require_once __DIR__ . '/local-plugin-fallback.php';

/**
* Adds the features page to the Settings menu.
*
Expand Down
225 changes: 225 additions & 0 deletions plugins/performance-lab/includes/admin/local-plugin-fallback.php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these functions can be put directly into plugins/performance-lab/includes/admin/plugins.php. No need for a separate file.

Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
<?php
/**
* Local Plugin Fallback functionality for Performance Lab.
*
* Provides fallback functionality to show plugin cards for locally installed
* performance-related plugins when external API requests are disabled or fail.
*
* @package performance-lab
* @since n.e.x.t
*/

// @codeCoverageIgnoreStart
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// @codeCoverageIgnoreEnd

// Ensure this file is loaded when Performance Lab plugin is active.
if ( ! function_exists( 'perflab_get_local_plugin_fallback_data' ) ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions should not be "pluggable". You can remove the if wrapper.


/**
* Gets local plugin information for Performance Lab standalone plugins.
*
* This function provides fallback data when external API requests to WordPress.org
* are disabled or fail, allowing the Performance Lab interface to still show
* cards for locally installed performance plugins.
*
* @since n.e.x.t
*
* @param string[] $plugin_slugs Array of plugin slugs to get local info for.
* @return array<string, array{name: string, slug: string, short_description: string, requires: string|false, requires_php: string|false, requires_plugins: string[], version: string, is_installed: bool, is_active: bool}> 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';
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since require_once will do nothing if the file was already included, I think this is sufficient:

Suggested change
// Ensure we have access to plugin functions.
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
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 ( false === $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.
$readme_description = perflab_get_plugin_readme_description( $plugin_file );
$description = '' !== $readme_description ? $readme_description : ( $plugin_headers['Description'] ?? '' );

$fallback_data[ $plugin_slug ] = array(
'name' => $plugin_headers['Name'] ?? $plugin_slug,
'slug' => $plugin_slug,
'short_description' => $description,
'requires' => $plugin_headers['RequiresWP'] ?? false,
'requires_php' => $plugin_headers['RequiresPHP'] ?? false,
'requires_plugins' => perflab_parse_requires_plugins( $plugin_headers, $plugin_slug ),
'version' => $plugin_headers['Version'] ?? '0.0.0',
'is_installed' => true,
'is_active' => $is_active,
);
}

return $fallback_data;
}

/**
* Finds the plugin file for a given slug among installed plugins.
*
* @since n.e.x.t
*
* @param array<string, array<string, string>> $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;
}

/**
* Gets plugin description from readme file.
*
* @since n.e.x.t
*
* @param string $plugin_file Plugin file path.
* @return string Plugin description from readme or empty string.
*/
function perflab_get_plugin_readme_description( string $plugin_file ): string {
if ( ! defined( 'WP_PLUGIN_DIR' ) ) {
return '';
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constant is always defined. No need for this if statement.

Suggested change
if ( ! defined( 'WP_PLUGIN_DIR' ) ) {
return '';
}

$plugin_dir = dirname( WP_PLUGIN_DIR . '/' . $plugin_file );
$readme_files = array( 'readme.txt', 'README.txt', 'readme.md', 'README.md' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plugins we feature all use readme.txt, so I think we can just use that.


foreach ( $readme_files as $readme_file ) {
$readme_path = $plugin_dir . '/' . $readme_file;
if ( file_exists( $readme_path ) ) {
$readme_content = file_get_contents( $readme_path );
if ( false !== $readme_content ) {
// Parse description from readme - look for description after "== Description ==".
if ( 1 === preg_match( '/==\s*Description\s*==(.*?)(?==|\z)/is', $readme_content, $matches ) ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this regular expression right? The description is the paragraph before the == Description ==. For example, it is line 10 here, not line 14:

=== Performance Lab ===
Contributors: wordpressdotorg
Tested up to: 6.8
Stable tag: 4.0.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Tags: performance, site health, measurement, optimization, diagnostics
Performance plugin from the WordPress Performance Team, which is a collection of standalone performance features.
== Description ==
The Performance Lab plugin is a collection of features focused on enhancing performance of your site, most of which should eventually be merged into WordPress core. The plugin facilitates the discovery and activation of the individual performance feature plugins which the performance team is developing. In this way you can test the features to get their benefits before they become available in WordPress core. You can also play an important role by providing feedback to further improve the solutions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two conditions can be combined.

$description = trim( $matches[1] );
// Remove markdown formatting and clean up.
$description = preg_replace( '/\*\*(.*?)\*\*/', '$1', $description ) ?? $description;
$description = preg_replace( '/\*(.*?)\*/', '$1', $description ) ?? $description;
$description = preg_replace( '/\[([^\]]+)\]\([^\)]+\)/', '$1', $description ) ?? $description;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Markdown isn't allowed in plugin descriptions, so these can be removed.

Suggested change
// Remove markdown formatting and clean up.
$description = preg_replace( '/\*\*(.*?)\*\*/', '$1', $description ) ?? $description;
$description = preg_replace( '/\*(.*?)\*/', '$1', $description ) ?? $description;
$description = preg_replace( '/\[([^\]]+)\]\([^\)]+\)/', '$1', $description ) ?? $description;

return trim( $description );
}
}
}
}

return '';
}

/**
* Sanitizes plugin description for display.
*
* @since n.e.x.t
*
* @param string $description Raw plugin description.
* @return string Sanitized description.
*/
function perflab_sanitize_plugin_description( string $description ): string {
if ( '' === $description ) {
return '';
}

// Strip all HTML tags and decode entities.
$description = wp_strip_all_tags( $description );
$description = html_entity_decode( $description, ENT_QUOTES, 'UTF-8' );

return trim( $description );
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function isn't used anymore.

Suggested change
/**
* Sanitizes plugin description for display.
*
* @since n.e.x.t
*
* @param string $description Raw plugin description.
* @return string Sanitized description.
*/
function perflab_sanitize_plugin_description( string $description ): string {
if ( '' === $description ) {
return '';
}
// Strip all HTML tags and decode entities.
$description = wp_strip_all_tags( $description );
$description = html_entity_decode( $description, ENT_QUOTES, 'UTF-8' );
return trim( $description );
}

/**
* Parses the requires_plugins from plugin headers.
*
* This attempts to extract required plugins from various possible header formats.
*
* @since n.e.x.t
*
* @param array<string, string> $plugin_headers Plugin headers array.
* @param string $plugin_slug Plugin slug to determine dependencies.
* @return string[] Array of required plugin slugs.
*/
function perflab_parse_requires_plugins( array $plugin_headers, string $plugin_slug ): array {
$requires_plugins = array();

// Check for RequiresPlugins header (WordPress 6.5+).
if ( isset( $plugin_headers['RequiresPlugins'] ) && '' !== $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.
// Embed Optimizer has a hard dependency on Optimization Detective.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Embed Optimizer has a hard dependency on Optimization Detective.
// For known Performance Lab plugins, add their specific dependencies.
// Embed Optimizer has a soft dependency on Optimization Detective.
if ( 'embed-optimizer' === $plugin_slug ) {
$requires_plugins[] = 'optimization-detective';
}

if ( 'embed-optimizer' === $plugin_slug ) {
$requires_plugins[] = 'optimization-detective';
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is now duplicated with:

// Add recommended plugins (soft dependencies) to the list of plugins installed and activated.
if ( 'embed-optimizer' === $plugin_slug ) {
$plugin_data['requires_plugins'][] = 'optimization-detective';
}

It would be good to extract this into a separate helper function.

In fact, this information could be added as part of perflab_get_standalone_plugin_data(), like as a new suggests_plugins key.


// Image Prioritizer has a hard dependency on Optimization Detective.
if ( 'image-prioritizer' === $plugin_slug ) {
$requires_plugins[] = 'optimization-detective';
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? It would be parsed out of RequiresPlugins, no?


return array_unique( array_filter( $requires_plugins ) );
}

/**
* Checks if external requests are blocked or likely to fail.
*
* @since n.e.x.t
*
* @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 ( '' === $allowed_hosts ) {
return true;
}

$allowed_hosts_array = array_map( 'trim', explode( ',', $allowed_hosts ) );
if ( ! in_array( 'wordpress.org', $allowed_hosts_array, true ) ) {
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 n.e.x.t
*
* @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 {
return perflab_get_plugin_settings_url( $plugin_slug );
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is no longer used.

Suggested change
/**
* Gets plugin settings URL for a given plugin slug.
*
* This attempts to determine the settings URL for locally installed performance plugins.
*
* @since n.e.x.t
*
* @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 {
return perflab_get_plugin_settings_url( $plugin_slug );
}


} // Close the function_exists check.
44 changes: 44 additions & 0 deletions plugins/performance-lab/includes/admin/plugins.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 n.e.x.t 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.
Expand All @@ -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',
Expand All @@ -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 ( count( $local_fallback_data ) > 0 ) {
// 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',
Expand All @@ -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 ( count( $local_fallback_data ) > 0 ) {
// 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',
Expand Down
1 change: 1 addition & 0 deletions plugins/performance-lab/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ function perflab_cleanup_option(): void {
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/load.php';
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/server-timing.php';
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/plugins.php';
require_once PERFLAB_PLUGIN_DIR_PATH . 'includes/admin/local-plugin-fallback.php';
}

// Load REST API.
Expand Down
Loading
Loading