File: /home/d5123/myboofola_com/wp-content/plugins/onwebchat/includes/woocommerce-sync.php
<?php
/**
* WooCommerce Product Sync Module
* Syncs WooCommerce products to onWebChat for AI bot training
*/
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
class OnWebChat_WooCommerce_Sync {
private $api_endpoint_prod = 'https://www.onwebchat.com/api/integrations/woocommerce';
private $api_endpoint_dev = 'http://127.0.0.1:81/api/integrations/woocommerce';
private $max_description_length = 1000;
private $batch_size = 50;
private $use_testing_mode;
/**
* Get the API endpoint based on testing mode
* @return string
*/
private function get_api_endpoint() {
return $this->use_testing_mode ? $this->api_endpoint_dev : $this->api_endpoint_prod;
}
public function __construct() {
// Read testing mode from global constant (defined in onwebchat.php)
$this->use_testing_mode = defined('ONWEBCHAT_WC_TESTING_MODE') ? ONWEBCHAT_WC_TESTING_MODE : false;
// Initialize settings
add_action('admin_init', array($this, 'register_settings'));
// Show authentication error notice globally (not just on WooCommerce tab)
add_action('admin_notices', array($this, 'show_auth_error_notice'));
// Product hooks - use WooCommerce hooks that fire AFTER meta data is saved
add_action('woocommerce_update_product', array($this, 'on_product_update'), 10, 1);
add_action('woocommerce_new_product', array($this, 'on_product_update'), 10, 1);
// Handle product deletion (both trash and permanent delete)
add_action('wp_trash_post', array($this, 'on_product_trash'), 10, 1);
add_action('before_delete_post', array($this, 'on_product_delete'), 10, 2);
// Bulk sync via WP Cron
add_action('onwebchat_wc_bulk_sync_batch', array($this, 'process_bulk_sync_batch'));
// Admin AJAX handlers
add_action('wp_ajax_onwebchat_wc_sync_now', array($this, 'ajax_sync_existing_products'));
add_action('wp_ajax_onwebchat_wc_regenerate_secret', array($this, 'ajax_regenerate_secret'));
add_action('wp_ajax_onwebchat_wc_reset_sync_status', array($this, 'ajax_reset_sync_status'));
add_action('wp_ajax_onwebchat_wc_connect', array($this, 'ajax_connect_woocommerce'));
add_action('wp_ajax_onwebchat_wc_manual_process_batch', array($this, 'ajax_manual_process_batch'));
add_action('wp_ajax_onwebchat_wc_get_sync_status', array($this, 'ajax_get_sync_status'));
add_action('wp_ajax_onwebchat_wc_save_sync_enabled', array($this, 'ajax_save_sync_enabled'));
}
/**
* AJAX: Connect WooCommerce with authentication
*/
public function ajax_connect_woocommerce() {
check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
$password = isset($_POST['password']) ? sanitize_text_field($_POST['password']) : '';
if (empty($email) || empty($password)) {
wp_send_json_error('Email and password are required');
}
$result = $this->request_secret_with_auth($email, $password);
if ($result['success']) {
// Clear any previous authentication errors
delete_transient('onwebchat_wc_auth_error');
wp_send_json_success(array(
'message' => 'WooCommerce sync connected successfully!'
));
} else {
wp_send_json_error($result['error']);
}
}
/**
* Show authentication error notice globally across all admin pages
* (Hidden when already on WooCommerce tab since it has its own error message)
*/
public function show_auth_error_notice() {
$auth_error = get_transient('onwebchat_wc_auth_error');
// Don't show if we're already on the WooCommerce tab (it has its own error message)
$is_woocommerce_tab = isset($_GET['page']) && $_GET['page'] === 'onwebchat_settings'
&& isset($_GET['tab']) && $_GET['tab'] === 'woocommerce';
if ($auth_error && class_exists('WooCommerce') && !$is_woocommerce_tab) {
?>
<div class="notice notice-error">
<p>
<strong>⚠️ onWebChat WooCommerce Sync Error:</strong>
<?php echo esc_html($auth_error); ?>
<a href="<?php echo esc_url(admin_url('admin.php?page=onwebchat_settings&tab=woocommerce')); ?>" class="button button-small" style="margin-left: 10px;">
Fix Authentication
</a>
</p>
</div>
<?php
}
}
/**
* Register WooCommerce sync settings
*/
public function register_settings() {
register_setting('onwebchat_wc_sync', 'onwebchat_wc_sync_enabled');
register_setting('onwebchat_wc_sync', 'onwebchat_wc_sync_mode');
register_setting('onwebchat_wc_sync', 'onwebchat_wc_sync_include_price');
register_setting('onwebchat_wc_sync', 'onwebchat_wc_sync_secret');
register_setting('onwebchat_wc_sync', 'onwebchat_wc_last_bulk_sync');
register_setting('onwebchat_wc_sync', 'onwebchat_wc_excluded_categories');
}
/**
* Hook: Product update (WooCommerce specific hook - fires AFTER all meta is saved)
*/
public function on_product_update($product_id) {
// Check if sync is enabled
if (!get_option('onwebchat_wc_sync_enabled', false)) {
return;
}
// Get product object (at this point all meta data including SKU is already saved)
$product = wc_get_product($product_id);
if (!$product) {
return;
}
// Only sync published products
if ($product->get_status() !== 'publish') {
return;
}
// Check if product category is excluded
if ($this->is_product_excluded($product)) {
return;
}
// Prepare and send product data
$product_data = $this->prepare_product_data($product);
$this->send_product_upsert($product_data, $product_id);
}
/**
* Hook: Product trash (when moved to trash)
*/
public function on_product_trash($post_id) {
// Check if it's a product
if (get_post_type($post_id) !== 'product') {
return;
}
if (!get_option('onwebchat_wc_sync_enabled', false)) {
return;
}
// Send delete request when product is trashed
$this->send_product_delete($post_id);
}
/**
* Hook: Product permanent delete
*/
public function on_product_delete($post_id, $post) {
if ($post->post_type !== 'product') {
return;
}
if (!get_option('onwebchat_wc_sync_enabled', false)) {
return;
}
// Send delete request when product is permanently deleted
$this->send_product_delete($post_id);
}
/**
* Check if product is in excluded categories
*/
private function is_product_excluded($product) {
$excluded_categories = get_option('onwebchat_wc_excluded_categories', array());
if (empty($excluded_categories)) {
return false;
}
$product_categories = $product->get_category_ids();
foreach ($product_categories as $cat_id) {
if (in_array($cat_id, $excluded_categories)) {
return true;
}
}
return false;
}
/**
* Prepare product data for sync
*/
private function prepare_product_data($product) {
$sync_mode = get_option('onwebchat_wc_sync_mode', 'short_fallback_full');
$include_price = get_option('onwebchat_wc_sync_include_price', false);
// Get description based on sync mode
$description = '';
$short_description = strip_tags($product->get_short_description());
if ($sync_mode === 'short_only') {
$description = $short_description;
} else if ($sync_mode === 'short_fallback_full') {
if (!empty($short_description)) {
$description = $short_description;
} else {
// Fallback to first 60-80 words of full description
$full_description = strip_tags($product->get_description());
$words = str_word_count($full_description, 2);
$word_array = array_keys($words);
if (count($word_array) > 80) {
$end_pos = $word_array[79];
$description = substr($full_description, 0, $end_pos) . '...';
} else {
$description = $full_description;
}
}
}
// Enforce max length
if (strlen($description) > $this->max_description_length) {
$description = substr($description, 0, $this->max_description_length) . '...';
}
// Build text field for embeddings
$sku = $product->get_sku();
$categories = $this->get_product_category_names($product);
$url = get_permalink($product->get_id());
// Format: "Product: name\nSKU: sku (if available)\nDescription: description\nCategories: cat1, cat2\nURL: url"
$text_parts = array();
$text_parts[] = 'Product: ' . $product->get_name();
if (!empty($sku)) {
$text_parts[] = 'SKU: ' . $sku;
}
if (!empty($description)) {
$text_parts[] = 'Description: ' . $description;
}
if (!empty($categories)) {
$text_parts[] = 'Categories: ' . implode(', ', $categories);
}
$text_parts[] = 'URL: ' . $url;
$text = implode("\n", $text_parts);
// Prepare data array
$data = array(
'product_id' => $product->get_id(),
'name' => $product->get_name(),
'short_description' => trim($description),
'url' => $url,
'sku' => $sku,
'categories' => $categories,
'text' => $text, // New field with formatted text for embeddings
);
// Optionally include price
if ($include_price) {
$data['price'] = $product->get_price();
$data['regular_price'] = $product->get_regular_price();
$data['sale_price'] = $product->get_sale_price();
}
return $data;
}
/**
* Get product category names
*/
private function get_product_category_names($product) {
$categories = array();
$category_ids = $product->get_category_ids();
foreach ($category_ids as $cat_id) {
$term = get_term($cat_id, 'product_cat');
if ($term && !is_wp_error($term)) {
$categories[] = $term->name;
}
}
return $categories;
}
/**
* Send batch of products to API (optimized)
* @param array $products - Array of product data
*/
private function send_product_batch($products) {
$chatId = get_option('onwebchat_plugin_option');
$chatId = (is_array($chatId) && isset($chatId['text_string'])) ? $chatId['text_string'] : '';
if (empty($chatId)) {
error_log('onWebChat WooCommerce Sync - Chat ID not configured');
return false;
}
// Ensure we have a secret (must be obtained via authenticated connection in WooCommerce settings)
$secret = $this->get_secret(false);
if (empty($secret)) {
error_log('onWebChat WooCommerce Sync - No secret configured. Please connect WooCommerce in the plugin settings.');
return array(
'success' => false,
'error' => 'No secret configured. Please connect WooCommerce integration.',
'needs_reconnect' => true
);
}
// Extract key part (before first slash if present)
$chatIdKey = explode('/', $chatId)[0];
$endpoint = $this->get_api_endpoint() . '/product/batch';
$payload = array(
'site_id' => $chatIdKey,
'site_url' => get_site_url(),
'products' => $products
);
// Generate authentication headers (same as send_authenticated_request)
$timestamp = time();
$nonce = base64_encode(random_bytes(16));
$body_json = wp_json_encode($payload);
// Create signature: HMAC_SHA256(secret, site_id.timestamp.nonce.body)
$message = $chatIdKey . '.' . $timestamp . '.' . $nonce . '.' . $body_json;
$signature = hash_hmac('sha256', $message, $secret);
// Send request
$request_args = array(
'method' => 'POST',
'timeout' => 30, // Longer timeout for batch operations
'headers' => array(
'Content-Type' => 'application/json',
'X-OWC-SiteId' => $chatIdKey,
'X-OWC-Timestamp' => $timestamp,
'X-OWC-Nonce' => $nonce,
'X-OWC-Signature' => $signature,
),
'body' => $body_json,
);
// Disable SSL verification for local dev server
if ($this->use_testing_mode) {
$request_args['sslverify'] = false;
}
$response = wp_remote_post($endpoint, $request_args);
if (is_wp_error($response)) {
error_log('onWebChat WooCommerce Sync - Batch sync error: ' . $response->get_error_message());
return array(
'success' => false,
'error' => 'Network error: ' . $response->get_error_message()
);
}
$response_code = wp_remote_retrieve_response_code($response);
$body = json_decode(wp_remote_retrieve_body($response), true);
// If authentication failed (401), the secret is invalid or out of sync
if ($response_code === 401) {
// Clear the invalid secret
delete_option('onwebchat_wc_sync_secret');
error_log('onWebChat WooCommerce Sync - Authentication failed (401): Secret is invalid or out of sync. Please reconnect WooCommerce in the plugin settings.');
// Store admin notice about authentication failure
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);
return array(
'success' => false,
'error' => 'Authentication failed. Secret is invalid. Please reconnect WooCommerce integration.',
'needs_reconnect' => true
);
}
if ($response_code === 200 && isset($body['success']) && $body['success']) {
// Clear any previous auth errors on success
delete_transient('onwebchat_wc_auth_error');
return $body; // Return full response with stats
}
$error_msg = 'Batch sync failed';
if (isset($body['error'])) {
$error_msg .= ': ' . $body['error'];
}
error_log('onWebChat WooCommerce Sync - ' . $error_msg . ' - Response: ' . print_r($body, true));
return array(
'success' => false,
'error' => $error_msg
);
}
/**
* Send sync completion notification to server (triggers Angular modal)
*/
private function send_sync_completion_notification($total_stats) {
$chatId = get_option('onwebchat_plugin_option');
$chatId = (is_array($chatId) && isset($chatId['text_string'])) ? $chatId['text_string'] : '';
if (empty($chatId)) {
error_log('onWebChat WooCommerce Sync - Chat ID not configured');
return false;
}
$secret = $this->get_secret(false);
if (empty($secret)) {
error_log('onWebChat WooCommerce Sync - No secret configured');
return false;
}
$chatIdKey = explode('/', $chatId)[0];
$endpoint = $this->get_api_endpoint() . '/product/sync-complete';
$payload = array(
'site_id' => $chatIdKey,
'stats' => $total_stats
);
// Generate authentication headers
$timestamp = time();
$nonce = base64_encode(random_bytes(16));
$body_json = wp_json_encode($payload);
$message = $chatIdKey . '.' . $timestamp . '.' . $nonce . '.' . $body_json;
$signature = hash_hmac('sha256', $message, $secret);
$request_args = array(
'method' => 'POST',
'timeout' => 10,
'headers' => array(
'Content-Type' => 'application/json',
'X-OWC-SiteId' => $chatIdKey,
'X-OWC-Timestamp' => $timestamp,
'X-OWC-Nonce' => $nonce,
'X-OWC-Signature' => $signature,
),
'body' => $body_json,
);
if ($this->use_testing_mode) {
$request_args['sslverify'] = false;
}
$response = wp_remote_post($endpoint, $request_args);
if (is_wp_error($response)) {
error_log('onWebChat WooCommerce Sync - Completion notification failed: ' . $response->get_error_message());
return false;
}
$response_code = wp_remote_retrieve_response_code($response);
// If authentication failed (401), the secret is invalid or out of sync
if ($response_code === 401) {
delete_option('onwebchat_wc_sync_secret');
error_log('onWebChat WooCommerce Sync - Completion notification authentication failed (401): Secret is invalid. Please reconnect WooCommerce.');
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);
return false;
}
if ($response_code >= 200 && $response_code < 300) {
error_log('onWebChat WooCommerce Sync - Completion notification sent successfully');
return true;
}
error_log('onWebChat WooCommerce Sync - Completion notification failed with code: ' . $response_code);
return false;
}
/**
* Send product upsert to server (uses batch endpoint with single product)
*/
private function send_product_upsert($product_data, $product_id) {
// Use batch endpoint with single product
$result = $this->send_product_batch(array($product_data));
if ($result && isset($result['success']) && $result['success']) {
// Clear any previous errors
delete_post_meta($product_id, '_onwebchat_sync_error');
update_post_meta($product_id, '_onwebchat_last_sync', current_time('timestamp'));
return true;
} else {
// Log error if batch failed
$error_message = 'Failed to sync product';
if ($result && isset($result['error'])) {
$error_message = $result['error'];
} elseif (!$result) {
$error_message = 'Batch sync request failed';
}
$this->log_error($product_id, $error_message);
return false;
}
}
/**
* Send product delete to server
*/
private function send_product_delete($product_id) {
$chatId = get_option('onwebchat_plugin_option');
$chatId = (is_array($chatId) && isset($chatId['text_string'])) ? $chatId['text_string'] : '';
if (empty($chatId)) {
return false;
}
// Extract key part (before first slash if present)
$chatIdKey = explode('/', $chatId)[0];
$endpoint = $this->get_api_endpoint() . '/product/delete';
$payload = array(
'site_id' => $chatIdKey, // Use key part only
'site_url' => get_site_url(),
'product_id' => $product_id
);
$this->send_authenticated_request($endpoint, $payload, $product_id);
}
/**
* Get cached secret from local options
* @param {bool} force_refresh - Not used (kept for compatibility), secret must be obtained via authenticated request
*/
private function get_secret($force_refresh = false) {
// Always return cached secret - never fetch automatically
// Secret must be obtained via authenticated request in WooCommerce settings
$secret = get_option('onwebchat_wc_sync_secret');
if (!empty($secret)) {
return $secret;
}
// No secret available - user must authenticate in WooCommerce settings
return null;
}
/**
* Request secret from server with authentication
* This is called when user clicks "Connect WooCommerce" with their password
*
* @param {string} email - User's onWebChat email
* @param {string} password - User's onWebChat password
* @return {array} - ['success' => bool, 'secret' => string, 'error' => string]
*/
public function request_secret_with_auth($email, $password) {
$chatId = get_option('onwebchat_plugin_option');
$chatId = (is_array($chatId) && isset($chatId['text_string'])) ? $chatId['text_string'] : '';
if (empty($chatId)) {
return array('success' => false, 'error' => 'No Chat ID configured');
}
// Extract key part (before first slash if present)
$key = explode('/', $chatId)[0];
// Request secret from server with authentication
$secret_endpoint = $this->get_api_endpoint() . '/secret';
$response = wp_remote_post($secret_endpoint, array(
'timeout' => 15,
'sslverify' => !$this->use_testing_mode,
'headers' => array(
'Content-Type' => 'application/json',
),
'body' => wp_json_encode(array(
'email' => $email,
'password' => $password,
'site_key' => $key,
'version' => defined('ONWEBCHAT_PLUGIN_VERSION') ? ONWEBCHAT_PLUGIN_VERSION : '',
)),
));
if (is_wp_error($response)) {
$error_message = $response->get_error_message();
error_log('onWebChat WooCommerce Sync - Connection error: ' . $error_message);
return array('success' => false, 'error' => 'Connection failed: ' . $error_message);
}
$status_code = wp_remote_retrieve_response_code($response);
$response_body_raw = wp_remote_retrieve_body($response);
$body = json_decode($response_body_raw, true);
// Log response for debugging
error_log('onWebChat WooCommerce Sync - API response: Status=' . $status_code . ', Body=' . substr($response_body_raw, 0, 500));
// Handle specific HTTP status codes
if ($status_code === 401) {
return array('success' => false, 'error' => 'Invalid email or password');
}
if ($status_code === 403) {
return array('success' => false, 'error' => 'You do not have access to this site');
}
// Success case
if ($status_code >= 200 && $status_code < 300 && isset($body['success']) && $body['success']) {
$secret = isset($body['secret']) ? $body['secret'] : null;
if (empty($secret)) {
error_log('onWebChat WooCommerce Sync - Success response but no secret provided');
return array('success' => false, 'error' => 'Server response missing secret');
}
update_option('onwebchat_wc_sync_secret', $secret);
return array('success' => true, 'secret' => $secret);
}
// Extract error message from various possible response formats
$error_message = 'Unknown error';
if (is_array($body)) {
// Try different possible error fields
if (isset($body['error'])) {
$error_message = is_string($body['error']) ? $body['error'] : json_encode($body['error']);
} elseif (isset($body['message'])) {
$error_message = is_string($body['message']) ? $body['message'] : json_encode($body['message']);
} elseif (isset($body['errors']) && is_array($body['errors'])) {
$error_message = implode(', ', $body['errors']);
}
} elseif (!empty($response_body_raw)) {
// If body is not JSON or empty, use raw response (truncated)
$error_message = 'Server returned: ' . substr(strip_tags($response_body_raw), 0, 200);
}
// Include status code in error message if not already included
if ($status_code && strpos($error_message, 'HTTP') === false) {
$error_message = 'HTTP ' . $status_code . ': ' . $error_message;
}
error_log('onWebChat WooCommerce Sync - Connection failed: ' . $error_message);
return array('success' => false, 'error' => $error_message);
}
/**
* Send authenticated request with HMAC signature
*/
private function send_authenticated_request($endpoint, $payload, $product_id = null) {
// Get cached secret (must be obtained via authenticated connection in WooCommerce settings)
$secret = $this->get_secret(false);
if (empty($secret)) {
if ($product_id) {
$this->log_error($product_id, 'No secret configured. Please connect WooCommerce in the plugin settings.');
}
return array('success' => false, 'error' => 'Secret not available. Please connect WooCommerce in plugin settings.');
}
$chatId = get_option('onwebchat_plugin_option');
$chatId = (is_array($chatId) && isset($chatId['text_string'])) ? $chatId['text_string'] : '';
// Extract key part (before first slash if present) for consistency with server
// e.g., "5f02c87b60726a4663b25463a424a034/1/1" -> "5f02c87b60726a4663b25463a424a034"
$chatIdKey = explode('/', $chatId)[0];
// Generate authentication headers
$timestamp = time();
$nonce = base64_encode(random_bytes(16));
$body_json = wp_json_encode($payload);
// Create signature: HMAC_SHA256(secret, site_id.timestamp.nonce.body)
// IMPORTANT: Use the key part (not full chat_id) to match server-side verification
$message = $chatIdKey . '.' . $timestamp . '.' . $nonce . '.' . $body_json;
$signature = hash_hmac('sha256', $message, $secret);
// Send request
$request_args = array(
'method' => 'POST',
'timeout' => 10,
'headers' => array(
'Content-Type' => 'application/json',
'X-OWC-SiteId' => $chatIdKey, // Use key part only
'X-OWC-Timestamp' => $timestamp,
'X-OWC-Nonce' => $nonce,
'X-OWC-Signature' => $signature,
),
'body' => $body_json,
);
// Disable SSL verification for local dev server
if ($this->use_testing_mode) {
$request_args['sslverify'] = false;
}
$response = wp_remote_post($endpoint, $request_args);
// Handle response
if (is_wp_error($response)) {
$error_message = $response->get_error_message();
if ($product_id) {
$this->log_error($product_id, $error_message);
}
return array('success' => false, 'error' => $error_message);
}
$status_code = wp_remote_retrieve_response_code($response);
// Success
if ($status_code >= 200 && $status_code < 300) {
return array('success' => true);
}
// If authentication failed (401), the secret may be invalid
if ($status_code === 401) {
// Clear the invalid secret
delete_option('onwebchat_wc_sync_secret');
if ($product_id) {
$this->log_error($product_id, 'Authentication failed. Please reconnect WooCommerce in the plugin settings.');
}
return array('success' => false, 'error' => 'Authentication failed. Please reconnect WooCommerce in plugin settings.');
}
// Error
$error_body = wp_remote_retrieve_body($response);
if ($product_id) {
$this->log_error($product_id, "HTTP $status_code: $error_body");
}
return array('success' => false, 'error' => "HTTP $status_code", 'status_code' => $status_code);
}
/**
* Log sync error to product meta
*/
private function log_error($product_id, $error_message) {
update_post_meta($product_id, '_onwebchat_sync_error', array(
'message' => $error_message,
'timestamp' => current_time('timestamp')
));
}
/**
* AJAX: Start bulk sync
*/
public function ajax_sync_existing_products() {
check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
// Check if sync is already in progress
if (get_option('onwebchat_wc_bulk_in_progress', false)) {
wp_send_json_error('A sync is already in progress. Please wait for it to complete.');
}
// Rate limiting: prevent syncing more than once every 5 minutes
$last_sync_time = get_option('onwebchat_wc_last_sync_start', 0);
$cooldown_period = 5 * 60; // 5 minutes in seconds //also in the file woocommerce.php // 5 * 60
$time_since_last_sync = time() - $last_sync_time;
if ($time_since_last_sync < $cooldown_period) {
$wait_time = $cooldown_period - $time_since_last_sync;
$minutes = ceil($wait_time / 60);
wp_send_json_error('Please wait ' . $minutes . ' minute(s) before syncing again.');
}
// Store the current sync start time
update_option('onwebchat_wc_last_sync_start', time());
// Reset bulk sync progress
update_option('onwebchat_wc_bulk_page', 0);
update_option('onwebchat_wc_bulk_done', 0);
// Count total products
$args = array(
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
);
$products = get_posts($args);
$total = count($products);
update_option('onwebchat_wc_bulk_total', $total);
update_option('onwebchat_wc_bulk_done', 0); // Initialize progress counter
update_option('onwebchat_wc_bulk_in_progress', true);
// Process sync directly instead of using unreliable WP Cron
$sync_result = $this->do_bulk_sync_all();
wp_send_json_success(array(
'message' => 'Bulk sync completed',
'total' => $total,
'result' => $sync_result
));
}
/**
* Process all products in bulk sync directly (not via cron)
*/
private function do_bulk_sync_all() {
$total = get_option('onwebchat_wc_bulk_total', 0);
$page = 0;
$total_done = 0;
$all_stats = array('created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => 0);
// Process all products in batches
while (true) {
$args = array(
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => $this->batch_size,
'paged' => $page + 1,
'orderby' => 'ID',
'order' => 'ASC',
);
$query = new WP_Query($args);
if (!$query->have_posts()) {
break;
}
// Collect products in this batch
$products_batch = array();
foreach ($query->posts as $post) {
$product = wc_get_product($post->ID);
if ($product && !$this->is_product_excluded($product)) {
$products_batch[] = $this->prepare_product_data($product);
}
}
// Send batch
if (!empty($products_batch)) {
$result = $this->send_product_batch($products_batch);
if ($result && isset($result['stats'])) {
$total_done += $result['stats']['created'] + $result['stats']['updated'] + $result['stats']['skipped'];
$all_stats['created'] += $result['stats']['created'];
$all_stats['updated'] += $result['stats']['updated'];
$all_stats['skipped'] += $result['stats']['skipped'];
$all_stats['errors'] += $result['stats']['errors'];
} else {
// Fallback
$total_done += count($products_batch);
}
// Update progress after each batch so AJAX polling can see it
update_option('onwebchat_wc_bulk_done', $total_done);
// Wait 4 seconds before next batch
sleep(4);
}
wp_reset_postdata();
$page++;
// Safety check - don't loop forever
if ($page > 100) {
break;
}
}
// Send completion notification to Angular dashboard with total stats
$this->send_sync_completion_notification($all_stats);
// Mark sync as complete
update_option('onwebchat_wc_bulk_in_progress', false);
update_option('onwebchat_wc_bulk_done', $total_done);
update_option('onwebchat_wc_last_bulk_sync', current_time('timestamp'));
return array(
'done' => $total_done,
'total' => $total,
'stats' => $all_stats
);
}
/**
* Process bulk sync batch (via WP Cron) - Uses batch API endpoint
*/
public function process_bulk_sync_batch() {
error_log('onWebChat WooCommerce Sync - process_bulk_sync_batch called');
if (!get_option('onwebchat_wc_bulk_in_progress', false)) {
error_log('onWebChat WooCommerce Sync - Sync not in progress, exiting');
return;
}
$page = get_option('onwebchat_wc_bulk_page', 0);
$done = get_option('onwebchat_wc_bulk_done', 0);
$total = get_option('onwebchat_wc_bulk_total', 0);
error_log('onWebChat WooCommerce Sync - Starting batch: page=' . $page . ', done=' . $done . ', total=' . $total);
// Get batch of products
$args = array(
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => $this->batch_size,
'paged' => $page + 1,
'orderby' => 'ID',
'order' => 'ASC',
);
$query = new WP_Query($args);
if ($query->have_posts()) {
// Collect all products in this batch
$products_batch = array();
foreach ($query->posts as $post) {
$product = wc_get_product($post->ID);
if ($product && !$this->is_product_excluded($product)) {
$products_batch[] = $this->prepare_product_data($product);
}
}
// Send entire batch in one request
$batch_done = 0;
if (!empty($products_batch)) {
$result = $this->send_product_batch($products_batch);
error_log('onWebChat WooCommerce Sync - Batch result: ' . print_r($result, true));
if ($result && isset($result['stats'])) {
// Count created + updated + skipped as "done"
$batch_done = $result['stats']['created'] + $result['stats']['updated'] + $result['stats']['skipped'];
error_log('onWebChat WooCommerce Sync - Batch done: ' . $batch_done);
} else {
// Fallback: assume all sent
$batch_done = count($products_batch);
error_log('onWebChat WooCommerce Sync - No stats in result, using fallback count: ' . $batch_done);
}
}
// Update progress
$new_done = $done + $batch_done;
error_log('onWebChat WooCommerce Sync - Progress update: done=' . $done . ' + batch_done=' . $batch_done . ' = new_done=' . $new_done . ' / total=' . $total);
update_option('onwebchat_wc_bulk_page', $page + 1);
update_option('onwebchat_wc_bulk_done', $new_done);
// Check if we've processed all products
if ($new_done >= $total || !$query->have_posts()) {
// Sync complete
error_log('onWebChat WooCommerce Sync - Marking sync as complete');
update_option('onwebchat_wc_bulk_in_progress', false);
update_option('onwebchat_wc_last_bulk_sync', current_time('timestamp'));
update_option('onwebchat_wc_bulk_done', $total); // Ensure it shows 100%
} else {
// Schedule next batch
error_log('onWebChat WooCommerce Sync - Scheduling next batch in 60 seconds');
wp_schedule_single_event(time() + 60, 'onwebchat_wc_bulk_sync_batch');
}
} else {
// No more products - sync complete
update_option('onwebchat_wc_bulk_in_progress', false);
update_option('onwebchat_wc_last_bulk_sync', current_time('timestamp'));
update_option('onwebchat_wc_bulk_done', $total); // Ensure it shows 100%
}
wp_reset_postdata();
}
/**
* AJAX: Regenerate secret (fetch from server)
*/
public function ajax_regenerate_secret() {
check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
// Clear cached secret - user will need to re-authenticate
delete_option('onwebchat_wc_sync_secret');
// Clear any authentication error notices
delete_transient('onwebchat_wc_auth_error');
wp_send_json_success(array(
'message' => 'Secret cleared. Please reconnect WooCommerce with your credentials.',
'needs_reconnect' => true
));
}
/**
* AJAX: Reset sync status
*/
public function ajax_reset_sync_status() {
check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$total = get_option('onwebchat_wc_bulk_total', 0);
// Mark sync as complete
update_option('onwebchat_wc_bulk_in_progress', false);
update_option('onwebchat_wc_bulk_done', $total);
update_option('onwebchat_wc_last_bulk_sync', current_time('timestamp'));
wp_send_json_success(array(
'message' => 'Sync status reset successfully'
));
}
/**
* AJAX handler to get current sync status
*/
public function ajax_get_sync_status() {
check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$in_progress = get_option('onwebchat_wc_bulk_in_progress', false);
$done = get_option('onwebchat_wc_bulk_done', 0);
$total = get_option('onwebchat_wc_bulk_total', 0);
wp_send_json_success(array(
'in_progress' => $in_progress,
'done' => $done,
'total' => $total
));
}
/**
* AJAX handler to save sync enabled setting
*/
public function ajax_save_sync_enabled() {
check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
$sync_enabled = isset($_POST['sync_enabled']) && $_POST['sync_enabled'] === '1';
update_option('onwebchat_wc_sync_enabled', $sync_enabled);
wp_send_json_success(array(
'message' => $sync_enabled ? 'WooCommerce product sync enabled' : 'WooCommerce product sync disabled',
'enabled' => $sync_enabled
));
}
/**
* Get sync status for admin display
*/
public function get_sync_status() {
$last_sync = get_option('onwebchat_wc_last_bulk_sync', 0);
$in_progress = get_option('onwebchat_wc_bulk_in_progress', false);
$done = get_option('onwebchat_wc_bulk_done', 0);
$total = get_option('onwebchat_wc_bulk_total', 0);
return array(
'last_sync' => $last_sync,
'in_progress' => $in_progress,
'done' => $done,
'total' => $total,
);
}
/**
* AJAX: Manually process batch (for debugging)
*/
public function ajax_manual_process_batch() {
check_ajax_referer('onwebchat_wc_sync_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions');
}
// Manually trigger the cron job
error_log('onWebChat WooCommerce Sync - Manual batch process triggered via AJAX');
$this->process_bulk_sync_batch();
// Return current status
$status = $this->get_sync_status();
wp_send_json_success(array(
'message' => 'Batch processed',
'status' => $status
));
}
}
// Initialize the sync module
global $onwebchat_wc_sync;
$onwebchat_wc_sync = new OnWebChat_WooCommerce_Sync();