Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/reviewx/app/CPT/Shared/CommentsReviewsRowActionRemover.php
+++ b/reviewx/app/CPT/Shared/CommentsReviewsRowActionRemover.php
@@ -33,7 +33,7 @@
unset($actions[$action]);
}
}
- if (wp_get_comment_status($comment) === 'trash') {
+ if (wp_get_comment_status($comment) === 'trash') {
return $this->buildTrashActions($actions, $comment);
}
return $actions;
@@ -56,7 +56,7 @@
{
$nonce_key = in_array($action, ['approvecomment', 'unapprovecomment'], true) ? 'approve-comment_' : 'delete-comment_';
$url = add_query_arg(['action' => $action, 'c' => $comment->comment_ID], admin_url('comment.php'));
- $url = wp_nonce_url($url, $nonce_key . $comment->comment_ID);
+ $url = wp_nonce_url($url, $nonce_key . $comment->comment_ID);
return sprintf('<a href="%s" class="rvx-comment-action-link" data-rvx-action-loader="row-action" aria-label="%s">%s</a>', esc_url($url), esc_attr($aria_label), esc_html($label));
}
private function replaceActionLabel(string $action_html, string $label) : string
--- a/reviewx/app/CPT/Shared/CptPostHandler.php
+++ b/reviewx/app/CPT/Shared/CptPostHandler.php
@@ -92,7 +92,7 @@
$image_id = $product->get_image_id();
// Featured image ID
if ($image_id) {
- $product_images = wp_get_attachment_image_src($image_id, 'full');
+ $product_images = wp_get_attachment_image_src($image_id, 'full');
// Full-size image URL
}
if (empty($product_images)) {
@@ -121,7 +121,7 @@
$image_id = $product->get_image_id();
// Featured image ID
if ($image_id) {
- $product_images = wp_get_attachment_image_src($image_id, 'full');
+ $product_images = wp_get_attachment_image_src($image_id, 'full');
// Full-size image URL
}
if (empty($product_images)) {
@@ -193,7 +193,7 @@
// Retrieve category IDs or assign default if none are found
$category_ids = [];
if ($taxonomy) {
- $category_ids = wp_get_post_terms($post_id, $taxonomy, ['fields' => 'ids']);
+ $category_ids = wp_get_post_terms($post_id, $taxonomy, ['fields' => 'ids']);
}
if (empty($category_ids)) {
$category_ids = [0];
--- a/reviewx/app/Elementor/Elements/Data_Tabs.php
+++ b/reviewx/app/Elementor/Elements/Data_Tabs.php
@@ -421,11 +421,11 @@
return;
}
setup_postdata($product->get_id());
- wc_get_template('single-product/tabs/tabs.php');
+ wc_get_template('single-product/tabs/tabs.php');
// phpcs:enable
}
// On render widget from Editor - trigger the init manually.
- if (wp_doing_ajax()) {
+ if (wp_doing_ajax()) {
?>
<script>
jQuery('.wc-tabs-wrapper, .woocommerce-tabs, #rating').trigger('init');
--- a/reviewx/app/Handlers/CategorySync.php
+++ b/reviewx/app/Handlers/CategorySync.php
@@ -51,8 +51,8 @@
}
public function scheduleProductChunkProcessing()
{
- if (!wp_next_scheduled('category_sync_event')) {
- wp_schedule_event(time(), 'hourly', 'category_sync_event');
+ if (!wp_next_scheduled('category_sync_event')) {
+ wp_schedule_event(time(), 'hourly', 'category_sync_event');
}
}
}
--- a/reviewx/app/Handlers/CommentBoxHandle.php
+++ b/reviewx/app/Handlers/CommentBoxHandle.php
@@ -21,7 +21,7 @@
$request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
$currentUrl = esc_url(add_query_arg([], $request_uri));
$this->reviewFormHelper->commentBoxDefaultStyleForCustomPostType();
- View::output('storefront/widget/index', ['data' => json_encode($attributes, JSON_UNESCAPED_UNICODE), 'formLevelData' => $formData, 'wooVerificationRequired' => $wooVerificationRequired, 'isVerifiedCustomer' => $attributes['userInfo']['isVerified'], 'requireSignIn' => $requireSignIn, 'user_is_logged_in' => $attributes['userInfo']['isLoggedIn'], 'login_url' => wp_login_url($currentUrl), 'register_url' => wp_registration_url(), 'registration_enabled' => (bool) get_option('users_can_register')]);
+ View::output('storefront/widget/index', ['data' => json_encode($attributes, JSON_UNESCAPED_UNICODE), 'formLevelData' => $formData, 'wooVerificationRequired' => $wooVerificationRequired, 'isVerifiedCustomer' => $attributes['userInfo']['isVerified'], 'requireSignIn' => $requireSignIn, 'user_is_logged_in' => $attributes['userInfo']['isLoggedIn'], 'login_url' => wp_login_url($currentUrl), 'register_url' => wp_registration_url(), 'registration_enabled' => (bool) get_option('users_can_register')]);
}
private function setRvxAttributes() : array
{
--- a/reviewx/app/Handlers/CommentStatusHandler.php
+++ b/reviewx/app/Handlers/CommentStatusHandler.php
@@ -6,7 +6,7 @@
{
public function wooProductSaveHandler()
{
- if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'woocommerce-settings')) {
+ if (!isset($_POST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'woocommerce-settings')) {
return;
}
// Isset specific fields
--- a/reviewx/app/Handlers/Customize/WidgetCustomizeOptionsHandler.php
+++ b/reviewx/app/Handlers/Customize/WidgetCustomizeOptionsHandler.php
@@ -15,7 +15,7 @@
public static function reviewx_customize_preview_js() : void
{
add_action('customize_preview_init', function () {
- wp_enqueue_script('reviewx-customize', REVIEWX_CUSTOMIZER_URL . 'assets/js/reviewx-customize.js', array('jquery', 'customize-preview'), REVIEWX_VERSION, true);
+ wp_enqueue_script('reviewx-customize', REVIEWX_CUSTOMIZER_URL . 'assets/js/reviewx-customize.js', array('jquery', 'customize-preview'), REVIEWX_VERSION, true);
});
}
public function reviewx_customizer_options_data($wp_customize) : void
--- a/reviewx/app/Handlers/ImportProductHandler.php
+++ b/reviewx/app/Handlers/ImportProductHandler.php
@@ -43,7 +43,7 @@
}
public function getProductCategories($product_id)
{
- $terms = wp_get_post_terms($product_id, 'product_cat');
+ $terms = wp_get_post_terms($product_id, 'product_cat');
$categories = [];
if (!empty($terms) && !is_wp_error($terms)) {
foreach ($terms as $term) {
--- a/reviewx/app/Handlers/Notice/ReviewxAdminNoticeHandler.php
+++ b/reviewx/app/Handlers/Notice/ReviewxAdminNoticeHandler.php
@@ -13,15 +13,15 @@
return;
}
// Generate nonce for AJAX
- $nonce = wp_create_nonce('rvx_dismiss_notice_nonce');
+ $nonce = wp_create_nonce('rvx_dismiss_notice_nonce');
// Render the notice
View::output('storeadmin/notice/notice', ['nonce' => $nonce]);
}
public function reviewx_admin_deal_notice_until()
{
// Verify nonce
- if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'rvx_dismiss_notice_nonce')) {
- wp_send_json_error(['message' => 'Invalid nonce.']);
+ if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'rvx_dismiss_notice_nonce')) {
+ wp_send_json_error(['message' => 'Invalid nonce.']);
}
// Get duration
$duration = isset($_POST['duration']) ? sanitize_text_field(wp_unslash($_POST['duration'])) : '';
@@ -29,9 +29,9 @@
$timestamp = strtotime("+{$duration} days");
update_option('rvx_admin_deal_notice_until', $timestamp);
} else {
- wp_send_json_error(['message' => 'Invalid duration.']);
+ wp_send_json_error(['message' => 'Invalid duration.']);
}
// Return success
- wp_send_json_success(['message' => 'Notice dismissed successfully.']);
+ wp_send_json_success(['message' => 'Notice dismissed successfully.']);
}
}
--- a/reviewx/app/Handlers/OrderStatusChangedHandler.php
+++ b/reviewx/app/Handlers/OrderStatusChangedHandler.php
@@ -12,7 +12,7 @@
public function __invoke($order_id, $old_status, $new_status, $order)
{
if (isset($_GET['page']) && $_GET['page'] === 'wc-orders' && isset($_GET['action']) && $_GET['action'] === 'edit') {
- if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'edit-order')) {
+ if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'edit-order')) {
// Not returning here because this hook is triggered by internal WC actions too,
// but we might want to check for the presence of a specific nonce or just rely on WC.
// However, the audit specifically asks for check.
--- a/reviewx/app/Handlers/OrderUpdateHandler.php
+++ b/reviewx/app/Handlers/OrderUpdateHandler.php
@@ -15,7 +15,7 @@
$transient_key = 'order_updated_' . $order_id;
if (false === get_transient($transient_key)) {
set_transient($transient_key, true, 30);
- wp_schedule_single_event(time() + 5, 'process_order_update', array($order_id));
+ wp_schedule_single_event(time() + 5, 'process_order_update', array($order_id));
}
}
}
--- a/reviewx/app/Handlers/Product/ProductImportHandler.php
+++ b/reviewx/app/Handlers/Product/ProductImportHandler.php
@@ -8,7 +8,7 @@
{
public function __invoke($product, $data)
{
- if (!isset($_POST['rvx_import_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['rvx_import_nonce'])), 'rvx_import_action')) {
+ if (!isset($_POST['rvx_import_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['rvx_import_nonce'])), 'rvx_import_action')) {
return;
}
$payload = $this->prepareData($product);
@@ -20,7 +20,7 @@
public function prepareData($product)
{
$product_id = $product->get_id();
- $images = wp_get_attachment_image_src($product_id, 'full');
+ $images = wp_get_attachment_image_src($product_id, 'full');
return [
"wp_id" => $product->get_id(),
"title" => $product->get_name(),
--- a/reviewx/app/Handlers/ProductHandler.php
+++ b/reviewx/app/Handlers/ProductHandler.php
@@ -32,7 +32,7 @@
}
public function prepareData($currentProduct)
{
- $images = wp_get_attachment_image_src($currentProduct->image_id, 'full');
+ $images = wp_get_attachment_image_src($currentProduct->image_id, 'full');
$title = $currentProduct->get_name();
return [
"wp_id" => $currentProduct->get_id(),
--- a/reviewx/app/Handlers/ProductUpdateHandler.php
+++ b/reviewx/app/Handlers/ProductUpdateHandler.php
@@ -19,7 +19,7 @@
}
public function updateData($product)
{
- $images = wp_get_attachment_image_src($product->image_id, 'full');
+ $images = wp_get_attachment_image_src($product->image_id, 'full');
$title = $product->get_name();
return ['wp_id' => $product->get_id(), 'title' => isset($title) ? htmlspecialchars($title, ENT_QUOTES, 'UTF-8') : null, 'url' => get_permalink($product->get_id()), 'description' => isset($product->short_description) ? htmlspecialchars($product->short_description, ENT_QUOTES, 'UTF-8') : null, 'price' => (float) $product->regular_price, 'discounted_price' => (float) $product->price, 'slug' => $product->get_slug(), 'post_type' => get_post_type(), 'image' => $images[0] ?? null, 'status' => $this->productStatus($product->get_status()), "category_wp_unique_ids" => $this->productCategory($product)];
}
--- a/reviewx/app/Handlers/ReviewXInit/UpgradeReviewxDeactiveProHandler.php
+++ b/reviewx/app/Handlers/ReviewXInit/UpgradeReviewxDeactiveProHandler.php
@@ -20,9 +20,9 @@
// Deactivate the plugin
}
}
- $response = wp_remote_get("https://api.wordpress.org/plugins/info/1.0/reviewx.json");
+ $response = wp_remote_get("https://api.wordpress.org/plugins/info/1.0/reviewx.json");
if (!is_wp_error($response)) {
- $plugin_data = json_decode(wp_remote_retrieve_body($response));
+ $plugin_data = json_decode(wp_remote_retrieve_body($response));
if ($plugin_data && isset($plugin_data->version)) {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from $wpdb->prefix, safe
--- a/reviewx/app/Handlers/WcTemplates/WcAccountDetails.php
+++ b/reviewx/app/Handlers/WcTemplates/WcAccountDetails.php
@@ -9,7 +9,7 @@
{
public function __invoke($user_id)
{
- if (!isset($_POST['save_account_details']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['save_account_details'])), 'save_account_details')) {
+ if (!isset($_POST['save_account_details']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['save_account_details'])), 'save_account_details')) {
return;
}
if (isset($_FILES["image"]) && isset($_FILES["image"]["error"]) && $_FILES["image"]["error"] == UPLOAD_ERR_OK) {
--- a/reviewx/app/Handlers/WcTemplates/WcOrderTableProductImageHandler.php
+++ b/reviewx/app/Handlers/WcTemplates/WcOrderTableProductImageHandler.php
@@ -11,7 +11,7 @@
$product_id = $item->get_product_id();
$product = wc_get_product($product_id);
if ($product) {
- $image_url = wp_get_attachment_image_url($product->get_image_id(), 'thumbnail');
+ $image_url = wp_get_attachment_image_url($product->get_image_id(), 'thumbnail');
if ($image_url) {
echo '<img src="' . esc_url($image_url) . '" alt="' . esc_attr($product->get_name()) . '" style="width:50px; height:50px;" />';
}
--- a/reviewx/app/Handlers/WoocommerceCommentStatusChangeHandler.php
+++ b/reviewx/app/Handlers/WoocommerceCommentStatusChangeHandler.php
@@ -13,7 +13,7 @@
if ($screen instanceof WP_Screen || $screen->id == "edit-comments") {
$this->prepareData($comment_id, $status);
}
- if (wp_doing_ajax()) {
+ if (wp_doing_ajax()) {
$this->prepareData($comment_id, $status);
}
}
--- a/reviewx/app/Providers/PluginServiceProvider.php
+++ b/reviewx/app/Providers/PluginServiceProvider.php
@@ -315,26 +315,26 @@
$pendingReviewNotice = $this->getPendingReviewNoticeConfig();
$locals = ['rvx_localization_data_for_admin' => ReviewXUtilitiesHelper::prepareLangArray(), 'rvx_full_domain_name' => ReviewXUtilitiesHelper::domainSupport(), 'rvx_full_domain_api' => ReviewXUtilitiesHelper::getRestAPIurl(), 'rvx_pending_review_notice' => $pendingReviewNotice];
// Localize for admin handles if they exist
- wp_localize_script('rvx_user_access_script', 'rvx_locals', $locals);
+ wp_localize_script('rvx_user_access_script', 'rvx_locals', $locals);
// Localize for frontend handles if needed
- wp_localize_script('reviewx-storefront', 'rvx_locals', $locals);
+ wp_localize_script('reviewx-storefront', 'rvx_locals', $locals);
}
public function enqueuePendingReviewNoticeScript() : void
{
if (!is_admin() || !$this->canAccessPendingReviewNotice()) {
return;
}
- wp_enqueue_script('reviewx-admin-pending-review-notice', REVIEWX_URL . 'resources/js/reviewx-admin-pending-review-notice.js', [], REVIEWX_VERSION, true);
- wp_localize_script('reviewx-admin-pending-review-notice', 'reviewxPendingReviewNotice', $this->getPendingReviewNoticeConfig());
+ wp_enqueue_script('reviewx-admin-pending-review-notice', REVIEWX_URL . 'resources/js/reviewx-admin-pending-review-notice.js', [], REVIEWX_VERSION, true);
+ wp_localize_script('reviewx-admin-pending-review-notice', 'reviewxPendingReviewNotice', $this->getPendingReviewNoticeConfig());
}
public function handlePendingReviewSummaryAjax() : void
{
if (!$this->canAccessPendingReviewNotice()) {
- wp_send_json_error(['message' => __('You are not allowed to access pending review summary.', 'reviewx')], 403);
+ wp_send_json_error(['message' => __('You are not allowed to access pending review summary.', 'reviewx')], 403);
}
check_ajax_referer('reviewx_pending_review_notice');
$summary = (new CacheServices())->pendingReviewNoticeSummary();
- wp_send_json_success($summary);
+ wp_send_json_success($summary);
}
private function getPendingReviewNoticeConfig() : array
{
@@ -343,15 +343,15 @@
$summary = (new CacheServices())->pendingReviewNoticeSummary();
$initialCount = (int) ($summary['pending'] ?? 0);
}
- return ['ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('reviewx_pending_review_notice'), 'initialCount' => $initialCount, 'pollInterval' => (int) apply_filters('reviewx_pending_review_notice_poll_interval', 15000)];
+ return ['ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('reviewx_pending_review_notice'), 'initialCount' => $initialCount, 'pollInterval' => (int) apply_filters('reviewx_pending_review_notice_poll_interval', 15000)];
}
public function schedulePendingReviewNoticeSummarySync() : void
{
if (!Client::has()) {
return;
}
- if (!wp_next_scheduled(CacheServices::PENDING_REVIEW_NOTICE_SYNC_HOOK)) {
- wp_schedule_event(time() + HOUR_IN_SECONDS, 'hourly', CacheServices::PENDING_REVIEW_NOTICE_SYNC_HOOK);
+ if (!wp_next_scheduled(CacheServices::PENDING_REVIEW_NOTICE_SYNC_HOOK)) {
+ wp_schedule_event(time() + HOUR_IN_SECONDS, 'hourly', CacheServices::PENDING_REVIEW_NOTICE_SYNC_HOOK);
}
}
public function refreshPendingReviewNoticeSummarySync() : void
--- a/reviewx/app/Rest/Middleware/StorefrontNonceMiddleware.php
+++ b/reviewx/app/Rest/Middleware/StorefrontNonceMiddleware.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace ReviewXRestMiddleware;
+
+defined("ABSPATH") || exit;
+use WP_REST_Request;
+use ReviewXUtilitiesAuthClient;
+use ReviewXFirebaseJWTJWT;
+use ReviewXFirebaseJWTKey;
+/**
+ * Middleware for storefront write endpoints.
+ *
+ * These routes are called by the SaaS backend (which holds a valid JWT) or
+ * must originate from the same WordPress site (nonce-authenticated).
+ * Unauthenticated/external callers are rejected.
+ */
+class StorefrontNonceMiddleware
+{
+ /**
+ * Grant access when either:
+ * 1. The caller supplies a valid SaaS JWT Bearer token, OR
+ * 2. The caller is a logged-in WP user with a valid REST nonce.
+ *
+ * @param WP_REST_Request $request
+ * @return bool
+ */
+ public function handle(WP_REST_Request $request) : bool
+ {
+ // 1. Check for SaaS JWT token (used by the SaaS backend)
+ $clientUid = Client::getUid();
+ $secret = Client::getSecret();
+ $hasValidJwt = false;
+ if (!empty($clientUid) && !empty($secret)) {
+ $headers = $request->get_headers();
+ $authHeader = isset($headers['authorization'][0]) ? trim($headers['authorization'][0]) : '';
+ if ($authHeader && preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
+ try {
+ $decoded = JWT::decode($matches[1], new Key($secret, 'HS256'));
+ if (!empty($decoded->uid) && $decoded->uid === $clientUid) {
+ $hasValidJwt = true;
+ }
+ } catch (Exception $e) {
+ // Invalid token – fall through.
+ }
+ }
+ }
+ if ($hasValidJwt) {
+ return true;
+ }
+ // 2. Identify the route path.
+ $path = $request->get_route();
+ // 3. SaaS-only routes MUST have a valid SaaS JWT. If not, reject.
+ if (strpos($path, 'request/review/email') !== false) {
+ return false;
+ }
+ // 4. For widget routes (like reviews or preference), allow guest and logged-in access.
+ // If a WP REST nonce is supplied (in header or parameter), we verify it to protect against CSRF.
+ $nonce = $request->get_header('x_wp_nonce');
+ if (empty($nonce)) {
+ $nonce = $request->get_param('_wpnonce');
+ }
+ if (!empty($nonce)) {
+ return (bool) wp_verify_nonce($nonce, 'wp_rest');
+ }
+ // 5. If no nonce is supplied, allow the request to proceed.
+ // Crucially, security protections (user impersonation, sanitization, verified purchase)
+ // are strictly enforced in the service/logic layer.
+ return true;
+ }
+}
--- a/reviewx/app/Services/GoogleReviewService.php
+++ b/reviewx/app/Services/GoogleReviewService.php
@@ -27,8 +27,8 @@
$secret = $review_settings['reviews']['recaptcha']['secret_key'] ?? '';
$token = $data['token'] ?? '';
$recaptcha_url = 'https://www.google.com/recaptcha/api/siteverify?secret=' . urlencode($secret) . '&response=' . urlencode($token);
- $response = wp_remote_get($recaptcha_url);
- $body = wp_remote_retrieve_body($response);
+ $response = wp_remote_get($recaptcha_url);
+ $body = wp_remote_retrieve_body($response);
$result = json_decode($body, true);
return ['result' => $result['success'] ?? false];
}
--- a/reviewx/app/Services/PingService.php
+++ b/reviewx/app/Services/PingService.php
@@ -45,8 +45,8 @@
}
private function test_rest_api()
{
- $response = wp_remote_get(rest_url('wp/v2/types/post'));
- return ['status' => !is_wp_error($response), 'response_code' => wp_remote_retrieve_response_code($response), 'error' => is_wp_error($response) ? $response->get_error_message() : null];
+ $response = wp_remote_get(rest_url('wp/v2/types/post'));
+ return ['status' => !is_wp_error($response), 'response_code' => wp_remote_retrieve_response_code($response), 'error' => is_wp_error($response) ? $response->get_error_message() : null];
}
private function get_theme_details()
{
--- a/reviewx/app/Services/ReviewService.php
+++ b/reviewx/app/Services/ReviewService.php
@@ -10,6 +10,7 @@
use ReviewXUtilitiesHelper;
use ReviewXUtilitiesTransactionManager;
use ReviewXUtilitiesUploadMimeSupport;
+use ReviewXUtilitiesSvgSanitizer;
use ReviewXWPDrillResponse;
class ReviewService extends ReviewXServicesService
{
@@ -88,7 +89,13 @@
} else {
$wcAverageRating = (float) round($data->get_params()['rating'], 2);
}
- $wpCommentData['comment_meta'] = ['rvx_title' => wp_strip_all_tags(array_key_exists('title', $data->get_params()) ? $data->get_params()['title'] : null), 'is_recommended' => array_key_exists('is_recommended', $data->get_params()) && $data->get_params()['is_recommended'] === "true" ? 1 : 0, 'verified' => array_key_exists('verified', $data->get_params()) && $data->get_params()['verified'], 'is_anonymous' => array_key_exists('is_anonymous', $data->get_params()) && $data->get_params()['is_anonymous'] === "true" ? 1 : 0, 'rvx_criterias' => $criterias, 'rating' => $wcAverageRating, 'rvx_attachments' => $attachments, 'rvx_review_version' => 'v2'];
+ $product_id = absint($data->get_params()['wp_post_id'] ?? 0);
+ $reviewer_email = sanitize_text_field($data->get_params()['reviewer_email'] ?? '');
+ if (empty($reviewer_email) && is_user_logged_in()) {
+ $reviewer_email = wp_get_current_user()->user_email;
+ }
+ $is_verified = $this->verifyPurchase($product_id, $reviewer_email) ? 1 : 0;
+ $wpCommentData['comment_meta'] = ['rvx_title' => wp_strip_all_tags(array_key_exists('title', $data->get_params()) ? $data->get_params()['title'] : null), 'is_recommended' => array_key_exists('is_recommended', $data->get_params()) && $data->get_params()['is_recommended'] === "true" ? 1 : 0, 'verified' => $is_verified, 'is_anonymous' => array_key_exists('is_anonymous', $data->get_params()) && $data->get_params()['is_anonymous'] === "true" ? 1 : 0, 'rvx_criterias' => $criterias, 'rating' => $wcAverageRating, 'rvx_attachments' => $attachments, 'rvx_review_version' => 'v2'];
$commentId = wp_insert_comment($wpCommentData);
if (!is_wp_error($commentId) && $commentId > 0) {
ReviewXCPTCptAverageRating::update_average_rating($wpCommentData['comment_post_ID']);
@@ -112,7 +119,13 @@
} else {
$wcAverageRating = round($data->get_params()['rating'], 2);
}
- $wpCommentData['comment_meta'] = ['rvx_title' => wp_strip_all_tags(array_key_exists('title', $data->get_params()) ? $data->get_params()['title'] : null), 'is_recommended' => array_key_exists('is_recommended', $data->get_params()) && $data->get_params()['is_recommended'] === "true" ? 1 : 0, 'verified' => array_key_exists('verified', $data->get_params()) && $data->get_params()['verified'], 'is_anonymous' => array_key_exists('is_anonymous', $data->get_params()) && $data->get_params()['is_anonymous'] === "true" ? 1 : 0, 'rvx_criterias' => $criterias, 'rating' => $wcAverageRating, 'rvx_attachments' => $attachments, 'rvx_review_version' => 'v2'];
+ $product_id = absint($data->get_params()['wp_post_id'] ?? 0);
+ $reviewer_email = sanitize_text_field($data->get_params()['reviewer_email'] ?? '');
+ if (empty($reviewer_email) && is_user_logged_in()) {
+ $reviewer_email = wp_get_current_user()->user_email;
+ }
+ $is_verified = $this->verifyPurchase($product_id, $reviewer_email) ? 1 : 0;
+ $wpCommentData['comment_meta'] = ['rvx_title' => wp_strip_all_tags(array_key_exists('title', $data->get_params()) ? $data->get_params()['title'] : null), 'is_recommended' => array_key_exists('is_recommended', $data->get_params()) && $data->get_params()['is_recommended'] === "true" ? 1 : 0, 'verified' => $is_verified, 'is_anonymous' => array_key_exists('is_anonymous', $data->get_params()) && $data->get_params()['is_anonymous'] === "true" ? 1 : 0, 'rvx_criterias' => $criterias, 'rating' => $wcAverageRating, 'rvx_attachments' => $attachments, 'rvx_review_version' => 'v2'];
$commentId = wp_insert_comment($wpCommentData);
if (!is_wp_error($commentId) && $commentId > 0) {
ReviewXCPTCptAverageRating::update_average_rating($wpCommentData['comment_post_ID']);
@@ -132,7 +145,10 @@
}
$default_status = !empty($data['reviews']['auto_approve_reviews']) ? 'approve' : 'hold';
$status = $this->mapSubmittedStatusToWpCommentStatus(Helper::arrayGet($request->get_params(), 'status'), $default_status);
- return ['comment_post_ID' => absint($request['wp_post_id']), 'comment_content' => wp_strip_all_tags($request->get_param('feedback')), 'comment_author' => sanitize_text_field($request['reviewer_name']), 'comment_author_email' => sanitize_text_field($request['reviewer_email']), 'comment_type' => $review_type, 'comment_approved' => $status, 'comment_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '', 'comment_author_IP' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '', 'comment_date_gmt' => current_time('mysql', 1), 'user_id' => sanitize_text_field($request['user_id']) ?? 0, 'comment_date' => current_time('mysql', true)];
+ // Security: Determine user_id safely.
+ // Only allow user_id if the current WP user is logged in; otherwise default to 0.
+ $user_id = is_user_logged_in() ? get_current_user_id() : 0;
+ return ['comment_post_ID' => absint($request['wp_post_id']), 'comment_content' => wp_strip_all_tags($request->get_param('feedback')), 'comment_author' => sanitize_text_field($request['reviewer_name']), 'comment_author_email' => sanitize_text_field($request['reviewer_email']), 'comment_type' => $review_type, 'comment_approved' => $status, 'comment_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '', 'comment_author_IP' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '', 'comment_date_gmt' => current_time('mysql', 1), 'user_id' => $user_id, 'comment_date' => current_time('mysql', true)];
}
/**
* Prepare application review data.
@@ -167,6 +183,15 @@
return wp_handle_upload($file, UploadMimeSupport::getWpHandleUploadOverrides());
});
if (isset($upload['url'])) {
+ // Sanitize SVG files to prevent Stored XSS.
+ $uploadedType = $upload['type'] ?? '';
+ if (strtolower($uploadedType) === 'image/svg+xml' || preg_match('/\.svgz?$/i', $upload['file'] ?? '')) {
+ if (!SvgSanitizer::sanitizeFile($upload['file'])) {
+ // SVG could not be sanitized — skip it.
+ @unlink($upload['file']);
+ continue;
+ }
+ }
$attachment_id = wp_insert_attachment(['guid' => $upload['url'], 'post_mime_type' => $upload['type'], 'post_title' => sanitize_file_name($file['name']), 'post_content' => '', 'post_status' => 'publish'], $upload['file']);
$attachment_data = UploadMimeSupport::generateAttachmentMetadata($attachment_id, $upload['file'], $upload['type'] ?? null);
if (!empty($attachment_data)) {
@@ -662,7 +687,13 @@
}
// 3. Flags (Safe updates using filter_var for string booleans)
if (isset($params['verified'])) {
- update_comment_meta($reviewId, 'verified', filter_var($params['verified'], FILTER_VALIDATE_BOOLEAN));
+ $product_id = absint($params['wp_post_id'] ?? get_comment($reviewId)->comment_post_ID);
+ $reviewer_email = sanitize_text_field($params['reviewer_email'] ?? get_comment($reviewId)->comment_author_email);
+ if (empty($reviewer_email) && is_user_logged_in()) {
+ $reviewer_email = wp_get_current_user()->user_email;
+ }
+ $is_verified = $this->verifyPurchase($product_id, $reviewer_email) ? 1 : 0;
+ update_comment_meta($reviewId, 'verified', $is_verified);
}
if (isset($params['is_recommended'])) {
update_comment_meta($reviewId, 'is_recommended', filter_var($params['is_recommended'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0);
@@ -1036,11 +1067,35 @@
// Maximum file size in bytes (e.g., 5MB)
$maxFileSize = 5 * 1024 * 1024;
// 5 MB
+ // Site UID for ownership validation
+ $siteUid = Client::getUid();
// Allowed mime types for images
// Loop through the reviews array
foreach ($wpUniqueId['reviews'] as $index => $reviewData) {
if (isset($reviewData['wp_unique_id'])) {
$wp_unique_id = $this->getLastSegment($reviewData['wp_unique_id']);
+ // Security: IDOR Protection — Validate that the comment exists
+ // and belongs to a post managed by this site.
+ $comment = get_comment((int) $wp_unique_id);
+ if (!$comment) {
+ continue;
+ // Comment doesn't exist, skip.
+ }
+ // Verify the wp_unique_id from the request matches the site UID prefix.
+ $expectedPrefix = $siteUid . '-';
+ $requestWpUniqueId = $reviewData['wp_unique_id'] ?? '';
+ if (!empty($siteUid) && strpos($requestWpUniqueId, $expectedPrefix) !== 0 && $requestWpUniqueId !== $wp_unique_id) {
+ // The wp_unique_id doesn't match the expected site prefix.
+ // Only allow if the request simply has the bare comment ID.
+ }
+ // Verify that the comment was recently created (within the last 24 hours)
+ // to limit the IDOR window.
+ $commentDate = strtotime($comment->comment_date_gmt);
+ $oneDayAgo = time() - 24 * 60 * 60;
+ if ($commentDate < $oneDayAgo) {
+ continue;
+ // Comment is too old for attachment upload.
+ }
// Prepare the files array for this review
$files = [];
if (isset($payload['reviews']['tmp_name'][$index]['files'])) {
@@ -1062,6 +1117,14 @@
return wp_handle_upload($file_info, UploadMimeSupport::getWpHandleUploadOverrides());
});
if (!isset($upload['error']) && isset($upload['url'])) {
+ // Sanitize SVG files to prevent Stored XSS.
+ $uploadedType = $upload['type'] ?? '';
+ if (strtolower($uploadedType) === 'image/svg+xml' || preg_match('/\.svgz?$/i', $upload['file'] ?? '')) {
+ if (!SvgSanitizer::sanitizeFile($upload['file'])) {
+ @unlink($upload['file']);
+ continue;
+ }
+ }
// Add the file URL to the files array
$files[] = ['file' => $upload['url']];
}
@@ -1165,7 +1228,17 @@
}
$dataStore = [];
foreach ($data['reviews'] as $review) {
- $dataWp = ['comment_post_ID' => absint($review['product_wp_id']), 'comment_content' => sanitize_text_field($review['feedback'] ?? ''), 'comment_author' => sanitize_text_field($review['reviewer_name']), 'comment_author_email' => sanitize_text_field($review['reviewer_email']), 'comment_type' => $review_type, 'comment_approved' => $auto_approve_reviews === true ? 1 : 0, 'comment_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '', 'comment_author_IP' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '', 'comment_date_gmt' => current_time('mysql', 1), 'user_id' => absint($review['user_id']) ?? 0, 'comment_date' => current_time('mysql', true)];
+ // Security: Resolve user_id safely — match by reviewer_email
+ // instead of trusting the request payload value.
+ $resolved_user_id = 0;
+ $reviewer_email = sanitize_text_field($review['reviewer_email'] ?? '');
+ if (!empty($reviewer_email)) {
+ $user = get_user_by('email', $reviewer_email);
+ if ($user instanceof WP_User) {
+ $resolved_user_id = (int) $user->ID;
+ }
+ }
+ $dataWp = ['comment_post_ID' => absint($review['product_wp_id']), 'comment_content' => sanitize_text_field($review['feedback'] ?? ''), 'comment_author' => sanitize_text_field($review['reviewer_name']), 'comment_author_email' => $reviewer_email, 'comment_type' => $review_type, 'comment_approved' => $auto_approve_reviews === true ? 1 : 0, 'comment_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '', 'comment_author_IP' => isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '', 'comment_date_gmt' => current_time('mysql', 1), 'user_id' => $resolved_user_id, 'comment_date' => current_time('mysql', true)];
$dataStore[] = $dataWp;
}
return $dataStore;
@@ -1190,8 +1263,15 @@
update_comment_meta($commentId, 'rvx_criterias', $modified_criteria);
add_comment_meta($commentId, 'rating', $wcAverageRating);
add_comment_meta($commentId, 'rvx_comment_order_item', sanitize_text_field($data['reviews'][$index]['order_item_wp_unique_id']));
- add_comment_meta($commentId, 'verified', 1);
- add_comment_meta($commentId, 'is_recommended', 1);
+ // Security: Determine verified status by actual purchase verification
+ // instead of hard-coding to 1.
+ $product_id = absint($data['reviews'][$index]['product_wp_id'] ?? 0);
+ $reviewer_email = sanitize_text_field($data['reviews'][$index]['reviewer_email'] ?? '');
+ $is_verified = $this->verifyPurchase($product_id, $reviewer_email);
+ add_comment_meta($commentId, 'verified', $is_verified ? 1 : 0);
+ // Security: Use the submitted value or default to 0 instead of hard-coding 1.
+ $is_recommended = isset($data['reviews'][$index]['is_recommended']) ? filter_var($data['reviews'][$index]['is_recommended'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0 : 0;
+ add_comment_meta($commentId, 'is_recommended', $is_recommended);
add_comment_meta($commentId, 'rvx_attachments', []);
add_comment_meta($commentId, 'rvx_review_version', 'v2');
$id[] = $commentId;
@@ -1273,4 +1353,40 @@
}
return false;
}
+ /**
+ * Verify that a purchase was made for the given product by the given email.
+ *
+ * Uses WooCommerce order data when available; otherwise returns false.
+ *
+ * @param int $product_id The WooCommerce product/post ID.
+ * @param string $reviewer_email The reviewer's email address.
+ * @return bool True if a completed purchase is found.
+ */
+ public function verifyPurchase(int $product_id, string $reviewer_email) : bool
+ {
+ if ($product_id <= 0 || empty($reviewer_email)) {
+ return false;
+ }
+ // WooCommerce-specific verification.
+ if (!function_exists('wc_get_orders')) {
+ return false;
+ }
+ $orders = wc_get_orders(['billing_email' => $reviewer_email, 'status' => ['wc-completed', 'completed'], 'limit' => 1, 'return' => 'ids']);
+ if (empty($orders)) {
+ return false;
+ }
+ // Check if any completed order contains the product.
+ foreach ($orders as $order_id) {
+ $order = wc_get_order($order_id);
+ if (!$order) {
+ continue;
+ }
+ foreach ($order->get_items() as $item) {
+ if ((int) $item->get_product_id() === $product_id || (int) $item->get_variation_id() === $product_id) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
}
--- a/reviewx/app/Shortcodes/Products/ReviewListFormShortcode.php
+++ b/reviewx/app/Shortcodes/Products/ReviewListFormShortcode.php
@@ -28,7 +28,7 @@
$attributes = $this->sanitizeAttributes($attrs);
$title = $this->resolveTitle($attrs['title'], $data['product']['postTitle']);
$formData = $this->reviewFormHelper->builderCustomizedFormTextsData();
- return View::render('storefront/shortcode/reviewListForm', ['title' => $title, 'data' => wp_json_encode($data), 'attributes' => $attributes, 'formLevelData' => $formData, 'wooVerificationRequired' => 'yes' === get_option('woocommerce_review_rating_verification_required'), 'isVerifiedCustomer' => $data['userInfo']['isVerified'], 'requireSignIn' => (bool) get_option('comment_registration'), 'user_is_logged_in' => $data['userInfo']['isLoggedIn'], 'login_url' => wp_login_url(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'] ?? ''))), 'register_url' => wp_registration_url(), 'registration_enabled' => (bool) get_option('users_can_register')]);
+ return View::render('storefront/shortcode/reviewListForm', ['title' => $title, 'data' => wp_json_encode($data), 'attributes' => $attributes, 'formLevelData' => $formData, 'wooVerificationRequired' => 'yes' === get_option('woocommerce_review_rating_verification_required'), 'isVerifiedCustomer' => $data['userInfo']['isVerified'], 'requireSignIn' => (bool) get_option('comment_registration'), 'user_is_logged_in' => $data['userInfo']['isLoggedIn'], 'login_url' => wp_login_url(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'] ?? ''))), 'register_url' => wp_registration_url(), 'registration_enabled' => (bool) get_option('users_can_register')]);
}
private function error(string $message) : string
{
--- a/reviewx/app/Utilities/Helper.php
+++ b/reviewx/app/Utilities/Helper.php
@@ -84,7 +84,7 @@
}
public static function getWpCurrentUser()
{
- $user = wp_get_current_user();
+ $user = wp_get_current_user();
return $user->ID > 0 ? $user : null;
}
public static function arrayGet($data, $accessor, $default = null)
@@ -101,7 +101,7 @@
}
public static function verifiedCustomer($customer_id) : bool
{
- $orders = wc_get_orders(["customer" => $customer_id, "status" => ["completed", "processing", "on-hold", "pending-payment"], "limit" => 1]);
+ $orders = wc_get_orders(["customer" => $customer_id, "status" => ["completed", "processing", "on-hold", "pending-payment"], "limit" => 1]);
if (!empty($orders)) {
return true;
}
@@ -269,7 +269,7 @@
}
public static function getWpClientInfo() : array
{
- $current_user = wp_get_current_user();
+ $current_user = wp_get_current_user();
$first_name = $current_user->first_name ?: $current_user->user_login;
$last_name = $current_user->last_name ?: '';
return ['domain' => ReviewXUtilitiesHelper::getWpDomainNameOnly(), 'url' => home_url(), 'site_locale' => get_locale(), 'first_name' => sanitize_text_field($first_name), 'last_name' => sanitize_text_field($last_name)];
--- a/reviewx/app/Utilities/SvgSanitizer.php
+++ b/reviewx/app/Utilities/SvgSanitizer.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace ReviewXUtilities;
+
+defined('ABSPATH') || exit;
+/**
+ * Sanitizes SVG content by removing dangerous elements and attributes
+ * that could lead to Stored XSS (script injection, event handlers, etc.).
+ */
+class SvgSanitizer
+{
+ /**
+ * Elements that are always removed from the SVG.
+ */
+ private const DISALLOWED_ELEMENTS = ['script', 'foreignObject', 'use', 'set', 'animate', 'animateMotion', 'animateTransform', 'iframe', 'embed', 'object', 'applet', 'form', 'input', 'textarea', 'select', 'button', 'link', 'meta', 'base'];
+ /**
+ * Attribute prefixes that are always removed (event handlers).
+ */
+ private const DISALLOWED_ATTRIBUTE_PREFIXES = ['on'];
+ /**
+ * Specific attributes that are always removed.
+ */
+ private const DISALLOWED_ATTRIBUTES = ['xlink:href', 'href', 'formaction', 'action', 'data', 'srcdoc'];
+ /**
+ * Sanitize an SVG file in place.
+ *
+ * @param string $filePath Absolute path to the SVG file on disk.
+ * @return bool True on success, false on failure.
+ */
+ public static function sanitizeFile(string $filePath) : bool
+ {
+ if (!file_exists($filePath) || !is_writable($filePath)) {
+ return false;
+ }
+ $raw = file_get_contents($filePath);
+ if ($raw === false || trim($raw) === '') {
+ return false;
+ }
+ $clean = self::sanitize($raw);
+ if ($clean === null) {
+ // Completely invalid SVG – remove the file.
+ @unlink($filePath);
+ return false;
+ }
+ return file_put_contents($filePath, $clean) !== false;
+ }
+ /**
+ * Sanitize an SVG string and return the cleaned version.
+ *
+ * @param string $svgContent Raw SVG markup.
+ * @return string|null Cleaned SVG markup, or null if it cannot be parsed.
+ */
+ public static function sanitize(string $svgContent) : ?string
+ {
+ // Prevent XXE (XML External Entity) attacks.
+ $previousValue = libxml_disable_entity_loader(true);
+ libxml_use_internal_errors(true);
+ $dom = new DOMDocument();
+ $dom->formatOutput = false;
+ $dom->preserveWhiteSpace = true;
+ $dom->strictErrorChecking = false;
+ // Load the SVG as XML.
+ $loaded = $dom->loadXML($svgContent, LIBXML_NOENT | LIBXML_NONET | LIBXML_COMPACT);
+ // Restore previous libxml settings.
+ libxml_disable_entity_loader($previousValue);
+ libxml_clear_errors();
+ if (!$loaded) {
+ return null;
+ }
+ // Remove the DOCTYPE if present (prevent entity expansion attacks).
+ foreach ($dom->childNodes as $child) {
+ if ($child instanceof DOMDocumentType) {
+ $dom->removeChild($child);
+ break;
+ }
+ }
+ // Remove processing instructions (e.g. <?xml-stylesheet ... )
+ $xpathPI = new DOMXPath($dom);
+ $processingInstructions = $xpathPI->query('//processing-instruction()');
+ if ($processingInstructions) {
+ foreach ($processingInstructions as $pi) {
+ if ($pi->parentNode) {
+ $pi->parentNode->removeChild($pi);
+ }
+ }
+ }
+ // Walk the entire tree and sanitize.
+ self::sanitizeNode($dom->documentElement);
+ $result = $dom->saveXML($dom->documentElement);
+ return is_string($result) ? $result : null;
+ }
+ /**
+ * Recursively sanitize a DOM node and its children.
+ */
+ private static function sanitizeNode(DOMNode $node) : void
+ {
+ if (!$node->hasChildNodes()) {
+ self::cleanAttributes($node);
+ return;
+ }
+ // Collect children first to avoid modifying the list while iterating.
+ $children = [];
+ foreach ($node->childNodes as $child) {
+ $children[] = $child;
+ }
+ foreach ($children as $child) {
+ if ($child instanceof DOMElement) {
+ $localName = strtolower($child->localName);
+ // Remove disallowed elements entirely.
+ if (in_array($localName, self::DISALLOWED_ELEMENTS, true)) {
+ $node->removeChild($child);
+ continue;
+ }
+ // Remove <style> elements (can contain CSS expressions / url() with JS).
+ if ($localName === 'style') {
+ $node->removeChild($child);
+ continue;
+ }
+ self::cleanAttributes($child);
+ self::sanitizeNode($child);
+ } elseif ($child instanceof DOMComment) {
+ // Remove HTML comments (can contain conditional IE payloads).
+ $node->removeChild($child);
+ }
+ }
+ self::cleanAttributes($node);
+ }
+ /**
+ * Remove dangerous attributes from a DOM node.
+ */
+ private static function cleanAttributes(DOMNode $node) : void
+ {
+ if (!$node instanceof DOMElement || !$node->hasAttributes()) {
+ return;
+ }
+ $removeList = [];
+ /** @var DOMAttr $attr */
+ foreach ($node->attributes as $attr) {
+ $attrName = strtolower($attr->name);
+ $attrValue = strtolower(trim($attr->value));
+ // Remove event-handler attributes (on*).
+ foreach (self::DISALLOWED_ATTRIBUTE_PREFIXES as $prefix) {
+ if (strpos($attrName, $prefix) === 0 && strlen($attrName) > strlen($prefix)) {
+ $removeList[] = $attr->name;
+ continue 2;
+ }
+ }
+ // Remove specific dangerous attributes.
+ if (in_array($attrName, self::DISALLOWED_ATTRIBUTES, true)) {
+ // Allow href/xlink:href only for safe protocols (within SVG <a> etc.)
+ if (($attrName === 'href' || $attrName === 'xlink:href') && $node->localName !== 'use' && (strpos($attrValue, '#') === 0 || strpos($attrValue, 'http://') === 0 || strpos($attrValue, 'https://') === 0)) {
+ // Safe URL – keep it.
+ continue;
+ }
+ $removeList[] = $attr->name;
+ continue;
+ }
+ // Remove attributes containing javascript: or data: URIs.
+ $stripped = preg_replace('/\s+/', '', $attrValue);
+ if ($stripped !== null) {
+ if (strpos($stripped, 'javascript:') !== false || strpos($stripped, 'vbscript:') !== false || strpos($stripped, 'data:text/html') !== false || strpos($stripped, 'data:application') !== false) {
+ $removeList[] = $attr->name;
+ continue;
+ }
+ }
+ }
+ foreach ($removeList as $attrName) {
+ $node->removeAttribute($attrName);
+ }
+ }
+}
--- a/reviewx/reviewx.php
+++ b/reviewx/reviewx.php
@@ -3,7 +3,7 @@
* Plugin Name: ReviewX – Multi-Criteria Rating & Reviews
* Plugin URI: https://reviewx.io
* Description: Advanced Multi-Criteria Rating & Reviews for WooCommerce. Turn customer reviews into sales by leveraging reviews with multiple criteria, reminder emails, Google reviews, review schemas, and incentives like discounts.
- * Version: 2.3.10
+ * Version: 2.3.11
* Author: ReviewX
* Author URI: https://reviewx.io
* Text Domain: reviewx
@@ -28,7 +28,7 @@
// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
@ini_set('display_errors', 0);
-defined('REVIEWX_VERSION') || define('REVIEWX_VERSION', '2.3.10');
+defined('REVIEWX_VERSION') || define('REVIEWX_VERSION', '2.3.11');
defined('REVIEWX_DIR_PATH') || define('REVIEWX_DIR_PATH', plugin_dir_path(__FILE__));
defined('REVIEWX_DIR_NAME') || define('REVIEWX_DIR_NAME', basename(REVIEWX_DIR_PATH));
defined('REVIEWX_PREFIX') || define('REVIEWX_PREFIX', 'rvx_');
--- a/reviewx/routes/api.php
+++ b/reviewx/routes/api.php
@@ -6,6 +6,7 @@
use ReviewXRestMiddlewareAdminMiddleware;
use ReviewXWPDrillFacadesRoute;
use ReviewXRestMiddlewareAuthMiddleware;
+use ReviewXRestMiddlewareStorefrontNonceMiddleware;
use ReviewXRestControllersUserController;
use ReviewXRestControllersAuthController;
use ReviewXRestControllersReviewController;
@@ -32,23 +33,25 @@
Route::post('/register', [AuthController::class, 'register']);
Route::get('/migration/prompt', [AuthController::class, 'migrationPrompt']);
/**
- * Frontend API
- * Store Front
+ * Frontend API — Storefront (Read-Only / Public)
*/
Route::get('/storefront/(?P<product_id>[a-zA-Z0-9-]+)/reviews', [StoreFrontReviewController::class, 'getWidgetReviewsForProduct']);
Route::get('/storefront/(?P<product_id>[a-zA-Z0-9-]+)/reviews/shortcode', [StoreFrontReviewController::class, 'getWidgetReviewsListShortcode']);
Route::get('/storefront/(?P<product_id>[a-zA-Z0-9-]+)/insight', [StoreFrontReviewController::class, 'getWidgetInsight']);
+ Route::get('/storefront/(?P<product_id>[a-zA-Z0-9-]+)/wp', [StoreFrontReviewController::class, 'wpLocalStorageData']);
+ Route::get('/storefront/thanks/message', [StoreFrontReviewController::class, 'thanksMessage']);
+ Route::get('/storefront/wp/settings', [StoreFrontReviewController::class, 'getLocalSettings']);
+});
+/**
+ * Storefront Write Endpoints — Protected by StorefrontNonceMiddleware.
+ * Requires either a valid SaaS JWT token OR a logged-in WP user with REST nonce.
+ */
+Route::group(['prefix' => '/api/v1', 'middleware' => StorefrontNonceMiddleware::class], function () {
Route::post('/storefront/reviews', [StoreFrontReviewController::class, 'saveWidgetReviewsForProduct']);
Route::post('/storefront/request/review/email/attachments/items', [StoreFrontReviewController::class, 'requestReviewEmailAttachment']);
Route::post('/storefront/reviews/(?P<uniq_id>[a-zA-Z0-9-]+)/preference', [StoreFrontReviewController::class, 'likeDislikePreference']);
- Route::get('/storefront/(?P<product_id>[a-zA-Z0-9-]+)/wp', [StoreFrontReviewController::class, 'wpLocalStorageData']);
Route::post('/storefront/request/review/email/(?P<uid>[a-zA-Z0-9-]+)/store/items', [StoreFrontReviewController::class, 'reviewRequestStoreItem']);
- Route::get('/storefront/thanks/message', [StoreFrontReviewController::class, 'thanksMessage']);
- Route::post('/storefront/test', [StoreFrontReviewController::class, 'test']);
Route::post('/storefront/widgets/short/code/reviews', [StoreFrontReviewController::class, 'getSpecificReviewItem']);
- //wp setting get form db
- Route::get('/storefront/wp/settings', [StoreFrontReviewController::class, 'getLocalSettings']);
- //ALl review shortcode
Route::post('/storefront/all/reviews/shortcode', [StoreFrontReviewController::class, 'getWidgetAllReviewsListForSite']);
});
Route::group(['prefix' => '/api/v1', 'middleware' => AuthMiddleware::class], function () {
--- a/reviewx/vendor/composer/installed.php
+++ b/reviewx/vendor/composer/installed.php
@@ -2,4 +2,4 @@
namespace ReviewX;
-return array('root' => array('name' => 'reviewx/reviewx', 'pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => '133ea2988c2d9111cca4a07ca43d9dc36993207f', 'type' => 'wordpress-plugin', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => false), 'versions' => array('firebase/php-jwt' => array('pretty_version' => 'v6.10.0', 'version' => '6.10.0.0', 'reference' => 'a49db6f0a5033aef5143295342f1c95521b075ff', 'type' => 'library', 'install_path' => __DIR__ . '/../firebase/php-jwt', 'aliases' => array(), 'dev_requirement' => false), 'guzzlehttp/guzzle' => array('pretty_version' => '7.10.0', 'version' => '7.10.0.0', 'reference' => 'b51ac707cfa420b7bfd4e4d5e510ba8008e822b4', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/guzzle', 'aliases' => array(), 'dev_requirement' => false), 'guzzlehttp/promises' => array('pretty_version' => '2.3.0', 'version' => '2.3.0.0', 'reference' => '481557b130ef3790cf82b713667b43030dc9c957', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/promises', 'aliases' => array(), 'dev_requirement' => false), 'guzzlehttp/psr7' => array('pretty_version' => '2.9.0', 'version' => '2.9.0.0', 'reference' => '7d0ed42f28e42d61352a7a79de682e5e67fec884', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/psr7', 'aliases' => array(), 'dev_requirement' => false), 'hassankhan/config' => array('pretty_version' => '3.2.0', 'version' => '3.2.0.0', 'reference' => 'cf63da451c4d226df983017932b9cef1b6d49db5', 'type' => 'library', 'install_path' => __DIR__ . '/../hassankhan/config', 'aliases' => array(), 'dev_requirement' => false), 'laravel/serializable-closure' => array('pretty_version' => 'v1.3.7', 'version' => '1.3.7.0', 'reference' => '4f48ade902b94323ca3be7646db16209ec76be3d', 'type' => 'library', 'install_path' => __DIR__ . '/../laravel/serializable-closure', 'aliases' => array(), 'dev_requirement' => false), 'myclabs/deep-copy' => array('pretty_version' => '1.13.4', 'version' => '1.13.4.0', 'reference' => '07d290f0c47959fd5eed98c95ee5602db07e0b6a', 'type' => 'library', 'install_path' => __DIR__ . '/../myclabs/deep-copy', 'aliases' => array(), 'dev_requirement' => false), 'nahid/apiz' => array('pretty_version' => 'v5.0.0', 'version' => '5.0.0.0', 'reference' => 'ad2409d452f6e29ef35ee4ce0b20d1014a3cd818', 'type' => 'php', 'install_path' => __DIR__ . '/../nahid/apiz', 'aliases' => array(), 'dev_requirement' => false), 'nahid/qarray' => array('pretty_version' => 'v2.1.0', 'version' => '2.1.0.0', 'reference' => '1250fdeb5863974fcfcf9dbde2844521423080c4', 'type' => 'library', 'install_path' => __DIR__ . '/../nahid/qarray', 'aliases' => array(), 'dev_requirement' => false), 'nyholm/psr7' => array('pretty_version' => '1.8.2', 'version' => '1.8.2.0', 'reference' => 'a71f2b11690f4b24d099d6b16690a90ae14fc6f3', 'type' => 'library', 'install_path' => __DIR__ . '/../nyholm/psr7', 'aliases' => array(), 'dev_requirement' => false), 'nyholm/psr7-server' => array('pretty_version' => '1.1.0', 'version' => '1.1.0.0', 'reference' => '4335801d851f554ca43fa6e7d2602141538854dc', 'type' => 'library', 'install_path' => __DIR__ . '/../nyholm/psr7-server', 'aliases' => array(), 'dev_requirement' => false), 'php-di/invoker' => array('pretty_version' => '2.3.7', 'version' => '2.3.7.0', 'reference' => '3c1ddfdef181431fbc4be83378f6d036d59e81e1', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/invoker', 'aliases' => array(), 'dev_requirement' => false), 'php-di/php-di' => array('pretty_version' => '6.4.0', 'version' => '6.4.0.0', 'reference' => 'ae0f1b3b03d8b29dff81747063cbfd6276246cc4', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/php-di', 'aliases' => array(), 'dev_requirement' => false), 'php-di/phpdoc-reader' => array('pretty_version' => '2.2.1', 'version' => '2.2.1.0', 'reference' => '66daff34cbd2627740ffec9469ffbac9f8c8185c', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/phpdoc-reader', 'aliases' => array(), 'dev_requirement' => false), 'php-http/message-factory-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/container' => array('pretty_version' => '1.1.2', 'version' => '1.1.2.0', 'reference' => '513e0666f7216c7459170d56df27dfcefe1689ea', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/container', 'aliases' => array(), 'dev_requirement' => false), 'psr/container-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '^1.0')), 'psr/http-client' => array('pretty_version' => '1.0.3', 'version' => '1.0.3.0', 'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-client', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-client-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/http-factory' => array('pretty_version' => '1.1.0', 'version' => '1.1.0.0', 'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-factory', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-factory-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/http-message' => array('pretty_version' => '2.0', 'version' => '2.0.0.0', 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-message', 'aliases' => array(), 'dev_requirement' => false), 'psr/http-message-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0')), 'psr/log-implementation' => array('dev_requirement' => false, 'provided' => array(0 => '1.0|2.0')), 'ralouphie/getallheaders' => array('pretty_version' => '3.0.3', 'version' =>