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

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

Plugin ultimeter
Severity Medium (CVSS 6.1)
CWE 79
Vulnerable Version 3.0.5
Patched Version 3.0.7
Disclosed April 29, 2026

Analysis Overview

Atomic Edge analysis of CVE-2024-13362: This is a reflected DOM-based Cross-Site Scripting (XSS) vulnerability in the Freemius SDK up to version 2.10.1, which is embedded in many WordPress plugins and themes. An unauthenticated attacker can inject arbitrary scripts via the `url` parameter. The vulnerability carries a CVSS score of 6.1 (Medium).

Root Cause: The vulnerability lies in how the Freemius SDK constructs trial promotion messages before outputting them. In `class-freemius.php`, around line 23927, the `trial_promotion_message` filter is applied to a string that includes the `$trial_url` variable. This URL is constructed using user-controlled `url` parameter values. The patched diff shows the code restructuring the promotional message by separating the button and message text, and wrapping them in `

` elements. However, the core issue is that the URL from the `url` parameter is not sanitized or escaped before being used in an anchor (``) tag href attribute. The old code used `sprintf(‘…’, $trial_url)`, directly embedding the unsanitized URL into the HTML. An attacker can inject a `javascript:` URI or an event handler XSS vector via the `url` parameter.

Exploitation: The attack vector is a crafted link that tricks an authenticated or unauthenticated user into clicking. The Freemius SDK uses the `url` parameter from the request (likely from a GET parameter in the WordPress admin area or during a trial prompt redirect). An attacker crafts a URL like `https://victim-site.com/wp-admin/admin.php?page=freemius-trial&url=javascript:alert(document.domain)`. When the victim clicks the trial promotion link, the unsanitized URL is placed into an `` tag’s `href`, executing the injected JavaScript in the context of the victim’s browser.

Patch Analysis: The patch, visible in the diff, does not directly modify the input sanitization of the `url` parameter itself. Instead, it restructures the HTML output for the trial promotion message. The old code concatenated the button directly into the message string: `”{$message} {$cc_string} {$button}”`. The new code splits this into separate `

` elements: `”

{$message_text}

{$button}

“`. While this structural change improves HTML formatting, the fix for the XSS must be applied elsewhere (likely in the `apply_filters` call or in how `$trial_url` is generated). The patch indicates that the proper escaping is expected to happen via WordPress’s built-in functions or by the theme/plugin implementing the filter. The vulnerability persists if the `url` parameter is not sanitized before use.

Impact: Successful exploitation allows an unauthenticated attacker to execute arbitrary JavaScript in the context of the victim’s browser. This can lead to session hijacking, phishing, or defacement of the affected WordPress admin pages. Since the vulnerability is reflected and requires user interaction (clicking a link), the impact is limited to targeted attacks, but the lack of authentication makes it easily exploitable via social engineering.

Differential between vulnerable and patched code

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

Code Diff
--- a/ultimeter/freemius/includes/class-freemius.php
+++ b/ultimeter/freemius/includes/class-freemius.php
@@ -110,6 +110,12 @@
         private $_enable_anonymous = true;

         /**
+         * @since 2.9.1
+         * @var string|null Hints the SDK whether the plugin supports parallel activation mode, preventing the auto-deactivation of the free version when the premium version is activated, and vice versa.
+         */
+        private $_premium_plugin_basename_from_parallel_activation;
+
+        /**
          * @since 1.1.7.5
          * @var bool Hints the SDK if plugin should run in anonymous mode (only adds feedback form).
          */
@@ -1651,6 +1657,31 @@
                     );
                 }
             }
+
+            if (
+                $this->is_user_in_admin() &&
+                $this->is_parallel_activation() &&
+                $this->_premium_plugin_basename !== $this->_premium_plugin_basename_from_parallel_activation
+            ) {
+                $this->_premium_plugin_basename = $this->_premium_plugin_basename_from_parallel_activation;
+
+                register_activation_hook(
+                    dirname( $this->_plugin_dir_path ) . '/' . $this->_premium_plugin_basename,
+                    array( &$this, '_activate_plugin_event_hook' )
+                );
+            }
+        }
+
+        /**
+         * Determines if a plugin is running in parallel activation mode.
+         *
+         * @author Leo Fajardo (@leorw)
+         * @since 2.9.1
+         *
+         * @return bool
+         */
+        private function is_parallel_activation() {
+            return ! empty( $this->_premium_plugin_basename_from_parallel_activation );
         }

         /**
@@ -5155,11 +5186,35 @@
                 $this->_plugin :
                 new FS_Plugin();

+            $is_premium     = $this->get_bool_option( $plugin_info, 'is_premium', true );
             $premium_suffix = $this->get_option( $plugin_info, 'premium_suffix', '(Premium)' );

+            $module_type = $this->get_option( $plugin_info, 'type', $this->_module_type );
+
+            $parallel_activation = $this->get_option( $plugin_info, 'parallel_activation' );
+
+            if (
+                ! $is_premium &&
+                is_array( $parallel_activation ) &&
+                ( WP_FS__MODULE_TYPE_PLUGIN === $module_type ) &&
+                $this->get_bool_option( $parallel_activation, 'enabled' )
+            ) {
+                $premium_basename = $this->get_option( $parallel_activation, 'premium_version_basename' );
+
+                if ( empty( $premium_basename ) ) {
+                    throw new Exception('You need to specify the premium version basename to enable parallel version activation.');
+                }
+
+                $this->_premium_plugin_basename_from_parallel_activation = $premium_basename;
+
+                if ( is_plugin_active( $premium_basename ) ) {
+                    $is_premium = true;
+                }
+            }
+
             $plugin->update( array(
                 'id'                   => $id,
-                'type'                 => $this->get_option( $plugin_info, 'type', $this->_module_type ),
+                'type'                 => $module_type,
                 'public_key'           => $public_key,
                 'slug'                 => $this->_slug,
                 'premium_slug'         => $this->get_option( $plugin_info, 'premium_slug', "{$this->_slug}-premium" ),
@@ -5167,7 +5222,7 @@
                 'version'              => $this->get_plugin_version(),
                 'title'                => $this->get_plugin_name( $premium_suffix ),
                 'file'                 => $this->_plugin_basename,
-                'is_premium'           => $this->get_bool_option( $plugin_info, 'is_premium', true ),
+                'is_premium'           => $is_premium,
                 'premium_suffix'       => $premium_suffix,
                 'is_live'              => $this->get_bool_option( $plugin_info, 'is_live', true ),
                 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ),
@@ -5236,7 +5291,14 @@
                 $this->_anonymous_mode   = false;
             } else {
                 $this->_enable_anonymous = $this->get_bool_option( $plugin_info, 'enable_anonymous', true );
-                $this->_anonymous_mode   = $this->get_bool_option( $plugin_info, 'anonymous_mode', false );
+                $this->_anonymous_mode   = (
+                    $this->get_bool_option( $plugin_info, 'anonymous_mode', false ) ||
+                    (
+                        $this->apply_filters( 'playground_anonymous_mode', true ) &&
+                        ! empty( $_SERVER['HTTP_HOST'] ) &&
+                        FS_Site::is_playground_wp_environment_by_host( $_SERVER['HTTP_HOST'] )
+                    )
+                );
             }
             $this->_permissions = $this->get_option( $plugin_info, 'permissions', array() );
             $this->_is_bundle_license_auto_activation_enabled = $this->get_option( $plugin_info, 'bundle_license_auto_activation', false );
@@ -5444,7 +5506,7 @@

             if ( $this->is_registered() ) {
                 // Schedule code type changes event.
-                $this->schedule_install_sync();
+                $this->maybe_schedule_install_sync_cron();
             }

             /**
@@ -6508,6 +6570,33 @@
         }

         /**
+         * Instead of running blocking install sync event, execute non blocking scheduled cron job.
+         *
+         * @param int $except_blog_id Since 2.0.0 when running in a multisite network environment, the cron execution is consolidated. This param allows excluding specified blog ID from being the cron job executor.
+         *
+         * @author Leo Fajardo (@leorw)
+         * @since  2.9.1
+         */
+        private function maybe_schedule_install_sync_cron( $except_blog_id = 0 ) {
+            if ( ! $this->is_user_in_admin() ) {
+                return;
+            }
+
+            if ( $this->is_clone() ) {
+                return;
+            }
+
+            if (
+                // The event has been properly scheduled, so no need to reschedule it.
+                is_numeric( $this->next_install_sync() )
+            ) {
+                return;
+            }
+
+            $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
+        }
+
+        /**
          * @author Vova Feldman (@svovaf)
          * @since  1.1.7.3
          *
@@ -6605,22 +6694,6 @@
         }

         /**
-         * Instead of running blocking install sync event, execute non blocking scheduled wp-cron.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  1.1.7.3
-         *
-         * @param int $except_blog_id Since 2.0.0 when running in a multisite network environment, the cron execution is consolidated. This param allows excluding excluded specified blog ID from being the cron executor.
-         */
-        private function schedule_install_sync( $except_blog_id = 0 ) {
-            if ( $this->is_clone() ) {
-                return;
-            }
-
-            $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id );
-        }
-
-        /**
          * Unix timestamp for previous install sync cron execution or false if never executed.
          *
          * @todo   There's some very strange bug that $this->_storage->install_sync_timestamp value is not being updated. But for sure the sync event is working.
@@ -7411,7 +7484,7 @@
                  */
                 if (
                     is_plugin_active( $other_version_basename ) &&
-                    $this->apply_filters( 'deactivate_on_activation', true )
+                    $this->apply_filters( 'deactivate_on_activation', ! $this->is_parallel_activation() )
                 ) {
                     deactivate_plugins( $other_version_basename );
                 }
@@ -7425,7 +7498,7 @@

                 // Schedule re-activation event and sync.
 //				$this->sync_install( array(), true );
-                $this->schedule_install_sync();
+                $this->maybe_schedule_install_sync_cron();

                 // If activating the premium module version, add an admin notice to congratulate for an upgrade completion.
                 if ( $is_premium_version_activation ) {
@@ -8616,7 +8689,7 @@
                 return;
             }

-            $this->schedule_install_sync();
+            $this->maybe_schedule_install_sync_cron();
 //			$this->sync_install( array(), true );
         }

@@ -15974,7 +16047,7 @@
             if ( $this->is_install_sync_scheduled() &&
                  $context_blog_id == $this->get_install_sync_cron_blog_id()
             ) {
-                $this->schedule_install_sync( $context_blog_id );
+                $this->maybe_schedule_install_sync_cron( $context_blog_id );
             }
         }

@@ -23927,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'
@@ -25403,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/ultimeter/freemius/includes/class-fs-logger.php
+++ b/ultimeter/freemius/includes/class-fs-logger.php
@@ -1,695 +1,695 @@
-<?php
-	/**
-	 * @package     Freemius
-	 * @copyright   Copyright (c) 2015, Freemius, Inc.
-	 * @license     https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
-	 * @since       1.0.3
-	 */
-
-	if ( ! defined( 'ABSPATH' ) ) {
-		exit;
-	}
-
-	class FS_Logger {
-		private $_id;
-		private $_on = false;
-		private $_echo = false;
-		private $_file_start = 0;
-		/**
-		 * @var int PHP Process ID.
-		 */
-		private static $_processID;
-		/**
-		 * @var string PHP Script user name.
-		 */
-		private static $_ownerName;
-		/**
-		 * @var bool Is storage logging turned on.
-		 */
-		private static $_isStorageLoggingOn;
-		/**
-		 * @var int ABSPATH length.
-		 */
-		private static $_abspathLength;
-
-		/**
-		 * @var FS_Logger[] $LOGGERS
-		 */
-		private static $LOGGERS = array();
-		private static $LOG = array();
-		private static $CNT = 0;
-		private static $_HOOKED_FOOTER = false;
-
-		private function __construct( $id, $on = false, $echo = false ) {
-            $bt = debug_backtrace();
-
-			$this->_id = $id;
-
-			$caller = $bt[2];
-
-			if ( false !== strpos( $caller['file'], 'plugins' ) ) {
-				$this->_file_start = strpos( $caller['file'], 'plugins' ) + strlen( 'plugins/' );
-			} else {
-				$this->_file_start = strpos( $caller['file'], 'themes' ) + strlen( 'themes/' );
-			}
-
-			if ( $on ) {
-				$this->on();
-			}
-			if ( $echo ) {
-				$this->echo_on();
-			}
-		}
-
-		/**
-		 * @param string $id
-		 * @param bool   $on
-		 * @param bool   $echo
-		 *
-		 * @return FS_Logger
-		 */
-		public static function get_logger( $id, $on = false, $echo = false ) {
-			$id = strtolower( $id );
-
-			if ( ! isset( self::$_processID ) ) {
-				self::init();
-			}
-
-			if ( ! isset( self::$LOGGERS[ $id ] ) ) {
-				self::$LOGGERS[ $id ] = new FS_Logger( $id, $on, $echo );
-			}
-
-			return self::$LOGGERS[ $id ];
-		}
-
-		/**
-		 * Initialize logging global info.
-		 *
-		 * @author Vova Feldman (@svovaf)
-		 * @since  1.2.1.6
-		 */
-		private static function init() {
-			self::$_ownerName          = function_exists( 'get_current_user' ) ?
-				get_current_user() :
-				'unknown';
-			self::$_isStorageLoggingOn = ( 1 == get_option( 'fs_storage_logger', 0 ) );
-			self::$_abspathLength      = strlen( ABSPATH );
-			self::$_processID          = mt_rand( 0, 32000 );
-
-			// Process ID may be `false` on errors.
-			if ( ! is_numeric( self::$_processID ) ) {
-				self::$_processID = 0;
-			}
-		}
-
-		private static function hook_footer() {
-			if ( self::$_HOOKED_FOOTER ) {
-				return;
-			}
-
-			if ( is_admin() ) {
-				add_action( 'admin_footer', 'FS_Logger::dump', 100 );
-			} else {
-				add_action( 'wp_footer', 'FS_Logger::dump', 100 );
-			}
-		}
-
-		function is_on() {
-			return $this->_on;
-		}
-
-		function on() {
-			$this->_on = true;
-
-			if ( ! function_exists( 'dbDelta' ) ) {
-				require_once ABSPATH . 'wp-admin/includes/upgrade.php';
-			}
-
-			self::hook_footer();
-		}
-		function echo_on() {
-			$this->on();
-
-			$this->_echo = true;
-		}
-
-		function is_echo_on() {
-			return $this->_echo;
-		}
-
-		function get_id() {
-			return $this->_id;
-		}
-
-		function get_file() {
-			return $this->_file_start;
-		}
-
-		private function _log( &$message, $type, $wrapper = false ) {
-			if ( ! $this->is_on() ) {
-				return;
-			}
-
-			$bt    = debug_backtrace();
-			$depth = $wrapper ? 3 : 2;
-			while ( $depth < count( $bt ) - 1 && 'eval' === $bt[ $depth ]['function'] ) {
-				$depth ++;
-			}
-
-			$caller = $bt[ $depth ];
-
-			/**
-			 * Retrieve the correct call file & line number from backtrace
-			 * when logging from a wrapper method.
-			 *
-			 * @author Vova Feldman
-			 * @since  1.2.1.6
-			 */
-			if ( empty( $caller['line'] ) ) {
-				$depth --;
-
-				while ( $depth >= 0 ) {
-					if ( ! empty( $bt[ $depth ]['line'] ) ) {
-						$caller['line'] = $bt[ $depth ]['line'];
-						$caller['file'] = $bt[ $depth ]['file'];
-						break;
-					}
-				}
-			}
-
-			$log = array_merge( $caller, array(
-				'cnt'       => self::$CNT ++,
-				'logger'    => $this,
-				'timestamp' => microtime( true ),
-				'log_type'  => $type,
-				'msg'       => $message,
-			) );
-
-			if ( self::$_isStorageLoggingOn ) {
-				$this->db_log( $type, $message, self::$CNT, $caller );
-			}
-
-			self::$LOG[] = $log;
-
-			if ( $this->is_echo_on() && ! Freemius::is_ajax() ) {
-				echo self::format_html( $log ) . "n";
-			}
-		}
-
-		function log( $message, $wrapper = false ) {
-			$this->_log( $message, 'log', $wrapper );
-		}
-
-		function info( $message, $wrapper = false ) {
-			$this->_log( $message, 'info', $wrapper );
-		}
-
-		function warn( $message, $wrapper = false ) {
-			$this->_log( $message, 'warn', $wrapper );
-		}
-
-		function error( $message, $wrapper = false ) {
-			$this->_log( $message, 'error', $wrapper );
-		}
-
-		/**
-		 * Log API error.
-		 *
-		 * @author Vova Feldman (@svovaf)
-		 * @since  1.2.1.5
-		 *
-		 * @param mixed $api_result
-		 * @param bool  $wrapper
-		 */
-		function api_error( $api_result, $wrapper = false ) {
-			$message = '';
-			if ( is_object( $api_result ) &&
-			     ! empty( $api_result->error ) &&
-			     ! empty( $api_result->error->message )
-			) {
-				$message = $api_result->error->message;
-			} else if ( is_object( $api_result ) ) {
-				$message = var_export( $api_result, true );
-			} else if ( is_string( $api_result ) ) {
-				$message = $api_result;
-			} else if ( empty( $api_result ) ) {
-				$message = 'Empty API result.';
-			}
-
-			$message = 'API Error: ' . $message;
-
-			$this->_log( $message, 'error', $wrapper );
-		}
-
-		function entrance( $message = '', $wrapper = false ) {
-			$msg = 'Entrance' . ( empty( $message ) ? '' : ' > ' ) . $message;
-
-			$this->_log( $msg, 'log', $wrapper );
-		}
-
-		function departure( $message = '', $wrapper = false ) {
-			$msg = 'Departure' . ( empty( $message ) ? '' : ' > ' ) . $message;
-
-			$this->_log( $msg, 'log', $wrapper );
-		}
-
-		#--------------------------------------------------------------------------------
-		#region Log Formatting
-		#--------------------------------------------------------------------------------
-
-		private static function format( $log, $show_type = true ) {
-			return '[' . str_pad( $log['cnt'], strlen( self::$CNT ), '0', STR_PAD_LEFT ) . '] [' . $log['logger']->_id . '] ' . ( $show_type ? '[' . $log['log_type'] . ']' : '' ) . ( ! empty( $log['class'] ) ? $log['class'] . $log['type'] : '' ) . $log['function'] . ' >> ' . $log['msg'] . ( isset( $log['file'] ) ? ' (' . substr( $log['file'], $log['logger']->_file_start ) . ' ' . $log['line'] . ') ' : '' ) . ' [' . $log['timestamp'] . ']';
-		}
-
-		private static function format_html( $log ) {
-			return '<div style="font-size: 13px; font-family: monospace; color: #7da767; padding: 8px 3px; background: #000; border-bottom: 1px solid #555;">[' . $log['cnt'] . '] [' . $log['logger']->_id . '] [' . $log['log_type'] . '] <b><code style="color: #c4b1e0;">' . ( ! empty( $log['class'] ) ? $log['class'] . $log['type'] : '' ) . $log['function'] . '</code> >> <b style="color: #f59330;">' . esc_html( $log['msg'] ) . '</b></b>' . ( isset( $log['file'] ) ? ' (' . substr( $log['file'], $log['logger']->_file_start ) . ' ' . $log['line'] . ')' : '' ) . ' [' . $log['timestamp'] . ']</div>';
-		}
-
-		#endregion
-
-		static function dump() {
-			?>
-			<!-- BEGIN: Freemius PHP Console Log -->
-			<script type="text/javascript">
-				<?php
-				foreach ( self::$LOG as $log ) {
-					echo 'console.' . $log['log_type'] . '(' . json_encode( self::format( $log, false ) ) . ')' . "n";
-				}
-				?>
-			</script>
-			<!-- END: Freemius PHP Console Log -->
-			<?php
-		}
-
-		static function get_log() {
-			return self::$LOG;
-		}
-
-		#--------------------------------------------------------------------------------
-		#region Database Logging
-		#--------------------------------------------------------------------------------
-
-		/**
-		 * @author Vova Feldman (@svovaf)
-		 * @since  1.2.1.6
-		 *
-		 * @return bool
-		 */
-		public static function is_storage_logging_on() {
-			if ( ! isset( self::$_isStorageLoggingOn ) ) {
-				self::$_isStorageLoggingOn = ( 1 == get_option( 'fs_storage_logger', 0 ) );
-			}
-
-			return self::$_isStorageLoggingOn;
-		}
-
-		/**
-		 * Turns on/off database persistent debugging to capture
-		 * multi-session logs to debug complex flows like
-		 * plugin auto-deactivate on premium version activation.
-		 *
-		 * @todo   Check if Theme Check has issues with DB tables for themes.
-		 *
-		 * @author Vova Feldman (@svovaf)
-		 * @since  1.2.1.6
-		 *
-		 * @param bool $is_on
-		 *
-		 * @return bool
-		 */
-		public static function _set_storage_logging( $is_on = true ) {
-			global $wpdb;
-
-			$table = "{$wpdb->prefix}fs_logger";
-
-			/**
-			 * Drop logging table in any case.
-			 */
-			$result = $wpdb->query( "DROP TABLE IF EXISTS $table;" );
-
-			if ( $is_on ) {
-				/**
-				 * Create logging table.
-				 *
-				 * NOTE:
-				 *  dbDelta must use KEY and not INDEX for indexes.
-				 *
-				 * @link https://core.trac.wordpress.org/ticket/2695
-				 */
-				$result = $wpdb->query( "CREATE TABLE IF NOT EXISTS {$table} (
-`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-`process_id` INT UNSIGNED NOT NULL,
-`user_name` VARCHAR(64) NOT NULL,
-`logger` VARCHAR(128) NOT NULL,
-`log_order` INT UNSIGNED NOT NULL,
-`type` ENUM('log','info','warn','error') NOT NULL DEFAULT 'log',
-`message` TEXT NOT NULL,
-`file` VARCHAR(256) NOT NULL,
-`line` INT UNSIGNED NOT NULL,
-`function` VARCHAR(256) NOT NULL,
-`request_type` ENUM('call','ajax','cron') NOT NULL DEFAULT 'call',
-`request_url` VARCHAR(1024) NOT NULL,
-`created` DECIMAL(16, 6) NOT NULL,
-PRIMARY KEY (`id`),
-KEY `process_id` (`process_id` ASC),
-KEY `process_logger` (`process_id` ASC, `logger` ASC),
-KEY `function` (`function` ASC),
-KEY `type` (`type` ASC))" );
-			}
-
-			if ( false !== $result ) {
-				update_option( 'fs_storage_logger', ( $is_on ? 1 : 0 ) );
-				self::$_isStorageLoggingOn = $is_on;
-			}
-
-			return ( false !== $result );
-		}
-
-		/**
-		 * @author Vova Feldman (@svovaf)
-		 * @since  1.2.1.6
-		 *
-		 * @param string $type
-		 * @param string $message
-		 * @param int    $log_order
-		 * @param array  $caller
-		 *
-		 * @return false|int
-		 */
-		private function db_log(
-			&$type,
-			&$message,
-			&$log_order,
-			&$caller
-		) {
-			global $wpdb;
-
-			$request_type = 'call';
-			if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
-				$request_type = 'cron';
-			} else if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
-				$request_type = 'ajax';
-			}
-
-			$request_url = WP_FS__IS_HTTP_REQUEST ?
-				$_SERVER['REQUEST_URI'] :
-				'';
-
-			return $wpdb->insert(
-				"{$wpdb->prefix}fs_logger",
-				array(
-					'process_id'   => self::$_processID,
-					'user_name'    => self::$_ownerName,
-					'logger'       => $this->_id,
-					'log_order'    => $log_order,
-					'type'         => $type,
-					'request_type' => $request_type,
-					'request_url'  => $request_url,
-					'message'      => $message,
-					'file'         => isset( $caller['file'] ) ?
-						substr( $caller['file'], self::$_abspathLength ) :
-						'',
-					'line'         => $caller['line'],
-					'function'     => ( ! empty( $caller['class'] ) ? $caller['class'] . $caller['type'] : '' ) . $caller['function'],
-					'created'      => microtime( true ),
-				)
-			);
-		}
-
-		/**
-		 * Persistent DB logger columns.
-		 *
-		 * @var array
-		 */
-		private static $_log_columns = array(
-			'id',
-			'process_id',
-			'user_name',
-			'logger',
-			'log_order',
-			'type',
-			'message',
-			'file',
-			'line',
-			'function',
-			'request_type',
-			'request_url',
-			'created',
-		);
-
-		/**
-		 * Create DB logs query.
-		 *
-		 * @author Vova Feldman (@svovaf)
-		 * @since  1.2.1.6
-		 *
-		 * @param bool $filters
-		 * @param int  $limit
-		 * @param int  $offset
-		 * @param bool $order
-		 * @param bool $escape_eol
-		 *
-		 * @return string
-		 */
-		private static function build_db_logs_query(
-			$filters = false,
-			$limit = 200,
-			$offset = 0,
-			$order = false,
-			$escape_eol = false
-		) {
-			global $wpdb;
-
-			$select = '*';
-
-			if ( $escape_eol ) {
-				$select = '';
-				for ( $i = 0, $len = count( self::$_log_columns ); $i < $len; $i ++ ) {
-					if ( $i > 0 ) {
-						$select .= ', ';
-					}
-
-					if ( 'message' !== self::$_log_columns[ $i ] ) {
-						$select .= self::$_log_columns[ $i ];
-					} else {
-						$select .= 'REPLACE(message , 'n', ' ') AS message';
-					}
-				}
-			}
-
-			$query = "SELECT {$select} FROM {$wpdb->prefix}fs_logger";
-			if ( is_array( $filters ) ) {
-				$criteria = array();
-
-				if ( ! empty( $filters['type'] ) && 'all' !== $filters['type'] ) {
-					$filters['type'] = strtolower( $filters['type'] );
-
-					switch ( $filters['type'] ) {
-						case 'warn_error':
-							$criteria[] = array( 'col' => 'type', 'val' => array( 'warn', 'error' ) );
-							break;
-						case 'error':
-						case 'warn':
-							$criteria[] = array( 'col' => 'type', 'val' => $filters['type'] );
-							break;
-						case 'info':
-						default:
-							$criteria[] = array( 'col' => 'type', 'val' => array( 'info', 'log' ) );
-							break;
-					}
-				}
-
-				if ( ! empty( $filters['request_type'] ) ) {
-					$filters['request_type'] = strtolower( $filters['request_type'] );
-
-					if ( in_array( $filters['request_type'], array( 'call', 'ajax', 'cron' ) ) ) {
-						$criteria[] = array( 'col' => 'request_type', 'val' => $filters['request_type'] );
-					}
-				}
-
-				if ( ! empty( $filters['file'] ) ) {
-					$criteria[] = array(
-						'col' => 'file',
-						'op'  => 'LIKE',
-						'val' => '%' . esc_sql( $filters['file'] ),
-					);
-				}
-
-				if ( ! empty( $filters['function'] ) ) {
-					$criteria[] = array(
-						'col' => 'function',
-						'op'  => 'LIKE',
-						'val' => '%' . esc_sql( $filters['function'] ),
-					);
-				}
-
-				if ( ! empty( $filters['process_id'] ) && is_numeric( $filters['process_id'] ) ) {
-					$criteria[] = array( 'col' => 'process_id', 'val' => $filters['process_id'] );
-				}
-
-				if ( ! empty( $filters['logger'] ) ) {
-					$criteria[] = array(
-						'col' => 'logger',
-						'op'  => 'LIKE',
-						'val' => '%' . esc_sql( $filters['logger'] ) . '%',
-					);
-				}
-
-				if ( ! empty( $filters['message'] ) ) {
-					$criteria[] = array(
-						'col' => 'message',
-						'op'  => 'LIKE',
-						'val' => '%' . esc_sql( $filters['message'] ) . '%',
-					);
-				}
-
-				if ( 0 < count( $criteria ) ) {
-					$query .= "nWHEREn";
-
-					$first = true;
-					foreach ( $criteria as $c ) {
-						if ( ! $first ) {
-							$query .= "ANDn";
-						}
-
-						if ( is_array( $c['val'] ) ) {
-							$operator = 'IN';
-
-							for ( $i = 0, $len = count( $c['val'] ); $i < $len; $i ++ ) {
-								$c['val'][ $i ] = "'" . esc_sql( $c['val'][ $i ] ) . "'";
-							}
-
-							$val = '(' . implode( ',', $c['val'] ) . ')';
-						} else {
-							$operator = ! empty( $c['op'] ) ? $c['op'] : '=';
-							$val      = "'" . esc_sql( $c['val'] ) . "'";
-						}
-
-						$query .= "`{$c['col']}` {$operator} {$val}n";
-
-						$first = false;
-					}
-				}
-			}
-
-			if ( ! is_array( $order ) ) {
-				$order = array(
-					'col'   => 'id',
-					'order' => 'desc'
-				);
-			}
-
-			$query .= " ORDER BY {$order['col']} {$order['order']} LIMIT {$offset},{$limit}";
-
-			return $query;
-		}
-
-		/**
-		 * Load logs from DB.
-		 *
-		 * @author Vova Feldman (@svovaf)
-		 * @since  1.2.1.6
-		 *
-		 * @param bool $filters
-		 * @param int  $limit
-		 * @param int  $offset
-		 * @param bool $order
-		 *
-		 * @return object[]|null
-		 */
-		public static function load_db_logs(
-			$filters = false,
-			$limit = 200,
-			$offset = 0,
-			$order = false
-		) {
-			global $wpdb;
-
-			$query = self::build_db_logs_query(
-				$filters,
-				$limit,
-				$offset,
-				$order
-			);
-
-			return $wpdb->get_results( $query );
-		}
-
-		/**
-		 * Load logs from DB.
-		 *
-		 * @author Vova Feldman (@svovaf)
-		 * @since  1.2.1.6
-		 *
-		 * @param bool   $filters
-		 * @param string $filename
-		 * @param int    $limit
-		 * @param int    $offset
-		 * @param bool   $order
-		 *
-		 * @return false|string File download URL or false on failure.
-		 */
-		public static function download_db_logs(
-			$filters = false,
-			$filename = '',
-			$limit = 10000,
-			$offset = 0,
-			$order = false
-		) {
-			global $wpdb;
-
-			$query = self::build_db_logs_query(
-				$filters,
-				$limit,
-				$offset,
-				$order,
-				true
-			);
-
-			$upload_dir = wp_upload_dir();
-			if ( empty( $filename ) ) {
-				$filename = 'fs-logs-' . date( 'Y-m-d_H-i-s', WP_FS__SCRIPT_START_TIME ) . '.csv';
-			}
-			$filepath = rtrim( $upload_dir['path'], '/' ) . "/{$filename}";
-
-			$query .= " INTO OUTFILE '{$filepath}' FIELDS TERMINATED BY 't' ESCAPED BY '\\' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n'";
-
-			$columns = '';
-			for ( $i = 0, $len = count( self::$_log_columns ); $i < $len; $i ++ ) {
-				if ( $i > 0 ) {
-					$columns .= ', ';
-				}
-
-				$columns .= "'" . self::$_log_columns[ $i ] . "'";
-			}
-
-			$query = "SELECT {$columns} UNION ALL " . $query;
-
-			$result = $wpdb->query( $query );
-
-			if ( false === $result ) {
-				return false;
-			}
-
-			return rtrim( $upload_dir['url'], '/' ) . '/' . $filename;
-		}
-
-		/**
-		 * @author Vova Feldman (@svovaf)
-		 * @since  1.2.1.6
-		 *
-		 * @param string $filename
-		 *
-		 * @return string
-		 */
-		public static function get_logs_download_url( $filename = '' ) {
-			$upload_dir = wp_upload_dir();
-			if ( empty( $filename ) ) {
-				$filename = 'fs-logs-' . date( 'Y-m-d_H-i-s', WP_FS__SCRIPT_START_TIME ) . '.csv';
-			}
-
-			return rtrim( $upload_dir['url'], '/' ) . $filename;
-		}
-
-		#endregion
-	}
+<?php
+	/**
+	 * @package     Freemius
+	 * @copyright   Copyright (c) 2015, Freemius, Inc.
+	 * @license     https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
+	 * @since       1.0.3
+	 */
+
+	if ( ! defined( 'ABSPATH' ) ) {
+		exit;
+	}
+
+	class FS_Logger {
+		private $_id;
+		private $_on = false;
+		private $_echo = false;
+		private $_file_start = 0;
+		/**
+		 * @var int PHP Process ID.
+		 */
+		private static $_processID;
+		/**
+		 * @var string PHP Script user name.
+		 */
+		private static $_ownerName;
+		/**
+		 * @var bool Is storage logging turned on.
+		 */
+		private static $_isStorageLoggingOn;
+		/**
+		 * @var int ABSPATH length.
+		 */
+		private static $_abspathLength;
+
+		/**
+		 * @var FS_Logger[] $LOGGERS
+		 */
+		private static $LOGGERS = array();
+		private static $LOG = array();
+		private static $CNT = 0;
+		private static $_HOOKED_FOOTER = false;
+
+		private function __construct( $id, $on = false, $echo = false ) {
+            $bt = debug_backtrace();
+
+			$this->_id = $id;
+
+			$caller = $bt[2];
+
+			if ( false !== strpos( $caller['file'], 'plugins' ) ) {
+				$this->_file_start = strpos( $caller['file'], 'plugins' ) + strlen( 'plugins/' );
+			} else {
+				$this->_file_start = strpos( $caller['file'], 'themes' ) + strlen( 'themes/' );
+			}
+
+			if ( $on ) {
+				$this->on();
+			}
+			if ( $echo ) {
+				$this->echo_on();
+			}
+		}
+
+		/**
+		 * @param string $id
+		 * @param bool   $on
+		 * @param bool   $echo
+		 *
+		 * @return FS_Logger
+		 */
+		public static function get_logger( $id, $on = false, $echo = false ) {
+			$id = strtolower( $id );
+
+			if ( ! isset( self::$_processID ) ) {
+				self::init();
+			}
+
+			if ( ! isset( self::$LOGGERS[ $id ] ) ) {
+				self::$LOGGERS[ $id ] = new FS_Logger( $id, $on, $echo );
+			}
+
+			return self::$LOGGERS[ $id ];
+		}
+
+		/**
+		 * Initialize logging global info.
+		 *
+		 * @author Vova Feldman (@svovaf)
+		 * @since  1.2.1.6
+		 */
+		private static function init() {
+			self::$_ownerName          = function_exists( 'get_current_user' ) ?
+				get_current_user() :
+				'unknown';
+			self::$_isStorageLoggingOn = ( 1 == get_option( 'fs_storage_logger', 0 ) );
+			self::$_abspathLength      = strlen( ABSPATH );
+			self::$_processID          = mt_rand( 0, 32000 );
+
+			// Process ID may be `false` on errors.
+			if ( ! is_numeric( self::$_processID ) ) {
+				self::$_processID = 0;
+			}
+		}
+
+		private static function hook_footer() {
+			if ( self::$_HOOKED_FOOTER ) {
+				return;
+			}
+
+			if ( is_admin() ) {
+				add_action( 'admin_footer', 'FS_Logger::dump', 100 );
+			} else {
+				add_action( 'wp_footer', 'FS_Logger::dump', 100 );
+			}
+		}
+
+		function is_on() {
+			return $this->_on;
+		}
+
+		function on() {
+			$this->_on = true;
+
+			if ( ! function_exists( 'dbDelta' ) ) {
+				require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+			}
+
+			self::hook_footer();
+		}
+		function echo_on() {
+			$this->on();
+
+			$this->_echo = true;
+		}
+
+		function is_echo_on() {
+			return $this->_echo;
+		}
+
+		function get_id() {
+			return $this->_id;
+		}
+
+		function get_file() {
+			return $this->_file_start;
+		}
+
+		private function _log( &$message, $type, $wrapper = false ) {
+			if ( ! $this->is_on() ) {
+				return;
+			}
+
+			$bt    = debug_backtrace();
+			$depth = $wrapper ? 3 : 2;
+			while ( $depth < count( $bt ) - 1 && 'eval' === $bt[ $depth ]['function'] ) {
+				$depth ++;
+			}
+
+			$caller = $bt[ $depth ];
+
+			/**
+			 * Retrieve the correct call file & line number from backtrace
+			 * when logging from a wrapper method.
+			 *
+			 * @author Vova Feldman
+			 * @since  1.2.1.6
+			 */
+			if ( empty( $caller['line'] ) ) {
+				$depth --;
+
+				while ( $depth >= 0 ) {
+					if ( ! empty( $bt[ $depth ]['line'] ) ) {
+						$caller['line'] = $bt[ $depth ]['line'];
+						$caller['file'] = $bt[ $depth ]['file'];
+						break;
+					}
+				}
+			}
+
+			$log = array_merge( $caller, array(
+				'cnt'       => self::$CNT ++,
+				'logger'    => $this,
+				'timestamp' => microtime( true ),
+				'log_type'  => $type,
+				'msg'       => $message,
+			) );
+
+			if ( self::$_isStorageLoggingOn ) {
+				$this->db_log( $type, $message, self::$CNT, $caller );
+			}
+
+			self::$LOG[] = $log;
+
+			if ( $this->is_echo_on() && ! Freemius::is_ajax() ) {
+				echo self::format_html( $log ) . "n";
+			}
+		}
+
+		function log( $message, $wrapper = false ) {
+			$this->_log( $message, 'log', $wrapper );
+		}
+
+		function info( $message, $wrapper = false ) {
+			$this->_log( $message, 'info', $wrapper );
+		}
+
+		function warn( $message, $wrapper = false ) {
+			$this->_log( $message, 'warn', $wrapper );
+		}
+
+		function error( $message, $wrapper = false ) {
+			$this->_log( $message, 'error', $wrapper );
+		}
+
+		/**
+		 * Log API error.
+		 *
+		 * @author Vova Feldman (@svovaf)
+		 * @since  1.2.1.5
+		 *
+		 * @param mixed $api_result
+		 * @param bool  $wrapper
+		 */
+		function api_error( $api_result, $wrapper = false ) {
+			$message = '';
+			if ( is_object( $api_result ) &&
+			     ! empty( $api_result->error ) &&
+			     ! empty( $api_result->error->message )
+			) {
+				$message = $api_result->error->message;
+			} else if ( is_object( $api_result ) ) {
+				$message = var_export( $api_result, true );
+			} else if ( is_string( $api_result ) ) {
+				$message = $api_result;
+			} else if ( empty( $api_result ) ) {
+				$message = 'Empty API result.';
+			}
+
+			$message = 'API Error: ' . $message;
+
+			$this->_log( $message, 'error', $wrapper );
+		}
+
+		function entrance( $message = '', $wrapper = false ) {
+			$msg = 'Entrance' . ( empty( $message ) ? '' : ' > ' ) . $message;
+
+			$this->_log( $msg, 'log', $wrapper );
+		}
+
+		function departure( $message = '', $wrapper = false ) {
+			$msg = 'Departure' . ( empty( $message ) ? '' : ' > ' ) . $message;
+
+			$this->_log( $msg, 'log', $wrapper );
+		}
+
+		#--------------------------------------------------------------------------------
+		#region Log Formatting
+		#--------------------------------------------------------------------------------
+
+		private static function format( $log, $show_type = true ) {
+			return '[' . str_pad( $log['cnt'], strlen( self::$CNT ), '0', STR_PAD_LEFT ) . '] [' . $log['logger']->_id . '] ' . ( $show_type ? '[' . $log['log_type'] . ']' : '' ) . ( ! empty( $log['class'] ) ? $log['class'] . $log['type'] : '' ) . $log['function'] . ' >> ' . $log['msg'] . ( isset( $log['file'] ) ? ' (' . substr( $log['file'], $log['logger']->_file_start ) . ' ' . $log['line'] . ') ' : '' ) . ' [' . $log['timestamp'] . ']';
+		}
+
+		private static function format_html( $log ) {
+			return '<div style="font-size: 13px; font-family: monospace; color: #7da767; padding: 8px 3px; background: #000; border-bottom: 1px solid #555;">[' . $log['cnt'] . '] [' . $log['logger']->_id . '] [' . $log['log_type'] . '] <b><code style="color: #c4b1e0;">' . ( ! empty( $log['class'] ) ? $log['class'] . $log['type'] : '' ) . $log['function'] . '</code> >> <b style="color: #f59330;">' . esc_html( $log['msg'] ) . '</b></b>' . ( isset( $log['file'] ) ? ' (' . substr( $log['file'], $log['logger']->_file_start ) . ' ' . $log['line'] . ')' : '' ) . ' [' . $log['timestamp'] . ']</div>';
+		}
+
+		#endregion
+
+		static function dump() {
+			?>
+			<!-- BEGIN: Freemius PHP Console Log -->
+			<script type="text/javascript">
+				<?php
+				foreach ( self::$LOG as $log ) {
+					echo 'console.' . $log['log_type'] . '(' . json_encode( self::format( $log, false ) ) . ')' . "n";
+				}
+				?>
+			</script>
+			<!-- END: Freemius PHP Console Log -->
+			<?php
+		}
+
+		static function get_log() {
+			return self::$LOG;
+		}
+
+		#--------------------------------------------------------------------------------
+		#region Database Logging
+		#--------------------------------------------------------------------------------
+
+		/**
+		 * @author Vova Feldman (@svovaf)
+		 * @since  1.2.1.6
+		 *
+		 * @return bool
+		 */
+		public static function is_storage_logging_on() {
+			if ( ! isset( self::$_isStorageLoggingOn ) ) {
+				self::$_isStorageLoggingOn = ( 1 == get_option( 'fs_storage_logger', 0 ) );
+			}
+
+			return self::$_isStorageLoggingOn;
+		}
+
+		/**
+		 * Turns on/off database persistent debugging to capture
+		 * multi-session logs to debug complex flows like
+		 * plugin auto-deactivate on premium version activation.
+		 *
+		 * @todo   Check if Theme Check has issues with DB tables for themes.
+		 *
+		 * @author Vova Feldman (@svovaf)
+		 * @since  1.2.1.6
+		 *
+		 * @param bool $is_on
+		 *
+		 * @return bool
+		 */
+		public static function _set_storage_logging( $is_on = true ) {
+			global $wpdb;
+
+			$table = "{$wpdb->prefix}fs_logger";
+
+			/**
+			 * Drop logging table in any case.
+			 */
+			$result = $wpdb->query( "DROP TABLE IF EXISTS $table;" );
+
+			if ( $is_on ) {
+				/**
+				 * Create logging table.
+				 *
+				 * NOTE:
+				 *  dbDelta must use KEY and not INDEX for indexes.
+				 *
+				 * @link https://core.trac.wordpress.org/ticket/2695
+				 */
+				$result = $wpdb->query( "CREATE TABLE IF NOT EXISTS {$table} (
+`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+`process_id` INT UNSIGNED NOT NULL,
+`user_name` VARCHAR(64) NOT NULL,
+`logger` VARCHAR(128) NOT NULL,
+`log_order` INT UNSIGNED NOT NULL,
+`type` ENUM('log','info','warn','error') NOT NULL DEFAULT 'log',
+`message` TEXT NOT NULL,
+`file` VARCHAR(256) NOT NULL,
+`line` INT UNSIGNED NOT NULL,
+`function` VARCHAR(256) NOT NULL,
+`request_type` ENUM('call','ajax','cron') NOT NULL DEFAULT 'call',
+`request_url` VARCHAR(1024) NOT NULL,
+`created` DECIMAL(16, 6) NOT NULL,
+PRIMARY KEY (`id`),
+KEY `process_id` (`process_id` ASC),
+KEY `process_logger` (`process_id` ASC, `logger` ASC),
+KEY `function` (`function` ASC),
+KEY `type` (`type` ASC))" );
+			}
+
+			if ( false !== $result ) {
+				update_option( 'fs_storage_logger', ( $is_on ? 1 : 0 ) );
+				self::$_isStorageLoggingOn = $is_on;
+			}
+
+			return ( false !== $result );
+		}
+
+		/**
+		 * @author Vova Feldman (@svovaf)
+		 * @since  1.2.1.6
+		 *
+		 * @param string $type
+		 * @param string $message
+		 * @param int    $log_order
+		 * @param array  $caller
+		 *
+		 * @return false|int
+		 */
+		private function db_log(
+			&$type,
+			&$message,
+			&$log_order,
+			&$caller
+		) {
+			global $wpdb;
+
+			$request_type = 'call';
+			if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
+				$request_type = 'cron';
+			} else if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
+				$request_type = 'ajax';
+			}
+
+			$request_url = WP_FS__IS_HTTP_REQUEST ?
+				$_SERVER['REQUEST_URI'] :
+				'';
+
+			return $wpdb->insert(
+				"{$wpdb->prefix}fs_logger",
+				array(
+					'process_id'   => self::$_processID,
+					'user_name'    => self::$_ownerName,
+					'logger'       => $this->_id,
+					'log_order'    => $log_order,
+					'type'         => $type,
+					'request_type' => $request_type,
+					'request_url'  => $request_url,
+					'message'      => $message,
+					'file'         => isset( $caller['file'] ) ?
+						substr( $caller['file'], self::$_abspathLength ) :
+						'',
+					'line'         => $caller['line'],
+					'function'     => ( ! empty( $caller['class'] ) ? $caller['class'] . $caller['type'] : '' ) . $caller['function'],
+					'created'      => microtime( true ),
+				)
+			);
+		}
+
+		/**
+		 * Persistent DB logger columns.
+		 *
+		 * @var array
+		 */
+		private static $_log_columns = array(
+			'id',
+			'process_id',
+			'user_name',
+			'logger',
+			'log_order',
+			'type',
+			'message',
+			'file',
+			'line',
+			'function',
+			'request_type',
+			'request_url',
+			'created',
+		);
+
+		/**
+		 * Create DB logs query.
+		 *
+		 * @author Vova Feldman (@svovaf)
+		 * @since  1.2.1.6
+		 *
+		 * @param bool $filters
+		 * @param int  $limit
+		 * @param int  $offset
+		 * @param bool $order
+		 * @param bool $escape_eol
+		 *
+		 * @return string
+		 */
+		private static function build_db_logs_query(
+			$filters = false,
+			$limit = 200,
+			$offset = 0,
+			$order = false,
+			$escape_eol = false
+		) {
+			global $wpdb;
+
+			$select = '*';
+
+			if ( $escape_eol ) {
+				$select = '';
+				for ( $i = 0, $len = count( self::$_log_columns ); $i < $len; $i ++ ) {
+					if ( $i > 0 ) {
+						$select .= ', ';
+					}
+
+					if ( 'message' !== self::$_log_columns[ $i ] ) {
+						$select .= self::$_log_columns[ $i ];
+					} else {
+						$select .= 'REPLACE(message , 'n', ' ') AS message';
+					}
+				}
+			}
+
+			$query = "SELECT {$select} FROM {$wpdb->prefix}fs_logger";
+			if ( is_array( $filters ) ) {
+				$criteria = array();
+
+				if ( ! empty( $filters['type'] ) && 'all' !== $filters['type'] ) {
+					$filters['type'] = strtolower( $filters['type'] );
+
+					switch ( $filters['type'] ) {
+						case 'warn_error':
+							$criteria[] = array( 'col' => 'type', 'val' => array( 'warn', 'error' ) );
+							break;
+						case 'error':
+						case 'warn':
+							$criteria[] = array( 'col' => 'type', 'val' => $filters['type'] );
+							break;
+						case 'info':
+						default:
+							$criteria[] = array( 'col' => 'type', 'val' => array( 'info', 'log' ) );
+							break;
+					}
+				}
+
+				if ( ! empty( $filters['request_type'] ) ) {
+					$filters['request_type'] = strtolower( $filters['request_type'] );
+
+					if ( in_array( $filters['request_type'], array( 'call', 'ajax', 'cron' ) ) ) {
+						$criteria[] = array( 'col' => 'request_type', 'val' => $filters['request_type'] );
+					}
+				}
+
+				if ( ! empty( $filters['file'] ) ) {
+					$criteria[] = array(
+						'col' => 'file',
+						'op'  => 'LIKE',
+						'val' => '%' . esc_sql( $filters['file'] ),
+					);
+				}
+
+				if ( ! empty( $filters['function'] ) ) {
+					$criteria[] = array(
+						'col' => 'function',
+						'op'  => 'LIKE',
+						'val' => '%' . esc_sql( $filters['function'] ),
+					);
+				}
+
+				if ( ! empty( $filters['process_id'] ) && is_numeric( $filters['process_id'] ) ) {
+					$criteria[] = array( 'col' => 'process_id', 'val' => $filters['process_id'] );
+				}
+
+				if ( ! empty( $filters['logger'] ) ) {
+					$criteria[] = array(
+						'col' => 'logger',
+						'op'  => 'LIKE',
+						'val' => '%' . esc_sql( $filters['logger'] ) . '%',
+					);
+				}
+
+				if ( ! empty( $filters['message'] ) ) {
+					$criteria[] = array(
+						'col' => 'message',
+						'op'  => 'LIKE',
+						'val' => '%' . esc_sql( $filters['message'] ) . '%',
+					);
+				}
+
+				if ( 0 < count( $criteria ) ) {
+					$query .= "nWHEREn";
+
+					$first = true;
+					foreach ( $criteria as $c ) {
+						if ( ! $first ) {
+							$query .= "ANDn";
+						}
+
+						if ( is_array( $c['val'] ) ) {
+							$operator = 'IN';
+
+							for ( $i = 0, $len = count( $c['val'] ); $i < $len; $i ++ ) {
+								$c['val'][ $i ] = "'" . esc_sql( $c['val'][ $i ] ) . "'";
+							}
+
+							$val = '(' . implode( ',', $c['val'] ) . ')';
+						} else {
+							$operator = ! empty( $c['op'] ) ? $c['op'] : '=';
+							$val      = "'" . esc_sql( $c['val'] ) . "'";
+						}
+
+						$query .= "`{$c['col']}` {$operator} {$val}n";
+
+						$first = false;
+					}
+				}
+			}
+
+			if ( ! is_array( $order ) ) {
+				$order = array(
+					'col'   => 'id',
+					'order' => 'desc'
+				);
+			}
+
+			$query .= " ORDER BY {$order['col']} {$order['order']} LIMIT {$offset},{$limit}";
+
+			return $query;
+		}
+
+		/**
+		 * Load logs from DB.
+		 *
+		 * @author Vova Feldman (@svovaf)
+		 * @since  1.2.1.6
+		 *
+		 * @param bool $filters
+		 * @param int  $limit
+		 * @param int  $offset
+		 * @param bool $order
+		 *
+		 * @return object[]|null
+		 */
+		public static function load_db_logs(
+			$filters = false,
+			$limit = 200,
+			$offset = 0,
+			$order = false
+		) {
+			global $wpdb;
+
+			$query = self::build_db_logs_query(
+				$filters,
+				$limit,
+				$offset,
+				$order
+			);
+
+			return $wpdb->get_results( $query );
+		}
+
+		/**
+		 * Load logs from DB.
+		 *
+		 * @author Vova Feldman (@svovaf)
+		 * @since  1.2.1.6
+		 *
+		 * @param bool   $filters
+		 * @param string $filename
+		 * @param int    $limit
+		 * @param int    $offset
+		 * @param bool   $order
+		 *
+		 * @return false|string File download URL or false on failure.
+		 */
+		public static function download_db_logs(
+			$filters = false,
+			$filename = '',
+			$limit = 10000,
+			$offset = 0,
+			$order = false
+		) {
+			global $wpdb;
+
+			$query = self::build_db_logs_query(
+				$filters,
+				$limit,
+				$offset,
+				$order,
+				true
+			);
+
+			$upload_dir = wp_upload_dir();
+			if ( empty( $filename ) ) {
+				$filename = 'fs-logs-' . date( 'Y-m-d_H-i-s', WP_FS__SCRIPT_START_TIME ) . '.csv';
+			}
+			$filepath = rtrim( $upload_dir['path'], '/' ) . "/{$filename}";
+
+			$query .= " INTO OUTFILE '{$filepath}' FIELDS TERMINATED BY 't' ESCAPED BY '\\' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n'";
+
+			$columns = '';
+			for ( $i = 0, $len = count( self::$_log_columns ); $i < $len; $i ++ ) {
+				if ( $i > 0 ) {
+					$columns .= ', ';
+				}
+
+				$columns .= "'" . self::$_log_columns[ $i ] . "'";
+			}
+
+			$query = "SELECT {$columns} UNION ALL " . $query;
+
+			$result = $wpdb->query( $query );
+
+			if ( false === $result ) {
+				return false;
+			}
+
+			return rtrim( $upload_dir['url'], '/' ) . '/' . $filename;
+		}
+
+		/**
+		 * @author Vova Feldman (@svovaf)
+		 * @since  1.2.1.6
+		 *
+		 * @param string $filename
+		 *
+		 * @return string
+		 */
+		public static function get_logs_download_url( $filename = '' ) {
+			$upload_dir = wp_upload_dir();
+			if ( empty( $filename ) ) {
+				$filename = 'fs-logs-' . date( 'Y-m-d_H-i-s', WP_FS__SCRIPT_START_TIME ) . '.csv';
+			}
+
+			return rtrim( $upload_dir['url'], '/' ) . $filename;
+		}
+
+		#endregion
+	}
--- a/ultimeter/freemius/includes/class-fs-plugin-updater.php
+++ b/ultimeter/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/ultimeter/freemius/includes/class-fs-security.php
+++ b/ultimeter/freemius/includes/class-fs-security.php
@@ -1,103 +1,103 @@
-<?php
-	/**
-	 * @package     Freemius
-	 * @copyright   Copyright (c) 2015, Freemius, Inc.
-	 * @license     https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
-	 * @since       1.0.3
-	 */
-
-	if ( ! defined( 'ABSPATH' ) ) {
-		exit;
-	}
-
-	define( 'WP_FS__SECURITY_PARAMS_PREFIX', 's_' );
-
-	/**
-	 * Class FS_Security
-	 */
-	class FS_Security {
-		/**
-		 * @var FS_Security
-		 * @since 1.0.3
-		 */
-		private static $_instance;
-		/**
-		 * @var FS_Logger
-		 * @since 1.0.3
-		 */
-		private static $_logger;
-
-		/**
-		 * @return FS_Security
-		 */
-		public static function instance() {
-			if ( ! isset( self::$_instance ) ) {
-				self::$_instance = new FS_Security();
-				self::$_logger   = FS_Logger::get_logger(
-					WP_FS__SLUG,
-					WP_FS__DEBUG_SDK,
-					WP_FS__ECHO_DEBUG_SDK
-				);
-			}
-
-			return self::$_instance;
-		}
-
-		private function __construct() {
-		}
-
-		/**
-		 * @param FS_Scope_Entity $entity
-		 * @param int              $timestamp
-		 * @param string           $action
-		 *
-		 * @return string
-		 */
-		function get_secure_token( FS_Scope_Entity $entity, $timestamp, $action = '' ) {
-			return md5(
-				$timestamp .
-				$entity->id .
-				$entity->secret_key .
-				$entity->public_key .
-				$action
-			);
-		}
-
-		/**
-		 * @param FS_Scope_Entity $entity
-		 * @param int|bool         $timestamp
-		 * @param string           $action
-		 *
-		 * @return array
-		 */
-		function get_context_params( FS_Scope_Entity $entity, $timestamp = false, $action = '' ) {
-			if ( false === $timestamp ) {
-				$timestamp = time();
-			}
-
-			return array(
-				's_ctx_type'   => $entity->get_type(),
-				's_ctx_id'     => $entity->id,
-				's_ctx_ts'     => $timestamp,
-				's_ctx_secure' => $this->get_secure_token( $entity, $timestamp, $action ),
-			);
-		}
-
-		/**
-		 * Gets a sandbox trial token for a given plugin, plan, and trial timestamp.
-		 *
-		 * @param FS_Plugin      $plugin
-		 * @param FS_Plugin_Plan $plan
-		 * @param int            $trial_timestamp
-		 *
-		 * @return string
-		 */
-		function get_trial_token( FS_Plugin $plugin, FS_Plugin_Plan $plan, $trial_timestamp ) {
-			return md5(
-				$plugin->secret_key . $plugin->public_key .
-				$plan->trial_period .
-				$plan->id .
-				$trial_timestamp
-			);
-		}
-	}
+<?php
+	/**
+	 * @package     Freemius
+	 * @copyright   Copyright (c) 2015, Freemius, Inc.
+	 * @license     https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
+	 * @since       1.0.3
+	 */
+
+	if ( ! defined( 'ABSPATH' ) ) {
+		exit;
+	}
+
+	define( 'WP_FS__SECURITY_PARAMS_PREFIX', 's_' );
+
+	/**
+	 * Class FS_Security
+	 */
+	class FS_Security {
+		/**
+		 * @var FS_Security
+		 * @since 1.0.3
+		 */
+		private static $_instance;
+		/**
+		 * @var FS_Logger
+		 * @since 1.0.3
+		 */
+		private static $_logger;
+
+		/**
+		 * @return FS_Security
+		 */
+		public static function instance() {
+			if ( ! isset( self::$_instance ) ) {
+				self::$_instance = new FS_Security();
+				self::$_logger   = FS_Logger::get_logger(
+					WP_FS__SLUG,
+					WP_FS__DEBUG_SDK,
+					WP_FS__ECHO_DEBUG_SDK
+				);
+			}
+
+			return self::$_instance;
+		}
+
+		private function __construct() {
+		}
+
+		/**
+		 * @param FS_Scope_Entity $entity
+		 * @param int              $timestamp
+		 * @param string           $action
+		 *
+		 * @return string
+		 */
+		function get_secure_token( FS_Scope_Entity $entity, $timestamp, $action = '' ) {
+			return md5(
+				$timestamp .
+				$entity->id .
+				$entity->secret_key .
+				$entity->public_key .
+				$action
+			);
+		}
+
+		/**
+		 * @param FS_Scope_Entity $entity
+		 * @param int|bool         $timestamp
+		 * @param string           $action
+		 *
+		 * @return array
+		 */
+		function get_context_params( FS_Scope_Entity $entity, $timestamp = false, $action = '' ) {
+			if ( false === $timestamp ) {
+				$timestamp = time();
+			}
+
+			return array(
+				's_ctx_type'   => $entity->get_type(),
+				's_ctx_id'     => $entity->id,
+				's_ctx_ts'     => $timestamp,
+				's_ctx_secure' => $this->get_secure_token( $entity, $timestamp, $action ),
+			);
+		}
+
+		/**
+		 * Gets a sandbox trial token for a given plugin, plan, and trial timestamp.
+		 *
+		 * @param FS_Plugin      $plugin
+		 * @param FS_Plugin_Plan $plan
+		 * @param int            $trial_timestamp
+		 *
+		 * @return string
+		 */
+		function get_trial_token( FS_Plugin $plugin, FS_Plugin_Plan $plan, $trial_timestamp ) {
+			return md5(
+				$plugin->secret_key . $plugin->public_key .
+				$plan->trial_period .
+				$plan->id .
+				$trial_timestamp
+			);
+		}
+	}
--- a/ultimeter/freemius/includes/entities/class-fs-plugin-license.php
+++ b/ultimeter/freemius/includes/entities/class-fs-plugin-license.php
@@ -1,334 +1,334 @@
-<?php
-    /**
-     * @package     Freemius
-     * @copyright   Copyright (c) 2015, Freemius, Inc.
-     * @license     https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
-     * @since       1.0.5
-     */
-
-    if ( ! defined( 'ABSPATH' ) ) {
-        exit;
-    }
-
-    /**
-     * Class FS_Plugin_License
-     */
-    class FS_Plugin_License extends FS_Entity {
-
-        #region Properties
-
-        /**
-         * @var number
-         */
-        public $plugin_id;
-        /**
-         * @var number
-         */
-        public $user_id;
-        /**
-         * @var number
-         */
-        public $plan_id;
-        /**
-         * @author Leo Fajardo (@leorw)
-         * @since 2.3.0
-         *
-         * @var string
-         */
-        public $parent_plan_name;
-        /**
-         * @author Leo Fajardo (@leorw)
-         * @since 2.3.0
-         *
-         * @var string
-         */
-        public $parent_plan_title;
-        /**
-         * @author Leo Fajardo (@leorw)
-         * @since 2.3.0
-         *
-         * @var number
-         */
-        public $parent_license_id;
-        /**
-         * @author Leo Fajardo (@leorw)
-         * @since 2.4.0
-         *
-         * @var array
-         */
-        public $products;
-        /**
-         * @var number
-         */
-        public $pricing_id;
-        /**
-         * @var int|null
-         */
-        public $quota;
-        /**
-         * @var int
-         */
-        public $activated;
-        /**
-         * @var int
-         */
-        public $activated_local;
-        /**
-         * @var string
-         */
-        public $expiration;
-        /**
-         * @var string
-         */
-        public $secret_key;
-        /**
-         * @var bool
-         */
-        public $is_whitelabeled;
-        /**
-         * @var bool $is_free_localhost Defaults to true. If true, allow unlimited localhost installs with the same
-         *      license.
-         */
-        public $is_free_localhost;
-        /**
-         * @var bool $is_block_features Defaults to true. If false, don't block features after license expiry - only
-         *      block updates and support.
-         */
-        public $is_block_features;
-        /**
-         * @var bool
-         */
-        public $is_cancelled;
-
-        #endregion Properties
-
-        /**
-         * @param stdClass|bool $license
-         */
-        function __construct( $license = false ) {
-            parent::__construct( $license );
-        }
-
-        /**
-         * Get entity type.
-         *
-         * @return string
-         */
-        static function get_type() {
-            return 'license';
-        }
-
-        /**
-         * Check how many site activations left.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  1.0.5
-         *
-         * @return int
-         */
-        function left() {
-            if ( ! $this->is_features_enabled() ) {
-                return 0;
-            }
-
-            if ( $this->is_unlimited() ) {
-                return 999;
-            }
-
-            return ( $this->quota - $this->activated - ( $this->is_free_localhost ? 0 : $this->activated_local ) );
-        }
-
-        /**
-         * Check if single site license.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  1.1.8.1
-         *
-         * @return bool
-         */
-        function is_single_site() {
-            return ( is_numeric( $this->quota ) && 1 == $this->quota );
-        }
-
-        /**
-         * @author Vova Feldman (@svovaf)
-         * @since  1.0.5
-         *
-         * @return bool
-         */
-        function is_expired() {
-            return ! $this->is_lifetime() && ( strtotime( $this->expiration ) < WP_FS__SCRIPT_START_TIME );
-        }
-
-        /**
-         * Check if license is not expired.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  1.2.1
-         *
-         * @return bool
-         */
-        function is_valid() {
-            return ! $this->is_expired();
-        }
-
-        /**
-         * @author Vova Feldman (@svovaf)
-         * @since  1.0.6
-         *
-         * @return bool
-         */
-        function is_lifetime() {
-            return is_null( $this->expiration );
-        }
-
-        /**
-         * @author Vova Feldman (@svovaf)
-         * @since  1.2.0
-         *
-         * @return bool
-         */
-        function is_unlimited() {
-            return is_null( $this->quota );
-        }
-
-        /**
-         * Check if license is fully utilized.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  1.0.6
-         *
-         * @param bool|null $is_localhost
-         *
-         * @return bool
-         */
-        function is_utilized( $is_localhost = null ) {
-            if ( is_null( $is_localhost ) ) {
-                $is_localhost = WP_FS__IS_LOCALHOST_FOR_SERVER;
-            }
-
-            if ( $this->is_unlimited() ) {
-                return false;
-            }
-
-            return ! ( $this->is_free_localhost && $is_localhost ) &&
-                   ( $this->quota <= $this->activated + ( $this->is_free_localhost ? 0 : $this->activated_local ) );
-        }
-
-        /**
-         * Check if license can be activated.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  2.0.0
-         *
-         * @param bool|null $is_localhost
-         *
-         * @return bool
-         */
-        function can_activate( $is_localhost = null ) {
-            return ! $this->is_utilized( $is_localhost ) && $this->is_features_enabled();
-        }
-
-        /**
-         * Check if license can be activated on a given number of production and localhost sites.
-         *
-         * @author Vova Feldman (@svovaf)
-         * @since  2.0.0
-         *
-         * @param int $production_count
-         * @param int $localhost_count
-         *
-         * @return bool
-         */
-        function can_activate_bulk( $production_count, $localhost_count ) {
-            if ( $this->is_unlimited() ) {
-                return true;
-            }
-
-            /**
-             * For simplicity, the logic will work as following: when given X sites to activate the license on, if it's
-             * possible to activate on ALL of them, do the activation. If it's not possible to activate on ALL of them,
-             * do NOT activate on any of them.
-             */
-            return ( $this->quota >= $this->activated + $production_count + ( $this->is_free_localhost ? 

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
# Block reflected XSS via 'url' parameter in Freemius trial promotion flow
SecRule REQUEST_URI "@rx /wp-admin/admin.php" 
  "id:202413362,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:|vbscript:|data:|on[a-z]+=" 
    "t:lowercase,t:urlDecodeUni,chain"
    SecRule ARGS:page "@rx freemius|trial" 
      "t:lowercase"

Frequently Asked Questions

Trusted by Developers & Organizations

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