Completed
Push — renovate/jest-monorepo ( bd2eaf...d289c3 )
by
unknown
44:07 queued 37:28
created

  A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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