ArrayUtil.php
1 year ago
CallbackUtil.php
4 months ago
DiscountsUtil.php
2 years ago
FeaturesUtil.php
4 months ago
I18nUtil.php
3 years ago
LoggingUtil.php
1 year ago
MetaDataUtil.php
4 weeks ago
NumberUtil.php
10 months ago
OrderUtil.php
6 months ago
PluginUtil.php
7 months ago
RestApiUtil.php
6 months ago
ShippingUtil.php
1 year ago
StringUtil.php
1 year ago
TimeUtil.php
2 years ago
PluginUtil.php
342 lines
| 1 | <?php |
| 2 | /** |
| 3 | * A class of utilities for dealing with plugins. |
| 4 | */ |
| 5 | |
| 6 | namespace Automattic\WooCommerce\Utilities; |
| 7 | |
| 8 | use Automattic\WooCommerce\Enums\FeaturePluginCompatibility; |
| 9 | use Automattic\WooCommerce\Internal\Features\FeaturesController; |
| 10 | use Automattic\WooCommerce\Internal\Utilities\PluginInstaller; |
| 11 | use Automattic\WooCommerce\Proxies\LegacyProxy; |
| 12 | |
| 13 | /** |
| 14 | * A class of utilities for dealing with plugins. |
| 15 | */ |
| 16 | class PluginUtil { |
| 17 | |
| 18 | /** |
| 19 | * The LegacyProxy instance to use. |
| 20 | * |
| 21 | * @var LegacyProxy |
| 22 | */ |
| 23 | private $proxy; |
| 24 | |
| 25 | /** |
| 26 | * The cached list of WooCommerce aware plugin ids. |
| 27 | * |
| 28 | * @var null|array |
| 29 | */ |
| 30 | private $woocommerce_aware_plugins = null; |
| 31 | |
| 32 | /** |
| 33 | * The cached list of enabled WooCommerce aware plugin ids. |
| 34 | * |
| 35 | * @var null|array |
| 36 | */ |
| 37 | private $woocommerce_aware_active_plugins = null; |
| 38 | |
| 39 | /** |
| 40 | * List of plugins excluded from feature compatibility warnings in UI. |
| 41 | * |
| 42 | * @var string[] |
| 43 | */ |
| 44 | private $plugins_excluded_from_compatibility_ui; |
| 45 | |
| 46 | /** |
| 47 | * Creates a new instance of the class. |
| 48 | */ |
| 49 | public function __construct() { |
| 50 | add_action( 'activated_plugin', array( $this, 'handle_plugin_de_activation' ), 10, 0 ); |
| 51 | add_action( 'deactivated_plugin', array( $this, 'handle_plugin_de_activation' ), 10, 0 ); |
| 52 | |
| 53 | $this->plugins_excluded_from_compatibility_ui = array( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ); |
| 54 | } |
| 55 | |
| 56 | /** |
| 57 | * Initialize the class instance. |
| 58 | * |
| 59 | * @internal |
| 60 | * |
| 61 | * @param LegacyProxy $proxy The instance of LegacyProxy to use. |
| 62 | */ |
| 63 | final public function init( LegacyProxy $proxy ) { |
| 64 | $this->proxy = $proxy; |
| 65 | require_once ABSPATH . WPINC . '/plugin.php'; |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * Wrapper for WP's private `wp_get_active_and_valid_plugins` and `wp_get_active_network_plugins` functions. |
| 70 | * |
| 71 | * This combines the results of the two functions to get a list of all plugins that are active within a site. |
| 72 | * It's more useful than just retrieving the option values because it also validates that the plugin files exist. |
| 73 | * This wrapper is also a hedge against backward-incompatible changes since both of the WP methods are marked as |
| 74 | * being "@access private", so if need be we can update our methods here to preserve functionality. |
| 75 | * |
| 76 | * Note that the doc block for `wp_get_active_and_valid_plugins` says it returns "Array of paths to plugin files |
| 77 | * relative to the plugins directory", but it actually returns absolute paths. |
| 78 | * |
| 79 | * @return string[] Array of plugin basenames (paths relative to the plugin directory). |
| 80 | */ |
| 81 | public function get_all_active_valid_plugins() { |
| 82 | $local = wp_get_active_and_valid_plugins(); |
| 83 | |
| 84 | if ( is_multisite() ) { |
| 85 | require_once ABSPATH . WPINC . '/ms-load.php'; |
| 86 | $network = wp_get_active_network_plugins(); |
| 87 | } else { |
| 88 | $network = array(); |
| 89 | } |
| 90 | |
| 91 | $all = array_merge( $local, $network ); |
| 92 | $all = array_unique( $all ); |
| 93 | $all = array_map( 'plugin_basename', $all ); |
| 94 | sort( $all ); |
| 95 | |
| 96 | return $all; |
| 97 | } |
| 98 | |
| 99 | /** |
| 100 | * Get a list with the names of the WordPress plugins that are WooCommerce aware |
| 101 | * (they have a "WC tested up to" header). |
| 102 | * |
| 103 | * @param bool $active_only True to return only active plugins, false to return all the active plugins. |
| 104 | * @return string[] A list of plugin ids (path/file.php). |
| 105 | */ |
| 106 | public function get_woocommerce_aware_plugins( bool $active_only = false ): array { |
| 107 | if ( is_null( $this->woocommerce_aware_plugins ) ) { |
| 108 | // In case `get_plugins` was called much earlier in the request (before our headers could be injected), we |
| 109 | // invalidate the plugin cache list. |
| 110 | wp_cache_delete( 'plugins', 'plugins' ); |
| 111 | $all_plugins = $this->proxy->call_function( 'get_plugins' ); |
| 112 | |
| 113 | $this->woocommerce_aware_plugins = |
| 114 | array_keys( |
| 115 | array_filter( |
| 116 | $all_plugins, |
| 117 | array( $this, 'is_woocommerce_aware_plugin' ) |
| 118 | ) |
| 119 | ); |
| 120 | |
| 121 | $this->woocommerce_aware_active_plugins = |
| 122 | array_values( |
| 123 | array_filter( |
| 124 | $this->woocommerce_aware_plugins, |
| 125 | function ( $plugin_name ) { |
| 126 | return $this->proxy->call_function( 'is_plugin_active', $plugin_name ); |
| 127 | } |
| 128 | ) |
| 129 | ); |
| 130 | } |
| 131 | |
| 132 | return $active_only ? $this->woocommerce_aware_active_plugins : $this->woocommerce_aware_plugins; |
| 133 | } |
| 134 | |
| 135 | /** |
| 136 | * Get the printable name of a plugin. |
| 137 | * |
| 138 | * @param string $plugin_id Plugin id (path/file.php). |
| 139 | * @return string Printable plugin name, or the plugin id itself if printable name is not available. |
| 140 | */ |
| 141 | public function get_plugin_name( string $plugin_id ): string { |
| 142 | $plugin_data = $this->proxy->call_function( 'get_plugin_data', WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin_id ); |
| 143 | return $plugin_data['Name'] ?? $plugin_id; |
| 144 | } |
| 145 | |
| 146 | /** |
| 147 | * Check if a plugin is WooCommerce aware. |
| 148 | * |
| 149 | * @param string|array $plugin_file_or_data Plugin id (path/file.php) or plugin data (as returned by get_plugins). |
| 150 | * @return bool True if the plugin exists and is WooCommerce aware. |
| 151 | * @throws \Exception The input is neither a string nor an array. |
| 152 | */ |
| 153 | public function is_woocommerce_aware_plugin( $plugin_file_or_data ): bool { |
| 154 | if ( is_string( $plugin_file_or_data ) ) { |
| 155 | return in_array( $plugin_file_or_data, $this->get_woocommerce_aware_plugins(), true ); |
| 156 | } elseif ( is_array( $plugin_file_or_data ) ) { |
| 157 | return '' !== ( $plugin_file_or_data['WC tested up to'] ?? '' ); |
| 158 | } else { |
| 159 | throw new \Exception( 'is_woocommerce_aware_plugin requires a plugin name or an array of plugin data as input' ); |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * Match plugin identifier passed as a parameter with the output from `get_plugins()`. |
| 165 | * |
| 166 | * @param string $plugin_file Plugin identifier, either 'my-plugin/my-plugin.php', or output from __FILE__. |
| 167 | * |
| 168 | * @return string|false Key from the array returned by `get_plugins` if matched. False if no match. |
| 169 | */ |
| 170 | public function get_wp_plugin_id( $plugin_file ) { |
| 171 | $wp_plugins = array_keys( $this->proxy->call_function( 'get_plugins' ) ); |
| 172 | |
| 173 | // Try to match plugin_basename(). |
| 174 | $plugin_basename = $this->proxy->call_function( 'plugin_basename', $plugin_file ); |
| 175 | if ( in_array( $plugin_basename, $wp_plugins, true ) ) { |
| 176 | return $plugin_basename; |
| 177 | } |
| 178 | |
| 179 | // Try to match by the my-file/my-file.php (dir + file name), then by my-file.php (file name only). |
| 180 | $plugin_file = str_replace( array( '\\', '/' ), DIRECTORY_SEPARATOR, $plugin_file ); |
| 181 | $file_name_parts = explode( DIRECTORY_SEPARATOR, $plugin_file ); |
| 182 | $file_name = array_pop( $file_name_parts ); |
| 183 | $directory_name = array_pop( $file_name_parts ); |
| 184 | $full_matches = array(); |
| 185 | $partial_matches = array(); |
| 186 | foreach ( $wp_plugins as $wp_plugin ) { |
| 187 | if ( false !== strpos( $wp_plugin, $directory_name . DIRECTORY_SEPARATOR . $file_name ) ) { |
| 188 | $full_matches[] = $wp_plugin; |
| 189 | } |
| 190 | |
| 191 | if ( ! empty( $file_name ) && false !== strpos( $wp_plugin, $file_name ) ) { |
| 192 | $partial_matches[] = $wp_plugin; |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | if ( 1 === count( $full_matches ) ) { |
| 197 | return $full_matches[0]; |
| 198 | } |
| 199 | |
| 200 | if ( 1 === count( $partial_matches ) ) { |
| 201 | return $partial_matches[0]; |
| 202 | } |
| 203 | |
| 204 | return false; |
| 205 | } |
| 206 | |
| 207 | /** |
| 208 | * Handle plugin activation and deactivation by clearing the WooCommerce aware plugin ids cache. |
| 209 | * |
| 210 | * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. |
| 211 | */ |
| 212 | public function handle_plugin_de_activation(): void { |
| 213 | $this->woocommerce_aware_plugins = null; |
| 214 | $this->woocommerce_aware_active_plugins = null; |
| 215 | } |
| 216 | |
| 217 | /** |
| 218 | * Utility method to generate warning string for incompatible features based on active plugins. |
| 219 | * |
| 220 | * Additionally, this method will manually print a warning message on the HPOS feature if both |
| 221 | * the Legacy REST API and HPOS are active. |
| 222 | * |
| 223 | * @param string $feature_id Feature id. |
| 224 | * @param array $plugin_feature_info Array of plugin feature info, as provided by FeaturesController->get_compatible_plugins_for_feature(). |
| 225 | * |
| 226 | * @return string Warning string. |
| 227 | */ |
| 228 | public function generate_incompatible_plugin_feature_warning( string $feature_id, array $plugin_feature_info ): string { |
| 229 | $incompatibles = $this->get_items_considered_incompatible( $feature_id, $plugin_feature_info ); |
| 230 | $incompatibles = array_filter( $incompatibles, 'is_plugin_active' ); |
| 231 | $incompatibles = array_values( array_diff( $incompatibles, $this->get_plugins_excluded_from_compatibility_ui() ) ); |
| 232 | $incompatible_count = count( $incompatibles ); |
| 233 | |
| 234 | $feature_warnings = array(); |
| 235 | if ( 'custom_order_tables' === $feature_id && 'yes' === get_option( 'woocommerce_api_enabled' ) ) { |
| 236 | if ( is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ) ) { |
| 237 | $legacy_api_and_hpos_incompatibility_warning_text = |
| 238 | sprintf( |
| 239 | // translators: %s is a URL. |
| 240 | __( '⚠ <b><a target="_blank" href="%s">The Legacy REST API plugin</a> is installed and active on this site.</b> Please be aware that the WooCommerce Legacy REST API is <b>not</b> compatible with HPOS.', 'woocommerce' ), |
| 241 | 'https://wordpress.org/plugins/woocommerce-legacy-rest-api/' |
| 242 | ); |
| 243 | } else { |
| 244 | $legacy_api_and_hpos_incompatibility_warning_text = |
| 245 | sprintf( |
| 246 | // translators: %s is a URL. |
| 247 | __( '⚠ <b><a target="_blank" href="%s">The Legacy REST API</a> is active on this site.</b> Please be aware that the WooCommerce Legacy REST API is <b>not</b> compatible with HPOS.', 'woocommerce' ), |
| 248 | admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=legacy_api' ) |
| 249 | ); |
| 250 | } |
| 251 | |
| 252 | /** |
| 253 | * Filter to modify the warning text that appears in the HPOS section of the features settings page |
| 254 | * when both the Legacy REST API is active (via WooCommerce core or via the Legacy REST API plugin) |
| 255 | * and the orders table is in use as the primary data store for orders. |
| 256 | * |
| 257 | * @param string $legacy_api_and_hpos_incompatibility_warning_text Original warning text. |
| 258 | * @returns string|null Actual warning text to use, or null to suppress the warning. |
| 259 | * |
| 260 | * @since 8.9.0 |
| 261 | */ |
| 262 | $legacy_api_and_hpos_incompatibility_warning_text = apply_filters( 'woocommerce_legacy_api_and_hpos_incompatibility_warning_text', $legacy_api_and_hpos_incompatibility_warning_text ); |
| 263 | |
| 264 | if ( ! is_null( $legacy_api_and_hpos_incompatibility_warning_text ) ) { |
| 265 | $feature_warnings[] = $legacy_api_and_hpos_incompatibility_warning_text . "\n"; |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | if ( $incompatible_count > 0 ) { |
| 270 | if ( 1 === $incompatible_count ) { |
| 271 | /* translators: %s = printable plugin name */ |
| 272 | $feature_warnings[] = sprintf( __( '⚠ 1 Incompatible plugin detected (%s).', 'woocommerce' ), $this->get_plugin_name( $incompatibles[0] ) ); |
| 273 | } elseif ( 2 === $incompatible_count ) { |
| 274 | $feature_warnings[] = sprintf( |
| 275 | /* translators: %1\$s, %2\$s = printable plugin names */ |
| 276 | __( '⚠ 2 Incompatible plugins detected (%1$s and %2$s).', 'woocommerce' ), |
| 277 | $this->get_plugin_name( $incompatibles[0] ), |
| 278 | $this->get_plugin_name( $incompatibles[1] ) |
| 279 | ); |
| 280 | } else { |
| 281 | $feature_warnings[] = sprintf( |
| 282 | /* translators: %1\$s, %2\$s = printable plugin names, %3\$d = plugins count */ |
| 283 | _n( |
| 284 | '⚠ Incompatible plugins detected (%1$s, %2$s and %3$d other).', |
| 285 | '⚠ Incompatible plugins detected (%1$s and %2$s plugins and %3$d others).', |
| 286 | $incompatible_count - 2, |
| 287 | 'woocommerce' |
| 288 | ), |
| 289 | $this->get_plugin_name( $incompatibles[0] ), |
| 290 | $this->get_plugin_name( $incompatibles[1] ), |
| 291 | $incompatible_count - 2 |
| 292 | ); |
| 293 | } |
| 294 | |
| 295 | $incompatible_plugins_url = add_query_arg( |
| 296 | array( |
| 297 | 'plugin_status' => 'incompatible_with_feature', |
| 298 | 'feature_id' => $feature_id, |
| 299 | ), |
| 300 | admin_url( 'plugins.php' ) |
| 301 | ); |
| 302 | |
| 303 | $feature_warnings[] = sprintf( |
| 304 | /* translators: %1$s opening link tag %2$s closing link tag. */ |
| 305 | __( '%1$sView and manage%2$s', 'woocommerce' ), |
| 306 | '<a href="' . esc_url( $incompatible_plugins_url ) . '">', |
| 307 | '</a>' |
| 308 | ); |
| 309 | } |
| 310 | |
| 311 | return str_replace( "\n", '<br>', implode( "\n", $feature_warnings ) ); |
| 312 | } |
| 313 | |
| 314 | /** |
| 315 | * Filter plugin/feature compatibility info, returning the names of the plugins/features that are considered incompatible. |
| 316 | * "Uncertain" information will be included or not depending on the value of the value of the 'default_plugin_compatibility' |
| 317 | * flag in the feature definition (default is 'compatible'). |
| 318 | * |
| 319 | * @param string $feature_id Feature id. |
| 320 | * @param array $compatibility_info Array containing "compatible', 'incompatible' and 'uncertain' keys. |
| 321 | * @return array Items in 'incompatible' and 'uncertain' if plugins are incompatible by default with the feature; only items in 'incompatible' otherwise. |
| 322 | */ |
| 323 | public function get_items_considered_incompatible( string $feature_id, array $compatibility_info ): array { |
| 324 | $incompatible_by_default = FeaturePluginCompatibility::COMPATIBLE !== wc_get_container()->get( FeaturesController::class )->get_default_plugin_compatibility( $feature_id ); |
| 325 | |
| 326 | return $incompatible_by_default ? |
| 327 | array_merge( $compatibility_info['incompatible'], $compatibility_info['uncertain'] ) : |
| 328 | $compatibility_info['incompatible']; |
| 329 | } |
| 330 | |
| 331 | /** |
| 332 | * Get the names of the plugins that are excluded from the feature compatibility UI. |
| 333 | * These plugins won't be considered as incompatible with any existing feature for the purposes |
| 334 | * of displaying compatibility warning in UI, even if they declare incompatibilities explicitly. |
| 335 | * |
| 336 | * @return string[] Plugin names relative to the root plugins directory. |
| 337 | */ |
| 338 | public function get_plugins_excluded_from_compatibility_ui() { |
| 339 | return $this->plugins_excluded_from_compatibility_ui; |
| 340 | } |
| 341 | } |
| 342 |