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