PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 2.15.0
Independent Analytics – WordPress Analytics Plugin v2.15.0
2.15.0 2.14.10 trunk 1.1 1.10 1.10.1 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.17.1 1.17.2 1.17.3 1.17.4 1.18 1.18.1 1.19.0 1.19.1 1.2 1.20.0 1.21.0 1.22.0 1.22.1 1.23.0 1.23.1 1.24.0 1.24.1 1.25.0 1.25.1 1.26.0 1.27.0 1.28.0 1.28.1 1.28.2 1.28.3 1.29.0 1.3 1.30.0 1.30.1 1.4 1.5 1.6 1.7 1.8 1.9 2.0.0 2.0.1 2.1.4 2.1.5 2.1.6 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.10 2.11.2 2.11.3 2.11.4 2.11.5 2.11.6 2.11.7 2.11.8 2.11.9 2.12.0 2.12.1 2.12.2 2.13.1 2.13.2 2.13.5 2.13.6 2.14.0 2.14.1 2.14.2 2.14.4 2.14.6 2.14.7 2.14.8 2.14.9 2.2.0 2.2.1 2.3.1 2.3.2 2.4.2 2.4.3 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.7.1 2.7.2 2.7.3 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7
independent-analytics / IAWP / REST_API.php
independent-analytics / IAWP Last commit date
AJAX 5 days ago Admin_Page 5 days ago Click_Tracking 5 days ago ColumnOptions 6 months ago Cron 9 months ago Data_Pruning 5 days ago Date_Picker 3 months ago Date_Range 5 months ago Ecommerce 5 months ago Email_Reports 5 days ago Examiner 3 months ago Favicon 2 months ago Form_Submissions 5 days ago Integrations 5 days ago Journey 3 months ago Migrations 6 months ago Models 5 days ago Overview 2 months ago Public_API 5 months ago RealTime 5 days ago Rows 5 days ago Statistics 5 days ago Tables 5 days ago Utils 5 days ago Views 5 days ago ViewsColumn 5 days ago WooCommerceOrderMetaBox 5 months ago ActivationLifecycle.php 3 months ago Admin_Bar_Stats.php 5 days ago Appearance.php 1 year ago BreakdanceFormAction.php 5 days ago Campaign_Builder.php 5 days ago Capability_Manager.php 5 days ago Chart.php 2 months ago Chart_Data.php 1 year ago Click_Tracking.php 2 months ago ComplianzIntegration.php 3 months ago Cron_Job.php 5 months ago Cron_Manager.php 5 months ago Dashboard_Options.php 6 months ago Dashboard_Widget.php 2 months ago Database.php 8 months ago Database_Manager.php 5 days ago Empty_Report_Option.php 2 years ago Env.php 5 days ago Examiner_Config.php 11 months ago FetchFaviconsJob.php 5 months ago Filters.php 3 months ago Geo_Database_Health_Check_Job.php 9 months ago Geo_Database_Manager.php 6 months ago Geoposition.php 1 year ago Icon_Directory.php 6 months ago Icon_Directory_Factory.php 2 years ago Illuminate_Builder.php 10 months ago Independent_Analytics.php 5 days ago Interrupt.php 3 months ago Known_Referrers.php 5 days ago MainWP.php 2 months ago Map.php 9 months ago Map_Data.php 1 year ago Migration_Fixer_Job.php 5 months ago Patch.php 1 year ago Payload_Validator.php 9 months ago Plugin_Conflict_Detector.php 5 days ago Plugin_Group.php 6 months ago Plugin_Group_Option.php 2 years ago Query.php 1 year ago Query_Taps.php 4 months ago Quick_Stats.php 3 months ago REST_API.php 5 days ago Report.php 1 year ago Report_Finder.php 6 months ago Report_Options_Parser.php 9 months ago Resource_Identifier.php 2 months ago Settings.php 2 months ago Sort_Configuration.php 9 months ago Tables.php 8 months ago Track_Resource_Changes.php 1 year ago View_Counter.php 6 months ago VisitorSaltRefreshInterval.php 6 months ago WP_Option_Cache_Bust.php 2 years ago
REST_API.php
523 lines
1 <?php
2
3 namespace IAWP;
4
5 use IAWP\Click_Tracking\Link_Rule_Finder;
6 use IAWP\Click_Tracking\Site;
7 use IAWP\Models\Visitor;
8 use IAWP\Utils\Device;
9 use IAWP\Utils\Request;
10 use IAWP\Utils\Salt;
11 use IAWP\Utils\Security;
12 use IAWP\Utils\URL;
13 use IAWP\Views\CampaignParameters;
14 use IAWP\Views\View;
15 use IAWPSCOPED\Illuminate\Support\Str;
16 /** @internal */
17 class REST_API
18 {
19 public function __construct()
20 {
21 \add_action('rest_api_init', [$this, 'register_rest_api']);
22 \add_action('wp_footer', [$this, 'echo_tracking_script']);
23 // Support tracking where wp_footer isn't called
24 \add_action('iawp_output_tracking_script', [$this, 'echo_tracking_script']);
25 // Support for PDF Viewer by Themencode (free and pro versions)
26 \add_action('tnc_pvfw_viewer_head', [$this, 'echo_tracking_script']);
27 \add_action('tnc_pvfw_head', [$this, 'echo_tracking_script']);
28 // Support for Coming Soon and Maintenance by Colorlib
29 \add_action('ccsm_header', [$this, 'echo_tracking_script']);
30 // Support for CMP - Coming Soon & Maintenance
31 \add_action('cmp_footer', [$this, 'echo_tracking_script']);
32 // Support for Maintenance plugin
33 \add_action('add_gg_analytics_code', [$this, 'echo_tracking_script']);
34 }
35 public function echo_tracking_script()
36 {
37 \IAWP\Migrations\Migrations::handle_migration_18_error();
38 \IAWP\Migrations\Migrations::handle_migration_22_error();
39 \IAWP\Migrations\Migrations::handle_migration_29_error();
40 \IAWP\Migrations\Migrations::handle_migration_45_collation_error();
41 \IAWP\Migrations\Migrations::handle_migration_46_error();
42 \IAWP\Migrations\Migrations::create_or_migrate();
43 if (\IAWP\Migrations\Migrations::is_migrating()) {
44 return;
45 }
46 if (!\get_option('iawp_track_authenticated_users') && \is_user_logged_in()) {
47 return;
48 }
49 if (Request::is_blocked_user_role()) {
50 return;
51 }
52 if (isset($_COOKIE['iawp_ignore_visitor'])) {
53 return;
54 }
55 // Don't track post or page previews
56 if (\is_preview()) {
57 return;
58 }
59 // Don't track the Thrive Leads form builder
60 if (\array_key_exists('tve', $_GET)) {
61 return;
62 }
63 $payload = [];
64 $current_resource = \IAWP\Resource_Identifier::for_resource_being_viewed();
65 if (\is_null($current_resource)) {
66 return;
67 }
68 /** Use the iawp_user_excluded_posts filter to exclude the analytics tracking script on any page.
69 *
70 * add_filter( 'iawp_user_excluded_posts', function ( $ids ) {
71 * $ids[] = 1;
72 *
73 * return $ids;
74 * });
75 */
76 $user_excluded_posts = \apply_filters('iawp_user_excluded_posts', []);
77 if ($current_resource->type() === 'singular' && \in_array($current_resource->meta_value(), $user_excluded_posts)) {
78 return;
79 }
80 $payload['resource'] = $current_resource->type();
81 if ($current_resource->has_meta()) {
82 $payload[$current_resource->meta_key()] = $current_resource->meta_value();
83 }
84 $payload['page'] = \max(1, \get_query_var('paged'));
85 $data = ['payload' => $payload];
86 $data['signature'] = \md5(Salt::request_payload_salt() . \json_encode($data['payload']));
87 $track_view_url = \get_rest_url(null, '/iawp/search');
88 $track_click_url = \IAWPSCOPED\iawp_url_to('/iawp-click-endpoint.php');
89 $link_rules_json = \json_encode(Link_Rule_Finder::cached_link_rules());
90 $is_using_complianz = \function_exists('cmplz_integration_plugin_is_enabled') && \cmplz_integration_plugin_is_enabled('independent-analytics');
91 $attributes = '';
92 if ($is_using_complianz) {
93 $attributes = \wp_sanitize_script_attributes(['type' => 'text/plain', 'data-category' => 'statistics', 'data-service' => 'independent-analytics']);
94 ?>
95 <script>
96 document.addEventListener('cmplz_enable_category', (event) => {
97 if(event.detail.category === 'statistics') {
98 const event = new Event("iawpSearch");
99 document.dispatchEvent(event)
100 }
101 })
102 </script>
103 <?php
104 }
105 ?>
106 <script id="independent-analytics-script" <?php
107 echo $attributes;
108 ?> >
109 // Do not change this comment line otherwise Speed Optimizer won't be able to detect this script
110
111 (function () {
112 function sendRequest(url, body) {
113 if(!window.fetch) {
114 const xhr = new XMLHttpRequest();
115 xhr.open("POST", url, true);
116 xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
117 xhr.send(JSON.stringify(body))
118 return
119 }
120
121 const request = fetch(url, {
122 method: 'POST',
123 body: JSON.stringify(body),
124 keepalive: true,
125 headers: {
126 'Content-Type': 'application/json;charset=UTF-8'
127 }
128 });
129 }
130 const calculateParentDistance = (child, parent) => {
131 let count = 0;
132 let currentElement = child;
133
134 // Traverse up the DOM tree until we reach parent or the top of the DOM
135 while (currentElement && currentElement !== parent) {
136 currentElement = currentElement.parentNode;
137 count++;
138 }
139
140 // If parent was not found in the hierarchy, return -1
141 if (!currentElement) {
142 return -1; // Indicates parent is not an ancestor of element
143 }
144
145 return count; // Number of layers between element and parent
146 }
147 const isMatchingClass = (linkRule, href, classes, ids) => {
148 return classes.includes(linkRule.value)
149 }
150 const isMatchingId = (linkRule, href, classes, ids) => {
151 return ids.includes(linkRule.value)
152 }
153 const isMatchingDomain = (linkRule, href, classes, ids) => {
154 if(!URL.canParse(href)) {
155 return false
156 }
157
158 const url = new URL(href)
159 const host = url.host
160 const hostsToMatch = [host]
161
162 if(host.startsWith('www.')) {
163 hostsToMatch.push(host.substring(4))
164 } else {
165 hostsToMatch.push('www.' + host)
166 }
167
168 return hostsToMatch.includes(linkRule.value)
169 }
170 const isMatchingExtension = (linkRule, href, classes, ids) => {
171 if(!URL.canParse(href)) {
172 return false
173 }
174
175 const url = new URL(href)
176
177 return url.pathname.endsWith('.' + linkRule.value)
178 }
179 const isMatchingSubdirectory = (linkRule, href, classes, ids) => {
180 if(!URL.canParse(href)) {
181 return false
182 }
183
184 const url = new URL(href)
185
186 return url.pathname.startsWith('/' + linkRule.value + '/')
187 }
188 const isMatchingProtocol = (linkRule, href, classes, ids) => {
189 if(!URL.canParse(href)) {
190 return false
191 }
192
193 const url = new URL(href)
194
195 return url.protocol === linkRule.value + ':'
196 }
197 const isMatchingExternal = (linkRule, href, classes, ids) => {
198 if(!URL.canParse(href) || !URL.canParse(document.location.href)) {
199 return false
200 }
201
202 const matchingProtocols = ['http:', 'https:']
203 const siteUrl = new URL(document.location.href)
204 const linkUrl = new URL(href)
205
206 // Links to subdomains will appear to be external matches according to JavaScript,
207 // but the PHP rules will filter those events out.
208 return matchingProtocols.includes(linkUrl.protocol) && siteUrl.host !== linkUrl.host
209 }
210 const isMatch = (linkRule, href, classes, ids) => {
211 switch (linkRule.type) {
212 case 'class':
213 return isMatchingClass(linkRule, href, classes, ids)
214 case 'id':
215 return isMatchingId(linkRule, href, classes, ids)
216 case 'domain':
217 return isMatchingDomain(linkRule, href, classes, ids)
218 case 'extension':
219 return isMatchingExtension(linkRule, href, classes, ids)
220 case 'subdirectory':
221 return isMatchingSubdirectory(linkRule, href, classes, ids)
222 case 'protocol':
223 return isMatchingProtocol(linkRule, href, classes, ids)
224 case 'external':
225 return isMatchingExternal(linkRule, href, classes, ids)
226 default:
227 return false;
228 }
229 }
230 const track = (element) => {
231 const href = element.href ?? null
232 const classes = Array.from(element.classList)
233 const ids = [element.id]
234 const linkRules = <?php
235 echo $link_rules_json;
236 ?>
237
238 if(linkRules.length === 0) {
239 return
240 }
241
242 // For link rules that target an id, we need to allow that id to appear
243 // in any ancestor up to the 7th ancestor. This loop looks for those matches
244 // and counts them.
245 linkRules.forEach((linkRule) => {
246 if(linkRule.type !== 'id') {
247 return;
248 }
249
250 const matchingAncestor = element.closest('#' + linkRule.value)
251
252 if(!matchingAncestor || matchingAncestor.matches('html, body')) {
253 return;
254 }
255
256 const depth = calculateParentDistance(element, matchingAncestor)
257
258 if(depth < 7) {
259 ids.push(linkRule.value)
260 }
261 });
262
263 // For link rules that target a class, we need to allow that class to appear
264 // in any ancestor up to the 7th ancestor. This loop looks for those matches
265 // and counts them.
266 linkRules.forEach((linkRule) => {
267 if(linkRule.type !== 'class') {
268 return;
269 }
270
271 const matchingAncestor = element.closest('.' + linkRule.value)
272
273 if(!matchingAncestor || matchingAncestor.matches('html, body')) {
274 return;
275 }
276
277 const depth = calculateParentDistance(element, matchingAncestor)
278
279 if(depth < 7) {
280 classes.push(linkRule.value)
281 }
282 });
283
284 const hasMatch = linkRules.some((linkRule) => {
285 return isMatch(linkRule, href, classes, ids)
286 })
287
288 if(!hasMatch) {
289 return
290 }
291
292 const url = "<?php
293 echo $track_click_url;
294 ?>";
295 const siteId = "<?php
296 echo Site::id();
297 ?>";
298 const body = {
299 href: href,
300 classes: classes.join(' '),
301 ids: ids.join(' '),
302 siteId: siteId,
303 ...<?php
304 echo \json_encode($data);
305 ?>
306 };
307
308 sendRequest(url, body)
309 }
310 let hasSearched = false;
311 function search() {
312 if(hasSearched) {
313 return;
314 }
315 hasSearched = true;
316
317 if (document.hasOwnProperty("visibilityState") && document.visibilityState === "prerender") {
318 return;
319 }
320
321 <?php
322 if (!\defined('IAWP_TESTING')) {
323 ?>
324 if (navigator.webdriver || /bot|crawler|spider|crawling|semrushbot|chrome-lighthouse/i.test(navigator.userAgent)) {
325 return;
326 }
327 <?php
328 }
329 ?>
330
331 let referrer_url = null;
332
333 if (typeof document.referrer === 'string' && document.referrer.length > 0) {
334 referrer_url = document.referrer;
335 }
336
337 const params = location.search.slice(1).split('&').reduce((acc, s) => {
338 const [k, v] = s.split('=');
339 return Object.assign(acc, {[k]: v});
340 }, {});
341
342 const url = "<?php
343 echo $track_view_url;
344 ?>";
345 const body = {
346 referrer_url,
347 utm_source: params.utm_source,
348 utm_medium: params.utm_medium,
349 utm_campaign: params.utm_campaign,
350 utm_term: params.utm_term,
351 utm_content: params.utm_content,
352 gclid: params.gclid,
353 ...<?php
354 echo \json_encode($data);
355 ?>
356 };
357
358 sendRequest(url, body)
359 }
360 document.addEventListener('mousedown', function (event) {
361 <?php
362 if (!\defined('IAWP_TESTING')) {
363 ?>
364 if (navigator.webdriver || /bot|crawler|spider|crawling|semrushbot|chrome-lighthouse/i.test(navigator.userAgent)) {
365 return;
366 }
367 <?php
368 }
369 ?>
370
371 const element = event.target.closest('a')
372
373 if(!element) {
374 return
375 }
376
377 const isPro = <?php
378 echo \IAWPSCOPED\iawp_is_pro() ? 'true' : 'false';
379 ?>
380
381 if(!isPro) {
382 return
383 }
384
385 // Don't track left clicks with this event. The click event is used for that.
386 if(event.button === 0) {
387 return
388 }
389
390 track(element)
391 })
392 document.addEventListener('click', function (event) {
393 <?php
394 if (!\defined('IAWP_TESTING')) {
395 ?>
396 if (navigator.webdriver || /bot|crawler|spider|crawling|semrushbot|chrome-lighthouse/i.test(navigator.userAgent)) {
397 return;
398 }
399 <?php
400 }
401 ?>
402
403 const element = event.target.closest('a, button, input[type="submit"], input[type="button"]')
404
405 if(!element) {
406 return
407 }
408
409 const isPro = <?php
410 echo \IAWPSCOPED\iawp_is_pro() ? 'true' : 'false';
411 ?>
412
413 if(!isPro) {
414 return
415 }
416
417 track(element)
418 })
419 document.addEventListener('play', function (event) {
420 <?php
421 if (!\defined('IAWP_TESTING')) {
422 ?>
423 if (navigator.webdriver || /bot|crawler|spider|crawling|semrushbot|chrome-lighthouse/i.test(navigator.userAgent)) {
424 return;
425 }
426 <?php
427 }
428 ?>
429
430 const element = event.target.closest('audio, video')
431
432 if(!element) {
433 return
434 }
435
436 const isPro = <?php
437 echo \IAWPSCOPED\iawp_is_pro() ? 'true' : 'false';
438 ?>
439
440 if(!isPro) {
441 return
442 }
443
444 track(element)
445 }, true)
446 document.addEventListener("DOMContentLoaded", function (e) {
447 search();
448 });
449 document.addEventListener("iawpSearch", function (e) {
450 search();
451 });
452 })();
453 </script>
454 <?php
455 }
456 public function register_rest_api()
457 {
458 \register_rest_route('iawp', '/search', ['methods' => 'POST', 'callback' => [$this, 'track_view'], 'permission_callback' => function () {
459 return \true;
460 }]);
461 }
462 public function track_view($request)
463 {
464 if (Device::getInstance()->is_bot() && !\defined('IAWP_TESTING')) {
465 return;
466 }
467 \IAWP\Migrations\Migrations::handle_migration_18_error();
468 \IAWP\Migrations\Migrations::handle_migration_22_error();
469 \IAWP\Migrations\Migrations::handle_migration_29_error();
470 \IAWP\Migrations\Migrations::handle_migration_45_collation_error();
471 \IAWP\Migrations\Migrations::handle_migration_46_error();
472 \IAWP\Migrations\Migrations::create_or_migrate();
473 if (\IAWP\Migrations\Migrations::is_migrating()) {
474 return;
475 }
476 if (Request::is_ip_address_blocked()) {
477 return;
478 }
479 $correct_signature = \md5(Salt::request_payload_salt() . \json_encode($request['payload']));
480 if ($request['signature'] !== $correct_signature) {
481 return new \WP_REST_Response(['success' => \false], 200, ['X-IAWP' => 'iawp']);
482 }
483 $visitor = Visitor::fetch_current_visitor();
484 $campaign_parameters = CampaignParameters::make($this->decode_or_nullify($request['utm_source']), $this->decode_or_nullify($request['utm_medium']), $this->decode_or_nullify($request['utm_campaign']), $this->decode_or_nullify($request['utm_term']), $this->decode_or_nullify($request['utm_content']));
485 if (\IAWPSCOPED\iawp_is_free()) {
486 $campaign_parameters = null;
487 }
488 new View($request['payload'], $this->calculate_referrer_url($request), $visitor, $campaign_parameters);
489 return new \WP_REST_Response(['success' => \true], 200, ['X-IAWP' => 'iawp']);
490 }
491 private function calculate_referrer_url($request) : ?string
492 {
493 $referrer_url = $request['referrer_url'];
494 $url = new URL($referrer_url ?? '');
495 if (\is_string($this->decode_or_nullify($request['gclid'])) && $url->get_domain() !== 'googleads.g.doubleclick.net') {
496 $referrer_url = 'https://googleads.iawp';
497 }
498 if (\is_string($this->decode_or_nullify($request['fbclid'])) && ($url->get_domain() === 'facebook.com' || Str::endsWith($url->get_domain(), '.facebook.com'))) {
499 $referrer_url = 'https://facebookads.iawp';
500 }
501 if ($referrer_url === null || $url->get_domain() === null) {
502 return null;
503 }
504 if (Str::endsWith($url->get_domain(), '.local')) {
505 return null;
506 }
507 return $referrer_url;
508 }
509 private function decode_or_nullify($string) : ?string
510 {
511 if (!isset($string)) {
512 return null;
513 }
514 $safe_string = \trim(\urldecode($string));
515 $safe_string = \str_replace('+', ' ', $safe_string);
516 $safe_string = Security::string($safe_string);
517 if (\strlen($safe_string) === 0) {
518 return null;
519 }
520 return $safe_string;
521 }
522 }
523