PluginProbe
Live Chat & AI Chatbot – onWebChat / 3.5.5
Live Chat & AI Chatbot – onWebChat v3.5.5
3.9.2 3.9.3 3.9.1 3.9.0 3.8.4 3.8.2 3.8.1 3.8.0 3.7.2 3.7.1 3.7.0 3.6.0 3.5.5 trunk 1.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 All 48 releases
onwebchat / includes / woocommerce-sync.php

woocommerce-sync.php in Live Chat & AI Chatbot – onWebChat 3.5.5, at includes/woocommerce-sync.php

1,103 lines 42.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WooCommerce Product Sync Module
4 * Syncs WooCommerce products to onWebChat for AI bot training
5 */
6
7 if (!defined('ABSPATH')) {
8 exit; // Exit if accessed directly
9 }
10
11 class OnWebChat_WooCommerce_Sync {
12
13 private $api_endpoint_prod = 'https://www.onwebchat.com/api/integrations/woocommerce';
14 private $api_endpoint_dev = 'http://127.0.0.1:81/api/integrations/woocommerce';
15 private $max_description_length = 1000;
16 private $batch_size = 50;
17 private $use_testing_mode;
18
19 /**
20 * Get the API endpoint based on testing mode
21 * @return string
22 */
23 private function get_api_endpoint() {
24 return $this->use_testing_mode ? $this->api_endpoint_dev : $this->api_endpoint_prod;
25 }
26
27 public function __construct() {
28 // Read testing mode from global constant (defined in onwebchat.php)
29 $this->use_testing_mode = defined('ONWEBCHAT_WC_TESTING_MODE') ? ONWEBCHAT_WC_TESTING_MODE : false;
30 // Initialize settings
31 add_action('admin_init', array($this, 'register_settings'));
32
33 // Show authentication error notice globally (not just on WooCommerce tab)
34 add_action('admin_notices', array($this, 'show_auth_error_notice'));
35
36 // Product hooks - use WooCommerce hooks that fire AFTER meta data is saved
37 add_action('woocommerce_update_product', array($this, 'on_product_update'), 10, 1);
38 add_action('woocommerce_new_product', array($this, 'on_product_update'), 10, 1);
39
40 // Handle product deletion (both trash and permanent delete)
41 add_action('wp_trash_post', array($this, 'on_product_trash'), 10, 1);
42 add_action('before_delete_post', array($this, 'on_product_delete'), 10, 2);
43
44 // Bulk sync via WP Cron
45 add_action('onwebchat_wc_bulk_sync_batch', array($this, 'process_bulk_sync_batch'));
46
47 // Admin AJAX handlers
48 add_action('wp_ajax_onwebchat_wc_sync_now', array($this, 'ajax_sync_existing_products'));
49 add_action('wp_ajax_onwebchat_wc_regenerate_secret', array($this, 'ajax_regenerate_secret'));
50 add_action('wp_ajax_onwebchat_wc_reset_sync_status', array($this, 'ajax_reset_sync_status'));
51 add_action('wp_ajax_onwebchat_wc_connect', array($this, 'ajax_connect_woocommerce'));
52 add_action('wp_ajax_onwebchat_wc_manual_process_batch', array($this, 'ajax_manual_process_batch'));
53 add_action('wp_ajax_onwebchat_wc_get_sync_status', array($this, 'ajax_get_sync_status'));
54 add_action('wp_ajax_onwebchat_wc_save_sync_enabled', array($this, 'ajax_save_sync_enabled'));
55 }
56
57 /**
58 * AJAX: Connect WooCommerce with authentication
59 */
60 public function ajax_connect_woocommerce() {
61 check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
62
63 if (!current_user_can('manage_options')) {
64 wp_send_json_error('Insufficient permissions');
65 }
66
67 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
68 $password = isset($_POST['password']) ? sanitize_text_field($_POST['password']) : '';
69
70 if (empty($email) || empty($password)) {
71 wp_send_json_error('Email and password are required');
72 }
73
74 $result = $this->request_secret_with_auth($email, $password);
75
76 if ($result['success']) {
77 // Clear any previous authentication errors
78 delete_transient('onwebchat_wc_auth_error');
79
80 wp_send_json_success(array(
81 'message' => 'WooCommerce sync connected successfully!'
82 ));
83 } else {
84 wp_send_json_error($result['error']);
85 }
86 }
87
88 /**
89 * Show authentication error notice globally across all admin pages
90 * (Hidden when already on WooCommerce tab since it has its own error message)
91 */
92 public function show_auth_error_notice() {
93 $auth_error = get_transient('onwebchat_wc_auth_error');
94
95 // Don't show if we're already on the WooCommerce tab (it has its own error message)
96 $is_woocommerce_tab = isset($_GET['page']) && $_GET['page'] === 'onwebchat_settings'
97 && isset($_GET['tab']) && $_GET['tab'] === 'woocommerce';
98
99 if ($auth_error && class_exists('WooCommerce') && !$is_woocommerce_tab) {
100 ?>
101 <div class="notice notice-error">
102 <p>
103 <strong>⚠️ onWebChat WooCommerce Sync Error:</strong>
104 <?php echo esc_html($auth_error); ?>
105 <a href="<?php echo esc_url(admin_url('admin.php?page=onwebchat_settings&tab=woocommerce')); ?>" class="button button-small" style="margin-left: 10px;">
106 Fix Authentication
107 </a>
108 </p>
109 </div>
110 <?php
111 }
112 }
113
114 /**
115 * Register WooCommerce sync settings
116 */
117 public function register_settings() {
118 register_setting('onwebchat_wc_sync', 'onwebchat_wc_sync_enabled');
119 register_setting('onwebchat_wc_sync', 'onwebchat_wc_sync_mode');
120 register_setting('onwebchat_wc_sync', 'onwebchat_wc_sync_include_price');
121 register_setting('onwebchat_wc_sync', 'onwebchat_wc_sync_secret');
122 register_setting('onwebchat_wc_sync', 'onwebchat_wc_last_bulk_sync');
123 register_setting('onwebchat_wc_sync', 'onwebchat_wc_excluded_categories');
124 }
125
126 /**
127 * Hook: Product update (WooCommerce specific hook - fires AFTER all meta is saved)
128 */
129 public function on_product_update($product_id) {
130 // Check if sync is enabled
131 if (!get_option('onwebchat_wc_sync_enabled', false)) {
132 return;
133 }
134
135 // Get product object (at this point all meta data including SKU is already saved)
136 $product = wc_get_product($product_id);
137 if (!$product) {
138 return;
139 }
140
141 // Only sync published products
142 if ($product->get_status() !== 'publish') {
143 return;
144 }
145
146 // Check if product category is excluded
147 if ($this->is_product_excluded($product)) {
148 return;
149 }
150
151 // Prepare and send product data
152 $product_data = $this->prepare_product_data($product);
153 $this->send_product_upsert($product_data, $product_id);
154 }
155
156 /**
157 * Hook: Product trash (when moved to trash)
158 */
159 public function on_product_trash($post_id) {
160 // Check if it's a product
161 if (get_post_type($post_id) !== 'product') {
162 return;
163 }
164
165 if (!get_option('onwebchat_wc_sync_enabled', false)) {
166 return;
167 }
168
169 // Send delete request when product is trashed
170 $this->send_product_delete($post_id);
171 }
172
173 /**
174 * Hook: Product permanent delete
175 */
176 public function on_product_delete($post_id, $post) {
177 if ($post->post_type !== 'product') {
178 return;
179 }
180
181 if (!get_option('onwebchat_wc_sync_enabled', false)) {
182 return;
183 }
184
185 // Send delete request when product is permanently deleted
186 $this->send_product_delete($post_id);
187 }
188
189 /**
190 * Check if product is in excluded categories
191 */
192 private function is_product_excluded($product) {
193 $excluded_categories = get_option('onwebchat_wc_excluded_categories', array());
194 if (empty($excluded_categories)) {
195 return false;
196 }
197
198 $product_categories = $product->get_category_ids();
199 foreach ($product_categories as $cat_id) {
200 if (in_array($cat_id, $excluded_categories)) {
201 return true;
202 }
203 }
204
205 return false;
206 }
207
208 /**
209 * Prepare product data for sync
210 */
211 private function prepare_product_data($product) {
212 $sync_mode = get_option('onwebchat_wc_sync_mode', 'short_fallback_full');
213 $include_price = get_option('onwebchat_wc_sync_include_price', false);
214
215 // Get description based on sync mode
216 $description = '';
217 $short_description = strip_tags($product->get_short_description());
218
219 if ($sync_mode === 'short_only') {
220 $description = $short_description;
221 } else if ($sync_mode === 'short_fallback_full') {
222 if (!empty($short_description)) {
223 $description = $short_description;
224 } else {
225 // Fallback to first 60-80 words of full description
226 $full_description = strip_tags($product->get_description());
227 $words = str_word_count($full_description, 2);
228 $word_array = array_keys($words);
229
230 if (count($word_array) > 80) {
231 $end_pos = $word_array[79];
232 $description = substr($full_description, 0, $end_pos) . '...';
233 } else {
234 $description = $full_description;
235 }
236 }
237 }
238
239 // Enforce max length
240 if (strlen($description) > $this->max_description_length) {
241 $description = substr($description, 0, $this->max_description_length) . '...';
242 }
243
244 // Build text field for embeddings
245 $sku = $product->get_sku();
246 $categories = $this->get_product_category_names($product);
247 $url = get_permalink($product->get_id());
248
249 // Format: "Product: name\nSKU: sku (if available)\nDescription: description\nCategories: cat1, cat2\nURL: url"
250 $text_parts = array();
251 $text_parts[] = 'Product: ' . $product->get_name();
252
253 if (!empty($sku)) {
254 $text_parts[] = 'SKU: ' . $sku;
255 }
256
257 if (!empty($description)) {
258 $text_parts[] = 'Description: ' . $description;
259 }
260
261 if (!empty($categories)) {
262 $text_parts[] = 'Categories: ' . implode(', ', $categories);
263 }
264
265 $text_parts[] = 'URL: ' . $url;
266
267 $text = implode("\n", $text_parts);
268
269 // Prepare data array
270 $data = array(
271 'product_id' => $product->get_id(),
272 'name' => $product->get_name(),
273 'short_description' => trim($description),
274 'url' => $url,
275 'sku' => $sku,
276 'categories' => $categories,
277 'text' => $text, // New field with formatted text for embeddings
278 );
279
280 // Optionally include price
281 if ($include_price) {
282 $data['price'] = $product->get_price();
283 $data['regular_price'] = $product->get_regular_price();
284 $data['sale_price'] = $product->get_sale_price();
285 }
286
287 return $data;
288 }
289
290 /**
291 * Get product category names
292 */
293 private function get_product_category_names($product) {
294 $categories = array();
295 $category_ids = $product->get_category_ids();
296
297 foreach ($category_ids as $cat_id) {
298 $term = get_term($cat_id, 'product_cat');
299 if ($term && !is_wp_error($term)) {
300 $categories[] = $term->name;
301 }
302 }
303
304 return $categories;
305 }
306
307 /**
308 * Send batch of products to API (optimized)
309 * @param array $products - Array of product data
310 */
311 private function send_product_batch($products) {
312 $chatId = get_option('onwebchat_plugin_option');
313 $chatId = (is_array($chatId) && isset($chatId['text_string'])) ? $chatId['text_string'] : '';
314
315 if (empty($chatId)) {
316 error_log('onWebChat WooCommerce Sync - Chat ID not configured');
317 return false;
318 }
319
320 // Ensure we have a secret (must be obtained via authenticated connection in WooCommerce settings)
321 $secret = $this->get_secret(false);
322 if (empty($secret)) {
323 error_log('onWebChat WooCommerce Sync - No secret configured. Please connect WooCommerce in the plugin settings.');
324 return array(
325 'success' => false,
326 'error' => 'No secret configured. Please connect WooCommerce integration.',
327 'needs_reconnect' => true
328 );
329 }
330
331 // Extract key part (before first slash if present)
332 $chatIdKey = explode('/', $chatId)[0];
333
334 $endpoint = $this->get_api_endpoint() . '/product/batch';
335 $payload = array(
336 'site_id' => $chatIdKey,
337 'site_url' => get_site_url(),
338 'products' => $products
339 );
340
341 // Generate authentication headers (same as send_authenticated_request)
342 $timestamp = time();
343 $nonce = base64_encode(random_bytes(16));
344 $body_json = wp_json_encode($payload);
345
346 // Create signature: HMAC_SHA256(secret, site_id.timestamp.nonce.body)
347 $message = $chatIdKey . '.' . $timestamp . '.' . $nonce . '.' . $body_json;
348 $signature = hash_hmac('sha256', $message, $secret);
349
350 // Send request
351 $request_args = array(
352 'method' => 'POST',
353 'timeout' => 30, // Longer timeout for batch operations
354 'headers' => array(
355 'Content-Type' => 'application/json',
356 'X-OWC-SiteId' => $chatIdKey,
357 'X-OWC-Timestamp' => $timestamp,
358 'X-OWC-Nonce' => $nonce,
359 'X-OWC-Signature' => $signature,
360 ),
361 'body' => $body_json,
362 );
363
364 // Disable SSL verification for local dev server
365 if ($this->use_testing_mode) {
366 $request_args['sslverify'] = false;
367 }
368
369 $response = wp_remote_post($endpoint, $request_args);
370
371 if (is_wp_error($response)) {
372 error_log('onWebChat WooCommerce Sync - Batch sync error: ' . $response->get_error_message());
373 return array(
374 'success' => false,
375 'error' => 'Network error: ' . $response->get_error_message()
376 );
377 }
378
379 $response_code = wp_remote_retrieve_response_code($response);
380 $body = json_decode(wp_remote_retrieve_body($response), true);
381
382 // If authentication failed (401), the secret is invalid or out of sync
383 if ($response_code === 401) {
384 // Clear the invalid secret
385 delete_option('onwebchat_wc_sync_secret');
386
387 error_log('onWebChat WooCommerce Sync - Authentication failed (401): Secret is invalid or out of sync. Please reconnect WooCommerce in the plugin settings.');
388
389 // Store admin notice about authentication failure
390 set_transient('onwebchat_wc_auth_error', 'Authentication failed. Your WooCommerce secret is invalid or out of sync. Please reconnect WooCommerce integration in the plugin settings.', 3600);
391
392 return array(
393 'success' => false,
394 'error' => 'Authentication failed. Secret is invalid. Please reconnect WooCommerce integration.',
395 'needs_reconnect' => true
396 );
397 }
398
399 if ($response_code === 200 && isset($body['success']) && $body['success']) {
400 // Clear any previous auth errors on success
401 delete_transient('onwebchat_wc_auth_error');
402 return $body; // Return full response with stats
403 }
404
405 $error_msg = 'Batch sync failed';
406 if (isset($body['error'])) {
407 $error_msg .= ': ' . $body['error'];
408 }
409 error_log('onWebChat WooCommerce Sync - ' . $error_msg . ' - Response: ' . print_r($body, true));
410
411 return array(
412 'success' => false,
413 'error' => $error_msg
414 );
415 }
416
417 /**
418 * Send sync completion notification to server (triggers Angular modal)
419 */
420 private function send_sync_completion_notification($total_stats) {
421 $chatId = get_option('onwebchat_plugin_option');
422 $chatId = (is_array($chatId) && isset($chatId['text_string'])) ? $chatId['text_string'] : '';
423
424 if (empty($chatId)) {
425 error_log('onWebChat WooCommerce Sync - Chat ID not configured');
426 return false;
427 }
428
429 $secret = $this->get_secret(false);
430 if (empty($secret)) {
431 error_log('onWebChat WooCommerce Sync - No secret configured');
432 return false;
433 }
434
435 $chatIdKey = explode('/', $chatId)[0];
436
437 $endpoint = $this->get_api_endpoint() . '/product/sync-complete';
438 $payload = array(
439 'site_id' => $chatIdKey,
440 'stats' => $total_stats
441 );
442
443 // Generate authentication headers
444 $timestamp = time();
445 $nonce = base64_encode(random_bytes(16));
446 $body_json = wp_json_encode($payload);
447 $message = $chatIdKey . '.' . $timestamp . '.' . $nonce . '.' . $body_json;
448 $signature = hash_hmac('sha256', $message, $secret);
449
450 $request_args = array(
451 'method' => 'POST',
452 'timeout' => 10,
453 'headers' => array(
454 'Content-Type' => 'application/json',
455 'X-OWC-SiteId' => $chatIdKey,
456 'X-OWC-Timestamp' => $timestamp,
457 'X-OWC-Nonce' => $nonce,
458 'X-OWC-Signature' => $signature,
459 ),
460 'body' => $body_json,
461 );
462
463 if ($this->use_testing_mode) {
464 $request_args['sslverify'] = false;
465 }
466
467 $response = wp_remote_post($endpoint, $request_args);
468
469 if (is_wp_error($response)) {
470 error_log('onWebChat WooCommerce Sync - Completion notification failed: ' . $response->get_error_message());
471 return false;
472 }
473
474 $response_code = wp_remote_retrieve_response_code($response);
475
476 // If authentication failed (401), the secret is invalid or out of sync
477 if ($response_code === 401) {
478 delete_option('onwebchat_wc_sync_secret');
479 error_log('onWebChat WooCommerce Sync - Completion notification authentication failed (401): Secret is invalid. Please reconnect WooCommerce.');
480 set_transient('onwebchat_wc_auth_error', 'Authentication failed. Your WooCommerce secret is invalid or out of sync. Please reconnect WooCommerce integration in the plugin settings.', 3600);
481 return false;
482 }
483
484 if ($response_code >= 200 && $response_code < 300) {
485 error_log('onWebChat WooCommerce Sync - Completion notification sent successfully');
486 return true;
487 }
488
489 error_log('onWebChat WooCommerce Sync - Completion notification failed with code: ' . $response_code);
490 return false;
491 }
492
493 /**
494 * Send product upsert to server (uses batch endpoint with single product)
495 */
496 private function send_product_upsert($product_data, $product_id) {
497 // Use batch endpoint with single product
498 $result = $this->send_product_batch(array($product_data));
499
500 if ($result && isset($result['success']) && $result['success']) {
501 // Clear any previous errors
502 delete_post_meta($product_id, '_onwebchat_sync_error');
503 update_post_meta($product_id, '_onwebchat_last_sync', current_time('timestamp'));
504 return true;
505 } else {
506 // Log error if batch failed
507 $error_message = 'Failed to sync product';
508 if ($result && isset($result['error'])) {
509 $error_message = $result['error'];
510 } elseif (!$result) {
511 $error_message = 'Batch sync request failed';
512 }
513 $this->log_error($product_id, $error_message);
514 return false;
515 }
516 }
517
518 /**
519 * Send product delete to server
520 */
521 private function send_product_delete($product_id) {
522 $chatId = get_option('onwebchat_plugin_option');
523 $chatId = (is_array($chatId) && isset($chatId['text_string'])) ? $chatId['text_string'] : '';
524
525 if (empty($chatId)) {
526 return false;
527 }
528
529 // Extract key part (before first slash if present)
530 $chatIdKey = explode('/', $chatId)[0];
531
532 $endpoint = $this->get_api_endpoint() . '/product/delete';
533 $payload = array(
534 'site_id' => $chatIdKey, // Use key part only
535 'site_url' => get_site_url(),
536 'product_id' => $product_id
537 );
538
539 $this->send_authenticated_request($endpoint, $payload, $product_id);
540 }
541
542 /**
543 * Get cached secret from local options
544 * @param {bool} force_refresh - Not used (kept for compatibility), secret must be obtained via authenticated request
545 */
546 private function get_secret($force_refresh = false) {
547 // Always return cached secret - never fetch automatically
548 // Secret must be obtained via authenticated request in WooCommerce settings
549 $secret = get_option('onwebchat_wc_sync_secret');
550
551 if (!empty($secret)) {
552 return $secret;
553 }
554
555 // No secret available - user must authenticate in WooCommerce settings
556 return null;
557 }
558
559 /**
560 * Request secret from server with authentication
561 * This is called when user clicks "Connect WooCommerce" with their password
562 *
563 * @param {string} email - User's onWebChat email
564 * @param {string} password - User's onWebChat password
565 * @return {array} - ['success' => bool, 'secret' => string, 'error' => string]
566 */
567 public function request_secret_with_auth($email, $password) {
568 $chatId = get_option('onwebchat_plugin_option');
569 $chatId = (is_array($chatId) && isset($chatId['text_string'])) ? $chatId['text_string'] : '';
570
571 if (empty($chatId)) {
572 return array('success' => false, 'error' => 'No Chat ID configured');
573 }
574
575 // Extract key part (before first slash if present)
576 $key = explode('/', $chatId)[0];
577
578 // Request secret from server with authentication
579 $secret_endpoint = $this->get_api_endpoint() . '/secret';
580
581 $response = wp_remote_post($secret_endpoint, array(
582 'timeout' => 15,
583 'sslverify' => !$this->use_testing_mode,
584 'headers' => array(
585 'Content-Type' => 'application/json',
586 ),
587 'body' => wp_json_encode(array(
588 'email' => $email,
589 'password' => $password,
590 'site_key' => $key,
591 'version' => defined('ONWEBCHAT_PLUGIN_VERSION') ? ONWEBCHAT_PLUGIN_VERSION : '',
592 )),
593 ));
594
595 if (is_wp_error($response)) {
596 $error_message = $response->get_error_message();
597 error_log('onWebChat WooCommerce Sync - Connection error: ' . $error_message);
598 return array('success' => false, 'error' => 'Connection failed: ' . $error_message);
599 }
600
601 $status_code = wp_remote_retrieve_response_code($response);
602 $response_body_raw = wp_remote_retrieve_body($response);
603 $body = json_decode($response_body_raw, true);
604
605 // Log response for debugging
606 error_log('onWebChat WooCommerce Sync - API response: Status=' . $status_code . ', Body=' . substr($response_body_raw, 0, 500));
607
608 // Handle specific HTTP status codes
609 if ($status_code === 401) {
610 return array('success' => false, 'error' => 'Invalid email or password');
611 }
612
613 if ($status_code === 403) {
614 return array('success' => false, 'error' => 'You do not have access to this site');
615 }
616
617 // Success case
618 if ($status_code >= 200 && $status_code < 300 && isset($body['success']) && $body['success']) {
619 $secret = isset($body['secret']) ? $body['secret'] : null;
620 if (empty($secret)) {
621 error_log('onWebChat WooCommerce Sync - Success response but no secret provided');
622 return array('success' => false, 'error' => 'Server response missing secret');
623 }
624 update_option('onwebchat_wc_sync_secret', $secret);
625 return array('success' => true, 'secret' => $secret);
626 }
627
628 // Extract error message from various possible response formats
629 $error_message = 'Unknown error';
630
631 if (is_array($body)) {
632 // Try different possible error fields
633 if (isset($body['error'])) {
634 $error_message = is_string($body['error']) ? $body['error'] : json_encode($body['error']);
635 } elseif (isset($body['message'])) {
636 $error_message = is_string($body['message']) ? $body['message'] : json_encode($body['message']);
637 } elseif (isset($body['errors']) && is_array($body['errors'])) {
638 $error_message = implode(', ', $body['errors']);
639 }
640 } elseif (!empty($response_body_raw)) {
641 // If body is not JSON or empty, use raw response (truncated)
642 $error_message = 'Server returned: ' . substr(strip_tags($response_body_raw), 0, 200);
643 }
644
645 // Include status code in error message if not already included
646 if ($status_code && strpos($error_message, 'HTTP') === false) {
647 $error_message = 'HTTP ' . $status_code . ': ' . $error_message;
648 }
649
650 error_log('onWebChat WooCommerce Sync - Connection failed: ' . $error_message);
651 return array('success' => false, 'error' => $error_message);
652 }
653
654 /**
655 * Send authenticated request with HMAC signature
656 */
657 private function send_authenticated_request($endpoint, $payload, $product_id = null) {
658 // Get cached secret (must be obtained via authenticated connection in WooCommerce settings)
659 $secret = $this->get_secret(false);
660
661 if (empty($secret)) {
662 if ($product_id) {
663 $this->log_error($product_id, 'No secret configured. Please connect WooCommerce in the plugin settings.');
664 }
665 return array('success' => false, 'error' => 'Secret not available. Please connect WooCommerce in plugin settings.');
666 }
667
668 $chatId = get_option('onwebchat_plugin_option');
669 $chatId = (is_array($chatId) && isset($chatId['text_string'])) ? $chatId['text_string'] : '';
670
671 // Extract key part (before first slash if present) for consistency with server
672 // e.g., "5f02c87b60726a4663b25463a424a034/1/1" -> "5f02c87b60726a4663b25463a424a034"
673 $chatIdKey = explode('/', $chatId)[0];
674
675 // Generate authentication headers
676 $timestamp = time();
677 $nonce = base64_encode(random_bytes(16));
678 $body_json = wp_json_encode($payload);
679
680 // Create signature: HMAC_SHA256(secret, site_id.timestamp.nonce.body)
681 // IMPORTANT: Use the key part (not full chat_id) to match server-side verification
682 $message = $chatIdKey . '.' . $timestamp . '.' . $nonce . '.' . $body_json;
683 $signature = hash_hmac('sha256', $message, $secret);
684
685 // Send request
686 $request_args = array(
687 'method' => 'POST',
688 'timeout' => 10,
689 'headers' => array(
690 'Content-Type' => 'application/json',
691 'X-OWC-SiteId' => $chatIdKey, // Use key part only
692 'X-OWC-Timestamp' => $timestamp,
693 'X-OWC-Nonce' => $nonce,
694 'X-OWC-Signature' => $signature,
695 ),
696 'body' => $body_json,
697 );
698
699 // Disable SSL verification for local dev server
700 if ($this->use_testing_mode) {
701 $request_args['sslverify'] = false;
702 }
703
704 $response = wp_remote_post($endpoint, $request_args);
705
706 // Handle response
707 if (is_wp_error($response)) {
708 $error_message = $response->get_error_message();
709 if ($product_id) {
710 $this->log_error($product_id, $error_message);
711 }
712 return array('success' => false, 'error' => $error_message);
713 }
714
715 $status_code = wp_remote_retrieve_response_code($response);
716
717 // Success
718 if ($status_code >= 200 && $status_code < 300) {
719 return array('success' => true);
720 }
721
722 // If authentication failed (401), the secret may be invalid
723 if ($status_code === 401) {
724 // Clear the invalid secret
725 delete_option('onwebchat_wc_sync_secret');
726
727 if ($product_id) {
728 $this->log_error($product_id, 'Authentication failed. Please reconnect WooCommerce in the plugin settings.');
729 }
730 return array('success' => false, 'error' => 'Authentication failed. Please reconnect WooCommerce in plugin settings.');
731 }
732
733 // Error
734 $error_body = wp_remote_retrieve_body($response);
735 if ($product_id) {
736 $this->log_error($product_id, "HTTP $status_code: $error_body");
737 }
738
739 return array('success' => false, 'error' => "HTTP $status_code", 'status_code' => $status_code);
740 }
741
742 /**
743 * Log sync error to product meta
744 */
745 private function log_error($product_id, $error_message) {
746 update_post_meta($product_id, '_onwebchat_sync_error', array(
747 'message' => $error_message,
748 'timestamp' => current_time('timestamp')
749 ));
750 }
751
752 /**
753 * AJAX: Start bulk sync
754 */
755 public function ajax_sync_existing_products() {
756 check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
757
758 if (!current_user_can('manage_options')) {
759 wp_send_json_error('Insufficient permissions');
760 }
761
762 // Check if sync is already in progress
763 if (get_option('onwebchat_wc_bulk_in_progress', false)) {
764 wp_send_json_error('A sync is already in progress. Please wait for it to complete.');
765 }
766
767 // Rate limiting: prevent syncing more than once every 5 minutes
768 $last_sync_time = get_option('onwebchat_wc_last_sync_start', 0);
769 $cooldown_period = 5 * 60; // 5 minutes in seconds //also in the file woocommerce.php // 5 * 60
770 $time_since_last_sync = time() - $last_sync_time;
771
772 if ($time_since_last_sync < $cooldown_period) {
773 $wait_time = $cooldown_period - $time_since_last_sync;
774 $minutes = ceil($wait_time / 60);
775 wp_send_json_error('Please wait ' . $minutes . ' minute(s) before syncing again.');
776 }
777
778 // Store the current sync start time
779 update_option('onwebchat_wc_last_sync_start', time());
780
781 // Reset bulk sync progress
782 update_option('onwebchat_wc_bulk_page', 0);
783 update_option('onwebchat_wc_bulk_done', 0);
784
785 // Count total products
786 $args = array(
787 'post_type' => 'product',
788 'post_status' => 'publish',
789 'posts_per_page' => -1,
790 'fields' => 'ids',
791 );
792
793 $products = get_posts($args);
794 $total = count($products);
795
796 update_option('onwebchat_wc_bulk_total', $total);
797 update_option('onwebchat_wc_bulk_done', 0); // Initialize progress counter
798 update_option('onwebchat_wc_bulk_in_progress', true);
799
800 // Process sync directly instead of using unreliable WP Cron
801 $sync_result = $this->do_bulk_sync_all();
802
803 wp_send_json_success(array(
804 'message' => 'Bulk sync completed',
805 'total' => $total,
806 'result' => $sync_result
807 ));
808 }
809
810 /**
811 * Process all products in bulk sync directly (not via cron)
812 */
813 private function do_bulk_sync_all() {
814 $total = get_option('onwebchat_wc_bulk_total', 0);
815 $page = 0;
816 $total_done = 0;
817 $all_stats = array('created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => 0);
818
819 // Process all products in batches
820 while (true) {
821 $args = array(
822 'post_type' => 'product',
823 'post_status' => 'publish',
824 'posts_per_page' => $this->batch_size,
825 'paged' => $page + 1,
826 'orderby' => 'ID',
827 'order' => 'ASC',
828 );
829
830 $query = new WP_Query($args);
831
832 if (!$query->have_posts()) {
833 break;
834 }
835
836 // Collect products in this batch
837 $products_batch = array();
838 foreach ($query->posts as $post) {
839 $product = wc_get_product($post->ID);
840 if ($product && !$this->is_product_excluded($product)) {
841 $products_batch[] = $this->prepare_product_data($product);
842 }
843 }
844
845 // Send batch
846 if (!empty($products_batch)) {
847 $result = $this->send_product_batch($products_batch);
848 if ($result && isset($result['stats'])) {
849 $total_done += $result['stats']['created'] + $result['stats']['updated'] + $result['stats']['skipped'];
850 $all_stats['created'] += $result['stats']['created'];
851 $all_stats['updated'] += $result['stats']['updated'];
852 $all_stats['skipped'] += $result['stats']['skipped'];
853 $all_stats['errors'] += $result['stats']['errors'];
854 } else {
855 // Fallback
856 $total_done += count($products_batch);
857 }
858
859 // Update progress after each batch so AJAX polling can see it
860 update_option('onwebchat_wc_bulk_done', $total_done);
861
862 // Wait 4 seconds before next batch
863 sleep(4);
864 }
865
866 wp_reset_postdata();
867 $page++;
868
869 // Safety check - don't loop forever
870 if ($page > 100) {
871 break;
872 }
873 }
874
875 // Send completion notification to Angular dashboard with total stats
876 $this->send_sync_completion_notification($all_stats);
877
878 // Mark sync as complete
879 update_option('onwebchat_wc_bulk_in_progress', false);
880 update_option('onwebchat_wc_bulk_done', $total_done);
881 update_option('onwebchat_wc_last_bulk_sync', current_time('timestamp'));
882
883 return array(
884 'done' => $total_done,
885 'total' => $total,
886 'stats' => $all_stats
887 );
888 }
889
890 /**
891 * Process bulk sync batch (via WP Cron) - Uses batch API endpoint
892 */
893 public function process_bulk_sync_batch() {
894 error_log('onWebChat WooCommerce Sync - process_bulk_sync_batch called');
895
896 if (!get_option('onwebchat_wc_bulk_in_progress', false)) {
897 error_log('onWebChat WooCommerce Sync - Sync not in progress, exiting');
898 return;
899 }
900
901 $page = get_option('onwebchat_wc_bulk_page', 0);
902 $done = get_option('onwebchat_wc_bulk_done', 0);
903 $total = get_option('onwebchat_wc_bulk_total', 0);
904
905 error_log('onWebChat WooCommerce Sync - Starting batch: page=' . $page . ', done=' . $done . ', total=' . $total);
906
907 // Get batch of products
908 $args = array(
909 'post_type' => 'product',
910 'post_status' => 'publish',
911 'posts_per_page' => $this->batch_size,
912 'paged' => $page + 1,
913 'orderby' => 'ID',
914 'order' => 'ASC',
915 );
916
917 $query = new WP_Query($args);
918
919 if ($query->have_posts()) {
920 // Collect all products in this batch
921 $products_batch = array();
922
923 foreach ($query->posts as $post) {
924 $product = wc_get_product($post->ID);
925 if ($product && !$this->is_product_excluded($product)) {
926 $products_batch[] = $this->prepare_product_data($product);
927 }
928 }
929
930 // Send entire batch in one request
931 $batch_done = 0;
932 if (!empty($products_batch)) {
933 $result = $this->send_product_batch($products_batch);
934 error_log('onWebChat WooCommerce Sync - Batch result: ' . print_r($result, true));
935 if ($result && isset($result['stats'])) {
936 // Count created + updated + skipped as "done"
937 $batch_done = $result['stats']['created'] + $result['stats']['updated'] + $result['stats']['skipped'];
938 error_log('onWebChat WooCommerce Sync - Batch done: ' . $batch_done);
939 } else {
940 // Fallback: assume all sent
941 $batch_done = count($products_batch);
942 error_log('onWebChat WooCommerce Sync - No stats in result, using fallback count: ' . $batch_done);
943 }
944 }
945
946 // Update progress
947 $new_done = $done + $batch_done;
948 error_log('onWebChat WooCommerce Sync - Progress update: done=' . $done . ' + batch_done=' . $batch_done . ' = new_done=' . $new_done . ' / total=' . $total);
949 update_option('onwebchat_wc_bulk_page', $page + 1);
950 update_option('onwebchat_wc_bulk_done', $new_done);
951
952 // Check if we've processed all products
953 if ($new_done >= $total || !$query->have_posts()) {
954 // Sync complete
955 error_log('onWebChat WooCommerce Sync - Marking sync as complete');
956 update_option('onwebchat_wc_bulk_in_progress', false);
957 update_option('onwebchat_wc_last_bulk_sync', current_time('timestamp'));
958 update_option('onwebchat_wc_bulk_done', $total); // Ensure it shows 100%
959 } else {
960 // Schedule next batch
961 error_log('onWebChat WooCommerce Sync - Scheduling next batch in 60 seconds');
962 wp_schedule_single_event(time() + 60, 'onwebchat_wc_bulk_sync_batch');
963 }
964 } else {
965 // No more products - sync complete
966 update_option('onwebchat_wc_bulk_in_progress', false);
967 update_option('onwebchat_wc_last_bulk_sync', current_time('timestamp'));
968 update_option('onwebchat_wc_bulk_done', $total); // Ensure it shows 100%
969 }
970
971 wp_reset_postdata();
972 }
973
974 /**
975 * AJAX: Regenerate secret (fetch from server)
976 */
977 public function ajax_regenerate_secret() {
978 check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
979
980 if (!current_user_can('manage_options')) {
981 wp_send_json_error('Insufficient permissions');
982 }
983
984 // Clear cached secret - user will need to re-authenticate
985 delete_option('onwebchat_wc_sync_secret');
986
987 // Clear any authentication error notices
988 delete_transient('onwebchat_wc_auth_error');
989
990 wp_send_json_success(array(
991 'message' => 'Secret cleared. Please reconnect WooCommerce with your credentials.',
992 'needs_reconnect' => true
993 ));
994 }
995
996 /**
997 * AJAX: Reset sync status
998 */
999 public function ajax_reset_sync_status() {
1000 check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
1001
1002 if (!current_user_can('manage_options')) {
1003 wp_send_json_error('Insufficient permissions');
1004 }
1005
1006 $total = get_option('onwebchat_wc_bulk_total', 0);
1007
1008 // Mark sync as complete
1009 update_option('onwebchat_wc_bulk_in_progress', false);
1010 update_option('onwebchat_wc_bulk_done', $total);
1011 update_option('onwebchat_wc_last_bulk_sync', current_time('timestamp'));
1012
1013 wp_send_json_success(array(
1014 'message' => 'Sync status reset successfully'
1015 ));
1016 }
1017
1018 /**
1019 * AJAX handler to get current sync status
1020 */
1021 public function ajax_get_sync_status() {
1022 check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
1023
1024 if (!current_user_can('manage_options')) {
1025 wp_send_json_error('Insufficient permissions');
1026 }
1027
1028 $in_progress = get_option('onwebchat_wc_bulk_in_progress', false);
1029 $done = get_option('onwebchat_wc_bulk_done', 0);
1030 $total = get_option('onwebchat_wc_bulk_total', 0);
1031
1032 wp_send_json_success(array(
1033 'in_progress' => $in_progress,
1034 'done' => $done,
1035 'total' => $total
1036 ));
1037 }
1038
1039 /**
1040 * AJAX handler to save sync enabled setting
1041 */
1042 public function ajax_save_sync_enabled() {
1043 check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
1044
1045 if (!current_user_can('manage_options')) {
1046 wp_send_json_error('Insufficient permissions');
1047 }
1048
1049 $sync_enabled = isset($_POST['sync_enabled']) && $_POST['sync_enabled'] === '1';
1050
1051 update_option('onwebchat_wc_sync_enabled', $sync_enabled);
1052
1053 wp_send_json_success(array(
1054 'message' => $sync_enabled ? 'WooCommerce product sync enabled' : 'WooCommerce product sync disabled',
1055 'enabled' => $sync_enabled
1056 ));
1057 }
1058
1059 /**
1060 * Get sync status for admin display
1061 */
1062 public function get_sync_status() {
1063 $last_sync = get_option('onwebchat_wc_last_bulk_sync', 0);
1064 $in_progress = get_option('onwebchat_wc_bulk_in_progress', false);
1065 $done = get_option('onwebchat_wc_bulk_done', 0);
1066 $total = get_option('onwebchat_wc_bulk_total', 0);
1067
1068 return array(
1069 'last_sync' => $last_sync,
1070 'in_progress' => $in_progress,
1071 'done' => $done,
1072 'total' => $total,
1073 );
1074 }
1075
1076 /**
1077 * AJAX: Manually process batch (for debugging)
1078 */
1079 public function ajax_manual_process_batch() {
1080 check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
1081
1082 if (!current_user_can('manage_options')) {
1083 wp_send_json_error('Insufficient permissions');
1084 }
1085
1086 // Manually trigger the cron job
1087 error_log('onWebChat WooCommerce Sync - Manual batch process triggered via AJAX');
1088 $this->process_bulk_sync_batch();
1089
1090 // Return current status
1091 $status = $this->get_sync_status();
1092 wp_send_json_success(array(
1093 'message' => 'Batch processed',
1094 'status' => $status
1095 ));
1096 }
1097 }
1098
1099 // Initialize the sync module
1100 global $onwebchat_wc_sync;
1101 $onwebchat_wc_sync = new OnWebChat_WooCommerce_Sync();
1102
1103