Add handling for plugin-feature incompatibilities (#34879)

* Extend FeaturesController to handle WooCommerce-aware plugins

The methods that return compatibility info now have an extra
'uncertain' part with information regarding plugins that are
WooCommerce-aware but haven't declared compatibility.

* Add a warning about incompatible plugins in the features page.

Includes a link to the list of incompatible plugins.

* Add handling of incompatible plugins in the plugins page.

Plugins that are incompatible with at least one enabled feature
will show a warning in the WooCommerce plugins page, and will
have their "Activate" link disabled.

* - Hook on 'views_plugins' to display two views in plugins page,
  "All" and "Incompatible with feature X"

- Exclude the legacy Analytics features from the feature and plugins
  activation restrictions

- Allow disabling a feature from the settings pages if it's enabled
  and is incompatible with at least one plugin (it won't be possible
  to re-enable it once the settings page reloads)

* Fix FeaturesController::declare_compatibility not working in Windows
(which uses \ instead of / as directory separator)

* - Add two methods to bypass the feature/plugin activation protection.

- Fix: the incompatible plugins count in the feature settings page
  now only counts active plugins.

* Add changelog file

* Fix unit tests

* - Rename "custom orders table" feature to "high performance order tables"

- Add an extra parameter to FeaturesController::get_compatible_plugins_for_feature
  to retrieve all matching plugins or only active plugins.

* Minor wording fixes

* Address PR feedback.

* Allow enabling plugins when WP_DEBUG is true.

* Return if plugin_status is not set.

* Dont change the Activate button.

Co-authored-by: Vedanshu Jain <vedanshu.jain.2012@gmail.com>
This commit is contained in:
Néstor Soriano 2022-10-12 16:08:46 +02:00 committed by GitHub
parent a9dbb6d64e
commit edc1c6c98a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 942 additions and 58 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add handling for plugin-feature incompatibilities

View File

@ -515,6 +515,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
type="checkbox"
class="<?php echo esc_attr( isset( $value['class'] ) ? $value['class'] : '' ); ?>"
value="1"
<?php disabled( $value['disabled'] ?? true ); ?>
<?php checked( $option_value, 'yes' ); ?>
<?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?>
/> <?php echo $description; // WPCS: XSS ok. ?>

View File

@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\PluginUtil;
/**
* Service provider for the features enabling/disabling/compatibility engine.
@ -24,6 +25,7 @@ class FeaturesServiceProvider extends AbstractServiceProvider {
* Register the classes.
*/
public function register() {
$this->share( FeaturesController::class )->addArgument( LegacyProxy::class );
$this->share( FeaturesController::class )
->addArguments( array( LegacyProxy::class, PluginUtil::class ) );
}
}

View File

@ -11,6 +11,8 @@ use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider
use Automattic\WooCommerce\Internal\Utilities\COTMigrationUtil;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Internal\Utilities\HtmlSanitizer;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\PluginUtil;
use Automattic\WooCommerce\Utilities\OrderUtil;
/**
@ -27,6 +29,7 @@ class UtilsClassesServiceProvider extends AbstractServiceProvider {
DatabaseUtil::class,
HtmlSanitizer::class,
OrderUtil::class,
PluginUtil::class,
COTMigrationUtil::class,
);
@ -37,6 +40,8 @@ class UtilsClassesServiceProvider extends AbstractServiceProvider {
$this->share( DatabaseUtil::class );
$this->share( HtmlSanitizer::class );
$this->share( OrderUtil::class );
$this->share( PluginUtil::class )
->addArgument( LegacyProxy::class );
$this->share( COTMigrationUtil::class )
->addArguments( array( CustomOrdersTableController::class, DataSynchronizer::class ) );
}

View File

@ -5,11 +5,13 @@
namespace Automattic\WooCommerce\Internal\Features;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\Admin\Analytics;
use Automattic\WooCommerce\Admin\Features\Navigation\Init;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\PluginUtil;
defined( 'ABSPATH' ) || exit;
@ -38,6 +40,13 @@ class FeaturesController {
*/
private $compatibility_info_by_plugin;
/**
* Ids of the legacy features (they existed before the features engine was implemented).
*
* @var array
*/
private $legacy_feature_ids;
/**
* The registered compatibility info for WooCommerce plugins, with feature ids as keys.
*
@ -52,6 +61,29 @@ class FeaturesController {
*/
private $proxy;
/**
* The PluginUtil instance to use.
*
* @var PluginUtil
*/
private $plugin_util;
/**
* Flag indicating that features will be enableable from the settings page
* even when they are incompatible with active plugins.
*
* @var bool
*/
private $force_allow_enabling_features = false;
/**
* Flag indicating that plugins will be activable from the plugins page
* even when they are incompatible with enabled features.
*
* @var bool
*/
private $force_allow_enabling_plugins = false;
/**
* Creates a new instance of the class.
*/
@ -69,25 +101,26 @@ class FeaturesController {
'is_experimental' => false,
),
'custom_order_tables' => array(
'name' => __( 'Custom order tables', 'woocommerce' ),
'description' => __( 'Enable the custom orders tables feature (still in development)', 'woocommerce' ),
'name' => __( 'High-Performance order storage (COT)', 'woocommerce' ),
'description' => __( 'Enable the high performance order storage feature.', 'woocommerce' ),
'is_experimental' => true,
),
);
$this->init_features( $features );
$this->legacy_feature_ids = array( 'analytics', 'new_navigation' );
foreach ( array_keys( $this->features ) as $feature_id ) {
$this->compatibility_info_by_feature[ $feature_id ] = array(
'compatible' => array(),
'incompatible' => array(),
);
}
$this->init_features( $features );
self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
self::add_filter( 'added_option', array( $this, 'process_added_option' ), 999, 3 );
self::add_filter( 'woocommerce_get_sections_advanced', array( $this, 'add_features_section' ), 10, 1 );
self::add_filter( 'woocommerce_get_settings_advanced', array( $this, 'add_feature_settings' ), 10, 2 );
self::add_filter( 'deactivated_plugin', array( $this, 'handle_plugin_deactivation' ), 10, 1 );
self::add_filter( 'all_plugins', array( $this, 'filter_plugins_list' ), 10, 1 );
self::add_action( 'admin_notices', array( $this, 'display_notice_in_plugins_page' ), 10, 0 );
self::add_action( 'after_plugin_row', array( $this, 'handle_plugin_list_rows' ), 10, 2 );
self::add_action( 'current_screen', array( $this, 'enqueue_script_to_fix_plugin_list_html' ), 10, 1 );
self::add_filter( 'views_plugins', array( $this, 'handle_plugins_page_views_list' ), 10, 1 );
}
/**
@ -115,9 +148,11 @@ class FeaturesController {
* @internal
*
* @param LegacyProxy $proxy The instance of LegacyProxy to use.
* @param PluginUtil $plugin_util The instance of PluginUtil to use.
*/
final public function init( LegacyProxy $proxy ) {
$this->proxy = $proxy;
final public function init( LegacyProxy $proxy, PluginUtil $plugin_util ) {
$this->proxy = $proxy;
$this->plugin_util = $plugin_util;
}
/**
@ -224,6 +259,8 @@ class FeaturesController {
return false;
}
$plugin_name = str_replace( '\\', '/', $plugin_name );
// Register compatibility by plugin.
ArrayUtil::ensure_key_is_array( $this->compatibility_info_by_plugin, $plugin_name );
@ -268,52 +305,80 @@ class FeaturesController {
* This method can't be called before the 'woocommerce_init' hook is fired.
*
* @param string $plugin_name Plugin name, in the form 'directory/file.php'.
* @param bool $enabled_features_only True to return only names of enabled plugins.
* @return array An array having a 'compatible' and an 'incompatible' key, each holding an array of feature ids.
*/
public function get_compatible_features_for_plugin( string $plugin_name ) : array {
public function get_compatible_features_for_plugin( string $plugin_name, bool $enabled_features_only = false ) : array {
$this->verify_did_woocommerce_init( __FUNCTION__ );
$features = $this->features;
if ( $enabled_features_only ) {
$features = array_filter(
$features,
array( $this, 'feature_is_enabled' ),
ARRAY_FILTER_USE_KEY
);
}
if ( ! isset( $this->compatibility_info_by_plugin[ $plugin_name ] ) ) {
return array(
'compatible' => array(),
'incompatible' => array(),
'uncertain' => array_keys( $features ),
);
}
return $this->compatibility_info_by_plugin[ $plugin_name ];
$info = $this->compatibility_info_by_plugin[ $plugin_name ];
$info['compatible'] = array_values( array_intersect( array_keys( $features ), $info['compatible'] ) );
$info['incompatible'] = array_values( array_intersect( array_keys( $features ), $info['incompatible'] ) );
$info['uncertain'] = array_values( array_diff( array_keys( $features ), $info['compatible'], $info['incompatible'] ) );
return $info;
}
/**
* Get the names of the plugins that have been declared compatible or incompatible with a given feature.
*
* @param string $feature_id Feature id.
* @param bool $active_only True to return only active plugins.
* @return array An array having a 'compatible' and an 'incompatible' key, each holding an array of plugin names.
*/
public function get_compatible_plugins_for_feature( string $feature_id ) : array {
public function get_compatible_plugins_for_feature( string $feature_id, bool $active_only = false ) : array {
$this->verify_did_woocommerce_init( __FUNCTION__ );
$woo_aware_plugins = $this->plugin_util->get_woocommerce_aware_plugins( $active_only );
if ( ! $this->feature_exists( $feature_id ) ) {
return array(
'compatible' => array(),
'incompatible' => array(),
'uncertain' => $woo_aware_plugins,
);
}
return $this->compatibility_info_by_feature[ $feature_id ];
$info = $this->compatibility_info_by_feature[ $feature_id ];
$info['uncertain'] = array_values( array_diff( $woo_aware_plugins, $info['compatible'], $info['incompatible'] ) );
return $info;
}
/**
* Check if the 'woocommerce_init' has run or is running, do a 'wc_doing_it_wrong' if not.
*
* @param string $function Name of the invoking method.
* @param string|null $function Name of the invoking method, if not null, 'wc_doing_it_wrong' will be invoked if 'woocommerce_init' has not run and is not running.
* @return bool True if 'woocommerce_init' has run or is running, false otherwise.
*/
private function verify_did_woocommerce_init( string $function ) {
private function verify_did_woocommerce_init( string $function = null ): bool {
if ( ! $this->proxy->call_function( 'did_action', 'woocommerce_init' ) &&
! $this->proxy->call_function( 'doing_action', 'woocommerce_init' ) ) {
$class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . $function;
/* translators: 1: class::method 2: plugins_loaded */
$this->proxy->call_function( 'wc_doing_it_wrong', $class_and_method, sprintf( __( '%1$s should not be called before the %2$s action.', 'woocommerce' ), $class_and_method, 'woocommerce_init' ), '7.0' );
if ( ! is_null( $function ) ) {
$class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . $function;
/* translators: 1: class::method 2: plugins_loaded */
$this->proxy->call_function( 'wc_doing_it_wrong', $class_and_method, sprintf( __( '%1$s should not be called before the %2$s action.', 'woocommerce' ), $class_and_method, 'woocommerce_init' ), '7.0' );
}
return false;
}
return true;
}
/**
@ -333,6 +398,33 @@ class FeaturesController {
return "woocommerce_feature_${feature_id}_enabled";
}
/**
* Checks whether a feature id corresponds to a legacy feature
* (a feature that existed prior to the implementation of the features engine).
*
* @param string $feature_id The feature id to check.
* @return bool True if the id corresponds to a legacy feature.
*/
public function is_legacy_feature( string $feature_id ): bool {
return in_array( $feature_id, $this->legacy_feature_ids, true );
}
/**
* Sets a flag indicating that it's allowed to enable features for which incompatible plugins are active
* from the WooCommerce feature settings page.
*/
public function allow_enabling_features_with_incompatible_plugins(): void {
$this->force_allow_enabling_features = true;
}
/**
* Sets a flag indicating that it's allowed to activate plugins for which incompatible features are enabled
* from the WordPress plugins page.
*/
public function allow_activating_plugins_with_incompatible_features(): void {
$this->force_allow_enabling_plugins = true;
}
/**
* Handler for the 'added_option' hook.
*
@ -358,7 +450,7 @@ class FeaturesController {
$matches = array();
$success = preg_match( '/^woocommerce_feature_([a-zA-Z0-9_]+)_enabled$/', $option, $matches );
if ( ! $success ) {
if ( ! $success && Analytics::TOGGLE_OPTION_NAME !== $option && Init::TOGGLE_OPTION_NAME !== $option ) {
return;
}
@ -366,7 +458,13 @@ class FeaturesController {
return;
}
$feature_id = $matches[1];
if ( Analytics::TOGGLE_OPTION_NAME === $option ) {
$feature_id = 'analytics';
} elseif ( Init::TOGGLE_OPTION_NAME === $option ) {
$feature_id = 'new_navigation';
} else {
$feature_id = $matches[1];
}
/**
* Action triggered when a feature is enabled or disabled (the value of the corresponding setting option is changed).
@ -511,14 +609,327 @@ class FeaturesController {
}
}
if ( ! $this->is_legacy_feature( $feature_id ) && ! $disabled && $this->verify_did_woocommerce_init() ) {
$plugin_info_for_feature = $this->get_compatible_plugins_for_feature( $feature_id, true );
$incompatibles = array_merge( $plugin_info_for_feature['incompatible'], $plugin_info_for_feature['uncertain'] );
$incompatibles = array_filter( $incompatibles, 'is_plugin_active' );
$incompatible_count = count( $incompatibles );
if ( $incompatible_count > 0 ) {
if ( 1 === $incompatible_count ) {
/* translators: %s = printable plugin name */
$desc_tip = sprintf( __( "⚠ This feature shouldn't be enabled, the %s plugin is active and isn't compatible with it.", 'woocommerce' ), $this->plugin_util->get_plugin_name( $incompatibles[0] ) );
} elseif ( 2 === $incompatible_count ) {
/* translators: %1\$s, %2\$s = printable plugin names */
$desc_tip = sprintf(
__( "⚠ This feature shouldn't be enabled: the %1\$s and %2\$s plugins are active and aren't compatible with it.", 'woocommerce' ),
$this->plugin_util->get_plugin_name( $incompatibles[0] ),
$this->plugin_util->get_plugin_name( $incompatibles[1] )
);
} else {
/* translators: %1\$s, %2\$s = printable plugin names, %3\$d = plugins count */
$desc_tip = sprintf(
_n(
"⚠ This feature shouldn't be enabled: %1\$s, %2\$s and %3\$d more active plugin isn't compatible with it",
"⚠ This feature shouldn't be enabled: the %1\$s and %2\$s plugins are active and aren't compatible with it. There are %3\$d other incompatible plugins.",
$incompatible_count - 2,
'woocommerce'
),
$this->plugin_util->get_plugin_name( $incompatibles[0] ),
$this->plugin_util->get_plugin_name( $incompatibles[1] ),
$incompatible_count - 2
);
}
$incompatible_plugins_url = add_query_arg(
array(
'plugin_status' => 'incompatible_with_feature',
'feature_id' => $feature_id,
),
admin_url( 'plugins.php' )
);
/* translators: %s = URL of the plugins page */
$extra_desc_tip = sprintf( __( " <a href='%s'>Manage incompatible plugins</a>", 'woocommerce' ), $incompatible_plugins_url );
$desc_tip .= $extra_desc_tip;
$disabled = ! $this->feature_is_enabled( $feature_id );
}
}
return array(
'title' => $feature['name'],
'desc' => $description,
'type' => 'checkbox',
'id' => $this->feature_enable_option_name( $feature_id ),
'class' => $disabled ? 'disabled' : '',
'disabled' => $disabled && ! $this->force_allow_enabling_features,
'desc_tip' => $desc_tip,
'default' => $this->feature_is_enabled_by_default( $feature_id ) ? 'yes' : 'no',
);
}
/**
* Handle the plugin deactivation hook.
*
* @param string $plugin_name Name of the plugin that has been deactivated.
*/
private function handle_plugin_deactivation( $plugin_name ): void {
unset( $this->compatibility_info_by_plugin[ $plugin_name ] );
foreach ( array_keys( $this->compatibility_info_by_feature ) as $feature ) {
$compatibles = $this->compatibility_info_by_feature[ $feature ]['compatible'];
$this->compatibility_info_by_feature[ $feature ]['compatible'] = array_diff( $compatibles, array( $plugin_name ) );
$incompatibles = $this->compatibility_info_by_feature[ $feature ]['incompatible'];
$this->compatibility_info_by_feature[ $feature ]['incompatible'] = array_diff( $incompatibles, array( $plugin_name ) );
}
}
/**
* Handler for the all_plugins filter.
*
* Returns the list of plugins incompatible with a given plugin
* if we are in the plugins page and the query string of the current request
* looks like '?plugin_status=incompatible_with_feature&feature_id=<feature id>'.
*
* @param array $list The original list of plugins.
*/
private function filter_plugins_list( $list ): array {
if ( ! $this->verify_did_woocommerce_init() ) {
return $list;
}
// phpcs:disable WordPress.Security.NonceVerification
if ( get_current_screen() && 'plugins' !== get_current_screen()->id || 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) {
return $list;
}
$feature_id = ArrayUtil::get_value_or_default( $_GET, 'feature_id' );
if ( is_null( $feature_id ) || ! $this->feature_exists( $feature_id ) ) {
return $list;
}
// phpcs:enable WordPress.Security.NonceVerification
$plugin_info_for_feature = $this->get_compatible_plugins_for_feature( $feature_id );
$incompatibles = array_merge( $plugin_info_for_feature['incompatible'], $plugin_info_for_feature['uncertain'] );
if ( 0 === count( $incompatibles ) ) {
return $list;
}
return array_intersect_key( $list, array_flip( $incompatibles ) );
}
/**
* Handler for the admin_notices action.
*
* Shows a "You are viewing the plugins that are incompatible with the X feature"
* if we are in the plugins page and the query string of the current request
* looks like '?plugin_status=incompatible_with_feature&feature_id=<feature id>'.
*/
private function display_notice_in_plugins_page(): void {
if ( ! $this->verify_did_woocommerce_init() ) {
return;
}
// phpcs:disable WordPress.Security.NonceVerification
if ( 'plugins' !== get_current_screen()->id || 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) {
return;
}
$feature_id = ArrayUtil::get_value_or_default( $_GET, 'feature_id' );
if ( is_null( $feature_id ) || ! $this->feature_exists( $feature_id ) ) {
return;
}
// phpcs:enable WordPress.Security.NonceVerification
$plugins_page_url = admin_url( 'plugins.php' );
$plugin_name = $this->features[ $feature_id ]['name'];
$features_page_url = $this->get_features_page_url();
$message = sprintf(
__( "You are viewing the plugins that are incompatible with the '%1\$s' feature. <a href='%2\$s'>View all plugins</a> - <a href='%3\$s'>Manage wooCommerce features</a>", 'woocommerce' ),
$plugin_name,
$plugins_page_url,
$features_page_url
);
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
?>
<div class="notice notice-info">
<p><?php echo $message; ?></p>
</div>
<?php
// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Handler for the 'after_plugin_row' action.
* Displays a "This plugin is incompatible with X features" notice if necessary.
*
* @param string $plugin_file The id of the plugin for which a row has been rendered in the plugins page.
* @param array $plugin_data Plugin data, as returned by 'get_plugins'.
*/
private function handle_plugin_list_rows( $plugin_file, $plugin_data ) {
global $wp_list_table;
if ( is_null( $wp_list_table ) || ! $this->plugin_util->is_woocommerce_aware_plugin( $plugin_data ) ) {
return;
}
if ( 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) {
return;
}
$feature_compatibility_info = $this->get_compatible_features_for_plugin( $plugin_file, true );
$incompatible_features = array_merge( $feature_compatibility_info['incompatible'], $feature_compatibility_info['uncertain'] );
$incompatible_features = array_values(
array_filter(
$incompatible_features,
function( $feature_id ) {
return ! $this->is_legacy_feature( $feature_id );
}
)
);
$incompatible_features_count = count( $incompatible_features );
if ( $incompatible_features_count > 0 ) {
$columns_count = $wp_list_table->get_column_count();
$is_active = $this->proxy->call_function( 'is_plugin_active', $plugin_file );
$is_active_class = $is_active ? 'active' : 'inactive';
$is_active_td_style = $is_active ? " style='border-left: 4px solid #72aee6;'" : '';
if ( 1 === $incompatible_features_count ) {
$message = sprintf(
/* translators: %s = printable plugin name */
__( "⚠ This plugin is incompatible with the enabled WooCommerce feature '%s', it shouldn't be activated.", 'woocommerce' ),
$this->features[ $incompatible_features[0] ]['name']
);
} elseif ( 2 === $incompatible_features_count ) {
/* translators: %1\$s, %2\$s = printable plugin names */
$message = sprintf(
__( "⚠ This plugin is incompatible with the enabled WooCommerce features '%1\$s' and '%2\$s', it shouldn't be activated.", 'woocommerce' ),
$this->features[ $incompatible_features[0] ]['name'],
$this->features[ $incompatible_features[1] ]['name']
);
} else {
/* translators: %1\$s, %2\$s = printable plugin names, %3\$d = plugins count */
$message = sprintf(
__( "⚠ This plugin is incompatible with the enabled WooCommerce features '%1\$s', '%2\$s' and %3\$d more, it shouldn't be activated.", 'woocommerce' ),
$this->features[ $incompatible_features[0] ]['name'],
$this->features[ $incompatible_features[1] ]['name'],
$incompatible_features_count - 2
);
}
$features_page_url = $this->get_features_page_url();
$manage_features_message = __( 'Manage WooCommerce features', 'woocommerce' );
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
?>
<tr class='plugin-update-tr update <?php echo $is_active_class; ?>' data-plugin='<?php echo $plugin_file; ?>' data-plugin-row-type='feature-incomp-warn'>
<td colspan='<?php echo $columns_count; ?>' class='plugin-update'<?php echo $is_active_td_style; ?>>
<div class='notice inline notice-warning notice-alt'>
<p>
<?php echo $message; ?>
<a href="<?php echo $features_page_url; ?>"><?php echo $manage_features_message; ?></a>
</p>
</div>
</td>
</tr>
<?php
// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
/**
* Get the URL of the features settings page.
*
* @return string
*/
private function get_features_page_url(): string {
return admin_url( 'admin.php?page=wc-settings&tab=advanced&section=features' );
}
/**
* Fix for the HTML of the plugins list when there are feature-plugin incompatibility warnings.
*
* WordPress renders the plugin information rows in the plugins page in <tr> elements as follows:
*
* - If the plugin needs update, the <tr> will have an "update" class. This will prevent the lower
* border line to be drawn. Later an additional <tr> with an "update available" warning will be rendered,
* it will have a "plugin-update-tr" class which will draw the missing lower border line.
* - Otherwise, the <tr> will be already drawn with the lower border line.
*
* This is a problem for our rendering of the "plugin is incompatible with X features" warning:
*
* - If the plugin info <tr> has "update", our <tr> will render nicely right after it; but then
* our own "plugin-update-tr" class will draw an additional line before the "needs update" warning.
* - If not, the plugin info <tr> will render its lower border line right before our compatibility info <tr>.
*
* This small script fixes this by adding the "update" class to the plugin info <tr> if it doesn't have it
* (so no extra line before our <tr>), or removing 'plugin-update-tr' from our <tr> otherwise
* (and then some extra manual tweaking of margins is needed).
*
* @param string $current_screen The current screen object.
*/
private function enqueue_script_to_fix_plugin_list_html( $current_screen ): void {
if ( 'plugins' !== $current_screen->id ) {
return;
}
wc_enqueue_js(
"
const warningRows = document.querySelectorAll('tr[data-plugin-row-type=\"feature-incomp-warn\"]');
for(const warningRow of warningRows) {
const pluginName = warningRow.getAttribute('data-plugin');
const pluginInfoRow = document.querySelector('tr.active[data-plugin=\"' + pluginName + '\"]:not(.plugin-update-tr), tr.inactive[data-plugin=\"' + pluginName + '\"]:not(.plugin-update-tr)');
if(pluginInfoRow.classList.contains('update')) {
warningRow.classList.remove('plugin-update-tr');
warningRow.querySelector('.notice').style.margin = '5px 10px 15px 30px';
}
else {
pluginInfoRow.classList.add('update');
}
}
"
);
}
/**
* Handler for the 'views_plugins' hook that shows the links to the different views in the plugins page.
* If we come from a "Manage incompatible plugins" in the features page we'll show just two views:
* "All" (so that it's easy to go back to a known state) and "Incompatible with X".
* We'll skip the rest of the views since the counts are wrong anyway, as we are modifying
* the plugins list via the 'all_plugins' filter.
*
* @param array $views An array of view ids => view links.
* @return string[] The actual views array to use.
*/
private function handle_plugins_page_views_list( $views ): array {
// phpcs:disable WordPress.Security.NonceVerification
if ( 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) {
return $views;
}
$feature_id = ArrayUtil::get_value_or_default( $_GET, 'feature_id' );
if ( is_null( $feature_id ) || ! $this->feature_exists( $feature_id ) ) {
return $views;
}
// phpcs:enable WordPress.Security.NonceVerification
$feature_compatibility_info = $this->get_compatible_plugins_for_feature( $feature_id, false );
$incompatible_plugins_count = count( $feature_compatibility_info['incompatible'] ) + count( $feature_compatibility_info['uncertain'] );
$feature_name = $this->features[ $feature_id ]['name'];
/* translators: %s = name of a WooCommerce feature */
$incompatible_text = sprintf( __( "Incompatible with '%s'", 'woocommerce' ), $feature_name );
$incompatible_link = "<a href='plugins.php?plugin_status=incompatible_with_feature&feature_id={$feature_id}' class='current' aria-current='page'>{$incompatible_text} <span class='count'>({$incompatible_plugins_count})</span></a>";
$all_plugins_count = count( get_plugins() );
$all_text = __( 'All', 'woocommerce' );
$all_link = "<a href='plugins.php?plugin_status=all'>{$all_text} <span class='count'>({$all_plugins_count})</span></a>";
return array(
'all' => $all_link,
'incompatible_with_feature' => $incompatible_link,
);
}
}

View File

@ -85,4 +85,20 @@ class FeaturesUtil {
public static function get_compatible_plugins_for_feature( string $feature_id ): array {
return wc_get_container()->get( FeaturesController::class )->get_compatible_plugins_for_feature( $feature_id );
}
/**
* Sets a flag indicating that it's allowed to enable features for which incompatible plugins are active
* from the WooCommerce feature settings page.
*/
public static function allow_enabling_features_with_incompatible_plugins(): void {
wc_get_container()->get( FeaturesController::class )->allow_enabling_features_with_incompatible_plugins();
}
/**
* Sets a flag indicating that it's allowed to activate plugins for which incompatible features are enabled
* from the WordPress plugins page.
*/
public static function allow_activating_plugins_with_incompatible_features(): void {
wc_get_container()->get( FeaturesController::class )->allow_activating_plugins_with_incompatible_features();
}
}

View File

@ -0,0 +1,127 @@
<?php
/**
* A class of utilities for dealing with plugins.
*/
namespace Automattic\WooCommerce\Utilities;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Proxies\LegacyProxy;
/**
* A class of utilities for dealing with plugins.
*/
class PluginUtil {
use AccessiblePrivateMethods;
/**
* The LegacyProxy instance to use.
*
* @var LegacyProxy
*/
private $proxy;
/**
* The cached list of WooCommerce aware plugin ids.
*
* @var null|array
*/
private $woocommerce_aware_plugins = null;
/**
* The cached list of enabled WooCommerce aware plugin ids.
*
* @var null|array
*/
private $woocommerce_aware_active_plugins = null;
/**
* Creates a new instance of the class.
*/
public function __construct() {
self::add_action( 'activated_plugin', array( $this, 'handle_plugin_de_activation' ), 10, 0 );
self::add_action( 'deactivated_plugin', array( $this, 'handle_plugin_de_activation' ), 10, 0 );
}
/**
* Initialize the class instance.
*
* @internal
*
* @param LegacyProxy $proxy The instance of LegacyProxy to use.
*/
final public function init( LegacyProxy $proxy ) {
$this->proxy = $proxy;
require_once ABSPATH . WPINC . '/plugin.php';
}
/**
* Get a list with the names of the WordPress plugins that are WooCommerce aware
* (they have a "WC tested up to" header).
*
* @param bool $active_only True to return only active plugins, false to return all the active plugins.
* @return string[] A list of plugin ids (path/file.php).
*/
public function get_woocommerce_aware_plugins( bool $active_only = false ): array {
if ( is_null( $this->woocommerce_aware_plugins ) ) {
$all_plugins = $this->proxy->call_function( 'get_plugins' );
$this->woocommerce_aware_plugins =
array_keys(
array_filter(
$all_plugins,
array( $this, 'is_woocommerce_aware_plugin' )
)
);
$this->woocommerce_aware_active_plugins =
array_values(
array_filter(
$this->woocommerce_aware_plugins,
function ( $plugin_name ) {
return $this->proxy->call_function( 'is_plugin_active', $plugin_name );
}
)
);
}
return $active_only ? $this->woocommerce_aware_active_plugins : $this->woocommerce_aware_plugins;
}
/**
* Get the printable name of a plugin.
*
* @param string $plugin_id Plugin id (path/file.php).
* @return string Printable plugin name, or the plugin id itself if printable name is not available.
*/
public function get_plugin_name( string $plugin_id ): string {
$plugin_data = $this->proxy->call_function( 'get_plugin_data', WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin_id );
return ArrayUtil::get_value_or_default( $plugin_data, 'Name', $plugin_id );
}
/**
* Check if a plugin is WooCommerce aware.
*
* @param string|array $plugin_file_or_data Plugin id (path/file.php) or plugin data (as returned by get_plugins).
* @return bool True if the plugin exists and is WooCommerce aware.
* @throws \Exception The input is neither a string nor an array.
*/
public function is_woocommerce_aware_plugin( $plugin_file_or_data ): bool {
if ( is_string( $plugin_file_or_data ) ) {
return in_array( $plugin_file_or_data, $this->get_woocommerce_aware_plugins(), true );
} elseif ( is_array( $plugin_file_or_data ) ) {
return '' !== ArrayUtil::get_value_or_default( $plugin_file_or_data, 'WC tested up to', '' );
} else {
throw new \Exception( 'is_woocommerce_aware_plugin requires a plugin name or an array of plugin data as input' );
}
}
/**
* Handle plugin activation and deactivation by clearing the WooCommerce aware plugin ids cache.
*/
private function handle_plugin_de_activation(): void {
$this->woocommerce_aware_plugins = null;
$this->woocommerce_aware_active_plugins = null;
}
}

View File

@ -6,8 +6,9 @@
namespace Automattic\WooCommerce\Tests\Internal\Features;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Admin\Features\Analytics;
use Automattic\WooCommerce\Admin\Features\Navigation\Init;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\PluginUtil;
/**
* Tests for the FeaturesController class.
@ -20,13 +21,17 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
*/
private $sut;
/**
* The fake PluginUtil instance to use.
*
* @var PluginUtil
*/
private $fake_plugin_util;
/**
* Runs before each test.
*/
public function setUp(): void {
$this->reset_container_resolutions();
$this->reset_legacy_proxy_mocks();
$features = array(
'mature1' => array(
'name' => 'Mature feature 1',
@ -35,7 +40,7 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
),
'mature2' => array(
'name' => 'Mature feature 2',
'description' => 'The mature feature number 1',
'description' => 'The mature feature number 2',
'is_experimental' => false,
),
'experimental1' => array(
@ -50,7 +55,43 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
),
);
$this->sut = $this->get_instance_of( FeaturesController::class );
$this->do_set_up( $features );
}
/**
* Runs before each test.
*
* @param array $features The fake features list to use.
*/
public function do_set_up( array $features ): void {
$this->reset_container_resolutions();
$this->reset_legacy_proxy_mocks();
// phpcs:disable Squiz.Commenting
$this->fake_plugin_util = new class() extends PluginUtil {
private $active_plugins;
public function __construct() {
}
public function set_active_plugins( $plugins ) {
$this->active_plugins = $plugins;
}
public function get_woocommerce_aware_plugins( bool $active_only = false ): array {
$plugins = $this->active_plugins;
if ( ! $active_only ) {
$plugins[] = 'the_plugin_inactive';
}
return $plugins;
}
};
// phpcs:enable Squiz.Commenting
$this->fake_plugin_util->set_active_plugins( array( 'the_plugin', 'the_plugin_2', 'the_plugin_3', 'the_plugin_4' ) );
$this->sut = new FeaturesController();
$this->sut->init( wc_get_container()->get( LegacyProxy::class ), $this->fake_plugin_util );
$init_features_method = new \ReflectionMethod( $this->sut, 'init_features' );
$init_features_method->setAccessible( true );
$init_features_method->invoke( $this->sut, $features );
@ -77,7 +118,7 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
),
'mature2' => array(
'name' => 'Mature feature 2',
'description' => 'The mature feature number 1',
'description' => 'The mature feature number 2',
'is_experimental' => false,
),
);
@ -99,7 +140,7 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
),
'mature2' => array(
'name' => 'Mature feature 2',
'description' => 'The mature feature number 1',
'description' => 'The mature feature number 2',
'is_experimental' => false,
),
'experimental1' => array(
@ -137,7 +178,7 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
),
'mature2' => array(
'name' => 'Mature feature 2',
'description' => 'The mature feature number 1',
'description' => 'The mature feature number 2',
'is_experimental' => false,
'is_enabled' => false,
),
@ -391,12 +432,13 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
$expected = array(
'compatible' => array(),
'incompatible' => array(),
'uncertain' => array( 'mature1', 'mature2', 'experimental1', 'experimental2' ),
);
$this->assertEquals( $expected, $result );
}
/**
* @testdox 'get_compatible_features_for_plugin' returns proper information for a plugin that has declared compatibility with the passed feature.
* @testdox 'get_compatible_features_for_plugin' returns proper information for a plugin that has declared compatibility with the passed feature, and reacts to plugin deactivation accordingly.
*/
public function test_get_compatible_features_for_registered_plugin() {
$this->simulate_inside_before_woocommerce_init_hook();
@ -404,17 +446,90 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
$this->sut->declare_compatibility( 'mature1', 'the_plugin', true );
$this->sut->declare_compatibility( 'mature2', 'the_plugin', true );
$this->sut->declare_compatibility( 'experimental1', 'the_plugin', false );
$this->sut->declare_compatibility( 'experimental2', 'the_plugin', false );
$this->reset_legacy_proxy_mocks();
$this->simulate_after_woocommerce_init_hook();
$result = $this->sut->get_compatible_features_for_plugin( 'the_plugin' );
$result = $this->sut->get_compatible_features_for_plugin( 'the_plugin' );
$expected = array(
'compatible' => array( 'mature1', 'mature2' ),
'incompatible' => array( 'experimental1', 'experimental2' ),
'incompatible' => array( 'experimental1' ),
'uncertain' => array( 'experimental2' ),
);
$this->assertEquals( $expected, $result );
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
do_action( 'deactivated_plugin', 'the_plugin' );
$this->fake_plugin_util->set_active_plugins( array( 'the_plugin_2', 'the_plugin_3', 'the_plugin_4' ) );
$result = $this->sut->get_compatible_features_for_plugin( 'the_plugin' );
$expected = array(
'compatible' => array(),
'incompatible' => array(),
'uncertain' => array( 'mature1', 'mature2', 'experimental1', 'experimental2' ),
);
$this->assertEquals( $expected, $result );
}
/**
* @testdox 'get_compatible_features_for_plugin' returns proper information for a plugin that has declared compatibility with the passed feature, when only enabled features are requested.
*/
public function test_get_compatible_enabled_features_for_registered_plugin() {
$features = array(
'mature1' => array(
'name' => 'Mature feature 1',
'description' => 'The mature feature number 1',
'is_experimental' => false,
),
'mature2' => array(
'name' => 'Mature feature 2',
'description' => 'The mature feature number 2',
'is_experimental' => false,
),
'mature3' => array(
'name' => 'Mature feature 3',
'description' => 'The mature feature number 3',
'is_experimental' => false,
),
'experimental1' => array(
'name' => 'Experimental feature 1',
'description' => 'The experimental feature number 1',
'is_experimental' => true,
),
'experimental2' => array(
'name' => 'Experimental feature 2',
'description' => 'The experimental feature number 2',
'is_experimental' => true,
),
'experimental3' => array(
'name' => 'Experimental feature 3',
'description' => 'The experimental feature number 3',
'is_experimental' => true,
),
);
$this->do_set_up( $features );
$this->simulate_inside_before_woocommerce_init_hook();
$this->sut->declare_compatibility( 'mature1', 'the_plugin', true );
$this->sut->declare_compatibility( 'mature2', 'the_plugin', true );
$this->sut->declare_compatibility( 'experimental1', 'the_plugin', false );
$this->sut->declare_compatibility( 'experimental2', 'the_plugin', false );
$this->reset_legacy_proxy_mocks();
$this->simulate_after_woocommerce_init_hook();
update_option( 'woocommerce_feature_mature1_enabled', 'yes' );
update_option( 'woocommerce_feature_mature2_enabled', 'no' );
update_option( 'woocommerce_feature_mature3_enabled', 'yes' );
update_option( 'woocommerce_feature_experimental1_enabled', 'no' );
update_option( 'woocommerce_feature_experimental2_enabled', 'yes' );
update_option( 'woocommerce_feature_experimental3_enabled', 'no' );
$result = $this->sut->get_compatible_features_for_plugin( 'the_plugin', true );
$expected = array(
'compatible' => array( 'mature1' ),
'incompatible' => array( 'experimental2' ),
'uncertain' => array( 'mature3' ),
);
$this->assertEquals( $expected, $result );
}
@ -448,56 +563,111 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
}
/**
* @testdox 'get_compatible_plugins_for_feature' returns empty information for invalid feature ids.
* @testdox 'get_compatible_plugins_for_feature' returns empty information for invalid feature ids when only active plugins are requested.
*/
public function test_get_compatible_plugins_for_non_existing_feature() {
public function test_get_compatible_active_plugins_for_non_existing_feature() {
$this->simulate_after_woocommerce_init_hook();
$result = $this->sut->get_compatible_plugins_for_feature( 'NON_EXISTING' );
$result = $this->sut->get_compatible_plugins_for_feature( 'NON_EXISTING', true );
$expected = array(
'compatible' => array(),
'incompatible' => array(),
'uncertain' => array( 'the_plugin', 'the_plugin_2', 'the_plugin_3', 'the_plugin_4' ),
);
$this->assertEquals( $expected, $result );
}
/**
* @testdox 'get_compatible_plugins_for_feature' returns empty information for features for which no compatibility has been declared.
* @testdox 'get_compatible_plugins_for_feature' returns empty information for invalid feature ids when all plugins are requested.
*/
public function test_get_compatible_plugins_for_existing_feature_without_compatibility_declarations() {
public function test_get_all_compatible_plugins_for_non_existing_feature() {
$this->simulate_after_woocommerce_init_hook();
$result = $this->sut->get_compatible_plugins_for_feature( 'mature1' );
$result = $this->sut->get_compatible_plugins_for_feature( 'NON_EXISTING', false );
$expected = array(
'compatible' => array(),
'incompatible' => array(),
'uncertain' => array( 'the_plugin', 'the_plugin_2', 'the_plugin_3', 'the_plugin_4', 'the_plugin_inactive' ),
);
$this->assertEquals( $expected, $result );
}
/**
* @testdox 'get_compatible_plugins_for_feature' returns proper information for a feature for which compatibility has been declared.
* @testdox 'get_compatible_plugins_for_feature' returns empty information for features for which no compatibility has been declared when only active plugins are requested.
*/
public function test_get_compatible_plugins_for_feature() {
public function test_get_active_compatible_plugins_for_existing_feature_without_compatibility_declarations() {
$this->simulate_after_woocommerce_init_hook();
$result = $this->sut->get_compatible_plugins_for_feature( 'mature1', true );
$expected = array(
'compatible' => array(),
'incompatible' => array(),
'uncertain' => array( 'the_plugin', 'the_plugin_2', 'the_plugin_3', 'the_plugin_4' ),
);
$this->assertEquals( $expected, $result );
}
/**
* @testdox 'get_compatible_plugins_for_feature' returns empty information for features for which no compatibility has been declared when all plugins are requested.
*/
public function test_get_all_compatible_plugins_for_existing_feature_without_compatibility_declarations() {
$this->simulate_after_woocommerce_init_hook();
$result = $this->sut->get_compatible_plugins_for_feature( 'mature1', false );
$expected = array(
'compatible' => array(),
'incompatible' => array(),
'uncertain' => array( 'the_plugin', 'the_plugin_2', 'the_plugin_3', 'the_plugin_4', 'the_plugin_inactive' ),
);
$this->assertEquals( $expected, $result );
}
/**
* @testdox 'get_compatible_plugins_for_feature' returns proper information for a feature for which compatibility has been declared, and reacts to plugin deactivation accordingly.
*
* @testWith [true]
* [false]
*
* @param bool $active_only True to test retrieving only active plugins.
*/
public function test_get_compatible_plugins_for_feature( bool $active_only ) {
$this->simulate_inside_before_woocommerce_init_hook();
$this->sut->declare_compatibility( 'mature1', 'the_plugin_1', true );
$this->fake_plugin_util->set_active_plugins( array( 'the_plugin', 'the_plugin_2', 'the_plugin_3', 'the_plugin_4', 'the_plugin_5', 'the_plugin_6' ) );
$this->sut->declare_compatibility( 'mature1', 'the_plugin', true );
$this->sut->declare_compatibility( 'mature1', 'the_plugin_2', true );
$this->sut->declare_compatibility( 'mature1', 'the_plugin_3', false );
$this->reset_legacy_proxy_mocks();
$this->sut->declare_compatibility( 'mature1', 'the_plugin_4', false );
$this->simulate_after_woocommerce_init_hook();
$result = $this->sut->get_compatible_plugins_for_feature( 'mature1' );
$expected = array(
'compatible' => array( 'the_plugin_1', 'the_plugin_2' ),
'incompatible' => array( 'the_plugin_3' ),
$result = $this->sut->get_compatible_plugins_for_feature( 'mature1', $active_only );
$expected_uncertain = $active_only ? array( 'the_plugin_5', 'the_plugin_6' ) : array( 'the_plugin_5', 'the_plugin_6', 'the_plugin_inactive' );
$expected = array(
'compatible' => array( 'the_plugin', 'the_plugin_2' ),
'incompatible' => array( 'the_plugin_3', 'the_plugin_4' ),
'uncertain' => $expected_uncertain,
);
$this->assertEquals( $expected, $result );
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment
do_action( 'deactivated_plugin', 'the_plugin_2' );
do_action( 'deactivated_plugin', 'the_plugin_4' );
do_action( 'deactivated_plugin', 'the_plugin_6' );
// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingHookComment
$this->fake_plugin_util->set_active_plugins( array( 'the_plugin', 'the_plugin_3', 'the_plugin_5' ) );
$result = $this->sut->get_compatible_plugins_for_feature( 'mature1', $active_only );
$expected_uncertain = $active_only ? array( 'the_plugin_5' ) : array( 'the_plugin_5', 'the_plugin_inactive' );
$expected = array(
'compatible' => array( 'the_plugin' ),
'incompatible' => array( 'the_plugin_3' ),
'uncertain' => $expected_uncertain,
);
$this->assertEquals( $expected, $result );
}

View File

@ -0,0 +1,148 @@
<?php
namespace Automattic\WooCommerce\Tests\Utilities;
use Automattic\WooCommerce\Utilities\PluginUtil;
use Automattic\WooCommerce\Utilities\StringUtil;
/**
* A collection of tests for the PluginUtil class.
*/
class PluginUtilTests extends \WC_Unit_Test_Case {
/**
* The system under test.
*
* @var PluginUtil
*/
private $sut;
/**
* Runs before each test.
*/
public function setUp(): void {
parent::setUp();
$this->reset_container_resolutions();
$this->reset_legacy_proxy_mocks();
$this->mock_plugin_functions();
$this->sut = $this->get_instance_of( PluginUtil::class );
}
/**
* @testdox 'get_woocommerce_aware_plugins' properly gets the names of all the existing WooCommerce aware plugins.
*/
public function test_get_all_woo_aware_plugins() {
$result = $this->sut->get_woocommerce_aware_plugins( false );
$expected = array(
'woo_aware_1',
'woo_aware_2',
'woo_aware_3',
);
$this->assertEquals( $expected, $result );
}
/**
* @testdox 'get_woocommerce_aware_plugins' properly gets the names of the active WooCommerce aware plugins.
*/
public function test_get_active_woo_aware_plugins() {
$result = $this->sut->get_woocommerce_aware_plugins( true );
$expected = array(
'woo_aware_1',
'woo_aware_2',
);
$this->assertEquals( $expected, $result );
}
/**
* 'test_get_plugin_name' returns the printable plugin name when available.
*/
public function test_get_plugin_name_with_name() {
$result = $this->sut->get_plugin_name( 'woo_aware_1' );
$this->assertEquals( 'The WooCommerce aware plugin #1', $result );
}
/**
* 'test_get_plugin_name' returns back the plugin id when printable plugin name is not available.
*/
public function test_get_plugin_name_with_no_name() {
$result = $this->sut->get_plugin_name( 'woo_aware_2' );
$this->assertEquals( 'woo_aware_2', $result );
}
/**
* @testDox 'is_woocommerce_aware_plugin' works as expected when a plugin id (path/file.php) is passed.
*
* @testWith ["woo_aware_1", true]
* ["not_woo_aware_2", false]
* ["NOT_EXISTS", false]
*
* @param string $plugin_file The plugin file name to test.
* @param bool $expected_result The expected result from the method.
*/
public function test_is_woocommerce_aware_plugin_by_plugin_file( string $plugin_file, bool $expected_result ) {
$result = $this->sut->is_woocommerce_aware_plugin( $plugin_file );
$this->assertEquals( $expected_result, $result );
}
/**
* Data provider for test_is_woocommerce_aware_plugin_by_plugin_data.
*
* @return array[]
*/
public function data_provider_for_test_is_woocommerce_aware_plugin_by_plugin_data() {
return array(
array( array( 'WC tested up to' => '1.0' ), true ),
array( array( 'WC tested up to' => '' ), false ),
array( array(), false ),
);
}
/**
* @testDox 'is_woocommerce_aware_plugin' works as expected when a an array of plugin data is passed.
*
* @dataProvider data_provider_for_test_is_woocommerce_aware_plugin_by_plugin_data
*
* @param array $plugin_data The plugin data to test.
* @param bool $expected_result The expected result from the method.
*/
public function test_get_is_woocommerce_aware_plugin_by_plugin_data( array $plugin_data, bool $expected_result ) {
$result = $this->sut->is_woocommerce_aware_plugin( $plugin_data );
$this->assertEquals( $expected_result, $result );
}
/**
* Forces a fake list of plugins to be used by the tests.
*/
private function mock_plugin_functions() {
$this->register_legacy_proxy_function_mocks(
array(
'get_plugins' => function() {
return array(
'woo_aware_1' => array( 'WC tested up to' => '1.0' ),
'woo_aware_2' => array( 'WC tested up to' => '2.0' ),
'woo_aware_3' => array( 'WC tested up to' => '2.0' ),
'not_woo_aware_1' => array( 'WC tested up to' => '' ),
'not_woo_aware_2' => array( 'foo' => 'bar' ),
);
},
'is_plugin_active' => function( $plugin_name ) {
return 'woo_aware_3' !== $plugin_name;
},
'get_plugin_data' => function( $plugin_name ) {
return StringUtil::ends_with( $plugin_name, 'woo_aware_1' ) ?
array(
'WC tested up to' => '1.0',
'Name' => 'The WooCommerce aware plugin #1',
) :
array( 'WC tested up to' => '1.0' );
},
)
);
}
}