PluginProbe
WiserReview Product Reviews for WooCommerce / 3.4
WiserReview Product Reviews for WooCommerce v3.4
3.7 3.6 3.5 trunk 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 3.0 All 29 releases
wiser-review / wiser-review.php

wiser-review.php in WiserReview Product Reviews for WooCommerce 3.4, at wiser-review.php

3,318 lines 113.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: WiserReview Product Reviews for WooCommerce
4 * Plugin URI: https://wiserreview.com
5 * Description: Collect, manage, and display powerful product reviews and testimonials in WooCommerce using the WiserReview platform. Boost trust and conversion with automated review collection.
6 * Version: 3.4
7 * Author: Wiser Notify
8 * Author URI: https://wiserreview.com
9 * Requires Plugins: woocommerce
10 * Tested up to: 6.9
11 * Requires at least: 5.6
12 * Requires PHP: 7.4
13 * Text Domain: wiser-review
14 * License: GPLv2 or later
15 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
16 *
17 * @package wiser-review
18 * */
19
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit;
22 }
23 define( 'WISERRW_PLUGIN_FILE', __FILE__ );
24 define( 'WISERRW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
25 define( 'WISERRW_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
26 define( 'WISERRW_PLUGIN_VERSION', '3.4' );
27 define( 'WISERRW_API_HOST', 'https://api.wiserreview.com/api/woocommerce/' );
28 define( 'WISERRW_RS_HOST', 'https://rs.wiserreview.com' );
29
30 if ( ! function_exists( 'wiserrw_scripts' ) ) {
31 /**
32 * Plugin styles and scripts.
33 */
34 function wiserrw_scripts( $hook ) {
35 $allowed_screens = array(
36 'toplevel_page_wiser-review'
37 );
38 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
39 $screen_id = $screen ? $screen->id : $hook;
40 if ( ! in_array( $screen_id, $allowed_screens, true ) ) {
41 return;
42 }
43
44 wp_enqueue_style( 'wiserrw-admin-styles', plugins_url( '/assets/css/wiserrw-admin.css', WISERRW_PLUGIN_FILE ), array(), WISERRW_PLUGIN_VERSION );
45 wp_register_script( 'wiserrw-admin-script', plugins_url( '/assets/js/wiserw-js.js', WISERRW_PLUGIN_FILE ), array(), WISERRW_PLUGIN_VERSION );
46 wp_localize_script(
47 'wiserrw-admin-script',
48 'wiserrw_ajax_var',
49 array(
50 'url' => admin_url( 'admin-ajax.php' ),
51 'wiserrw_security' => wp_create_nonce( 'wiserrw_export_orders_nonce' ),
52 'action' => 'wiserrw_save_settings',
53 'nonce' => wp_create_nonce( 'wiserrw_save_settings' )
54 )
55 );
56 }
57 }
58 add_action( 'admin_enqueue_scripts', 'wiserrw_scripts' );
59
60 function wiserrw_ajax_save_settings() {
61 // Verify nonce for security
62 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'wiserrw_save_settings' ) ) {
63 wp_send_json_error( array(
64 'message' => 'Security check failed.',
65 ), 403 );
66 }
67
68 // Check user capabilities
69 if ( ! current_user_can( 'manage_options' ) ) {
70 wp_send_json_error( array(
71 'message' => 'Unauthorized access.',
72 ), 403 );
73 }
74
75 $wiserrw_data = isset( $_POST['wiserrw_data'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['wiserrw_data'] ) ) : array();
76
77 // Sanitize review form URL
78 if ( ! empty( $wiserrw_data['wiserrw_review_form_url'] ) ) {
79 $wiserrw_data['wiserrw_review_form_url'] = rtrim( strtok( trim( $wiserrw_data['wiserrw_review_form_url'] ), '?' ), '/' );
80 }
81 // Force review page toggle off if URL is invalid
82 $review_url = $wiserrw_data['wiserrw_review_form_url'] ?? '';
83 if ( empty( $review_url ) || strpos( $review_url, 'https://rating.wiserreview.com/' ) !== 0 ) {
84 unset( $wiserrw_data['wiserrw_review_page_enabled'] );
85 $wiserrw_data['wiserrw_review_form_url'] = '';
86 }
87
88 // Check if review page toggle changed BEFORE saving
89 $old_settings = get_option( 'wiserrw_api_settings', array() );
90 $old_review_page = $old_settings['wiserrw_review_page_enabled'] ?? '';
91 $new_review_page = $wiserrw_data['wiserrw_review_page_enabled'] ?? '';
92 $review_page_changed = ( $old_review_page !== $new_review_page );
93
94 update_option( 'wiserrw_api_settings', $wiserrw_data );
95
96 // Flush rewrite rules AFTER saving so the new setting is active
97 if ( $review_page_changed ) {
98 wiserrw_review_page_add_endpoint();
99 flush_rewrite_rules();
100 }
101
102 wp_send_json_success( array(
103 'message' => 'Settings saved.',
104 'saved' => $wiserrw_data,
105 ) );
106 }
107 add_action( 'wp_ajax_wiserrw_save_settings', 'wiserrw_ajax_save_settings' );
108
109 if ( ! function_exists( 'wiserrw_api_settings' ) ) {
110 /**
111 * Setup API settings page in admin panel.
112 */
113 function wiserrw_api_settings() {
114 add_menu_page(
115 __( 'Wiser Review', 'wiser-review' ),
116 __( 'Wiser Review', 'wiser-review' ),
117 'manage_options',
118 'wiser-review',
119 'wiserrw_api_settings_callback',
120 plugins_url( '/assets/images/icon.svg', WISERRW_PLUGIN_FILE ),
121 );
122 }
123 }
124 add_action( 'admin_menu', 'wiserrw_api_settings' );
125
126 /**
127 * Callback for the plugin settings.
128 */
129 function wiserrw_api_settings_callback() {
130 $wiserrw_api_settings = get_option('wiserrw_api_settings', array());
131 if (!wp_style_is('woocommerce_admin_styles', 'registered')) {
132 return;
133 }
134 wp_enqueue_style('woocommerce_admin_styles');
135 wp_enqueue_script('wiserrw-admin-script');
136 include_once WISERRW_PLUGIN_DIR . '/views/wiserw-plugin-settings.php';
137 }
138
139 /**
140 * Recursive sanitation for an array
141 *
142 * @param array $array to sanitize.
143 *
144 * @return mixed
145 */
146 function wiserrw_recursive_sanitize_text_field( $array ) {
147 foreach ( $array as $key => &$value ) {
148 if ( is_array( $value ) ) {
149 $value = wiserrw_recursive_sanitize_text_field( $value );
150 } else {
151 $value = sanitize_text_field( $value );
152 }
153 }
154 return $array;
155 }
156
157 /**
158 * Validate API Key
159 *
160 * @param string $api_key API Key to validate.
161 *
162 * @return bool
163 */
164
165 function wiserrw_validate_api( $api_key ) {
166 if ( empty( $api_key ) ) {
167 return (object) array( 'status' => 'error', 'msg' => 'API key is required' );
168 }
169
170 $url = WISERRW_API_HOST . 'verify?key=' . urlencode( $api_key ) .
171 '&wiserrw_hook_url=' . urlencode( get_site_url() . '/wp-json/wiserrw/v1/reviews' ) .
172 '&ver=' . urlencode( WISERRW_PLUGIN_VERSION );
173
174 $args = array(
175 'method' => 'GET',
176 'timeout' => 20,
177 'headers' => array(
178 'x-wiserrw-api-key' => $api_key,
179 ),
180 );
181
182 $response = wp_remote_get( $url, $args );
183
184 if ( is_wp_error( $response ) ) {
185 error_log( 'API Verify Error: ' . $response->get_error_message() );
186 return (object) array( 'status' => 'error', 'msg' => 'Connection failed. Please try again.' );
187 }
188
189 $response_code = wp_remote_retrieve_response_code( $response );
190 if ( $response_code !== 200 ) {
191 return (object) array( 'status' => 'error', 'msg' => 'Invalid response from server. Please check your API key.' );
192 }
193
194 $body = json_decode( wp_remote_retrieve_body( $response ) );
195
196 if ( ! is_object( $body ) ) {
197 return (object) array( 'status' => 'error', 'msg' => 'Invalid response format. Please try again.' );
198 }
199
200 if ( isset( $body->data ) && isset( $body->data->wsid ) ) {
201 update_option( 'wiserrw_api_data', $body->data );
202 update_option( 'wiserrw_wsid', $body->data->wsid );
203 update_option( 'wiserrw_automation_id', $body->data->automation_id ?? '' );
204 }
205
206 return $body;
207 }
208
209
210 /**
211 * Show Product Reviews
212 */
213 function wiserrw_show_product_review() {
214 $wiserrw_api_settings = get_option( 'wiserrw_api_settings' );
215 if ( isset($wiserrw_api_settings['wiserrw_product_review_enabled']) && '1' === $wiserrw_api_settings['wiserrw_product_review_enabled'] ) {
216 $pid = wiserrw_get_current_product_id();
217 echo wiserrw_product_review( array( 'id' => $pid ) );
218 wiserrw_restore_product_globals();
219 }
220 }
221 add_action( 'woocommerce_after_single_product_summary', 'wiserrw_show_product_review', 14 );
222 // fix for the kadence theme
223 add_action( 'kadence_after_short_description_block', 'wiserrw_show_product_review' );
224
225
226 /**
227 * Show Product Reviews
228 */
229 function wiserrw_show_product_rating_on_collection() {
230 $wiserrw_api_settings = get_option( 'wiserrw_api_settings' );
231 if ( isset($wiserrw_api_settings['wiserrw_product_card_enabled']) && '1' === $wiserrw_api_settings['wiserrw_product_card_enabled'] ) {
232 $pid = wiserrw_get_current_product_id();
233 echo wiserrw_rating_count( array( 'id' => $pid ) );
234 wiserrw_restore_product_globals();
235 }
236 }
237 add_action( 'woocommerce_after_shop_loop_item_title', 'wiserrw_show_product_rating_on_collection', 5 );
238
239 /**
240 * Add review JS script to head
241 */
242 function wiserr_review_script() {
243 $settings = (array) get_option( 'wiserrw_api_settings', array() );
244 if ( empty( $settings['wiserrw_live_mode_checkbox'] ) || $settings['wiserrw_live_mode_checkbox'] !== '1' ) {
245 return;
246 }
247
248 $api_data = get_option( 'wiserrw_api_data', array() );
249
250 // Support object or array; avoid bool access warnings
251 $header_pixel = '';
252 if ( is_object( $api_data ) && ! empty( $api_data->header_pixel ) ) {
253 $header_pixel = $api_data->header_pixel;
254 } elseif ( is_array( $api_data ) && ! empty( $api_data['header_pixel'] ) ) {
255 $header_pixel = $api_data['header_pixel'];
256 } else {
257 return; // nothing to print
258 }
259
260 $allowed_tags = array(
261 'script' => array(
262 'src' => true,
263 'type' => true,
264 'async' => true,
265 'defer' => true,
266 ),
267 );
268
269 echo wp_kses( $header_pixel, $allowed_tags );
270 }
271 add_action( 'wp_footer', 'wiserr_review_script' );
272
273
274 /**
275 * Show Product Rating Count.
276 */
277 function wiserrw_show_rating_count() {
278 $wiserrw_api_settings = get_option( 'wiserrw_api_settings' );
279 if ( isset($wiserrw_api_settings['wiserrw_star_rating_enabled']) && '1' === $wiserrw_api_settings['wiserrw_star_rating_enabled'] ) {
280 $pid = wiserrw_get_current_product_id();
281 echo wiserrw_rating_count( array( 'id' => $pid ) );
282 wiserrw_restore_product_globals();
283 }
284 }
285 add_action( 'woocommerce_single_product_summary', 'wiserrw_show_rating_count', 9 );
286 // fix for the kadence theme
287 if ( wp_get_theme()->get( 'Name' ) === 'Kadence' ) {
288 add_action( 'woocommerce_after_add_to_cart_button', 'wiserrw_show_rating_count' );
289 }
290
291
292 /**
293 * Show Review Nudges Section.
294 */
295 function wiserrw_show_review_nudges_section() {
296 $wiserrw_api_settings = get_option('wiserrw_api_settings', array());
297 if (isset($wiserrw_api_settings['wiserrw_review_nudges_enabled'])
298 && '1' === $wiserrw_api_settings['wiserrw_review_nudges_enabled']) {
299 $pid = wiserrw_get_current_product_id();
300 echo wiserrw_product_nudges( array( 'id' => $pid ) );
301 wiserrw_restore_product_globals();
302 }
303 }
304 add_action( 'woocommerce_after_add_to_cart_form', 'wiserrw_show_review_nudges_section' );
305
306 /**
307 * Get current product ID reliably across all themes including Divi 5, Bricks, Elementor Pro.
308 * Uses WooCommerce's $GLOBALS['product'] first, falls back to get_the_ID().
309 */
310 function wiserrw_get_current_product_id() {
311 if ( isset( $GLOBALS['product'] ) && $GLOBALS['product'] instanceof WC_Product ) {
312 return $GLOBALS['product']->get_id();
313 }
314 return get_the_ID() ?: 0;
315 }
316
317 /**
318 * Restore WC product globals after shortcode processing.
319 * Prevents corrupted product data for downstream WooCommerce hooks.
320 */
321 function wiserrw_restore_product_globals() {
322 if ( isset( $GLOBALS['post'] ) && function_exists( 'wc_setup_product_data' ) ) {
323 wc_setup_product_data( $GLOBALS['post'] );
324 }
325 }
326
327 /**
328 * Get product brand name from common sources used by WooCommerce merchants.
329 * Checks taxonomies (WooCommerce Brands, Perfect Brands), attributes, and custom meta.
330 *
331 * @param int|WC_Product $product_or_id Product ID or WC_Product object.
332 * @return string Brand name or empty string if not found.
333 */
334 function wiserrw_get_product_brand( $product_or_id ) {
335 $product = is_numeric( $product_or_id ) ? wc_get_product( $product_or_id ) : $product_or_id;
336 if ( ! $product ) {
337 return '';
338 }
339 $p_id = $product->get_id();
340
341 // 1. Native WooCommerce Brands (WC 9.0+) / WooCommerce Brands plugin
342 $terms = get_the_terms( $p_id, 'product_brand' );
343 if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
344 return $terms[0]->name;
345 }
346
347 // 2. Perfect Brands for WooCommerce plugin
348 $terms = get_the_terms( $p_id, 'pwb-brand' );
349 if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
350 return $terms[0]->name;
351 }
352
353 // 3. YITH WooCommerce Brands Add-On
354 $terms = get_the_terms( $p_id, 'yith_product_brand' );
355 if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
356 return $terms[0]->name;
357 }
358
359 // 4. Product attributes — check common brand attribute names in multiple languages
360 $brand_attr_names = array(
361 'Brand', 'brand', 'Manufacturer', // English
362 'Varumärke', // Swedish
363 'Marca', // Spanish / Italian / Portuguese
364 'Marke', // German
365 'Marque', // French
366 'Merk', // Dutch
367 'Márka', // Hungarian
368 'Marka', // Polish / Turkish
369 'Бренд', // Russian
370 'ブランド', // Japanese
371 '品牌', // Chinese
372 'العلا�
373 ة التجارية', // Arabic
374 );
375 foreach ( $brand_attr_names as $name ) {
376 $brand = $product->get_attribute( $name );
377 if ( ! empty( $brand ) ) {
378 return $brand;
379 }
380 }
381
382 // 5. Custom post meta (common conventions)
383 $brand = get_post_meta( $p_id, '_brand', true )
384 ?: get_post_meta( $p_id, '_manufacturer', true )
385 ?: get_post_meta( $p_id, 'hwp_product_brand', true );
386
387 return (string) $brand;
388 }
389
390 /**
391 * Normalize language code to 2-letter format, with WordPress site locale fallback.
392 * Use this to ensure $lang is never empty — falls back to site locale for
393 * single-language stores without WPML/Polylang.
394 *
395 * @param string $lang Language code detected from WPML/Polylang (may be empty).
396 * @return string 2-letter language code (e.g., "en", "de", "fr").
397 */
398 function wiserrw_normalize_lang( $lang = '' ) {
399 if ( empty( $lang ) ) {
400 // Fallback to WordPress site locale (e.g., "en_US" → "en")
401 $lang = get_locale();
402 }
403 return substr( (string) $lang, 0, 2 );
404 }
405
406 /**
407 * Shortcode for the Rating Count.
408 */
409 function wiserrw_rating_count( $atts ) {
410 $atts = shortcode_atts(
411 array(
412 'id' => wiserrw_get_current_product_id(),
413 ),
414 $atts,
415 'wiserrw_rating_count'
416 );
417
418 $product_id = intval( $atts['id'] );
419 if ( ! $product_id ) {
420 return '';
421 }
422
423 // Get SKU directly (faster than wc_get_product)
424 $sku = get_post_meta( $product_id, '_sku', true );
425
426 // Check settings
427 $wiserrw_api_settings = get_option( 'wiserrw_api_settings' );
428 if ( ! $wiserrw_api_settings || '1' !== ( $wiserrw_api_settings['wiserrw_live_mode_checkbox'] ?? '' ) ) {
429 return '';
430 }
431
432 // API HTML
433 $wiserrw_api_data = get_option( 'wiserrw_api_data' );
434 $product_rating_count = $wiserrw_api_data->product_rating_count ?? '';
435 $product_rating_count = preg_replace( '/data-pid=("|\')(.*?)\1/', 'data-pid="' . $product_id . '"', $product_rating_count );
436 $product_rating_count = preg_replace( '/data-skuid=("|\')(.*?)\1/', 'data-skuid="' . $sku . '"', $product_rating_count );
437
438 // --- Multilingual (cache per request) ---
439 static $lang_cache = array();
440 if ( isset( $lang_cache[$product_id] ) ) {
441 $opid_value = $lang_cache[$product_id]['opid'];
442 $current_lang = $lang_cache[$product_id]['lang'];
443 } else {
444 $original_pid = 0;
445 $current_lang = '';
446
447 // WPML
448 if ( has_filter( 'wpml_element_trid' ) ) {
449 $trid = apply_filters( 'wpml_element_trid', null, $product_id, 'post_product' );
450 $translations = $trid ? apply_filters( 'wpml_get_element_translations', null, $trid, 'post_product' ) : array();
451 $current_lang = apply_filters( 'wpml_current_language', null );
452
453 if ( $translations && is_array( $translations ) ) {
454 foreach ( $translations as $lang => $translation ) {
455 if ( ! empty( $translation->original ) ) {
456 $original_pid = (int) $translation->element_id;
457 }
458 }
459 }
460 }
461
462 // Polylang
463 if ( function_exists( 'pll_get_post' ) && function_exists( 'pll_current_language' ) ) {
464 $current_lang = pll_current_language();
465 $default_lang = pll_default_language();
466 if ( $current_lang && $default_lang && $current_lang !== $default_lang ) {
467 $original_pid = pll_get_post( $product_id, $default_lang );
468 }
469 }
470
471 // Fallback: current product if no original
472 $opid_value = ( $original_pid && $original_pid !== $product_id ) ? $original_pid : $product_id;
473 $current_lang = wiserrw_normalize_lang( $current_lang );
474
475 // Cache result
476 $lang_cache[$product_id] = array(
477 'opid' => $opid_value,
478 'lang' => $current_lang,
479 );
480 }
481
482 // Extract wrapper
483 $wiser_open_tag = '';
484 if ( preg_match( '/<div\b[^>]*>/i', $product_rating_count, $matches ) ) {
485 $wiser_open_tag = $matches[0];
486
487 // Ensure opid
488 if ( preg_match( '/data-opid=/', $wiser_open_tag ) ) {
489 $wiser_open_tag = preg_replace( '/data-opid=("|\')(.*?)\1/', 'data-opid="' . $opid_value . '"', $wiser_open_tag );
490 } else {
491 $wiser_open_tag = str_replace( '<div', '<div data-opid="' . esc_attr( $opid_value ) . '"', $wiser_open_tag );
492 }
493
494 // Ensure lang
495 if ( ! empty( $current_lang ) ) {
496 if ( preg_match( '/data-lang=/', $wiser_open_tag ) ) {
497 $wiser_open_tag = preg_replace( '/data-lang=("|\')(.*?)\1/', 'data-lang="' . $current_lang . '"', $wiser_open_tag );
498 } else {
499 $wiser_open_tag = str_replace( '<div', '<div data-lang="' . esc_attr( $current_lang ) . '"', $wiser_open_tag );
500 }
501 }
502 }
503
504 // Inner HTML: prefer post meta, patch if missing
505 $meta_html = get_post_meta( $product_id, 'wiserrw_star_rating_html', true );
506 // if ( $meta_html ) {
507 // Patch opid
508 if ( preg_match( '/data-opid=/', $meta_html ) ) {
509 $meta_html = preg_replace( '/data-opid=("|\')(.*?)\1/', 'data-opid="' . $opid_value . '"', $meta_html );
510 } else {
511 $meta_html = preg_replace( '/<div\b/i', '<div data-opid="' . esc_attr( $opid_value ) . '"', $meta_html, 1 );
512 }
513
514 // Patch lang
515 if ( ! empty( $current_lang ) ) {
516 if ( preg_match( '/data-lang=/', $meta_html ) ) {
517 $meta_html = preg_replace( '/data-lang=("|\')(.*?)\1/', 'data-lang="' . $current_lang . '"', $meta_html );
518 } else {
519 $meta_html = preg_replace( '/<div\b/i', '<div data-lang="' . esc_attr( $current_lang ) . '"', $meta_html, 1 );
520 }
521 }
522
523 $inner_html = $meta_html;
524
525 // Build output
526 $output = '';
527 if ( $wiser_open_tag ) {
528 $output .= $wiser_open_tag;
529 }
530 $output .= $inner_html;
531 $output .= '</div>';
532
533 return $output;
534 }
535 add_shortcode( 'wiserrw_rating_count', 'wiserrw_rating_count' );
536
537
538
539
540 /**
541 * Shortcode for the product.
542 */
543
544
545
546 function wiserrw_product_review( $atts ) {
547 $atts = shortcode_atts(
548 array(
549 'id' => wiserrw_get_current_product_id(),
550 ),
551 $atts,
552 'wiserrw_product_review'
553 );
554
555 $product_id = intval( $atts['id'] );
556 if ( ! $product_id ) {
557 return '';
558 }
559
560 $product = wc_get_product( $product_id );
561 if ( ! $product ) {
562 return '';
563 }
564
565 $wiserrw_api_settings = get_option( 'wiserrw_api_settings' );
566 if ( ! $wiserrw_api_settings || '1' !== ( $wiserrw_api_settings['wiserrw_live_mode_checkbox'] ?? '' ) ) {
567 return '';
568 }
569
570 $sku = $product->get_sku();
571 $data_login = is_user_logged_in() ? 'true' : 'false';
572 $wiserrw_api_data = get_option( 'wiserrw_api_data' );
573
574 // --- Multilingual (WPML / Polylang) ---
575 $original_pid = 0;
576 $current_lang = '';
577
578 // WPML
579 if ( has_filter( 'wpml_element_trid' ) ) {
580 $trid = apply_filters( 'wpml_element_trid', null, $product_id, 'post_product' );
581 $translations = $trid ? apply_filters( 'wpml_get_element_translations', null, $trid, 'post_product' ) : array();
582 $current_lang = apply_filters( 'wpml_current_language', null );
583
584 if ( $translations && is_array( $translations ) ) {
585 foreach ( $translations as $lang => $translation ) {
586 if ( ! empty( $translation->original ) ) {
587 $original_pid = (int) $translation->element_id;
588 }
589 }
590 }
591 }
592
593 // Polylang
594 if ( function_exists( 'pll_get_post' ) && function_exists( 'pll_current_language' ) ) {
595 $current_lang = pll_current_language();
596 $default_lang = pll_default_language();
597 if ( $current_lang && $default_lang && $current_lang !== $default_lang ) {
598 $original_pid = pll_get_post( $product_id, $default_lang );
599 }
600 }
601
602 // Fallback: use current product if no original found
603 $opid_value = ( $original_pid && $original_pid !== $product_id ) ? $original_pid : $product_id;
604 $current_lang = wiserrw_normalize_lang( $current_lang );
605
606 // --- Patch API HTML ---
607 $product_review_section = $wiserrw_api_data->product_review_section ?? '';
608
609 // --- Ensure data-lgin (NOT data-login — "lgin" is the correct attribute name used by the pixel JS) ---
610 if ( preg_match( '/data-lgin=/', $product_review_section ) ) {
611 $product_review_section = preg_replace( '/data-lgin=("|\')(.*?)\1/', 'data-lgin="' . $data_login . '"', $product_review_section );
612 } else {
613 $product_review_section = preg_replace( '/<div\b/i', '<div data-lgin="' . esc_attr( $data_login ) . '"', $product_review_section, 1 );
614 }
615
616 // --- Ensure pid ---
617 $product_review_section = preg_replace( '/data-pid=("|\')(.*?)\1/', 'data-pid="' . $product_id . '"', $product_review_section );
618
619 // --- Ensure skuid ---
620 $product_review_section = preg_replace( '/data-skuid=("|\')(.*?)\1/', 'data-skuid="' . $sku . '"', $product_review_section );
621
622 // --- Ensure opid ---
623 if ( preg_match( '/data-opid=/', $product_review_section ) ) {
624 $product_review_section = preg_replace( '/data-opid=("|\')(.*?)\1/', 'data-opid="' . $opid_value . '"', $product_review_section );
625 } else {
626 $product_review_section = preg_replace( '/<div\b/i', '<div data-opid="' . esc_attr( $opid_value ) . '"', $product_review_section, 1 );
627 }
628
629 // --- Ensure lang ---
630 if ( ! empty( $current_lang ) ) {
631 if ( preg_match( '/data-lang=/', $product_review_section ) ) {
632 $product_review_section = preg_replace( '/data-lang=("|\')(.*?)\1/', 'data-lang="' . $current_lang . '"', $product_review_section );
633 } else {
634 $product_review_section = preg_replace( '/<div\b/i', '<div data-lang="' . esc_attr( $current_lang ) . '"', $product_review_section, 1 );
635 }
636 }
637
638 // --- Extract the wrapper div tag ---
639 $wiser_open_tag = '';
640 if ( preg_match( '/<div\b[^>]*>/i', $product_review_section, $matches ) ) {
641 $wiser_open_tag = $matches[0];
642
643 // --- Ensure opid for wrapper ---
644 if ( preg_match( '/data-opid=/', $wiser_open_tag ) ) {
645 $wiser_open_tag = preg_replace( '/data-opid=("|\')(.*?)\1/', 'data-opid="' . $opid_value . '"', $wiser_open_tag );
646 } else {
647 $wiser_open_tag = str_replace( '<div', '<div data-opid="' . esc_attr( $opid_value ) . '"', $wiser_open_tag );
648 }
649
650 // --- Ensure lang for wrapper ---
651 if ( ! empty( $current_lang ) ) {
652 if ( preg_match( '/data-lang=/', $wiser_open_tag ) ) {
653 $wiser_open_tag = preg_replace( '/data-lang=("|\')(.*?)\1/', 'data-lang="' . $current_lang . '"', $wiser_open_tag );
654 } else {
655 $wiser_open_tag = str_replace( '<div', '<div data-lang="' . esc_attr( $current_lang ) . '"', $wiser_open_tag );
656 }
657 }
658 }
659
660 // --- Use post meta if available, fallback if not ---
661 $meta_html = get_post_meta( $product_id, 'wiserrw_product_html', true );
662 //if ( $meta_html ) {
663 // Patch opid for meta HTML
664 if ( preg_match( '/data-opid=/', $meta_html ) ) {
665 $meta_html = preg_replace( '/data-opid=("|\')(.*?)\1/', 'data-opid="' . $opid_value . '"', $meta_html );
666 } else {
667 $meta_html = preg_replace( '/<div\b/i', '<div data-opid="' . esc_attr( $opid_value ) . '"', $meta_html, 1 );
668 }
669
670 // Patch lang for meta HTML
671 if ( ! empty( $current_lang ) ) {
672 if ( preg_match( '/data-lang=/', $meta_html ) ) {
673 $meta_html = preg_replace( '/data-lang=("|\')(.*?)\1/', 'data-lang="' . $current_lang . '"', $meta_html );
674 } else {
675 $meta_html = preg_replace( '/<div\b/i', '<div data-lang="' . esc_attr( $current_lang ) . '"', $meta_html, 1 );
676 }
677 }
678
679 $inner_html = $meta_html;
680
681 // Build output
682 $output = '';
683 if ( $wiser_open_tag ) {
684 $output .= $wiser_open_tag;
685 }
686 $output .= $inner_html;
687 $output .= '</div>';
688
689 return $output;
690 }
691 add_shortcode( 'wiserrw_product_review', 'wiserrw_product_review' );
692
693 /**
694 * Shortcode for the Nudges.
695 */
696 function wiserrw_product_nudges( $atts = [] ) {
697 $atts = shortcode_atts(
698 array(
699 'id' => wiserrw_get_current_product_id(),
700 ),
701 $atts,
702 'wiserrw_product_nudges'
703 );
704
705 $product_id = intval( $atts['id'] );
706 if ( ! $product_id ) {
707 return '';
708 }
709
710 $product = wc_get_product( $product_id );
711 if ( ! $product ) {
712 return '';
713 }
714
715 $wiserrw_api_settings = get_option( 'wiserrw_api_settings' );
716 if ( ! $wiserrw_api_settings || '1' !== ( $wiserrw_api_settings['wiserrw_live_mode_checkbox'] ?? '' ) ) {
717 return '';
718 }
719
720 $sku = $product->get_sku();
721 $wiserrw_api_data = get_option( 'wiserrw_api_data' );
722
723 // --- Multilingual (WPML / Polylang) ---
724 $original_pid = 0;
725 $current_lang = '';
726
727 // WPML
728 if ( has_filter( 'wpml_element_trid' ) ) {
729 $trid = apply_filters( 'wpml_element_trid', null, $product_id, 'post_product' );
730 $translations = $trid ? apply_filters( 'wpml_get_element_translations', null, $trid, 'post_product' ) : array();
731 $current_lang = apply_filters( 'wpml_current_language', null );
732
733 if ( $translations && is_array( $translations ) ) {
734 foreach ( $translations as $lang => $translation ) {
735 if ( ! empty( $translation->original ) ) {
736 $original_pid = (int) $translation->element_id;
737 }
738 }
739 }
740 }
741
742 // Polylang
743 if ( function_exists( 'pll_get_post' ) && function_exists( 'pll_current_language' ) ) {
744 $current_lang = pll_current_language();
745 $default_lang = pll_default_language();
746 if ( $current_lang && $default_lang && $current_lang !== $default_lang ) {
747 $original_pid = pll_get_post( $product_id, $default_lang );
748 }
749 }
750
751 // Fallback if no original found
752 $opid_value = ( $original_pid && $original_pid !== $product_id ) ? $original_pid : $product_id;
753 $current_lang = wiserrw_normalize_lang( $current_lang );
754
755 // --- Patch API HTML ---
756 $nudges_section = $wiserrw_api_data->nudges_section ?? '';
757
758 // Ensure pid
759 $nudges_section = preg_replace( '/data-pid=("|\')(.*?)\1/', 'data-pid="' . $product_id . '"', $nudges_section );
760
761 // Ensure skuid
762 $nudges_section = preg_replace( '/data-skuid=("|\')(.*?)\1/', 'data-skuid="' . $sku . '"', $nudges_section );
763
764 // Ensure opid
765 if ( preg_match( '/data-opid=/', $nudges_section ) ) {
766 $nudges_section = preg_replace( '/data-opid=("|\')(.*?)\1/', 'data-opid="' . $opid_value . '"', $nudges_section );
767 } else {
768 $nudges_section = preg_replace( '/<div\b/i', '<div data-opid="' . esc_attr( $opid_value ) . '"', $nudges_section, 1 );
769 }
770
771 // Ensure lang
772 if ( ! empty( $current_lang ) ) {
773 if ( preg_match( '/data-lang=/', $nudges_section ) ) {
774 $nudges_section = preg_replace( '/data-lang=("|\')(.*?)\1/', 'data-lang="' . $current_lang . '"', $nudges_section );
775 } else {
776 $nudges_section = preg_replace( '/<div\b/i', '<div data-lang="' . esc_attr( $current_lang ) . '"', $nudges_section, 1 );
777 }
778 }
779
780 return $nudges_section;
781 }
782 add_shortcode( 'wiserrw_product_nudges', 'wiserrw_product_nudges' );
783
784
785 // ─────────────────────────────────────────────────────────────────────────────
786 // LEAVE A REVIEW — My Account page + inline order links
787 // ─────────────────────────────────────────────────────────────────────────────
788
789 /**
790 * Build a review form URL for a given product.
791 *
792 * @param int $product_id WC product ID.
793 * @param int|null $order_id WC order ID (enables verified-buyer matching).
794 * @return string|false Full URL, or false if not configured.
795 */
796 function wiserrw_review_form_url( $product_id, $order_id = null, $sku = '' ) {
797 $settings = get_option( 'wiserrw_api_settings', array() );
798
799 if ( empty( $settings['wiserrw_live_mode_checkbox'] ) || '1' !== $settings['wiserrw_live_mode_checkbox'] ) {
800 return false;
801 }
802
803 if ( empty( $settings['wiserrw_review_page_enabled'] ) || '1' !== $settings['wiserrw_review_page_enabled'] ) {
804 return false;
805 }
806
807 $base_url = $settings['wiserrw_review_form_url'] ?? '';
808 if ( ! $base_url || strpos( $base_url, 'https://rating.wiserreview.com/' ) !== 0 ) {
809 return false;
810 }
811
812 $params = array( 'pid' => (int) $product_id );
813 if ( $order_id ) {
814 $params['oid'] = (int) $order_id;
815 }
816 if ( ! empty( $sku ) ) {
817 $params['sku'] = (string) $sku;
818 }
819
820 return $base_url . '?' . http_build_query( $params );
821 }
822
823 /**
824 * My Account endpoint: "leave-a-review"
825 */
826 define( 'WISERRW_REVIEW_ENDPOINT', 'leave-a-review' );
827
828 add_action( 'init', 'wiserrw_review_page_add_endpoint' );
829 function wiserrw_review_page_add_endpoint() {
830 $settings = get_option( 'wiserrw_api_settings', array() );
831 $enabled = ! empty( $settings['wiserrw_review_page_enabled'] ) && '1' === $settings['wiserrw_review_page_enabled'];
832
833 if ( $enabled ) {
834 add_rewrite_endpoint( WISERRW_REVIEW_ENDPOINT, EP_ROOT | EP_PAGES );
835 } else {
836 // Clean up stale rewrite rules if endpoint was previously registered
837 $rules = get_option( 'rewrite_rules', array() );
838 if ( is_array( $rules ) && isset( $rules[ WISERRW_REVIEW_ENDPOINT . '(/(.*))?/?$' ] ) ) {
839 flush_rewrite_rules();
840 }
841 }
842 }
843
844 add_filter( 'woocommerce_account_menu_items', 'wiserrw_review_page_menu_item' );
845 function wiserrw_review_page_menu_item( array $items ): array {
846 $settings = get_option( 'wiserrw_api_settings', array() );
847 if ( empty( $settings['wiserrw_review_page_enabled'] ) || '1' !== $settings['wiserrw_review_page_enabled'] ) {
848 return $items;
849 }
850
851 $new = array();
852 foreach ( $items as $key => $label ) {
853 if ( $key === 'customer-logout' ) {
854 $new[ WISERRW_REVIEW_ENDPOINT ] = __( 'Leave a Review', 'wiser-review' );
855 }
856 $new[ $key ] = $label;
857 }
858 return $new;
859 }
860
861 add_action( 'woocommerce_account_' . WISERRW_REVIEW_ENDPOINT . '_endpoint', 'wiserrw_review_page_content' );
862 function wiserrw_review_page_content() {
863 // Don't render if feature is disabled
864 $settings = get_option( 'wiserrw_api_settings', array() );
865 if ( empty( $settings['wiserrw_review_page_enabled'] ) || '1' !== $settings['wiserrw_review_page_enabled'] ) {
866 return;
867 }
868
869 $customer_id = get_current_user_id();
870 if ( ! $customer_id ) {
871 echo '<p>' . esc_html__( 'Please log in to see your purchased products.', 'wiser-review' ) . '</p>';
872 return;
873 }
874
875 $orders = wc_get_orders( array(
876 'customer_id' => $customer_id,
877 'status' => array( 'completed', 'processing' ),
878 'limit' => -1,
879 'orderby' => 'date',
880 'order' => 'DESC',
881 ) );
882
883 // Collect unique products/variations, keeping most recent order ID per variation
884 $products = array();
885 $order_ids = array();
886 $variation_data = array(); // key => array( 'attributes_text', 'image_url' )
887 $parent_cache = array(); // avoid repeated wc_get_product() calls for same parent
888 foreach ( $orders as $order ) {
889 foreach ( $order->get_items() as $item ) {
890 if ( ! $item instanceof WC_Order_Item_Product ) {
891 continue;
892 }
893 $parent_id = $item->get_product_id();
894 $variation_id = $item->get_variation_id();
895 // Use variation+parent combo as unique key (so each variation shows as its own card)
896 $key = $variation_id ? $parent_id . '_' . $variation_id : (string) $parent_id;
897
898 if ( isset( $products[ $key ] ) ) {
899 continue;
900 }
901
902 // Load parent (cached) and variation product
903 if ( ! isset( $parent_cache[ $parent_id ] ) ) {
904 $parent_cache[ $parent_id ] = wc_get_product( $parent_id );
905 }
906 $parent_product = $parent_cache[ $parent_id ];
907 $display_product = $variation_id ? wc_get_product( $variation_id ) : $parent_product;
908
909 if ( ! $display_product || ! $parent_product || ! $parent_product->is_visible() ) {
910 continue;
911 }
912
913 // Variation attributes as readable text (e.g., "Size: XS, Color: Green")
914 $attributes_text = $variation_id ? wc_get_formatted_variation( $display_product, true, false, true ) : '';
915
916 // Prefer variation image, fall back to parent image
917 $image_id = $display_product->get_image_id() ?: $parent_product->get_image_id();
918 $image_url = $image_id ? wp_get_attachment_image_url( $image_id, 'woocommerce_thumbnail' ) : '';
919
920 $products[ $key ] = $parent_product; // parent used for display (name) + review URL pid
921 $order_ids[ $key ] = $order->get_id();
922 $variation_data[ $key ] = array(
923 'attributes_text' => $attributes_text,
924 'image_url' => $image_url,
925 'sku' => $display_product->get_sku(), // variant SKU (falls back to parent SKU if variant has no SKU)
926 );
927 }
928 }
929
930 if ( empty( $products ) ) {
931 echo '<p>' . esc_html__( 'You have no completed orders with reviewable products yet.', 'wiser-review' ) . '</p>';
932 return;
933 }
934 ?>
935 <style>
936 .wiserrw-review-grid {
937 display: grid;
938 grid-template-columns: repeat( auto-fill, minmax( 200px, 1fr ) );
939 gap: 20px;
940 margin: 20px 0;
941 }
942 .wiserrw-review-card {
943 border: 1px solid #e5e5e5;
944 border-radius: 8px;
945 overflow: hidden;
946 display: flex;
947 flex-direction: column;
948 background: #fff;
949 transition: box-shadow .15s;
950 }
951 .wiserrw-review-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,.1); }
952 .wiserrw-review-card-img {
953 width: 100%;
954 aspect-ratio: 1;
955 object-fit: cover;
956 background: #f7f7f7;
957 }
958 .wiserrw-review-card-img.placeholder {
959 display: flex;
960 align-items: center;
961 justify-content: center;
962 font-size: 40px;
963 color: #ccc;
964 }
965 .wiserrw-review-card-body {
966 padding: 12px;
967 flex: 1;
968 display: flex;
969 flex-direction: column;
970 gap: 10px;
971 }
972 .wiserrw-review-card-name {
973 font-size: 13px;
974 font-weight: 600;
975 line-height: 1.4;
976 color: #1d2327;
977 }
978 .wiserrw-review-card-attrs {
979 font-size: 11px;
980 color: #757575;
981 line-height: 1.4;
982 margin-top: 2px;
983 }
984 .wiserrw-review-card-attrs dl,
985 .wiserrw-review-card-attrs dt,
986 .wiserrw-review-card-attrs dd {
987 display: inline;
988 margin: 0;
989 padding: 0;
990 }
991 .wiserrw-review-card-attrs dt { font-weight: 500; }
992 .wiserrw-review-card-attrs dt::after { content: ": "; }
993 .wiserrw-review-card-attrs dd::after { content: " "; }
994 .wiserrw-review-card-link {
995 display: block;
996 text-align: center;
997 padding: 8px 12px;
998 background: #2271b1;
999 color: #fff !important;
1000 text-decoration: none !important;
1001 border-radius: 5px;
1002 font-size: 13px;
1003 font-weight: 600;
1004 transition: background .15s;
1005 margin-top: auto;
1006 }
1007 .wiserrw-review-card-link:hover { background: #135e96; }
1008 </style>
1009
1010 <h3><?php esc_html_e( 'Leave a Review', 'wiser-review' ); ?></h3>
1011 <p><?php esc_html_e( 'Thank you for shopping with us! Click any product below to leave a review.', 'wiser-review' ); ?></p>
1012
1013 <div class="wiserrw-review-grid">
1014 <?php foreach ( $products as $key => $product ) :
1015 $vdata = $variation_data[ $key ] ?? array();
1016 $sku_for_url = $vdata['sku'] ?? '';
1017 // Always pass parent product ID as pid; variant-level tracking happens via sku.
1018 $review_url = wiserrw_review_form_url( $product->get_id(), $order_ids[ $key ] ?? null, $sku_for_url );
1019 $thumb_url = ! empty( $vdata['image_url'] ) ? $vdata['image_url'] : get_the_post_thumbnail_url( $product->get_id(), 'woocommerce_thumbnail' );
1020 $attr_text = $vdata['attributes_text'] ?? '';
1021 ?>
1022 <div class="wiserrw-review-card">
1023 <?php if ( $thumb_url ) : ?>
1024 <img class="wiserrw-review-card-img"
1025 src="<?php echo esc_url( $thumb_url ); ?>"
1026 alt="<?php echo esc_attr( $product->get_name() ); ?>" loading="lazy" />
1027 <?php else : ?>
1028 <div class="wiserrw-review-card-img placeholder" aria-hidden="true">&#128230;</div>
1029 <?php endif; ?>
1030
1031 <div class="wiserrw-review-card-body">
1032 <div class="wiserrw-review-card-name">
1033 <?php echo esc_html( $product->get_name() ); ?>
1034 </div>
1035 <?php if ( ! empty( $attr_text ) ) : ?>
1036 <div class="wiserrw-review-card-attrs"><?php echo wp_kses_post( $attr_text ); ?></div>
1037 <?php endif; ?>
1038
1039 <?php if ( $review_url ) : ?>
1040 <a href="<?php echo esc_url( $review_url ); ?>"
1041 target="_blank"
1042 rel="noopener noreferrer"
1043 class="wiserrw-review-card-link">
1044 &#11088; <?php esc_html_e( 'Leave a Review', 'wiser-review' ); ?>
1045 </a>
1046 <?php endif; ?>
1047 </div>
1048 </div>
1049 <?php endforeach; ?>
1050 </div>
1051 <?php
1052 }
1053
1054 /**
1055 * Inline "Leave a Review" link per product on My Account → View Order page.
1056 */
1057 add_action( 'wp_enqueue_scripts', 'wiserrw_frontend_review_link_styles' );
1058 function wiserrw_frontend_review_link_styles() {
1059 if ( ! is_wc_endpoint_url() && ! is_account_page() ) {
1060 return;
1061 }
1062 $settings = get_option( 'wiserrw_api_settings', array() );
1063 if ( empty( $settings['wiserrw_review_page_enabled'] ) || '1' !== $settings['wiserrw_review_page_enabled'] ) {
1064 return;
1065 }
1066 $css = '.wiserrw-inline-review-link {
1067 display: inline-block; padding: 4px 10px; border: 1px solid currentColor;
1068 border-radius: 4px; font-size: 12px; font-weight: 600;
1069 text-decoration: none; opacity: 0.85; transition: opacity .15s; margin-top: 6px;
1070 }
1071 .wiserrw-inline-review-link:hover { opacity: 1; text-decoration: none; }
1072 .woocommerce-MyAccount-navigation ul li.woocommerce-MyAccount-navigation-link--leave-a-review a::before {
1073 content: "";
1074 display: inline-block;
1075 width: 16px; height: 16px;
1076 margin-right: 6px;
1077 vertical-align: middle;
1078 background: currentColor;
1079 -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 20 20\'%3E%3Cpath d=\'M10 1.12l2.77 5.61 6.19.9-4.48 4.37 1.06 6.17L10 15.27l-5.54 2.9 1.06-6.17L1.04 7.63l6.19-.9L10 1.12z\'/%3E%3C/svg%3E") center/contain no-repeat;
1080 mask: url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 20 20\'%3E%3Cpath d=\'M10 1.12l2.77 5.61 6.19.9-4.48 4.37 1.06 6.17L10 15.27l-5.54 2.9 1.06-6.17L1.04 7.63l6.19-.9L10 1.12z\'/%3E%3C/svg%3E") center/contain no-repeat;
1081 }';
1082 wp_register_style( 'wiserrw-review-link', false );
1083 wp_enqueue_style( 'wiserrw-review-link' );
1084 wp_add_inline_style( 'wiserrw-review-link', $css );
1085 }
1086
1087 add_action( 'woocommerce_order_item_meta_end', 'wiserrw_inline_order_review_link', 10, 3 );
1088 function wiserrw_inline_order_review_link( $item_id, $item, $order ) {
1089 // Only show on My Account pages
1090 if ( ! is_account_page() ) {
1091 return;
1092 }
1093 if ( ! $item instanceof WC_Order_Item_Product ) {
1094 return;
1095 }
1096 $product = $item->get_product(); // returns WC_Product_Variation for variations
1097 if ( ! $product ) {
1098 return;
1099 }
1100 // Always pass parent product ID as pid; variant-level tracking happens via sku.
1101 $sku_for_url = $product->get_sku(); // variant SKU when variant, else parent SKU (WC falls back)
1102 $url = wiserrw_review_form_url( $item->get_product_id(), $order->get_id(), $sku_for_url );
1103 if ( ! $url ) {
1104 return;
1105 }
1106 echo '<div>'
1107 . '<a href="' . esc_url( $url ) . '" '
1108 . 'target="_blank" rel="noopener noreferrer" '
1109 . 'class="wiserrw-inline-review-link">'
1110 . '&#11088;&nbsp;' . esc_html__( 'Leave a Review', 'wiser-review' )
1111 . '</a>'
1112 . '</div>';
1113 }
1114
1115 /**
1116 * Flush rewrite rules when the review page feature is toggled.
1117 */
1118 register_activation_hook( WISERRW_PLUGIN_FILE, 'wiserrw_review_page_flush_on_activate' );
1119 function wiserrw_review_page_flush_on_activate() {
1120 wiserrw_review_page_add_endpoint();
1121 flush_rewrite_rules();
1122 }
1123
1124 register_deactivation_hook( WISERRW_PLUGIN_FILE, 'wiserrw_review_page_flush_on_deactivate' );
1125 function wiserrw_review_page_flush_on_deactivate() {
1126 // Remove the endpoint by not registering it, then flush to clean up rewrite rules
1127 remove_action( 'init', 'wiserrw_review_page_add_endpoint' );
1128 flush_rewrite_rules();
1129 }
1130
1131
1132 /**
1133 * Send WooCommercer Order Data.
1134 *
1135 * @param int $order_id API Key to validate.
1136 */
1137 function wiserrw_woocommerce_order_status_completed( $order_id ) {
1138 $wiserrw_api_settings = get_option('wiserrw_api_settings', array());
1139 if (empty($wiserrw_api_settings) || empty($wiserrw_api_settings['wiserrw_api_key'])) {
1140 return; // Exit if no API settings
1141 }
1142
1143 $api_key = $wiserrw_api_settings['wiserrw_api_key'];
1144 $wsid = get_option( 'wiserrw_wsid', true );
1145 $automation_id = get_option( 'wiserrw_automation_id', true );
1146 if (!isset($wsid) || !isset($automation_id)) {
1147 return; // Exit if invalid API data
1148 }
1149
1150 $order = wc_get_order($order_id);
1151 if (!$order) {
1152 return; // Exit if invalid order
1153 }
1154
1155 $api_endpoint = WISERRW_API_HOST . 'webhook?wsid=' . $wsid . '&atmid=' . $automation_id.'&ver='.WISERRW_PLUGIN_VERSION;
1156
1157 // Initialize arrays
1158 $wiserrw_order_data = array();
1159 $customer_data = array();
1160
1161 /* Order Details */
1162 $wiserrw_order_data['oid'] = $order->get_id();
1163 $date_created = $order->get_date_created();
1164 $wiserrw_order_data['cdt'] = $date_created ? $date_created->format('Y-m-d H:i:s') : current_time('mysql');
1165 $wiserrw_order_data['orderAmount'] = $order->get_total();
1166
1167 /* Shipping Details */
1168 $country = $order->get_shipping_country();
1169 $state = $order->get_shipping_state();
1170 // Fallback to billing if shipping not available
1171 if ( empty( $country ) ) {
1172 $country = $order->get_billing_country();
1173 }
1174 if ( empty( $state ) ) {
1175 $state = $order->get_billing_state();
1176 }
1177
1178 /* Customer Details */
1179 $customer_data['email'] = $order->get_billing_email();
1180 $customer_data['phone'] = $order->get_billing_phone();
1181 $customer_data['first_name'] = $order->get_billing_first_name();
1182 $customer_data['last_name'] = $order->get_billing_last_name();
1183
1184 // Address
1185 $customer_data['cntry'] = $country;
1186 $customer_data['state'] = $state;
1187
1188 $wiserrw_order_data['customer'] = $customer_data;
1189
1190 /* Product Details */
1191 $wiserrw_order_data['line_items'] = array(); // Initialize line items array
1192 $items = $order->get_items();
1193
1194 foreach ( $items as $item ) {
1195 $product = $item->get_product();
1196 if (!$product || !$product instanceof WC_Product) {
1197 continue; // Skip if product doesn't exist anymore
1198 }
1199
1200 $product_id = $item->get_product_id();
1201 $variation_id = $item->get_variation_id();
1202 $product_price = $product->get_price();
1203 $url = get_permalink($product_id);
1204 $product_name = $item->get_name();
1205 // Variant SKU if the purchased item is a variant, else parent SKU (WC get_sku() on a variation falls back to parent SKU automatically).
1206 $product_sku = $product->get_sku();
1207
1208 // --- Multilingual handling (Added for opid/lang) ---
1209 $original_pid = 0;
1210 $current_lang = '';
1211
1212 // WPML
1213 if ( has_filter( 'wpml_element_trid' ) ) {
1214 $trid = apply_filters( 'wpml_element_trid', null, $product_id, 'post_product' );
1215 $translations = $trid ? apply_filters( 'wpml_get_element_translations', null, $trid, 'post_product' ) : [];
1216 $current_lang = apply_filters( 'wpml_current_language', null );
1217 if ( $translations && is_array( $translations ) ) {
1218 foreach ( $translations as $lang => $translation ) {
1219 if ( ! empty( $translation->original ) ) {
1220 $original_pid = (int) $translation->element_id;
1221 }
1222 }
1223 }
1224 }
1225
1226 // Polylang
1227 if ( function_exists( 'pll_get_post' ) && function_exists( 'pll_current_language' ) ) {
1228 $current_lang = pll_current_language();
1229 $default_lang = pll_default_language();
1230 if ( $current_lang && $default_lang && $current_lang !== $default_lang ) {
1231 $original_pid = pll_get_post( $product_id, $default_lang );
1232 }
1233 }
1234
1235 // Fallback (if no translation plugin, opid = current product id)
1236 $opid_value = ( $original_pid && $original_pid !== $product_id ) ? $original_pid : $product_id;
1237
1238 // Prefer variation image if the purchased item is a variant, fall back to parent product image.
1239 $image_id = $variation_id ? (int) $product->get_image_id() : 0;
1240 $image_id = $image_id ?: (int) get_post_thumbnail_id( $product_id );
1241 $image = $image_id ? wp_get_attachment_image_src( $image_id, 'single-post-thumbnail' ) : false;
1242 $image_url = is_array($image) ? $image[0] : '';
1243
1244 // Create new product data array for each item
1245 $product_data = array(
1246 'id' => $product_id,
1247 'pn' => $product_name,
1248 'name' => $product_name,
1249 'prc' => $product_price,
1250 'pu' => $url,
1251 'piu' => $image_url,
1252 'sku' => $product_sku,
1253 'opid' => $opid_value,
1254 'lang' => wiserrw_normalize_lang( $current_lang ),
1255 );
1256
1257 $wiserrw_order_data['line_items'][] = $product_data;
1258 }
1259
1260 // Send data to API
1261 $response = wp_remote_post(
1262 $api_endpoint,
1263 array(
1264 'method' => 'POST',
1265 'headers' => array(
1266 'Content-Type' => 'application/json',
1267 'x-wiserrw-api-key' => $api_key,
1268 ),
1269 'timeout' => 45,
1270 'redirection' => 5,
1271 'httpversion' => '1.0',
1272 'blocking' => true,
1273 'body' => wp_json_encode($wiserrw_order_data),
1274 )
1275 );
1276
1277 if (is_wp_error($response)) {
1278 $error_message = $response->get_error_message();
1279 error_log(sprintf(
1280 'WiserReview API Error (Order #%d): %s',
1281 $order_id,
1282 $error_message
1283 ));
1284 } else {
1285 $response_code = wp_remote_retrieve_response_code($response);
1286 if ($response_code !== 200) {
1287 $response_body = wp_remote_retrieve_body($response);
1288 error_log(sprintf(
1289 'WiserReview API Non-200 Response (Order #%d): Code %d - %s',
1290 $order_id,
1291 $response_code,
1292 $response_body
1293 ));
1294 }
1295 }
1296 }
1297
1298 add_action( 'woocommerce_order_status_completed', 'wiserrw_woocommerce_order_status_completed' );
1299
1300 /**
1301 * Export Orders.
1302 */
1303 function wiserrw_export_orders() {
1304 check_ajax_referer( 'wiserrw_export_orders_nonce', 'wiserrw_security' );
1305 if ( ! current_user_can( 'manage_woocommerce' ) ) {
1306 wp_send_json_error( array( 'message' => 'You are not allowed to perform this action.' ), 403 );
1307 }
1308 $from_date = isset( $_POST['start_date'] ) ? sanitize_text_field( wp_unslash( $_POST['start_date'] ) ) : '';
1309 $to_date = isset( $_POST['end_date'] ) ? sanitize_text_field( wp_unslash( $_POST['end_date'] ) ) : '';
1310 $duration = isset( $_POST['duration'] ) ? sanitize_text_field( wp_unslash( $_POST['duration'] ) ) : '';
1311 $offset = isset( $_POST['offset'] ) ? intval( sanitize_text_field( wp_unslash( $_POST['offset'] ) ) ) : '';
1312 $limit = isset( $_POST['limit'] ) ? intval( sanitize_text_field( wp_unslash( $_POST['limit'] ) ) ) : '';
1313
1314 if ( '' !== $duration ) {
1315 switch ( $duration ) {
1316 case '30_days':
1317 $date_from = strtotime( '-30 days' );
1318 break;
1319 case '3_months':
1320 $date_from = strtotime( '-3 months' );
1321 break;
1322 case '6_months':
1323 $date_from = strtotime( '-6 months' );
1324 break;
1325 case '1_year':
1326 $date_from = strtotime( '-1 year' );
1327 break;
1328 case '7_days':
1329 $date_from = strtotime( '-7 days' );
1330 break;
1331 default:
1332 $date_from = strtotime( '-7 days' );
1333 break;
1334 }
1335 // Get orders between date_from and now
1336 $date_to = current_time('timestamp');
1337 $all_orders = wc_get_orders(
1338 array(
1339 'status' => array( 'completed', 'processing' ),
1340 'date_created' => $date_from . '...' . $date_to, // Using WooCommerce's date range format
1341 'limit' => -1,
1342 )
1343 );
1344 } else {
1345 // For custom date range, ensure we get full days
1346 $start_date = strtotime($from_date . ' 00:00:00');
1347 $end_date = strtotime($to_date . ' 23:59:59');
1348
1349 if (!$start_date || !$end_date) {
1350 wp_send_json_error(array('message' => 'Invalid date range provided'));
1351 die();
1352 }
1353
1354 $all_orders = wc_get_orders(
1355 array(
1356 'status' => array( 'completed', 'processing' ),
1357 'date_created' => $start_date . '...' . $end_date, // Using WooCommerce's date range format
1358 'limit' => -1,
1359 )
1360 );
1361 }
1362 $total = count( $all_orders );
1363 $chunk_ids = array_slice( $all_orders, $offset, $limit );
1364 $upload_dir = wp_upload_dir();
1365 $filename = 'orders-export-progress.csv';
1366 $filepath = $upload_dir['basedir'] . '/' . $filename;
1367 $is_first_chunk = 0 === $offset;
1368
1369 $fp = fopen( $filepath, $is_first_chunk ? 'w' : 'a' );
1370 if ( $fp ) {
1371 if ( $is_first_chunk ) {
1372 fputcsv( $fp, array('Order Id', 'First Name', 'Last Name', 'Phone', 'Email', 'Country', 'State', 'Product Title', 'Product URL', 'Product Image URL', 'Product ID', 'Reviewer Image URL', 'Order Date' ) );
1373 }
1374
1375 foreach ( $chunk_ids as $order) {
1376 $order_id = $order->get_id();
1377 $user_id = $order->get_user_id();
1378
1379
1380 $country = $order->get_shipping_country();
1381 $state = $order->get_shipping_state();
1382
1383 // Fallback to billing if shipping not available
1384 if ( empty( $country ) ) {
1385 $country = $order->get_billing_country();
1386 }
1387 if ( empty( $state ) ) {
1388 $state = $order->get_billing_state();
1389 }
1390
1391 /* Product Details */
1392 $items = $order->get_items();
1393 foreach ( $items as $item ) {
1394 $product = $item->get_product();
1395 if (!$product) {
1396 continue;
1397 }
1398
1399 $product_id = $item->get_product_id();
1400 $product_price = $product->get_price();
1401 $url = get_permalink( $product_id );
1402 $product_name = $item->get_name();
1403 $image = wp_get_attachment_image_src( get_post_thumbnail_id( $product_id ), 'single-post-thumbnail' );
1404 $image_url = is_array($image) ? $image[0] : '';
1405 $reviewer_url = get_avatar_url( $user_id );
1406 $products_str[] = $product->get_name() . "|" . $image_url;
1407
1408 fputcsv(
1409 $fp,
1410 array(
1411 $order_id,
1412 $order->get_billing_first_name(),
1413 $order->get_billing_last_name(),
1414 $order->get_billing_phone(),
1415 $order->get_billing_email(),
1416 $country,
1417 $state,
1418 $product->get_name(),
1419 $url,
1420 $image_url,
1421 $order->get_id(),
1422 $reviewer_url,
1423 $order->get_date_created()->date( 'Y-m-d H:i:s' ),
1424 )
1425 );
1426 }
1427 }
1428
1429 fclose( $fp );
1430 }
1431
1432 $processed = $offset + count( $chunk_ids );
1433 $percent = round( ( $processed / $total ) * 100 );
1434
1435 wp_send_json_success(
1436 array(
1437 'percent' => $percent,
1438 'done' => $processed >= $total,
1439 'file_url' => $upload_dir['baseurl'] . '/' . $filename,
1440 )
1441 );
1442 }
1443
1444 add_action( 'wp_ajax_wiserrw_export_orders', 'wiserrw_export_orders' );
1445
1446 /**
1447 * Bulk Orders API.
1448 */
1449 function wiserrw_bulk_orders_send() {
1450
1451 check_ajax_referer( 'wiserrw_export_orders_nonce', 'wiserrw_security' );
1452
1453 if ( ! current_user_can( 'manage_woocommerce' ) ) {
1454 wp_send_json_error(
1455 array( 'message' => __( 'You are not allowed to perform this action.', 'wiser-review' ) ),
1456 403
1457 );
1458 }
1459
1460 $wiserrw_api_settings = get_option( 'wiserrw_api_settings' );
1461 $api_key = $wiserrw_api_settings['wiserrw_api_key'];
1462 $wsid = get_option( 'wiserrw_wsid', true );
1463 $automation_id = get_option( 'wiserrw_automation_id', true );
1464
1465 if ( !isset($wsid) || !isset($automation_id) ) {
1466 return; // Exit if invalid API data
1467 }
1468
1469 $wiserrw_order_data = array();
1470 $customer_data = array();
1471 $api_endpoint = WISERRW_API_HOST . 'webhook?wsid='
1472 . $wsid . '&atmid=' . $automation_id . '&ver=' . WISERRW_PLUGIN_VERSION;
1473
1474 $from_date = isset( $_POST['start_date'] ) ? sanitize_text_field( wp_unslash( $_POST['start_date'] ) ) : '';
1475 $to_date = isset( $_POST['end_date'] ) ? sanitize_text_field( wp_unslash( $_POST['end_date'] ) ) : '';
1476 $duration = isset( $_POST['duration'] ) ? sanitize_text_field( wp_unslash( $_POST['duration'] ) ) : '';
1477 $offset = isset( $_POST['offset'] ) ? intval( sanitize_text_field( wp_unslash( $_POST['offset'] ) ) ) : '';
1478 $limit = isset( $_POST['limit'] ) ? intval( sanitize_text_field( wp_unslash( $_POST['limit'] ) ) ) : '';
1479
1480 if ( '' !== $duration ) {
1481 switch ( $duration ) {
1482 case '30_days': $date_from = strtotime( '-30 days' ); break;
1483 case '3_months': $date_from = strtotime( '-3 months' ); break;
1484 case '6_months': $date_from = strtotime( '-6 months' ); break;
1485 case '1_year': $date_from = strtotime( '-1 year' ); break;
1486 case '7_days': $date_from = strtotime( '-7 days' ); break;
1487 default: $date_from = strtotime( '-7 days' ); break;
1488 }
1489
1490 $date_to = current_time('timestamp');
1491 $all_orders = wc_get_orders(array(
1492 'status' => array( 'completed', 'processing' ),
1493 'date_created' => $date_from . '...' . $date_to,
1494 'limit' => -1,
1495 ));
1496
1497 } else {
1498 $start_date = new WC_DateTime($from_date);
1499 $end_date = new WC_DateTime($to_date);
1500 $all_orders = wc_get_orders(array(
1501 'status' => array( 'completed', 'processing' ),
1502 'date_created' => $start_date->getTimestamp() . '...' . $end_date->getTimestamp(),
1503 'limit' => -1,
1504 ));
1505 }
1506
1507 $total = count( $all_orders );
1508 if ( $total === 0 ) {
1509 wp_send_json_success(array(
1510 'percent' => 100,
1511 'done' => true,
1512 'message' => 'No orders found for selected range.',
1513 ));
1514 wp_die();
1515 }
1516
1517 if ( $total > 0 ) {
1518 $chunk_ids = array_slice( $all_orders, $offset, $limit );
1519 $api_errors = array();
1520
1521 foreach ( $chunk_ids as $order ) {
1522 if ( $order instanceof WC_Order_Refund ) {
1523 continue; // skip refunds
1524 }
1525
1526 $order_id = $order->get_id();
1527
1528 /* Order Details */
1529 $wiserrw_order_data['oid'] = $order_id;
1530 $wiserrw_order_data['cdt'] = $order->get_date_created()
1531 ? $order->get_date_created()->format('Y-m-d H:i:s') // �
1532 Clean date format
1533 : current_time('mysql');
1534 $wiserrw_order_data['orderAmount'] = $order->get_total();
1535
1536 /* Shipping Details */
1537 $country = $order->get_shipping_country();
1538 $state = $order->get_shipping_state();
1539
1540 // Fallback to billing if shipping not available
1541
1542 if ( empty( $country ) ) {
1543 $country = $order->get_billing_country();
1544 }
1545 if ( empty( $state ) ) {
1546 $state = $order->get_billing_state();
1547 }
1548
1549
1550
1551 /* Customer Details */
1552 $customer_data = array(
1553 'email' => $order->get_billing_email(),
1554 'phone' => $order->get_billing_phone(),
1555 'first_name' => $order->get_billing_first_name(),
1556 'last_name' => $order->get_billing_last_name(),
1557
1558 );
1559
1560 $wiserrw_order_data['cntry'] = $country;
1561 $wiserrw_order_data['state'] = $state;
1562 $wiserrw_order_data['customer'] = $customer_data;
1563
1564 /* Product Details */
1565 $wiserrw_order_data['line_items'] = array();
1566 foreach ( $order->get_items() as $item ) {
1567 $product = $item->get_product();
1568 if ( !$product || !$product instanceof WC_Product ) {
1569 continue;
1570 }
1571
1572 $product_id = $item->get_product_id();
1573 $variation_id = $item->get_variation_id();
1574 $product_name = $item->get_name();
1575 $product_price = $product->get_price();
1576 $url = get_permalink($product_id);
1577 // Variant SKU if the purchased item is a variant, else parent SKU (WC falls back automatically).
1578 $product_sku = $product->get_sku();
1579 // Prefer variation image if variant, fall back to parent product image.
1580 $image_id = $variation_id ? (int) $product->get_image_id() : 0;
1581 $image_id = $image_id ?: (int) get_post_thumbnail_id( $product_id );
1582 $image = $image_id ? wp_get_attachment_image_src( $image_id, 'single-post-thumbnail' ) : false;
1583 $image_url = is_array($image) ? $image[0] : '';
1584
1585 // --- Multilingual handling ---
1586 $original_pid = 0;
1587 $current_lang = '';
1588
1589 // WPML
1590 if ( has_filter( 'wpml_element_trid' ) ) {
1591 $trid = apply_filters( 'wpml_element_trid', null, $product_id, 'post_product' );
1592 $translations = $trid ? apply_filters( 'wpml_get_element_translations', null, $trid, 'post_product' ) : [];
1593 $current_lang = apply_filters( 'wpml_current_language', null );
1594 if ( $translations && is_array( $translations ) ) {
1595 foreach ( $translations as $lang => $translation ) {
1596 if ( ! empty( $translation->original ) ) {
1597 $original_pid = (int) $translation->element_id;
1598 }
1599 }
1600 }
1601 }
1602
1603 // Polylang
1604 if ( function_exists( 'pll_get_post' ) && function_exists( 'pll_current_language' ) ) {
1605 $current_lang = pll_current_language();
1606 $default_lang = pll_default_language();
1607 if ( $current_lang && $default_lang && $current_lang !== $default_lang ) {
1608 $original_pid = pll_get_post( $product_id, $default_lang );
1609 }
1610 }
1611
1612 // Fallback
1613 $opid_value = ( $original_pid && $original_pid !== $product_id ) ? $original_pid : $product_id;
1614
1615 $product_data = array(
1616 'id' => $product_id,
1617 'pn' => $product_name,
1618 'name' => $product_name,
1619 'prc' => $product_price,
1620 'pu' => $url,
1621 'piu' => $image_url,
1622 'sku' => $product_sku,
1623 'opid' => $opid_value,
1624 'lang' => wiserrw_normalize_lang( $current_lang ),
1625 );
1626
1627 $wiserrw_order_data['line_items'][] = $product_data;
1628 }
1629
1630 // Send to API
1631 $response = wp_remote_post($api_endpoint, array(
1632 'method' => 'POST',
1633 'headers' => array(
1634 'Content-Type' => 'application/json',
1635 'x-wiserrw-api-key' => $api_key,
1636 ),
1637 'timeout' => 45,
1638 'blocking' => true,
1639 'body' => wp_json_encode($wiserrw_order_data),
1640 ));
1641
1642 if ( is_wp_error($response) ) {
1643 $api_errors[] = $response->get_error_message();
1644 } else {
1645 $code = wp_remote_retrieve_response_code($response);
1646 if ( $code !== 200 ) {
1647 $api_errors[] = wp_remote_retrieve_body($response);
1648 }
1649 }
1650 }
1651
1652 $processed = $offset + count( $chunk_ids );
1653 $percent = min(round( ( $processed / $total ) * 100 ), 100);
1654 $is_done = $processed >= $total;
1655
1656 wp_send_json_success(array(
1657 'percent' => $percent,
1658 'done' => $is_done,
1659 'total_processed' => $is_done ? $total : $processed,
1660 'total_orders' => $total,
1661 'processed_chunk' => count($chunk_ids),
1662 'api_error_messages' => $api_errors
1663 ));
1664 }
1665
1666 die();
1667 }
1668
1669 add_action( 'wp_ajax_wiserrw_bulk_orders_send', 'wiserrw_bulk_orders_send' );
1670
1671 /**
1672 * Bulk Orders count API.
1673 */
1674 function wiserrw_fetch_order_count() {
1675 check_ajax_referer( 'wiserrw_export_orders_nonce', 'wiserrw_security' );
1676 if ( ! current_user_can( 'manage_woocommerce' ) ) {
1677 wp_send_json_error( array( 'message' => __('You are not allowed to perform this action.', 'wiser-review' ) ), 403 );
1678 }
1679 $duration = isset( $_POST['duration'] ) ? sanitize_text_field( wp_unslash( $_POST['duration'] ) ) : '';
1680 $from_date = isset( $_POST['start_date'] ) ? sanitize_text_field( wp_unslash( $_POST['start_date'] ) ) : '';
1681 $to_date = isset( $_POST['end_date'] ) ? sanitize_text_field( wp_unslash( $_POST['end_date'] ) ) : '';
1682 $offset = isset( $_POST['offset'] ) ? intval( sanitize_text_field( wp_unslash( $_POST['offset'] ) ) ) : '';
1683 $limit = isset( $_POST['limit'] ) ? intval( sanitize_text_field( wp_unslash( $_POST['limit'] ) ) ) : '';
1684 if ( '' !== $duration ) {
1685 switch ( $duration ) {
1686 case '30_days':
1687 $date_from = strtotime( '-30 days' );
1688 break;
1689 case '3_months':
1690 $date_from = strtotime( '-3 months' );
1691 break;
1692 case '6_months':
1693 $date_from = strtotime( '-6 months' );
1694 break;
1695 case '1_year':
1696 $date_from = strtotime( '-1 year' );
1697 break;
1698 case '7_days':
1699 $date_from = strtotime( '-7 days' );
1700 break;
1701 default:
1702 $date_from = strtotime( '-7 days' );
1703 break;
1704 }
1705 $all_orders = wc_get_orders(
1706 array(
1707 'status' => array( 'completed', 'processing' ),
1708 'date_created' => '>' . gmdate( 'Y-m-d H:i:s', $date_from ),
1709 'limit' => -1,
1710 )
1711 );
1712 } else {
1713 $start_date = new WC_DateTime($from_date);
1714 $end_date = new WC_DateTime($to_date);
1715 $all_orders = wc_get_orders(
1716 array(
1717 'status' => array( 'completed', 'processing' ),
1718 'date_created' => gmdate( $start_date . '...' . $end_date ),
1719 'limit' => -1,
1720 )
1721 );
1722 }
1723
1724 $total = count( $all_orders );
1725 wp_send_json_success(
1726 array(
1727 'total_orders' => $total,
1728 )
1729 );
1730 }
1731 add_action( 'wp_ajax_wiserrw_fetch_order_count', 'wiserrw_fetch_order_count' );
1732
1733 function wiserrw_count_products_with_reviews() {
1734 global $wpdb;
1735
1736 return $wpdb->get_var("
1737 SELECT count(ID)
1738 FROM {$wpdb->posts}
1739 WHERE post_type = 'product'
1740 AND post_status IN ('publish', 'draft')");
1741 }
1742
1743 function wiserrw_get_products( $page, $per_page ) {
1744 global $wpdb;
1745 $offset = ( $page - 1 ) * $per_page;
1746
1747 // ORDER BY ID is required: without it MySQL row order is undefined,
1748 // so concurrent inserts/edits during sync can shift pages and cause
1749 // products to be silently skipped between page boundaries.
1750 // Free in compute terms — ID is the PRIMARY KEY (clustered index).
1751 return $wpdb->get_results( $wpdb->prepare( "
1752 SELECT ID,
1753 post_title as title,
1754 post_name as slug,
1755 post_status,
1756 comment_count
1757 FROM {$wpdb->posts}
1758 WHERE post_type = 'product'
1759 AND post_status IN ('publish', 'draft')
1760 ORDER BY ID ASC
1761 LIMIT %d, %d
1762 ", $offset, $per_page ), ARRAY_A );
1763 }
1764
1765
1766 function wiserrw_sync_products() {
1767 $wiserrw_api_settings = get_option( 'wiserrw_api_settings' );
1768 $api_host = WISERRW_API_HOST;
1769 $per_page = isset( $_REQUEST['per_page'] ) ? intval( $_REQUEST['per_page'] ) : 50;
1770 $api_key = $wiserrw_api_settings['wiserrw_api_key'];
1771 $wsid = get_option( 'wiserrw_wsid', true );
1772 $automation_id = get_option( 'wiserrw_automation_id', true );
1773
1774 if ( ! $wsid || ! $automation_id ) {
1775 wp_die();
1776 }
1777
1778 // Pagination setup
1779 if ( isset( $_REQUEST['total_pages'] ) ) {
1780 $total_pages = (int) sanitize_text_field( wp_unslash( $_REQUEST['total_pages'] ) );
1781 } else {
1782 $count = wiserrw_count_products_with_reviews();
1783 $total_pages = (int) ceil( $count / max( 1, $per_page ) );
1784 }
1785
1786 $page = isset( $_REQUEST['page'] ) ? (int) sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : 1;
1787 $finished = 0;
1788
1789 if ( $page <= $total_pages ) {
1790 $message = "Synced $page / $total_pages pages";
1791 $products = wiserrw_get_products( $page, $per_page );
1792 $products_json = [];
1793
1794 foreach ( $products as $prod ) {
1795 $p_id = (int) $prod['ID'];
1796 $registered = get_post_meta( $p_id, '_wiserrw_product_registered', true );
1797 if ( $registered ) {
1798 continue;
1799 }
1800
1801 $pr = wc_get_product( $p_id );
1802 if ( ! $pr ) {
1803 continue;
1804 }
1805
1806 // Collect SKUs
1807 $arrsku = [];
1808 if ( $pr->is_type( 'variable' ) ) {
1809 $parent_sku = (string) $pr->get_sku();
1810 if ( $parent_sku !== '' ) {
1811 $arrsku[] = $parent_sku;
1812 }
1813 $prod_variation = new WC_Product_Variable( $p_id );
1814 foreach ( $prod_variation->get_children() as $vid ) {
1815 $vsku = (string) get_post_meta( $vid, '_sku', true );
1816 if ( $vsku !== '' ) {
1817 $arrsku[] = $vsku;
1818 }
1819 }
1820 } else {
1821 $pr_sku = (string) $pr->get_sku();
1822 if ( $pr_sku !== '' ) {
1823 $arrsku[] = $pr_sku;
1824 }
1825 }
1826
1827 // Representative barcode
1828 $gtin = get_post_meta( $p_id, 'hwp_product_gtin', true )
1829 ?: $pr->get_attribute( 'GTIN' )
1830 ?: $pr->get_attribute( 'EAN' )
1831 ?: $pr->get_attribute( 'ISBN' )
1832 ?: get_post_meta( $p_id, '_global_unique_id', true );
1833
1834 // Brand (from taxonomies, attributes, or meta)
1835 $brand = wiserrw_get_product_brand( $pr );
1836
1837 // Title and URL
1838 $title = $prod['title'];
1839 if ( empty( $title ) ) {
1840 $cats = get_the_terms( $p_id, 'product_cat' );
1841 $title = ( ! empty( $cats ) && ! is_wp_error( $cats ) ) ? $cats[0]->name : get_bloginfo( 'name' );
1842 }
1843
1844 $product_url = get_permalink( $p_id );
1845 if ( ! $product_url || $prod['post_status'] === 'draft' ) {
1846 if ( $prod['post_status'] === 'draft' ) {
1847 $product_url = home_url( '/?post_type=product&p=' . $p_id );
1848 } else {
1849 $shop_page_url = get_permalink( wc_get_page_id( 'shop' ) );
1850 $product_url = $shop_page_url ? $shop_page_url : home_url( '/product/' );
1851 $product_url = trailingslashit( $product_url ) . sanitize_title( $title );
1852 }
1853 }
1854
1855 // Image URL
1856 $img_src = wp_get_attachment_image_src( get_post_thumbnail_id( $p_id ), 'shop_single' );
1857 $img_url = ( is_array( $img_src ) && isset( $img_src[0] ) ) ? $img_src[0] : wc_placeholder_img_src( 'shop_single' );
1858
1859 // --- Multilingual handling (product-specific) ---
1860 $original_pid = 0;
1861 $current_lang = '';
1862
1863 // WPML
1864 if ( has_filter( 'wpml_element_trid' ) ) {
1865 $trid = apply_filters( 'wpml_element_trid', null, $p_id, 'post_product' );
1866 $translations = $trid ? apply_filters( 'wpml_get_element_translations', null, $trid, 'post_product' ) : [];
1867 if ( $translations && is_array( $translations ) ) {
1868 foreach ( $translations as $lang => $translation ) {
1869 if ( ! empty( $translation->original ) ) {
1870 $original_pid = (int) $translation->element_id;
1871 }
1872 if ( (int) $translation->element_id === $p_id ) {
1873 $current_lang = $lang; // �
1874 product-specific language
1875 }
1876 }
1877 }
1878 }
1879
1880 // Polylang
1881 if ( function_exists( 'pll_get_post' ) && function_exists( 'pll_get_post_language' ) ) {
1882 $current_lang = pll_get_post_language( $p_id ); // �
1883 product-specific language
1884 $default_lang = function_exists( 'pll_default_language' ) ? pll_default_language() : '';
1885 if ( $current_lang && $default_lang && $current_lang !== $default_lang ) {
1886 $original_pid = pll_get_post( $p_id, $default_lang );
1887 }
1888 }
1889
1890 // Fallback
1891 $opid_value = ( $original_pid && $original_pid !== $p_id ) ? $original_pid : $p_id;
1892
1893 // Build product payload
1894 $product_json = [
1895 'pid' => $p_id,
1896 'arrsku' => $arrsku,
1897 'shp' => get_site_url(),
1898 'wsid' => $wsid,
1899 'pn' => $title,
1900 'piu' => esc_url( $img_url ),
1901 'pu' => esc_url( $product_url ),
1902 'barcode' => (string) $gtin,
1903 'brand' => (string) $brand,
1904 'opid' => $opid_value,
1905 'lang' => wiserrw_normalize_lang( $current_lang ),
1906 ];
1907
1908 $products_json[] = $product_json;
1909
1910 // Note: do NOT mark as registered here. We only set the flag
1911 // after the batch POST returns 200 below — otherwise a failed
1912 // API call (network error, 5xx, timeout) would silently lose
1913 // up to 50 products per failed batch, since the next Sync
1914 // would skip them as "already registered".
1915 }
1916
1917 if ( ! empty( $products_json ) ) {
1918 $data = [
1919 'method' => 'POST',
1920 'blocking' => true,
1921 'headers' => [
1922 'Content-Type' => 'application/json',
1923 'x-wiserrw-api-key' => $api_key,
1924 ],
1925 'body' => wp_json_encode( [
1926 'products' => $products_json,
1927 'ver' => WISERRW_PLUGIN_VERSION // �
1928 Added once at root level
1929 ] ),
1930 ];
1931 $url = $api_host . 'productWebhook?wsid=' . $wsid . '&key=' . $api_key;
1932 $response = wp_safe_remote_post( $url, $data );
1933
1934 // Only mark products as registered if the backend confirmed receipt.
1935 // Failed batches stay unregistered → automatically retried on the
1936 // next Sync click (no Reset needed).
1937 if ( ! is_wp_error( $response ) && (int) wp_remote_retrieve_response_code( $response ) === 200 ) {
1938 foreach ( $products_json as $payload ) {
1939 if ( ! empty( $payload['pid'] ) ) {
1940 update_post_meta( (int) $payload['pid'], '_wiserrw_product_registered', '1' );
1941 }
1942 }
1943 }
1944 }
1945 } else {
1946 $message = 'Finished';
1947 $finished = 1;
1948 }
1949
1950 if ( $page == $total_pages ) {
1951 update_option( 'wiserrw_all_products_synced', 1 );
1952 }
1953
1954 $page++;
1955 $result = [
1956 'next_page' => $page,
1957 'total_pages' => $total_pages,
1958 'message' => $message,
1959 'finished' => $finished,
1960 ];
1961
1962 echo wp_json_encode( $result );
1963 wp_die();
1964 }
1965
1966
1967 add_action( 'wp_ajax_wiserrw_sync_products', 'wiserrw_sync_products' );
1968 add_action( 'wp_ajax_nopriv_wiserrw_sync_products', 'wiserrw_sync_products' );
1969
1970
1971 function wiserrw_reset_products() {
1972 global $wpdb;
1973
1974 check_ajax_referer( 'wiserrw_export_orders_nonce', 'wiserrw_security' );
1975 if ( ! current_user_can( 'manage_woocommerce' ) ) {
1976 wp_send_json_error( array( 'message' => __('You are not allowed to perform this action.','wiser-review' ) ), 403 );
1977 }
1978 // Clear only the "registered" flag for all products
1979 $wpdb->query(
1980 $wpdb->prepare(
1981 "DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s",
1982 '_wiserrw_product_registered'
1983 )
1984 );
1985
1986 // JSON response (unchanged style)
1987 $result = array(
1988 'message' => __( 'Sync status cleared. Click Sync Products to re-sync your product catalog with WiserReview.', 'wiser-review' ),
1989 );
1990 echo wp_json_encode( $result );
1991 wp_die();
1992 }
1993
1994 // Register the AJAX action
1995 add_action( 'wp_ajax_wiserrw_reset_products', 'wiserrw_reset_products' );
1996 add_action( 'wp_ajax_nopriv_wiserrw_reset_products', 'wiserrw_reset_products' );
1997
1998
1999
2000 function wiserrw_product_delete_hook( $p_id ) {
2001 if ( get_post_type( $p_id ) === 'product' ) {
2002 $wiserrw_api_settings = get_option( 'wiserrw_api_settings' );
2003 $api_host = WISERRW_API_HOST;
2004 $api_key = $wiserrw_api_settings['wiserrw_api_key'];
2005 $wsid = get_option( 'wiserrw_wsid', true );
2006 $automation_id = get_option( 'wiserrw_automation_id', true );
2007 if (!isset($wsid) || !isset($automation_id)) {
2008 return; // Exit if invalid API data
2009 }
2010 $url = $api_host . 'productWebhook/'.$p_id.'?wsid='.$wsid.'&key='.$api_key;
2011 $data = array(
2012 'method' => 'DELETE',
2013 'headers' => array(
2014 'x-wiserrw-api-key' => $api_key,
2015 ),
2016 );
2017 $response = wp_remote_request( $url, $data );
2018 }
2019
2020 }
2021 add_action( 'deleted_post', 'wiserrw_product_delete_hook' );
2022 add_action( 'trashed_post', 'wiserrw_product_delete_hook' );
2023
2024 /* -------------------- Product Duplicate Hook -------------------- */
2025
2026 /**
2027 * Strip WiserReview-specific cached data when WooCommerce duplicates a
2028 * product. Without this, "Duplicate" copies all postmeta from the source
2029 * product including:
2030 *
2031 * - wiserrw_star_rating_html / wiserrw_product_html / wiserrw_schema_json
2032 * → the new product would visibly show the source's review count
2033 * and aggregate rating to shoppers and to Google.
2034 *
2035 * - _wiserrw_product_registered → bulk Sync would skip the duplicate
2036 * thinking it's already on the backend, so the new product never
2037 * gets registered.
2038 *
2039 * - _wiserrw_watch_hash → the per-save hook compares against this old
2040 * hash, sees "no change", and never pushes the duplicate to the
2041 * WiserReview backend even after the merchant edits and saves it.
2042 *
2043 * Excluding these keys at copy-time is the canonical WC fix. The
2044 * duplicate then behaves like a brand-new product: empty rating cache
2045 * until reviews come in, fresh push to backend on next save.
2046 *
2047 * @param array $exclude_meta Existing list of meta keys WC will skip.
2048 * @return array Updated exclusion list.
2049 */
2050 add_filter( 'woocommerce_duplicate_product_exclude_meta', 'wiserrw_exclude_meta_on_duplicate' );
2051 function wiserrw_exclude_meta_on_duplicate( $exclude_meta ) {
2052 return array_merge( (array) $exclude_meta, array(
2053 'wiserrw_star_rating_html',
2054 'wiserrw_product_html',
2055 'wiserrw_schema_json',
2056 '_wiserrw_product_registered',
2057 '_wiserrw_watch_hash',
2058 ) );
2059 }
2060
2061 /* -------------------- Product Update Hook -------------------- */
2062
2063 // Only send when name / slug / image / SKU / barcode changed.
2064
2065
2066 // Build the watch-hash from the product's currently-watched fields. Used
2067 // (a) synchronously inside wiserrw_product_update_hook() to skip enqueueing
2068 // jobs that wouldn't change anything, and (b) again inside the async handler
2069 // as a safety check against state drift between enqueue and execution.
2070 function wiserrw_compute_watch_hash( $product ) {
2071 if ( ! $product instanceof WC_Product ) {
2072 return '';
2073 }
2074
2075 $p_id = $product->get_id();
2076 $is_variation = $product instanceof WC_Product_Variation;
2077 $parent_id = $is_variation ? $product->get_parent_id() : $p_id;
2078
2079 $name = (string) $product->get_name();
2080 $permalink = (string) get_permalink( $is_variation ? $parent_id : $p_id );
2081
2082 $effective_image_id = (int) $product->get_image_id();
2083 if ( ! $effective_image_id ) {
2084 $effective_image_id = (int) get_post_thumbnail_id( $is_variation ? $parent_id : $p_id );
2085 }
2086 $image_id = (string) $effective_image_id;
2087
2088 if ( $product->is_type( 'variable' ) && ! $is_variation ) {
2089 $child_skus = array();
2090 foreach ( $product->get_children() as $vid ) {
2091 $vsku = (string) get_post_meta( $vid, '_sku', true );
2092 if ( $vsku !== '' ) {
2093 $child_skus[] = $vsku;
2094 }
2095 }
2096 sort( $child_skus, SORT_NATURAL | SORT_FLAG_CASE );
2097 $sku_fingerprint = implode( ',', $child_skus );
2098 } else {
2099 $sku_fingerprint = (string) $product->get_sku();
2100 }
2101
2102 if ( $is_variation ) {
2103 $barcode = get_post_meta( $p_id, 'hwp_var_gtin', true )
2104 ?: get_post_meta( $parent_id, 'hwp_product_gtin', true )
2105 ?: $product->get_attribute( 'GTIN' )
2106 ?: $product->get_attribute( 'EAN' )
2107 ?: $product->get_attribute( 'ISBN' )
2108 ?: get_post_meta( $p_id, '_global_unique_id', true );
2109 } else {
2110 $barcode = get_post_meta( $p_id, 'hwp_product_gtin', true )
2111 ?: $product->get_attribute( 'GTIN' )
2112 ?: $product->get_attribute( 'EAN' )
2113 ?: $product->get_attribute( 'ISBN' )
2114 ?: get_post_meta( $p_id, '_global_unique_id', true );
2115 }
2116 $barcode = (string) $barcode;
2117
2118 $brand = wiserrw_get_product_brand( $is_variation ? $parent_id : $p_id );
2119
2120 return md5( $name . '|' . $permalink . '|' . $image_id . '|' . $sku_fingerprint . '|' . $barcode . '|' . $brand );
2121 }
2122
2123 add_action( 'woocommerce_after_product_object_save', 'wiserrw_product_update_hook', 99, 1 );
2124
2125 function wiserrw_product_update_hook( $product ) {
2126 if ( ! $product instanceof WC_Product ) {
2127 return;
2128 }
2129
2130 $p_id = $product->get_id();
2131
2132 // Skip autosaves / revisions
2133 if ( wp_is_post_autosave( $p_id ) || wp_is_post_revision( $p_id ) ) {
2134 return;
2135 }
2136
2137 // Pre-enqueue hash check: if no watched field changed, don't queue a job
2138 // at all. Hash computation is cheap (meta reads + string concat, no HTTP)
2139 // — adds ~1–2ms to the save but prevents bulk imports / WPML / ERP syncs
2140 // from flooding Action Scheduler with thousands of no-op jobs.
2141 $new_hash = wiserrw_compute_watch_hash( $product );
2142 $old_hash = (string) get_post_meta( $p_id, '_wiserrw_watch_hash', true );
2143 if ( $new_hash !== '' && $new_hash === $old_hash ) {
2144 return;
2145 }
2146
2147 // Defer the watch-hash check, payload build, and outbound productWebhook
2148 // POST to a background job so the WC product save POST is not held up by
2149 // the remote HTTP call. The handler re-fetches the product, so it always
2150 // works against the latest persisted state at job-run time.
2151 $args = array( (string) $p_id );
2152 if ( function_exists( 'as_enqueue_async_action' ) ) {
2153 // Dedup: skip if a job for this exact pid is already pending.
2154 // Avoids piling up identical jobs on bulk edits / imports / WPML
2155 // translations re-saving the same product in quick succession.
2156 if ( ! as_has_scheduled_action( 'wiserrw_async_product_update', $args, 'wiserreview' ) ) {
2157 as_enqueue_async_action( 'wiserrw_async_product_update', $args, 'wiserreview' );
2158 }
2159 } else {
2160 wp_schedule_single_event( time() + 1, 'wiserrw_async_product_update', $args );
2161 }
2162 }
2163
2164 add_action( 'wiserrw_async_product_update', 'wiserrw_async_product_update_handler', 10, 1 );
2165
2166 function wiserrw_async_product_update_handler( $p_id ) {
2167 $product = wc_get_product( $p_id );
2168 if ( ! $product instanceof WC_Product ) {
2169 return;
2170 }
2171
2172 $is_variation = $product instanceof WC_Product_Variation;
2173 $parent_id = $is_variation ? $product->get_parent_id() : $p_id;
2174
2175 // --- Current values we watch ---
2176 $name = (string) $product->get_name();
2177 $permalink = (string) get_permalink( $is_variation ? $parent_id : $p_id );
2178
2179 // Effective image id (variation image, else parent/simple image)
2180 $effective_image_id = (int) $product->get_image_id();
2181 if ( ! $effective_image_id ) {
2182 $effective_image_id = (int) get_post_thumbnail_id( $is_variation ? $parent_id : $p_id );
2183 }
2184 $image_id = (string) $effective_image_id;
2185
2186 // SKU fingerprint
2187 if ( $product->is_type( 'variable' ) && ! $is_variation ) {
2188 $child_skus = array();
2189 foreach ( $product->get_children() as $vid ) {
2190 $vsku = (string) get_post_meta( $vid, '_sku', true );
2191 if ( $vsku !== '' ) {
2192 $child_skus[] = $vsku;
2193 }
2194 }
2195 sort( $child_skus, SORT_NATURAL | SORT_FLAG_CASE );
2196 $sku_fingerprint = implode( ',', $child_skus );
2197 } else {
2198 $sku_fingerprint = (string) $product->get_sku();
2199 }
2200
2201 // Barcode
2202 if ( $is_variation ) {
2203 $barcode = get_post_meta( $p_id, 'hwp_var_gtin', true )
2204 ?: get_post_meta( $parent_id, 'hwp_product_gtin', true )
2205 ?: $product->get_attribute( 'GTIN' )
2206 ?: $product->get_attribute( 'EAN' )
2207 ?: $product->get_attribute( 'ISBN' )
2208 ?: get_post_meta( $p_id, '_global_unique_id', true );
2209 } else {
2210 $barcode = get_post_meta( $p_id, 'hwp_product_gtin', true )
2211 ?: $product->get_attribute( 'GTIN' )
2212 ?: $product->get_attribute( 'EAN' )
2213 ?: $product->get_attribute( 'ISBN' )
2214 ?: get_post_meta( $p_id, '_global_unique_id', true );
2215 }
2216 $barcode = (string) $barcode;
2217
2218 // Brand (detected from taxonomies, attributes, or meta)
2219 $brand = wiserrw_get_product_brand( $is_variation ? $parent_id : $p_id );
2220
2221 // Watch hash
2222 $new_hash = md5( $name . '|' . $permalink . '|' . $image_id . '|' . $sku_fingerprint . '|' . $barcode . '|' . $brand );
2223 $old_hash = (string) get_post_meta( $p_id, '_wiserrw_watch_hash', true );
2224 if ( $new_hash === $old_hash ) {
2225 return;
2226 }
2227
2228 // --- API settings ---
2229 $wiserrw_api_settings = get_option( 'wiserrw_api_settings' );
2230 if ( empty( $wiserrw_api_settings['wiserrw_api_key'] ) ) {
2231 return;
2232 }
2233 $api_host = WISERRW_API_HOST;
2234 $api_key = $wiserrw_api_settings['wiserrw_api_key'];
2235 $wsid = get_option( 'wiserrw_wsid', true );
2236 $automation_id = get_option( 'wiserrw_automation_id', true );
2237 if ( ! $wsid || ! $automation_id ) {
2238 return;
2239 }
2240
2241 // arrsku
2242 $arrsku = array();
2243 if ( $product->is_type( 'variable' ) && ! $is_variation ) {
2244 $parent_sku = (string) $product->get_sku();
2245 if ( $parent_sku !== '' ) {
2246 $arrsku[] = $parent_sku;
2247 }
2248 foreach ( $product->get_children() as $vid ) {
2249 $vsku = (string) get_post_meta( $vid, '_sku', true );
2250 if ( $vsku !== '' ) {
2251 $arrsku[] = $vsku;
2252 }
2253 }
2254 } else {
2255 if ( $sku_fingerprint !== '' ) {
2256 $arrsku[] = $sku_fingerprint;
2257 }
2258 }
2259
2260 // Image URL
2261 $thumb_id = $effective_image_id;
2262 $img_src = $thumb_id ? wp_get_attachment_image_src( $thumb_id, 'shop_single' ) : false;
2263 $img_url = ( is_array( $img_src ) && isset( $img_src[0] ) ) ? $img_src[0] : wc_placeholder_img_src( 'shop_single' );
2264
2265 // --- Multilingual handling (Added) ---
2266 $original_pid = 0;
2267 $current_lang = '';
2268
2269 // WPML
2270 if ( has_filter( 'wpml_element_trid' ) ) {
2271 $trid = apply_filters( 'wpml_element_trid', null, $p_id, 'post_product' );
2272 $translations = $trid ? apply_filters( 'wpml_get_element_translations', null, $trid, 'post_product' ) : array();
2273 if ( $translations && is_array( $translations ) ) {
2274 foreach ( $translations as $lang => $translation ) {
2275 if ( ! empty( $translation->original ) ) {
2276 $original_pid = (int) $translation->element_id;
2277 }
2278 if ( (int) $translation->element_id === $p_id ) {
2279 $current_lang = $lang;
2280 }
2281 }
2282 }
2283 }
2284
2285 // Polylang
2286 if ( function_exists( 'pll_get_post_language' ) ) {
2287 $current_lang = pll_get_post_language( $p_id );
2288 if ( function_exists( 'pll_default_language' ) ) {
2289 $default_lang = pll_default_language();
2290 if ( $current_lang && $default_lang && $current_lang !== $default_lang ) {
2291 $original_pid = pll_get_post( $p_id, $default_lang );
2292 }
2293 }
2294 }
2295
2296 // Fallbacks
2297 $opid_value = ( $original_pid && $original_pid !== $p_id ) ? $original_pid : $p_id;
2298
2299 // --- Build product payload ---
2300 $product_json = array(
2301 'pid' => (string) $p_id,
2302 'arrsku' => $arrsku,
2303 'shp' => get_site_url(),
2304 'wsid' => $wsid,
2305 'pn' => $name ?: get_the_title( $parent_id ),
2306 'piu' => $img_url,
2307 'pu' => $permalink,
2308 'barcode' => $barcode,
2309 'brand' => (string) $brand,
2310 'opid' => $opid_value,
2311 'lang' => wiserrw_normalize_lang( $current_lang ),
2312 );
2313
2314 $args = array(
2315 'method' => 'POST',
2316 'blocking' => true,
2317 'headers' => array(
2318 'Content-Type' => 'application/json',
2319 'x-wiserrw-api-key' => $api_key,
2320 ),
2321 'body' => wp_json_encode( array(
2322 'products' => array( $product_json ),
2323 'ver' => WISERRW_PLUGIN_VERSION
2324 ) ),
2325 );
2326
2327 // Clear flag before send
2328 delete_post_meta( $p_id, '_wiserrw_product_registered' );
2329
2330 $response = wp_safe_remote_post( $api_host . 'productWebhook?wsid=' . $wsid . '&key=' . $api_key, $args );
2331 if ( is_wp_error( $response ) ) {
2332 return;
2333 }
2334
2335 if ( (int) wp_remote_retrieve_response_code( $response ) === 200 ) {
2336 update_post_meta( $p_id, '_wiserrw_product_registered', '1' );
2337 update_post_meta( $p_id, '_wiserrw_watch_hash', $new_hash );
2338 }
2339 }
2340
2341
2342
2343 function wiserrw_custom_endpoints() {
2344 register_rest_route(
2345 'wiserrw/v1',
2346 '/reviews',
2347 array(
2348 array(
2349 'methods' => 'POST',
2350 'permission_callback' => 'wiserrw_api_auth',
2351 'callback' => 'wiserrw_api_callback',
2352 'args' => array(),
2353 )
2354 )
2355 );
2356 }
2357 add_action( 'rest_api_init', 'wiserrw_custom_endpoints' );
2358
2359
2360 function wiserrw_api_auth( WP_REST_Request $request ) {
2361 $provided = $request->get_header( 'wiserrw-wsid' );
2362 if ( ! $provided ) {
2363 $provided = $request->get_param( 'wiserrw_wsid' );
2364 }
2365
2366 if ( ! is_string( $provided ) || $provided === '' ) {
2367 return new WP_Error(
2368 'wiserrw_auth_missing',
2369 'Missing API key. Send it in the API Key header or api_key.',
2370 array( 'status' => 401 )
2371 );
2372 }
2373
2374 $wiserrw_api_settings = get_option( 'wiserrw_api_settings', array() );
2375 $wiserrw_wsid = get_option( 'wiserrw_wsid', true );
2376 if ($wiserrw_wsid == '') {
2377 return new WP_Error(
2378 'wiserrw_auth_config',
2379 'API key not configured.',
2380 array( 'status' => 500 )
2381 );
2382 }
2383
2384 $api_key = $wiserrw_wsid;
2385 $valid_keys = array($api_key);
2386
2387 foreach ( $valid_keys as $valid ) {
2388 if ( hash_equals( $valid, $provided ) ) {
2389 return true;
2390 }
2391 } return new WP_Error(
2392 'wiserrw_auth_missing',
2393 'Invalid API key.',
2394 array( 'status' => 401 )
2395 );
2396 }
2397
2398
2399
2400 // === MAIN API ENDPOINT (returns instantly) ===
2401 function wiserrw_api_callback( WP_REST_Request $request ) {
2402
2403 $payload = array(
2404 'status' => 'ok',
2405 'method' => $request->get_method(),
2406 'time' => current_time( 'mysql', true ),
2407 'data' => $request->get_json_params() ?: $request->get_params(),
2408 );
2409
2410 // Fire background call to self (non-blocking).
2411 // The /async endpoint runs the heavy work (css/html sync, AS
2412 // enqueues for schema) in a separate PHP-FPM worker, so the
2413 // visitor's pixel request returns immediately on healthy stacks.
2414 wp_remote_post(
2415 rest_url( 'wiserreview/v1/async' ),
2416 array(
2417 'blocking' => false,
2418 'timeout' => 3,
2419 'body' => array(
2420 'json_data' => wp_json_encode( $payload ),
2421 ),
2422 )
2423 );
2424
2425 // Return immediately
2426 return new WP_REST_Response( $payload, 200 );
2427 }
2428 add_action( 'rest_api_init', function() {
2429 register_rest_route( 'wiserreview/v1', '/api', array(
2430 'methods' => 'POST',
2431 'callback' => 'wiserrw_api_callback',
2432 'permission_callback' => '__return_true',
2433 ));
2434 });
2435
2436 // === ASYNC BACKGROUND HANDLER ===
2437 add_action( 'rest_api_init', function() {
2438 register_rest_route( 'wiserreview/v1', '/async', array(
2439 'methods' => 'POST',
2440 'callback' => 'wiserrw_async_handler',
2441 'permission_callback' => '__return_true',
2442 ));
2443 });
2444
2445 function wiserrw_async_handler( WP_REST_Request $request ) {
2446
2447 $json_data = $request->get_param( 'json_data' );
2448 if ( empty( $json_data ) ) {
2449 return new WP_REST_Response( array( 'error' => 'No data received' ), 400 );
2450 }
2451
2452 $payload = json_decode( $json_data, true );
2453 if ( empty( $payload['data'] ) || ! is_array( $payload['data'] ) ) {
2454 return new WP_REST_Response( array( 'error' => 'Invalid payload' ), 400 );
2455 }
2456
2457 // Build a fake REST request to reuse your existing logic
2458 $fake_request = new WP_REST_Request( 'POST', '/wiserreview/v1/api' );
2459
2460 foreach ( $payload['data'] as $key => $value ) {
2461 $fake_request->set_param( $key, $value );
2462 }
2463
2464 // Force JSON body so get_json_params() works inside callback
2465 $fake_request->set_body( wp_json_encode( $payload['data'] ) );
2466 $fake_request->set_header( 'Content-Type', 'application/json' );
2467
2468 // Run your main process in background
2469 do_action( 'wiserrw_endpoint_called', $fake_request );
2470
2471 return new WP_REST_Response( array( 'status' => 'background task started' ), 200 );
2472 }
2473
2474 // === MAIN LOGIC HANDLER ===
2475 function wiserrw_api_response_callback( $request ) {
2476
2477 // --- Safely parse JSON and normal params ---
2478 $raw = $request->get_body();
2479 $data = json_decode( $raw, true );
2480
2481 // Fallback if JSON body not parsed
2482 if ( empty( $data ) || ! is_array( $data ) ) {
2483 $data = $request->get_json_params() ?: $request->get_params();
2484 }
2485
2486 if ( empty( $data ) || ! is_array( $data ) ) {
2487 return;
2488 }
2489 // �
2490 CHANGED: read everything from body, not from query
2491 $wsid_param = $data['wsid'] ?? '';
2492 $verify_api = $data['verifyApi'] ?? '';
2493 $updtyp = $data['updtyp'] ?? '';
2494 $widget_id = $data['widgetId'] ?? '';
2495
2496 // Current workspace ID saved in site
2497 $wsid = get_option( 'wiserrw_wsid', '' );
2498
2499 switch ( $updtyp ) {
2500
2501 // --- CSS update ---
2502 case 'update_css':
2503 // �
2504 CHANGED: verifyApi now read from body (string "true")
2505 $is_verified = filter_var( $verify_api, FILTER_VALIDATE_BOOLEAN );
2506
2507 if ( $is_verified && $wsid !== '' && $wsid === $wsid_param ) {
2508 $wiserrw_api_settings = get_option( 'wiserrw_api_settings', array() );
2509 $api_key = $wiserrw_api_settings['wiserrw_api_key'] ?? '';
2510
2511 // Validate API and update CSS
2512 wiserrw_validate_api( $api_key );
2513 wiserrw_update_css( $wsid );
2514 }
2515 break;
2516
2517 // --- HTML update (clear widget cache) ---
2518 case 'update_html':
2519 if ( ! empty( $widget_id ) ) {
2520 $option_key = 'wiserreview_widget_' . $widget_id;
2521 delete_option( $option_key );
2522 wiserrw_update_html( $wsid, $widget_id );
2523 }
2524 break;
2525
2526 // --- Schema generation and async remote update ---
2527 case 'update_schema':
2528 if ( ! empty( $data['arrPid'] ) && is_array( $data['arrPid'] ) ) {
2529 $wsid_to_use = $data['wsid'] ?? $wsid;
2530 $async_api_settings = get_option( 'wiserrw_api_settings', array() );
2531 $async_api_key = $async_api_settings['wiserrw_api_key'] ?? '';
2532
2533 foreach ( $data['arrPid'] as $p_id ) {
2534 if ( ! empty( $p_id ) && is_numeric( $p_id ) ) {
2535
2536 // Enqueue one async job per product so each pid runs in
2537 // its own short-lived background worker. Avoids holding
2538 // one PHP-FPM worker hostage for the whole arrPid loop
2539 // (which would slow down concurrent WC admin/frontend
2540 // requests) and survives max_execution_time.
2541 $args = array( (string) $p_id, $wsid_to_use, $async_api_key );
2542
2543 if ( function_exists( 'as_enqueue_async_action' ) ) {
2544 // Dedup: skip if a job for this exact pid (with same
2545 // wsid + api key) is already pending. Note: args must
2546 // match exactly — if the API key rotates between
2547 // calls, the old pending job will not be deduped.
2548 if ( ! as_has_scheduled_action( 'wiserrw_async_schema_update', $args, 'wiserreview' ) ) {
2549 as_enqueue_async_action( 'wiserrw_async_schema_update', $args, 'wiserreview' );
2550 }
2551 } else {
2552 wp_schedule_single_event( time() + 1, 'wiserrw_async_schema_update', $args );
2553 }
2554 }
2555 }
2556 }
2557 break;
2558
2559 default:
2560 // No action for unknown updtyp
2561 break;
2562 }
2563 }
2564 add_action( 'wiserrw_endpoint_called', 'wiserrw_api_response_callback' );
2565
2566 add_action( 'wiserrw_async_schema_update', 'wiserrw_async_schema_update_handler', 10, 3 );
2567
2568 function wiserrw_async_schema_update_handler( $p_id, $wsid, $api_key ) {
2569 if ( empty( $p_id ) || ! is_numeric( $p_id ) ) {
2570 return;
2571 }
2572 $p_id = (string) $p_id;
2573
2574 // Run schema generation now (needs WP context)
2575 wiserrw_generate_schema( $p_id );
2576
2577 // Send async schema update (non-blocking)
2578 wp_remote_post(
2579 WISERRW_RS_HOST . '/api/wp/updSchema',
2580 array(
2581 'method' => 'POST',
2582 'headers' => array(
2583 'Content-Type' => 'application/json',
2584 'x-wiserrw-api-key' => $api_key,
2585 ),
2586 'body' => wp_json_encode( array(
2587 'wsid' => $wsid,
2588 'pid' => $p_id,
2589 'updtyp' => 'update_schema',
2590 'ver' => WISERRW_PLUGIN_VERSION,
2591 )),
2592 'blocking' => false,
2593 'timeout' => 3,
2594 )
2595 );
2596 }
2597
2598 function wiserrw_update_css( $wsid ) {
2599 $url = WISERRW_RS_HOST . '/api/wp/updSchema';
2600 $wiserrw_api_settings = get_option( 'wiserrw_api_settings', array() );
2601 $api_key = $wiserrw_api_settings['wiserrw_api_key'] ?? '';
2602 $body = array(
2603 'wsid' => $wsid,
2604 'updtyp' => 'update_css',
2605 'ver' => WISERRW_PLUGIN_VERSION
2606 );
2607 $response = wp_remote_post(
2608 $url,
2609 array(
2610 'method' => 'POST',
2611 'headers' => array(
2612 'Content-Type' => 'application/json',
2613 'x-wiserrw-api-key' => $api_key,
2614 ),
2615 'body' => wp_json_encode( $body ),
2616 'timeout' => 30,
2617 )
2618 );
2619 }
2620
2621 function wiserrw_update_schema( $wsid, $pid ) {
2622 $url = WISERRW_RS_HOST . '/api/wp/updSchema';
2623 $wiserrw_api_settings = get_option( 'wiserrw_api_settings', array() );
2624 $api_key = $wiserrw_api_settings['wiserrw_api_key'] ?? '';
2625 $body = array(
2626 'wsid' => $wsid,
2627 'pid' => $pid,
2628 'updtyp' => 'update_schema',
2629 'ver' => WISERRW_PLUGIN_VERSION
2630 );
2631 $response = wp_remote_post(
2632 $url,
2633 array(
2634 'method' => 'POST',
2635 'headers' => array(
2636 'Content-Type' => 'application/json',
2637 'x-wiserrw-api-key' => $api_key,
2638 ),
2639 'body' => wp_json_encode( $body ),
2640 'timeout' => 30,
2641 )
2642 );
2643 }
2644
2645 function wiserrw_update_html( $wsid, $widget_id ) {
2646 $url = WISERRW_RS_HOST . '/api/wp/updSchema';
2647 $wiserrw_api_settings = get_option( 'wiserrw_api_settings', array() );
2648 $api_key = $wiserrw_api_settings['wiserrw_api_key'] ?? '';
2649 $body = array(
2650 'wsid' => $wsid,
2651 'widgetId' => $widget_id,
2652 'updtyp' => 'update_html',
2653 'ver' => WISERRW_PLUGIN_VERSION
2654 );
2655 $response = wp_remote_post(
2656 $url,
2657 array(
2658 'method' => 'POST',
2659 'headers' => array(
2660 'Content-Type' => 'application/json',
2661 'x-wiserrw-api-key' => $api_key,
2662 ),
2663 'body' => wp_json_encode( $body ),
2664 'timeout' => 30,
2665 )
2666 );
2667 }
2668
2669
2670 function wiserrw_get_lang_context( $product_id ) {
2671 $ctx = array(
2672 'original_pid' => 0,
2673 'current_pid' => (int) $product_id,
2674 'lang' => '',
2675 );
2676
2677 if ( function_exists( 'pll_get_post' ) ) {
2678 $ctx['original_pid'] = (int) pll_get_post( $product_id, pll_default_language() );
2679 $ctx['lang'] = (string) pll_get_post_language( $product_id );
2680 return $ctx;
2681 }
2682
2683 if ( has_filter( 'wpml_object_id' ) ) {
2684 $trid = apply_filters( 'wpml_element_trid', null, $product_id, 'post_product' );
2685 $translations = $trid ? apply_filters( 'wpml_get_element_translations', null, $trid, 'post_product' ) : array();
2686 if ( is_array( $translations ) ) {
2687 foreach ( $translations as $lang => $t ) {
2688 $eid = is_object($t) ? (int)$t->element_id : (int)($t['element_id'] ?? 0);
2689 if ( ! empty( $t->original ) || ( is_array($t) && ! empty($t['original']) ) ) {
2690 $ctx['original_pid'] = $eid;
2691 }
2692 if ( $eid === (int) $product_id ) {
2693 $ctx['lang'] = $lang;
2694 }
2695 }
2696 }
2697 }
2698
2699 return $ctx;
2700 }
2701
2702 function wiserrw_generate_schema( $p_id ) {
2703 if ( empty( $p_id ) || ! is_numeric( $p_id ) ) {
2704 return;
2705 }
2706
2707 // --- Multilingual handling (WPML + Polylang safe) ---
2708 $original_pid = 0;
2709 $current_lang = '';
2710
2711 // WPML
2712 if ( has_filter( 'wpml_element_trid' ) ) {
2713 $trid = apply_filters( 'wpml_element_trid', null, $p_id, 'post_product' );
2714 $translations = $trid ? apply_filters( 'wpml_get_element_translations', null, $trid, 'post_product' ) : array();
2715 $current_lang = apply_filters( 'wpml_current_language', null );
2716 if ( $translations && is_array( $translations ) ) {
2717 foreach ( $translations as $lang => $translation ) {
2718 if ( ! empty( $translation->original ) ) {
2719 $original_pid = (int) $translation->element_id;
2720 }
2721 }
2722 }
2723 }
2724
2725 // Polylang
2726 if ( function_exists( 'pll_get_post' ) && function_exists( 'pll_current_language' ) ) {
2727 $current_lang = pll_current_language();
2728 $default_lang = pll_default_language();
2729 if ( $current_lang && $default_lang && $current_lang !== $default_lang ) {
2730 $original_pid = pll_get_post( $p_id, $default_lang );
2731 }
2732 }
2733
2734 $opid_value = ( $original_pid && $original_pid !== $p_id ) ? $original_pid : $p_id;
2735 $lang_value = wiserrw_normalize_lang( $current_lang );
2736
2737 // --- API settings ---
2738 $wiserrw_api_settings = get_option( 'wiserrw_api_settings', array() );
2739 if ( ! is_array( $wiserrw_api_settings ) || empty( $wiserrw_api_settings['wiserrw_api_key'] ) ) {
2740 return;
2741 }
2742
2743 $wsid = get_option( 'wiserrw_wsid', true );
2744 $automation_id = get_option( 'wiserrw_automation_id', true );
2745 if ( ! $wsid || ! $automation_id ) {
2746 return;
2747 }
2748
2749 $api_key = $wiserrw_api_settings['wiserrw_api_key'];
2750 $url = WISERRW_RS_HOST . '/api/getData';
2751 $body = array(
2752 'arrType' => array( 'main', 'star_rating' ),
2753 'pid' => (string) $p_id,
2754 'opid' => (string) $opid_value,
2755 'lang' => $lang_value,
2756 'arrPid' => array( (string) $p_id ),
2757 'isFrom' => 'woocommerce',
2758 'wsid' => $wsid,
2759 'updtyp' => 'update_schema',
2760 'ver' => WISERRW_PLUGIN_VERSION
2761 );
2762
2763 $response = wp_remote_post(
2764 $url,
2765 array(
2766 'method' => 'POST',
2767 'headers' => array(
2768 'Content-Type' => 'application/json',
2769 'x-wiserrw-api-key' => $api_key,
2770 ),
2771 'body' => wp_json_encode( $body ),
2772 'timeout' => 30,
2773 )
2774 );
2775
2776 if ( is_wp_error( $response ) ) {
2777 return;
2778 }
2779
2780 $json = wp_remote_retrieve_body( $response );
2781 $data = json_decode( $json, true );
2782 if ( empty( $data['data'] ) ) {
2783 return;
2784 }
2785
2786 $schema_array = array();
2787 $reviewCount = 0;
2788
2789 foreach ( $data['data'] as $row ) {
2790 $wdtyp = $row['wdtyp'];
2791
2792 if ( $wdtyp == 'main' ) {
2793 $reviewCount = intval( $row['dataCount']['prtng'] );
2794
2795
2796 $schema_array['@context'] = "https://schema.org";
2797 $schema_array['@type'] = "Product";
2798 $schema_array['@id'] = get_the_permalink( $p_id );
2799 $schema_array['url'] = get_the_permalink( $p_id );
2800 $schema_array['name'] = get_the_title( $p_id );
2801 $schema_array['aggregateRating'] = array(
2802 '@type' => 'AggregateRating',
2803 'ratingValue' => $row['dataCount']['avgrtng'],
2804 'reviewCount' => $reviewCount,
2805 'bestRating' => '5',
2806 'worstRating' => '1',
2807 );
2808 }
2809
2810 if ( $wdtyp == 'star_rating' ) {// && !empty( $row['dataRating'][0]['html'] )
2811 update_post_meta( $p_id, 'wiserrw_star_rating_html', $row['dataRating'][0]['html'] );
2812 }
2813
2814 if ( $wdtyp == 'main' && !empty( $row['htmlForSEO'] ) ) {
2815 update_post_meta( $p_id, 'wiserrw_product_html', $row['htmlForSEO']);
2816 }
2817
2818 if ( ! empty( $row['data'] ) && is_array( $row['data'] ) ) {
2819 foreach ( $row['data'] as $review_row ) {
2820 $schema_array['review'][] = array(
2821 "@type" => "Review",
2822 'author' => array(
2823 '@type' => "Person",
2824 'name' => $review_row['un']
2825 ),
2826 'reviewBody' => $review_row['orgnlrtxt'],
2827 'reviewRating' => array(
2828 '@type' => 'Rating',
2829 'ratingValue' => $review_row['rtng'],
2830 'bestRating' => '5',
2831 'worstRating' => '1',
2832 ),
2833 'publisher' => array(
2834 "@type" => "Organization",
2835 "name" => "WiserReview"
2836 )
2837 );
2838 }
2839 }
2840 }
2841
2842 // �
2843 Save or delete only once, after loop
2844 if ( $reviewCount > 0 ) {
2845 update_post_meta( $p_id, 'wiserrw_schema_json', json_encode( $schema_array, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) );
2846 } else {
2847 delete_post_meta( $p_id, 'wiserrw_schema_json' );
2848 // if($HideZeroStar){
2849 // delete_post_meta( $p_id, 'wiserrw_star_rating_html' );
2850 // }
2851 delete_post_meta( $p_id, 'wiserrw_product_html' );
2852 }
2853
2854 wiserrw_send_product_update_on_save_post( $p_id );
2855 }
2856
2857
2858 /* -------------------------------------------------------------------------
2859 * SEO plugin integration — merge aggregateRating into existing Product
2860 * schema instead of emitting a duplicate <script> block.
2861 *
2862 * Strategy:
2863 * Layer 1 — woocommerce_structured_data_product: WooCommerce core's
2864 * Product schema filter. Yoast Free, AIOSEO Pro, SEOPress
2865 * Pro, and the no-SEO-plugin case all funnel through this,
2866 * so a single callback enriches them all.
2867 * Layer 2 — rank_math/snippet/rich_snippet_product_entity: RankMath
2868 * actively removes WC's schema action and emits its own
2869 * Product schema, so we hook RankMath directly.
2870 * Layer 4 — Standalone <script> output is suppressed below in
2871 * wiserrw_product_schema() when an integration target is
2872 * present, so the page never has duplicate Product nodes.
2873 *
2874 * Performance: zero DB queries beyond a single get_post_meta() (already
2875 * cached by WP's object cache). Static per-request cache. No HTTP. No
2876 * regex on output. Hot path adds <0.1ms per product page render.
2877 * ----------------------------------------------------------------------- */
2878
2879 /**
2880 * Detect which supported SEO plugin is active on this site, if any.
2881 * Used by the integration callbacks (info-only) and by the settings UI
2882 * to show merchants which plugin we're auto-merging schema into.
2883 *
2884 * @return string|null Human-readable plugin name, or null if none detected.
2885 */
2886 function wiserrw_detect_active_seo_plugin() {
2887 if ( defined( 'RANK_MATH_VERSION' ) ) {
2888 return 'RankMath';
2889 }
2890 if ( defined( 'WPSEO_VERSION' ) ) {
2891 return 'Yoast SEO';
2892 }
2893 if ( defined( 'AIOSEO_VERSION' ) || defined( 'AIOSEO_FILE' ) ) {
2894 return 'All in One SEO';
2895 }
2896 if ( defined( 'SEOPRESS_VERSION' ) ) {
2897 return 'SEOPress';
2898 }
2899 return null;
2900 }
2901
2902 /**
2903 * Pull the aggregateRating + review data we already generated for a
2904 * product. Backed by the existing 'wiserrw_schema_json' post_meta —
2905 * no new storage, no extra API calls. Statically cached per request.
2906 *
2907 * @param int $product_id WooCommerce product post ID.
2908 * @return array|false { aggregateRating: array, review?: array } or
2909 * false when no rating data exists for this product.
2910 */
2911 function wiserrw_get_rating_for_seo_integration( $product_id ) {
2912 static $cache = array();
2913
2914 $product_id = (int) $product_id;
2915 if ( $product_id <= 0 ) {
2916 return false;
2917 }
2918 if ( array_key_exists( $product_id, $cache ) ) {
2919 return $cache[ $product_id ];
2920 }
2921
2922 $json = get_post_meta( $product_id, 'wiserrw_schema_json', true );
2923 if ( empty( $json ) ) {
2924 return $cache[ $product_id ] = false;
2925 }
2926
2927 $data = json_decode( $json, true );
2928 // Don't inject empty / zero-review aggregateRating — Google flags it
2929 // as invalid structured data.
2930 if ( ! is_array( $data ) || empty( $data['aggregateRating']['reviewCount'] ) ) {
2931 return $cache[ $product_id ] = false;
2932 }
2933
2934 $result = array(
2935 'aggregateRating' => $data['aggregateRating'],
2936 );
2937 if ( ! empty( $data['review'] ) && is_array( $data['review'] ) ) {
2938 $result['review'] = $data['review'];
2939 }
2940
2941 return $cache[ $product_id ] = $result;
2942 }
2943
2944 /**
2945 * Layer 1 — WooCommerce native Product schema (used by WC core, Yoast,
2946 * AIOSEO Pro, SEOPress Pro and any other plugin that builds on top of
2947 * WC's structured data pipeline).
2948 */
2949 add_filter( 'woocommerce_structured_data_product', 'wiserrw_inject_rating_into_wc_schema', 99, 2 );
2950 function wiserrw_inject_rating_into_wc_schema( $markup, $product ) {
2951 if ( ! is_array( $markup ) || ! ( $product instanceof WC_Product ) ) {
2952 return $markup;
2953 }
2954 $rating = wiserrw_get_rating_for_seo_integration( $product->get_id() );
2955 if ( empty( $rating ) ) {
2956 return $markup;
2957 }
2958 $markup['aggregateRating'] = $rating['aggregateRating'];
2959 if ( ! empty( $rating['review'] ) ) {
2960 $markup['review'] = $rating['review'];
2961 }
2962 return $markup;
2963 }
2964
2965 /**
2966 * Layer 2 — RankMath's Product schema. RankMath unhooks WC's structured
2967 * data and renders its own, so the WC filter above never fires for
2968 * stores running RankMath. Verified from RankMath source: the filter
2969 * runs in class-singular.php (do_filter('snippet/rich_snippet_' . $type
2970 * . '_entity', $entity)).
2971 */
2972 add_filter( 'rank_math/snippet/rich_snippet_product_entity', 'wiserrw_inject_rating_into_rankmath', 99, 1 );
2973 function wiserrw_inject_rating_into_rankmath( $entity ) {
2974 if ( ! is_array( $entity ) ) {
2975 return $entity;
2976 }
2977 $product_id = get_the_ID();
2978 $rating = wiserrw_get_rating_for_seo_integration( $product_id );
2979 if ( empty( $rating ) ) {
2980 return $entity;
2981 }
2982 $entity['aggregateRating'] = $rating['aggregateRating'];
2983 if ( ! empty( $rating['review'] ) ) {
2984 $entity['review'] = $rating['review'];
2985 }
2986 return $entity;
2987 }
2988
2989
2990 // Add schema only on WooCommerce single product pages
2991 add_action( 'wp_head', 'wiserrw_product_schema' );
2992 function wiserrw_product_schema() {
2993 if ( ! is_product() ) {
2994 return;
2995 }
2996
2997 // �
2998 Check if schema generation is enabled in settings
2999 $wiserrw_api_settings = get_option( 'wiserrw_api_settings', array() );
3000
3001 // Check if live mode is enabled (required for schema)
3002 $live_mode_enabled = isset( $wiserrw_api_settings['wiserrw_live_mode_checkbox'] );
3003
3004 if ( ! $live_mode_enabled ) {
3005 return; // Plugin not in live mode
3006 }
3007
3008 // Check if schema toggle is enabled
3009 $schema_enabled = isset( $wiserrw_api_settings['wiserrw_schema_enabled'] );
3010
3011 if ( ! $schema_enabled ) {
3012 return; // Schema generation disabled by user
3013 }
3014
3015 // Layer 4 — Suppress our standalone Product schema when WC core or
3016 // RankMath is actually rendering Product schema on this page. Our
3017 // aggregateRating + reviews are already injected into theirs via the
3018 // filter callbacks above, so emitting our own would create duplicate
3019 // Product entities and split rich-result signals across schemas.
3020 //
3021 // We don't just check class_exists — performance plugins, themes, or
3022 // merchants sometimes remove WC's wp_footer action without removing
3023 // the class. has_action() verifies the schema is genuinely scheduled
3024 // to render so we don't suppress ourselves into a no-schema page.
3025 //
3026 // Merchants can still override via the 'wiserrw_suppress_standalone_schema'
3027 // filter for any other custom setup.
3028 $wc_schema_will_render = (
3029 function_exists( 'WC' )
3030 && isset( WC()->structured_data )
3031 && false !== has_action( 'wp_footer', array( WC()->structured_data, 'output_structured_data' ) )
3032 );
3033 $integration_active = $wc_schema_will_render || defined( 'RANK_MATH_VERSION' );
3034
3035 if ( apply_filters( 'wiserrw_suppress_standalone_schema', $integration_active ) ) {
3036 return;
3037 }
3038
3039 $p_id = get_queried_object_id();
3040 if ( ! $p_id ) {
3041 return;
3042 }
3043
3044 $wiserrw_schema_json = get_post_meta( $p_id, 'wiserrw_schema_json', true );
3045
3046 if ( $wiserrw_schema_json ) {
3047 echo '<script id="WiserReviewSchemaJson" type="application/ld+json">';
3048 echo $wiserrw_schema_json;
3049 echo '</script>';
3050 }
3051 }
3052
3053 function wiserrw_check_rest_api() {
3054 add_filter( 'https_ssl_verify', '__return_false' );
3055 $response = wp_remote_get( home_url( '/wp-json/' ) );
3056 remove_filter( 'https_ssl_verify', '__return_false' );
3057
3058 if ( is_wp_error( $response ) ) {
3059 echo 'WooCommerce REST API is not enabled. Enable it in settings and click Save again here..';
3060 return;
3061 }
3062
3063 $body = wp_remote_retrieve_body( $response );
3064 $data = json_decode( $body, true );
3065
3066 if ( !isset( $data['routes']['/wiserrw/v1/reviews'] ) ) {
3067 echo 'Please click Save again to apply the latest changes.';
3068 }
3069 }
3070
3071 function wiserrw_clear_wp_cache() {
3072 if( isset( $_GET['wiser_clear_cache'] ) && ( current_user_can( 'administrator' ) || current_user_can( 'editor' ) ) ) {
3073
3074 global $wp_fastest_cache;
3075
3076 // WP Rocket
3077 if ( function_exists( 'rocket_clean_domain' ) ) {
3078 rocket_clean_domain();
3079 }
3080
3081 if ( function_exists( 'wp_cache_flush' ) ) {
3082 wp_cache_flush();
3083 }
3084
3085 // WP Super Cache
3086 if ( function_exists( 'wp_cache_clear_cache' ) ) {
3087 wp_cache_clear_cache();
3088 }
3089
3090 // W3 Total Cache
3091 if ( function_exists( 'w3tc_flush_posts' ) ) {
3092 w3tc_flush_posts();
3093 }
3094
3095 // Cache Enabler
3096 if ( has_action( 'ce_clear_cache' ) ) {
3097 do_action( 'ce_clear_cache' );
3098 }
3099
3100 // Breeze
3101 if ( class_exists( 'Breeze_PurgeCache' ) ) {
3102 if ( method_exists( 'Breeze_PurgeCache', 'breeze_cache_flush' ) ) {
3103 Breeze_PurgeCache::breeze_cache_flush();
3104 }
3105 }
3106
3107 if ( class_exists( 'Swift_Performance_Cache' ) ) {
3108 if ( method_exists( 'Swift_Performance_Cache', 'clear_all_cache' ) ) {
3109 Swift_Performance_Cache::clear_all_cache();
3110 }
3111 }
3112
3113 // WP Fastest Cache
3114 if ( method_exists( 'WpFastestCache', 'deleteCache' ) && ! empty( $wp_fastest_cache ) ) {
3115 $wp_fastest_cache->deleteCache();
3116 }
3117
3118 // Autoptimize
3119 if ( class_exists( 'WpeCommon' ) ) {
3120 if ( method_exists( 'WpeCommon', 'purge_memcached' ) ) {
3121 WpeCommon::purge_memcached();
3122 }
3123 if ( method_exists( 'WpeCommon', 'clear_maxcdn_cache' ) ) {
3124 WpeCommon::clear_maxcdn_cache();
3125 }
3126 if ( method_exists( 'WpeCommon', 'purge_varnish_cache' ) ) {
3127 WpeCommon::purge_varnish_cache();
3128 }
3129 }
3130
3131 // SGOptimzer
3132 if ( function_exists('sg_cachepress_purge_cache') ) {
3133 sg_cachepress_purge_cache();
3134 }
3135 }
3136 }
3137 add_action( 'admin_init', 'wiserrw_clear_wp_cache' );
3138
3139 function wiserrw_send_product_update_on_save_post( $post_ID ) {
3140
3141 // WooCommerce must be available
3142 if ( ! class_exists( 'WooCommerce' ) ) return;
3143
3144 $product = wc_get_product( $post_ID );
3145 if ( ! $product ) return;
3146
3147 $wsid = get_option('wiserrw_wsid',true);
3148 $wiserrw_api_settings = get_option( 'wiserrw_api_settings', array() );
3149 $api_key = $wiserrw_api_settings['wiserrw_api_key'] ?? '';
3150 $data = array(
3151 'wsid' => $wsid,
3152 'pid' => $post_ID
3153 );
3154 // Make the request
3155 $response = wp_remote_post(
3156 WISERRW_RS_HOST . '/api/wp/updSchema',
3157 array(
3158 'method' => 'POST',
3159 'headers' => array(
3160 'Content-Type' => 'application/json',
3161 'x-wiserrw-api-key' => $api_key,
3162 ),
3163 'timeout' => 45,
3164 'redirection' => 5,
3165 'httpversion' => '1.0',
3166 'blocking' => true,
3167 'body' => wp_json_encode( $data ),
3168 'ver' => WISERRW_PLUGIN_VERSION
3169 )
3170 );
3171
3172 update_option( 'wiserrw__update_hook',wp_remote_retrieve_body($response) );
3173
3174 }
3175
3176 function wiserrw_show_product_rating_on_collection_woo_events() {
3177 $wiserrw_api_settings = (array) get_option( 'wiserrw_api_settings', [] );
3178
3179 if ( ! empty( $wiserrw_api_settings['wiserrw_product_card_enabled'] ) && $wiserrw_api_settings['wiserrw_product_card_enabled'] === '1' ) {
3180 $pid = wiserrw_get_current_product_id();
3181 echo wiserrw_rating_count( array( 'id' => $pid ) );
3182 wiserrw_restore_product_globals();
3183 }
3184 }
3185
3186 add_action( 'we_after_grid_content_html', 'wiserrw_show_product_rating_on_collection_woo_events', 5 );
3187
3188 // WiserReview Shortcode: [wiserreview_widget id="WidgetID value" type="carousel"]
3189 function wiserreview_widget_shortcode( $atts ) {
3190 $wsid = get_option('wiserrw_wsid',true);
3191 $atts = shortcode_atts( array(
3192 'id' => '',
3193 'type' => '',
3194 ), $atts, 'carousel_widget' );
3195
3196 if ( empty( $atts['id'] ) ) {
3197 return 'No widget ID provided';
3198 }
3199
3200 if ( empty( $atts['type'] ) ) {
3201 return 'No widget Type provided';
3202 }
3203
3204 $widget_id = sanitize_text_field( $atts['id'] );
3205 $type = sanitize_text_field( $atts['type'] );
3206 $option_key = 'wiserreview_widget_' . $widget_id;
3207
3208 // 1. If cache exists → serve instantly
3209 $cached_html = get_option( $option_key );
3210 if ( ! empty( $cached_html ) ) {
3211 return $cached_html;
3212 }
3213
3214 // 2. Render fallback immediately
3215 $fallback = wiserreview_shortcode_fallback( $widget_id, $type );
3216
3217 // 3. Try API fetch in same request (non-blocking for rendering)
3218 // The user sees fallback, but next time cache will be ready
3219 // $api_url = add_query_arg(
3220 // array(
3221 // 'widgetId' => $widget_id,
3222 // 'type' => $type,
3223 // 'ver' => WISERRW_PLUGIN_VERSION
3224 // ),
3225 // WISERRW_RS_HOST . '/api/getWdgtHTML'
3226
3227 // );
3228
3229 // $wiserrw_api_settings = get_option( 'wiserrw_api_settings', array() );
3230 // $api_key = $wiserrw_api_settings['wiserrw_api_key'] ?? '';
3231 // $response = wp_remote_get( $api_url, array(
3232 // 'timeout' => 20,
3233 // 'headers' => array(
3234 // 'x-wiserrw-api-key' => $api_key,
3235 // ),
3236 // ) );
3237
3238 // if ( ! is_wp_error( $response ) ) {
3239 // $json = json_decode( wp_remote_retrieve_body( $response ), true );
3240 // if ( isset( $json['html'] ) && ! empty( $json['html'] ) ) {
3241 // update_option( $option_key, $json['html'] );
3242 // wiserrw_update_html( $wsid, $widget_id );
3243 // }
3244 // }
3245
3246 // 2. Cache miss → schedule async fetch (one job per widget) and return fallback now.
3247 // The remote API call + wiserrw_update_html() run in the background via
3248 // Action Scheduler (bundled with WooCommerce) or wp-cron fallback so the
3249 // page render is not held up. See wiserrw_async_widget_fetch_handler() below.
3250
3251 $lock_key = 'wiserrw_widget_lock_' . $widget_id;
3252 if ( false === get_transient( $lock_key ) ) {
3253 // 60s lock prevents a thundering herd: only the first cache-miss
3254 // visitor schedules the fetch; concurrent visitors back off and
3255 // simply render the fallback while the job is in flight.
3256 set_transient( $lock_key, time(), 60 );
3257 $args = array( $widget_id, $type );
3258
3259 // Prefer Action Scheduler (bundled with WooCommerce 4.0+). It runs
3260 // jobs in a dedicated background queue, so visitor PHP workers
3261 // never execute the upstream fetch. WP-Cron fallback is only used
3262 // when AS is unavailable (rare — non-WC sites or stripped builds).
3263 if ( function_exists( 'as_enqueue_async_action' ) ) {
3264 as_enqueue_async_action( 'wiserrw_async_widget_fetch', $args, 'wiserreview' );
3265 } else {
3266 wp_schedule_single_event( time() + 1, 'wiserrw_async_widget_fetch', $args );
3267 }
3268 }
3269
3270 return $fallback;
3271 }
3272 add_action( 'wiserrw_async_widget_fetch', 'wiserrw_async_widget_fetch_handler', 10, 2 );
3273
3274 function wiserrw_async_widget_fetch_handler( $widget_id, $type ) {
3275 $option_key = 'wiserreview_widget_' . $widget_id;
3276 $lock_key = 'wiserrw_widget_lock_' . $widget_id;
3277 $wsid = get_option( 'wiserrw_wsid', true );
3278
3279 $api_url = add_query_arg(
3280 array(
3281 'widgetId' => $widget_id,
3282 'type' => $type,
3283 'ver' => WISERRW_PLUGIN_VERSION
3284 ),
3285 WISERRW_RS_HOST . '/api/getWdgtHTML'
3286
3287 );
3288
3289 $wiserrw_api_settings = get_option( 'wiserrw_api_settings', array() );
3290 $api_key = $wiserrw_api_settings['wiserrw_api_key'] ?? '';
3291 $response = wp_remote_get( $api_url, array(
3292 'timeout' => 20,
3293 'headers' => array(
3294 'x-wiserrw-api-key' => $api_key,
3295 ),
3296 ) );
3297
3298 if ( ! is_wp_error( $response ) ) {
3299 $json = json_decode( wp_remote_retrieve_body( $response ), true );
3300 if ( isset( $json['html'] ) && ! empty( $json['html'] ) ) {
3301 update_option( $option_key, $json['html'] );
3302 wiserrw_update_html( $wsid, $widget_id );
3303 }
3304 }
3305
3306 delete_transient( $lock_key );
3307 }
3308
3309 add_shortcode( 'wiserreview_widget', 'wiserreview_widget_shortcode' );
3310
3311 function wiserreview_shortcode_fallback( $widget_id, $type ) {
3312 $widget_id = esc_attr( $widget_id );
3313 $type = esc_attr( $type );
3314
3315 return '<script src="https://embed.wiserreview.com/embed/' . $widget_id . '/widget.js" defer></script>'
3316 . '<div data-type="' . $type . '" class="wiser_review_' . $type . '" data-id="' . $widget_id . '"></div>';
3317 }
3318