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

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

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

Analysis Overview

Atomic Edge analysis of CVE-2024-13362:
This vulnerability is a reflected DOM-based Cross-Site Scripting (XSS) flaw in the Freemius WordPress SDK (versions up to 2.10.1), which is bundled with the Pay For Post with WooCommerce plugin. The vulnerability allows unauthenticated attackers to inject arbitrary web scripts via the ‘url’ parameter. The CVSS score is 6.1 (Medium), indicating a significant risk for phishing and credential theft.

Root Cause:
The root cause lies in insufficient input sanitization and output escaping in the ‘trial_promotion_message’ filter within the Freemius SDK’s ‘class-freemius.php’ file. Specifically, the patch shows changes around line 24000 where a user-controlled ‘url’ parameter (likely part of the trial URL) is directly injected into a button HTML element. The vulnerable code constructs an ‘‘ tag with an ‘href’ attribute containing this unsanitized URL. An attacker can craft a URL containing JavaScript that, when the admin user clicks the trial button, executes in the context of the WordPress admin panel. The insufficient escaping means the attacker can break out of the attribute context and inject arbitrary attributes or script tags.

Exploitation:
An attacker would craft a malicious link containing a ‘url’ parameter (e.g., ‘https://victim-site.com/wp-admin/admin.php?page=freemius-trial&url=javascript:alert(1)’) and trick an authenticated administrator into clicking it. Alternatively, the attacker could use a standard XSS vector in the URL, such as ‘?url=’, if the parameter is reflected directly without proper encoding. The attack does not require authentication but relies on social engineering to get an admin user to interact with the crafted URL within the WordPress backend. The injected script runs in the admin’s browser, giving the attacker access to admin-level actions.

Patch Analysis:
The patch modifies how the trial promotion button is rendered. In the vulnerable code, the user-controlled URL was placed directly into an ‘href’ attribute of a button wrapped in an ‘
‘ tag. The fixed code restructures this into a ‘

‘ container with a ‘‘ element. More critically, the patch moves the ‘button’ variable outside the ‘apply_filters’ call, ensuring it is not passed through the filter that could contain user input. The key line ‘$message_text = $this->apply_filters( ‘trial_promotion_message’, “{$message} {$cc_string}” );’ now only passes the message and credit card string through the filter, not the button with the URL. This prevents attackers from manipulating the button’s URL via the filter hook.

Impact:
Successful exploitation allows an attacker to execute arbitrary JavaScript in the browser of an authenticated WordPress administrator. This can lead to session hijacking, theft of authentication cookies, defacement of the admin dashboard, or tricking the admin into performing actions like creating new admin users or modifying sensitive settings. Since the vulnerability is in the Freemius SDK, it affects thousands of plugins and themes that bundle it, making the potential attack surface very large. The reflected nature means the attack is triggered only when the victim interacts with the crafted link, but the impact is severe due to the admin-level context.

Differential between vulnerable and patched code

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

Code Diff
--- a/woocommerce-pay-per-post/freemius/wordpress-sdk/includes/class-freemius.php
+++ b/woocommerce-pay-per-post/freemius/wordpress-sdk/includes/class-freemius.php
@@ -1661,9 +1661,9 @@
             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
             ) {
-                $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,
@@ -1681,7 +1681,7 @@
          * @return bool
          */
         private function is_parallel_activation() {
-            return ! empty( $this->premium_plugin_basename_from_parallel_activation );
+            return ! empty( $this->_premium_plugin_basename_from_parallel_activation );
         }

         /**
@@ -5205,7 +5205,7 @@
                     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;
+                $this->_premium_plugin_basename_from_parallel_activation = $premium_basename;

                 if ( is_plugin_active( $premium_basename ) ) {
                     $is_premium = true;
@@ -24000,13 +24000,15 @@

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

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

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

                     foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
--- a/woocommerce-pay-per-post/freemius/wordpress-sdk/includes/class-fs-plugin-updater.php
+++ b/woocommerce-pay-per-post/freemius/wordpress-sdk/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/woocommerce-pay-per-post/freemius/wordpress-sdk/includes/entities/class-fs-plugin-plan.php
+++ b/woocommerce-pay-per-post/freemius/wordpress-sdk/includes/entities/class-fs-plugin-plan.php
@@ -13,7 +13,6 @@
 	/**
 	 * Class FS_Plugin_Plan
 	 *
-	 * @property FS_Pricing[] $pricing
 	 */
 	class FS_Plugin_Plan extends FS_Entity {

--- a/woocommerce-pay-per-post/freemius/wordpress-sdk/includes/entities/class-fs-site.php
+++ b/woocommerce-pay-per-post/freemius/wordpress-sdk/includes/entities/class-fs-site.php
@@ -10,16 +10,16 @@
         exit;
     }

-    /**
-     * @property int $blog_id
-     */
-    #[AllowDynamicProperties]
     class FS_Site extends FS_Scope_Entity {
         /**
          * @var number
          */
         public $site_id;
         /**
+         * @var int
+         */
+        public $blog_id;
+        /**
          * @var number
          */
         public $plugin_id;
@@ -231,6 +231,7 @@

             foreach ( $sandbox_wp_environment_domains as $domain) {
                 if (
+                    ( $host === $domain ) ||
                     fs_ends_with( $host, '.' . $domain ) ||
                     fs_ends_with( $host, '-' . $domain )
                 ) {
--- a/woocommerce-pay-per-post/freemius/wordpress-sdk/includes/entities/class-fs-user.php
+++ b/woocommerce-pay-per-post/freemius/wordpress-sdk/includes/entities/class-fs-user.php
@@ -48,6 +48,19 @@
 			parent::__construct( $user );
 		}

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

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

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

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

         private static $_added_sticky_javascript = false;
--- a/woocommerce-pay-per-post/freemius/wordpress-sdk/start.php
+++ b/woocommerce-pay-per-post/freemius/wordpress-sdk/start.php
@@ -15,7 +15,7 @@
 	 *
 	 * @var string
 	 */
-	$this_sdk_version = '2.10.0';
+	$this_sdk_version = '2.11.0';

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

@@ -108,15 +108,33 @@
         $is_current_sdk_from_parent_theme = $file_path == $themes_directory . '/' . get_template() . '/' . $theme_candidate_sdk_basename . '/' . basename( $file_path );
     }

+    $theme_name = null;
     if ( $is_current_sdk_from_active_theme ) {
-        $this_sdk_relative_path = '../' . $themes_directory_name . '/' . get_stylesheet() . '/' . $theme_candidate_sdk_basename;
+        $theme_name             = get_stylesheet();
+        $this_sdk_relative_path = '../' . $themes_directory_name . '/' . $theme_name . '/' . $theme_candidate_sdk_basename;
         $is_theme               = true;
     } else if ( $is_current_sdk_from_parent_theme ) {
-        $this_sdk_relative_path = '../' . $themes_directory_name . '/' . get_template() . '/' . $theme_candidate_sdk_basename;
+        $theme_name             = get_template();
+        $this_sdk_relative_path = '../' . $themes_directory_name . '/' . $theme_name . '/' . $theme_candidate_sdk_basename;
         $is_theme               = true;
     } else {
         $this_sdk_relative_path = plugin_basename( $fs_root_path );
         $is_theme               = false;
+
+        /**
+         * If this file was included from another plugin with lower SDK version, and if this plugin is symlinked, then we need to get the actual plugin path,
+         * as the value right now will be wrong, it will only remove the directory separator from the file_path.
+         *
+         * The check of `fs_find_direct_caller_plugin_file` determines that this file was indeed included by a different plugin than the main plugin.
+         */
+        if ( DIRECTORY_SEPARATOR . $this_sdk_relative_path === $fs_root_path && function_exists( 'fs_find_direct_caller_plugin_file' ) ) {
+            $original_plugin_dir_name = dirname( fs_find_direct_caller_plugin_file( $file_path ) );
+
+            // Remove everything before the original plugin directory name.
+            $this_sdk_relative_path = substr( $this_sdk_relative_path, strpos( $this_sdk_relative_path, $original_plugin_dir_name ) );
+
+            unset( $original_plugin_dir_name );
+        }
     }

 	if ( ! isset( $fs_active_plugins ) ) {
@@ -202,7 +220,7 @@
 	) {
 		if ( $is_theme ) {
             // Saving relative path and not only directory name as it could be a subfolder
-            $plugin_path = $this_sdk_relative_path;
+            $plugin_path = $theme_name;
 		} else {
 			$plugin_path = plugin_basename( fs_find_direct_caller_plugin_file( $file_path ) );
 		}
@@ -357,7 +375,7 @@
 		return;
 	}

-	if ( version_compare( $this_sdk_version, $fs_active_plugins->newest->version, '<' ) ) {
+	if ( isset( $fs_active_plugins->newest ) && version_compare( $this_sdk_version, $fs_active_plugins->newest->version, '<' ) ) {
 		$newest_sdk = $fs_active_plugins->plugins[ $fs_active_plugins->newest->sdk_path ];

 		$plugins_or_theme_dir_path = ( ! isset( $newest_sdk->type ) || 'theme' !== $newest_sdk->type ) ?
--- a/woocommerce-pay-per-post/includes/class-woocommerce-pay-per-post-helper.php
+++ b/woocommerce-pay-per-post/includes/class-woocommerce-pay-per-post-helper.php
@@ -698,4 +698,31 @@
         return $last_purchase_date;
     }

+    public static function customer_has_purchased_product( $user_id, $product_id_or_variation_id ) {
+        if ( empty( $product_id_or_variation_id ) || empty( $user_id ) ) {
+            return false;
+        }
+        global $wpdb;
+        $user_id = absint( $user_id );
+        $product_id_or_variation_id = absint( $product_id_or_variation_id );
+        // Get paid order statuses
+        $statuses = array_map( fn( $status ) => "wc-{$status}", wc_get_is_paid_statuses() );
+        $status_sql = "'" . implode( "','", $statuses ) . "'";
+        // Preformat for SQL
+        if ( AutomatticWooCommerceUtilitiesOrderUtil::custom_orders_table_usage_is_enabled() ) {
+            // HOPS enabled - Query modern order tables
+            $order_table = AutomatticWooCommerceInternalDataStoresOrdersOrdersTableDataStore::get_orders_table_name();
+            $query = $wpdb->prepare(
+                "ntttSELECT COUNT(*)ntttFROM {$wpdb->prefix}wc_order_product_lookupntttLEFT JOIN {$order_table} AS orders ON orders.id={$wpdb->prefix}wc_order_product_lookup.order_idntttLEFT JOIN {$wpdb->prefix}wc_customer_lookup ON {$wpdb->prefix}wc_customer_lookup.customer_id={$wpdb->prefix}wc_order_product_lookup.customer_idntttWHERE nttt{$wpdb->prefix}wc_customer_lookup.user_id= %d ntttAND ({$wpdb->prefix}wc_order_product_lookup.product_id = %d OR {$wpdb->prefix}wc_order_product_lookup.variation_id = %d)n            AND orders.status IN ({$status_sql})nttt",
+                $user_id,
+                $product_id_or_variation_id,
+                $product_id_or_variation_id
+            );
+        } else {
+            // Legacy WooCommerce - Query posts/meta tables
+            $query = $wpdb->prepare( "n            SELECT COUNT(*) n            FROM {$wpdb->prefix}posts AS ordersn            INNER JOIN {$wpdb->prefix}postmeta AS meta ON orders.ID = meta.post_idn            INNER JOIN {$wpdb->prefix}woocommerce_order_items AS items ON orders.ID = items.order_idn            INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS itemmeta ON items.order_item_id = itemmeta.order_item_idn            WHERE orders.post_type = 'shop_order'n            AND orders.post_status IN ({$status_sql})n            AND meta.meta_key = '_customer_user'n            AND meta.meta_value = %dn            AND (n                itemmeta.meta_key IN ('_product_id', '_variation_id')n                AND itemmeta.meta_value = %dn            )n        ", $user_id, $product_id_or_variation_id );
+        }
+        return $wpdb->get_var( $query ) > 0;
+    }
+
 }
--- a/woocommerce-pay-per-post/includes/class-woocommerce-pay-per-post-protection-checks.php
+++ b/woocommerce-pay-per-post/includes/class-woocommerce-pay-per-post-protection-checks.php
@@ -37,12 +37,12 @@
         foreach ( $product_ids as $id ) {
             if ( Woocommerce_Pay_Per_Post_Helper::can_use_woocommerce_subscriptions() ) {
                 $subscriptions = new WooCommerceSubscriptions();
-                if ( wc_customer_bought_product( $current_user->user_email, $current_user->ID, trim( $id ) ) && !$subscriptions->is_subscription_product( $id ) ) {
+                if ( Woocommerce_Pay_Per_Post_Helper::customer_has_purchased_product( $current_user->ID, trim( $id ) ) && !$subscriptions->is_subscription_product( $id ) ) {
                     Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . get_the_ID() . ' - Woocommerce_Pay_Per_Post_Protection_Checks/check_if_purchased  - WooSubscriptions Enabled and User has purchased product id #' . trim( $id ) . ' that is NOT a subscription product' );
                     return true;
                 }
             } else {
-                if ( wc_customer_bought_product( $current_user->user_email, $current_user->ID, trim( $id ) ) ) {
+                if ( Woocommerce_Pay_Per_Post_Helper::customer_has_purchased_product( $current_user->ID, trim( $id ) ) ) {
                     Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . get_the_ID() . ' - Woocommerce_Pay_Per_Post_Protection_Checks/check_if_purchased  - User has purchased product id #' . trim( $id ) );
                     return true;
                 }
@@ -106,28 +106,43 @@
     }

     public static function check_if_post_contains_subscription_products( $post_id, $product_ids ) : bool {
-        $subscriptions = new WooCommerceSubscriptions();
-        return $subscriptions->post_contains_subscription_products( $post_id, $product_ids );
+        if ( Woocommerce_Pay_Per_Post_Helper::can_use_woocommerce_subscriptions() ) {
+            $subscriptions = new WooCommerceSubscriptions();
+            return $subscriptions->post_contains_subscription_products( $post_id, $product_ids );
+        }
+        return false;
     }

     public static function check_if_product_is_a_subscription_product( $id ) : bool {
-        return WC_Subscriptions_Product::is_subscription( $id );
+        if ( Woocommerce_Pay_Per_Post_Helper::can_use_woocommerce_subscriptions() ) {
+            return WC_Subscriptions_Product::is_subscription( $id );
+        }
+        return false;
     }

     public static function check_if_product_is_a_membership_product( $id ) : bool {
-        return array_key_exists( (int) $id, wc_memberships_get_membership_plans() );
+        if ( Woocommerce_Pay_Per_Post_Helper::can_use_woocommerce_memberships() ) {
+            return array_key_exists( (int) $id, wc_memberships_get_membership_plans() );
+        }
+        return false;
     }

     public static function check_if_post_contains_membership_products( $id, $product_ids ) : bool {
-        $memberships = new WooCommerceMemberships();
-        Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . get_the_ID() . ' - Woocommerce_Pay_Per_Post_Protection_Checks/check_if_post_contains_membership_products  - Does Post Contain Membership Products? - ' . (( $memberships->post_contains_membership_products( $id ) ? 'true' : 'false' )) );
-        return $memberships->post_contains_membership_products( $id, $product_ids );
+        if ( Woocommerce_Pay_Per_Post_Helper::can_use_woocommerce_memberships() ) {
+            $memberships = new WooCommerceMemberships();
+            Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . get_the_ID() . ' - Woocommerce_Pay_Per_Post_Protection_Checks/check_if_post_contains_membership_products  - Does Post Contain Membership Products? - ' . (( $memberships->post_contains_membership_products( $id ) ? 'true' : 'false' )) );
+            return $memberships->post_contains_membership_products( $id, $product_ids );
+        }
+        return false;
     }

     public static function check_if_post_contains_paid_memberships_pro_membership_products( $post_id, $product_ids ) : bool {
-        $pmp = new PaidMembershipsPro();
-        Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . get_the_ID() . ' - Woocommerce_Pay_Per_Post_Protection_Checks/check_if_post_contains_paid_memberships_pro_membership_products  - Does Post Contain Paid Membership Pro Membership Products? - ' . (( $pmp->post_contains_membership_products( $id ) ? 'true' : 'false' )) );
-        return $pmp->post_contains_membership_products( $post_id, $product_ids );
+        if ( Woocommerce_Pay_Per_Post_Helper::can_use_paid_membership_pro() ) {
+            $pmp = new PaidMembershipsPro();
+            Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . get_the_ID() . ' - Woocommerce_Pay_Per_Post_Protection_Checks/check_if_post_contains_paid_memberships_pro_membership_products  - Does Post Contain Paid Membership Pro Membership Products? - ' . (( $pmp->post_contains_membership_products( $id ) ? 'true' : 'false' )) );
+            return $pmp->post_contains_membership_products( $post_id, $product_ids );
+        }
+        return false;
     }

 }
--- a/woocommerce-pay-per-post/public/class-woocommerce-pay-per-post-restrict-content.php
+++ b/woocommerce-pay-per-post/public/class-woocommerce-pay-per-post-restrict-content.php
@@ -127,31 +127,38 @@
     }

     public function check_if_purchased() : bool {
-        Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased  - Called. - BACKTRACE - Called From - ' . print_r( debug_backtrace()[1]['function'], true ) );
-        Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased  - Products associated with page = ' . print_r( $this->product_ids['product_ids'], true ) );
-        if ( empty( $this->product_ids['product_ids'] ) ) {
-            $this->product_ids['product_ids'] = [];
-        }
+        Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased - Called.' );
+        // Log associated products
+        Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased - Products associated with page = ' . print_r( $this->product_ids['product_ids'] ?? [], true ) );
+        // Merge section product IDs if they exist
+        $product_ids = (array) $this->product_ids['product_ids'] ?? [];
         if ( !empty( $this->product_ids['section_product_ids'] ) ) {
-            $this->product_ids['product_ids'] = array_merge( $this->product_ids['product_ids'], $this->product_ids['section_product_ids'] );
+            $product_ids = array_merge( $product_ids, $this->product_ids['section_product_ids'] );
+        }
+        // If there are no product IDs, return false
+        if ( empty( $product_ids ) ) {
+            Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased - No products found for this page.' );
+            return false;
         }
-        if ( !empty( $this->product_ids['product_ids'] ) && count( (array) $this->product_ids['product_ids'] ) > 0 ) {
-            foreach ( $this->product_ids['product_ids'] as $id ) {
-                Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased  - Checking to see if user has purchased product #' . print_r( $id, true ) );
-                if ( Woocommerce_Pay_Per_Post_Helper::can_use_woocommerce_subscriptions() && $this->integrations['woocommerce-subscriptions']->is_subscription_product( $id ) ) {
-                    if ( wc_customer_bought_product( $this->current_user->user_email, $this->current_user->ID, trim( $id ) ) && $this->check_if_is_subscriber() ) {
-                        Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased  - User has purchased product id #' . trim( $id ) );
-                        return true;
-                    }
-                } else {
-                    if ( wc_customer_bought_product( $this->current_user->user_email, $this->current_user->ID, trim( $id ) ) ) {
-                        Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased  - User has purchased product id #' . trim( $id ) );
-                        return true;
-                    }
-                }
-                Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased  - User has NOT purchased product id #' . trim( $id ) );
+        // Loop through product IDs to check ownership
+        foreach ( $product_ids as $id ) {
+            $id = trim( $id );
+            // ID sanitization
+            Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased - Checking product ID: ' . $id );
+            // Check for subscription product and purchase
+            if ( Woocommerce_Pay_Per_Post_Helper::can_use_woocommerce_subscriptions() && $this->integrations['woocommerce-subscriptions']->is_subscription_product( $id ) && Woocommerce_Pay_Per_Post_Helper::customer_has_purchased_product( $this->current_user->ID, $id ) && $this->check_if_is_subscriber() ) {
+                Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased - User has purchased subscription product ID: ' . $id );
+                return true;
+            }
+            // Check for regular product purchase
+            if ( Woocommerce_Pay_Per_Post_Helper::customer_has_purchased_product( $this->current_user->ID, $id ) ) {
+                Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased - User has purchased product ID: ' . $id );
+                return true;
             }
+            Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased - User has NOT purchased product ID: ' . $id );
         }
+        // If no purchases are found
+        Woocommerce_Pay_Per_Post_Helper::logger( 'Post ID: ' . $this->post_id . ' - Woocommerce_Pay_Per_Post_Restrict_Content/check_if_purchased - User has not purchased any associated products.' );
         return false;
     }

--- a/woocommerce-pay-per-post/vendor-prefixed/nesbot/carbon/src/Carbon/AbstractTranslator.php
+++ b/woocommerce-pay-per-post/vendor-prefixed/nesbot/carbon/src/Carbon/AbstractTranslator.php
@@ -159,6 +159,8 @@
             return true;
         }

+        $this->assertValidLocale($locale);
+
         foreach ($this->getDirectories() as $directory) {
             $data = @include sprintf('%s/%s.php', rtrim($directory, '\/'), $locale);

--- a/woocommerce-pay-per-post/woocommerce-pay-per-post.php
+++ b/woocommerce-pay-per-post/woocommerce-pay-per-post.php
@@ -10,9 +10,9 @@
  * Plugin Name: Pay For Post with WooCommerce
  * Plugin URI:              pramadillo.com/plugins/woocommerce-pay-per-post
  * Description:             Allows for the sale of a specific post/page in WordPress through WooCommerce.
- * Version:                 3.1.26
+ * Version:                 3.1.28
  * WC requires at least:    2.6
- * WC tested up to:         9.4.2
+ * WC tested up to:         9.5.2
  * Elementor tested up to: 3.25.10
  * Elementor Pro tested up to: 3.25.4
  * Author:                  Pramadillo
@@ -25,7 +25,7 @@
 if ( !defined( 'ABSPATH' ) ) {
     exit;
 }
-const WC_PPP_VERSION = '3.1.26';
+const WC_PPP_VERSION = '3.1.28';
 const WC_PPP_SLUG = 'wc_pay_per_post';
 const WC_PPP_NAME = 'Pay For Post with WooCommerce';
 const WC_PPP_TEMPLATE_PATH = 'woocommerce-pay-per-post/';

Frequently Asked Questions

Trusted by Developers & Organizations

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