Completed
Push — add/redirect-everything ( 50f94e...5deed2 )
by
unknown
181:35 queued 174:43
created

post_contains_text()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 19
rs 9.6333
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
		$site_info          = 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
		$cart_checkout_info = self::get_cart_checkout_info();
92
		return array_merge( $site_info, $cart_checkout_info );
93
	}
94
95
	/**
96
	 * Render tracks event properties as string of JavaScript object props.
97
	 *
98
	 * @param  array $properties Array of key/value pairs.
99
	 * @return string String of the form "key1: value1, key2: value2, " (etc).
100
	 */
101
	private function render_properties_as_js( $properties ) {
102
		$js_args_string = '';
103
		foreach ( $properties as $key => $value ) {
104
			$js_args_string = $js_args_string . "'$key': '" . esc_js( $value ) . "', ";
105
		}
106
		return $js_args_string;
107
	}
108
109
	/**
110
	 * Record an event with optional custom properties.
111
	 *
112
	 * @param string  $event_name The name of the event to record.
113
	 * @param integer $product_id The id of the product relating to the event.
114
	 * @param array   $properties Optional array of (key => value) event properties.
115
	 */
116
	public function record_event( $event_name, $product_id, $properties = array() ) {
117
		$product = wc_get_product( $product_id );
118
		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...
119
			return;
120
		}
121
		$product_details = $this->get_product_details( $product );
122
123
		$all_props = array_merge(
124
			$properties,
125
			$this->get_common_properties()
126
		);
127
128
		wc_enqueue_js(
129
			"_wca.push( {
130
					'_en': '" . esc_js( $event_name ) . "',
131
					'pi': '" . esc_js( $product_id ) . "',
132
					'pn': '" . esc_js( $product_details['name'] ) . "',
133
					'pc': '" . esc_js( $product_details['category'] ) . "',
134
					'pp': '" . esc_js( $product_details['price'] ) . "',
135
					'pt': '" . esc_js( $product_details['type'] ) . "'," .
136
					$this->render_properties_as_js( $all_props ) . '
137
				} );'
138
		);
139
	}
140
141
	/**
142
	 * On product lists or other non-product pages, add an event listener to "Add to Cart" button click
143
	 */
144
	public function loop_session_events() {
145
		// Check for previous events queued in session data.
146
		if ( is_object( WC()->session ) ) {
147
			$data = WC()->session->get( 'wca_session_data' );
148
			if ( ! empty( $data ) ) {
149
				foreach ( $data as $data_instance ) {
150
					$this->record_event(
151
						$data_instance['event'],
152
						$data_instance['product_id'],
153
						array(
154
							'pq' => $data_instance['quantity'],
155
						)
156
					);
157
				}
158
				// Clear data, now that these events have been recorded.
159
				WC()->session->set( 'wca_session_data', '' );
160
			}
161
		}
162
	}
163
164
	/**
165
	 * On the cart page, add an event listener for removal of product click
166
	 */
167
	public function remove_from_cart() {
168
		$common_props = $this->render_properties_as_js(
169
			$this->get_common_properties()
170
		);
171
172
		// We listen at div.woocommerce because the cart 'form' contents get forcibly
173
		// updated and subsequent removals from cart would then not have this click
174
		// handler attached.
175
		wc_enqueue_js(
176
			"jQuery( 'div.woocommerce' ).on( 'click', 'a.remove', function() {
177
				var productID = jQuery( this ).data( 'product_id' );
178
				var quantity = jQuery( this ).parent().parent().find( '.qty' ).val()
179
				var productDetails = {
180
					'id': productID,
181
					'quantity': quantity ? quantity : '1',
182
				};
183
				_wca.push( {
184
					'_en': 'woocommerceanalytics_remove_from_cart',
185
					'pi': productDetails.id,
186
					'pq': productDetails.quantity, " .
187
					$common_props . '
188
				} );
189
			} );'
190
		);
191
	}
192
193
	/**
194
	 * Adds the product ID to the remove product link (for use by remove_from_cart above) if not present
195
	 *
196
	 * @param string $url Full HTML a tag of the link to remove an item from the cart.
197
	 * @param string $key Unique Key ID for a cart item.
198
	 *
199
	 * @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...
200
	 */
201 View Code Duplication
	public function remove_from_cart_attributes( $url, $key ) {
202
		if ( false !== strpos( $url, 'data-product_id' ) ) {
203
			return $url;
204
		}
205
206
		$item    = WC()->cart->get_cart_item( $key );
207
		$product = $item['data'];
208
209
		$new_attributes = sprintf(
210
			'" data-product_id="%s">',
211
			esc_attr( $product->get_id() )
212
		);
213
214
		$url = str_replace( '">', $new_attributes, $url );
215
		return $url;
216
	}
217
218
	/**
219
	 * Gather relevant product information
220
	 *
221
	 * @param array $product product
222
	 * @return array
223
	 */
224
	public function get_product_details( $product ) {
225
		return array(
226
			'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...
227
			'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...
228
			'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...
229
			'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...
230
			'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...
231
		);
232
	}
233
234
	/**
235
	 * Track a product page view
236
	 */
237
	public function capture_product_view() {
238
		global $product;
239
		$this->record_event(
240
			'woocommerceanalytics_product_view',
241
			$product->get_id()
242
		);
243
	}
244
245
	/**
246
	 * On the Checkout page, trigger an event for each product in the cart
247
	 */
248
	public function checkout_process() {
249
		$cart = WC()->cart->get_cart();
250
251
		foreach ( $cart as $cart_item_key => $cart_item ) {
252
			/**
253
			* This filter is already documented in woocommerce/templates/cart/cart.php
254
			*/
255
			$product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
256
257
			if ( ! $product ) {
258
				continue;
259
			}
260
261
			$this->record_event(
262
				'woocommerceanalytics_product_checkout',
263
				$product->get_id(),
264
				array(
265
					'pq' => $cart_item['quantity'],
266
				)
267
			);
268
		}
269
	}
270
271
	/**
272
	 * After the checkout process, fire an event for each item in the order
273
	 *
274
	 * @param string $order_id Order Id.
275
	 */
276
	public function order_process( $order_id ) {
277
		$order = wc_get_order( $order_id );
278
279
		// loop through products in the order and queue a purchase event.
280
		foreach ( $order->get_items() as $order_item_id => $order_item ) {
281
			$product = $order->get_product_from_item( $order_item );
282
283
			$this->record_event(
284
				'woocommerceanalytics_product_purchase',
285
				$product->get_id(),
286
				array(
287
					'oi' => $order->get_order_number(),
288
					'pq' => $order_item->get_quantity(),
289
				)
290
			);
291
		}
292
	}
293
294
	/**
295
	 * Listen for clicks on the "Update Cart" button to know if an item has been removed by
296
	 * updating its quantity to zero
297
	 */
298
	public function remove_from_cart_via_quantity() {
299
		$common_props = $this->render_properties_as_js(
300
			$this->get_common_properties()
301
		);
302
303
		wc_enqueue_js(
304
			"
305
			jQuery( 'button[name=update_cart]' ).on( 'click', function() {
306
				var cartItems = jQuery( '.cart_item' );
307
				cartItems.each( function( item ) {
308
					var qty = jQuery( this ).find( 'input.qty' );
309
					if ( qty && qty.val() === '0' ) {
310
						var productID = jQuery( this ).find( '.product-remove a' ).data( 'product_id' );
311
						_wca.push( {
312
							'_en': 'woocommerceanalytics_remove_from_cart',
313
							'pi': productID, " .
314
							$common_props . '
315
						} );
316
					}
317
				} );
318
			} );'
319
		);
320
	}
321
322
	/**
323
	 * Get the current user id
324
	 *
325
	 * @return int
326
	 */
327
	public function get_user_id() {
328
		if ( is_user_logged_in() ) {
329
			$blogid = Jetpack::get_option( 'id' );
330
			$userid = get_current_user_id();
331
			return $blogid . ':' . $userid;
332
		}
333
		return 'null';
334
	}
335
336
	/**
337
	 * @param $cart_item_key
338
	 * @param $product_id
339
	 * @param $quantity
340
	 * @param $variation_id
341
	 * @param $variation
342
	 * @param $cart_item_data
343
	 */
344
	public function capture_add_to_cart( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) {
345
		$referer_postid = isset( $_SERVER['HTTP_REFERER'] ) ? url_to_postid( $_SERVER['HTTP_REFERER'] ) : 0;
346
		// if the referring post is not a product OR the product being added is not the same as post
347
		// (eg. related product list on single product page) then include a product view event
348
		$product_by_referer_postid = wc_get_product( $referer_postid );
349
		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...
350
			$this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_product_view' );
351
		}
352
		// add cart event to the session data
353
		$this->capture_event_in_session_data( $product_id, $quantity, 'woocommerceanalytics_add_to_cart' );
354
	}
355
356
	/**
357
	 * @param $product_id
358
	 * @param $quantity
359
	 * @param $event
360
	 */
361
	public function capture_event_in_session_data( $product_id, $quantity, $event ) {
362
363
		$product = wc_get_product( $product_id );
364
		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...
365
			return;
366
		}
367
368
		$quantity = ( $quantity == 0 ) ? 1 : $quantity;
369
370
		// check for existing data
371
		if ( is_object( WC()->session ) ) {
372
			$data = WC()->session->get( 'wca_session_data' );
373
			if ( empty( $data ) || ! is_array( $data ) ) {
374
				$data = array();
375
			}
376
		} else {
377
			$data = array();
378
		}
379
380
		// extract new event data
381
		$new_data = array(
382
			'event'      => $event,
383
			'product_id' => (string) $product_id,
384
			'quantity'   => (string) $quantity,
385
		);
386
387
		// append new data
388
		$data[] = $new_data;
389
390
		WC()->session->set( 'wca_session_data', $data );
391
	}
392
393
	/**
394
	 * Gets product categories or varation attributes as a formatted concatenated string
395
	 *
396
	 * @param object $product WC_Product.
397
	 * @return string
398
	 */
399 View Code Duplication
	public function get_product_categories_concatenated( $product ) {
400
401
		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...
402
			return '';
403
		}
404
405
		$variation_data = $product->is_type( 'variation' ) ? wc_get_product_variation_attributes( $product->get_id() ) : '';
406
		if ( is_array( $variation_data ) && ! empty( $variation_data ) ) {
407
			$line = wc_get_formatted_variation( $variation_data, true );
408
		} else {
409
			$out        = array();
410
			$categories = get_the_terms( $product->get_id(), 'product_cat' );
411
			if ( $categories ) {
412
				foreach ( $categories as $category ) {
413
					$out[] = $category->name;
414
				}
415
			}
416
			$line = join( '/', $out );
417
		}
418
		return $line;
419
	}
420
421
	/**
422
	 * Search a specific post for text content.
423
	 *
424
	 * Note: similar code is in a WooCommerce core PR:
425
	 * https://github.com/woocommerce/woocommerce/pull/25932
426
	 *
427
	 * @param integer $post_id The id of the post to search.
428
	 * @param string  $text    The text to search for.
429
	 * @return integer 1 if post contains $text (otherwise 0).
430
	 */
431
	public static function post_contains_text( $post_id, $text ) {
432
		global $wpdb;
433
434
		// Search for the text anywhere in the post.
435
		$wildcarded = "%{$text}%";
436
437
		$result = $wpdb->get_var(
438
			$wpdb->prepare(
439
				"
440
				SELECT COUNT( * ) FROM {$wpdb->prefix}posts
441
				WHERE ID=%d
442
				AND {$wpdb->prefix}posts.post_content LIKE %s
443
				",
444
				array( $post_id, $wildcarded )
445
			)
446
		);
447
448
		return ( '0' !== $result ) ? 1 : 0;
449
	}
450
451
	/**
452
	 * Get info about the cart & checkout pages, in particular
453
	 * whether the store is using shortcodes or Gutenberg blocks.
454
	 * This info is cached in a transient.
455
	 *
456
	 * Note: similar code is in a WooCommerce core PR:
457
	 * https://github.com/woocommerce/woocommerce/pull/25932
458
	 *
459
	 * @return array
460
	 */
461
	public static function get_cart_checkout_info() {
462
		$transient_name = 'jetpack_woocommerce_analytics_cart_checkout_info_cache';
463
464
		$info = get_transient( $transient_name );
465
		if ( false === $info ) {
466
			$cart_page_id     = wc_get_page_id( 'cart' );
467
			$checkout_page_id = wc_get_page_id( 'checkout' );
468
469
			$info = array(
470
				'cart_page_contains_cart_block'         => self::post_contains_text(
471
					$cart_page_id,
472
					'<!-- wp:woocommerce/cart'
473
				),
474
				'cart_page_contains_cart_shortcode'     => self::post_contains_text(
475
					$cart_page_id,
476
					'[woocommerce_cart]'
477
				),
478
				'checkout_page_contains_checkout_block' => self::post_contains_text(
479
					$checkout_page_id,
480
					'<!-- wp:woocommerce/checkout'
481
				),
482
				'checkout_page_contains_checkout_shortcode' => self::post_contains_text(
483
					$checkout_page_id,
484
					'[woocommerce_checkout]'
485
				),
486
			);
487
488
			set_transient( $transient_name, $info, DAY_IN_SECONDS );
489
		}
490
491
		return $info;
492
	}
493
}
494