PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
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 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / woocommerce-analytics / src / class-woocommerce-analytics.php

class-woocommerce-analytics.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.7, at jetpack_vendor/automattic/woocommerce-analytics/src/class-woocommerce-analytics.php

502 lines 16.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Main class for the WooCommerce Analytics package.
4 * Originally ported from the Jetpack_Google_Analytics code.
5 *
6 * @package automattic/woocommerce-analytics
7 */
8
9 namespace Automattic;
10
11 use Automattic\Jetpack\Assets;
12 use Automattic\Jetpack\Connection\Manager as Jetpack_Connection;
13 use Automattic\Woocommerce_Analytics\My_Account;
14 use Automattic\Woocommerce_Analytics\Universal;
15 use Automattic\Woocommerce_Analytics\WC_Analytics_Tracking_Proxy;
16 use Composer\InstalledVersions;
17
18 /**
19 * Instantiate WooCommerce Analytics
20 */
21 class Woocommerce_Analytics {
22 /**
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.
26 */
27 const PACKAGE_VERSION = '0.18.0';
28
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 /**
68 * Initializer.
69 * Used to configure the WooCommerce Analytics package.
70 *
71 * @return void
72 */
73 public static function init() {
74 if ( ! self::should_track_store() || did_action( 'woocommerce_analytics_init' ) ) {
75 return;
76 }
77
78 // loading _wca.
79 add_action( 'wp_head', array( __CLASS__, 'wp_head_top' ), 1 );
80
81 // loading s.js.
82 add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_tracking_script' ) );
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.
88 add_action( 'init', array( new Universal(), 'init_hooks' ) );
89 add_action( 'init', array( new My_Account(), 'init_hooks' ) );
90
91 // Initialize REST API endpoints.
92 add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
93
94 /**
95 * Fires after the WooCommerce Analytics package is initialized
96 *
97 * @since 0.1.5
98 */
99 do_action( 'woocommerce_analytics_init' );
100 }
101
102 /**
103 * WooCommerce Analytics is only available to Jetpack connected WooCommerce stores
104 * with WooCommerce version 3.0 or higher
105 *
106 * @return bool
107 */
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
112 // Ensure this is available, even with mu-plugins.
113 if ( ! function_exists( 'is_plugin_active' ) ) {
114 require_once ABSPATH . 'wp-admin/includes/plugin.php';
115 }
116
117 /**
118 * Make sure WooCommerce is installed and active
119 *
120 * This action is documented in https://docs.woocommerce.com/document/create-a-plugin
121 */
122 if ( ! defined( 'WC_PLUGIN_FILE' ) || ! is_plugin_active( plugin_basename( WC_PLUGIN_FILE ) ) ) {
123 return false;
124 }
125 // Ensure the WooCommerce class exists and is a valid version.
126 $minimum_woocommerce_active = class_exists( 'WooCommerce' ) && version_compare( \WC_VERSION, '3.0', '>=' );
127 if ( ! $minimum_woocommerce_active ) {
128 return false;
129 }
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
144 // Tracking only Site pages.
145 if ( is_admin() || wp_doing_ajax() || wp_is_xml_request() || is_login() || is_feed() || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
146 return false;
147 }
148
149 // Make sure the site is connected to WordPress.com.
150 if ( ! ( new Jetpack_Connection() )->is_connected() ) {
151 return false;
152 }
153
154 return true;
155 }
156
157 /**
158 * Make _wca available to queue events
159 */
160 public static function wp_head_top() {
161 if ( is_cart() || is_checkout() || is_checkout_pay_page() || is_order_received_page() || is_add_payment_method_page() ) {
162 echo '<script>window._wca_prevent_referrer = true;</script>' . "\r\n";
163 }
164 echo '<script>window._wca = window._wca || [];</script>' . "\r\n";
165 }
166
167 /**
168 * Place script to call s.js, Store Analytics.
169 */
170 public static function enqueue_tracking_script() {
171 $url = sprintf(
172 'https://stats.wp.com/s-%d.js',
173 gmdate( 'YW' )
174 );
175
176 wp_enqueue_script(
177 'woocommerce-analytics',
178 $url,
179 array(),
180 null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- The version is set in the URL.
181 array(
182 'in_footer' => false,
183 'strategy' => 'defer',
184 )
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;
500 }
501 }
502