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