PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.8.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.8.2
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 / Base.php
matomo / classes / WpMatomo / Ecommerce Last commit date
Base.php 10 months ago EasyDigitalDownloads.php 4 years ago MatomoTestEcommerce.php 10 months ago MemberPress.php 3 months ago ServerSideVisitorId.php 1 year ago Woocommerce.php 4 months ago
Base.php
348 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 Exception;
13 use WpMatomo;
14 use WpMatomo\Admin\TrackingSettings;
15 use WpMatomo\AjaxTracker;
16 use WpMatomo\Logger;
17 use WpMatomo\Settings;
18 use WpMatomo\Site\Sync\SyncConfig;
19
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit; // if accessed directly
22 }
23
24 class Base {
25 const DELAYED_SERVER_SIDE_TRACKING_HOOK = 'matomo_delayed_tracking';
26 const DELAYED_SERVER_SIDE_TRACKING_SESSION_KEY = 'matomo_delayed_tracking_data';
27
28 protected $key_order_tracked = 'order-tracked';
29
30 /**
31 * @var Logger
32 */
33 protected $logger;
34
35 /**
36 * @var AjaxTracker
37 */
38 protected $tracker;
39
40 /**
41 * We can't echo cart updates directly as we wouldn't know where in the template rendering stage we are and whether
42 * we're supposed to print or not etc. Also there might be multiple cart updates triggered during one page load so
43 * we want to make sure to print only the most recent tracking code
44 *
45 * @var string
46 */
47 protected $cart_update_queue = '';
48
49 /**
50 * @var Settings
51 */
52 protected $settings;
53
54 private $ajax_tracker_calls = [];
55
56 /**
57 * @var SyncConfig
58 */
59 private $config;
60
61 public function __construct( AjaxTracker $tracker, Settings $settings, SyncConfig $config ) {
62 $this->logger = new Logger();
63 $this->tracker = $tracker;
64 $this->settings = $settings;
65 $this->config = $config;
66
67 // by using prefix we make sure it will be removed on unistall and make sure it's clear it belongs to us
68 $this->key_order_tracked = Settings::OPTION_PREFIX . $this->key_order_tracked;
69 }
70
71 public function register_hooks() {
72 if ( ! is_admin() ) {
73 add_action( 'wp_footer', [ $this, 'on_print_queues' ], 99999, 0 );
74 add_action( 'wp_footer', [ $this, 'maybe_do_delayed_tracking_early' ], 99999, 0 );
75 }
76
77 add_action( self::DELAYED_SERVER_SIDE_TRACKING_HOOK, [ $this, 'do_delayed_tracking' ], 10, 1 );
78 }
79
80 public function on_print_queues() {
81 // we need to queue in case there are multiple cart updates within one page load
82 if ( ! empty( $this->cart_update_queue ) ) {
83 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
84 echo $this->cart_update_queue;
85 }
86 }
87
88 protected function has_order_been_tracked_already( $order_id ) {
89 // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
90 return get_post_meta( $order_id, $this->key_order_tracked, true ) == 1;
91 }
92
93 protected function set_order_been_tracked( $order_id ) {
94 update_post_meta( $order_id, $this->key_order_tracked, 1 );
95 }
96
97 protected function should_track_background() {
98 return ( defined( 'DOING_AJAX' ) && DOING_AJAX )
99 || ( defined( 'REST_REQUEST' ) && REST_REQUEST )
100 || ( defined( 'MATOMO_TRACK_ECOMMERCE_SERVER_SIDE' ) && MATOMO_TRACK_ECOMMERCE_SERVER_SIDE )
101 || ( did_action( 'wp_footer' ) && ! doing_action( 'wp_footer' ) )
102 || $this->settings->get_global_option( 'track_mode' ) === TrackingSettings::TRACK_MODE_TAGMANAGER;
103 }
104
105 protected function make_matomo_js_tracker_call( $params ) {
106 $this->ajax_tracker_calls[] = $params;
107
108 $code = 'window._paq = window._paq || [];';
109 if ( $this->settings->get_global_option( 'disable_cookies' ) ) {
110 $code .= ' ' . WpMatomo\TrackingCode\TrackingCodeGenerator::get_disable_cookies_partial();
111 }
112 $code .= sprintf( ' window._paq.push(%s);', wp_json_encode( $params ) );
113
114 return $code;
115 }
116
117 protected function wrap_script( $script ) {
118 if ( $this->should_track_background() ) {
119 if ( $this->should_delay_server_side_tracking() ) {
120 $this->delay_background_tracking();
121 return false;
122 }
123
124 $this->track_in_background( $this->ajax_tracker_calls );
125
126 $this->ajax_tracker_calls = [];
127
128 return '';
129 }
130
131 if ( empty( $script ) ) {
132 return '';
133 }
134
135 if ( function_exists( 'wp_get_inline_script_tag' ) ) {
136 $script = wp_get_inline_script_tag( $script );
137 } else {
138 // line feed is required to match the wp_get_inline_script_tag output
139 $script = '<script >' . PHP_EOL . $script . PHP_EOL . '</script>' . PHP_EOL;
140 }
141
142 // NOTE: we expect the script to be echo'd after wrap_script is called
143 $this->set_order_been_tracked_if_ajax_calls_has_order_track();
144
145 return $script;
146 }
147
148 private function set_order_been_tracked_if_ajax_calls_has_order_track() {
149 foreach ( $this->ajax_tracker_calls as $call ) {
150 $tracker_method = array_shift( $call );
151 $this->set_order_been_tracked_if_call_is_track_order( $tracker_method, $call );
152 }
153 }
154
155 private function track_in_background( $ajax_tracker_calls, $visitor_id = null, $tracking_time = null, $ip = null ) {
156 $original_visitor_id = $this->tracker->forcedVisitorId;
157 if ( ! empty( $visitor_id ) ) {
158 $this->tracker->set_visitor_id_safe( $visitor_id );
159 }
160
161 if ( ! empty( $tracking_time ) ) {
162 $this->tracker->setForceVisitDateTime( $tracking_time );
163 }
164
165 if ( ! empty( $ip ) ) {
166 $this->tracker->setIp( $ip );
167 }
168
169 try {
170 foreach ( $ajax_tracker_calls as $call ) {
171 $methods = [
172 'addEcommerceItem' => 'addEcommerceItem',
173 'trackEcommerceOrder' => 'doTrackEcommerceOrder',
174 'trackEcommerceCartUpdate' => 'doTrackEcommerceCartUpdate',
175 ];
176 if ( ! empty( $call[0] ) && ! empty( $methods[ $call[0] ] ) ) {
177 try {
178 $tracker_method = $methods[ $call[0] ];
179 array_shift( $call );
180 $response = call_user_func_array( [ $this->tracker, $tracker_method ], $call );
181
182 if ( $this->tracker->is_success_response( $response ) ) {
183 $this->set_order_been_tracked_if_call_is_track_order( $tracker_method, $call );
184 }
185 } catch ( Exception $e ) {
186 $this->logger->log_exception( $call[0], $e );
187 }
188 }
189 }
190 } finally {
191 $this->tracker->forcedVisitorId = $original_visitor_id;
192 $this->tracker->forcedDatetime = false;
193 $this->tracker->ip = false;
194 }
195 }
196
197 private function set_order_been_tracked_if_call_is_track_order( $tracker_method, $params ) {
198 if (
199 'doTrackEcommerceOrder' === $tracker_method
200 || 'trackEcommerceOrder' === $tracker_method
201 ) {
202 $order_id = reset( $params );
203 $this->set_order_been_tracked( $order_id );
204 }
205 }
206
207 protected function delay_background_tracking() {
208 $delay_time = $this->get_seconds_to_delay_tracking();
209 $delayed_time = time() + $delay_time;
210
211 $client_headers = $this->config->get_config_value( 'General', 'proxy_client_headers' );
212 if ( ! empty( $client_headers ) ) {
213 foreach ( $client_headers as $header ) {
214 if ( isset( $_SERVER[ $header ] ) ) {
215 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
216 $ip = wp_unslash( $_SERVER[ $header ] );
217 }
218 }
219 }
220 if ( empty( $ip ) ) {
221 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
222 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? wp_unslash( $_SERVER['REMOTE_ADDR'] ) : null;
223 }
224
225 $tracking_data = [
226 'calls' => $this->ajax_tracker_calls,
227 'delayed_time' => $delayed_time,
228 'visitor_id' => $this->tracker->forcedVisitorId,
229 'tracking_time' => time(),
230 'ip' => $ip,
231 ];
232
233 $this->add_tracking_calls_to_session( $tracking_data );
234
235 wp_schedule_single_event( $delayed_time, self::DELAYED_SERVER_SIDE_TRACKING_HOOK, [ $tracking_data ] );
236
237 $this->ajax_tracker_calls = [];
238 }
239
240 protected function should_delay_server_side_tracking() {
241 return $this->supports_delayed_tracking();
242 }
243
244 protected function get_seconds_to_delay_tracking() {
245 $delay = $this->settings->get_option( Settings::SERVER_SIDE_TRACKING_DELAY_SECS );
246 if ( $delay <= 0 ) {
247 $delay = 180;
248 }
249 return $delay;
250 }
251
252 public function maybe_do_delayed_tracking_early() {
253 // do not output tracking code if we do not support delayed tracking
254 // or if the current request requires background tracking (in which case
255 // outputting JS code would not work)
256 if (
257 ! $this->supports_delayed_tracking()
258 || $this->should_track_background()
259 ) {
260 return;
261 }
262
263 $all_queued_tracking = $this->get_tracking_calls_in_session();
264 if ( ! empty( $all_queued_tracking ) && is_array( $all_queued_tracking ) ) {
265 foreach ( $all_queued_tracking as $tracking_data ) {
266 if ( ! wp_get_scheduled_event( self::DELAYED_SERVER_SIDE_TRACKING_HOOK, [ $tracking_data ], $tracking_data['delayed_time'] ) ) {
267 continue; // delayed tracking event already ran
268 }
269
270 $script = '';
271 foreach ( $tracking_data['calls'] as $call ) {
272 $script .= $this->make_matomo_js_tracker_call( $call );
273
274 $tracker_method = array_shift( $call );
275 $this->set_order_been_tracked_if_call_is_track_order( $tracker_method, $call );
276 }
277
278 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
279 echo $this->wrap_script( $script );
280
281 wp_unschedule_event( $tracking_data['delayed_time'], self::DELAYED_SERVER_SIDE_TRACKING_HOOK, [ $tracking_data ] );
282 }
283
284 $this->remove_tracking_calls_in_session();
285 }
286 }
287
288 public function do_delayed_tracking( $tracking_info ) {
289 // WP cron jobs can be executed during normal requests. in such a case, make sure we don't use
290 // the visitor ID of the current request/session
291 $this->tracker->setNewVisitorId();
292
293 $calls = isset( $tracking_info['calls'] ) ? $tracking_info['calls'] : [];
294 $visitor_id = isset( $tracking_info['visitor_id'] ) ? $tracking_info['visitor_id'] : null;
295 $tracking_time = isset( $tracking_info['tracking_time'] ) ? $tracking_info['tracking_time'] : null;
296 $ip = isset( $tracking_info['ip'] ) ? $tracking_info['ip'] : null;
297
298 $this->track_in_background( $calls, $visitor_id, $tracking_time, $ip );
299 }
300
301 /**
302 * Returns true if this ecommerce tracking implementation supports delayed
303 * server side tracking.
304 *
305 * In order for an implementation to support this, it must be able to save
306 * tracking information as session data.
307 *
308 * @return false
309 */
310 protected function supports_delayed_tracking() {
311 return false;
312 }
313
314 /**
315 * Removes all tracking calls currently stored in the session.
316 *
317 * This method must be overridden by ecommerce tracking implementations.
318 *
319 * @return void
320 */
321 protected function remove_tracking_calls_in_session() {
322 // empty
323 }
324
325 /**
326 * Adds provided queued tracking information to the session.
327 *
328 * This method must be overridden by ecommerce tracking implementations.
329 *
330 * @param array $calls
331 * @return void
332 */
333 protected function add_tracking_calls_to_session( $calls ) {
334 // empty
335 }
336
337 /**
338 * Returns tracking calls stored in the session, if any.
339 *
340 * This method must be overridden by ecommerce tracking implementations.
341 *
342 * @return array
343 */
344 protected function get_tracking_calls_in_session() {
345 return [];
346 }
347 }
348