PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.5
MxChat – AI Chatbot & Content Generation for WordPress v3.1.5
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 +119 -436 3.2.83.1.5 View file →
@@ -59,8 +59,11 @@
59 59 add_action('wp_ajax_mxchat_paginate_entries', array($this, 'ajax_mxchat_paginate_entries'));
60 60 add_action('wp_ajax_mxchat_get_entry_content', array($this, 'ajax_mxchat_get_entry_content'));
61 61 add_action('wp_ajax_mxchat_save_entry_content', array($this, 'ajax_mxchat_save_entry_content'));
62 62
63 + // Hook for content deletion
64 + add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1);
65 +
63 66 // WordPress post management hooks
64 67 add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
65 68 add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3);
66 69 add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
@@ -160,13 +163,9 @@
160 163 public function mxchat_is_pdf_url($url, $response) {
161 164 $content_type = wp_remote_retrieve_header($response, 'content-type');
162 165 $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
163 166
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;
167 + return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf';
169 168 }
170 169
171 170
172 171 public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') {
@@ -860,17 +859,10 @@
860 859 }
861 860
862 861 // For manual entries (no source_url or mxchat:// prefix), delete the old entry by ID first
863 862 // 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) ) {
863 + if ( $entry_id > 0 && (empty($source_url) || strpos($source_url, 'mxchat://') === 0) ) {
867 864 $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 865 }
874 866
875 867 // Use the existing submit_content_to_db which handles chunking, Pinecone, and WP DB
876 868 $vector_id = ! empty($source_url) ? md5($source_url) : md5('mxchat_manual_' . $entry_id);
@@ -947,22 +939,9 @@
947 939 exit;
948 940 }
949 941
950 942 $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 -
943 +
965 944 // Get bot_id from form submission
966 945 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
967 946
968 947 // Get bot-specific options and validate API key
@@ -990,18 +969,10 @@
990 969 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
991 970 exit;
992 971 }
993 972
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 - ));
973 + // Fetch URL
974 + $response = wp_remote_get($submitted_url, array('timeout' => 30));
1004 975
1005 976 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1006 977 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
1007 978 set_transient('mxchat_admin_notice_error',
@@ -2835,12 +2806,11 @@
2835 2806 foreach ($primary_indexes as $path => $source) {
2836 2807 $url = trailingslashit($site_url) . $path;
2837 2808
2838 2809 $response = wp_remote_head($url, array(
2839 - 'timeout' => 10,
2810 + 'timeout' => 3, // Short timeout
2840 2811 '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',
2812 + 'redirection' => 1
2843 2813 ));
2844 2814
2845 2815 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2846 2816 // Found a sitemap index - parse it to get sub-sitemaps
@@ -2898,15 +2868,10 @@
2898 2868 private function parse_sitemap_index($url) {
2899 2869 $sub_sitemaps = array();
2900 2870
2901 2871 $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 - ),
2872 + 'timeout' => 5,
2873 + 'sslverify' => false
2909 2874 ));
2910 2875
2911 2876 if (is_wp_error($response)) {
2912 2877 return $sub_sitemaps;
@@ -2957,15 +2922,10 @@
2957 2922 * Get URL count from a sitemap
2958 2923 */
2959 2924 private function get_sitemap_url_count($url) {
2960 2925 $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 - ),
2926 + 'timeout' => 10,
2927 + 'sslverify' => false
2968 2928 ));
2969 2929
2970 2930 if (is_wp_error($response)) {
2971 2931 return 0;
@@ -2988,11 +2948,10 @@
2988 2948 $sitemaps = array();
2989 2949 $robots_url = trailingslashit($site_url) . 'robots.txt';
2990 2950
2991 2951 $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',
2952 + 'timeout' => 5,
2953 + 'sslverify' => false
2995 2954 ));
2996 2955
2997 2956 if (is_wp_error($response)) {
2998 2957 return $sitemaps;
@@ -3482,22 +3441,9 @@
3482 3441 }
3483 3442
3484 3443 // Get bot_id from request
3485 3444 $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 -
3445 +
3500 3446 // Process only ONE post at a time to avoid request size issues
3501 3447 $post_id = reset($post_ids);
3502 3448 $post = get_post($post_id);
3503 3449
@@ -3603,57 +3549,23 @@
3603 3549 }
3604 3550
3605 3551 // ADD ACF FIELDS SUPPORT
3606 3552 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
3607 - $pdf_extracted_count = 0;
3608 3553 if (!empty($acf_fields)) {
3609 3554 $acf_content_parts = array();
3610 - $pdf_attachment_ids = array();
3611 -
3555 +
3612 3556 foreach ($acf_fields as $field_name => $field_value) {
3613 3557 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
3614 -
3558 +
3615 3559 if (!empty($formatted_value)) {
3616 3560 $field_label = ucwords(str_replace('_', ' ', $field_name));
3617 3561 $acf_content_parts[] = $field_label . ": " . $formatted_value;
3618 3562 }
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 3563 }
3627 -
3564 +
3628 3565 if (!empty($acf_content_parts)) {
3629 3566 $content .= "\n\n" . implode("\n", $acf_content_parts);
3630 3567 }
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 3568 }
3657 3569
3658 3570 // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
3659 3571 $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
@@ -3783,9 +3695,8 @@
3783 3695 'title' => $post->post_title,
3784 3696 'operation_type' => $operation_type,
3785 3697 'vector_id' => $vector_id,
3786 3698 'acf_fields_found' => $acf_field_count,
3787 - 'pdf_extracted_count' => (int) $pdf_extracted_count,
3788 3699 'content_preview' => substr($content, 0, 100) . '...',
3789 3700 'bot_id' => $bot_id
3790 3701 ));
3791 3702 exit;
@@ -4200,20 +4111,9 @@
4200 4111
4201 4112 // Get bot-specific options
4202 4113 $bot_options = $this->get_bot_options($bot_id);
4203 4114 $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 -
4115 +
4216 4116 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
4217 4117 //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model);
4218 4118
4219 4119 // Determine provider and endpoint
@@ -5131,197 +5031,8 @@
5131 5031 return implode(', ', array_filter($text_parts));
5132 5032 }
5133 5033
5134 5034 /**
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 5035 * Handle ACF save - fires after ACF fields are saved
5325 5036 * This ensures ACF field data is available when syncing to knowledge base
5326 5037 */
5327 5038 public function mxchat_handle_acf_save($post_id) {
@@ -5429,33 +5140,40 @@
5429 5140 // If the post was previously published but is now not published, remove from knowledge base
5430 5141 if ($previous_status === 'publish' && $post->post_status !== 'publish') {
5431 5142 // Use the stored URL from when it was published, or fall back to current permalink
5432 5143 $source_url = $previous_url ?: get_permalink($post_id);
5144 +
5145 + if ($source_url) {
5146 + // Check if Pinecone is enabled
5147 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5148 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5433 5149
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');
5150 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5151 + // Delete from Pinecone
5152 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
5153 + } else {
5154 + // Delete from WordPress DB
5155 + global $wpdb;
5156 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5157 +
5158 + $result = $wpdb->delete(
5159 + $table_name,
5160 + array('source_url' => $source_url),
5161 + array('%s')
5162 + );
5163 + }
5437 5164 }
5438 -
5165 +
5439 5166 // Clean up the transients and exit early
5440 5167 delete_transient($previous_status_key);
5441 5168 delete_transient($previous_url_key);
5442 5169 return;
5443 5170 }
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 -
5171 +
5454 5172 // Store the current status for next time (if this is an update)
5455 5173 if ($update) {
5456 5174 set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
5457 -
5175 +
5458 5176 // If the post is currently published, also store its URL
5459 5177 if ($post->post_status === 'publish') {
5460 5178 $current_url = get_permalink($post_id);
5461 5179 set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
@@ -5564,9 +5282,8 @@
5564 5282 // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
5565 5283 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5566 5284 if (!empty($acf_fields)) {
5567 5285 $acf_content_parts = array();
5568 - $pdf_attachment_ids = array();
5569 5286
5570 5287 foreach ($acf_fields as $field_name => $field_value) {
5571 5288 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
5572 5289 if (!empty($formatted_value)) {
@@ -5573,44 +5290,13 @@
5573 5290 // Convert field name to readable label
5574 5291 $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
5575 5292 $acf_content_parts[] = $field_label . ": " . $formatted_value;
5576 5293 }
5577 -
5578 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids);
5579 5294 }
5580 5295
5581 5296 if (!empty($acf_content_parts)) {
5582 5297 $final_content .= "\n\n" . implode("\n", $acf_content_parts);
5583 5298 }
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 5299 }
5614 5300
5615 5301 // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
5616 5302 $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
@@ -5717,14 +5403,12 @@
5717 5403 if (!$should_sync) {
5718 5404 return;
5719 5405 }
5720 5406
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);
5407 + // Get the URL before post is deleted
5408 + $source_url = get_permalink($post_id);
5725 5409 if (!$source_url) {
5726 - //error_log('MXChat: Failed to resolve source URL for post ' . $post_id);
5410 + //error_log('MXChat: Failed to get permalink for post ' . $post_id);
5727 5411 return;
5728 5412 }
5729 5413
5730 5414 // Use chunk-aware deletion (handles both chunked and non-chunked content)
@@ -5732,36 +5416,56 @@
5732 5416
5733 5417 if (is_wp_error($delete_result)) {
5734 5418 //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
5735 5419 }
5420 +}
5736 5421
5737 - delete_transient('mxchat_prev_url_' . $post_id);
5738 - delete_transient('mxchat_prev_status_' . $post_id);
5739 -}
5740 5422
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 - }
5423 + /**
5424 + * Deletes data from Pinecone using a source URL
5425 + */
5426 + public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
5427 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
5428 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
5754 5429
5755 - $current = get_permalink($post_id);
5756 - if (!$current) {
5757 - return '';
5758 - }
5759 - return preg_replace('#__trashed(/?)$#', '$1', $current);
5760 -}
5430 + if (empty($host) || empty($api_key)) {
5431 + //error_log('MXChat: Pinecone deletion failed - missing configuration');
5432 + return false;
5433 + }
5761 5434
5435 + $api_endpoint = "https://{$host}/vectors/delete";
5436 + $vector_id = md5($source_url);
5762 5437
5438 + $request_body = array(
5439 + 'ids' => array($vector_id)
5440 + );
5763 5441
5442 + $response = wp_remote_post($api_endpoint, array(
5443 + 'headers' => array(
5444 + 'Api-Key' => $api_key,
5445 + 'accept' => 'application/json',
5446 + 'content-type' => 'application/json'
5447 + ),
5448 + 'body' => wp_json_encode($request_body),
5449 + 'timeout' => 30
5450 + ));
5451 +
5452 + if (is_wp_error($response)) {
5453 + //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
5454 + return false;
5455 + }
5456 +
5457 + $response_code = wp_remote_retrieve_response_code($response);
5458 + if ($response_code !== 200) {
5459 + //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
5460 + return false;
5461 + }
5462 +
5463 + return true;
5464 + }
5465 +
5466 +
5467 +
5764 5468 public function mxchat_handle_product_change($post_id, $post, $update) {
5765 5469 if ($post->post_type !== 'product') {
5766 5470 return;
5767 5471 }
@@ -5913,18 +5617,28 @@
5913 5617 if (get_post_type($post_id) !== 'product') {
5914 5618 return;
5915 5619 }
5916 5620
5917 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
5918 - if (!$source_url) {
5919 - return;
5920 - }
5621 + $source_url = get_permalink($post_id);
5921 5622
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');
5623 + // Check if Pinecone is enabled
5624 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5625 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5924 5626
5925 - delete_transient('mxchat_prev_url_' . $post_id);
5926 - delete_transient('mxchat_prev_status_' . $post_id);
5627 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5628 + // Delete from Pinecone
5629 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
5630 + } else {
5631 + // Delete from WordPress DB
5632 + global $wpdb;
5633 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5634 +
5635 + $wpdb->delete(
5636 + $table_name,
5637 + array('source_url' => $source_url),
5638 + array('%s')
5639 + );
5640 + }
5927 5641 }
5928 5642
5929 5643 /**
5930 5644 * Handle individual Pinecone content deletion
@@ -7363,37 +7077,11 @@
7363 7077 throw new Exception('Unknown item type: ' . $item_type);
7364 7078 }
7365 7079
7366 7080 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 7081 throw new Exception($result->get_error_message());
7394 7082 }
7395 -
7083 +
7396 7084 if ($result === false) {
7397 7085 throw new Exception('Processing returned false - item may be empty or invalid');
7398 7086 }
7399 7087
@@ -7683,24 +7371,21 @@
7683 7371 return new WP_Error('no_pages', 'PDF has no pages');
7684 7372 }
7685 7373
7686 7374 $processed = 0;
7687 - $skipped_pages = array();
7688 7375
7689 7376 for ($i = 0; $i < $total_pages; $i++) {
7690 - $page_num = $i + 1;
7691 7377 $text = $pages[$i]->getText();
7692 7378 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 7379 continue;
7695 7380 }
7696 7381
7697 7382 $sanitized = $this->mxchat_sanitize_content_for_api($text);
7698 7383 if (empty($sanitized)) {
7699 - $skipped_pages[] = 'Page ' . $page_num . ': Text was extracted but contained only special characters, control codes, or unsupported content';
7700 7384 continue;
7701 7385 }
7702 7386
7387 + $page_num = $i + 1;
7703 7388 $metadata = array(
7704 7389 'document_type' => 'pdf',
7705 7390 'total_pages' => $total_pages,
7706 7391 'current_page' => $page_num,
@@ -7724,12 +7409,8 @@
7724 7409
7725 7410 // Clean up the temp PDF file
7726 7411 wp_delete_file($pdf_path);
7727 7412
7728 - if (!empty($skipped_pages)) {
7729 - error_log('MxChat PDF: Skipped ' . count($skipped_pages) . ' of ' . $total_pages . ' pages: ' . implode('; ', $skipped_pages));
7730 - }
7731 -
7732 7413 return $processed > 0 ? true : false;
7733 7414
7734 7415 } catch (Exception $e) {
7735 7416 if (file_exists($pdf_path)) {
@@ -7894,17 +7575,18 @@
7894 7575 return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF');
7895 7576 }
7896 7577
7897 7578 $text = $pages[$page_number - 1]->getText();
7898 -
7579 +
7899 7580 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');
7581 + // Not an error - just an empty page
7582 + return false;
7901 7583 }
7902 -
7584 +
7903 7585 $sanitized = $this->mxchat_sanitize_content_for_api($text);
7904 -
7586 +
7905 7587 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');
7588 + return false;
7907 7589 }
7908 7590
7909 7591 // Create metadata
7910 7592 $metadata = array(
@@ -8008,16 +7690,17 @@
8008 7690
8009 7691 // Calculate percentage
8010 7692 $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0;
8011 7693
8012 - // Get failed items details (include all failed items, not just those that exhausted retries)
7694 + // Get failed items details
8013 7695 $failed_items = array();
8014 7696 if ($failed > 0) {
8015 7697 $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
7698 + "SELECT item_type, item_data, error_message, attempts
7699 + FROM $table_name
7700 + WHERE queue_id = %s
8019 7701 AND status = 'failed'
7702 + AND attempts >= max_attempts
8020 7703 ORDER BY id DESC
8021 7704 LIMIT 50",
8022 7705 $queue_id
8023 7706 ));