Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 10, 2026

CVE-2024-13362: Freemius <= 2.10.1 – Reflected DOM-Based Cross-Site Scripting via url Parameter (independent-analytics)

Severity Medium (CVSS 6.1)
CWE 79
Vulnerable Version 2.9.7
Patched Version 2.10.0
Disclosed April 29, 2026

Analysis Overview

Atomic Edge analysis of CVE-2024-13362:

This is a reflected DOM-based cross-site scripting vulnerability in the Freemius SDK versions 2.10.1 and earlier, which affects multiple WordPress plugins and themes that bundle the Freemius library. The vulnerability allows unauthenticated attackers to inject arbitrary JavaScript into pages via the ‘url’ parameter due to insufficient input sanitization and output escaping. The CVSS score is 6.1 (Medium).

The root cause lies in the Freemius SDK’s handling of the ‘url’ parameter across various components. Specifically, the SDK’s code does not properly sanitize or escape the URL parameter before using it in JavaScript contexts. This allows attackers to inject arbitrary script code that executes in the victim’s browser when they interact with a crafted link. The affected endpoints typically include AJAX handlers and admin page functionality that process URL parameters without proper validation.

Exploitation requires tricking a user into clicking a specially crafted link. The attacker would send a URL containing malicious JavaScript in the ‘url’ parameter, such as: https://target.com/wp-admin/admin.php?page=freemius&url=javascript:alert(1). When the user clicks this link, the injected script executes in their browser context, allowing the attacker to steal cookies, session tokens, or perform actions on behalf of the victim.

The patch addresses the issue by adding proper input sanitization and output escaping for the ‘url’ parameter across all affected Freemius SDK components. The patch ensures that URLs are validated against a whitelist of allowed schemes and that output is properly encoded to prevent script execution.

Successful exploitation could lead to complete compromise of the affected WordPress admin account. An attacker could steal authentication cookies, capture keystrokes, exfiltrate sensitive data, or perform administrative actions on behalf of the victim. This is especially dangerous because many plugins and themes bundle Freemius, giving the vulnerability widespread impact.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/independent-analytics/IAWP/AJAX/AJAX_Manager.php
+++ b/independent-analytics/IAWP/AJAX/AJAX_Manager.php
@@ -24,12 +24,8 @@
         $this->instances[] = new IAWPAJAXDelete_Report();
         $this->instances[] = new IAWPAJAXDismiss_Notice();
         $this->instances[] = new IAWPAJAXEdit_Link();
-        $this->instances[] = new IAWPAJAXExport_Campaigns();
-        $this->instances[] = new IAWPAJAXExport_Clicks();
-        $this->instances[] = new IAWPAJAXExport_Devices();
-        $this->instances[] = new IAWPAJAXExport_Geo();
-        $this->instances[] = new IAWPAJAXExport_Pages();
-        $this->instances[] = new IAWPAJAXExport_Referrers();
+        $this->instances[] = new IAWPAJAXExport_Report_Statistics();
+        $this->instances[] = new IAWPAJAXExport_Report_Table();
         $this->instances[] = new IAWPAJAXExport_Reports();
         $this->instances[] = new IAWPAJAXFilter();
         $this->instances[] = new IAWPAJAXImport_Reports();
--- a/independent-analytics/IAWP/AJAX/Edit_Link.php
+++ b/independent-analytics/IAWP/AJAX/Edit_Link.php
@@ -39,6 +39,9 @@
             $link_properties['value'] = Link_Validator::sanitize_domain($link_properties['value']);
         } elseif ($link_properties['type'] == 'subdirectory') {
             $link_properties['value'] = Link_Validator::sanitize_subdirectory($link_properties['value']);
+        } elseif ($link_properties['type'] == 'external') {
+            // There's no value for external
+            $link_properties['value'] = '';
         } else {
             $link_properties['value'] = sanitize_text_field($link_properties['value']);
         }
--- a/independent-analytics/IAWP/AJAX/Export_Campaigns.php
+++ b/independent-analytics/IAWP/AJAX/Export_Campaigns.php
@@ -1,30 +0,0 @@
-<?php
-
-namespace IAWPAJAX;
-
-use IAWPCapability_Manager;
-use IAWPDate_RangeExact_Date_Range;
-use IAWPRowsCampaigns;
-use IAWPTablesTable_Campaigns;
-/** @internal */
-class Export_Campaigns extends IAWPAJAXAJAX
-{
-    protected function action_name() : string
-    {
-        return 'iawp_export_campaigns';
-    }
-    protected function requires_pro() : bool
-    {
-        return true;
-    }
-    protected function action_callback() : void
-    {
-        if (!Capability_Manager::can_edit()) {
-            return;
-        }
-        $campaigns = new Campaigns(Exact_Date_Range::comprehensive_range());
-        $table = new Table_Campaigns();
-        $csv = $table->csv($campaigns->rows());
-        echo $csv->to_string();
-    }
-}
--- a/independent-analytics/IAWP/AJAX/Export_Clicks.php
+++ b/independent-analytics/IAWP/AJAX/Export_Clicks.php
@@ -1,30 +0,0 @@
-<?php
-
-namespace IAWPAJAX;
-
-use IAWPCapability_Manager;
-use IAWPDate_RangeExact_Date_Range;
-use IAWPRowsClicks;
-use IAWPTablesTable_Clicks;
-/** @internal */
-class Export_Clicks extends IAWPAJAXAJAX
-{
-    protected function action_name() : string
-    {
-        return 'iawp_export_clicks';
-    }
-    protected function requires_pro() : bool
-    {
-        return true;
-    }
-    protected function action_callback() : void
-    {
-        if (!Capability_Manager::can_edit()) {
-            return;
-        }
-        $table = new Table_Clicks();
-        $clicks = new Clicks(Exact_Date_Range::comprehensive_range(), null, null, $table->sanitize_sort_parameters());
-        $csv = $table->csv($clicks->rows());
-        echo $csv->to_string();
-    }
-}
--- a/independent-analytics/IAWP/AJAX/Export_Devices.php
+++ b/independent-analytics/IAWP/AJAX/Export_Devices.php
@@ -1,26 +0,0 @@
-<?php
-
-namespace IAWPAJAX;
-
-use IAWPCapability_Manager;
-use IAWPDate_RangeExact_Date_Range;
-use IAWPRowsDevice_Types;
-use IAWPTablesTable_Devices;
-/** @internal */
-class Export_Devices extends IAWPAJAXAJAX
-{
-    protected function action_name() : string
-    {
-        return 'iawp_export_devices';
-    }
-    protected function action_callback() : void
-    {
-        if (!Capability_Manager::can_edit()) {
-            return;
-        }
-        $device_types = new Device_Types(Exact_Date_Range::comprehensive_range());
-        $table = new Table_Devices();
-        $csv = $table->csv($device_types->rows());
-        echo $csv->to_string();
-    }
-}
--- a/independent-analytics/IAWP/AJAX/Export_Geo.php
+++ b/independent-analytics/IAWP/AJAX/Export_Geo.php
@@ -1,26 +0,0 @@
-<?php
-
-namespace IAWPAJAX;
-
-use IAWPCapability_Manager;
-use IAWPDate_RangeExact_Date_Range;
-use IAWPRowsCountries;
-use IAWPTablesTable_Geo;
-/** @internal */
-class Export_Geo extends IAWPAJAXAJAX
-{
-    protected function action_name() : string
-    {
-        return 'iawp_export_geo';
-    }
-    protected function action_callback() : void
-    {
-        if (!Capability_Manager::can_edit()) {
-            return;
-        }
-        $geos = new Countries(Exact_Date_Range::comprehensive_range());
-        $table = new Table_Geo();
-        $csv = $table->csv($geos->rows());
-        echo $csv->to_string();
-    }
-}
--- a/independent-analytics/IAWP/AJAX/Export_Pages.php
+++ b/independent-analytics/IAWP/AJAX/Export_Pages.php
@@ -1,26 +0,0 @@
-<?php
-
-namespace IAWPAJAX;
-
-use IAWPCapability_Manager;
-use IAWPDate_RangeExact_Date_Range;
-use IAWPRowsPages;
-use IAWPTablesTable_Pages;
-/** @internal */
-class Export_Pages extends IAWPAJAXAJAX
-{
-    protected function action_name() : string
-    {
-        return 'iawp_export_pages';
-    }
-    protected function action_callback() : void
-    {
-        if (!Capability_Manager::can_edit()) {
-            return;
-        }
-        $resources = new Pages(Exact_Date_Range::comprehensive_range());
-        $table = new Table_Pages();
-        $csv = $table->csv($resources->rows());
-        echo $csv->to_string();
-    }
-}
--- a/independent-analytics/IAWP/AJAX/Export_Referrers.php
+++ b/independent-analytics/IAWP/AJAX/Export_Referrers.php
@@ -1,26 +0,0 @@
-<?php
-
-namespace IAWPAJAX;
-
-use IAWPCapability_Manager;
-use IAWPDate_RangeExact_Date_Range;
-use IAWPRowsReferrers;
-use IAWPTablesTable_Referrers;
-/** @internal */
-class Export_Referrers extends IAWPAJAXAJAX
-{
-    protected function action_name() : string
-    {
-        return 'iawp_export_referrers';
-    }
-    protected function action_callback() : void
-    {
-        if (!Capability_Manager::can_edit()) {
-            return;
-        }
-        $referrers = new Referrers(Exact_Date_Range::comprehensive_range());
-        $table = new Table_Referrers();
-        $csv = $table->csv($referrers->rows());
-        echo $csv->to_string();
-    }
-}
--- a/independent-analytics/IAWP/AJAX/Export_Report_Statistics.php
+++ b/independent-analytics/IAWP/AJAX/Export_Report_Statistics.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace IAWPAJAX;
+
+use DateTime;
+use IAWPDate_RangeDate_Range;
+use IAWPDate_RangeExact_Date_Range;
+use IAWPDate_RangeRelative_Date_Range;
+use IAWPEnv;
+use IAWPStatisticsIntervalsIntervals;
+use IAWPStatisticsStatistics;
+use IAWPTablesTable;
+use IAWPUtilsTimezone;
+use Throwable;
+/** @internal */
+class Export_Report_Statistics extends IAWPAJAXAJAX
+{
+    protected function action_name() : string
+    {
+        return 'iawp_export_report_statistics';
+    }
+    protected function action_callback() : void
+    {
+        $date_range = $this->get_date_range();
+        $is_new_date_range = $this->get_field('is_new_date_range') === 'true';
+        $filters = $this->get_field('filters') ?? [];
+        $sort_column = $this->get_field('sort_column') ?? null;
+        $sort_direction = $this->get_field('sort_direction') ?? null;
+        $group = $this->get_field('group') ?? null;
+        $chart_interval = $is_new_date_range ? Intervals::default_for($date_range->number_of_days()) : Intervals::find_by_id($this->get_field('chart_interval'));
+        $page = intval($this->get_field('page') ?? 1);
+        $number_of_rows = $page * IAWPSCOPEDiawp()->pagination_page_size();
+        $table_type = $this->get_field('table_type');
+        $is_geo_table = $table_type === 'geo';
+        $table_class = Env::get_table();
+        /** @var Table $table */
+        $table = new $table_class($group);
+        $filters = $table->sanitize_filters($filters);
+        $sort_configuration = $table->sanitize_sort_parameters($sort_column, $sort_direction);
+        $rows_class = $table->group()->rows_class();
+        $statistics_class = $table->group()->statistics_class();
+        if ($is_geo_table) {
+            $rows_query = new $rows_class($date_range, null, $filters, $sort_configuration);
+        } else {
+            $rows_query = new $rows_class($date_range, $number_of_rows, $filters, $sort_configuration);
+        }
+        if (empty($filters)) {
+            /** @var Statistics $statistics */
+            $statistics = new $statistics_class($date_range, null, $chart_interval);
+        } else {
+            $statistics = new $statistics_class($date_range, $rows_query, $chart_interval);
+        }
+        wp_send_json_success(['csv' => $statistics->get_statistics_as_csv()->to_string()]);
+    }
+    /**
+     * Get the date range for the filter request
+     *
+     * The date info can be supplied in one of two ways.
+     *
+     * The first is to provide a relative_range_id which is converted into start, end, and label.
+     *
+     * The second is to provide explicit start and end fields which will be used as is.
+     *
+     * @return Date_Range
+     */
+    private function get_date_range() : Date_Range
+    {
+        $relative_range_id = $this->get_field('relative_range_id');
+        $exact_start = $this->get_field('exact_start');
+        $exact_end = $this->get_field('exact_end');
+        if (!is_null($exact_start) && !is_null($exact_end)) {
+            try {
+                $start = new DateTime($exact_start, Timezone::site_timezone());
+                $end = new DateTime($exact_end, Timezone::site_timezone());
+                return new Exact_Date_Range($start, $end);
+            } catch (Throwable $e) {
+                // Do nothing and fall back to default relative date range
+            }
+        }
+        return new Relative_Date_Range($relative_range_id);
+    }
+}
--- a/independent-analytics/IAWP/AJAX/Export_Report_Table.php
+++ b/independent-analytics/IAWP/AJAX/Export_Report_Table.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace IAWPAJAX;
+
+use DateTime;
+use IAWPDate_RangeDate_Range;
+use IAWPDate_RangeExact_Date_Range;
+use IAWPDate_RangeRelative_Date_Range;
+use IAWPEnv;
+use IAWPTablesTable;
+use IAWPUtilsTimezone;
+use Throwable;
+/** @internal */
+class Export_Report_Table extends IAWPAJAXAJAX
+{
+    protected function action_name() : string
+    {
+        return 'iawp_export_report_table';
+    }
+    protected function action_callback() : void
+    {
+        $date_range = $this->get_date_range();
+        $filters = $this->get_field('filters') ?? [];
+        $sort_column = $this->get_field('sort_column') ?? null;
+        $sort_direction = $this->get_field('sort_direction') ?? null;
+        $group = $this->get_field('group') ?? null;
+        $table_class = Env::get_table();
+        /** @var Table $table */
+        $table = new $table_class($group);
+        $filters = $table->sanitize_filters($filters);
+        $sort_configuration = $table->sanitize_sort_parameters($sort_column, $sort_direction);
+        $rows_class = $table->group()->rows_class();
+        $rows_query = new $rows_class($date_range, null, $filters, $sort_configuration);
+        $rows = $rows_query->rows();
+        $csv = $table->csv($rows, true);
+        wp_send_json_success(['csv' => $csv->to_string()]);
+    }
+    /**
+     * Get the date range for the filter request
+     *
+     * The date info can be supplied in one of two ways.
+     *
+     * The first is to provide a relative_range_id which is converted into start, end, and label.
+     *
+     * The second is to provide explicit start and end fields which will be used as is.
+     *
+     * @return Date_Range
+     */
+    private function get_date_range() : Date_Range
+    {
+        $relative_range_id = $this->get_field('relative_range_id');
+        $exact_start = $this->get_field('exact_start');
+        $exact_end = $this->get_field('exact_end');
+        if (!is_null($exact_start) && !is_null($exact_end)) {
+            try {
+                $start = new DateTime($exact_start, Timezone::site_timezone());
+                $end = new DateTime($exact_end, Timezone::site_timezone());
+                return new Exact_Date_Range($start, $end);
+            } catch (Throwable $e) {
+                // Do nothing and fall back to default relative date range
+            }
+        }
+        return new Relative_Date_Range($relative_range_id);
+    }
+}
--- a/independent-analytics/IAWP/AJAX/Export_Reports.php
+++ b/independent-analytics/IAWP/AJAX/Export_Reports.php
@@ -34,6 +34,6 @@
         $reports_array = array_map(function ($report) {
             return $report->to_array();
         }, $reports);
-        wp_send_json_success(['json' => json_encode(['plugin_version' => '2.9.7', 'database_version' => '39', 'export_version' => '1', 'reports' => $reports_array])]);
+        wp_send_json_success(['json' => json_encode(['plugin_version' => '2.10.0', 'database_version' => '42', 'export_version' => '1', 'reports' => $reports_array])]);
     }
 }
--- a/independent-analytics/IAWP/AJAX/Sort_Links.php
+++ b/independent-analytics/IAWP/AJAX/Sort_Links.php
@@ -4,7 +4,6 @@

 use IAWPCapability_Manager;
 use IAWPIlluminate_Builder;
-use IAWPQuery;
 use IAWPTables;
 /** @internal */
 class Sort_Links extends IAWPAJAXAJAX
--- a/independent-analytics/IAWP/Admin_Page/Analytics_Page.php
+++ b/independent-analytics/IAWP/Admin_Page/Analytics_Page.php
@@ -11,6 +11,7 @@
 use IAWPPlugin_Conflict_Detector;
 use IAWPQuick_Stats;
 use IAWPReal_Time;
+use IAWPReport;
 use IAWPReport_Finder;
 use IAWPTablesTable;
 use IAWPTablesTable_Campaigns;
@@ -28,49 +29,51 @@
         $options = Dashboard_Options::getInstance();
         $date_rage = $options->get_date_range();
         $tab = (new Env())->get_tab();
+        $report = (new Report_Finder())->current();
+        $is_showing_skeleton_ui = $report instanceof Report && $report->has_filters();
         if ($tab === 'views') {
             $table = new Table_Pages();
             $statistics_class = $table->group()->statistics_class();
             $statistics = new $statistics_class($date_rage, null, $options->chart_interval());
-            $stats = new Quick_Stats($statistics);
-            $chart = new Chart($statistics);
+            $stats = new Quick_Stats($statistics, false, $is_showing_skeleton_ui);
+            $chart = new Chart($statistics, false, $is_showing_skeleton_ui);
             $this->interface($table, $stats, $chart);
         } elseif ($tab === 'referrers') {
             $table = new Table_Referrers();
             $statistics_class = $table->group()->statistics_class();
             $statistics = new $statistics_class($date_rage, null, $options->chart_interval());
-            $stats = new Quick_Stats($statistics);
-            $chart = new Chart($statistics);
+            $stats = new Quick_Stats($statistics, false, $is_showing_skeleton_ui);
+            $chart = new Chart($statistics, false, $is_showing_skeleton_ui);
             $this->interface($table, $stats, $chart);
         } elseif ($tab === 'geo') {
             $table = new Table_Geo($options->group());
             $statistics_class = $table->group()->statistics_class();
             $statistics = new $statistics_class($date_rage, null, $options->chart_interval());
-            $stats = new Quick_Stats($statistics);
+            $stats = new Quick_Stats($statistics, false, $is_showing_skeleton_ui);
             $table_data_class = $table->group()->rows_class();
             $geo_data = new $table_data_class($date_rage);
-            $chart = new Chart_Geo($geo_data->rows());
+            $chart = new Chart_Geo($geo_data->rows(), null, $is_showing_skeleton_ui);
             $this->interface($table, $stats, $chart);
         } elseif ($tab === 'campaigns') {
             $table = new Table_Campaigns();
             $statistics_class = $table->group()->statistics_class();
             $statistics = new $statistics_class($date_rage, null, $options->chart_interval());
-            $stats = new Quick_Stats($statistics);
-            $chart = new Chart($statistics);
+            $stats = new Quick_Stats($statistics, false, $is_showing_skeleton_ui);
+            $chart = new Chart($statistics, false, $is_showing_skeleton_ui);
             $this->interface($table, $stats, $chart);
         } elseif ($tab === 'clicks') {
-            $table = new Table_Clicks();
+            $table = new Table_Clicks($options->group());
             $statistics_class = $table->group()->statistics_class();
             $statistics = new $statistics_class($date_rage, null, $options->chart_interval());
-            $stats = new Quick_Stats($statistics);
-            $chart = new Chart($statistics);
+            $stats = new Quick_Stats($statistics, false, $is_showing_skeleton_ui);
+            $chart = new Chart($statistics, false, $is_showing_skeleton_ui);
             $this->interface($table, $stats, $chart);
         } elseif ($tab === 'devices') {
             $table = new Table_Devices($options->group());
             $statistics_class = $table->group()->statistics_class();
             $statistics = new $statistics_class($date_rage, null, $options->chart_interval());
-            $stats = new Quick_Stats($statistics);
-            $chart = new Chart($statistics);
+            $stats = new Quick_Stats($statistics, false, $is_showing_skeleton_ui);
+            $chart = new Chart($statistics, false, $is_showing_skeleton_ui);
             $this->interface($table, $stats, $chart);
         } elseif ($tab === 'real-time') {
             (new Real_Time())->render_real_time_analytics();
--- a/independent-analytics/IAWP/Chart.php
+++ b/independent-analytics/IAWP/Chart.php
@@ -11,10 +11,12 @@
 {
     private $statistics;
     private $is_preview;
-    public function __construct(Statistics $statistics, bool $is_preview = false)
+    private $is_showing_skeleton_ui;
+    public function __construct(Statistics $statistics, bool $is_preview = false, bool $is_showing_skeleton_ui = false)
     {
         $this->statistics = $statistics;
         $this->is_preview = $is_preview;
+        $this->is_showing_skeleton_ui = $is_showing_skeleton_ui;
     }
     public function get_html() : string
     {
@@ -26,6 +28,10 @@
         }, $primary_statistic->statistic_over_time());
         $data = [];
         foreach ($this->statistics->get_statistics() as $statistic) {
+            if ($this->is_showing_skeleton_ui) {
+                $data[$statistic->id()] = [];
+                continue;
+            }
             $data[$statistic->id()] = array_map(function ($data_point) {
                 return $data_point[1];
             }, $statistic->statistic_over_time());
@@ -52,6 +58,13 @@
         if (IAWPSCOPEDiawp()->is_surecart_support_enabled()) {
             return SureCart_Store::get_currency_code();
         }
+        if (IAWPSCOPEDiawp()->is_edd_support_enabled()) {
+            return edd_get_currency();
+        }
+        if (IAWPSCOPEDiawp()->is_pmpro_support_enabled()) {
+            global $pmpro_default_currency;
+            return $pmpro_default_currency;
+        }
         return null;
     }
 }
--- a/independent-analytics/IAWP/Chart_Geo.php
+++ b/independent-analytics/IAWP/Chart_Geo.php
@@ -8,20 +8,26 @@
 {
     private $countries;
     private $title;
+    private $is_showing_skeleton_ui;
     /**
      * @param Geo[] $geos
      * @param $title
      */
-    public function __construct(array $geos, $title = null)
+    public function __construct(array $geos, $title = null, bool $is_showing_skeleton_ui = false)
     {
         $this->countries = $this->parse($geos);
         $this->title = $title;
+        $this->is_showing_skeleton_ui = $is_showing_skeleton_ui;
     }
     public function get_html()
     {
-        $chart_data = array_map(function ($country) {
-            return [$country['country_code'], $country['views'], $this->get_tooltip($country)];
-        }, $this->countries);
+        if ($this->is_showing_skeleton_ui) {
+            $chart_data = [];
+        } else {
+            $chart_data = array_map(function ($country) {
+                return [$country['country_code'], $country['views'], $this->get_tooltip($country)];
+            }, $this->countries);
+        }
         $dark_mode = IAWPSCOPEDiawp()->get_option('iawp_dark_mode', '0');
         ob_start();
         ?>
--- a/independent-analytics/IAWP/Click_Tracking.php
+++ b/independent-analytics/IAWP/Click_Tracking.php
@@ -16,9 +16,9 @@
             return $link_rule->to_array();
         })->all(), 'types' => self::types(), 'extensions' => self::extensions(), 'protocols' => self::protocols(), 'error_messages' => Link_Validator::error_messages(), 'show_click_tracking_cache_message' => $show_click_tracking_cache_message]);
     }
-    public static function types()
+    public static function types() : array
     {
-        return ['class' => esc_html__('Class', 'independent-analytics'), 'extension' => esc_html__('Extension', 'independent-analytics'), 'domain' => esc_html__('Domain', 'independent-analytics'), 'subdirectory' => esc_html__('Subdirectory', 'independent-analytics'), 'protocol' => esc_html__('Protocol', 'independent-analytics')];
+        return ['class' => __('Class', 'independent-analytics'), 'extension' => __('Extension', 'independent-analytics'), 'domain' => __('Domain', 'independent-analytics'), 'external' => __('External', 'independent-analytics'), 'subdirectory' => __('Subdirectory', 'independent-analytics'), 'protocol' => __('Protocol', 'independent-analytics')];
     }
     public static function extensions()
     {
@@ -26,6 +26,6 @@
     }
     public static function protocols()
     {
-        return ['mailto', 'tel'];
+        return ['mailto', 'tel', 'sms'];
     }
 }
--- a/independent-analytics/IAWP/Click_Tracking/Click.php
+++ b/independent-analytics/IAWP/Click_Tracking/Click.php
@@ -52,7 +52,7 @@
         if (is_null($href)) {
             return null;
         }
-        if (Str::startsWith($href, ['tel:', 'mailto:'])) {
+        if (Str::startsWith($href, ['tel:', 'sms:', 'mailto:'])) {
             return Str::before($href, ':');
         }
         return null;
@@ -62,7 +62,7 @@
         if (is_null($href)) {
             return null;
         }
-        if (Str::startsWith($href, ['tel:', 'mailto:'])) {
+        if (Str::startsWith($href, ['tel:', 'sms:', 'mailto:'])) {
             return Str::after($href, ':');
         }
         return $href;
--- a/independent-analytics/IAWP/Click_Tracking/Click_Processing_Job.php
+++ b/independent-analytics/IAWP/Click_Tracking/Click_Processing_Job.php
@@ -69,14 +69,14 @@
     }
     private function create_job_file(string $file) : ?string
     {
-        if (!is_file($file)) {
+        if (!is_readable($file) || !is_writable($file)) {
             return null;
         }
         $job_id = rand();
         $extension = pathinfo($file, PATHINFO_EXTENSION);
         $job_file = Str::finish(dirname($file), DIRECTORY_SEPARATOR) . "iawp-click-data-{$job_id}.{$extension}";
-        rename($file, $job_file);
-        if (!is_file($job_file)) {
+        $was_renamed = rename($file, $job_file);
+        if (!$was_renamed || !is_file($job_file)) {
             return null;
         }
         return $job_file;
--- a/independent-analytics/IAWP/Click_Tracking/Link_Rule_Finder.php
+++ b/independent-analytics/IAWP/Click_Tracking/Link_Rule_Finder.php
@@ -41,6 +41,8 @@
                 return $this->is_matching_subdirectory($link_rule);
             case 'protocol':
                 return $this->is_matching_protocol($link_rule);
+            case 'external':
+                return $this->is_matching_external($link_rule);
             default:
                 return false;
         }
@@ -100,6 +102,19 @@
         }
         return $this->protocol === $link_rule->value();
     }
+    private function is_matching_external($link_rule) : bool
+    {
+        if (is_null($this->href)) {
+            return false;
+        }
+        $site_url = URL::new(get_site_url());
+        $link_url = URL::new($this->href);
+        // Only track valid http/https URLs and not other protocols like mailto:, tel:, etc
+        if (!$link_url->is_valid_url()) {
+            return false;
+        }
+        return $link_url->get_domain() !== $site_url->get_domain();
+    }
     public static function new(?string $protocol, ?string $href, string $classes) : self
     {
         return new self($protocol, $href, $classes);
--- a/independent-analytics/IAWP/Data_Pruning/Pruning_Scheduler.php
+++ b/independent-analytics/IAWP/Data_Pruning/Pruning_Scheduler.php
@@ -5,8 +5,8 @@
 use IAWPSCOPEDCarbonCarbonImmutable;
 use IAWPIlluminate_Builder;
 use IAWPQuery;
-use IAWPUtilsWordPress_Site_Date_Format_Pattern;
 use IAWPUtilsTimezone;
+use IAWPUtilsWordPress_Site_Date_Format_Pattern;
 /** @internal */
 class Pruning_Scheduler
 {
--- a/independent-analytics/IAWP/Date_Picker/Month.php
+++ b/independent-analytics/IAWP/Date_Picker/Month.php
@@ -2,7 +2,6 @@

 namespace IAWPDate_Picker;

-use IAWPUtilsTimezone;
 /** @internal */
 class Month
 {
--- a/independent-analytics/IAWP/Date_Range/Exact_Date_Range.php
+++ b/independent-analytics/IAWP/Date_Range/Exact_Date_Range.php
@@ -3,7 +3,6 @@
 namespace IAWPDate_Range;

 use DateTime;
-use IAWPUtilsTimezone;
 /** @internal */
 class Exact_Date_Range extends IAWPDate_RangeDate_Range
 {
--- a/independent-analytics/IAWP/Ecommerce/EDD_Order.php
+++ b/independent-analytics/IAWP/Ecommerce/EDD_Order.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace IAWPEcommerce;
+
+use IAWPIlluminate_Builder;
+use IAWPModelsVisitor;
+use IAWPTables;
+/** @internal */
+class EDD_Order
+{
+    private $order_id;
+    private $status;
+    private $total;
+    private $total_refunded;
+    private $total_refunds;
+    private $is_discounted;
+    public function __construct(int $order_id)
+    {
+        $order = edd_get_order($order_id);
+        $refunds = edd_get_order_refunds($order_id);
+        $total_refunded = 0;
+        $total_refunds = 0;
+        foreach ($refunds as $refund) {
+            $total_refunds++;
+            $total_refunded += abs((float) $refund->total);
+        }
+        $this->order_id = $order_id;
+        $this->status = $order->status;
+        $this->total = intval(round((float) $order->total * 100));
+        $this->total_refunded = intval(round($total_refunded * 100));
+        $this->total_refunds = $total_refunds;
+        $this->is_discounted = (float) $order->discount > 0;
+    }
+    public function insert()
+    {
+        $visitor = Visitor::fetch_current_visitor();
+        if (!$visitor->has_recorded_session()) {
+            return;
+        }
+        Illuminate_Builder::new()->from(Tables::orders())->insertOrIgnore(['is_included_in_analytics' => $this->is_included_in_analytics($this->status), 'edd_order_id' => $this->order_id, 'edd_order_status' => $this->status, 'view_id' => $visitor->most_recent_view_id(), 'initial_view_id' => $visitor->most_recent_initial_view_id(), 'total' => $this->total, 'total_refunded' => $this->total_refunded, 'total_refunds' => $this->total_refunds, 'is_discounted' => $this->is_discounted, 'created_at' => (new DateTime())->format('Y-m-d H:i:s')]);
+    }
+    public function update() : void
+    {
+        Illuminate_Builder::new()->from(Tables::orders())->where('edd_order_id', '=', $this->order_id)->update(['is_included_in_analytics' => $this->is_included_in_analytics($this->status), 'edd_order_status' => $this->status, 'total' => $this->total, 'total_refunded' => $this->total_refunded, 'total_refunds' => $this->total_refunds, 'is_discounted' => $this->is_discounted]);
+    }
+    private function is_included_in_analytics(string $status) : bool
+    {
+        return in_array($status, ['complete', 'refunded', 'partially_refunded']);
+    }
+    public static function register_hooks() : void
+    {
+        // While the EDD docs recommend it, we cannot use `edd_after_order_actions` as it runs in
+        // a cron job 30 seconds after the order is completed. We need to run in the same request,
+        // so we can determine which visitor make the purchase and attach the order correctly.
+        add_action('edd_complete_purchase', function ($order_id) {
+            try {
+                $order = new self($order_id);
+                $order->insert();
+            } catch (Throwable $e) {
+                error_log('Independent Analytics was unable to track the analytics for a EDD order. Please report this error to Independent Analytics. The error message is below.');
+                error_log($e->getMessage());
+            }
+        }, 10, 1);
+        // Track order status changes
+        add_action('edd_update_payment_status', function ($order_id) {
+            try {
+                $order = new self($order_id);
+                $order->update();
+            } catch (Throwable $e) {
+                error_log('Independent Analytics was unable to track the analytics for a EDD order. Please report this error to Independent Analytics. The error message is below.');
+                error_log($e->getMessage());
+            }
+        }, 10, 1);
+        // Track refunds to orders
+        add_action('edd_refund_order', function ($order_id) {
+            try {
+                $order = new self($order_id);
+                $order->update();
+            } catch (Throwable $e) {
+                error_log('Independent Analytics was unable to track the analytics for a EDD order. Please report this error to Independent Analytics. The error message is below.');
+                error_log($e->getMessage());
+            }
+        }, 10, 1);
+    }
+}
--- a/independent-analytics/IAWP/Ecommerce/PMPro_Order.php
+++ b/independent-analytics/IAWP/Ecommerce/PMPro_Order.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace IAWPEcommerce;
+
+use IAWPIlluminate_Builder;
+use IAWPModelsVisitor;
+use IAWPTables;
+/** @internal */
+class PMPro_Order
+{
+    private $order_id;
+    private $status;
+    private $total;
+    private $total_refunded;
+    private $total_refunds;
+    private $is_discounted;
+    public function __construct(int $order_id)
+    {
+        $order = new MemberOrder($order_id);
+        $this->order_id = $order_id;
+        $this->status = $order->status;
+        $this->total = intval(round((float) $order->total * 100));
+        $this->total_refunded = $order->status === 'refunded' ? $this->total : 0;
+        $this->total_refunds = $order->status === 'refunded' ? 1 : 0;
+        $this->is_discounted = is_numeric($order->discount_code_id) && (int) $order->discount_code_id !== 0;
+    }
+    public function insert()
+    {
+        $visitor = Visitor::fetch_current_visitor();
+        if (!$visitor->has_recorded_session()) {
+            return;
+        }
+        Illuminate_Builder::new()->from(Tables::orders())->insertOrIgnore(['is_included_in_analytics' => $this->is_included_in_analytics($this->status), 'pmpro_order_id' => $this->order_id, 'pmpro_order_status' => $this->status, 'view_id' => $visitor->most_recent_view_id(), 'initial_view_id' => $visitor->most_recent_initial_view_id(), 'total' => $this->total, 'total_refunded' => $this->total_refunded, 'total_refunds' => $this->total_refunds, 'is_discounted' => $this->is_discounted, 'created_at' => (new DateTime())->format('Y-m-d H:i:s')]);
+    }
+    public function update() : void
+    {
+        Illuminate_Builder::new()->from(Tables::orders())->where('pmpro_order_id', '=', $this->order_id)->update(['is_included_in_analytics' => $this->is_included_in_analytics($this->status), 'pmpro_order_status' => $this->status, 'total' => $this->total, 'total_refunded' => $this->total_refunded, 'total_refunds' => $this->total_refunds, 'is_discounted' => $this->is_discounted]);
+    }
+    private function is_included_in_analytics(string $status) : bool
+    {
+        return in_array($status, ['success', 'refunded']);
+    }
+    public static function register_hooks() : void
+    {
+        // Create a new order when a PMPro order is created
+        add_action('pmpro_added_order', function ($pmpro_order) {
+            try {
+                $order = new self((int) $pmpro_order->id);
+                $order->insert();
+            } catch (Throwable $e) {
+                error_log('Independent Analytics was unable to track the analytics for a Paid Memberships Pro order. Please report this error to Independent Analytics. The error message is below.');
+                error_log($e->getMessage());
+            }
+        }, 10, 1);
+        // Calculating is_discounted doesn't seem possible at the time pmpro_added_order runs. This hooks
+        // runs just after it but allows is_discounted to be correctly determined.
+        add_action('pmpro_discount_code_used', function ($discount_code_id, $user_id, $order_id) {
+            try {
+                $order = new self((int) $order_id);
+                $order->update();
+            } catch (Throwable $e) {
+                error_log('Independent Analytics was unable to track the analytics for a Paid Memberships Pro order. Please report this error to Independent Analytics. The error message is below.');
+                error_log($e->getMessage());
+            }
+        }, 10, 3);
+        // Update an existing order when a PMPro order is updated
+        add_action('pmpro_updated_order', function ($pmpro_order) {
+            try {
+                $order = new self((int) $pmpro_order->id);
+                $order->update();
+            } catch (Throwable $e) {
+                error_log('Independent Analytics was unable to track the analytics for a Paid Memberships Pro order. Please report this error to Independent Analytics. The error message is below.');
+                error_log($e->getMessage());
+            }
+        }, 10, 1);
+    }
+}
--- a/independent-analytics/IAWP/Ecommerce/SureCart_Event_Sync_Job.php
+++ b/independent-analytics/IAWP/Ecommerce/SureCart_Event_Sync_Job.php
@@ -15,7 +15,8 @@
     }
     public function handle() : void
     {
-        $last_seen_event_at = get_option('iawp_last_seen_surecart_event_at', time());
+        $one_day_ago = time() - 86400;
+        $last_seen_event_at = get_option('iawp_last_seen_surecart_event_at', $one_day_ago);
         $events = new Collection();
         $page = 1;
         while (true) {
--- a/independent-analytics/IAWP/Email_Reports/Email_Reports.php
+++ b/independent-analytics/IAWP/Email_Reports/Email_Reports.php
@@ -7,13 +7,15 @@
 use IAWPRowsCampaigns;
 use IAWPRowsCountries;
 use IAWPRowsDevice_Types;
+use IAWPRowsForms;
+use IAWPRowsLink_Patterns;
 use IAWPRowsPages;
 use IAWPRowsReferrers;
 use IAWPSort_Configuration;
 use IAWPStatisticsPage_Statistics;
 use IAWPStatisticsStatistic;
-use IAWPUtilsURL;
 use IAWPUtilsTimezone;
+use IAWPUtilsURL;
 /** @internal */
 class Email_Reports
 {
@@ -29,10 +31,6 @@
         add_action('add_option_iawp_dow', [$this, 'maybe_reschedule'], 10, 0);
         add_action('iawp_send_email_report', [$this, 'send_email_report']);
     }
-    private function interval() : IAWPEmail_ReportsInterval
-    {
-        return IAWPEmail_ReportsInterval_Factory::from_option();
-    }
     public function schedule()
     {
         $this->unschedule();
@@ -101,9 +99,15 @@
             return;
         }
         $from = IAWPSCOPEDiawp()->get_option('iawp_email_report_from_address', get_option('admin_email'));
+        $reply_to = IAWPSCOPEDiawp()->get_option('iawp_email_report_reply_to_address', get_option('admin_email'));
         $body = $this->get_email_body();
         $headers[] = 'From: ' . get_bloginfo('name') . ' <' . esc_attr($from) . '>';
+        $headers[] = 'Reply-To: ' . esc_attr($reply_to);
         $headers[] = 'Content-Type: text/html; charset=UTF-8';
+        // Prevents WP HTML Mail plugin from breaking email design (https://wordpress.org/plugins/wp-html-mail/)
+        add_filter('haet_mail_use_template', function () {
+            return false;
+        });
         return wp_mail($to, $this->subject_line($is_test_email), $body, $headers);
     }
     public function get_email_body($colors = '')
@@ -114,6 +118,7 @@
         }));
         $chart = new IAWPEmail_ReportsEmail_Chart($statistics);
         $colors = $colors == '' ? IAWPSCOPEDiawp()->get_option('iawp_email_report_colors', ['#5123a0', '#fafafa', '#3a1e6b', '#fafafa', '#5123a0', '#a985e6', '#ece9f2', '#f7f5fa', '#ece9f2', '#dedae6']) : explode(',', $colors);
+        $footer_text = IAWPSCOPEDiawp()->get_option('iawp_email_report_footer', sprintf(esc_html__('This email was generated and delivered by %s', 'independent-analytics'), esc_url(get_site_url())));
         return IAWPSCOPEDiawp_blade()->run('email.email', [
             'site_title' => get_bloginfo('name'),
             'site_url' => URL::new(get_site_url())->get_domain(),
@@ -127,8 +132,13 @@
             'y_labels' => $chart->y_labels,
             'x_labels' => $chart->x_labels,
             'colors' => $colors,
+            'footer_text' => $footer_text,
         ]);
     }
+    private function interval() : IAWPEmail_ReportsInterval
+    {
+        return IAWPEmail_ReportsInterval_Factory::from_option();
+    }
     private function subject_line(bool $is_test_email) : string
     {
         $parts = [];
@@ -143,7 +153,7 @@
     private function get_top_ten() : array
     {
         $date_range = $this->interval()->date_range();
-        $queries = ['pages' => 'title', 'referrers' => 'referrer', 'countries' => 'country', 'devices' => 'device_type', 'campaigns' => 'title', 'landing_pages' => 'title', 'exit_pages' => 'title'];
+        $queries = ['pages' => 'title', 'referrers' => 'referrer', 'countries' => 'country', 'devices' => 'device_type', 'campaigns' => 'title', 'forms' => 'form_title', 'clicks' => 'link_name', 'landing_pages' => 'title', 'exit_pages' => 'title'];
         $top_ten = [];
         $sort_configuration = new Sort_Configuration('views', 'desc');
         $title = '';
@@ -163,6 +173,12 @@
             } elseif ($type === 'campaigns') {
                 $query = new Campaigns($date_range, 10, null, $sort_configuration);
                 $title = esc_html__('Campaigns', 'independent-analytics');
+            } elseif ($type === 'forms') {
+                $query = new Forms($date_range, 10, null, new Sort_Configuration('submissions', 'desc'));
+                $title = esc_html__('Forms', 'independent-analytics');
+            } elseif ($type === 'clicks') {
+                $query = new Link_Patterns($date_range, 10, null, new Sort_Configuration('link_clicks', 'desc'));
+                $title = esc_html__('Link Patterns', 'independent-analytics');
             } elseif ($type === 'landing_pages') {
                 $query = new Pages($date_range, 10, null, new Sort_Configuration('entrances', 'desc'));
                 $title = esc_html__('Landing Pages', 'independent-analytics');
@@ -181,12 +197,20 @@
                     $edited_title = $row->device_type();
                 } elseif ($type == 'campaigns') {
                     $edited_title = $row->utm_campaign();
+                } elseif ($type == 'forms') {
+                    $edited_title = $row->form_title();
+                } elseif ($type == 'clicks') {
+                    $edited_title = $row->link_name();
                 } else {
                     $edited_title = $row->title();
                 }
                 $edited_title = mb_strlen($edited_title) > 30 ? mb_substr($edited_title, 0, 30) . '...' : $edited_title;
                 $metric = 'views';
-                if ($type == 'landing_pages') {
+                if ($type == 'clicks') {
+                    $metric = 'link_clicks';
+                } elseif ($type == 'forms') {
+                    $metric = 'submissions';
+                } elseif ($type == 'landing_pages') {
                     $metric = 'entrances';
                 } elseif ($type == 'exit_pages') {
                     $metric = 'exits';
--- a/independent-analytics/IAWP/Email_Reports/Interval.php
+++ b/independent-analytics/IAWP/Email_Reports/Interval.php
@@ -6,8 +6,8 @@
 use IAWPDate_RangeDate_Range;
 use IAWPDate_RangeRelative_Date_Range;
 use IAWPUtilsString_Util;
-use IAWPSCOPEDIlluminateSupportCarbon;
 use IAWPUtilsTimezone;
+use IAWPSCOPEDIlluminateSupportCarbon;
 /** @internal */
 class Interval
 {
--- a/independent-analytics/IAWP/Form_Submissions/Form.php
+++ b/independent-analytics/IAWP/Form_Submissions/Form.php
@@ -17,9 +17,37 @@
     private $title;
     private $plugin_id;
     private static $forms = null;
-    private static $plugins = [['id' => 1, 'name' => 'Fluent Forms', 'plugin_slugs' => ['fluentform/fluentform.php']], ['id' => 2, 'name' => 'WPForms', 'plugin_slugs' => ['wpforms-lite/wpforms.php', 'wpforms/wpforms.php']], ['id' => 3, 'name' => 'Contact Form 7', 'plugin_slugs' => ['contact-form-7/wp-contact-form-7.php']], ['id' => 4, 'name' => 'Gravity Forms', 'plugin_slugs' => ['gravityforms/gravityforms.php']], ['id' => 5, 'name' => 'Ninja Forms', 'plugin_slugs' => ['ninja-forms/ninja-forms.php']], ['id' => 6, 'name' => 'MailOptin', 'plugin_slugs' => ['mailoptin/mailoptin.php']], ['id' => 7, 'name' => 'Convert Pro', 'plugin_slugs' => ['convertpro/convertpro.php']], ['id' => 8, 'name' => 'Elementor Pro', 'plugin_slugs' => ['elementor-pro/elementor-pro.php']], ['id' => 9, 'name' => 'JetFormBuilder', 'plugin_slugs' => ['jetformbuilder/jet-form-builder.php']], ['id' => 10, 'name' => 'Formidable Forms', 'plugin_slugs' => ['formidable/formidable.php']], ['id' => 11, 'name' => 'WS Form', 'plugin_slugs' => ['ws-form/ws-form.php', 'ws-form-pro/ws-form.php']], ['id' => 12, 'name' => 'Amelia', 'plugin_slugs' => ['ameliabooking/ameliabooking.php']], ['id' => 13, 'name' => 'Bricks Builder', 'theme' => 'bricks'], ['id' => 14, 'name' => 'ARForms', 'plugin_slugs' => ['arforms-form-builder/arforms-form-builder.php']], ['id' => 15, 'name' => 'Custom form submissions'], ['id' => 16, 'name' => 'Bit Form', 'plugin_slugs' => ['bit-form/bitforms.php']], ['id' => 17, 'name' => 'Forminator', 'plugin_slugs' => ['forminator/forminator.php']], ['id' => 18, 'name' => 'Hustle', 'plugin_slugs' => ['wordpress-popup/popover.php', 'hustle/opt-in.php']], ['id' => 19, 'name' => 'Avada', 'plugin_slugs' => ['fusion-builder/fusion-builder.php', 'fusion-core/fusion-core.php']], ['id' => 20, 'name' => 'WP Store Locator', 'plugin_slugs' => ['wp-store-locator/wp-store-locator.php']]];
+    private static $plugins = [
+        ['id' => 1, 'name' => 'Fluent Forms', 'plugin_slugs' => ['fluentform/fluentform.php']],
+        ['id' => 2, 'name' => 'WPForms', 'plugin_slugs' => ['wpforms-lite/wpforms.php', 'wpforms/wpforms.php']],
+        ['id' => 3, 'name' => 'Contact Form 7', 'plugin_slugs' => ['contact-form-7/wp-contact-form-7.php']],
+        ['id' => 4, 'name' => 'Gravity Forms', 'plugin_slugs' => ['gravityforms/gravityforms.php']],
+        ['id' => 5, 'name' => 'Ninja Forms', 'plugin_slugs' => ['ninja-forms/ninja-forms.php']],
+        ['id' => 6, 'name' => 'MailOptin', 'plugin_slugs' => ['mailoptin/mailoptin.php']],
+        ['id' => 7, 'name' => 'Convert Pro', 'plugin_slugs' => ['convertpro/convertpro.php']],
+        ['id' => 8, 'name' => 'Elementor Pro', 'plugin_slugs' => ['elementor-pro/elementor-pro.php']],
+        ['id' => 9, 'name' => 'JetFormBuilder', 'plugin_slugs' => ['jetformbuilder/jet-form-builder.php']],
+        ['id' => 10, 'name' => 'Formidable Forms', 'plugin_slugs' => ['formidable/formidable.php']],
+        ['id' => 11, 'name' => 'WS Form', 'plugin_slugs' => ['ws-form/ws-form.php', 'ws-form-pro/ws-form.php']],
+        ['id' => 12, 'name' => 'Amelia', 'plugin_slugs' => ['ameliabooking/ameliabooking.php']],
+        ['id' => 13, 'name' => 'Bricks Builder', 'theme' => 'bricks'],
+        ['id' => 14, 'name' => 'ARForms', 'plugin_slugs' => ['arforms-form-builder/arforms-form-builder.php']],
+        ['id' => 15, 'name' => 'Custom form submissions'],
+        ['id' => 16, 'name' => 'Bit Form', 'plugin_slugs' => ['bit-form/bitforms.php']],
+        ['id' => 17, 'name' => 'Forminator', 'plugin_slugs' => ['forminator/forminator.php']],
+        ['id' => 18, 'name' => 'Hustle', 'plugin_slugs' => ['wordpress-popup/popover.php', 'hustle/opt-in.php']],
+        ['id' => 19, 'name' => 'Avada', 'plugin_slugs' => ['fusion-builder/fusion-builder.php', 'fusion-core/fusion-core.php']],
+        ['id' => 20, 'name' => 'WP Store Locator', 'plugin_slugs' => ['wp-store-locator/wp-store-locator.php']],
+        // [
+        //     'id'           => 21,
+        //     'name'         => 'Thrive Leads',
+        //     'plugin_slugs' => ['thrive-leads/thrive-leads.php'],
+        // ],
+        ['id' => 22, 'name' => 'SureForms', 'plugin_slugs' => ['sureforms/sureforms.php']],
+        ['id' => 23, 'name' => 'Kali Forms', 'plugin_slugs' => ['kali-forms/kali-forms.php']],
+    ];
     /**
-     * @var array An key(plugin_id) value(bool) pair of plugin IDs
+     * @var array A key/value pair (plugin_id/bool) of plugin IDs
      */
     private static $has_any_tracked_submissions_cache = [];
     private function __construct(int $id, string $title, int $plugin_id)
--- a/independent-analytics/IAWP/Form_Submissions/Submission_Listener.php
+++ b/independent-analytics/IAWP/Form_Submissions/Submission_Listener.php
@@ -273,6 +273,56 @@
             } catch (Throwable $e) {
             }
         }, 10, 0);
+        // Thrive
+        // add_action('thrive_core_lead_signup', function ($data, $user) {
+        //     try {
+        //         $submission = new Submission(
+        //             21,
+        //             intval($data['form_id']),
+        //             Security::string($data['form_name'])
+        //         );
+        //         $submission->record_submission();
+        //     } catch (Throwable $e) {
+        //
+        //     }
+        // }, 10, 2);
+        // Thrive
+        // add_action('tcb_api_form_submit', function ($data) {
+        //     try {
+        //         // This parsing of the object is copied from Thrive Leads own tve_leads_process_conversion function
+        //         $form_id   = ! empty($data['thrive_leads']['tl_data']['form_type_id']) ? $data['thrive_leads']['tl_data']['form_type_id'] : null;
+        //         $form_name = ! empty($data['thrive_leads']['tl_data']['form_name']) ? $data['thrive_leads']['tl_data']['form_name'] : null;
+        //
+        //         if (!is_numeric($form_id) || null === $form_id || null === $form_name) {
+        //             return;
+        //         }
+        //
+        //         $submission = new Submission(
+        //             21,
+        //             intval($form_id),
+        //             Security::string($form_name)
+        //         );
+        //         $submission->record_submission();
+        //     } catch (Throwable $e) {
+        //
+        //     }
+        // }, 10, 1);
+        // SureForms
+        add_action('srfm_form_submit', function ($data) {
+            try {
+                $submission = new IAWPForm_SubmissionsSubmission(22, intval($data['form_id']), Security::string($data['form_name']));
+                $submission->record_submission();
+            } catch (Throwable $e) {
+            }
+        }, 10, 1);
+        // Kali Forms
+        add_action('kaliforms_after_form_process_action', function ($data) {
+            try {
+                $submission = new IAWPForm_SubmissionsSubmission(23, intval($data['data']['formId']), Security::string(get_the_title($data['data']['formId'])));
+                $submission->record_submission();
+            } catch (Throwable $e) {
+            }
+        }, 10, 1);
         // // Template
         // add_action('iawp_some_form_callback', function () {
         //     try {
--- a/independent-analytics/IAWP/Geo_Database_Manager.php
+++ b/independent-analytics/IAWP/Geo_Database_Manager.php
@@ -10,11 +10,11 @@
 /** @internal */
 class Geo_Database_Manager
 {
-    // Updating the database? Read the Wiki page "Updating The Geo Database"
-    // https://github.com/andrewjmead/independent-analytics/wiki/Updating-The-Geo-Database
-    private $zip_download_url = 'https://assets.independentwp.com/iawp-geo-db-6.mmdb.zip';
-    private $raw_download_url = 'https://assets.independentwp.com/iawp-geo-db-6.mmdb';
-    private $database_checksum = '2213359f8d395c4f1a352007af9495ae';
+    // 🚨🚨 Updating the database? Follow the wiki: 🚨🚨
+    // https://github.com/andrewjmead/independent-analytics/wiki/Update-the-Geo-Database
+    private $zip_download_url = 'https://assets.independentwp.com/iawp-geo-db-7.mmdb.zip';
+    private $raw_download_url = 'https://assets.independentwp.com/iawp-geo-db-7.mmdb';
+    private $database_checksum = 'e26ab675eccee3de08e4cd2aceb5a217';
     public function download() : void
     {
         if (!$this->should_download()) {
--- a/independent-analytics/IAWP/Independent_Analytics.php
+++ b/independent-analytics/IAWP/Independent_Analytics.php
@@ -11,6 +11,8 @@
 use IAWPAJAXAJAX_Manager;
 use IAWPClick_TrackingClick_Processing_Job;
 use IAWPData_PruningPruner;
+use IAWPEcommerceEDD_Order;
+use IAWPEcommercePMPro_Order;
 use IAWPEcommerceSureCart_Event_Sync_Job;
 use IAWPEcommerceSureCart_Order;
 use IAWPEcommerceWooCommerce_Order;
@@ -33,6 +35,8 @@
     public $cron_manager;
     private $is_woocommerce_support_enabled;
     private $is_surecart_support_enabled;
+    private $is_edd_support_enabled;
+    private $is_pmpro_support_enabled;
     private $is_form_submission_support_enabled;
     // This is where we attach functions to WP hooks
     private function __construct()
@@ -49,6 +53,8 @@
             new IAWPTrack_Resource_Changes();
             Menu_Bar_Stats::register();
             WooCommerce_Order::register_hooks();
+            EDD_Order::register_hooks();
+            PMPro_Order::register_hooks();
             SureCart_Order::register_hooks();
         }
         IAWPCron_Job::register_custom_intervals();
@@ -68,14 +74,37 @@
         add_filter('plugin_action_links_independent-analytics/iawp.php', [$this, 'plugin_action_links']);
         add_action('init', [$this, 'polylang_translations']);
         add_action('init', [$this, 'load_textdomain']);
+        // Freemius adjustments
         IAWP_FS()->add_filter('pricing_url', [$this, 'change_freemius_pricing_url'], 10);
         IAWP_FS()->add_filter('show_deactivation_feedback_form', function () {
             return false;
         });
+        IAWP_FS()->override_i18n(['yee-haw' => __('Success', 'independent-analytics')]);
+        // Other hooks
         add_action('admin_init', [$this, 'maybe_delete_mu_plugin']);
         add_action('admin_body_class', [$this, 'add_body_class']);
         add_filter('sgs_whitelist_wp_content', [$this, 'whitelist_click_endpoint']);
         add_filter('cmplz_whitelisted_script_tags', [$this, 'whitelist_script_tag_for_complianz']);
+        add_filter('plugin_action_links_independent-analytics/iawp.php', [$this, 'add_upgrade_link_in_plugins_menu'], 999);
+        add_filter('plugin_row_meta', [$this, 'add_docs_link_in_plugins_menu'], 10, 2);
+    }
+    public function add_upgrade_link_in_plugins_menu($links)
+    {
+        if (IAWPSCOPEDiawp_is_pro()) {
+            return $links;
+        }
+        $upgrade_link = '<a target="_blank" style="color:#36B366;font-weight:700;"
+            href="https://independentwp.com/pricing/?utm_source=User+Dashboard&utm_medium=WP+Admin&utm_campaign=Plugin+Settings+Link"
+            >' . esc_html__('Upgrade to Pro', 'independent-analytics') . '</a>';
+        array_unshift($links, $upgrade_link);
+        return $links;
+    }
+    public function add_docs_link_in_plugins_menu($plugin_meta, $plugin_file)
+    {
+        if ($plugin_file == 'independent-analytics/iawp.php' || $plugin_file == 'independent-analytics-pro/iawp.php') {
+            $plugin_meta[] = '<a target="_blank" href="https://independentwp.com/knowledgebase/">' . esc_html__('Knowledge Base', 'independent-analytics') . '</a>';
+        }
+        return $plugin_meta;
     }
     public function add_body_class($classes)
     {
@@ -326,15 +355,23 @@
         }
         return $this->is_surecart_support_enabled;
     }
-    public function is_ecommerce_support_enabled() : bool
+    public function is_edd_support_enabled() : bool
     {
-        return $this->is_woocommerce_support_enabled() || $this->is_surecart_support_enabled();
+        if (!is_bool($this->is_edd_support_enabled)) {
+            $this->is_edd_support_enabled = $this->actually_check_if_edd_support_is_enabled();
+        }
+        return $this->is_edd_support_enabled;
     }
-    // This whitelists our plugin with the "Complianz" plugin
-    public function whitelist_script_tag_for_complianz($scripts)
+    public function is_pmpro_support_enabled() : bool
     {
-        $scripts[] = '/wp-json/iawp/search';
-        return $scripts;
+        if (!is_bool($this->is_pmpro_support_enabled)) {
+            $this->is_pmpro_support_enabled = $this->actually_check_if_pmpro_support_is_enabled();
+        }
+        return $this->is_pmpro_support_enabled;
+    }
+    public function is_ecommerce_support_enabled() : bool
+    {
+        return $this->is_woocommerce_support_enabled() || $this->is_surecart_support_enabled() || $this->is_edd_support_enabled() || $this->is_pmpro_support_enabled();
     }
     // This is for compatibility with the "Lock and Protect System Folders" setting in the Security Optimizer plugin
     public function whitelist_click_endpoint($whitelist)
@@ -345,6 +382,12 @@
         $whitelist[] = 'iawp-click-endpoint.php';
         return $whitelist;
     }
+    // This whitelists our plugin with the "Complianz" plugin
+    public function whitelist_script_tag_for_complianz($scripts)
+    {
+        $scripts[] = '/wp-json/iawp/search';
+        return $scripts;
+    }
     private function actually_check_if_woocommerce_support_is_enabled() : bool
     {
         global $wpdb;
@@ -372,8 +415,31 @@
         if (IAWPSCOPEDiawp_is_free()) {
             return false;
         }
+        if (IAWPCapability_Manager::can_only_view_authored_analytics()) {
+            return false;
+        }
         return is_plugin_active('surecart/surecart.php');
     }
+    private function actually_check_if_edd_support_is_enabled() : bool
+    {
+        if (IAWPSCOPEDiawp_is_free()) {
+            return false;
+        }
+        if (IAWPCapability_Manager::can_only_view_authored_analytics()) {
+            return false;
+        }
+        return is_plugin_active('easy-digital-downloads/easy-digital-downloads.php') || is_plugin_active('easy-digital-downloads-pro/easy-digital-downloads.php');
+    }
+    private function actually_check_if_pmpro_support_is_enabled() : bool
+    {
+        if (IAWPSCOPEDiawp_is_free()) {
+            return false;
+        }
+        if (IAWPCapability_Manager::can_only_view_authored_analytics()) {
+            return false;
+        }
+        return is_plugin_active('paid-memberships-pro-dev/paid-memberships-pro.php') || is_plugin_active('paid-memberships-pro/paid-memberships-pro.php');
+    }
     private function get_menu_icon()
     {
         if (is_null(IAWPEnv::get_page())) {
--- a/independent-analytics/IAWP/MainWP.php
+++ b/independent-analytics/IAWP/MainWP.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace IAWP;
+
+use IAWPDate_RangeRelative_Date_Range;
+use IAWPStatisticsIntervalsIntervals;
+use IAWPUtilsSecurity;
+/** @internal */
+class MainWP
+{
+    public static function initialize()
+    {
+        add_filter('mainwp_site_sync_others_data', function ($information, $data = []) {
+            return self::attach_analytics($information, $data);
+        }, 10, 2);
+    }
+    private static function attach_analytics(array $information, array $data) : array
+    {
+        $should_sync_analytics = array_key_exists('iawp_sync_analytics', $data) && true === $data['iawp_sync_analytics'];
+        if (!$should_sync_analytics) {
+            return $information;
+        }
+        try {
+            $table = new IAWPTablesTable_Pages();
+            $statistics_class = $table->group()->statistics_class();
+            $date_range = new Relative_Date_Range('LAST_THIRTY');
+            $chart_interval = Intervals::default_for($date_range->number_of_days());
+            $statistics = new $statistics_class($date_range, null, $chart_interval);
+            $views = $statistics->get_statistic('views');
+            $visitors = $statistics->get_statistic('visitors');
+            $labels = array_map(function ($data_point) use($statistics) {
+                return Security::json_encode($statistics->chart_interval()->get_label_for($data_point[0]));
+            }, $views->statistic_over_time());
+            $views_over_time = array_map(function ($data_point) {
+                return $data_point[1];
+            }, $views->statistic_over_time());
+            $visitors_over_time = array_map(function ($data_point) {
+                return $data_point[1];
+            }, $visitors->statistic_over_time());
+            $information['iawp_analytics'] = ['analytics_dashboard_url' => IAWPSCOPEDiawp_dashboard_url(), 'labels' => $labels, 'views_over_time' => $views_over_time, 'visitors_over_time' => $visitors_over_time, 'views' => $views->value(), 'visitors' => $visitors->value()];
+        } catch (Throwable $e) {
+            // Do nothing
+        }
+        return $information;
+    }
+}
--- a/independent-analytics/IAWP/Migrations/Migration_36.php
+++ b/independent-analytics/IAWP/Migrations/Migration_36.php
@@ -2,7 +2,6 @@

 namespace IAWPMigrations;

-use IAWPDatabase;
 use IAWPQuery;
 /** @internal */
 class Migration_36 extends IAWPMigrationsStep_Migration
--- a/independent-analytics/IAWP/Migrations/Migration_40.php
+++ b/independent-analytics/IAWP/Migrations/Migration_40.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace IAWPMigrations;
+
+/** @internal */
+class Migration_40 extends IAWPMigrationsStep_Migration
+{
+    /**
+     * @return int
+     */
+    protected function database_version() : int
+    {
+        return 40;
+    }
+    /**
+     * @return array
+     */
+    protected function queries() : array
+    {
+        return [$this->add_edd_to_orders_table()];
+    }
+    private function add_edd_to_orders_table() : string
+    {
+        return "n            ALTER TABLE {$this->tables::orders()} n                ADD COLUMN edd_order_id BIGINT(20) UNSIGNED AFTER surecart_order_status,n                ADD COLUMN edd_order_status VARCHAR(64) AFTER edd_order_id,n                ADD UNIQUE INDEX (edd_order_id);n        ";
+    }
+}
--- a/independent-analytics/IAWP/Migrations/Migration_41.php
+++ b/independent-analytics/IAWP/Migrations/Migration_41.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace IAWPMigrations;
+
+/** @internal */
+class Migration_41 extends IAWPMigrationsStep_Migration
+{
+    /**
+     * @return int
+     */
+    protected function database_version() : int
+    {
+        return 41;
+    }
+    /**
+     *

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2024-13362
# Blocks reflected XSS via url parameter in Freemius SDK
# Matches malicious JavaScript schemes in the freemius parameter
SecRule REQUEST_URI "@contains /wp-admin/" 
  "id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2024-13362 - Freemius Reflected XSS via url parameter',severity:'CRITICAL',tag:'CVE-2024-13362'"
  SecRule ARGS:url "@rx ^javascript:" 
    "t:none,t:urlDecode"

# Alternative rule for AJAX endpoints
SecRule REQUEST_URI "@contains admin-ajax.php" 
  "id:20261995,phase:2,deny,status:403,chain,msg:'CVE-2024-13362 - Freemius AJAX Reflected XSS',severity:'CRITICAL',tag:'CVE-2024-13362'"
  SecRule ARGS_POST:url "@rx ^javascript:" 
    "t:none,t:urlDecode"

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School