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