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