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