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 ) { |
|
|
|
|
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. |
|
|
|
|
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(), |
|
|
|
|
225
|
|
|
'name' => $product->get_title(), |
|
|
|
|
226
|
|
|
'category' => $this->get_product_categories_concatenated( $product ), |
|
|
|
|
227
|
|
|
'price' => $product->get_price(), |
|
|
|
|
228
|
|
|
'type' => $product->get_type(), |
|
|
|
|
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 ) { |
|
|
|
|
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 ) { |
|
|
|
|
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 ) { |
|
|
|
|
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
|
|
|
|
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 thecomposer.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
orrequire-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 you have not tested against this specific condition, such errors might go unnoticed.