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