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