Completed
Push — master ( 39d208...bf971b )
by Rua
06:43
created

order_process()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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