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 / TrackingCode / TrackingCodeGenerator.php
matomo / classes / WpMatomo / TrackingCode Last commit date
GeneratorOptions.php 1 year ago TrackingCodeGenerator.php 4 months ago
TrackingCodeGenerator.php
472 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\TrackingCode;
11
12 use WP_Query;
13 use WpMatomo\Admin\CookieConsent;
14 use WpMatomo\Admin\TrackingSettings;
15 use WpMatomo\AjaxTracker;
16 use WpMatomo\Logger;
17 use WpMatomo\Paths;
18 use WpMatomo\Settings;
19 use WpMatomo\Site;
20
21 if ( ! defined( 'ABSPATH' ) ) {
22 exit; // if accessed directly
23 }
24
25 class TrackingCodeGenerator {
26 const TRACKPAGEVIEW = "_paq.push(['trackPageView']);";
27 const MTM_INIT = 'var _mtm = _mtm || [];';
28
29 /**
30 * @var Settings
31 */
32 private $settings;
33
34 /**
35 * @var GeneratorOptions
36 */
37 private $options;
38
39 /**
40 * @var Logger
41 */
42 private $logger;
43
44 public function __construct( Settings $settings, GeneratorOptions $options ) {
45 $this->settings = $settings;
46 $this->options = $options;
47 $this->logger = new Logger();
48 }
49
50 public static function get_disable_cookies_partial() {
51 // if ecommerce tracking is enabled, disableCookies can be added to _paq multiple times
52 // (since ecommerce tracking methods can be called before the main tracking JS in some situations).
53 // piwik.js complains if the initial _paq array has more than one of the same method, so
54 // we only add it if it's not there to begin with.
55 return 'if (!window._paq.find || !window._paq.find(function (m) { return m[0] === "disableCookies"; })) {
56 window._paq.push(["disableCookies"]);
57 }';
58 }
59
60 public function register_hooks() {
61 add_action( 'matomo_site_synced', [ $this, 'update_tracking_code' ], $prio = 10, $args = 0 );
62 add_action( 'matomo_tracking_settings_changed', [ $this, 'update_tracking_code' ], $prio = 10, $args = 0 );
63 }
64
65 public function update_tracking_code( $force = false ) {
66 if (
67 $this->settings->is_current_tracking_code()
68 && $this->settings->get_option( 'tracking_code' )
69 && ! $force
70 ) {
71 return false;
72 }
73
74 $track_mode = $this->settings->get_global_option( 'track_mode' );
75
76 if ( ! $this->settings->is_tracking_enabled()
77 || TrackingSettings::TRACK_MODE_MANUALLY === $track_mode ) {
78 return false;
79 }
80
81 $blod_id = get_current_blog_id();
82 $idsite = Site::get_matomo_site_id( $blod_id );
83
84 if ( ! $idsite ) {
85 $this->logger->log( 'Found no related idSite for blog ' . get_current_blog_id() );
86
87 return false;
88 }
89
90 if ( TrackingSettings::TRACK_MODE_DEFAULT === $track_mode ) {
91 $result = $this->prepare_tracking_code( $idsite );
92
93 if ( ! $this->settings->get_global_option( 'track_noscript' ) ) {
94 $result['noscript'] = '';
95 }
96 } elseif ( TrackingSettings::TRACK_MODE_TAGMANAGER === $track_mode && matomo_has_tag_manager() ) {
97 $result = $this->prepare_tagmanger_code( $this->settings, $this->logger );
98 } else {
99 $result = [
100 'script' => '<!-- Matomo: no supported track_mode selected -->',
101 'noscript' => '',
102 ];
103 }
104
105 if ( ! empty( $result['script'] ) ) {
106 $this->settings->set_option( 'tracking_code', $result['script'] );
107 $this->settings->set_option( 'noscript_code', $result['noscript'] );
108 }
109
110 $this->settings->set_option( Settings::OPTION_LAST_TRACKING_CODE_UPDATE, time() );
111 $this->settings->save();
112
113 return $result;
114 }
115
116 public function get_noscript_code() {
117 $this->update_tracking_code();
118
119 return $this->settings->get_noscript_tracking_code();
120 }
121
122 public function get_tracking_code() {
123 $this->update_tracking_code();
124
125 $tracking_code = $this->settings->get_js_tracking_code();
126
127 if ( $this->settings->track_user_id_enabled() ) {
128 $tracking_code = $this->apply_user_tracking( $tracking_code );
129 }
130 if ( $this->settings->track_404_enabled() && is_404() ) {
131 $tracking_code = $this->apply_404_changes( $tracking_code );
132 }
133 if ( $this->settings->track_search_enabled() ) {
134 $tracking_code = $this->apply_search_changes( $tracking_code );
135 }
136
137 return $tracking_code;
138 }
139
140 /**
141 * @param Settings $settings
142 * @param Logger $logger
143 *
144 * @return array
145 */
146 private function prepare_tagmanger_code( $settings, $logger ) {
147 $logger->log( 'Apply tag manager code changes:' );
148
149 $container_ids = $settings->get_global_option( 'tagmanger_container_ids' );
150
151 $code = '<!-- Matomo Tag Manager -->';
152
153 if ( ! empty( $container_ids ) && is_array( $container_ids ) ) {
154 $paths = new Paths();
155 $upload_url = $paths->get_upload_base_url();
156
157 foreach ( $container_ids as $container_id => $enabled ) {
158 if ( $enabled
159 && ctype_alnum( $container_id )
160 && strlen( $container_id ) <= 16 ) {
161 $container_url = $upload_url . '/container_' . rawurlencode( $container_id ) . '.js';
162
163 $data_cf_async = '';
164 if ( $settings->get_global_option( 'track_datacfasync' ) ) {
165 $data_cf_async = 'data-cfasync="false"';
166 }
167
168 if ( $settings->get_global_option( 'force_protocol' ) === 'https' ) {
169 $container_url = preg_replace( '(^http://)', 'https://', $container_url );
170 }
171
172 $code .= '
173 <script ' . $data_cf_async . '>
174 ' . self::MTM_INIT . '
175 _mtm.push({\'mtm.startTime\': (new Date().getTime()), \'event\': \'mtm.Start\'});
176 var d=document, g=d.createElement(\'script\'), s=d.getElementsByTagName(\'script\')[0];
177 g.type=\'text/javascript\'; g.async=true; g.src="' . $container_url . '"; s.parentNode.insertBefore(g,s);
178 </script>';
179 }
180 }
181 }
182
183 $code .= '<!-- End Matomo Tag Manager -->';
184
185 return [
186 'script' => $code,
187 'noscript' => '',
188 ];
189 }
190
191 public function get_tracker_endpoint() {
192 $paths = new Paths();
193
194 if ( $this->options->get_track_api_endpoint() === 'restapi' ) {
195 $tracker_endpoint = $paths->get_tracker_api_rest_api_endpoint();
196 } else {
197 $tracker_endpoint = $paths->get_tracker_api_url_in_matomo_dir();
198 }
199
200 if ( $this->options->get_force_protocol() === 'https' ) {
201 $tracker_endpoint = preg_replace( '(^http://)', 'https://', $tracker_endpoint );
202 } else {
203 $tracker_endpoint = preg_replace( '(^https?://)', '//', $tracker_endpoint );
204 }
205
206 return $tracker_endpoint;
207 }
208
209 public function get_js_endpoint() {
210 $paths = new Paths();
211 if ( $this->options->get_track_js_endpoint() === 'restapi' ) {
212 $js_endpoint = $paths->get_js_tracker_rest_api_endpoint();
213 } elseif ( $this->options->get_track_js_endpoint() === 'plugin' ) {
214 $js_endpoint = plugins_url( 'app/matomo.js', MATOMO_ANALYTICS_FILE );
215 } else {
216 $js_endpoint = $paths->get_js_tracker_url_in_matomo_dir();
217 }
218
219 if ( $this->options->get_force_protocol() === 'https' ) {
220 $js_endpoint = preg_replace( '(^http://)', 'https://', $js_endpoint );
221 } else {
222 $js_endpoint = preg_replace( '(^https?://)', '//', $js_endpoint );
223 }
224
225 return $js_endpoint;
226 }
227
228 /**
229 * @param int|string $idsite
230 *
231 * @return array
232 */
233 public function prepare_tracking_code( $idsite ) {
234 $log_level = is_admin() ? Logger::LEVEL_DEBUG : Logger::LEVEL_INFO;
235
236 $this->logger->log( 'Apply tracking code changes:', $log_level );
237
238 $tracker_endpoint = $this->get_tracker_endpoint();
239 $js_endpoint = $this->get_js_endpoint();
240
241 $options = [];
242
243 if ( $this->options->get_set_download_extensions() ) {
244 $options[] = "_paq.push(['setDownloadExtensions', " . wp_json_encode( $this->options->get_set_download_extensions() ) . ']);';
245 }
246 if ( $this->options->get_add_download_extensions() ) {
247 $options[] = "_paq.push(['addDownloadExtensions', " . wp_json_encode( $this->options->get_add_download_extensions() ) . ']);';
248 }
249 if ( $this->options->get_set_download_classes() ) {
250 $options[] = "_paq.push(['setDownloadClasses', " . wp_json_encode( $this->options->get_set_download_classes() ) . ']);';
251 }
252 if ( $this->options->get_set_link_classes() ) {
253 $options[] = "_paq.push(['setLinkClasses', " . wp_json_encode( $this->options->get_set_link_classes() ) . ']);';
254 }
255 if ( $this->options->get_disable_cookies() ) {
256 $options[] = self::get_disable_cookies_partial();
257 }
258 if ( $this->options->get_track_crossdomain_linking() ) {
259 $options[] = "_paq.push(['enableCrossDomainLinking']);";
260 }
261 if ( $this->options->get_track_jserrors() ) {
262 $options[] = "_paq.push(['enableJSErrorTracking']);";
263 }
264
265 $cookie_domain = $this->get_tracking_cookie_domain();
266 if ( ! empty( $cookie_domain ) ) {
267 $options[] = '_paq.push(["setCookieDomain", ' . wp_json_encode( $cookie_domain ) . ']);';
268 }
269
270 $track_across_alias = $this->options->get_track_across_alias();
271
272 if ( $track_across_alias ) {
273 // todo detect more hosts such as when using WPML etc
274 $hosts = [ wp_parse_url( home_url(), PHP_URL_HOST ) ];
275 $hosts = array_filter( $hosts );
276 $hosts = array_map(
277 function ( $host ) {
278 return '*.' . $host;
279 },
280 $hosts
281 );
282 if ( ! empty( $hosts ) ) {
283 $options[] = '_paq.push(["setDomains", ' . wp_json_encode( $hosts ) . ']);';
284 }
285 }
286 if ( $this->options->get_force_post() ) {
287 $options[] = "_paq.push(['setRequestMethod', 'POST']);";
288 }
289
290 $cookie_consent = new CookieConsent();
291 $cookie_consent_option = $cookie_consent->get_tracking_consent_option( $this->options->get_cookie_consent() );
292 // for unit test cases
293 if ( ! empty( $cookie_consent_option ) ) {
294 $options[] = $cookie_consent_option;
295 }
296
297 if ( $this->options->get_limit_cookies() ) {
298 $options[] = "_paq.push(['setVisitorCookieTimeout', " . wp_json_encode( $this->options->get_limit_cookies_visitor() ) . ']);';
299 $options[] = "_paq.push(['setSessionCookieTimeout', " . wp_json_encode( $this->options->get_limit_cookies_session() ) . ']);';
300 $options[] = "_paq.push(['setReferralCookieTimeout', " . wp_json_encode( $this->options->get_limit_cookies_referral() ) . ']);';
301 }
302 if ( $this->options->get_track_content() === 'all' ) {
303 $options[] = "_paq.push(['trackAllContentImpressions']);";
304 } elseif ( $this->options->get_track_content() === 'visible' ) {
305 $options[] = "_paq.push(['trackVisibleContentImpressions']);";
306 }
307 if ( (int) $this->options->get_track_heartbeat() > 0 ) {
308 $options[] = "_paq.push(['enableHeartBeatTimer', " . intval( $this->options->get_track_heartbeat() ) . ']);';
309 }
310
311 $data_cf_async = '';
312 $data_of_async_option = [];
313 if ( $this->options->get_track_datacfasync() ) {
314 $data_cf_async = 'data-cfasync="false"';
315 $data_of_async_option['data-cfasync'] = 'false';
316 }
317
318 $script = '';
319
320 if ( $this->settings->is_ai_bot_tracking_enabled() ) {
321 // recMode is a temporary parameter introduced in core to conditionally
322 // enable AI bot tracking. if AI bot tracking is enabled in MWP, we set
323 // it to `2` here, to enable "auto" mode when doing JS tracking. in this
324 // mode, tracking requests with AI bot user agents will be tracked as bots
325 // instead of visits, while all other requests will be tracked normally
326 // as visits.
327 $options[] = "_paq.push(['appendToTrackingUrl', 'recMode=2']);";
328
329 // set cookie via javascript cookie for known AI bots so we can skip tracking server side
330 // for them.
331 // NOTE: this must be done ONLY for known AI bots to be compliant with privacy regulations.
332 $user_agent_substrings = wp_json_encode( AjaxTracker::AI_BOT_USER_AGENT_SUBSTRINGS );
333 array_unshift(
334 $options,
335 <<<EOF
336 _paq.push([ function () {
337 var userAgentSubstrings = $user_agent_substrings;
338 for (var i = 0; i < userAgentSubstrings.length; ++i) {
339 var isAiBotUserAgent = navigator.userAgent.toLowerCase().indexOf(userAgentSubstrings[i].toLowerCase()) !== -1;
340 if (isAiBotUserAgent) {
341 var path = this.getCookiePath();
342 var domain = this.getCookieDomain();
343 var sameSite = 'Lax';
344
345 document.cookie = 'matomo_has_js=1;path=' +
346 (path || '/') +
347 (domain ? ';domain=' + domain : '') +
348 ';SameSite=' + sameSite
349 ;
350
351 return;
352 }
353 }
354 } ]);
355 EOF
356 );
357 }
358
359 $script .= "var _paq = window._paq = window._paq || [];\n";
360 $script .= implode( "\n", $options );
361 $script .= self::TRACKPAGEVIEW;
362 $script .= "_paq.push(['enableLinkTracking']);_paq.push(['alwaysUseSendBeacon']);";
363 $script .= "_paq.push(['setTrackerUrl', " . wp_json_encode( $tracker_endpoint ) . ']);';
364 $script .= "_paq.push(['setSiteId', '" . intval( $idsite ) . "']);";
365 $script .= "var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
366 g.type='text/javascript'; g.async=true; g.src=" . wp_json_encode( $js_endpoint ) . '; s.parentNode.insertBefore(g,s);';
367
368 $script = <<<EOF
369 (function () {
370 function initTracking() {
371 $script
372 }
373 if (document.prerendering) {
374 document.addEventListener('prerenderingchange', initTracking, {once: true});
375 } else {
376 initTracking();
377 }
378 })();
379 EOF;
380
381 if ( function_exists( 'wp_get_inline_script_tag' ) ) {
382 $script = wp_get_inline_script_tag(
383 $script,
384 $data_of_async_option
385 );
386 } else {
387 /*
388 * method wp_get_inline_script_tag add a line feed.
389 * to get the unit tests pass, we add a line feed when not using the method
390 */
391 $script = '<script ' . $data_cf_async . ">\n" . $script . "\n</script>\n";
392 }
393
394 $script = '<!-- Matomo -->' . $script . '<!-- End Matomo Code -->';
395
396 $no_script = '<noscript><p><img referrerpolicy="no-referrer-when-downgrade" src="' . esc_url( $tracker_endpoint ) . '?idsite=' . intval( $idsite ) . '&amp;rec=1" style="border:0;" alt="" /></p></noscript>';
397
398 $script = apply_filters( 'matomo_tracking_code_script', $script, $idsite );
399 $script = apply_filters( 'matomo_tracking_code_noscript', $script, $idsite );
400
401 $this->logger->log( 'Finished tracking code: ' . $script, $log_level );
402 $this->logger->log( 'Finished noscript code: ' . $no_script, $log_level );
403
404 return [
405 'script' => $script,
406 'noscript' => $no_script,
407 ];
408 }
409
410 public function get_tracking_cookie_domain() {
411 if ( $this->options->get_track_across()
412 || $this->options->get_track_crossdomain_linking() ) {
413 $host = wp_parse_url( home_url(), PHP_URL_HOST );
414 if ( ! empty( $host ) ) {
415 return '*.' . $host;
416 }
417 }
418
419 return '';
420 }
421
422 private function apply_404_changes( $tracking_code ) {
423 $this->logger->log( 'Apply 404 tracking changes. Blog ID: ' . get_current_blog_id() );
424
425 $code = "_paq.push(['setDocumentTitle', '404/URL = '+String(document.location.pathname+document.location.search).replace(/\//g,'%2f') + '/From = ' + String(document.referrer).replace(/\//g,'%2f')]);";
426 $tracking_code = str_replace( self::TRACKPAGEVIEW, $code . self::TRACKPAGEVIEW, $tracking_code );
427 $tracking_code = str_replace( self::MTM_INIT, $code . self::MTM_INIT, $tracking_code );
428
429 return $tracking_code;
430 }
431
432 private function apply_search_changes( $tracking_code ) {
433 $this->logger->log( 'Apply search tracking changes. Blog ID: ' . get_current_blog_id() );
434 $obj_search = new WP_Query( 's=' . get_search_query() . '&showposts=-1' );
435 $int_result_count = $obj_search->post_count;
436
437 $code = "window._paq = window._paq || []; window._paq.push(['trackSiteSearch','" . get_search_query() . "', false, " . $int_result_count . "]);\n";
438 $tracking_code = str_replace( self::TRACKPAGEVIEW, $code . self::TRACKPAGEVIEW, $tracking_code );
439 $tracking_code = str_replace( self::MTM_INIT, $code . self::MTM_INIT, $tracking_code );
440
441 return $tracking_code;
442 }
443
444 private function apply_user_tracking( $tracking_code ) {
445 $user_id_to_track = null;
446 if ( \is_user_logged_in() ) {
447 // Get the User ID Admin option, and the current user's data
448 $uid_from = $this->settings->get_global_option( 'track_user_id' );
449 $current_user = wp_get_current_user(); // current user
450 // Get the user ID based on the admin setting
451 if ( 'uid' === $uid_from ) {
452 $user_id_to_track = $current_user->ID;
453 } elseif ( 'email' === $uid_from ) {
454 $user_id_to_track = $current_user->user_email;
455 } elseif ( 'username' === $uid_from ) {
456 $user_id_to_track = $current_user->user_login;
457 } elseif ( 'displayname' === $uid_from ) {
458 $user_id_to_track = $current_user->display_name;
459 }
460 }
461 $user_id_to_track = apply_filters( 'matomo_tracking_user_id', $user_id_to_track );
462 // Check we got a User ID to track, and track it
463 if ( isset( $user_id_to_track ) && ! empty( $user_id_to_track ) ) {
464 $code = "window._paq = window._paq || []; window._paq.push(['setUserId', '" . esc_js( $user_id_to_track ) . "']);\n";
465 $tracking_code = str_replace( self::TRACKPAGEVIEW, $code . self::TRACKPAGEVIEW, $tracking_code );
466 $tracking_code = str_replace( self::MTM_INIT, $code . self::MTM_INIT, $tracking_code );
467 }
468
469 return $tracking_code;
470 }
471 }
472