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) { ?>

⚠️ onWebChat WooCommerce Sync Error: Fix Authentication

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'); // 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) . '...'; } $sku = $product->get_sku(); $categories = $this->get_product_category_names($product); $url = get_permalink($product->get_id()); // Structured fields. The server rebuilds the embedding text from these, // so there is no need to send a pre-formatted "text" blob. $data = array( 'product_id' => $product->get_id(), 'name' => $product->get_name(), 'short_description' => trim($description), 'url' => $url, 'sku' => $sku, 'categories' => $categories, 'currency' => get_woocommerce_currency(), ); // Price (always sent). Variable products carry a min/max range. $data['price'] = $product->get_price(); if ($product->is_type('variable')) { // Raw min/max prices, consistent with get_price() used for simple products. $data['price_min'] = $product->get_variation_price('min', false); $data['price_max'] = $product->get_variation_price('max', false); } else { $regular_price = $product->get_regular_price(); if ($regular_price !== '') { $data['regular_price'] = $regular_price; } // Only advertise a sale price while the sale is actually active. if ($product->is_on_sale()) { $data['sale_price'] = $product->get_sale_price(); } } // Stock availability $data['in_stock'] = $product->is_in_stock(); if ($product->managing_stock()) { $stock_qty = $product->get_stock_quantity(); if ($stock_qty !== null) { $data['quantity'] = (int) $stock_qty; } } // Brand (renders as "Brand:" on the server). Detect the common brand taxonomies. $brand = $this->get_product_brand($product); if (!empty($brand)) { $data['manufacturer'] = $brand; } // Variation attributes / options (Color, Size, ...) $attributes = $this->get_product_attributes($product); if (!empty($attributes)) { $data['attributes'] = $attributes; } // Tags $tags = $this->get_product_tags($product); if (!empty($tags)) { $data['tags'] = $tags; } // Average rating and review count $rating = (float) $product->get_average_rating(); if ($rating > 0) { $data['rating'] = $rating; $data['review_count'] = (int) $product->get_review_count(); } 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; } /** * Get the product's brand name from whichever brand taxonomy is available. * Supports WooCommerce 9.6+ native brands and the common brand plugins. */ private function get_product_brand($product) { $taxonomies = array('product_brand', 'pwb-brand', 'yith_product_brand', 'pa_brand'); foreach ($taxonomies as $taxonomy) { if (!taxonomy_exists($taxonomy)) { continue; } $terms = wp_get_post_terms($product->get_id(), $taxonomy, array('fields' => 'names')); if (!is_wp_error($terms) && !empty($terms)) { return $terms[0]; } } return ''; } /** * Get visible product attributes as an array of { name, options }. * Works for both custom and taxonomy-based (global) attributes. */ private function get_product_attributes($product) { $result = array(); foreach ($product->get_attributes() as $attribute) { if (!is_object($attribute) || !$attribute->get_visible()) { continue; } $name = wc_attribute_label($attribute->get_name()); if ($attribute->is_taxonomy()) { $options = wc_get_product_terms($product->get_id(), $attribute->get_name(), array('fields' => 'names')); } else { $options = $attribute->get_options(); } $options = array_values(array_filter(array_map('trim', (array) $options))); if (!empty($name) && !empty($options)) { $result[] = array( 'name' => $name, 'options' => $options, ); } } return $result; } /** * Get product tag names. */ private function get_product_tags($product) { $tags = wp_get_post_terms($product->get_id(), 'product_tag', array('fields' => 'names')); if (is_wp_error($tags) || empty($tags)) { return array(); } return $tags; } /** * 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. Redact the secret so it never lands in server/debug logs // (a successful response body contains the HMAC secret). $log_body = preg_replace('/("secret"\s*:\s*")[^"]*(")/i', '$1[REDACTED]$2', (string) $response_body_raw); error_log('onWebChat WooCommerce Sync - API response: Status=' . $status_code . ', Body=' . substr($log_body, 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); } // Defense in depth: the remote error is shown in the admin UI, so strip any markup here too // (the client also renders it as text). Prevents a malicious/MITM'd API response carrying HTML. $error_message = sanitize_text_field($error_message); // 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();