Completed
Push — try/another-block-button ( c033ce...50ed03 )
by
unknown
22:09 queued 13:07
created

wp_head_top()   A

Complexity

Conditions 6
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 2
nop 0
dl 0
loc 8
rs 9.2222
c 0
b 0
f 0
1
<?php
2
/**
3
 * Jetpack_WooCommerce_Analytics_Universal
4
 *
5
 * @package Jetpack
6
 * @author Automattic
7
 */
8
9
/**
10
 * Bail if accessed directly
11
 */
12
if ( ! defined( 'ABSPATH' ) ) {
13
	exit;
14
}
15
16
/**
17
 * Class Jetpack_WooCommerce_Analytics_Universal
18
 * Filters and Actions added to Store pages to perform analytics
19
 */
20
class Jetpack_WooCommerce_Analytics_Universal {
21
	/**
22
	 * Jetpack_WooCommerce_Analytics_Universal constructor.
23
	 */
24
	public function __construct() {
25
		// loading _wca
26
		add_action( 'wp_head', array( $this, 'wp_head_top' ), 1 );
27
28
		// add to carts from non-product pages or lists (search, store etc.)
29
		add_action( 'wp_head', array( $this, 'loop_session_events' ), 2 );
30
31
		// loading s.js.
32
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_tracking_script' ) );
33
34
		// Capture cart events
35
		add_action( 'woocommerce_add_to_cart', array( $this, 'capture_add_to_cart' ), 10, 6 );
36
37
		// single product page view
38
		add_action( 'woocommerce_after_single_product', array( $this, 'capture_product_view' ) );
39
40
		add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart' ) );
41
		add_action( 'woocommerce_after_mini_cart', array( $this, 'remove_from_cart' ) );
42
		add_action( 'wcct_before_cart_widget', array( $this, 'remove_from_cart' ) );
43
		add_filter( 'woocommerce_cart_item_remove_link', array( $this, 'remove_from_cart_attributes' ), 10, 2 );
44
45
		// Checkout.
46
		// Send events after checkout template (shortcode).
47
		add_action( 'woocommerce_after_checkout_form', array( $this, 'checkout_process' ) );
48
		// Send events after checkout block.
49
		add_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after', array( $this, 'checkout_process' ) );
50
51
		// order confirmed
52
		add_action( 'woocommerce_thankyou', array( $this, 'order_process' ), 10, 1 );
53
		add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart_via_quantity' ), 10, 1 );
54
	}
55
56
	/**
57
	 * Make _wca available to queue events
58
	 */
59
	public function wp_head_top() {
60
		if ( is_cart() || is_checkout() || is_checkout_pay_page() || is_order_received_page() || is_add_payment_method_page() ) {
61
			$prevent_referrer_code = '<script>window._wca_prevent_referrer = true;</script>';
62
			echo "$prevent_referrer_code\r\n";
63
		}
64
		$wca_code = '<script>window._wca = window._wca || [];</script>';
65
		echo "$wca_code\r\n";
66
	}
67
68
69
	/**
70
	 * Place script to call s.js, Store Analytics.
71
	 */
72
	public function enqueue_tracking_script() {
73
		$filename = sprintf(
74
			'https://stats.wp.com/s-%d.js',
75
			gmdate( 'YW' )
76
		);
77
78
		// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
79
		wp_enqueue_script( 'woocommerce-analytics', esc_url( $filename ), array(), null, false );
80
	}
81
82
	/**
83
	 * Default event properties which should be included with all events.
84
	 *
85
	 * @return array Array of standard event props.
86
	 */
87
	public function get_common_properties() {
88
		$site_info          = array(
89
			'blog_id'     => Jetpack::get_option( 'id' ),
90
			'ui'          => $this->get_user_id(),
91
			'url'         => home_url(),
92
			'woo_version' => WC()->version,
93
		);
94
		$cart_checkout_info = self::get_cart_checkout_info();
95
		return array_merge( $site_info, $cart_checkout_info );
96
	}
97
98
	/**
99
	 * Render tracks event properties as string of JavaScript object props.
100
	 *
101
	 * @param  array $properties Array of key/value pairs.
102
	 * @return string String of the form "key1: value1, key2: value2, " (etc).
103
	 */
104
	private function render_properties_as_js( $properties ) {
105
		$js_args_string = '';
106
		foreach ( $properties as $key => $value ) {
107
			$js_args_string = $js_args_string . "'$key': '" . esc_js( $value ) . "', ";
108
		}
109
		return $js_args_string;
110
	}
111
112
	/**
113
	 * Record an event with optional custom properties.
114
	 *
115
	 * @param string  $event_name The name of the event to record.
116
	 * @param integer $product_id The id of the product relating to the event.
117
	 * @param array   $properties Optional array of (key => value) event properties.
118
	 */
119
	public function record_event( $event_name, $product_id, $properties = array() ) {
120
		$product = wc_get_product( $product_id );
121
		if ( ! $product instanceof WC_Product ) {
0 ignored issues
show
Bug introduced by
The class WC_Product does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
122
			return;
123
		}
124
		$product_details = $this->get_product_details( $product );
125
126
		$all_props = array_merge(
127
			$properties,
128
			$this->get_common_properties()
129
		);
130
131
		wc_enqueue_js(
132
			"_wca.push( {
133
					'_en': '" . esc_js( $event_name ) . "',
134
					'pi': '" . esc_js( $product_id ) . "',
135
					'pn': '" . esc_js( $product_details['name'] ) . "',
136
					'pc': '" . esc_js( $product_details['category'] ) . "',
137
					'pp': '" . esc_js( $product_details['price'] ) . "',
138
					'pt': '" . esc_js( $product_details['type'] ) . "'," .
139
					$this->render_properties_as_js( $all_props ) . '
140
				} );'
141
		);
142
	}
143
144
	/**
145
	 * On product lists or other non-product pages, add an event listener to "Add to Cart" button click
146
	 */
147
	public function loop_session_events() {
148
		// Check for previous events queued in session data.
149
		if ( is_object( WC()->session ) ) {
150
			$data = WC()->session->get( 'wca_session_data' );
151
			if ( ! empty( $data ) ) {
152
				foreach ( $data as $data_instance ) {
153
					$this->record_event(
154
						$data_instance['event'],
155
						$data_instance['product_id'],
156
						array(
157
							'pq' => $data_instance['quantity'],
158
						)
159
					);
160
				}
161
				// Clear data, now that these events have been recorded.
162
				WC()->session->set( 'wca_session_data', '' );
163
			}
164
		}
165
	}
166
167
	/**
168
	 * On the cart page, add an event listener for removal of product click
169
	 */
170
	public function remove_from_cart() {
171
		$common_props = $this->render_properties_as_js(
172
			$this->get_common_properties()
173
		);
174
175
		// We listen at div.woocommerce because the cart 'form' contents get forcibly
176
		// updated and subsequent removals from cart would then not have this click
177
		// handler attached.
178
		wc_enqueue_js(
179
			"jQuery( 'div.woocommerce' ).on( 'click', 'a.remove', function() {
180
				var productID = jQuery( this ).data( 'product_id' );
181
				var quantity = jQuery( this ).parent().parent().find( '.qty' ).val()
182
				var productDetails = {
183
					'id': productID,
184
					'quantity': quantity ? quantity : '1',
185
				};
186
				_wca.push( {
187
					'_en': 'woocommerceanalytics_remove_from_cart',
188
					'pi': productDetails.id,
189
					'pq': productDetails.quantity, " .
190
					$common_props . '
191
				} );
192
			} );'
193
		);
194
	}
195
196
	/**
197
	 * Adds the product ID to the remove product link (for use by remove_from_cart above) if not present
198
	 *
199
	 * @param string $url Full HTML a tag of the link to remove an item from the cart.
200
	 * @param string $key Unique Key ID for a cart item.
201
	 *
202
	 * @return mixed.
0 ignored issues
show
Documentation introduced by
The doc-type mixed. could not be parsed: Unknown type name "mixed." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
203
	 */
204 View Code Duplication
	public function remove_from_cart_attributes( $url, $key ) {
205
		if ( false !== strpos( $url, 'data-product_id' ) ) {
206
			return $url;
207
		}
208
209
		$item    = WC()->cart->get_cart_item( $key );
210
		$product = $item['data'];
211
212
		$new_attributes = sprintf(
213
			'" data-product_id="%s">',
214
			esc_attr( $product->get_id() )
215
		);
216
217
		$url = str_replace( '">', $new_attributes, $url );
218
		return $url;
219
	}
220
221
	/**
222
	 * Gather relevant product information
223
	 *
224
	 * @param array $product product
225
	 * @return array
226
	 */
227
	public function get_product_details( $product ) {
228
		return array(
229
			'id'       => $product->get_id(),
0 ignored issues
show
Bug introduced by
The method get_id cannot be called on $product (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
230
			'name'     => $product->get_title(),
0 ignored issues
show
Bug introduced by
The method get_title cannot be called on $product (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
231
			'category' => $this->get_product_categories_concatenated( $product ),
0 ignored issues
show
Documentation introduced by
$product is of type array, but the function expects a object.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
232
			'price'    => $product->get_price(),
0 ignored issues
show
Bug introduced by
The method get_price cannot be called on $product (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
233
			'type'     => $product->get_type(),
0 ignored issues
show
Bug introduced by
The method get_type cannot be called on $product (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
234
		);
235
	}
236
237
	/**
238
	 * Track a product page view
239
	 */
240
	public function capture_product_view() {
241
		global $product;
242
		$this->record_event(
243
			'woocommerceanalytics_product_view',
244
			$product->get_id()
245
		);
246
	}
247
248
	/**
249
	 * On the Checkout page, trigger an event for each product in the cart
250
	 */
251
	public function checkout_process() {
252
		$cart = WC()->cart->get_cart();
253
254
		foreach ( $cart as $cart_item_key => $cart_item ) {
255
			/**
256
			* This filter is already documented in woocommerce/templates/cart/cart.php
257
			*/
258
			$product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
259
260
			if ( ! $product ) {
261
				continue;
262
			}
263
264
			$this->record_event(
265
				'woocommerceanalytics_product_checkout',
266
				$product->get_id(),
267
				array(
268
					'pq' => $cart_item['quantity'],
269
				)
270
			);
271
		}
272
	}
273
274
	/**
275
	 * After the checkout process, fire an event for each item in the order
276
	 *
277
	 * @param string $order_id Order Id.
278
	 */
279
	public function order_process( $order_id ) {
280
		$order = wc_get_order( $order_id );
281
282
		// loop through products in the order and queue a purchase event.
283
		foreach ( $order->get_items() as $order_item_id => $order_item ) {
284
			$product = $order->get_product_from_item( $order_item );
285
286
			$this->record_event(
287
				'woocommerceanalytics_product_purchase',
288
				$product->get_id(),
289
				array(
290
					'oi' => $order->get_order_number(),
291
					'pq' => $order_item->get_quantity(),
292
				)
293
			);
294
		}
295
	}
296
297
	/**
298
	 * Listen for clicks on the "Update Cart" button to know if an item has been removed by
299
	 * updating its quantity to zero
300
	 */
301
	public function remove_from_cart_via_quantity() {
302
		$common_props = $this->render_properties_as_js(
303
			$this->get_common_properties()
304
		);
305
306
		wc_enqueue_js(
307
			"
308
			jQuery( 'button[name=update_cart]' ).on( 'click', function() {
309
				var cartItems = jQuery( '.cart_item' );
310
				cartItems.each( function( item ) {
311
					var qty = jQuery( this ).find( 'input.qty' );
312
					if ( qty && qty.val() === '0' ) {
313
						var productID = jQuery( this ).find( '.product-remove a' ).data( 'product_id' );
314
						_wca.push( {
315
							'_en': 'woocommerceanalytics_remove_from_cart',
316
							'pi': productID, " .
317
							$common_props . '
318
						} );
319
					}
320
				} );
321
			} );'
322
		);
323
	}
324
325
	/**
326
	 * Get the current user id
327
	 *
328
	 * @return int
329
	 */
330
	public function get_user_id() {
331
		if ( is_user_logged_in() ) {
332
			$blogid = Jetpack::get_option( 'id' );
333
			$userid = get_current_user_id();
334
			return $blogid . ':' . $userid;
335
		}
336
		return 'null';
337
	}
338
339
	/**
340
	 * @param $cart_item_key
341
	 * @param $product_id
342
	 * @param $quantity
343
	 * @param $variation_id
344
	 * @param $variation
345
	 * @param $cart_item_data
346
	 */
347
	public function capture_add_to_cart( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) {
348
		$referer_postid = isset( $_SERVER['HTTP_REFERER'] ) ? url_to_postid( $_SERVER['HTTP_REFERER'] ) : 0;
349
		// if the referring post is not a product OR the product being added is not the same as post
350
		// (eg. related product list on single product page) then include a product view event
351
		$product_by_referer_postid = wc_get_product( $referer_postid );
352
		if ( ! $product_by_referer_postid instanceof WC_Product || (int) $product_id !== $referer_postid ) {
0 ignored issues
show
Bug introduced by
The class WC_Product does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
353
			$this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_product_view' );
354
		}
355
		// add cart event to the session data
356
		$this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_add_to_cart' );
357
	}
358
359
	/**
360
	 * @param $product_id
361
	 * @param $quantity
362
	 * @param $event
363
	 */
364
	public function capture_event_in_session_data( $product_id, $quantity, $event ) {
365
366
		$product = wc_get_product( $product_id );
367
		if ( ! $product instanceof WC_Product ) {
0 ignored issues
show
Bug introduced by
The class WC_Product does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
368
			return;
369
		}
370
371
		$quantity = ( $quantity == 0 ) ? 1 : $quantity;
372
373
		// check for existing data
374
		if ( is_object( WC()->session ) ) {
375
			$data = WC()->session->get( 'wca_session_data' );
376
			if ( empty( $data ) || ! is_array( $data ) ) {
377
				$data = array();
378
			}
379
		} else {
380
			$data = array();
381
		}
382
383
		// extract new event data
384
		$new_data = array(
385
			'event'      => $event,
386
			'product_id' => (string) $product_id,
387
			'quantity'   => (string) $quantity,
388
		);
389
390
		// append new data
391
		$data[] = $new_data;
392
393
		WC()->session->set( 'wca_session_data', $data );
394
	}
395
396
	/**
397
	 * Gets product categories or varation attributes as a formatted concatenated string
398
	 *
399
	 * @param object $product WC_Product.
400
	 * @return string
401
	 */
402 View Code Duplication
	public function get_product_categories_concatenated( $product ) {
403
404
		if ( ! $product instanceof WC_Product ) {
0 ignored issues
show
Bug introduced by
The class WC_Product does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
405
			return '';
406
		}
407
408
		$variation_data = $product->is_type( 'variation' ) ? wc_get_product_variation_attributes( $product->get_id() ) : '';
409
		if ( is_array( $variation_data ) && ! empty( $variation_data ) ) {
410
			$line = wc_get_formatted_variation( $variation_data, true );
411
		} else {
412
			$out        = array();
413
			$categories = get_the_terms( $product->get_id(), 'product_cat' );
414
			if ( $categories ) {
415
				foreach ( $categories as $category ) {
416
					$out[] = $category->name;
417
				}
418
			}
419
			$line = join( '/', $out );
420
		}
421
		return $line;
422
	}
423
424
	/**
425
	 * Search a specific post for text content.
426
	 *
427
	 * Note: similar code is in a WooCommerce core PR:
428
	 * https://github.com/woocommerce/woocommerce/pull/25932
429
	 *
430
	 * @param integer $post_id The id of the post to search.
431
	 * @param string  $text    The text to search for.
432
	 * @return integer 1 if post contains $text (otherwise 0).
433
	 */
434
	public static function post_contains_text( $post_id, $text ) {
435
		global $wpdb;
436
437
		// Search for the text anywhere in the post.
438
		$wildcarded = "%{$text}%";
439
440
		$result = $wpdb->get_var(
441
			$wpdb->prepare(
442
				"
443
				SELECT COUNT( * ) FROM {$wpdb->prefix}posts
444
				WHERE ID=%d
445
				AND {$wpdb->prefix}posts.post_content LIKE %s
446
				",
447
				array( $post_id, $wildcarded )
448
			)
449
		);
450
451
		return ( '0' !== $result ) ? 1 : 0;
452
	}
453
454
	/**
455
	 * Get info about the cart & checkout pages, in particular
456
	 * whether the store is using shortcodes or Gutenberg blocks.
457
	 * This info is cached in a transient.
458
	 *
459
	 * Note: similar code is in a WooCommerce core PR:
460
	 * https://github.com/woocommerce/woocommerce/pull/25932
461
	 *
462
	 * @return array
463
	 */
464
	public static function get_cart_checkout_info() {
465
		$transient_name = 'jetpack_woocommerce_analytics_cart_checkout_info_cache';
466
467
		$info = get_transient( $transient_name );
468
		if ( false === $info ) {
469
			$cart_page_id     = wc_get_page_id( 'cart' );
470
			$checkout_page_id = wc_get_page_id( 'checkout' );
471
472
			$info = array(
473
				'cart_page_contains_cart_block'         => self::post_contains_text(
474
					$cart_page_id,
475
					'<!-- wp:woocommerce/cart'
476
				),
477
				'cart_page_contains_cart_shortcode'     => self::post_contains_text(
478
					$cart_page_id,
479
					'[woocommerce_cart]'
480
				),
481
				'checkout_page_contains_checkout_block' => self::post_contains_text(
482
					$checkout_page_id,
483
					'<!-- wp:woocommerce/checkout'
484
				),
485
				'checkout_page_contains_checkout_shortcode' => self::post_contains_text(
486
					$checkout_page_id,
487
					'[woocommerce_checkout]'
488
				),
489
			);
490
491
			set_transient( $transient_name, $info, DAY_IN_SECONDS );
492
		}
493
494
		return $info;
495
	}
496
}
497