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:
parent
a9dbb6d64e
commit
edc1c6c98a
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add handling for plugin-feature incompatibilities
|
|
@ -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. ?>
|
||||
|
|
|
@ -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 ) );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ) );
|
||||
}
|
||||
|
|
|
@ -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§ion=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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
|
||||
|
|
|
@ -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' );
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue