PluginProbe
AI Chatbot for WooCommerce & Live Chat – onWebChat / 3.10.0
AI Chatbot for WooCommerce & Live Chat – onWebChat v3.10.0
3.10.0 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 All 49 releases
← All changes | includes/woocommerce-sync.php +85 -16 3.9.33.10.0 View file →
@@ -453,19 +453,16 @@
453 453 return (string) get_option('onwebchat_wc_sync_scope_all', '') !== '' || (bool) $this->get_sync_scope();
454 454 }
455 455
456 456 /**
457 - * Removing everything switches automatic sync off, so the products cannot
458 - * come straight back on the next product edit. Starting a bulk sync is the
459 - * merchant asking for products again, so the switch goes back on, but only
460 - * when it was this plugin that turned it off: a merchant who turned it off
461 - * themselves keeps it off. The note is one-shot and cleared either way.
457 + * Running a product sync is the merchant asking for their products in the
458 + * chatbot, so it also switches automatic sync on: later edits, stock changes
459 + * and images then reach the bot on their own. Before 3.10.0 the Sync button
460 + * left the switch alone, so a store that never ticked it synced once and
461 + * went stale without anyone noticing. The "off by removal" note (set when
462 + * removing every category turned the switch off) has served its purpose.
462 463 */
463 - private function resume_auto_sync_after_removal() {
464 - if (!get_option('onwebchat_wc_sync_off_by_removal', false)) {
465 - return;
466 - }
467 -
464 + private function enable_auto_sync_for_run() {
468 465 delete_option('onwebchat_wc_sync_off_by_removal');
469 466
470 467 if (!get_option('onwebchat_wc_sync_enabled', false)) {
471 468 update_option('onwebchat_wc_sync_enabled', true);
@@ -742,9 +739,11 @@
742 739 // Structured fields. The server rebuilds the embedding text from these,
743 740 // so there is no need to send a pre-formatted "text" blob.
744 741 $data = array(
745 742 'product_id' => $product->get_id(),
746 - 'name' => $product->get_name(),
743 + // Names are stored HTML-escaped ("Bags & Belts"), so decode them: the name is
744 + // also the title of the product card the widget shows (3.10.0+).
745 + 'name' => $this->decode_entities($product->get_name()),
747 746 'short_description' => trim($description),
748 747 'url' => $url,
749 748 'sku' => $sku,
750 749 'categories' => $categories,
@@ -818,10 +817,79 @@
818 817 $data['rating'] = $rating;
819 818 $data['review_count'] = (int) $product->get_review_count();
820 819 }
821 820
821 + // Product thumbnail (3.10.0+): shown as a small product card under the chatbot's reply
822 + // when it recommends this product. Always sent, '' when the product has no usable
823 + // image: the server then clears the thumbnail it stored for an earlier sync.
824 + $data['image'] = $this->get_product_image_url($product);
825 +
822 826 return $data;
823 827 }
828 +
829 + /**
830 + * Thumbnail URL for the product card the chat widget shows under a chatbot reply, or ''
831 + * when the product has no usable image.
832 + *
833 + * - The WooCommerce catalogue thumbnail size (300px by default), so the widget never
834 + * loads the full-size photo. WordPress falls back to the original file when that
835 + * size was never generated.
836 + * - Uploaded file names keep non-Latin letters (a Greek "κούπα.jpg" stays Greek in the
837 + * URL) and WordPress returns them unencoded, so every byte outside printable ASCII
838 + * is percent-encoded here: the widget, the dashboard and the server then all handle
839 + * one plain ASCII URL. Already encoded parts (%CE%BA...) are left as they are.
840 + * - Shops served over HTTPS get an HTTPS image link, otherwise the browser would block
841 + * the picture on the shop page as mixed content.
842 + * - Anything that is not an absolute http(s) URL, or is longer than the 1000 characters
843 + * the server stores, is dropped (the product then syncs without a picture).
844 + */
845 + private function get_product_image_url($product) {
846 + $image_id = (int) $product->get_image_id();
847 + if ($image_id <= 0) {
848 + return '';
849 + }
850 +
851 + $image_url = wp_get_attachment_image_url($image_id, 'woocommerce_thumbnail');
852 + if (!$image_url) {
853 + $image_url = wp_get_attachment_image_url($image_id, 'thumbnail');
854 + }
855 + if (!is_string($image_url)) {
856 + return '';
857 + }
858 +
859 + $image_url = trim($image_url);
860 + if ($image_url === '') {
861 + return '';
862 + }
863 +
864 + $site_is_https = is_ssl() || (stripos(home_url('/'), 'https://') === 0);
865 +
866 + // Protocol-relative URL (some CDN plugins return "//cdn.example.com/...").
867 + if (substr($image_url, 0, 2) === '//') {
868 + $image_url = ($site_is_https ? 'https:' : 'http:') . $image_url;
869 + }
870 +
871 + if ($site_is_https && stripos($image_url, 'http://') === 0) {
872 + $image_url = set_url_scheme($image_url, 'https');
873 + }
874 +
875 + // Percent-encode every byte outside printable ASCII (multibyte letters, spaces,
876 + // control characters). No /u flag on purpose: each byte of a UTF-8 sequence is
877 + // encoded separately, which is exactly the encoding a browser would apply.
878 + $image_url = preg_replace_callback('/[^\x21-\x7E]/', function ($m) {
879 + return rawurlencode($m[0]);
880 + }, $image_url);
881 +
882 + if (!is_string($image_url) || !preg_match('#^https?://[^\s<>"\'\\\\]+$#i', $image_url)) {
883 + return '';
884 + }
885 +
886 + if (strlen($image_url) > 1000) {
887 + return '';
888 + }
889 +
890 + return $image_url;
891 + }
824 892
825 893 /**
826 894 * Get product category names
827 895 */
@@ -831,9 +899,9 @@
831 899
832 900 foreach ($category_ids as $cat_id) {
833 901 $term = get_term($cat_id, 'product_cat');
834 902 if ($term && !is_wp_error($term)) {
835 - $categories[] = $term->name;
903 + $categories[] = $this->decode_entities($term->name);
836 904 }
837 905 }
838 906
839 907 return $categories;
@@ -852,9 +920,9 @@
852 920 }
853 921
854 922 $terms = wp_get_post_terms($product->get_id(), $taxonomy, array('fields' => 'names'));
855 923 if (!is_wp_error($terms) && !empty($terms)) {
856 - return $terms[0];
924 + return $this->decode_entities($terms[0]);
857 925 }
858 926 }
859 927
860 928 return '';
@@ -902,9 +970,9 @@
902 970 if (is_wp_error($tags) || empty($tags)) {
903 971 return array();
904 972 }
905 973
906 - return $tags;
974 + return array_map(array($this, 'decode_entities'), $tags);
907 975 }
908 976
909 977 /**
910 978 * Send batch of products to API (optimized)
@@ -1536,9 +1604,9 @@
1536 1604 }
1537 1605 }
1538 1606
1539 1607 $this->save_sync_scope($new_scope, $new_all);
1540 - $this->resume_auto_sync_after_removal();
1608 + $this->enable_auto_sync_for_run();
1541 1609 update_option('onwebchat_wc_bulk_run_terms', implode(',', array_map('intval', $run_terms)));
1542 1610 update_option('onwebchat_wc_bulk_run_exclude', implode(',', array_map('intval', $run_exclude)));
1543 1611
1544 1612 // Store the current sync start time
@@ -1614,8 +1682,9 @@
1614 1682 // Remember the merchant's choice so ongoing auto-sync stays within it:
1615 1683 // selected categories become the sync scope; an unrestricted "sync all"
1616 1684 // puts the whole catalogue in scope.
1617 1685 $this->save_sync_scope($category_ids, empty($category_ids));
1686 + $this->enable_auto_sync_for_run();
1618 1687
1619 1688 // Count total products within scope, capped at the hard limit.
1620 1689 $total = $this->count_products_in_scope($category_ids);
1621 1690 if ($total > self::MAX_SYNC_PRODUCTS) {
@@ -1632,9 +1701,9 @@
1632 1701 // sync, so a 0-product start (empty scope) can't leave the store stuck at
1633 1702 // "a sync is already in progress".
1634 1703 update_option('onwebchat_wc_bulk_in_progress', $total > 0);
1635 1704
1636 - wp_send_json_success(array('total' => $total));
1705 + wp_send_json_success(array('total' => $total, 'auto_sync_enabled' => true));
1637 1706 }
1638 1707
1639 1708 /**
1640 1709 * AJAX: process the next page of the in-progress bulk sync and report progress.