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

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

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

Analysis Overview

{
“analysis”: “Atomic Edge analysis of CVE-2024-13362:nThis is a reflected DOM-based Cross-Site Scripting vulnerability in the Freemius SDK bundled with the Message Filter for Contact Form 7 WordPress plugin (versions up to 2.10.1). The vulnerability allows unauthenticated attackers to inject arbitrary web scripts via the url parameter in the trial promotion notice. CVSS 6.1 (Medium).nnRoot cause: The vulnerability originates in the Freemius SDK file `freemius/includes/class-freemius.php`, specifically in the `_add_trial_notice()` method around line 24000. The vulnerable code concatenates user-supplied input (the `$trial_url` variable, which comes from the `url` parameter) directly into an HTML string without proper output escaping. The `apply_filters(‘trial_promotion_message’, …)` call passes unfiltered data into the sticky admin notice. The `$button` variable is constructed with `sprintf(‘…’, $trial_url)` where `$trial_url` contains user-controlled input from the URL query string. No sanitization or escaping is applied to the href attribute value before rendering.nnExploitation: An unauthenticated attacker crafts a malicious URL containing JavaScript payload in the `url` parameter, e.g., `?url=javascript:alert(document.cookie)`. When a logged-in WordPress administrator clicks this link, the admin notice displays the trial promotion with the attacker-controlled URL. The browser executes the `javascript:` scheme in the anchor tag’s href, leading to XSS. The attack is reflected because the injected script runs in the context of the admin page the victim visits.nnPatch analysis: The patch modifies the HTML structure in `freemius/includes/class-freemius.php` around line 24000. The old code: `$button = ‘ ‘;` is changed to: `$button = ‘

%s  ➜

‘;`. The new code removes the nested button element and changes the structure, but more importantly, the patch now applies the filter to a different content string: `$message_text = $this->apply_filters(‘trial_promotion_message’, “{$message} {$cc_string}”);` and then wraps it in a div container: `”

{$message_text}

{$button}

“`. This change does NOT escape the URL parameter itself. The vulnerability is not fully patched in the core Freemius SDK; the fix relies on the fact that Freemius 2.11.0 (included in this patch) no longer passes the button HTML through the filter. However, the `$trial_url` still comes from user input and is used in the href without escaping. The primary mitigation in this specific plugin update is the capability check change in `KMCFMessageFilter.php` line 177: `’capability’ => ‘manage_options’` (changed from `’read’`), which restricts access to admin menus to administrators only, but this does not fix the XSS in Freemius itself.nnImpact: Successful exploitation allows an unauthenticated attacker to execute arbitrary JavaScript in the context of the WordPress admin dashboard. This can lead to session hijacking, credential theft, administrative account creation (by forging requests), defacement, or malware injection. The attack requires the victim to click a crafted link while logged in, making it a reflected XSS with medium severity.”,
“poc_php”: null,
modsecurity_rule”: “# Atomic Edge WAF Rule – CVE-2024-13362n# Rule: Block reflected XSS via the trial_promotion_message filter in Freemius SDKn# Attack vector: Triggered when an authenticated admin visits a URL with a javascript: or data: URI in the ‘url’ parameter, which Freemius uses to construct the trial promotion notice.n# This rule checks for XSS payload patterns in the ‘url’ parameter on AJAX and admin requests that might trigger the Freemius notice.nSecRule REQUEST_URI “@rx /wp-admin/.*.php” “id:20240901,phase:2,deny,status:403,chain,msg:’CVE-2024-13362 Freemius trial XSS via url param’,severity:’CRITICAL’,tag:’CVE-2024-13362′”nSecRule ARGS:url “@rx (javascript:|data:|vbscript:)” “t:lowercase,t:removeNulls,chain”nSecRule ARGS:url “@rx [\”‘()]” “t:lowercase,t:urlDecodeUni””
}

Differential between vulnerable and patched code

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

Code Diff
--- a/cf7-message-filter/cf7-message-filter.php
+++ b/cf7-message-filter/cf7-message-filter.php
@@ -9,7 +9,7 @@
  * Plugin Name: Message Filter for Contact Form 7
  * Plugin URI: https://github.com/kofimokome/cf7-message-filter
  * Description: Filters messages submitted from contact form 7 if it has words or email marked as spam by the user
- * Version: 1.6.3.2
+ * Version: 1.6.3.3
  * Author: Kofi Mokome
  * Author URI: https://www.kofimokome.stream
  * License: GPL-2.0+
@@ -22,7 +22,7 @@
 namespace km_message_filter;

 use KMEnv;
-use WordPressTools;
+use WPTools;
 defined( 'ABSPATH' ) or die( 'Giving To Cesar What Belongs To Caesar' );
 if ( function_exists( 'kmcf7ms_fs' ) ) {
     kmcf7ms_fs()->set_basename( false, __FILE__ );
@@ -148,7 +148,7 @@
             delete_option( 'kmcfmf_weekly_stats' );
             delete_option( 'kmcfmf_weekend' );
         }
-        $wordpress_tools = new WordPressTools(__FILE__);
+        $wordpress_tools = new WPTools(__FILE__);
         $wordpress_tools->migration_manager->runMigrations();
         $message_filter = new KMCFMessageFilter();
         $message_filter->run();
@@ -171,7 +171,7 @@
     function KMCF7Uninstall() {
         global $wpdb;
         if ( get_option( 'kmcfmf_message_delete_data', 'off' ) == 'on' ) {
-            $instance = WordPressTools::getInstance( __FILE__ );
+            $instance = WPTools::getInstance( __FILE__ );
             $instance->migration_manager->dropAll();
             //query the wp options table and delete all options that start with kmcfmf_
             $pattern = 'kmcfmf_%';
--- a/cf7-message-filter/core/KMCFMessageFilter.php
+++ b/cf7-message-filter/core/KMCFMessageFilter.php
@@ -12,7 +12,7 @@

 	public function __construct() {
 		// do something here
-		$this->version  = '1.6.3.2';
+		$this->version  = '1.6.3.3';
 		$this->blocked  = get_option( "kmcfmf_messages_blocked_today_tmp", 0 );
 		self::$instance = $this;
 	}
@@ -174,7 +174,7 @@
 		$menu_page      = new KMMenuPage( array(
 			'page_title' => 'CF7 Form Filter',
 			'menu_title' => $menu_title,
-			'capability' => 'read',
+			'capability' => 'manage_options',
 			'menu_slug'  => 'kmcf7-message-filter',
 			'icon_url'   => 'dashicons-filter',
 			'position'   => 100,
--- a/cf7-message-filter/freemius/includes/class-freemius.php
+++ b/cf7-message-filter/freemius/includes/class-freemius.php
@@ -24000,13 +24000,15 @@

             // Start trial button.
             $button = ' ' . sprintf(
-                    '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s  ➜</button></a>',
+                    '<div><a class="button button-primary" href="%s">%s  ➜</a></div>',
                     $trial_url,
                     $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' )
                 );

+            $message_text = $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string}" );
+
             $this->_admin_notices->add_sticky(
-                $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ),
+                "<div class="fs-trial-message-container"><div>{$message_text}</div> {$button}</div>",
                 'trial_promotion',
                 '',
                 'promotion'
@@ -25476,7 +25478,7 @@
                 $img_dir = WP_FS__DIR_IMG;

                 // Locate the main assets folder.
-                if ( 1 < count( $fs_active_plugins->plugins ) ) {
+                if ( ! empty( $fs_active_plugins->plugins ) ) {
                     $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) );

                     foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
--- a/cf7-message-filter/freemius/includes/class-fs-plugin-updater.php
+++ b/cf7-message-filter/freemius/includes/class-fs-plugin-updater.php
@@ -542,24 +542,8 @@

             global $wp_current_filter;

-            $current_plugin_version = $this->_fs->get_plugin_version();
-
-            if ( ! empty( $wp_current_filter ) && 'upgrader_process_complete' === $wp_current_filter[0] ) {
-                if (
-                    is_null( $this->_update_details ) ||
-                    ( is_object( $this->_update_details ) && $this->_update_details->new_version !== $current_plugin_version )
-                ) {
-                    /**
-                     * After an update, clear the stored update details and reparse the plugin's main file in order to get
-                     * the updated version's information and prevent the previous update information from showing up on the
-                     * updates page.
-                     *
-                     * @author Leo Fajardo (@leorw)
-                     * @since 2.3.1
-                     */
-                    $this->_update_details  = null;
-                    $current_plugin_version = $this->_fs->get_plugin_version( true );
-                }
+            if ( ! empty( $wp_current_filter ) && in_array( 'upgrader_process_complete', $wp_current_filter ) ) {
+                return $transient_data;
             }

             if ( ! isset( $this->_update_details ) ) {
@@ -568,7 +552,7 @@
                     false,
                     fs_request_get_bool( 'force-check' ),
                     FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION,
-                    $current_plugin_version
+                    $this->_fs->get_plugin_version()
                 );

                 $this->_update_details = false;
--- a/cf7-message-filter/freemius/includes/entities/class-fs-plugin-plan.php
+++ b/cf7-message-filter/freemius/includes/entities/class-fs-plugin-plan.php
@@ -13,7 +13,6 @@
 	/**
 	 * Class FS_Plugin_Plan
 	 *
-	 * @property FS_Pricing[] $pricing
 	 */
 	class FS_Plugin_Plan extends FS_Entity {

--- a/cf7-message-filter/freemius/includes/entities/class-fs-site.php
+++ b/cf7-message-filter/freemius/includes/entities/class-fs-site.php
@@ -10,16 +10,16 @@
         exit;
     }

-    /**
-     * @property int $blog_id
-     */
-    #[AllowDynamicProperties]
     class FS_Site extends FS_Scope_Entity {
         /**
          * @var number
          */
         public $site_id;
         /**
+         * @var int
+         */
+        public $blog_id;
+        /**
          * @var number
          */
         public $plugin_id;
--- a/cf7-message-filter/freemius/includes/entities/class-fs-user.php
+++ b/cf7-message-filter/freemius/includes/entities/class-fs-user.php
@@ -48,6 +48,19 @@
 			parent::__construct( $user );
 		}

+		/**
+		 * This method removes the deprecated 'is_beta' property from the serialized data.
+		 * Should clean up the serialized data to avoid PHP 8.2 warning on next execution.
+		 *
+		 * @return void
+		 */
+		function __wakeup() {
+			if ( property_exists( $this, 'is_beta' ) ) {
+				// If we enter here, and we are running PHP 8.2, we already had the warning. But we sanitize data for next execution.
+				unset( $this->is_beta );
+			}
+		}
+
 		function get_name() {
 			return trim( ucfirst( trim( is_string( $this->first ) ? $this->first : '' ) ) . ' ' . ucfirst( trim( is_string( $this->last ) ? $this->last : '' ) ) );
 		}
--- a/cf7-message-filter/freemius/includes/managers/class-fs-admin-menu-manager.php
+++ b/cf7-message-filter/freemius/includes/managers/class-fs-admin-menu-manager.php
@@ -699,16 +699,36 @@
 				$menu = $this->find_main_submenu();
 			}

+			$menu_slug   = $menu['menu'][2];
 			$parent_slug = isset( $menu['parent_slug'] ) ?
-                $menu['parent_slug'] :
-                'admin.php';
+				$menu['parent_slug'] :
+				'admin.php';

-            return admin_url(
-                $parent_slug .
-                ( false === strpos( $parent_slug, '?' ) ? '?' : '&' ) .
-                'page=' .
-                $menu['menu'][2]
-            );
+			if ( fs_apply_filter( $this->_module_unique_affix, 'enable_cpt_advanced_menu_logic', false ) ) {
+				$parent_slug = 'admin.php';
+
+				/**
+				 * This line and the `if` block below it are based on the `menu_page_url()` function of WordPress.
+				 *
+				 * @author Leo Fajardo (@leorw)
+				 * @since 2.10.2
+				 */
+				global $_parent_pages;
+
+				if ( ! empty( $_parent_pages[ $menu_slug ] ) ) {
+					$_parent_slug = $_parent_pages[ $menu_slug ];
+					$parent_slug  = isset( $_parent_pages[ $_parent_slug ] ) ?
+						$parent_slug :
+						$menu['parent_slug'];
+				}
+			}
+
+			return admin_url(
+				$parent_slug .
+				( false === strpos( $parent_slug, '?' ) ? '?' : '&' ) .
+				'page=' .
+				$menu_slug
+			);
 		}

 		/**
--- a/cf7-message-filter/freemius/includes/managers/class-fs-admin-notice-manager.php
+++ b/cf7-message-filter/freemius/includes/managers/class-fs-admin-notice-manager.php
@@ -194,8 +194,14 @@
          * @since  1.0.7
          */
         static function _add_sticky_dismiss_javascript() {
+            $sticky_admin_notice_js_template_name = 'sticky-admin-notice-js.php';
+
+            if ( ! file_exists( fs_get_template_path( $sticky_admin_notice_js_template_name ) ) ) {
+                return;
+            }
+
             $params = array();
-            fs_require_once_template( 'sticky-admin-notice-js.php', $params );
+            fs_require_once_template( $sticky_admin_notice_js_template_name, $params );
         }

         private static $_added_sticky_javascript = false;
--- a/cf7-message-filter/freemius/start.php
+++ b/cf7-message-filter/freemius/start.php
@@ -15,7 +15,7 @@
 	 *
 	 * @var string
 	 */
-	$this_sdk_version = '2.10.1';
+	$this_sdk_version = '2.11.0';

 	#region SDK Selection Logic --------------------------------------------------------------------

--- a/cf7-message-filter/lib/requires.php
+++ b/cf7-message-filter/lib/requires.php
@@ -9,7 +9,7 @@
 	$plugin_path = plugin_dir_path( __FILE__ );

 	$files = [
-		$plugin_path . 'wordpress_tools/WordPressTools.php', //
+		$plugin_path . 'wordpress_tools/WPTools.php', //
 		$plugin_path . 'emoji_detector/Emoji.php', //
 	];

--- a/cf7-message-filter/lib/wordpress_tools/KMBuilder.php
+++ b/cf7-message-filter/lib/wordpress_tools/KMBuilder.php
@@ -182,8 +182,7 @@
 				$query .= $additions;
 				$data  = $this->getResults( $query );
 			}
-//			echo( $query );
-			// reset query variables;
+ 			// reset query variables;
 			$this->where        = '';
 			$this->orderBys     = [];
 			$this->groupBys     = [];
@@ -203,10 +202,11 @@
 		public function where( string $field, string $comparison, $value, $add_table_name = true ): KMBuilder {
 			$table_name = $add_table_name ? $this->table_name . '.' : '';
 			if ( strlen( $this->where ) == 0 ) {
-
+				$value = $this->escapeValue( $value );
 				if ( ! is_numeric( $value ) ) {
 					$value = "'" . $value . "'";
 				}
+
 				$this->where = " WHERE " . $table_name . $field . " " . $comparison . " " . $value;

 				return $this;
@@ -221,16 +221,48 @@
 		 */
 		public function andWhere( string $field, string $comparison, $value ): KMBuilder {
 			$table_name = $this->table_name;
+			$value      = $this->escapeValue( $value );
+
 			if ( ! is_numeric( $value ) ) {
 				$value = "'" . $value . "'";
 			}
+
 			$this->where .= " AND " . $table_name . '.' . $field . " " . $comparison . " " . $value;

+
 			return $this;
 		}

 		/**
 		 * @author kofimokome
+		 * @since 1.6.3.3
+		 */
+		private function escapeValue( $value ): string {
+			// Check if the value starts with %
+			$starts_with_percent = strpos( $value, '%' ) === 0;
+			// Check if the value ends with %
+			$ends_with_percent = strrpos( $value, '%' ) === ( strlen( $value ) - 1 );
+
+			// Remove % from the start and end of the value
+			$trimmed_value = trim( $value, '%' );
+
+			// Escape the trimmed value
+			$escaped_value = esc_sql( $trimmed_value );
+
+			// Add % back to the start and/or end if they were originally present
+			if ( $starts_with_percent ) {
+				$escaped_value = '%' . $escaped_value;
+			}
+			if ( $ends_with_percent ) {
+				$escaped_value .= '%';
+			}
+
+			return $escaped_value;
+		}
+
+
+		/**
+		 * @author kofimokome
 		 * @since 1.0.0
 		 */
 		public function getResults( $query ) {
@@ -300,11 +332,14 @@
 		 */
 		public function orWhere( string $field, string $comparison, $value ): KMBuilder {
 			$table_name = $this->table_name;
+			$value      = $this->escapeValue( $value );
 			if ( ! is_numeric( $value ) ) {
 				$value = "'" . $value . "'";
 			}
+
 			$this->where .= " OR " . $table_name . '.' . $field . " " . $comparison . " " . $value;

+
 			return $this;
 		}

@@ -313,9 +348,12 @@
 		 * @since 1.0.0
 		 */
 		public function whereJoin( string $field, string $comparison, $value, $table ): KMBuilder {
+			$value = $this->escapeValue( $value );
+
 			if ( ! is_numeric( $value ) ) {
 				$value = "'" . $value . "'";
 			}
+
 			$this->where = " WHERE " . $table . '.' . $field . " " . $comparison . " " . $value;

 			return $this;
@@ -326,9 +364,12 @@
 		 * @since 1.0.0
 		 */
 		public function andWhereJoin( string $field, string $comparison, $value, $table ): KMBuilder {
+			$value = $this->escapeValue( $value );
+
 			if ( ! is_numeric( $value ) ) {
 				$value = "'" . $value . "'";
 			}
+
 			$this->where .= " AND " . $table . '.' . $field . " " . $comparison . " " . $value;

 			return $this;
@@ -339,11 +380,14 @@
 		 * @since 1.0.0
 		 */
 		public function orWhereJoin( string $field, string $comparison, $value, $table ): KMBuilder {
+			$value = $this->escapeValue( $value );
 			if ( ! is_numeric( $value ) ) {
 				$value = "'" . $value . "'";
 			}
+
 			$this->where .= " OR " . $table . '.' . $field . " " . $comparison . " " . $value;

+
 			return $this;
 		}

@@ -450,7 +494,7 @@
 					$fields['created_at'] = gmdate( "Y-m-d H:i" );
 					$fields['updated_at'] = gmdate( "Y-m-d H:i" );
 				}
-				$fields['id'] = NULL;
+				$fields['id'] = null;
 				$result       = $wpdb->insert( $table_name, $fields );
 			} else { // we are updating
 				if ( $this->model->hasTimeStamps() ) {
--- a/cf7-message-filter/lib/wordpress_tools/WPTools.php
+++ b/cf7-message-filter/lib/wordpress_tools/WPTools.php
@@ -0,0 +1,116 @@
+<?php
+
+/**
+ * @author kofimokome
+ */
+
+require_once 'KMEnv.php';
+require_once 'KMRouteManager.php';
+require_once 'KMMigrationManager.php';
+require_once 'KMBlueprint.php';
+require_once 'KMBuilder.php';
+require_once 'KMMenuPage.php';
+require_once 'KMSubMenuPage.php';
+require_once 'KMSetting.php';
+require_once 'KMColumn.php';
+require_once 'KMModel.php';
+require_once 'KMMigration.php';
+require_once 'KMRoute.php';
+require_once 'KMValidator.php';
+require_once 'lib/plural/Plural.php';
+
+
+if ( ! class_exists('WPTools') ) {
+	class WPTools {
+		public $env;
+		public $route_manager;
+		public $migration_manager;
+		private $plugin_basename;
+		private $context;
+		private static $instances = [];
+
+
+		public function __construct( string $context ) {
+			$this->env                                                    = ( new KMEnv( $context ) )->getEnv();
+			$this->route_manager                                          = new KMRouteManager( $context );
+			$this->plugin_basename                                        = plugin_basename( $context );
+			$this->context                                                = $context;
+			$this->migration_manager                                      = new KMMigrationManager( $this->getPluginDir(), $context );
+			self::$instances[ explode( '/', $this->plugin_basename )[0] ] = $this;
+
+		}
+
+		/**
+		 * @author kofimokome
+		 */
+		public static function getInstance( string $context ): WPTools {
+			$plugin_basename = plugin_basename( $context );
+			$plugin          = explode( '/', $plugin_basename )[0];
+
+			if ( ! isset( self::$instances[ $plugin ] ) ) {
+				throw new Exception( 'WPTools instance not found' );
+			}
+
+			return self::$instances[ $plugin ];
+		}
+
+		/**
+		 * @author kofimokome
+		 */
+		public function routes(): KMRoute {
+			$route = new KMRoute( $this->route_manager );
+
+			return $route;
+		}
+
+		/**
+		 * @author kofimokome
+		 */
+		public function renderView( string $view, $echo = true ) {
+			return $this->route_manager->renderView( $view, $echo );
+		}
+
+		/**
+		 * @author kofimokome
+		 */
+		public function viewPath( string $view ) {
+			return $this->route_manager->viewPath( $view );
+		}
+
+		/**
+		 * @author kofimokome
+		 */
+		public function route( string $name, array $params = [] ): string {
+			return $this->route_manager->route( $name, $params );
+		}
+
+
+		/**
+		 * @throws Exception
+		 * @author kofimokome
+		 */
+		public function getPluginDir() {
+
+			$chars = explode( '/', $this->plugin_basename );
+			if ( sizeof( $chars ) > 0 ) {
+				$plugin_basename = $chars[0];
+
+				return WP_PLUGIN_DIR . '/' . $plugin_basename;
+			}
+
+			throw new Exception( 'Could not get plugin directory' );
+		}
+
+		/**
+		 * @author kofimokome
+		 */
+		public function getPluginURL(): string {
+			return rtrim( plugin_dir_url( $this->context ), '/' );
+		}
+
+		public function env() {
+			return ( new KMEnv( $this->context ) )->getEnv();
+		}
+	}
+
+}
--- a/cf7-message-filter/lib/wordpress_tools/WordPressTools.php
+++ b/cf7-message-filter/lib/wordpress_tools/WordPressTools.php
@@ -1,116 +0,0 @@
-<?php
-
-/**
- * @author kofimokome
- */
-
-require_once 'KMEnv.php';
-require_once 'KMRouteManager.php';
-require_once 'KMMigrationManager.php';
-require_once 'KMBlueprint.php';
-require_once 'KMBuilder.php';
-require_once 'KMMenuPage.php';
-require_once 'KMSubMenuPage.php';
-require_once 'KMSetting.php';
-require_once 'KMColumn.php';
-require_once 'KMModel.php';
-require_once 'KMMigration.php';
-require_once 'KMRoute.php';
-require_once 'KMValidator.php';
-require_once 'lib/plural/Plural.php';
-
-
-if ( ! class_exists( 'WordPressTools' ) ) {
-	class WordPressTools {
-		public $env;
-		public $route_manager;
-		public $migration_manager;
-		private $plugin_basename;
-		private $context;
-		private static $instances = [];
-
-
-		public function __construct( string $context ) {
-			$this->env                                                    = ( new KMEnv( $context ) )->getEnv();
-			$this->route_manager                                          = new KMRouteManager( $context );
-			$this->plugin_basename                                        = plugin_basename( $context );
-			$this->context                                                = $context;
-			$this->migration_manager                                      = new KMMigrationManager( $this->getPluginDir(), $context );
-			self::$instances[ explode( '/', $this->plugin_basename )[0] ] = $this;
-
-		}
-
-		/**
-		 * @author kofimokome
-		 */
-		public static function getInstance( string $context ): WordPressTools {
-			$plugin_basename = plugin_basename( $context );
-			$plugin          = explode( '/', $plugin_basename )[0];
-
-			if ( ! isset( self::$instances[ $plugin ] ) ) {
-				throw new Exception( 'WordPressTools instance not found' );
-			}
-
-			return self::$instances[ $plugin ];
-		}
-
-		/**
-		 * @author kofimokome
-		 */
-		public function routes(): KMRoute {
-			$route = new KMRoute( $this->route_manager );
-
-			return $route;
-		}
-
-		/**
-		 * @author kofimokome
-		 */
-		public function renderView( string $view, $echo = true ) {
-			return $this->route_manager->renderView( $view, $echo );
-		}
-
-		/**
-		 * @author kofimokome
-		 */
-		public function viewPath( string $view ) {
-			return $this->route_manager->viewPath( $view );
-		}
-
-		/**
-		 * @author kofimokome
-		 */
-		public function route( string $name, array $params = [] ): string {
-			return $this->route_manager->route( $name, $params );
-		}
-
-
-		/**
-		 * @throws Exception
-		 * @author kofimokome
-		 */
-		public function getPluginDir() {
-
-			$chars = explode( '/', $this->plugin_basename );
-			if ( sizeof( $chars ) > 0 ) {
-				$plugin_basename = $chars[0];
-
-				return WP_PLUGIN_DIR . '/' . $plugin_basename;
-			}
-
-			throw new Exception( 'Could not get plugin directory' );
-		}
-
-		/**
-		 * @author kofimokome
-		 */
-		public function getPluginURL(): string {
-			return rtrim( plugin_dir_url( $this->context ), '/' );
-		}
-
-		public function env() {
-			return ( new KMEnv( $this->context ) )->getEnv();
-		}
-	}
-
-}
--- a/cf7-message-filter/modules/dashboard/DashboardModule.php
+++ b/cf7-message-filter/modules/dashboard/DashboardModule.php
@@ -3,7 +3,7 @@
 namespace km_message_filter;

 use KMSubMenuPage;
-use WordPressTools;
+use WPTools;

 class DashboardModule extends Module {
 	private $blocked;
@@ -48,7 +48,7 @@
 	 * Displays content on dashboard sub menu page
 	 */
 	function dashboardPageContent() {
-		$instance = WordPressTools::getInstance( __FILE__ );
+		$instance = WPTools::getInstance( __FILE__ );
 		$instance->renderView( 'dashboard.index' );
 	}

--- a/cf7-message-filter/modules/messages/MessagesModule.php
+++ b/cf7-message-filter/modules/messages/MessagesModule.php
@@ -4,9 +4,9 @@

 use KMSubMenuPage;
 use KMValidator;
-use WordPressTools;
 use WPCF7_ContactForm;
 use WPCF7_Submission;
+use WPTools;

 class MessagesModule extends Module {
 	private static $instance;
@@ -18,7 +18,7 @@
 		$this->transferOldData();
 //		$this->module = 'packages';
 		self::$instance = $this;
-		$this->wp_tools = WordPressTools::getInstance( __FILE__ );
+		$this->wp_tools = WPTools::getInstance( __FILE__ );
 	}

 	/**
@@ -203,6 +203,9 @@
 	 */
 	public function serverMessages() {
 		try {
+			if ( ! current_user_can( 'manage_options' ) ) {
+				throw new Exception( __( 'You do not have permission to perform this action', KMCFMF_TEXT_DOMAIN ) );
+			}
 			$nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';
 			if ( ! wp_verify_nonce( $nonce, 'kmcfmf_can_get_blocked_messages' ) ) {
 				throw new Exception( __( 'Invalid nonce', KMCFMF_TEXT_DOMAIN ) );
@@ -213,8 +216,8 @@
 			$draw             = isset( $_REQUEST['draw'] ) ? intval( sanitize_text_field( wp_unslash( $_REQUEST['draw'] ) ) ) : '';
 			$length           = isset( $_REQUEST['length'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['length'] ) ) : '';
 			$start            = isset( $_REQUEST['start'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['start'] ) ) : '';
-			$search           = isset( $_REQUEST['search'] ) ? rest_sanitize_array( wp_unslash( $_REQUEST['search'] ) ) : '';
-			$search_value     = sanitize_text_field( wp_unslash( $search['value'] ) );
+			$search           = isset( $_REQUEST['search'] ) ? rest_sanitize_array( wp_unslash( $_REQUEST['search'] ) ) : [];
+			$search_value     = sanitize_text_field( wp_unslash( $search[0] ) );
 			$search_value     = trim( $search_value );
 			$current_page     = ( $start / $length ) + 1;
 			$results          = Message::where( 'message', 'LIKE', "%{$search_value}%" )->orderBy( 'id', 'desc' )->paginate( $length, $current_page );
@@ -232,7 +235,7 @@
 			$messages = array();

 			// todo: Investigate why this function returns two different results for some contact forms on the frontend and here
-			$rows = $this->getRows2( $form_id, $contact_form );
+			$rows = $this->getColumns2( $form_id, $contact_form );

 			foreach ( $results as $result ) {
 				$decoded_message = json_decode( $result->message );
@@ -280,7 +283,7 @@
 	 * New version of getRows()
 	 * @since 1.4.0
 	 */
-	public function getRows2( $form_id, $contact_form ) {
+	public function getColumns2( $form_id, $contact_form ) {
 		$rows = array();
 		switch ( $contact_form ) {
 			case 'cf7':
@@ -493,7 +496,7 @@
 								$result       = $submission->get_result();
 //					$contact_form->submit();
 								if ( $result['status'] != 'mail_sent' ) {
-									wp_send_json_error( __( $result, KMCFMF_TEXT_DOMAIN ), 400 );
+									wp_send_json_error( $result, KMCFMF_TEXT_DOMAIN, 400 );
 								}
 								$message_id = intval( $message_id );
 								$message    = Message::find( $message_id );
@@ -550,6 +553,75 @@
 	}

 	/**
+	 * @since v1.6.3.3
+	 * @author: kofimokome
+	 */
+	public function downloadCSV() {
+		$validator = KMValidator::make(
+			array(
+				'form_id'      => 'required',
+				'contact_form' => 'required',
+				'_wpnonce'     => 'required'
+			),
+			$_REQUEST
+		);
+		if ( current_user_can( 'manage_options' ) ) {
+			if ( $validated_data = $validator->validate() ) {
+				$nonce = sanitize_text_field( wp_unslash( $validated_data['_wpnonce'] ) );
+				if ( wp_verify_nonce( $nonce, 'kmcfmf_can_download_csv' ) ) {
+
+					$form_id      = sanitize_text_field( $validated_data['form_id'] );
+					$contact_form = sanitize_text_field( $validated_data['contact_form'] );
+					$columns      = $this->getColumns2( $form_id, $contact_form );
+					$columns[]    = 'date-blocked';
+					$messages     = Message::where( 'contact_form', '=', $contact_form );
+
+					if ( $form_id != 'all' ) {
+						$messages = $messages->andWhere( 'form_id', '=', $form_id );
+					}
+
+					$messages = $messages->orderBy( 'id', 'desc' )->get();
+
+					$filename = 'blocked_messages_' . date( 'Y-m-d' ) . '.csv';
+					$fp       = fopen( 'php://output', 'w' );
+					header( 'Content-type: application/csv' );
+					header( 'Content-Disposition: attachment; filename=' . $filename );
+					fputcsv( $fp, $columns );
+					foreach ( $messages as $message ) {
+						$decoded_message = json_decode( $message->message );
+
+						$row = [];
+						if ( $form_id == 'all' ) {
+							$row[] = $this->getFormName( $message->form_id, $message->contact_form );
+						}
+						foreach ( $columns as $column ) {
+							if ( $column == 'date-blocked' ) {
+								$row[] = $message->created_at;
+							} else {
+								if ( property_exists( $decoded_message, $column ) ) {
+									$row[] = esc_html( self::decodeUnicodeVars( $decoded_message->$column ) );
+								} else {
+									$row[] = " ";
+								}
+							}
+						}
+						fputcsv( $fp, $row );
+					}
+					fclose( $fp );
+					exit();
+				} else {
+					wp_send_json_error( __( "Invalid nonce", KMCFMF_TEXT_DOMAIN ), 400 );
+				}
+			} else {
+				wp_send_json_error( __( "You do not have permission to perform this action", KMCFMF_TEXT_DOMAIN ), 400 );
+			}
+		} else {
+			throw new Exception( __( 'You do not have permission to perform this action', KMCFMF_TEXT_DOMAIN ) );
+		}
+		wp_die();
+	}
+
+	/**
 	 * @since v1.3.4
 	 */
 	protected
@@ -562,6 +634,7 @@
 	protected
 	function addActions() {
 		parent::addActions();
+		add_action( 'wp_ajax_kmcf7_download_csv', [ $this, 'downloadCSV' ] );
 		add_action( 'wp_ajax_kmcf7_messages', [ $this, 'serverMessages' ] );
 		add_action( 'wp_ajax_kmcf7_delete_message', [ $this, 'deleteMessage' ] );
 		add_action( 'wp_ajax_kmcf7_resubmit_message', [ $this, 'resubmitMessage' ] );
--- a/cf7-message-filter/modules/settings/SettingsModule.php
+++ b/cf7-message-filter/modules/settings/SettingsModule.php
@@ -4,7 +4,7 @@

 use KMSetting;
 use KMSubMenuPage;
-use WordPressTools;
+use WPTools;

 class SettingsModule extends Module {
 	private $is_free;
@@ -15,7 +15,7 @@
 		$this->is_free = ( ! kmcf7ms_fs()->is_premium() || ! kmcf7ms_fs()->is_plan_or_trial( 'pro' ) );
 		$this->addSettings();
 		$this->checkWildcardInSettingFields();
-		$this->wp_tools = WordPressTools::getInstance( __FILE__ );
+		$this->wp_tools = WPTools::getInstance( __FILE__ );

 		$is_using_old_tag_ui = get_option( 'kmcfmf_use_old_tag_ui', 'deleted' );

--- a/cf7-message-filter/views/messages/list.php
+++ b/cf7-message-filter/views/messages/list.php
@@ -43,7 +43,7 @@
     </form>
 </div>-->
 <?php
-$rows = MessagesModule::getInstance()->getRows2( $form_id, $selected_contact_form );
+$form_columns = MessagesModule::getInstance()->getColumns2( $form_id, $selected_contact_form );

 ?>
 <form action="" class="form-inline mb-4 mt-4">
@@ -72,6 +72,7 @@
     <div class="alert alert-info">
 		<?php _e( "Hint: Press and hold <kbd>CMD</kbd> or <kbd>CRTL</kbd> while clicking on any cell to select it", KMCFMF_TEXT_DOMAIN ) ?>
     </div>
+
     <button class="btn btn-danger btn-sm km-delete-btn" style="display: none" onclick="showDeleteModal()">
 		<?php _e( "Delete selected", KMCFMF_TEXT_DOMAIN ) ?>
     </button>
@@ -80,24 +81,28 @@
         </button>-->
 </div>
 <div class="mb-3">
-    <b><?php _e( "Visible Columns", KMCFMF_TEXT_DOMAIN ) ?>: <a href="#" id="toggle-visible-columns-container">Show/Hide</a>
+    <b><?php _e( "Visible Columns", KMCFMF_TEXT_DOMAIN ) ?>: <a href="#"
+                                                                id="toggle-visible-columns-container">Show/Hide</a>
         <div id="visible-columns-container" class="mt-2">
             <input id="input-ID" name="ID" type="checkbox" value="2" class="table-column"
                    checked/> <span class="mr-2">ID</span>
-			<?php foreach ( $rows as $index => $row ):if ( strlen( trim( $row ) ) > 0 ): ?>
-                <input id="input-<?php echo $row ?>" name="<?php echo $row ?>" type="checkbox"
+			<?php foreach ( $form_columns as $index => $form_column ):if ( strlen( trim( $form_column ) ) > 0 ): ?>
+                <input id="input-<?php echo $form_column ?>" name="<?php echo $form_column ?>" type="checkbox"
                        value="<?php echo $index + 3 ?>" class="table-column"
-                       checked/> <span class="mr-2"> <?php echo $row ?></span>
+                       checked/> <span class="mr-2"> <?php echo $form_column ?></span>
 			<?php endif; endforeach; ?>
         </div>
 </div>
-<table id="km-table" class="kmcfmf_table table table-striped" style="overflow-x: scroll">
+<button class="btn btn-primary mb-3" onclick="showDownloadModal()">
+	<?php _e( "Download CSV", KMCFMF_TEXT_DOMAIN ) ?>
+</button>
+<table id="km-table" class="kmcfmf_table table table-striped" style="overflow-x: scroll;">
     <thead>
     <tr>
         <th></th>
         <th><?php _e( "Actions", KMCFMF_TEXT_DOMAIN ) ?></th>
         <th><b>ID</b></th>
-		<?php foreach ( $rows as $row ): ?>
+		<?php foreach ( $form_columns as $row ): ?>
             <th>
                 <b><?php echo $row ?></b>
             </th>
@@ -132,8 +137,11 @@
     const GET_MESSAGES_NONCE = "<?php echo wp_create_nonce( 'kmcfmf_can_get_blocked_messages' )?>";
     const DELETE_MESSAGE_NONCE = "<?php echo wp_create_nonce( 'kmcfmf_can_delete_messages' )?>";
     const RESUBMIT_MESSAGE_NONCE = "<?php echo wp_create_nonce( 'kmcfmf_can_resubmit_messages' )?>";
+    const DOWNLOAD_MESSAGE_NONCE = "<?php echo wp_create_nonce( 'kmcfmf_can_download_csv' )?>";
     const forms = <?php echo wp_json_encode( $forms )?>;
     const all_registered_form_placeholder = "<?php _e( "All Registered Forms", KMCFMF_TEXT_DOMAIN )?>"
+    const selected_contact_form = '<?php echo $selected_contact_form?>'
+    const form_id = '<?php echo $form_id?>'
     console.log(forms['cf7']);
     jQuery(function ($) {
         $(document).ready(function () {
@@ -186,10 +194,10 @@
                     }],
                     buttons: [
                         // 'colvis',
-                        {
+                       /* {
                             extend: 'csv',
                             text: 'Download CSV'
-                        },
+                        },*/
                     ],
                     lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]],
                     select: true
@@ -380,6 +388,65 @@
             }
         })
     }
+
+    function showDownloadModal() {
+
+        let formData = new FormData();
+        formData.append("action", 'kmcf7_download_csv');
+        formData.append("form_id", form_id);
+        formData.append("contact_form", selected_contact_form);
+        bootstrapSwal().fire({
+            title: 'Download CSV',
+            text: '<?php _e( "CSV Download could take a long time depending on the number of blocked messages", KMCFMF_TEXT_DOMAIN ) ?>',
+            icon: 'info',
+            showCancelButton: true,
+            confirmButtonText: '<?php _e( "Yes, download", KMCFMF_TEXT_DOMAIN )?>',
+            showLoaderOnConfirm: true,
+            preConfirm: (login) => {
+                return fetch("<?php echo $ajax_url?>" + "?_wpnonce=" + DOWNLOAD_MESSAGE_NONCE, {
+                    method: 'POST',
+                    body: formData
+                })
+                    .then(async response => {
+                        if (!response.ok) {
+                            const e = await response.text();
+                            let message = "Something went wrong";
+                            try {
+                                const response_json = JSON.parse(e)
+                                if (response_json.data)
+                                    message = response_json.data.message ?? response_json.data.toString()
+                            } catch (e) {
+                                // Silence is golden
+                            }
+                            throw new Error(message)
+                        } else
+                            return response.blob()
+                    })
+                    .catch(error => {
+                        Swal.showValidationMessage(
+                            `Request failed: ${error}`
+                        )
+                    })
+            },
+            allowOutsideClick: () => !Swal.isLoading()
+        }).then((result) => {
+            if (result.isConfirmed && result.value) {
+                const url = window.URL.createObjectURL(result.value);
+                const a = document.createElement('a');
+                a.style.display = 'none';
+                a.href = url;
+                a.download = 'blocked_messages.csv';
+                document.body.appendChild(a);
+                a.click();
+                window.URL.revokeObjectURL(url);
+                Swal.fire({
+                    title: `Download CSV`,
+                    icon: 'success',
+                    text: '<?php  _e( "CSV generated successfully", KMCFMF_TEXT_DOMAIN )?>',
+                })
+            }
+        })
+    }


 </script>
--- a/cf7-message-filter/views/messages/message.php
+++ b/cf7-message-filter/views/messages/message.php
@@ -23,7 +23,7 @@
 	$contact_form    = $message_object->contact_form;
 	$message         = json_decode( $message_object->message );
 	$messages_module = MessagesModule::getInstance();
-	$rows            = $messages_module->getRows2( $form_id, $contact_form );
+	$rows            = $messages_module->getColumns2( $form_id, $contact_form );
 	?>
     <table class="kmcfmf_table table table-striped" style="overflow-x: scroll">
         <thead>
--- a/cf7-message-filter/views/settings/debug.php
+++ b/cf7-message-filter/views/settings/debug.php
@@ -23,6 +23,7 @@
 	'is_contact_form_7_filter_enabled' => get_option( 'kmcfmf_enable_contact_form_7_toggle' ) == 'on' ? 'Yes' : "No",
 	'is_spam_filter_enabled'           => get_option( 'kmcfmf_email_filter_toggle' ) == 'on' ? 'Yes' : "No",
 	'is_message_filter_enabled'        => get_option( 'kmcfmf_message_filter_toggle' ) == 'on' ? 'Yes' : "No",
+	'is_email_filter_enabled'        => get_option( 'kmcfmf_email_filter_toggle' ) == 'on' ? 'Yes' : "No",
 	'is_wp_forms_filter_enabled'       => get_option( 'kmcfmf_enable_wp_forms_toggle' ) == 'on' ? 'Yes' : "No",
 	'is_sync_allowed'                  => $can_sync ? 'Yes' : 'No',

--- a/cf7-message-filter/views/settings/settings.php
+++ b/cf7-message-filter/views/settings/settings.php
@@ -2,7 +2,7 @@

 namespace km_message_filter;

-use WordPressTools;
+use WPTools;
 $ajax_url = admin_url( "admin-ajax.php" );
 $words = get_option( 'kmcfmf_restricted_words', '' );
 $words = sizeof( explode( ',', $words ) );
@@ -213,7 +213,7 @@
          style="display:none; position:absolute; z-index: 9; left:0;top:0; width: 100%; height: 100%; align-content: center; align-items: center; justify-content: center; background: rgba(0,0,0,0.2)">
         <div style="background: white; width: 500px; height:500px; overflow-y:auto; position: relative; padding-left: 10px; padding-right: 10px;">
 			<?php
-WordPressTools::getInstance( __FILE__ )->renderView( 'settings.filters', true );
+WPTools::getInstance( __FILE__ )->renderView( 'settings.filters', true );
 ?>
         </div>
     </div>

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