PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/woocommerce-analytics/src/class-woocommerce-analytics.php +384 -11 13.6.2 → 16.3 View file →
@@ -7,12 +7,14 @@
7 7 */
8 8
9 9 namespace Automattic;
10 10
11 +use Automattic\Jetpack\Assets;
11 12 use Automattic\Jetpack\Connection\Manager as Jetpack_Connection;
12 -use Automattic\Woocommerce_Analytics\Checkout_Flow;
13 13 use Automattic\Woocommerce_Analytics\My_Account;
14 14 use Automattic\Woocommerce_Analytics\Universal;
15 +use Automattic\Woocommerce_Analytics\WC_Analytics_Tracking_Proxy;
16 +use Composer\InstalledVersions;
15 17
16 18 /**
17 19 * Instantiate WooCommerce Analytics
18 20 */
@@ -17,13 +19,53 @@
17 19 * Instantiate WooCommerce Analytics
18 20 */
19 21 class Woocommerce_Analytics {
20 22 /**
21 - * Package version.
23 + * Package version, also the key that triggers the proxy speed module refresh. Rewritten from
24 + * composer.json by tasks/build-package.sh when the package is published, so bumping it here
25 + * does not refresh published copies. Bump the version in composer.json instead.
22 26 */
23 - const PACKAGE_VERSION = '0.1.7';
27 + const PACKAGE_VERSION = '0.18.0';
24 28
25 29 /**
30 + * Proxy speed module version option.
31 + *
32 + * @var string
33 + */
34 + const PROXY_SPEED_MODULE_VERSION_OPTION = 'woocommerce_analytics_proxy_speed_module_version';
35 +
36 + /**
37 + * Proxy speed module version check transient.
38 + *
39 + * @var string
40 + */
41 + const PROXY_SPEED_MODULE_VERSION_CHECK_TRANSIENT = 'woocommerce_analytics_proxy_speed_module_version_check';
42 +
43 + /**
44 + * Whether the MU-plugin speed module may serve requests.
45 + *
46 + * It reads this option because filters are unavailable before plugins load.
47 + * It tracks installation eligibility, so removal cannot reauthorize the module.
48 + *
49 + * @since 0.18.0
50 + *
51 + * @var string
52 + */
53 + const PROXY_SPEED_MODULE_AUTHORIZED_OPTION = 'woocommerce_analytics_proxy_speed_module_authorized';
54 +
55 + /**
56 + * Whether proxy tracking has ever been enabled on this site.
57 + *
58 + * Keeps the REST route registered to return 403 to cached pages after the
59 + * feature is disabled. Clear it only after those cached pages have expired.
60 + *
61 + * @since 0.18.0
62 + *
63 + * @var string
64 + */
65 + const PROXY_TRACKING_EVER_ENABLED_OPTION = 'woocommerce_analytics_proxy_tracking_ever_enabled';
66 +
67 + /**
26 68 * Initializer.
27 69 * Used to configure the WooCommerce Analytics package.
28 70 *
29 71 * @return void
@@ -38,17 +80,18 @@
38 80
39 81 // loading s.js.
40 82 add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_tracking_script' ) );
41 83
84 + // loading client-side analytics script.
85 + add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_client_script' ) );
86 +
87 + // Initialize general store tracking actions.
42 88 add_action( 'init', array( new Universal(), 'init_hooks' ) );
43 89 add_action( 'init', array( new My_Account(), 'init_hooks' ) );
44 - if (
45 - class_exists( '\Automattic\WooCommerce\Blocks\Package' )
46 - && version_compare( \Automattic\WooCommerce\Blocks\Package::get_version(), '11.6.2', '>=' )
47 - ) {
48 - add_action( 'init', array( new Checkout_Flow(), 'init_hooks' ) );
49 - }
50 90
91 + // Initialize REST API endpoints.
92 + add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
93 +
51 94 /**
52 95 * Fires after the WooCommerce Analytics package is initialized
53 96 *
54 97 * @since 0.1.5
@@ -62,8 +105,11 @@
62 105 *
63 106 * @return bool
64 107 */
65 108 public static function should_track_store() {
109 + // Register before early returns so the MU-plugin does not keep stale state.
110 + add_action( 'init', array( __CLASS__, 'sync_proxy_tracking_state' ), 20 );
111 +
66 112 // Ensure this is available, even with mu-plugins.
67 113 if ( ! function_exists( 'is_plugin_active' ) ) {
68 114 require_once ABSPATH . 'wp-admin/includes/plugin.php';
69 115 }
@@ -72,9 +118,9 @@
72 118 * Make sure WooCommerce is installed and active
73 119 *
74 120 * This action is documented in https://docs.woocommerce.com/document/create-a-plugin
75 121 */
76 - if ( ! is_plugin_active( 'woocommerce/woocommerce.php' ) ) {
122 + if ( ! defined( 'WC_PLUGIN_FILE' ) || ! is_plugin_active( plugin_basename( WC_PLUGIN_FILE ) ) ) {
77 123 return false;
78 124 }
79 125 // Ensure the WooCommerce class exists and is a valid version.
80 126 $minimum_woocommerce_active = class_exists( 'WooCommerce' ) && version_compare( \WC_VERSION, '3.0', '>=' );
@@ -81,10 +127,23 @@
81 127 if ( ! $minimum_woocommerce_active ) {
82 128 return false;
83 129 }
84 130
131 + // Ensure the WC Tracks classes exist.
132 + if ( ! class_exists( 'WC_Tracks' ) ) {
133 + if ( ! defined( 'WC_ABSPATH' ) || ! file_exists( WC_ABSPATH . 'includes/tracks/class-wc-tracks.php' ) ) {
134 + return false;
135 + }
136 +
137 + include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks.php';
138 + include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-event.php';
139 + include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-client.php';
140 + }
141 +
142 + add_action( 'admin_init', array( __CLASS__, 'maybe_update_proxy_speed_module' ) );
143 +
85 144 // Tracking only Site pages.
86 - if ( is_admin() ) {
145 + if ( is_admin() || wp_doing_ajax() || wp_is_xml_request() || is_login() || is_feed() || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
87 146 return false;
88 147 }
89 148
90 149 // Make sure the site is connected to WordPress.com.
@@ -123,6 +182,320 @@
123 182 'in_footer' => false,
124 183 'strategy' => 'defer',
125 184 )
126 185 );
186 + }
187 +
188 + /**
189 + * Enqueue client-side analytics script.
190 + */
191 + public static function enqueue_client_script() {
192 + Assets::register_script(
193 + 'woocommerce-analytics-client',
194 + '../build/woocommerce-analytics-client.js',
195 + __FILE__,
196 + array(
197 + 'in_footer' => true,
198 + 'strategy' => 'defer',
199 + 'enqueue' => true,
200 + )
201 + );
202 + }
203 +
204 + /**
205 + * Register REST API routes.
206 + *
207 + * A site that has never used proxy tracking does not get the endpoint. It stays
208 + * registered after being disabled, so cached pages receive a visible 403.
209 + */
210 + public static function register_rest_routes() {
211 + if ( 'yes' !== get_option( self::PROXY_TRACKING_EVER_ENABLED_OPTION ) ) {
212 + return;
213 + }
214 +
215 + $controller = new WC_Analytics_Tracking_Proxy();
216 + $controller->register_routes();
217 + }
218 +
219 + /**
220 + * Sync proxy tracking state for the REST route and MU-plugin speed module.
221 + *
222 + * @since 0.18.0
223 + *
224 + * @return void
225 + */
226 + public static function sync_proxy_tracking_state() {
227 + if ( \Automattic\Woocommerce_Analytics\Features::is_proxy_tracking_enabled()
228 + && 'yes' !== get_option( self::PROXY_TRACKING_EVER_ENABLED_OPTION ) ) {
229 + update_option( self::PROXY_TRACKING_EVER_ENABLED_OPTION, 'yes' );
230 + }
231 +
232 + // Track module eligibility so a removed module cannot be reauthorized.
233 + $authorized = self::should_install_proxy_speed_module() ? 'yes' : 'no';
234 + $current = get_option( self::PROXY_SPEED_MODULE_AUTHORIZED_OPTION );
235 +
236 + if ( $current === $authorized ) {
237 + return;
238 + }
239 +
240 + // An absent option already means unauthorized, so most sites installing this
241 + // package never need a row saying so.
242 + if ( false === $current && 'no' === $authorized ) {
243 + return;
244 + }
245 +
246 + update_option( self::PROXY_SPEED_MODULE_AUTHORIZED_OPTION, $authorized );
247 + }
248 +
249 + /**
250 + * Forget that proxy tracking was enabled, so the REST route is unregistered.
251 + *
252 + * This is separate from removing the speed module because cached pages may remain.
253 + *
254 + * @since 0.18.0
255 + *
256 + * @return void
257 + */
258 + public static function reset_proxy_tracking_state() {
259 + delete_option( self::PROXY_TRACKING_EVER_ENABLED_OPTION );
260 + delete_option( self::PROXY_SPEED_MODULE_AUTHORIZED_OPTION );
261 + }
262 +
263 + /**
264 + * Update the proxy speed module.
265 + *
266 + * The module must refuse itself because this periodic check can be delayed.
267 + */
268 + public static function maybe_update_proxy_speed_module() {
269 + // Skip if we've already checked recently.
270 + if ( get_transient( self::PROXY_SPEED_MODULE_VERSION_CHECK_TRANSIENT ) ) {
271 + return;
272 + }
273 +
274 + $version = get_option( self::PROXY_SPEED_MODULE_VERSION_OPTION, false );
275 +
276 + if ( self::should_install_proxy_speed_module() ) {
277 + if ( $version !== self::PACKAGE_VERSION ) {
278 + self::maybe_add_proxy_speed_module();
279 + }
280 + } elseif ( $version !== false ) {
281 + self::maybe_remove_proxy_speed_module();
282 + }
283 +
284 + // Set the transient after the update attempt to prevent checking on every admin_init.
285 + // If the update failed, it will be retried after the transient expires (1 day).
286 + set_transient( self::PROXY_SPEED_MODULE_VERSION_CHECK_TRANSIENT, 1, DAY_IN_SECONDS );
287 + }
288 +
289 + /**
290 + * Whether the proxy speed module belongs on this site.
291 + *
292 + * It requires its own opt-in and proxy tracking, so it cannot serve when
293 + * tracking is disabled.
294 + *
295 + * @since 0.18.0
296 + *
297 + * @return bool
298 + */
299 + private static function should_install_proxy_speed_module() {
300 + return \Automattic\Woocommerce_Analytics\Features::is_proxy_speed_module_enabled()
301 + && \Automattic\Woocommerce_Analytics\Features::is_proxy_tracking_enabled();
302 + }
303 +
304 + /**
305 + * Maybe add proxy speed module.
306 + */
307 + public static function maybe_add_proxy_speed_module() {
308 + if ( ! self::should_install_proxy_speed_module() ) {
309 + return;
310 + }
311 +
312 + // Write before the module file exists because it fails closed on this option.
313 + self::sync_proxy_tracking_state();
314 +
315 + if ( ! self::init_filesystem() ) {
316 + if ( function_exists( 'wc_get_logger' ) ) {
317 + wc_get_logger()->error( 'WooCommerce Analytics proxy speed module not installed: filesystem unavailable.', array( 'source' => 'woocommerce-analytics' ) );
318 + }
319 + return;
320 + }
321 +
322 + global $wp_filesystem;
323 +
324 + // Create the mu-plugin directory if it doesn't exist.
325 + if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
326 + wp_mkdir_p( WPMU_PLUGIN_DIR );
327 + }
328 +
329 + // If the mu-plugin directory doesn't exist, we can't copy the files.
330 + if ( ! is_dir( WPMU_PLUGIN_DIR ) ) {
331 + if ( function_exists( 'wc_get_logger' ) ) {
332 + wc_get_logger()->error( 'WooCommerce Analytics proxy speed module not installed: mu-plugins directory could not be created.', array( 'source' => 'woocommerce-analytics' ) );
333 + }
334 + return;
335 + }
336 +
337 + // Check if the mu-plugin directory is writable.
338 + if ( ! $wp_filesystem->is_writable( WPMU_PLUGIN_DIR ) ) {
339 + if ( function_exists( 'wc_get_logger' ) ) {
340 + wc_get_logger()->debug( 'WooCommerce Analytics proxy speed module not installed: mu-plugins directory is not writable.', array( 'source' => 'woocommerce-analytics' ) );
341 + }
342 + return;
343 + }
344 +
345 + if ( get_option( self::PROXY_SPEED_MODULE_VERSION_OPTION ) === self::PACKAGE_VERSION ) {
346 + // No need to copy the files again.
347 + return;
348 + }
349 +
350 + $mu_plugin_src_file = __DIR__ . '/mu-plugin/woocommerce-analytics-proxy-speed-module-template.php';
351 + $mu_plugin_dest_file = trailingslashit( WPMU_PLUGIN_DIR ) . 'woocommerce-analytics-proxy-speed-module.php';
352 +
353 + // Verify source file exists before attempting to copy.
354 + if ( ! file_exists( $mu_plugin_src_file ) ) {
355 + if ( function_exists( 'wc_get_logger' ) ) {
356 + wc_get_logger()->error( 'WooCommerce Analytics proxy speed module source file not found.', array( 'source' => 'woocommerce-analytics' ) );
357 + }
358 + return;
359 + }
360 +
361 + $content = $wp_filesystem->get_contents( $mu_plugin_src_file );
362 + if ( false === $content ) {
363 + if ( function_exists( 'wc_get_logger' ) ) {
364 + wc_get_logger()->error( 'Failed to read the WooCommerce Analytics proxy speed module source file.', array( 'source' => 'woocommerce-analytics' ) );
365 + }
366 + return;
367 + }
368 +
369 + // Get the autoloader path from the current plugin location.
370 + $autoloader_path = self::locate_autoloader_file();
371 + if ( null === $autoloader_path ) {
372 + if ( function_exists( 'wc_get_logger' ) ) {
373 + wc_get_logger()->error( 'WooCommerce Analytics proxy speed module not installed: could not locate autoloader.', array( 'source' => 'woocommerce-analytics' ) );
374 + }
375 + return;
376 + }
377 +
378 + // Replace placeholders with actual values.
379 + $content = str_replace(
380 + array( '{{AUTOLOADER_PATH}}', '{{VERSION}}' ),
381 + array( $autoloader_path, self::PACKAGE_VERSION ),
382 + $content
383 + );
384 +
385 + if ( ! $wp_filesystem->put_contents( $mu_plugin_dest_file, $content ) ) {
386 + if ( function_exists( 'wc_get_logger' ) ) {
387 + wc_get_logger()->error( 'Failed to write the WooCommerce Analytics proxy speed module file.', array( 'source' => 'woocommerce-analytics' ) );
388 + }
389 + return;
390 + }
391 +
392 + update_option( self::PROXY_SPEED_MODULE_VERSION_OPTION, self::PACKAGE_VERSION );
393 + }
394 +
395 + /**
396 + * Remove the proxy speed module when the plugin is deactivated.
397 + *
398 + * Clear its authorization because an undeletable MU-plugin can still load.
399 + */
400 + public static function maybe_remove_proxy_speed_module() {
401 + // Revoked before anything can fail: WP_Filesystem() returns false outright on
402 + // hosts that ask for credentials, and that is exactly the case where the file
403 + // survives and keeps loading.
404 + delete_option( self::PROXY_SPEED_MODULE_AUTHORIZED_OPTION );
405 +
406 + if ( ! self::init_filesystem() ) {
407 + return;
408 + }
409 +
410 + global $wp_filesystem;
411 +
412 + /**
413 + * Clean up MU plugin.
414 + */
415 + $file_path = trailingslashit( WPMU_PLUGIN_DIR ) . 'woocommerce-analytics-proxy-speed-module.php';
416 +
417 + if ( $wp_filesystem->exists( $file_path ) && $wp_filesystem->is_writable( $file_path ) ) {
418 + $deleted = $wp_filesystem->delete( $file_path );
419 + if ( ! $deleted && function_exists( 'wc_get_logger' ) ) {
420 + wc_get_logger()->error( 'Failed to delete WooCommerce Analytics proxy speed module file. The MU-plugin may continue running.', array( 'source' => 'woocommerce-analytics' ) );
421 + }
422 + }
423 +
424 + delete_option( self::PROXY_SPEED_MODULE_VERSION_OPTION );
425 + delete_transient( self::PROXY_SPEED_MODULE_VERSION_CHECK_TRANSIENT );
426 + }
427 +
428 + /**
429 + * Finds the path to the autoloader file.
430 + *
431 + * Uses multiple strategies to locate the autoloader, since this package
432 + * can be included in different plugins (Jetpack, WooCommerce Analytics, etc.):
433 + * 1. Jetpack autoloader global (if available)
434 + * 2. Composer's InstalledVersions API
435 + * 3. Directory-based guessing as a fallback
436 + *
437 + * @return string|null The path to the autoloader file, or null if not found.
438 + */
439 + private static function locate_autoloader_file() {
440 + global $jetpack_autoloader_loader;
441 +
442 + $autoload_file = null;
443 +
444 + // Try the Jetpack autoloader.
445 + if ( isset( $jetpack_autoloader_loader ) ) {
446 + $class_file = $jetpack_autoloader_loader->find_class_file( self::class );
447 + if ( $class_file ) {
448 + // Walk up 5 levels: src/ → woocommerce-analytics/ → automattic/ → jetpack_vendor/ → plugin root.
449 + $autoload_file = dirname( $class_file, 5 ) . '/vendor/autoload.php';
450 + }
451 + }
452 +
453 + // Try Composer's InstalledVersions API.
454 + if ( null === $autoload_file
455 + && is_callable( array( InstalledVersions::class, 'getInstallPath' ) )
456 + && InstalledVersions::isInstalled( 'automattic/woocommerce-analytics' )
457 + ) {
458 + $package_file = InstalledVersions::getInstallPath( 'automattic/woocommerce-analytics' );
459 + $expected_suffix = '/automattic/woocommerce-analytics';
460 + if ( substr( $package_file, -strlen( $expected_suffix ) ) === $expected_suffix ) {
461 + // Walk up 3 levels: woocommerce-analytics/ → automattic/ → jetpack_vendor/ → plugin root.
462 + $autoload_file = dirname( $package_file, 3 ) . '/vendor/autoload.php';
463 + }
464 + }
465 +
466 + // Guess based on directory structure.
467 + // First try standard vendor layout (vendor/automattic/woocommerce-analytics/src/),
468 + // then try standalone package with its own vendor dir.
469 + if ( null === $autoload_file ) {
470 + // Walk up 4 levels from src/: woocommerce-analytics/ → automattic/ → vendor/ → project root.
471 + $autoload_file = dirname( __DIR__, 4 ) . '/vendor/autoload.php';
472 + if ( ! file_exists( $autoload_file ) ) {
473 + $autoload_file = dirname( __DIR__ ) . '/vendor/autoload.php';
474 + }
475 + }
476 +
477 + if ( ! file_exists( $autoload_file ) ) {
478 + return null;
479 + }
480 +
481 + return $autoload_file;
482 + }
483 +
484 + /**
485 + * Initialize the WP filesystem.
486 + *
487 + * @return bool True if filesystem is initialized, false otherwise.
488 + */
489 + private static function init_filesystem() {
490 + if ( ! function_exists( 'WP_Filesystem' ) ) {
491 + require_once ABSPATH . 'wp-admin/includes/file.php';
492 + }
493 +
494 + // Initialize the WP filesystem.
495 + ob_start();
496 + $initialized = WP_Filesystem();
497 + ob_end_clean();
498 +
499 + return $initialized;
127 500 }
128 501 }