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