From 93e6cb091076f1a7352ea678e17c23451805e98c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 23 Apr 2024 17:38:14 -0700 Subject: [PATCH 1/8] Implement initial dependency plugin checking for feature activation --- includes/admin/plugins.php | 111 +++++++++++++++++++++++++++++++------ 1 file changed, 94 insertions(+), 17 deletions(-) diff --git a/includes/admin/plugins.php b/includes/admin/plugins.php index 1f1544d297..a0b39e1249 100644 --- a/includes/admin/plugins.php +++ b/includes/admin/plugins.php @@ -150,6 +150,84 @@ function perflab_render_plugins_ui() { ( + ! $plugin_data['requires_php'] || + is_php_version_compatible( $plugin_data['requires_php'] ) + ), + 'compatible_wp' => ( + ! $plugin_data['requires'] || + is_wp_version_compatible( $plugin_data['requires'] ) + ), + 'dependency_errors' => array(), + ); + + $plugin_status = install_plugin_install_status( $plugin_data ); + + // The plugin is installed and the user can activate it, or the use can activate plugins in general. + $availability['can_activate'] = ( + ( $plugin_status['file'] && current_user_can( 'activate_plugin', $plugin_status['file'] ) ) || + current_user_can( 'activate_plugins' ) + ); + + // The plugin is already installed or the user can install plugins. + $availability['can_install'] = ( + 'install' !== $plugin_status['status'] || + current_user_can( 'install_plugins' ) + ); + + foreach ( $plugin_data['requires_plugins'] as $requires_plugin ) { + $dependency_plugin_data = perflab_query_plugin_info( $requires_plugin ); + if ( $dependency_plugin_data instanceof WP_Error ) { + $availability['dependency_errors'][] = sprintf( + /* translators: %s is plugin slug */ + __( 'Unable to look up dependency "%s".', 'performance-lab' ), + $requires_plugin + ); + continue; + } + + $dependency_availability = perflab_get_plugin_availability( $dependency_plugin_data ); + + $availability['dependency_errors'] = array_merge( + $availability['dependency_errors'], + $dependency_availability['dependency_errors'] + ); + if ( ! $dependency_availability['available'] ) { + $availability['dependency_errors'][] = sprintf( + /* translators: %s is a link to the plugin on the directory. */ + __( 'Dependency plugin %s is not available.', 'performance-lab' ), + sprintf( + '%s', + esc_url( + __( 'https://wordpress.org/plugins/', 'default' ) . $requires_plugin . '/' + ), + esc_html( $plugin_data['name'] ) + ) + ); + } + } + + $availability['available'] = ( + $availability['compatible_php'] && + $availability['compatible_wp'] && + $availability['can_activate'] && + $availability['can_install'] && + count( $availability['dependency_errors'] ) === 0 + ); + + return $availability; +} + /** * Renders individual plugin cards. * @@ -170,29 +248,21 @@ function perflab_render_plugin_card( array $plugin_data ) { /** This filter is documented in wp-admin/includes/class-wp-plugin-install-list-table.php */ $description = apply_filters( 'plugin_install_description', $description, $plugin_data ); - $compatible_php = ! $plugin_data['requires_php'] || is_php_version_compatible( $plugin_data['requires_php'] ); - $compatible_wp = ! $plugin_data['requires'] || is_wp_version_compatible( $plugin_data['requires'] ); - $action_links = array(); + $availability = perflab_get_plugin_availability( $plugin_data ); + + $compatible_php = $availability['compatible_php']; + $compatible_wp = $availability['compatible_wp']; + + $action_links = array(); - $status = install_plugin_install_status( $plugin_data ); + $status = install_plugin_install_status( $plugin_data ); // TODO: Incorporate active state into availability. if ( is_plugin_active( $status['file'] ) ) { $action_links[] = sprintf( '', esc_html( _x( 'Active', 'plugin', 'default' ) ) ); - } elseif ( - $compatible_php && - $compatible_wp && - ( - ( $status['file'] && current_user_can( 'activate_plugin', $status['file'] ) ) || - current_user_can( 'activate_plugins' ) - ) && - ( - 'install' !== $status['status'] || - current_user_can( 'install_plugins' ) - ) - ) { + } elseif ( $availability['available'] ) { $url = esc_url_raw( add_query_arg( array( @@ -210,7 +280,7 @@ function perflab_render_plugin_card( array $plugin_data ) { esc_html__( 'Activate', 'default' ) ); } else { - $explanation = 'install' !== $status['status'] || current_user_can( 'install_plugins' ) ? _x( 'Cannot Activate', 'plugin', 'default' ) : _x( 'Cannot Install', 'plugin', 'default' ); + $explanation = $availability['can_install'] ? _x( 'Cannot Activate', 'plugin', 'default' ) : _x( 'Cannot Install', 'plugin', 'default' ); $action_links[] = sprintf( '', esc_html( $explanation ) @@ -316,6 +386,13 @@ function perflab_render_plugin_card( array $plugin_data ) { } echo ''; } + if ( $availability['dependency_errors'] ) { + echo '

'; + foreach ( $availability['dependency_errors'] as $dependency_error ) { + echo wp_kses_post( $dependency_error ) . ' '; + } + echo '

'; + } ?>
From d9f2e231e0864978b7b111f514db4dcf504d7852 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 29 Apr 2024 11:53:11 -0700 Subject: [PATCH 2/8] Remove dependency_errors from availability to simplify --- includes/admin/plugins.php | 62 ++++++++------------------------------ 1 file changed, 13 insertions(+), 49 deletions(-) diff --git a/includes/admin/plugins.php b/includes/admin/plugins.php index a0b39e1249..ca6f8aa82c 100644 --- a/includes/admin/plugins.php +++ b/includes/admin/plugins.php @@ -156,75 +156,46 @@ function perflab_render_plugins_ui() { * @since n.e.x.t * * @param array{name: string, slug: string, short_description: string, requires_php: string|false, requires: string|false, requires_plugins: string[], version: string} $plugin_data Plugin data from the WordPress.org API. - * @return array{compatible_php: bool, compatible_wp: bool, can_install: bool, can_activate: bool, dependency_errors: string[], available: bool} Availability. + * @return array{compatible_php: bool, compatible_wp: bool, can_install: bool, can_activate: bool} Availability. */ function perflab_get_plugin_availability( array $plugin_data ): array { $availability = array( - 'compatible_php' => ( + 'compatible_php' => ( ! $plugin_data['requires_php'] || is_php_version_compatible( $plugin_data['requires_php'] ) ), - 'compatible_wp' => ( + 'compatible_wp' => ( ! $plugin_data['requires'] || is_wp_version_compatible( $plugin_data['requires'] ) ), - 'dependency_errors' => array(), ); $plugin_status = install_plugin_install_status( $plugin_data ); - // The plugin is installed and the user can activate it, or the use can activate plugins in general. - $availability['can_activate'] = ( - ( $plugin_status['file'] && current_user_can( 'activate_plugin', $plugin_status['file'] ) ) || - current_user_can( 'activate_plugins' ) - ); - // The plugin is already installed or the user can install plugins. $availability['can_install'] = ( 'install' !== $plugin_status['status'] || current_user_can( 'install_plugins' ) ); + // The plugin is installed and the user can activate it, or the use can activate plugins in general. + $availability['can_activate'] = ( + ( $plugin_status['file'] && current_user_can( 'activate_plugin', $plugin_status['file'] ) ) || + current_user_can( 'activate_plugins' ) + ); + foreach ( $plugin_data['requires_plugins'] as $requires_plugin ) { $dependency_plugin_data = perflab_query_plugin_info( $requires_plugin ); if ( $dependency_plugin_data instanceof WP_Error ) { - $availability['dependency_errors'][] = sprintf( - /* translators: %s is plugin slug */ - __( 'Unable to look up dependency "%s".', 'performance-lab' ), - $requires_plugin - ); continue; } $dependency_availability = perflab_get_plugin_availability( $dependency_plugin_data ); - - $availability['dependency_errors'] = array_merge( - $availability['dependency_errors'], - $dependency_availability['dependency_errors'] - ); - if ( ! $dependency_availability['available'] ) { - $availability['dependency_errors'][] = sprintf( - /* translators: %s is a link to the plugin on the directory. */ - __( 'Dependency plugin %s is not available.', 'performance-lab' ), - sprintf( - '%s', - esc_url( - __( 'https://wordpress.org/plugins/', 'default' ) . $requires_plugin . '/' - ), - esc_html( $plugin_data['name'] ) - ) - ); + foreach ( array( 'compatible_php', 'compatible_wp', 'can_install', 'can_activate' ) as $key ) { + $availability[ $key ] = $availability[ $key ] && $dependency_availability[ $key ]; } } - $availability['available'] = ( - $availability['compatible_php'] && - $availability['compatible_wp'] && - $availability['can_activate'] && - $availability['can_install'] && - count( $availability['dependency_errors'] ) === 0 - ); - return $availability; } @@ -255,14 +226,14 @@ function perflab_render_plugin_card( array $plugin_data ) { $action_links = array(); - $status = install_plugin_install_status( $plugin_data ); // TODO: Incorporate active state into availability. + $status = install_plugin_install_status( $plugin_data ); if ( is_plugin_active( $status['file'] ) ) { $action_links[] = sprintf( '', esc_html( _x( 'Active', 'plugin', 'default' ) ) ); - } elseif ( $availability['available'] ) { + } elseif ( ! in_array( false, $availability, true ) ) { $url = esc_url_raw( add_query_arg( array( @@ -386,13 +357,6 @@ function perflab_render_plugin_card( array $plugin_data ) { } echo '
'; } - if ( $availability['dependency_errors'] ) { - echo '

'; - foreach ( $availability['dependency_errors'] as $dependency_error ) { - echo wp_kses_post( $dependency_error ) . ' '; - } - echo '

'; - } ?>
From d7389a88125e8187e480259f199516418cb86d6c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 29 Apr 2024 13:49:47 -0700 Subject: [PATCH 3/8] Implement recursive installation and activation of plugin plus dependencies --- includes/admin/load.php | 65 ++-------------------------------- includes/admin/plugins.php | 71 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 62 deletions(-) diff --git a/includes/admin/load.php b/includes/admin/load.php index 0701c29837..5d48d9c8b8 100644 --- a/includes/admin/load.php +++ b/includes/admin/load.php @@ -248,68 +248,9 @@ function perflab_install_activate_plugin_callback() { wp_die( esc_html__( 'Invalid plugin.', 'performance-lab' ) ); } - // Check if plugin (by slug) is installed by obtaining the plugin file. - // Remember a plugin file typically looks like "{slug}/load.php" or "{slug}/{slug}.php". - $plugin_file = null; - foreach ( array_keys( get_plugins() ) as $installed_plugin_file ) { - if ( strtok( $installed_plugin_file, '/' ) === $plugin_slug ) { - $plugin_file = $installed_plugin_file; - break; - } - } - - // Install the plugin if it is not installed yet (in which case the plugin file could not be discovered above). - if ( ! isset( $plugin_file ) ) { - // Check if the user have plugin installation capability. - if ( ! current_user_can( 'install_plugins' ) ) { - wp_die( esc_html__( 'Sorry, you are not allowed to install plugins on this site.', 'default' ) ); - } - - $api = perflab_query_plugin_info( $plugin_slug ); - - // Return early if plugin API returns an error. - if ( $api instanceof WP_Error ) { - wp_die( - wp_kses( - sprintf( - /* translators: %s: Support forums URL. */ - __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the support forums.', 'default' ), - __( 'https://wordpress.org/support/forums/', 'default' ) - ) . ' ' . $api->get_error_message(), - array( 'a' => array( 'href' => true ) ) - ) - ); - } - - // Replace new Plugin_Installer_Skin with new Quiet_Upgrader_Skin when output needs to be suppressed. - $skin = new WP_Ajax_Upgrader_Skin( array( 'api' => $api ) ); - $upgrader = new Plugin_Upgrader( $skin ); - $result = $upgrader->install( $api['download_link'] ); - - if ( is_wp_error( $result ) ) { - wp_die( esc_html( $result->get_error_message() ) ); - } elseif ( is_wp_error( $skin->result ) ) { - wp_die( esc_html( $skin->result->get_error_message() ) ); - } elseif ( $skin->get_errors()->has_errors() ) { - wp_die( esc_html( $skin->get_error_messages() ) ); - } - - $plugins = get_plugins( '/' . $plugin_slug ); - - if ( empty( $plugins ) ) { - wp_die( esc_html__( 'Plugin not found.', 'default' ) ); - } - - $plugin_file_names = array_keys( $plugins ); - $plugin_file = $plugin_slug . '/' . $plugin_file_names[0]; - } - - if ( ! current_user_can( 'activate_plugin', $plugin_file ) ) { - wp_die( esc_html__( 'Sorry, you are not allowed to activate this plugin.', 'default' ) ); - } - - $result = activate_plugin( $plugin_file ); - if ( is_wp_error( $result ) ) { + // Install and activate the plugin and its dependencies. + $result = perflab_install_and_activate_plugin( $plugin_slug ); + if ( $result instanceof WP_Error ) { wp_die( wp_kses_post( $result->get_error_message() ) ); } diff --git a/includes/admin/plugins.php b/includes/admin/plugins.php index ca6f8aa82c..d249f4e0a6 100644 --- a/includes/admin/plugins.php +++ b/includes/admin/plugins.php @@ -154,6 +154,7 @@ function perflab_render_plugins_ui() { * Checks if a given plugin is available. * * @since n.e.x.t + * @see perflab_install_and_activate_plugin() * * @param array{name: string, slug: string, short_description: string, requires_php: string|false, requires: string|false, requires_plugins: string[], version: string} $plugin_data Plugin data from the WordPress.org API. * @return array{compatible_php: bool, compatible_wp: bool, can_install: bool, can_activate: bool} Availability. @@ -199,6 +200,76 @@ function perflab_get_plugin_availability( array $plugin_data ): array { return $availability; } +/** + * Installs and activates a plugin by its slug. + * + * Dependencies are recursively installed as well. + * + * @since n.e.x.t + * @see perflab_get_plugin_availability() + * + * @param string $plugin_slug Plugin slug. + * @return WP_Error|null WP_Error on failure. + */ +function perflab_install_and_activate_plugin( string $plugin_slug ): ?WP_Error { + $plugin_data = perflab_query_plugin_info( $plugin_slug ); + if ( $plugin_data instanceof WP_Error ) { + return $plugin_data; + } + + // Install and activate plugin dependencies first. + foreach ( $plugin_data['requires_plugins'] as $requires_plugin_slug ) { + $result = perflab_install_and_activate_plugin( $requires_plugin_slug ); + if ( $result instanceof WP_Error ) { + return $result; + } + } + + // Install the plugin. + $plugin_status = install_plugin_install_status( $plugin_data ); + $plugin_file = $plugin_status['file']; + if ( 'install' === $plugin_status['status'] ) { + if ( ! current_user_can( 'install_plugins' ) ) { + return new WP_Error( 'cannot_install_plugin', __( 'Sorry, you are not allowed to install plugins on this site.', 'default' ) ); + } + + // Replace new Plugin_Installer_Skin with new Quiet_Upgrader_Skin when output needs to be suppressed. + $skin = new WP_Ajax_Upgrader_Skin( array( 'api' => $plugin_data ) ); + $upgrader = new Plugin_Upgrader( $skin ); + $result = $upgrader->install( $plugin_data['download_link'] ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( is_wp_error( $skin->result ) ) { + return $skin->result; + } elseif ( $skin->get_errors()->has_errors() ) { + return $skin->get_errors(); + } + + $plugins = get_plugins( '/' . $plugin_slug ); + if ( empty( $plugins ) ) { + return new WP_Error( 'plugin_not_found', __( 'Plugin not found.', 'default' ) ); + } + + $plugin_file_names = array_keys( $plugins ); + $plugin_file = $plugin_slug . '/' . $plugin_file_names[0]; + } + + // Activate the plugin. + if ( ! is_plugin_active( $plugin_file ) ) { + if ( ! current_user_can( 'activate_plugin', $plugin_file ) ) { + return new WP_Error( 'cannot_activate_plugin', __( 'Sorry, you are not allowed to activate this plugin.', 'default' ) ); + } + + $result = activate_plugin( $plugin_file ); + if ( $result instanceof WP_Error ) { + return $result; + } + } + + return null; +} + /** * Renders individual plugin cards. * From 16fa7f074fcc685f15582aa272e2f6d2708e4770 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 29 Apr 2024 13:56:49 -0700 Subject: [PATCH 4/8] Ignore IDE complaints about default text domain --- includes/admin/plugins.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/admin/plugins.php b/includes/admin/plugins.php index d249f4e0a6..7f17e44022 100644 --- a/includes/admin/plugins.php +++ b/includes/admin/plugins.php @@ -3,6 +3,7 @@ * Admin settings helper functions. * * @package performance-lab + * @noinspection PhpRedundantOptionalArgumentInspection */ if ( ! defined( 'ABSPATH' ) ) { From 19f1ad54d36039e3d41887de42cc9488d795044a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 29 Apr 2024 14:10:11 -0700 Subject: [PATCH 5/8] Improve phpdoc --- includes/admin/plugins.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/admin/plugins.php b/includes/admin/plugins.php index 7f17e44022..923cf8bde0 100644 --- a/includes/admin/plugins.php +++ b/includes/admin/plugins.php @@ -204,7 +204,7 @@ function perflab_get_plugin_availability( array $plugin_data ): array { /** * Installs and activates a plugin by its slug. * - * Dependencies are recursively installed as well. + * Dependencies are recursively installed and activated as well. * * @since n.e.x.t * @see perflab_get_plugin_availability() From 4cd54c8566a95364ddd8e2f86c6f288c1939a4ee Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 30 Apr 2024 08:03:40 -0700 Subject: [PATCH 6/8] Fix typo Co-authored-by: Mukesh Panchal --- includes/admin/plugins.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/admin/plugins.php b/includes/admin/plugins.php index 923cf8bde0..d390606837 100644 --- a/includes/admin/plugins.php +++ b/includes/admin/plugins.php @@ -180,7 +180,7 @@ function perflab_get_plugin_availability( array $plugin_data ): array { current_user_can( 'install_plugins' ) ); - // The plugin is installed and the user can activate it, or the use can activate plugins in general. + // The plugin is installed and the user can activate it, or the user can activate plugins in general. $availability['can_activate'] = ( ( $plugin_status['file'] && current_user_can( 'activate_plugin', $plugin_status['file'] ) ) || current_user_can( 'activate_plugins' ) From 7a5c9b08e1f481208d9d578acb4228cbd390ca33 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 1 May 2024 11:15:13 -0700 Subject: [PATCH 7/8] Ensure feature can be activated when missing dependencies --- includes/admin/plugins.php | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/includes/admin/plugins.php b/includes/admin/plugins.php index d390606837..8b0801f732 100644 --- a/includes/admin/plugins.php +++ b/includes/admin/plugins.php @@ -158,7 +158,7 @@ function perflab_render_plugins_ui() { * @see perflab_install_and_activate_plugin() * * @param array{name: string, slug: string, short_description: string, requires_php: string|false, requires: string|false, requires_plugins: string[], version: string} $plugin_data Plugin data from the WordPress.org API. - * @return array{compatible_php: bool, compatible_wp: bool, can_install: bool, can_activate: bool} Availability. + * @return array{compatible_php: bool, compatible_wp: bool, can_install: bool, can_activate: bool, activated: bool, installed: bool} Availability. */ function perflab_get_plugin_availability( array $plugin_data ): array { $availability = array( @@ -174,16 +174,21 @@ function perflab_get_plugin_availability( array $plugin_data ): array { $plugin_status = install_plugin_install_status( $plugin_data ); + $availability['installed'] = ( 'install' !== $plugin_status['status'] ); + $availability['activated'] = $plugin_status['file'] && is_plugin_active( $plugin_status['file'] ); + // The plugin is already installed or the user can install plugins. $availability['can_install'] = ( - 'install' !== $plugin_status['status'] || + $availability['installed'] || current_user_can( 'install_plugins' ) ); - // The plugin is installed and the user can activate it, or the user can activate plugins in general. + // The plugin is activated or the user can activate plugins. $availability['can_activate'] = ( - ( $plugin_status['file'] && current_user_can( 'activate_plugin', $plugin_status['file'] ) ) || - current_user_can( 'activate_plugins' ) + $availability['activated'] || + $plugin_status['file'] // When not false, the plugin is installed. + ? current_user_can( 'activate_plugin', $plugin_status['file'] ) + : current_user_can( 'activate_plugins' ) ); foreach ( $plugin_data['requires_plugins'] as $requires_plugin ) { @@ -193,7 +198,7 @@ function perflab_get_plugin_availability( array $plugin_data ): array { } $dependency_availability = perflab_get_plugin_availability( $dependency_plugin_data ); - foreach ( array( 'compatible_php', 'compatible_wp', 'can_install', 'can_activate' ) as $key ) { + foreach ( array( 'compatible_php', 'compatible_wp', 'can_install', 'can_activate', 'installed', 'activated' ) as $key ) { $availability[ $key ] = $availability[ $key ] && $dependency_availability[ $key ]; } } @@ -298,14 +303,17 @@ function perflab_render_plugin_card( array $plugin_data ) { $action_links = array(); - $status = install_plugin_install_status( $plugin_data ); - - if ( is_plugin_active( $status['file'] ) ) { + if ( $availability['activated'] ) { $action_links[] = sprintf( '', esc_html( _x( 'Active', 'plugin', 'default' ) ) ); - } elseif ( ! in_array( false, $availability, true ) ) { + } elseif ( + $availability['compatible_php'] && + $availability['compatible_wp'] && + $availability['can_install'] && + $availability['can_activate'] + ) { $url = esc_url_raw( add_query_arg( array( From 1362cefbef375173006afab76afc3da53dfdb29a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 1 May 2024 13:40:27 -0700 Subject: [PATCH 8/8] Account for possible recursive circular dependencies --- includes/admin/plugins.php | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/includes/admin/plugins.php b/includes/admin/plugins.php index 8b0801f732..865be630d7 100644 --- a/includes/admin/plugins.php +++ b/includes/admin/plugins.php @@ -157,10 +157,16 @@ function perflab_render_plugins_ui() { * @since n.e.x.t * @see perflab_install_and_activate_plugin() * - * @param array{name: string, slug: string, short_description: string, requires_php: string|false, requires: string|false, requires_plugins: string[], version: string} $plugin_data Plugin data from the WordPress.org API. + * @param array{name: string, slug: string, short_description: string, requires_php: string|false, requires: string|false, requires_plugins: string[], version: string} $plugin_data Plugin data from the WordPress.org API. + * @param array $processed_plugin_availabilities Plugin availabilities already processed. This param is only used by recursive calls. * @return array{compatible_php: bool, compatible_wp: bool, can_install: bool, can_activate: bool, activated: bool, installed: bool} Availability. */ -function perflab_get_plugin_availability( array $plugin_data ): array { +function perflab_get_plugin_availability( array $plugin_data, array &$processed_plugin_availabilities = array() ): array { + if ( array_key_exists( $plugin_data['slug'], $processed_plugin_availabilities ) ) { + // Prevent infinite recursion by returning the previously-computed value. + return $processed_plugin_availabilities[ $plugin_data['slug'] ]; + } + $availability = array( 'compatible_php' => ( ! $plugin_data['requires_php'] || @@ -191,6 +197,9 @@ function perflab_get_plugin_availability( array $plugin_data ): array { : current_user_can( 'activate_plugins' ) ); + // Store pending availability before recursing. + $processed_plugin_availabilities[ $plugin_data['slug'] ] = $availability; + foreach ( $plugin_data['requires_plugins'] as $requires_plugin ) { $dependency_plugin_data = perflab_query_plugin_info( $requires_plugin ); if ( $dependency_plugin_data instanceof WP_Error ) { @@ -203,6 +212,7 @@ function perflab_get_plugin_availability( array $plugin_data ): array { } } + $processed_plugin_availabilities[ $plugin_data['slug'] ] = $availability; return $availability; } @@ -214,10 +224,17 @@ function perflab_get_plugin_availability( array $plugin_data ): array { * @since n.e.x.t * @see perflab_get_plugin_availability() * - * @param string $plugin_slug Plugin slug. + * @param string $plugin_slug Plugin slug. + * @param string[] $processed_plugins Slugs for plugins which have already been processed. This param is only used by recursive calls. * @return WP_Error|null WP_Error on failure. */ -function perflab_install_and_activate_plugin( string $plugin_slug ): ?WP_Error { +function perflab_install_and_activate_plugin( string $plugin_slug, array &$processed_plugins = array() ): ?WP_Error { + if ( in_array( $plugin_slug, $processed_plugins, true ) ) { + // Prevent infinite recursion from possible circular dependency. + return null; + } + $processed_plugins[] = $plugin_slug; + $plugin_data = perflab_query_plugin_info( $plugin_slug ); if ( $plugin_data instanceof WP_Error ) { return $plugin_data;