Completed
Push — add/benefits-api-endpoint ( d54787...0ef760 )
by Jeremy
10:26 queued 03:30
created

enqueue_tracking_script()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 9
rs 9.9666
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_head', array( $this, 'wp_head_bottom' ), 999999 );
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
		// cart checkout
46
		add_action( 'woocommerce_after_checkout_form', array( $this, 'checkout_process' ) );
47
48
		// order confirmed
49
		add_action( 'woocommerce_thankyou', array( $this, 'order_process' ), 10, 1 );
50
		add_action( 'woocommerce_after_cart', array( $this, 'remove_from_cart_via_quantity' ), 10, 1 );
51
	}
52
53
	/**
54
	 * Make _wca available to queue events
55
	 */
56
	public function wp_head_top() {
57
		if ( is_cart() || is_checkout() || is_checkout_pay_page() || is_order_received_page() || is_add_payment_method_page() ) {
58
			$prevent_referrer_code = '<script>window._wca_prevent_referrer = true;</script>';
59
			echo "$prevent_referrer_code\r\n";
60
		}
61
		$wca_code = '<script>window._wca = window._wca || [];</script>';
62
		echo "$wca_code\r\n";
63
	}
64
65
66
	/**
67
	 * Place script to call s.js, Store Analytics
68
	 */
69
	public function wp_head_bottom() {
70
		$filename   = 's-' . gmdate( 'YW' ) . '.js';
71
		$async_code = "<script async src='https://stats.wp.com/" . $filename . "'></script>";
72
		echo "$async_code\r\n";
73
	}
74
75
	/**
76
	 * On product lists or other non-product pages, add an event listener to "Add to Cart" button click
77
	 */
78
	public function loop_session_events() {
79
		$blogid = Jetpack::get_option( 'id' );
80
81
		// check for previous add-to-cart cart events
82
		if ( is_object( WC()->session ) ) {
83
			$data = WC()->session->get( 'wca_session_data' );
84
			if ( ! empty( $data ) ) {
85
				foreach ( $data as $data_instance ) {
86
					$product = wc_get_product( $data_instance['product_id'] );
87
					if ( ! $product ) {
88
						continue;
89
					}
90
					$product_details = $this->get_product_details( $product );
91
					wc_enqueue_js(
92
						"_wca.push( {
93
								'_en': '" . esc_js( $data_instance['event'] ) . "',
94
								'blog_id': '" . esc_js( $blogid ) . "',
95
								'pi': '" . esc_js( $data_instance['product_id'] ) . "',
96
								'pn': '" . esc_js( $product_details['name'] ) . "',
97
								'pc': '" . esc_js( $product_details['category'] ) . "',
98
								'pp': '" . esc_js( $product_details['price'] ) . "',
99
								'pq': '" . esc_js( $data_instance['quantity'] ) . "',
100
								'pt': '" . esc_js( $product_details['type'] ) . "',
101
								'ui': '" . esc_js( $this->get_user_id() ) . "',
102
							} );"
103
					);
104
				}
105
				// clear data
106
				WC()->session->set( 'wca_session_data', '' );
107
			}
108
		}
109
	}
110
111
	/**
112
	 * On the cart page, add an event listener for removal of product click
113
	 */
114 View Code Duplication
	public function remove_from_cart() {
115
116
		// We listen at div.woocommerce because the cart 'form' contents get forcibly
117
		// updated and subsequent removals from cart would then not have this click
118
		// handler attached.
119
		$blogid = Jetpack::get_option( 'id' );
120
		wc_enqueue_js(
121
			"jQuery( 'div.woocommerce' ).on( 'click', 'a.remove', function() {
122
				var productID = jQuery( this ).data( 'product_id' );
123
				var quantity = jQuery( this ).parent().parent().find( '.qty' ).val()
124
				var productDetails = {
125
					'id': productID,
126
					'quantity': quantity ? quantity : '1',
127
				};
128
				_wca.push( {
129
					'_en': 'woocommerceanalytics_remove_from_cart',
130
					'blog_id': '" . esc_js( $blogid ) . "',
131
					'pi': productDetails.id,
132
					'pq': productDetails.quantity,
133
					'ui': '" . esc_js( $this->get_user_id() ) . "',
134
				} );
135
			} );"
136
		);
137
	}
138
139
	/**
140
	 * Adds the product ID to the remove product link (for use by remove_from_cart above) if not present
141
	 *
142
	 * @param string $url Full HTML a tag of the link to remove an item from the cart.
143
	 * @param string $key Unique Key ID for a cart item.
144
	 *
145
	 * @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...
146
	 */
147 View Code Duplication
	public function remove_from_cart_attributes( $url, $key ) {
148
		if ( false !== strpos( $url, 'data-product_id' ) ) {
149
			return $url;
150
		}
151
152
		$item    = WC()->cart->get_cart_item( $key );
153
		$product = $item['data'];
154
155
		$new_attributes = sprintf(
156
			'" data-product_id="%s">',
157
			esc_attr( $product->get_id() )
158
		);
159
160
		$url = str_replace( '">', $new_attributes, $url );
161
		return $url;
162
	}
163
164
	/**
165
	 * Gather relevant product information
166
	 *
167
	 * @param array $product product
168
	 * @return array
169
	 */
170
	public function get_product_details( $product ) {
171
		return array(
172
			'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...
173
			'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...
174
			'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...
175
			'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...
176
			'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...
177
		);
178
	}
179
180
	/**
181
	 * Track a product page view
182
	 */
183
	public function capture_product_view() {
184
185
		global $product;
186
		$blogid          = Jetpack::get_option( 'id' );
187
		$product_details = $this->get_product_details( $product );
188
189
		wc_enqueue_js(
190
			"_wca.push( {
191
				'_en': 'woocommerceanalytics_product_view',
192
				'blog_id': '" . esc_js( $blogid ) . "',
193
				'pi': '" . esc_js( $product_details['id'] ) . "',
194
				'pn': '" . esc_js( $product_details['name'] ) . "',
195
				'pc': '" . esc_js( $product_details['category'] ) . "',
196
				'pp': '" . esc_js( $product_details['price'] ) . "',
197
				'pt': '" . esc_js( $product_details['type'] ) . "',
198
				'ui': '" . esc_js( $this->get_user_id() ) . "',
199
			} );"
200
		);
201
	}
202
203
	/**
204
	 * On the Checkout page, trigger an event for each product in the cart
205
	 */
206
	public function checkout_process() {
207
208
		$universal_commands = array();
209
		$cart               = WC()->cart->get_cart();
210
		$blogid             = Jetpack::get_option( 'id' );
211
212
		foreach ( $cart as $cart_item_key => $cart_item ) {
213
			/**
214
			* This filter is already documented in woocommerce/templates/cart/cart.php
215
			*/
216
			$product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
217
218
			if ( ! $product ) {
219
				continue;
220
			}
221
222
			$product_details = $this->get_product_details( $product );
223
224
			$universal_commands[] = "_wca.push( {
225
				'_en': 'woocommerceanalytics_product_checkout',
226
				'blog_id': '" . esc_js( $blogid ) . "',
227
				'pi': '" . esc_js( $product_details['id'] ) . "',
228
				'pn': '" . esc_js( $product_details['name'] ) . "',
229
				'pc': '" . esc_js( $product_details['category'] ) . "',
230
				'pp': '" . esc_js( $product_details['price'] ) . "',
231
				'pq': '" . esc_js( $cart_item['quantity'] ) . "',
232
				'pt': '" . esc_js( $product_details['type'] ) . "',
233
				'ui': '" . esc_js( $this->get_user_id() ) . "',
234
			} );";
235
		}
236
237
		wc_enqueue_js( implode( "\r\n", $universal_commands ) );
238
	}
239
240
	/**
241
	 * After the checkout process, fire an event for each item in the order
242
	 *
243
	 * @param string $order_id Order Id.
244
	 */
245
	public function order_process( $order_id ) {
246
		$order              = wc_get_order( $order_id );
247
		$universal_commands = array();
248
		$blogid             = Jetpack::get_option( 'id' );
249
250
		// loop through products in the order and queue a purchase event.
251
		foreach ( $order->get_items() as $order_item_id => $order_item ) {
252
			$product = $order->get_product_from_item( $order_item );
253
254
			$product_details = $this->get_product_details( $product );
255
256
			$universal_commands[] = "_wca.push( {
257
				'_en': 'woocommerceanalytics_product_purchase',
258
				'blog_id': '" . esc_js( $blogid ) . "',
259
				'pi': '" . esc_js( $product_details['id'] ) . "',
260
				'pn': '" . esc_js( $product_details['name'] ) . "',
261
				'pc': '" . esc_js( $product_details['category'] ) . "',
262
				'pp': '" . esc_js( $product_details['price'] ) . "',
263
				'pq': '" . esc_js( $order_item->get_quantity() ) . "',
264
				'pt': '" . esc_js( $product_details['type'] ) . "',
265
				'oi': '" . esc_js( $order->get_order_number() ) . "',
266
				'ui': '" . esc_js( $this->get_user_id() ) . "',
267
			} );";
268
		}
269
270
		wc_enqueue_js( implode( "\r\n", $universal_commands ) );
271
	}
272
273
	/**
274
	 * Listen for clicks on the "Update Cart" button to know if an item has been removed by
275
	 * updating its quantity to zero
276
	 */
277 View Code Duplication
	public function remove_from_cart_via_quantity() {
278
		$blogid = Jetpack::get_option( 'id' );
279
280
		wc_enqueue_js(
281
			"
282
			jQuery( 'button[name=update_cart]' ).on( 'click', function() {
283
				var cartItems = jQuery( '.cart_item' );
284
				cartItems.each( function( item ) {
285
					var qty = jQuery( this ).find( 'input.qty' );
286
					if ( qty && qty.val() === '0' ) {
287
						var productID = jQuery( this ).find( '.product-remove a' ).data( 'product_id' );
288
						_wca.push( {
289
							'_en': 'woocommerceanalytics_remove_from_cart',
290
							'blog_id': '" . esc_js( $blogid ) . "',
291
							'pi': productID,
292
							'ui': '" . esc_js( $this->get_user_id() ) . "',
293
						} );
294
					}
295
				} );
296
			} );
297
		"
298
		);
299
	}
300
301
	/**
302
	 * Get the current user id
303
	 *
304
	 * @return int
305
	 */
306
	public function get_user_id() {
307
		if ( is_user_logged_in() ) {
308
			$blogid = Jetpack::get_option( 'id' );
309
			$userid = get_current_user_id();
310
			return $blogid . ':' . $userid;
311
		}
312
		return 'null';
313
	}
314
315
	/**
316
	 * @param $cart_item_key
317
	 * @param $product_id
318
	 * @param $quantity
319
	 * @param $variation_id
320
	 * @param $variation
321
	 * @param $cart_item_data
322
	 */
323
	public function capture_add_to_cart( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) {
324
		$referer_postid = isset( $_SERVER['HTTP_REFERER'] ) ? url_to_postid( $_SERVER['HTTP_REFERER'] ) : 0;
325
		// if the referring post is not a product OR the product being added is not the same as post
326
		// (eg. related product list on single product page) then include a product view event
327
		if ( ! wc_get_product( $referer_postid ) || $product_id != $referer_postid ) {
328
			$this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_product_view' );
329
		}
330
		// add cart event to the session data
331
		$this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_add_to_cart' );
332
	}
333
334
	/**
335
	 * @param $product_id
336
	 * @param $quantity
337
	 * @param $event
338
	 */
339
	public function capture_event_in_session_data( $product_id, $quantity, $event ) {
340
341
		$product = wc_get_product( $product_id );
342
		if ( ! $product ) {
343
			return;
344
		}
345
346
		$quantity = ( $quantity == 0 ) ? 1 : $quantity;
347
348
		// check for existing data
349
		if ( is_object( WC()->session ) ) {
350
			$data = WC()->session->get( 'wca_session_data' );
351
			if ( empty( $data ) || ! is_array( $data ) ) {
352
				$data = array();
353
			}
354
		} else {
355
			$data = array();
356
		}
357
358
		// extract new event data
359
		$new_data = array(
360
			'event'      => $event,
361
			'product_id' => (string) $product_id,
362
			'quantity'   => (string) $quantity,
363
		);
364
365
		// append new data
366
		$data[] = $new_data;
367
368
		WC()->session->set( 'wca_session_data', $data );
369
	}
370
371
	/**
372
	 * Gets product categories or varation attributes as a formatted concatenated string
373
	 *
374
	 * @param object $product WC_Product.
375
	 * @return string
376
	 */
377 View Code Duplication
	public function get_product_categories_concatenated( $product ) {
378
379
		if ( ! $product ) {
380
			return '';
381
		}
382
383
		$variation_data = $product->is_type( 'variation' ) ? wc_get_product_variation_attributes( $product->get_id() ) : '';
384
		if ( is_array( $variation_data ) && ! empty( $variation_data ) ) {
385
			$line = wc_get_formatted_variation( $variation_data, true );
386
		} else {
387
			$out        = array();
388
			$categories = get_the_terms( $product->get_id(), 'product_cat' );
389
			if ( $categories ) {
390
				foreach ( $categories as $category ) {
391
					$out[] = $category->name;
392
				}
393
			}
394
			$line = join( '/', $out );
395
		}
396
		return $line;
397
	}
398
399
}
400