PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.3
MxChat – AI Chatbot & Content Generation for WordPress v3.1.3
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | admin/class-knowledge-manager.php +125 -919 3.2.83.1.3 View file →
@@ -56,11 +56,12 @@
56 56 add_action('wp_ajax_mxchat_get_recent_entries', array($this, 'ajax_mxchat_get_recent_entries'));
57 57 add_action('wp_ajax_mxchat_detect_sitemaps', array($this, 'ajax_mxchat_detect_sitemaps'));
58 58 add_action('wp_ajax_mxchat_refresh_pinecone_entries', array($this, 'ajax_mxchat_refresh_pinecone_entries'));
59 59 add_action('wp_ajax_mxchat_paginate_entries', array($this, 'ajax_mxchat_paginate_entries'));
60 - add_action('wp_ajax_mxchat_get_entry_content', array($this, 'ajax_mxchat_get_entry_content'));
61 - add_action('wp_ajax_mxchat_save_entry_content', array($this, 'ajax_mxchat_save_entry_content'));
62 60
61 + // Hook for content deletion
62 + add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1);
63 +
63 64 // WordPress post management hooks
64 65 add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
65 66 add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3);
66 67 add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
@@ -160,13 +161,9 @@
160 161 public function mxchat_is_pdf_url($url, $response) {
161 162 $content_type = wp_remote_retrieve_header($response, 'content-type');
162 163 $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
163 164
164 - // Check Content-Disposition header for .pdf filename (Google Drive sends this)
165 - $disposition = wp_remote_retrieve_header($response, 'content-disposition');
166 - $has_pdf_disposition = ! empty($disposition) && stripos($disposition, '.pdf') !== false;
167 -
168 - return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf' || $has_pdf_disposition;
165 + return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf';
169 166 }
170 167
171 168
172 169 public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') {
@@ -623,269 +620,8 @@
623 620 }
624 621 }
625 622
626 623
627 -/**
628 - * AJAX: Get full content for editing — reassembles chunks if needed.
629 - * Works for both WordPress DB and Pinecone entries.
630 - */
631 -public function ajax_mxchat_get_entry_content() {
632 - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce');
633 -
634 - if ( ! current_user_can('manage_options') ) {
635 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
636 - }
637 -
638 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
639 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
640 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
641 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
642 -
643 - if ( $data_source === 'pinecone' ) {
644 - // Pinecone: fetch vectors by source_url, reassemble chunks
645 - $content = $this->get_pinecone_entry_content( $source_url, $entry_id, $bot_id );
646 - } else {
647 - // WordPress DB
648 - $content = $this->get_wordpress_entry_content( $source_url, $entry_id );
649 - }
650 -
651 - if ( is_wp_error( $content ) ) {
652 - wp_send_json_error( array( 'message' => $content->get_error_message() ) );
653 - }
654 -
655 - wp_send_json_success( $content );
656 -}
657 -
658 -/**
659 - * Get content from WordPress DB — reassembles chunks by source_url.
660 - */
661 -private function get_wordpress_entry_content( $source_url, $entry_id ) {
662 - global $wpdb;
663 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
664 -
665 - // If we have a source_url, check for chunks
666 - if ( ! empty( $source_url ) && strpos( $source_url, 'mxchat://' ) !== 0 ) {
667 - $rows = $wpdb->get_results( $wpdb->prepare(
668 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE source_url = %s ORDER BY id ASC",
669 - $source_url
670 - ) );
671 -
672 - if ( $rows && count( $rows ) > 1 ) {
673 - // Multiple rows = chunked. Reassemble.
674 - $chunks = array();
675 - foreach ( $rows as $row ) {
676 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
677 - $index = isset( $parsed['metadata']['chunk_index'] ) ? intval( $parsed['metadata']['chunk_index'] ) : count( $chunks );
678 - $chunks[ $index ] = $parsed['text'];
679 - }
680 - ksort( $chunks );
681 - return array(
682 - 'content' => implode( "\n\n", $chunks ),
683 - 'source_url' => $source_url,
684 - 'is_chunked' => true,
685 - 'chunk_count' => count( $chunks ),
686 - 'content_type' => $rows[0]->content_type,
687 - );
688 - } elseif ( $rows && count( $rows ) === 1 ) {
689 - $parsed = MxChat_Chunker::parse_stored_chunk( $rows[0]->article_content );
690 - return array(
691 - 'content' => $parsed['text'],
692 - 'source_url' => $source_url,
693 - 'entry_id' => $rows[0]->id,
694 - 'is_chunked' => false,
695 - 'content_type' => $rows[0]->content_type,
696 - );
697 - }
698 - }
699 -
700 - // Fallback: fetch by ID
701 - if ( $entry_id > 0 ) {
702 - $row = $wpdb->get_row( $wpdb->prepare(
703 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE id = %d",
704 - $entry_id
705 - ) );
706 - if ( $row ) {
707 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
708 - return array(
709 - 'content' => $parsed['text'],
710 - 'source_url' => $row->source_url,
711 - 'entry_id' => $row->id,
712 - 'is_chunked' => false,
713 - 'content_type' => $row->content_type,
714 - );
715 - }
716 - }
717 -
718 - return new WP_Error( 'not_found', 'Entry not found.' );
719 -}
720 -
721 -/**
722 - * Get content from Pinecone — fetches vectors by source_url, reassembles chunks.
723 - */
724 -private function get_pinecone_entry_content( $source_url, $entry_id, $bot_id ) {
725 - if ( ! class_exists('MxChat_Pinecone_Manager') ) {
726 - return new WP_Error( 'pinecone_unavailable', 'Pinecone manager not available.' );
727 - }
728 -
729 - // Get Pinecone config
730 - if ( $bot_id === 'default' || ! class_exists('MxChat_Multi_Bot_Manager') ) {
731 - $pinecone_options = get_option('mxchat_pinecone_addon_options');
732 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
733 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
734 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
735 - } else {
736 - $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
737 - $api_key = $bot_config['api_key'] ?? '';
738 - $host = $bot_config['host'] ?? '';
739 - $namespace = $bot_config['namespace'] ?? '';
740 - }
741 -
742 - if ( empty($host) || empty($api_key) ) {
743 - return new WP_Error( 'pinecone_config', 'Pinecone not configured.' );
744 - }
745 -
746 - // List vectors with the source_url prefix
747 - $base_id = md5( $source_url );
748 - $vector_ids = array( $base_id );
749 -
750 - // Find chunk vectors
751 - $list_url = "https://{$host}/vectors/list";
752 - $list_body = array( 'prefix' => $base_id . '_chunk_', 'limit' => 100 );
753 - if ( ! empty($namespace) ) {
754 - $list_body['namespace'] = $namespace;
755 - }
756 -
757 - $list_resp = wp_remote_post( $list_url, array(
758 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
759 - 'body' => wp_json_encode( $list_body ),
760 - 'timeout' => 15,
761 - ) );
762 -
763 - if ( ! is_wp_error($list_resp) ) {
764 - $list_data = json_decode( wp_remote_retrieve_body($list_resp), true );
765 - if ( ! empty($list_data['vectors']) ) {
766 - foreach ( $list_data['vectors'] as $v ) {
767 - $vector_ids[] = $v['id'];
768 - }
769 - }
770 - }
771 -
772 - // Fetch vectors with metadata
773 - $fetch_url = "https://{$host}/vectors/fetch";
774 - $fetch_body = array( 'ids' => $vector_ids );
775 - if ( ! empty($namespace) ) {
776 - $fetch_body['namespace'] = $namespace;
777 - }
778 -
779 - $fetch_resp = wp_remote_post( $fetch_url, array(
780 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
781 - 'body' => wp_json_encode( $fetch_body ),
782 - 'timeout' => 15,
783 - ) );
784 -
785 - if ( is_wp_error($fetch_resp) ) {
786 - return new WP_Error( 'pinecone_fetch', 'Failed to fetch from Pinecone.' );
787 - }
788 -
789 - $fetch_data = json_decode( wp_remote_retrieve_body($fetch_resp), true );
790 - $vectors = $fetch_data['vectors'] ?? array();
791 -
792 - if ( empty($vectors) ) {
793 - return new WP_Error( 'not_found', 'Entry not found in Pinecone.' );
794 - }
795 -
796 - // Reassemble chunks
797 - $chunks = array();
798 - $content_type = 'content';
799 - foreach ( $vectors as $vid => $vector ) {
800 - $meta = $vector['metadata'] ?? array();
801 - $text = $meta['text'] ?? '';
802 - $index = $meta['chunk_index'] ?? 0;
803 - $content_type = $meta['type'] ?? 'content';
804 - $chunks[ intval($index) ] = $text;
805 - }
806 - ksort( $chunks );
807 -
808 - return array(
809 - 'content' => implode( "\n\n", $chunks ),
810 - 'source_url' => $source_url,
811 - 'is_chunked' => count($chunks) > 1,
812 - 'chunk_count' => count($chunks),
813 - 'content_type' => $content_type,
814 - );
815 -}
816 -
817 -/**
818 - * AJAX: Save edited content — re-chunks and re-embeds as needed.
819 - * Works for both WordPress DB and Pinecone entries.
820 - */
821 -public function ajax_mxchat_save_entry_content() {
822 - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce');
823 -
824 - if ( ! current_user_can('manage_options') ) {
825 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
826 - }
827 -
828 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
829 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
830 - $content = isset($_POST['content']) ? wp_kses_post( wp_unslash($_POST['content']) ) : '';
831 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
832 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
833 - $content_type = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : 'content';
834 -
835 - if ( empty($content) ) {
836 - wp_send_json_error( array( 'message' => 'Content cannot be empty.' ) );
837 - }
838 -
839 - // Get the embedding API key
840 - $options = get_option('mxchat_options', array());
841 - $api_key = '';
842 -
843 - if ( $bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager') ) {
844 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
845 - $api_key = $bot_options['api_key'] ?? '';
846 - }
847 - if ( empty($api_key) ) {
848 - $api_key = $options['api_key'] ?? '';
849 - }
850 -
851 - global $wpdb;
852 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
853 -
854 - // If source_url is empty but we have an entry_id, look it up
855 - if ( empty($source_url) && $entry_id > 0 && $data_source === 'wordpress' ) {
856 - $row = $wpdb->get_row( $wpdb->prepare( "SELECT source_url FROM {$table} WHERE id = %d", $entry_id ) );
857 - if ( $row && ! empty($row->source_url) ) {
858 - $source_url = $row->source_url;
859 - }
860 - }
861 -
862 - // For manual entries (no source_url or mxchat:// prefix), delete the old entry by ID first
863 - // so submit_content_to_db creates a replacement instead of a duplicate
864 - // Also treat legacy mxchat.ai source URLs as manual — old bug assigned the site URL to manual entries
865 - $is_legacy_manual = !empty($source_url) && strpos($source_url, 'mxchat.ai') !== false && strpos($source_url, 'mxchat://') !== 0;
866 - if ( $entry_id > 0 && (empty($source_url) || strpos($source_url, 'mxchat://') === 0 || $is_legacy_manual) ) {
867 - $wpdb->delete( $table, array( 'id' => $entry_id ), array( '%d' ) );
868 - // Clear legacy URL so submit_content_to_db generates a unique mxchat:// identifier
869 - // instead of reusing the shared URL (which would mass-delete other entries with the same URL)
870 - if ( $is_legacy_manual ) {
871 - $source_url = '';
872 - }
873 - }
874 -
875 - // Use the existing submit_content_to_db which handles chunking, Pinecone, and WP DB
876 - $vector_id = ! empty($source_url) ? md5($source_url) : md5('mxchat_manual_' . $entry_id);
877 -
878 - // submit_content_to_db already handles: delete old chunks → re-chunk → re-embed → store
879 - $result = MxChat_Utils::submit_content_to_db( $content, $source_url, $api_key, $vector_id, $bot_id, $content_type );
880 -
881 - if ( is_wp_error($result) ) {
882 - wp_send_json_error( array( 'message' => $result->get_error_message() ) );
883 - }
884 -
885 - wp_send_json_success( array( 'message' => 'Content saved and re-embedded successfully.' ) );
886 -}
887 -
888 624 public function mxchat_get_pdf_processing_status($pdf_url) {
889 625 $pdf_url = esc_url_raw($pdf_url);
890 626 $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
891 627
@@ -947,22 +683,9 @@
947 683 exit;
948 684 }
949 685
950 686 $submitted_url = esc_url_raw($_POST['sitemap_url']);
951 -
952 - // Convert Google Drive sharing URLs to direct download URLs
953 - if ( strpos($submitted_url, 'drive.google.com') !== false ) {
954 - $file_id = '';
955 - if ( preg_match('/[?&]id=([a-zA-Z0-9_-]+)/', $submitted_url, $m) ) {
956 - $file_id = $m[1];
957 - } elseif ( preg_match('#/file/d/([a-zA-Z0-9_-]+)#', $submitted_url, $m) ) {
958 - $file_id = $m[1];
959 - }
960 - if ( ! empty($file_id) ) {
961 - $submitted_url = 'https://drive.google.com/uc?export=download&id=' . $file_id;
962 - }
963 - }
964 -
687 +
965 688 // Get bot_id from form submission
966 689 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
967 690
968 691 // Get bot-specific options and validate API key
@@ -990,18 +713,10 @@
990 713 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
991 714 exit;
992 715 }
993 716
994 - // Fetch URL — use browser-like headers so servers with bot protection don't block us
995 - $response = wp_remote_get($submitted_url, array(
996 - 'timeout' => 30,
997 - 'sslverify' => false,
998 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
999 - 'headers' => array(
1000 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
1001 - 'Accept-Language' => 'en-US,en;q=0.9',
1002 - ),
1003 - ));
717 + // Fetch URL
718 + $response = wp_remote_get($submitted_url, array('timeout' => 30));
1004 719
1005 720 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1006 721 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
1007 722 set_transient('mxchat_admin_notice_error',
@@ -1908,10 +1623,8 @@
1908 1623 'preview_length' => $preview_length,
1909 1624 'source_url' => $entry->source_url,
1910 1625 'has_link' => !empty($entry->source_url) && strpos($entry->source_url, 'mxchat://') !== 0,
1911 1626 'chunk_metadata' => $chunk_metadata,
1912 - 'bot_id' => $entry->bot_id ?? 'default',
1913 - 'edit_nonce' => wp_create_nonce('mxchat_edit_entry_nonce'),
1914 1627 'delete_nonce' => wp_create_nonce('mxchat_delete_prompt_nonce')
1915 1628 );
1916 1629 }
1917 1630
@@ -2117,22 +1830,10 @@
2117 1830 <?php else : ?>
2118 1831 <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2119 1832 <?php endif; ?>
2120 1833 </td>
2121 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
2122 - <?php if ($data_source !== 'pinecone') : ?>
1834 + <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2123 1835 <button type="button"
2124 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2125 - data-source-url="<?php echo esc_attr($source_url); ?>"
2126 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2127 - data-data-source="<?php echo esc_attr($data_source); ?>"
2128 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2129 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2130 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2131 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2132 - </button>
2133 - <?php endif; ?>
2134 - <button type="button"
2135 1836 class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2136 1837 data-source-url="<?php echo esc_attr($source_url); ?>"
2137 1838 data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2138 1839 data-data-source="<?php echo esc_attr($data_source); ?>"
@@ -2585,22 +2286,10 @@
2585 2286 <?php else : ?>
2586 2287 <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2587 2288 <?php endif; ?>
2588 2289 </td>
2589 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
2590 - <?php if ($data_source !== 'pinecone') : ?>
2290 + <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2591 2291 <button type="button"
2592 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2593 - data-source-url="<?php echo esc_attr($source_url); ?>"
2594 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2595 - data-data-source="<?php echo esc_attr($data_source); ?>"
2596 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2597 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2598 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2599 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2600 - </button>
2601 - <?php endif; ?>
2602 - <button type="button"
2603 2292 class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2604 2293 data-source-url="<?php echo esc_attr($source_url); ?>"
2605 2294 data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2606 2295 data-data-source="<?php echo esc_attr($data_source); ?>"
@@ -2730,19 +2419,9 @@
2730 2419 <?php else : ?>
2731 2420 <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2732 2421 <?php endif; ?>
2733 2422 </td>
2734 - <td style="padding: 12px 16px; white-space: nowrap;">
2735 - <button type="button"
2736 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2737 - data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>"
2738 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2739 - data-data-source="<?php echo esc_attr($data_source); ?>"
2740 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2741 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2742 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2743 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2744 - </button>
2423 + <td style="padding: 12px 16px;">
2745 2424 <button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-wordpress" data-entry-id="<?php echo esc_attr($prompt->id); ?>" data-bot-id="<?php echo esc_attr($current_bot_id); ?>" data-nonce="<?php echo wp_create_nonce('mxchat_delete_wordpress_prompt_nonce'); ?>" style="color: var(--mxch-error);">
2746 2425 <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2747 2426 </button>
2748 2427 </td>
@@ -2835,12 +2514,11 @@
2835 2514 foreach ($primary_indexes as $path => $source) {
2836 2515 $url = trailingslashit($site_url) . $path;
2837 2516
2838 2517 $response = wp_remote_head($url, array(
2839 - 'timeout' => 10,
2518 + 'timeout' => 3, // Short timeout
2840 2519 'sslverify' => false,
2841 - 'redirection' => 1,
2842 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
2520 + 'redirection' => 1
2843 2521 ));
2844 2522
2845 2523 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2846 2524 // Found a sitemap index - parse it to get sub-sitemaps
@@ -2898,15 +2576,10 @@
2898 2576 private function parse_sitemap_index($url) {
2899 2577 $sub_sitemaps = array();
2900 2578
2901 2579 $response = wp_remote_get($url, array(
2902 - 'timeout' => 30,
2903 - 'sslverify' => false,
2904 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
2905 - 'headers' => array(
2906 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
2907 - 'Accept-Language' => 'en-US,en;q=0.9',
2908 - ),
2580 + 'timeout' => 5,
2581 + 'sslverify' => false
2909 2582 ));
2910 2583
2911 2584 if (is_wp_error($response)) {
2912 2585 return $sub_sitemaps;
@@ -2957,15 +2630,10 @@
2957 2630 * Get URL count from a sitemap
2958 2631 */
2959 2632 private function get_sitemap_url_count($url) {
2960 2633 $response = wp_remote_get($url, array(
2961 - 'timeout' => 30,
2962 - 'sslverify' => false,
2963 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
2964 - 'headers' => array(
2965 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
2966 - 'Accept-Language' => 'en-US,en;q=0.9',
2967 - ),
2634 + 'timeout' => 10,
2635 + 'sslverify' => false
2968 2636 ));
2969 2637
2970 2638 if (is_wp_error($response)) {
2971 2639 return 0;
@@ -2988,11 +2656,10 @@
2988 2656 $sitemaps = array();
2989 2657 $robots_url = trailingslashit($site_url) . 'robots.txt';
2990 2658
2991 2659 $response = wp_remote_get($robots_url, array(
2992 - 'timeout' => 15,
2993 - 'sslverify' => false,
2994 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
2660 + 'timeout' => 5,
2661 + 'sslverify' => false
2995 2662 ));
2996 2663
2997 2664 if (is_wp_error($response)) {
2998 2665 return $sitemaps;
@@ -3482,22 +3149,9 @@
3482 3149 }
3483 3150
3484 3151 // Get bot_id from request
3485 3152 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
3486 -
3487 - // ACF→PDF extraction is opt-in per import batch. Persist the last-used value so users
3488 - // don't re-check on every batch; the default is OFF for installs that haven't set it.
3489 - $extract_acf_pdfs = !empty($_POST['extract_acf_pdfs']) && $_POST['extract_acf_pdfs'] !== 'false';
3490 - $mxchat_options = get_option('mxchat_options', array());
3491 - if (!is_array($mxchat_options)) {
3492 - $mxchat_options = array();
3493 - }
3494 - $prior_default = !empty($mxchat_options['acf_pdf_extract_default']);
3495 - if ($prior_default !== $extract_acf_pdfs) {
3496 - $mxchat_options['acf_pdf_extract_default'] = $extract_acf_pdfs ? 1 : 0;
3497 - update_option('mxchat_options', $mxchat_options);
3498 - }
3499 -
3153 +
3500 3154 // Process only ONE post at a time to avoid request size issues
3501 3155 $post_id = reset($post_ids);
3502 3156 $post = get_post($post_id);
3503 3157
@@ -3603,57 +3257,23 @@
3603 3257 }
3604 3258
3605 3259 // ADD ACF FIELDS SUPPORT
3606 3260 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
3607 - $pdf_extracted_count = 0;
3608 3261 if (!empty($acf_fields)) {
3609 3262 $acf_content_parts = array();
3610 - $pdf_attachment_ids = array();
3611 -
3263 +
3612 3264 foreach ($acf_fields as $field_name => $field_value) {
3613 3265 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
3614 -
3266 +
3615 3267 if (!empty($formatted_value)) {
3616 3268 $field_label = ucwords(str_replace('_', ' ', $field_name));
3617 3269 $acf_content_parts[] = $field_label . ": " . $formatted_value;
3618 3270 }
3619 -
3620 - // Walk this field's value tree for any PDF attachment references and queue them for extraction.
3621 - // Only when the user opted into ACF→PDF extraction for this batch; otherwise the ACF text
3622 - // still lands in the KB but the heavier PDF parsing is skipped.
3623 - if ($extract_acf_pdfs) {
3624 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids);
3625 - }
3626 3271 }
3627 -
3272 +
3628 3273 if (!empty($acf_content_parts)) {
3629 3274 $content .= "\n\n" . implode("\n", $acf_content_parts);
3630 3275 }
3631 -
3632 - // Extract text from each unique PDF found in ACF fields and append as a labeled section
3633 - if ($extract_acf_pdfs && !empty($pdf_attachment_ids)) {
3634 - $pdf_attachment_ids = array_unique(array_filter(array_map('intval', $pdf_attachment_ids)));
3635 - $pdf_sections = array();
3636 - foreach ($pdf_attachment_ids as $att_id) {
3637 - $pdf_text = $this->mxchat_extract_pdf_text_by_attachment_id($att_id);
3638 - if (!empty($pdf_text)) {
3639 - $pdf_title = get_the_title($att_id);
3640 - $pdf_url = wp_get_attachment_url($att_id);
3641 - $header = 'PDF Attachment';
3642 - if (!empty($pdf_title)) {
3643 - $header .= ': ' . $pdf_title;
3644 - }
3645 - if (!empty($pdf_url)) {
3646 - $header .= ' (' . $pdf_url . ')';
3647 - }
3648 - $pdf_sections[] = $header . "\n" . $pdf_text;
3649 - $pdf_extracted_count++;
3650 - }
3651 - }
3652 - if (!empty($pdf_sections)) {
3653 - $content .= "\n\nAttached PDFs:\n" . implode("\n\n", $pdf_sections);
3654 - }
3655 - }
3656 3276 }
3657 3277
3658 3278 // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
3659 3279 $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
@@ -3783,9 +3403,8 @@
3783 3403 'title' => $post->post_title,
3784 3404 'operation_type' => $operation_type,
3785 3405 'vector_id' => $vector_id,
3786 3406 'acf_fields_found' => $acf_field_count,
3787 - 'pdf_extracted_count' => (int) $pdf_extracted_count,
3788 3407 'content_preview' => substr($content, 0, 100) . '...',
3789 3408 'bot_id' => $bot_id
3790 3409 ));
3791 3410 exit;
@@ -4200,20 +3819,9 @@
4200 3819
4201 3820 // Get bot-specific options
4202 3821 $bot_options = $this->get_bot_options($bot_id);
4203 3822 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
4204 -
4205 - // Opt-in: when the custom provider is selected for embeddings, index through
4206 - // the same custom endpoint the query path uses so stored vectors and query
4207 - // vectors share a model. Returns the vector array on success, or an error
4208 - // string on failure (this function's existing failure contract).
4209 - if (isset($options['custom_provider_for_embeddings']) && $options['custom_provider_for_embeddings'] === 'on') {
4210 - if (!class_exists('MxChat_Utils')) {
4211 - require_once dirname(__FILE__) . '/../includes/class-mxchat-utils.php';
4212 - }
4213 - return MxChat_Utils::generate_embedding_custom($text, $options);
4214 - }
4215 -
3823 +
4216 3824 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4217 3825 //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
4218 3826
4219 3827 // Determine provider and endpoint
@@ -5131,197 +4739,8 @@
5131 4739 return implode(', ', array_filter($text_parts));
5132 4740 }
5133 4741
5134 4742 /**
5135 - * Walk an ACF field value tree and collect attachment IDs for any value that
5136 - * resolves to a PDF in the WordPress media library. Handles the three shapes
5137 - * ACF returns for File/Image/URL fields (array with ID+url, integer attachment ID,
5138 - * plain URL string), and recurses through repeater/group/flexible content.
5139 - *
5140 - * @param mixed $value The ACF field value (any depth)
5141 - * @param array $out Accumulator (passed by reference) for attachment IDs
5142 - * @param int $depth Recursion guard
5143 - */
5144 -private function mxchat_collect_pdf_attachment_ids_from_acf_value($value, &$out, $depth = 0) {
5145 - if ($depth > 6) {
5146 - return; // prevent runaway recursion on circular/very-deep structures
5147 - }
5148 -
5149 - if (empty($value)) {
5150 - return;
5151 - }
5152 -
5153 - // Array shapes: ACF File/Image return value=array; repeaters/groups are arrays of arrays
5154 - if (is_array($value)) {
5155 - // Direct File/Image-style array (has 'url' and usually 'ID' + 'mime_type')
5156 - $looks_like_attachment = isset($value['url']) || isset($value['ID']) || isset($value['id']);
5157 - if ($looks_like_attachment) {
5158 - $att_id = 0;
5159 - if (!empty($value['ID']) && is_numeric($value['ID'])) {
5160 - $att_id = (int) $value['ID'];
5161 - } elseif (!empty($value['id']) && is_numeric($value['id'])) {
5162 - $att_id = (int) $value['id'];
5163 - } elseif (!empty($value['url']) && is_string($value['url'])) {
5164 - $att_id = (int) attachment_url_to_postid($value['url']);
5165 - }
5166 -
5167 - $is_pdf = false;
5168 - if (!empty($value['mime_type']) && $value['mime_type'] === 'application/pdf') {
5169 - $is_pdf = true;
5170 - } elseif (!empty($value['subtype']) && strtolower((string) $value['subtype']) === 'pdf') {
5171 - $is_pdf = true;
5172 - } elseif (!empty($value['url']) && is_string($value['url']) && $this->mxchat_url_looks_like_pdf($value['url'])) {
5173 - $is_pdf = true;
5174 - } elseif ($att_id && get_post_mime_type($att_id) === 'application/pdf') {
5175 - $is_pdf = true;
5176 - }
5177 -
5178 - if ($is_pdf && $att_id && get_post_mime_type($att_id) === 'application/pdf') {
5179 - $out[] = $att_id;
5180 - }
5181 - // An array node that represents one attachment doesn't contain other
5182 - // attachments inside it — done with this branch.
5183 - return;
5184 - }
5185 -
5186 - // Recurse: repeater rows, flexible-content layouts, groups, etc.
5187 - foreach ($value as $sub) {
5188 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($sub, $out, $depth + 1);
5189 - }
5190 - return;
5191 - }
5192 -
5193 - // Plain numeric attachment ID (ACF File field set to "Return: ID")
5194 - if (is_numeric($value)) {
5195 - $att_id = (int) $value;
5196 - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') {
5197 - $out[] = $att_id;
5198 - }
5199 - return;
5200 - }
5201 -
5202 - // Plain string — URL pointing at a PDF (ACF File field set to "Return: URL", or a custom URL/text field)
5203 - if (is_string($value)) {
5204 - $trimmed = trim($value);
5205 - if ($trimmed !== '' && $this->mxchat_url_looks_like_pdf($trimmed)) {
5206 - $att_id = (int) attachment_url_to_postid($trimmed);
5207 - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') {
5208 - $out[] = $att_id;
5209 - }
5210 - }
5211 - return;
5212 - }
5213 -}
5214 -
5215 -/**
5216 - * Heuristic: does this URL/string look like a PDF reference?
5217 - * Tolerates query strings and fragments (#page=2).
5218 - */
5219 -private function mxchat_url_looks_like_pdf($url) {
5220 - if (!is_string($url) || $url === '') {
5221 - return false;
5222 - }
5223 - // Strip query + fragment before checking extension
5224 - $path = preg_replace('/[?#].*$/', '', $url);
5225 - return (bool) preg_match('/\.pdf$/i', $path);
5226 -}
5227 -
5228 -/**
5229 - * Extract text from a PDF attachment by ID using the bundled Smalot parser.
5230 - * Reads the file directly from disk via get_attached_file (no HTTP fetch).
5231 - * Result is cached on the attachment as post_meta keyed by file mtime so we
5232 - * only parse the same PDF once unless the file changes on disk.
5233 - *
5234 - * @param int $attachment_id
5235 - * @return string Extracted plain text, or '' on failure.
5236 - */
5237 -private function mxchat_extract_pdf_text_by_attachment_id($attachment_id) {
5238 - $attachment_id = (int) $attachment_id;
5239 - if ($attachment_id <= 0) {
5240 - return '';
5241 - }
5242 - if (get_post_mime_type($attachment_id) !== 'application/pdf') {
5243 - return '';
5244 - }
5245 -
5246 - $pdf_path = get_attached_file($attachment_id);
5247 - if (empty($pdf_path) || !file_exists($pdf_path) || !is_readable($pdf_path)) {
5248 - return '';
5249 - }
5250 -
5251 - // Raw-file size cap. Parsing very large PDFs can OOM the request; skip with a log entry
5252 - // and let the rest of the ACF content land in the KB. Filterable for users who need it bigger.
5253 - $default_max_bytes = 25 * 1024 * 1024;
5254 - $max_bytes = (int) apply_filters('mxchat_acf_pdf_max_bytes', $default_max_bytes, $attachment_id, $pdf_path);
5255 - if ($max_bytes > 0) {
5256 - $file_size = @filesize($pdf_path);
5257 - if ($file_size !== false && $file_size > $max_bytes) {
5258 - error_log(sprintf(
5259 - '[mxchat] ACF PDF skipped (over size cap): attachment %d "%s" %d bytes > cap %d',
5260 - $attachment_id,
5261 - basename($pdf_path),
5262 - $file_size,
5263 - $max_bytes
5264 - ));
5265 - return '';
5266 - }
5267 - }
5268 -
5269 - $mtime = @filemtime($pdf_path);
5270 - $cache_meta_key = '_mxchat_acf_pdf_text_v1';
5271 - $cached = get_post_meta($attachment_id, $cache_meta_key, true);
5272 - if (is_array($cached) && isset($cached['mtime'], $cached['text']) && (int) $cached['mtime'] === (int) $mtime) {
5273 - return (string) $cached['text'];
5274 - }
5275 -
5276 - $text = '';
5277 - try {
5278 - if (function_exists('mxchat_load_pdf_parser')) {
5279 - mxchat_load_pdf_parser();
5280 - }
5281 - if (!class_exists('\\Smalot\\PdfParser\\Parser')) {
5282 - return '';
5283 - }
5284 - $parser = new \Smalot\PdfParser\Parser();
5285 - $pdf = $parser->parseFile($pdf_path);
5286 - $pages = $pdf->getPages();
5287 - $page_texts = array();
5288 - foreach ($pages as $page) {
5289 - $page_text = '';
5290 - try {
5291 - $page_text = $page->getText();
5292 - } catch (\Exception $e) {
5293 - $page_text = '';
5294 - }
5295 - if (!empty($page_text)) {
5296 - $page_texts[] = $page_text;
5297 - }
5298 - }
5299 - $text = trim(implode("\n\n", $page_texts));
5300 - } catch (\Exception $e) {
5301 - error_log('[mxchat] ACF PDF extraction failed for attachment ' . $attachment_id . ': ' . $e->getMessage());
5302 - return '';
5303 - } catch (\Throwable $e) {
5304 - error_log('[mxchat] ACF PDF extraction error for attachment ' . $attachment_id . ': ' . $e->getMessage());
5305 - return '';
5306 - }
5307 -
5308 - // Cap per-PDF text to avoid blowing up the embedding payload on enormous PDFs.
5309 - // The chunker downstream will still split this into multiple vectors.
5310 - $max_len = (int) apply_filters('mxchat_acf_pdf_text_max_length', 50000);
5311 - if ($max_len > 0 && strlen($text) > $max_len) {
5312 - $text = substr($text, 0, $max_len);
5313 - }
5314 -
5315 - update_post_meta($attachment_id, $cache_meta_key, array(
5316 - 'mtime' => (int) $mtime,
5317 - 'text' => $text,
5318 - ));
5319 -
5320 - return $text;
5321 -}
5322 -
5323 -/**
5324 4743 * Handle ACF save - fires after ACF fields are saved
5325 4744 * This ensures ACF field data is available when syncing to knowledge base
5326 4745 */
5327 4746 public function mxchat_handle_acf_save($post_id) {
@@ -5429,33 +4848,40 @@
5429 4848 // If the post was previously published but is now not published, remove from knowledge base
5430 4849 if ($previous_status === 'publish' && $post->post_status !== 'publish') {
5431 4850 // Use the stored URL from when it was published, or fall back to current permalink
5432 4851 $source_url = $previous_url ?: get_permalink($post_id);
4852 +
4853 + if ($source_url) {
4854 + // Check if Pinecone is enabled
4855 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4856 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5433 4857
5434 - if ($source_url) {
5435 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5436 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
4858 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4859 + // Delete from Pinecone
4860 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
4861 + } else {
4862 + // Delete from WordPress DB
4863 + global $wpdb;
4864 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4865 +
4866 + $result = $wpdb->delete(
4867 + $table_name,
4868 + array('source_url' => $source_url),
4869 + array('%s')
4870 + );
4871 + }
5437 4872 }
5438 -
4873 +
5439 4874 // Clean up the transients and exit early
5440 4875 delete_transient($previous_status_key);
5441 4876 delete_transient($previous_url_key);
5442 4877 return;
5443 4878 }
5444 -
5445 - // Slug/permalink rename while still published: delete the old vectors before upserting new ones.
5446 - // Without this, md5(old_url) vectors (base + chunks) would be orphaned under the stale URL.
5447 - if ($post->post_status === 'publish' && !empty($previous_url)) {
5448 - $current_url = get_permalink($post_id);
5449 - if ($current_url && $current_url !== $previous_url) {
5450 - MxChat_Utils::delete_chunks_for_url($previous_url, 'default');
5451 - }
5452 - }
5453 -
4879 +
5454 4880 // Store the current status for next time (if this is an update)
5455 4881 if ($update) {
5456 4882 set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
5457 -
4883 +
5458 4884 // If the post is currently published, also store its URL
5459 4885 if ($post->post_status === 'publish') {
5460 4886 $current_url = get_permalink($post_id);
5461 4887 set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
@@ -5564,9 +4990,8 @@
5564 4990 // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
5565 4991 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5566 4992 if (!empty($acf_fields)) {
5567 4993 $acf_content_parts = array();
5568 - $pdf_attachment_ids = array();
5569 4994
5570 4995 foreach ($acf_fields as $field_name => $field_value) {
5571 4996 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
5572 4997 if (!empty($formatted_value)) {
@@ -5573,44 +4998,13 @@
5573 4998 // Convert field name to readable label
5574 4999 $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
5575 5000 $acf_content_parts[] = $field_label . ": " . $formatted_value;
5576 5001 }
5577 -
5578 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids);
5579 5002 }
5580 5003
5581 5004 if (!empty($acf_content_parts)) {
5582 5005 $final_content .= "\n\n" . implode("\n", $acf_content_parts);
5583 5006 }
5584 -
5585 - // Gate the auto-sync PDF-extraction loop behind an opt-in option.
5586 - // Mirrors the per-batch checkbox the manual content selector has; the
5587 - // 25 MB size cap lives in the shared extractor so it applies in both
5588 - // paths regardless. Default OFF — re-parsing every ACF PDF on every
5589 - // editor save is expensive and most sites don't want it.
5590 - $autosync_extract_acf_pdfs = get_option('mxchat_auto_sync_acf_pdfs', '0') === '1';
5591 - if ($autosync_extract_acf_pdfs && !empty($pdf_attachment_ids)) {
5592 - $pdf_attachment_ids = array_unique(array_filter(array_map('intval', $pdf_attachment_ids)));
5593 - $pdf_sections = array();
5594 - foreach ($pdf_attachment_ids as $att_id) {
5595 - $pdf_text = $this->mxchat_extract_pdf_text_by_attachment_id($att_id);
5596 - if (!empty($pdf_text)) {
5597 - $pdf_title = get_the_title($att_id);
5598 - $pdf_url = wp_get_attachment_url($att_id);
5599 - $header = 'PDF Attachment';
5600 - if (!empty($pdf_title)) {
5601 - $header .= ': ' . $pdf_title;
5602 - }
5603 - if (!empty($pdf_url)) {
5604 - $header .= ' (' . $pdf_url . ')';
5605 - }
5606 - $pdf_sections[] = $header . "\n" . $pdf_text;
5607 - }
5608 - }
5609 - if (!empty($pdf_sections)) {
5610 - $final_content .= "\n\nAttached PDFs:\n" . implode("\n\n", $pdf_sections);
5611 - }
5612 - }
5613 5007 }
5614 5008
5615 5009 // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
5616 5010 $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
@@ -5717,14 +5111,12 @@
5717 5111 if (!$should_sync) {
5718 5112 return;
5719 5113 }
5720 5114
5721 - // Resolve the pre-trash URL. wp_trash_post renames the slug with "__trashed" before firing
5722 - // this hook, so get_permalink() here would return the trashed URL and md5() would miss the
5723 - // real vector IDs stored under the original URL.
5724 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
5115 + // Get the URL before post is deleted
5116 + $source_url = get_permalink($post_id);
5725 5117 if (!$source_url) {
5726 - //error_log('MXChat: Failed to resolve source URL for post ' . $post_id);
5118 + //error_log('MXChat: Failed to get permalink for post ' . $post_id);
5727 5119 return;
5728 5120 }
5729 5121
5730 5122 // Use chunk-aware deletion (handles both chunked and non-chunked content)
@@ -5732,36 +5124,56 @@
5732 5124
5733 5125 if (is_wp_error($delete_result)) {
5734 5126 //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
5735 5127 }
5128 +}
5736 5129
5737 - delete_transient('mxchat_prev_url_' . $post_id);
5738 - delete_transient('mxchat_prev_status_' . $post_id);
5739 -}
5740 5130
5741 -/**
5742 - * Resolve the source URL for a post being trashed/deleted.
5743 - *
5744 - * Why: wp_trash_post appends "__trashed" to the slug before the wp_trash_post action fires, so
5745 - * get_permalink() returns a URL whose md5() won't match the vector IDs stored in Pinecone or
5746 - * the source_url rows in the WP DB. Prefer the URL captured by mxchat_store_pre_update_status
5747 - * (runs on pre_post_update, before the rename); fall back to stripping the __trashed suffix.
5748 - */
5749 -private function mxchat_resolve_pre_trash_url($post_id) {
5750 - $previous_url = get_transient('mxchat_prev_url_' . $post_id);
5751 - if (!empty($previous_url)) {
5752 - return $previous_url;
5753 - }
5131 + /**
5132 + * Deletes data from Pinecone using a source URL
5133 + */
5134 + public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
5135 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
5136 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
5754 5137
5755 - $current = get_permalink($post_id);
5756 - if (!$current) {
5757 - return '';
5758 - }
5759 - return preg_replace('#__trashed(/?)$#', '$1', $current);
5760 -}
5138 + if (empty($host) || empty($api_key)) {
5139 + //error_log('MXChat: Pinecone deletion failed - missing configuration');
5140 + return false;
5141 + }
5761 5142
5143 + $api_endpoint = "https://{$host}/vectors/delete";
5144 + $vector_id = md5($source_url);
5762 5145
5146 + $request_body = array(
5147 + 'ids' => array($vector_id)
5148 + );
5763 5149
5150 + $response = wp_remote_post($api_endpoint, array(
5151 + 'headers' => array(
5152 + 'Api-Key' => $api_key,
5153 + 'accept' => 'application/json',
5154 + 'content-type' => 'application/json'
5155 + ),
5156 + 'body' => wp_json_encode($request_body),
5157 + 'timeout' => 30
5158 + ));
5159 +
5160 + if (is_wp_error($response)) {
5161 + //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
5162 + return false;
5163 + }
5164 +
5165 + $response_code = wp_remote_retrieve_response_code($response);
5166 + if ($response_code !== 200) {
5167 + //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
5168 + return false;
5169 + }
5170 +
5171 + return true;
5172 + }
5173 +
5174 +
5175 +
5764 5176 public function mxchat_handle_product_change($post_id, $post, $update) {
5765 5177 if ($post->post_type !== 'product') {
5766 5178 return;
5767 5179 }
@@ -5913,18 +5325,28 @@
5913 5325 if (get_post_type($post_id) !== 'product') {
5914 5326 return;
5915 5327 }
5916 5328
5917 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
5918 - if (!$source_url) {
5919 - return;
5920 - }
5329 + $source_url = get_permalink($post_id);
5921 5330
5922 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5923 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
5331 + // Check if Pinecone is enabled
5332 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5333 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5924 5334
5925 - delete_transient('mxchat_prev_url_' . $post_id);
5926 - delete_transient('mxchat_prev_status_' . $post_id);
5335 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5336 + // Delete from Pinecone
5337 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
5338 + } else {
5339 + // Delete from WordPress DB
5340 + global $wpdb;
5341 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5342 +
5343 + $wpdb->delete(
5344 + $table_name,
5345 + array('source_url' => $source_url),
5346 + array('%s')
5347 + );
5348 + }
5927 5349 }
5928 5350
5929 5351 /**
5930 5352 * Handle individual Pinecone content deletion
@@ -7335,27 +6757,13 @@
7335 6757
7336 6758 $result = false;
7337 6759 $error_message = '';
7338 6760
7339 - // Read item directly from DB to get queue_id and preserve special chars in item_data
7340 - // (POST round-trip through JS mangles characters like apostrophes in URLs)
7341 - $db_item = $wpdb->get_row($wpdb->prepare(
7342 - "SELECT queue_id, item_data FROM $table_name WHERE id = %d",
7343 - $item_id
7344 - ));
7345 - $item_queue_id = $db_item ? $db_item->queue_id : '';
7346 - if ($db_item && !empty($db_item->item_data)) {
7347 - $db_data = json_decode($db_item->item_data, true);
7348 - if (is_array($db_data)) {
7349 - $item_data = $db_data;
7350 - }
7351 - }
7352 -
7353 6761 switch ($item_type) {
7354 6762 case 'url':
7355 - $result = $this->mxchat_process_queue_url($item_data, $bot_id, $item_queue_id);
6763 + $result = $this->mxchat_process_queue_url($item_data, $bot_id);
7356 6764 break;
7357 -
6765 +
7358 6766 case 'pdf_page':
7359 6767 $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id);
7360 6768 break;
7361 6769
@@ -7363,37 +6771,11 @@
7363 6771 throw new Exception('Unknown item type: ' . $item_type);
7364 6772 }
7365 6773
7366 6774 if (is_wp_error($result)) {
7367 - $error_code = $result->get_error_code();
7368 - // Content errors (empty page, sanitization) are permanent — retrying won't help
7369 - $permanent_codes = array('empty_page', 'empty_after_sanitization', 'no_api_key', 'page_not_found');
7370 - if (in_array($error_code, $permanent_codes)) {
7371 - // Mark as permanently failed — set attempts = max_attempts so it won't be retried
7372 - $current_item = $wpdb->get_row($wpdb->prepare(
7373 - "SELECT max_attempts FROM $table_name WHERE id = %d", $item_id
7374 - ));
7375 - $wpdb->update(
7376 - $table_name,
7377 - array(
7378 - 'status' => 'failed',
7379 - 'error_message' => $result->get_error_message(),
7380 - 'attempts' => $current_item ? $current_item->max_attempts : 3
7381 - ),
7382 - array('id' => $item_id),
7383 - array('%s', '%s', '%d'),
7384 - array('%d')
7385 - );
7386 - wp_send_json_error(array(
7387 - 'message' => $result->get_error_message(),
7388 - 'permanent_failure' => true,
7389 - 'item_id' => $item_id
7390 - ));
7391 - return;
7392 - }
7393 6775 throw new Exception($result->get_error_message());
7394 6776 }
7395 -
6777 +
7396 6778 if ($result === false) {
7397 6779 throw new Exception('Processing returned false - item may be empty or invalid');
7398 6780 }
7399 6781
@@ -7469,9 +6851,9 @@
7469 6851
7470 6852 /**
7471 6853 * Process a URL from the queue
7472 6854 */
7473 -private function mxchat_process_queue_url($item_data, $bot_id = 'default', $queue_id = '') {
6855 +private function mxchat_process_queue_url($item_data, $bot_id = 'default') {
7474 6856 $url = isset($item_data['url']) ? $item_data['url'] : '';
7475 6857
7476 6858 if (empty($url)) {
7477 6859 return new WP_Error('invalid_url', 'URL is empty');
@@ -7517,11 +6899,10 @@
7517 6899 // If WooCommerce extraction failed, fall through to HTML extraction
7518 6900 }
7519 6901
7520 6902 // Fetch URL content (fallback for non-products or when WooCommerce extraction fails)
7521 - $is_likely_pdf = (strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION)) === 'pdf');
7522 6903 $response = wp_remote_get($url, array(
7523 - 'timeout' => $is_likely_pdf ? 120 : 30,
6904 + 'timeout' => 30,
7524 6905 'redirection' => 5,
7525 6906 'user-agent' => 'MxChat/1.0'
7526 6907 ));
7527 6908
@@ -7530,16 +6911,11 @@
7530 6911 }
7531 6912
7532 6913 $response_code = wp_remote_retrieve_response_code($response);
7533 6914 if ($response_code !== 200) {
7534 - return new WP_Error('http_error', 'HTTP ' . $response_code . ' error for: ' . $url);
6915 + return new WP_Error('http_error', 'HTTP ' . $response_code . ' error');
7535 6916 }
7536 6917
7537 - // Check if URL is a PDF — expand into per-page queue items using the standard PDF pipeline
7538 - if ($this->mxchat_is_pdf_url($url, $response)) {
7539 - return $this->mxchat_expand_pdf_to_queue($url, $response, $bot_id, $queue_id);
7540 - }
7541 -
7542 6918 $html = wp_remote_retrieve_body($response);
7543 6919
7544 6920 if (empty($html)) {
7545 6921 return new WP_Error('empty_response', 'Empty response body');
@@ -7567,180 +6943,8 @@
7567 6943 return $result;
7568 6944 }
7569 6945
7570 6946 /**
7571 - * Expand a PDF URL into per-page queue items using the standard PDF pipeline.
7572 - * Called when a sitemap URL turns out to be a PDF — downloads, parses page count,
7573 - * and adds pdf_page items to the same queue so they process with full progress tracking.
7574 - */
7575 -private function mxchat_expand_pdf_to_queue($pdf_url, $response, $bot_id = 'default', $queue_id = '') {
7576 - set_time_limit(120); // PDFs need extra time for download + parsing
7577 -
7578 - $upload_dir = wp_upload_dir();
7579 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
7580 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
7581 -
7582 - $response_body = wp_remote_retrieve_body($response);
7583 - if (empty($response_body)) {
7584 - return new WP_Error('empty_pdf', 'Empty PDF response for: ' . $pdf_url);
7585 - }
7586 -
7587 - if (!wp_mkdir_p(dirname($pdf_path))) {
7588 - return new WP_Error('dir_error', 'Failed to create upload directory');
7589 - }
7590 -
7591 - file_put_contents($pdf_path, $response_body);
7592 -
7593 - if (!file_exists($pdf_path)) {
7594 - return new WP_Error('save_error', 'Failed to save PDF file');
7595 - }
7596 -
7597 - try {
7598 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
7599 -
7600 - if ($total_pages === false || $total_pages < 1) {
7601 - wp_delete_file($pdf_path);
7602 - return new WP_Error('no_pages', 'PDF has no pages: ' . $pdf_url);
7603 - }
7604 -
7605 - // Build per-page items identical to mxchat_handle_pdf_for_knowledge_base
7606 - $pages = array();
7607 - for ($i = 1; $i <= $total_pages; $i++) {
7608 - $pages[] = array(
7609 - 'pdf_path' => $pdf_path,
7610 - 'pdf_url' => $pdf_url,
7611 - 'page_number' => $i,
7612 - 'total_pages' => $total_pages
7613 - );
7614 - }
7615 -
7616 - // Add pdf_page items to the SAME queue so the JS picks them up automatically
7617 - if (!empty($queue_id)) {
7618 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
7619 - } else {
7620 - // Fallback: create a new PDF queue (shouldn't happen in sitemap flow)
7621 - $new_queue_id = 'pdf_' . md5($pdf_url . time());
7622 - $queued_count = $this->mxchat_add_to_queue($new_queue_id, 'pdf_page', $pages, $bot_id);
7623 - $this->mxchat_set_queue_meta($new_queue_id, 'source_url', $pdf_url);
7624 - $this->mxchat_set_queue_meta($new_queue_id, 'queue_type', 'pdf');
7625 - $this->mxchat_set_queue_meta($new_queue_id, 'total_items', $total_pages);
7626 - $this->mxchat_set_queue_meta($new_queue_id, 'bot_id', $bot_id);
7627 - $this->mxchat_set_queue_meta($new_queue_id, 'pdf_path', $pdf_path);
7628 - $this->mxchat_set_queue_meta($new_queue_id, 'created_at', current_time('mysql'));
7629 - }
7630 -
7631 - if ($queued_count === 0) {
7632 - wp_delete_file($pdf_path);
7633 - return new WP_Error('queue_error', 'Failed to add PDF pages to queue');
7634 - }
7635 -
7636 - // Return true so the original URL item is marked complete
7637 - // The new pdf_page items will be processed in subsequent batches
7638 - return true;
7639 -
7640 - } catch (Exception $e) {
7641 - if (file_exists($pdf_path)) {
7642 - wp_delete_file($pdf_path);
7643 - }
7644 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7645 - }
7646 -}
7647 -
7648 -/**
7649 - * Legacy: Process a PDF URL inline during sitemap queue processing.
7650 - * @deprecated Use mxchat_expand_pdf_to_queue instead — kept for reference only.
7651 - */
7652 -private function mxchat_process_pdf_url_inline($pdf_url, $response, $api_key, $bot_id = 'default') {
7653 - set_time_limit(120); // PDFs need more time — downloading + parsing all pages
7654 -
7655 - $upload_dir = wp_upload_dir();
7656 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
7657 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
7658 -
7659 - $response_body = wp_remote_retrieve_body($response);
7660 - if (empty($response_body)) {
7661 - return new WP_Error('empty_pdf', 'Empty PDF response');
7662 - }
7663 -
7664 - if (!wp_mkdir_p(dirname($pdf_path))) {
7665 - return new WP_Error('dir_error', 'Failed to create upload directory');
7666 - }
7667 -
7668 - file_put_contents($pdf_path, $response_body);
7669 -
7670 - if (!file_exists($pdf_path)) {
7671 - return new WP_Error('save_error', 'Failed to save PDF file');
7672 - }
7673 -
7674 - try {
7675 - mxchat_load_pdf_parser();
7676 - $parser = new \Smalot\PdfParser\Parser();
7677 - $pdf = $parser->parseFile($pdf_path);
7678 - $pages = $pdf->getPages();
7679 - $total_pages = count($pages);
7680 -
7681 - if ($total_pages < 1) {
7682 - wp_delete_file($pdf_path);
7683 - return new WP_Error('no_pages', 'PDF has no pages');
7684 - }
7685 -
7686 - $processed = 0;
7687 - $skipped_pages = array();
7688 -
7689 - for ($i = 0; $i < $total_pages; $i++) {
7690 - $page_num = $i + 1;
7691 - $text = $pages[$i]->getText();
7692 - if (empty($text)) {
7693 - $skipped_pages[] = 'Page ' . $page_num . ': No text could be extracted — page may contain only images, links, or non-standard encoding';
7694 - continue;
7695 - }
7696 -
7697 - $sanitized = $this->mxchat_sanitize_content_for_api($text);
7698 - if (empty($sanitized)) {
7699 - $skipped_pages[] = 'Page ' . $page_num . ': Text was extracted but contained only special characters, control codes, or unsupported content';
7700 - continue;
7701 - }
7702 -
7703 - $metadata = array(
7704 - 'document_type' => 'pdf',
7705 - 'total_pages' => $total_pages,
7706 - 'current_page' => $page_num,
7707 - 'source_url' => $pdf_url,
7708 - );
7709 -
7710 - $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
7711 - $page_url = esc_url($pdf_url . '#page=' . $page_num);
7712 -
7713 - MxChat_Utils::submit_content_to_db(
7714 - $content_with_metadata,
7715 - $page_url,
7716 - $api_key,
7717 - null,
7718 - $bot_id,
7719 - 'pdf'
7720 - );
7721 -
7722 - $processed++;
7723 - }
7724 -
7725 - // Clean up the temp PDF file
7726 - wp_delete_file($pdf_path);
7727 -
7728 - if (!empty($skipped_pages)) {
7729 - error_log('MxChat PDF: Skipped ' . count($skipped_pages) . ' of ' . $total_pages . ' pages: ' . implode('; ', $skipped_pages));
7730 - }
7731 -
7732 - return $processed > 0 ? true : false;
7733 -
7734 - } catch (Exception $e) {
7735 - if (file_exists($pdf_path)) {
7736 - wp_delete_file($pdf_path);
7737 - }
7738 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7739 - }
7740 -}
7741 -
7742 -/**
7743 6947 * Extract WooCommerce product content including pricing
7744 6948 *
7745 6949 * @param string $url The product URL
7746 6950 * @return string|false Product content with pricing, or false if not found
@@ -7894,17 +7098,18 @@
7894 7098 return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF');
7895 7099 }
7896 7100
7897 7101 $text = $pages[$page_number - 1]->getText();
7898 -
7102 +
7899 7103 if (empty($text)) {
7900 - return new WP_Error('empty_page', 'Page ' . $page_number . ': No text could be extracted — page may contain only images, links, or non-standard encoding');
7104 + // Not an error - just an empty page
7105 + return false;
7901 7106 }
7902 -
7107 +
7903 7108 $sanitized = $this->mxchat_sanitize_content_for_api($text);
7904 -
7109 +
7905 7110 if (empty($sanitized)) {
7906 - return new WP_Error('empty_after_sanitization', 'Page ' . $page_number . ': Text was extracted but contained only special characters, control codes, or unsupported content that was removed during cleanup');
7111 + return false;
7907 7112 }
7908 7113
7909 7114 // Create metadata
7910 7115 $metadata = array(
@@ -8008,16 +7213,17 @@
8008 7213
8009 7214 // Calculate percentage
8010 7215 $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0;
8011 7216
8012 - // Get failed items details (include all failed items, not just those that exhausted retries)
7217 + // Get failed items details
8013 7218 $failed_items = array();
8014 7219 if ($failed > 0) {
8015 7220 $failed_items = $wpdb->get_results($wpdb->prepare(
8016 - "SELECT item_type, item_data, error_message, attempts
8017 - FROM $table_name
8018 - WHERE queue_id = %s
7221 + "SELECT item_type, item_data, error_message, attempts
7222 + FROM $table_name
7223 + WHERE queue_id = %s
8019 7224 AND status = 'failed'
7225 + AND attempts >= max_attempts
8020 7226 ORDER BY id DESC
8021 7227 LIMIT 50",
8022 7228 $queue_id
8023 7229 ));