PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.15.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.15.2
5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / classes / WpMatomo / Ecommerce / Woocommerce.php
matomo / classes / WpMatomo / Ecommerce Last commit date
Base.php 4 years ago EasyDigitalDownloads.php 4 years ago MatomoTestEcommerce.php 4 years ago MemberPress.php 3 years ago Woocommerce.php 2 years ago
Woocommerce.php
379 lines
1 <?php
2 /**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 * @package matomo
8 */
9
10 namespace WpMatomo\Ecommerce;
11
12 use WC_Order;
13 use WC_Product;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // if accessed directly
17 }
18 if ( ! defined( 'MATOMO_WOOCOMMERCE_IGNORED_ORDER_STATUS' ) ) {
19 // phpcs:ignore PHPCompatibility.InitialValue.NewConstantArraysUsingDefine.Found
20 define( 'MATOMO_WOOCOMMERCE_IGNORED_ORDER_STATUS', [ 'cancelled', 'failed', 'refunded' ] );
21 }
22
23 class Woocommerce extends Base {
24 private $order_status_ignore = MATOMO_WOOCOMMERCE_IGNORED_ORDER_STATUS;
25
26 public function register_hooks() {
27 parent::register_hooks();
28
29 add_action( 'wp_head', [ $this, 'maybe_track_order_complete' ], 99999 );
30 add_action( 'woocommerce_after_single_product', [ $this, 'on_product_view' ], 99999, $args = 0 );
31 add_action( 'woocommerce_add_to_cart', [ $this, 'on_cart_updated_safe' ], 99999, 0 );
32 add_action( 'woocommerce_cart_item_removed', [ $this, 'on_cart_updated_safe' ], 99999, 0 );
33 add_action( 'woocommerce_cart_item_restored', [ $this, 'on_cart_updated_safe' ], 99999, 0 );
34 add_action( 'woocommerce_cart_item_set_quantity', [ $this, 'on_cart_updated_safe' ], 99999, 0 );
35 add_action( 'woocommerce_thankyou', [ $this, 'anonymise_orderid_in_url' ], 1, 1 );
36
37 if ( ! $this->should_track_background() ) {
38 // prevent possibly executing same event twice where eg first a PHP Matomo tracker request is created
39 // because of woocommerce_applied_coupon and then also because of woocommerce_update_cart_action_cart_updated itself
40 // causing two tracking requests to be issues from the server. refs #215
41 // when not ajax mode the later event will simply overwrite the first and it should be fine.
42 add_filter(
43 'woocommerce_update_cart_action_cart_updated',
44 [
45 $this,
46 'on_cart_updated_safe',
47 ],
48 99999,
49 1
50 );
51 }
52
53 add_action( 'woocommerce_applied_coupon', [ $this, 'on_coupon_updated_safe' ], 99999, 0 );
54 add_action( 'woocommerce_removed_coupon', [ $this, 'on_coupon_updated_safe' ], 99999, 0 );
55 }
56
57 public function anonymise_orderid_in_url( $order_id ) {
58 if ( ! empty( $order_id ) && is_numeric( $order_id ) ) {
59 $order_id = (int) $order_id;
60 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
61 echo "<script>(function () {
62 if (location.href) {
63 window._paq = window._paq || [];
64 var url = location.href;
65 if (url.indexOf('?') > 0) {
66 url = url.substr(0, url.indexOf('?')); // remove order key
67 }
68 window._paq.push(['setCustomUrl', url.replace('$order_id', 'orderid_anonymised')]);
69 }
70 })()</script>";
71 }
72 }
73
74 public function maybe_track_order_complete() {
75 global $wp;
76
77 if ( function_exists( 'is_order_received_page' ) && is_order_received_page() ) {
78 $order_id = isset( $wp->query_vars['order-received'] ) ? $wp->query_vars['order-received'] : 0;
79 if ( ! empty( $order_id ) && $order_id > 0 ) {
80 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
81 echo $this->on_order( $order_id );
82 }
83 }
84 }
85
86 public function on_coupon_updated_safe() {
87 try {
88 $val = null;
89 $val = $this->on_cart_updated( $val, true );
90 } catch ( \Exception $e ) {
91 $this->logger->log_exception( 'woo_on_cart_update', $e );
92 }
93
94 return $val;
95 }
96
97 public function on_cart_updated_safe( $val = null ) {
98 try {
99 $val = $this->on_cart_updated( $val );
100 } catch ( \Exception $e ) {
101 $this->logger->log_exception( 'woo_on_cart_update', $e );
102 }
103
104 return $val;
105 }
106
107 /**
108 * @param null $val needed for woocommerce_update_cart_action_cart_updated filter
109 * @param bool $is_coupon_update set to true if cart was updated because of a coupon
110 *
111 * @return mixed
112 */
113 public function on_cart_updated( $val = null, $is_coupon_update = false ) {
114 global $woocommerce;
115
116 /** @var \WC_Cart $cart */
117 $cart = $woocommerce->cart;
118 if ( ! $is_coupon_update ) {
119 // can cause cart coupon not to be applied when WooCommerce Subscriptions is used.
120 $cart->calculate_totals();
121 }
122 $cart_content = $cart->get_cart();
123
124 $tracking_code = '';
125
126 foreach ( $cart_content as $item ) {
127 /** @var WC_Product $product */
128 $product = wc_get_product( $item['product_id'] );
129
130 if ( $this->isWC3() ) {
131 $product_or_variation = $product;
132
133 if ( ! empty( $item['variation_id'] ) ) {
134 $variation = wc_get_product( $item['variation_id'] );
135 if ( ! empty( $variation ) ) {
136 $product_or_variation = $variation;
137 }
138 }
139 } else {
140 $order = new WC_Order( null );
141 $product_or_variation = $order->get_product_from_item( $item );
142 }
143
144 if ( empty( $product_or_variation ) ) {
145 continue;
146 }
147
148 $sku = $this->get_sku( $product_or_variation );
149
150 $price = 0;
151 if ( isset( $item['line_total'] ) ) {
152 $total = floatval( $item['line_total'] ) / max( 1, $item['quantity'] );
153 $price = round( $total, wc_get_price_decimals() );
154 }
155
156 $title = $product->get_title();
157 $categories = $this->get_product_categories( $product );
158 $quantity = isset( $item['quantity'] ) ? $item['quantity'] : 0;
159 $params = [ 'addEcommerceItem', '' . $sku, $title, $categories, $price, $quantity ];
160 $tracking_code .= $this->make_matomo_js_tracker_call( $params );
161 }
162
163 $total = 0;
164 if ( ! empty( $cart->total ) ) {
165 $total = $cart->total;
166 } elseif ( ! empty( $cart->cart_contents_total ) ) {
167 $total = $cart->cart_contents_total;
168 }
169
170 $tracking_code .= $this->make_matomo_js_tracker_call( [ 'trackEcommerceCartUpdate', $total ] );
171
172 $this->cart_update_queue = $this->wrap_script( $tracking_code );
173 $this->logger->log( 'Tracked ecommerce cart update: ' . $this->cart_update_queue );
174
175 return $val;
176 }
177
178 public function on_order( $order_id ) {
179 if ( $this->has_order_been_tracked_already( $order_id ) ) {
180 $this->logger->log( sprintf( 'Ignoring already tracked order %d', $order_id ) );
181
182 return '';
183 }
184
185 $this->logger->log( sprintf( 'Matomo new order %d', $order_id ) );
186
187 $order = wc_get_order( $order_id );
188 // @see https://github.com/matomo-org/matomo-for-wordpress/issues/514
189 if ( ! $order ) {
190 return;
191 }
192 $order_id_to_track = $order_id;
193 if ( method_exists( $order, 'get_order_number' ) ) {
194 $order_id_to_track = $order->get_order_number();
195 }
196
197 $order_status = $order->get_status();
198
199 $this->logger->log( sprintf( 'Order %s with order number %s has status: %s', $order_id, $order_id_to_track, $order_status ) );
200
201 if ( in_array( $order_status, $this->order_status_ignore, true ) ) {
202 $this->logger->log( 'Ignoring ecommerce order ' . $order_id . ' becauses of status: ' . $order_status );
203
204 return '';
205 }
206
207 $items = $order->get_items();
208
209 $tracking_code = '';
210 if ( $items ) {
211 foreach ( $items as $item ) {
212 /** @var \WC_Order_Item_Product $item */
213
214 $product_details = $this->get_product_details( $order, $item );
215
216 if ( ! empty( $product_details ) ) {
217 $params = [
218 'addEcommerceItem',
219 '' . $product_details['sku'],
220 $product_details['title'],
221 $product_details['categories'],
222 $product_details['price'],
223 $product_details['quantity'],
224 ];
225 $tracking_code .= $this->make_matomo_js_tracker_call( $params );
226 }
227 }
228 }
229
230 $params = [
231 'trackEcommerceOrder',
232 '' . $order_id_to_track,
233 $order->get_total(),
234 round( $order->get_subtotal(), 2 ),
235 $order->get_cart_tax(),
236 $this->isWC3() ? $order->get_shipping_total() : $order->get_total_shipping(),
237 $order->get_total_discount(),
238 ];
239 $tracking_code .= $this->make_matomo_js_tracker_call( $params );
240
241 $this->logger->log( sprintf( 'Tracked ecommerce order %s with number %s', $order_id, $order_id_to_track ) );
242
243 $this->set_order_been_tracked( $order_id );
244
245 return $this->wrap_script( $tracking_code );
246 }
247
248 private function isWC3() {
249 global $woocommerce;
250 $result = version_compare( $woocommerce->version, '3.0', '>=' );
251
252 return $result;
253 }
254
255 /**
256 * @param WC_Product $product
257 */
258 private function get_sku( $product ) {
259 if ( $product && $product->get_sku() ) {
260 return $product->get_sku();
261 }
262
263 return $this->get_product_id( $product );
264 }
265
266 /**
267 * @param WC_Product $product
268 */
269 private function get_product_id( $product ) {
270 if ( ! $product ) {
271 return;
272 }
273
274 if ( $this->isWC3() ) {
275 return $product->get_id();
276 }
277
278 return $product->id;
279 }
280
281 /**
282 * @param WC_Order $order
283 * @param $item
284 *
285 * @return mixed
286 */
287 private function get_product_details( $order, $item ) {
288 $product_or_variation = false;
289 if ( $this->isWC3() && ! empty( $item ) && is_object( $item ) && method_exists( $item, 'get_product' ) && is_callable(
290 [
291 $item,
292 'get_product',
293 ]
294 ) ) {
295 $product_or_variation = $item->get_product();
296 } elseif ( method_exists( $order, 'get_product_from_item' ) ) {
297 // eg woocommerce 2.x
298 $product_or_variation = $order->get_product_from_item( $item );
299 }
300
301 if ( is_object( $item ) && method_exists( $item, 'get_product_id' ) ) {
302 // woocommerce 3
303 $product_id = $item->get_product_id();
304 } elseif ( isset( $item['product_id'] ) ) {
305 // woocommerce 2.x
306 $product_id = $item['product_id'];
307 } else {
308 return;
309 }
310
311 $product = wc_get_product( $product_id );
312
313 $pr = $product_or_variation ? $product_or_variation : $product;
314 $sku = $this->get_sku( $pr );
315 $price = $order->get_item_total( $item );
316 $title = $product->get_title();
317 $categories = $this->get_product_categories( $product );
318 $quantity = $item['qty'];
319
320 return [
321 'sku' => $sku,
322 'title' => $title,
323 'categories' => $categories,
324 'quantity' => $quantity,
325 'price' => $price,
326 ];
327 }
328
329 /**
330 * @param WC_Product $product
331 *
332 * @return array
333 */
334 private function get_product_categories( $product ) {
335 $product_id = $this->get_product_id( $product );
336
337 $category_terms = get_the_terms( $product_id, 'product_cat' );
338
339 $categories = [];
340
341 if ( is_wp_error( $category_terms ) ) {
342 return $categories;
343 }
344
345 if ( ! empty( $category_terms ) ) {
346 foreach ( $category_terms as $category ) {
347 $categories[] = $category->name;
348 }
349 }
350
351 $max_num_categories = 5;
352 $categories = array_unique( $categories );
353 $categories = array_slice( $categories, 0, $max_num_categories );
354
355 return $categories;
356 }
357
358 public function on_product_view() {
359 global $product;
360
361 if ( empty( $product ) ) {
362 return;
363 }
364
365 /** @var WC_Product $product */
366 $params = [
367 'setEcommerceView',
368 $this->get_sku( $product ),
369 $product->get_title(),
370 $this->get_product_categories( $product ),
371 $product->get_price(),
372 ];
373
374 // we're not using wc_enqueue_js eg to prevent sometimes this code from being minified on some JS minifier plugins
375 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
376 echo $this->wrap_script( $this->make_matomo_js_tracker_call( $params ) );
377 }
378 }
379