PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.7
MxChat – AI Chatbot & Content Generation for WordPress v3.1.7
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 +102 -353 3.2.63.1.7 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'));
@@ -990,18 +993,10 @@
990 993 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
991 994 exit;
992 995 }
993 996
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 - ));
997 + // Fetch URL
998 + $response = wp_remote_get($submitted_url, array('timeout' => 30));
1004 999
1005 1000 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1006 1001 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
1007 1002 set_transient('mxchat_admin_notice_error',
@@ -2835,12 +2830,11 @@
2835 2830 foreach ($primary_indexes as $path => $source) {
2836 2831 $url = trailingslashit($site_url) . $path;
2837 2832
2838 2833 $response = wp_remote_head($url, array(
2839 - 'timeout' => 10,
2834 + 'timeout' => 3, // Short timeout
2840 2835 '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',
2836 + 'redirection' => 1
2843 2837 ));
2844 2838
2845 2839 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2846 2840 // Found a sitemap index - parse it to get sub-sitemaps
@@ -2898,15 +2892,10 @@
2898 2892 private function parse_sitemap_index($url) {
2899 2893 $sub_sitemaps = array();
2900 2894
2901 2895 $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 - ),
2896 + 'timeout' => 5,
2897 + 'sslverify' => false
2909 2898 ));
2910 2899
2911 2900 if (is_wp_error($response)) {
2912 2901 return $sub_sitemaps;
@@ -2957,15 +2946,10 @@
2957 2946 * Get URL count from a sitemap
2958 2947 */
2959 2948 private function get_sitemap_url_count($url) {
2960 2949 $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 - ),
2950 + 'timeout' => 10,
2951 + 'sslverify' => false
2968 2952 ));
2969 2953
2970 2954 if (is_wp_error($response)) {
2971 2955 return 0;
@@ -2988,11 +2972,10 @@
2988 2972 $sitemaps = array();
2989 2973 $robots_url = trailingslashit($site_url) . 'robots.txt';
2990 2974
2991 2975 $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',
2976 + 'timeout' => 5,
2977 + 'sslverify' => false
2995 2978 ));
2996 2979
2997 2980 if (is_wp_error($response)) {
2998 2981 return $sitemaps;
@@ -3482,22 +3465,9 @@
3482 3465 }
3483 3466
3484 3467 // Get bot_id from request
3485 3468 $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 -
3469 +
3500 3470 // Process only ONE post at a time to avoid request size issues
3501 3471 $post_id = reset($post_ids);
3502 3472 $post = get_post($post_id);
3503 3473
@@ -3603,57 +3573,23 @@
3603 3573 }
3604 3574
3605 3575 // ADD ACF FIELDS SUPPORT
3606 3576 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
3607 - $pdf_extracted_count = 0;
3608 3577 if (!empty($acf_fields)) {
3609 3578 $acf_content_parts = array();
3610 - $pdf_attachment_ids = array();
3611 -
3579 +
3612 3580 foreach ($acf_fields as $field_name => $field_value) {
3613 3581 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
3614 -
3582 +
3615 3583 if (!empty($formatted_value)) {
3616 3584 $field_label = ucwords(str_replace('_', ' ', $field_name));
3617 3585 $acf_content_parts[] = $field_label . ": " . $formatted_value;
3618 3586 }
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 3587 }
3627 -
3588 +
3628 3589 if (!empty($acf_content_parts)) {
3629 3590 $content .= "\n\n" . implode("\n", $acf_content_parts);
3630 3591 }
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 3592 }
3657 3593
3658 3594 // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
3659 3595 $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
@@ -3783,9 +3719,8 @@
3783 3719 'title' => $post->post_title,
3784 3720 'operation_type' => $operation_type,
3785 3721 'vector_id' => $vector_id,
3786 3722 'acf_fields_found' => $acf_field_count,
3787 - 'pdf_extracted_count' => (int) $pdf_extracted_count,
3788 3723 'content_preview' => substr($content, 0, 100) . '...',
3789 3724 'bot_id' => $bot_id
3790 3725 ));
3791 3726 exit;
@@ -5120,197 +5055,8 @@
5120 5055 return implode(', ', array_filter($text_parts));
5121 5056 }
5122 5057
5123 5058 /**
5124 - * Walk an ACF field value tree and collect attachment IDs for any value that
5125 - * resolves to a PDF in the WordPress media library. Handles the three shapes
5126 - * ACF returns for File/Image/URL fields (array with ID+url, integer attachment ID,
5127 - * plain URL string), and recurses through repeater/group/flexible content.
5128 - *
5129 - * @param mixed $value The ACF field value (any depth)
5130 - * @param array $out Accumulator (passed by reference) for attachment IDs
5131 - * @param int $depth Recursion guard
5132 - */
5133 -private function mxchat_collect_pdf_attachment_ids_from_acf_value($value, &$out, $depth = 0) {
5134 - if ($depth > 6) {
5135 - return; // prevent runaway recursion on circular/very-deep structures
5136 - }
5137 -
5138 - if (empty($value)) {
5139 - return;
5140 - }
5141 -
5142 - // Array shapes: ACF File/Image return value=array; repeaters/groups are arrays of arrays
5143 - if (is_array($value)) {
5144 - // Direct File/Image-style array (has 'url' and usually 'ID' + 'mime_type')
5145 - $looks_like_attachment = isset($value['url']) || isset($value['ID']) || isset($value['id']);
5146 - if ($looks_like_attachment) {
5147 - $att_id = 0;
5148 - if (!empty($value['ID']) && is_numeric($value['ID'])) {
5149 - $att_id = (int) $value['ID'];
5150 - } elseif (!empty($value['id']) && is_numeric($value['id'])) {
5151 - $att_id = (int) $value['id'];
5152 - } elseif (!empty($value['url']) && is_string($value['url'])) {
5153 - $att_id = (int) attachment_url_to_postid($value['url']);
5154 - }
5155 -
5156 - $is_pdf = false;
5157 - if (!empty($value['mime_type']) && $value['mime_type'] === 'application/pdf') {
5158 - $is_pdf = true;
5159 - } elseif (!empty($value['subtype']) && strtolower((string) $value['subtype']) === 'pdf') {
5160 - $is_pdf = true;
5161 - } elseif (!empty($value['url']) && is_string($value['url']) && $this->mxchat_url_looks_like_pdf($value['url'])) {
5162 - $is_pdf = true;
5163 - } elseif ($att_id && get_post_mime_type($att_id) === 'application/pdf') {
5164 - $is_pdf = true;
5165 - }
5166 -
5167 - if ($is_pdf && $att_id && get_post_mime_type($att_id) === 'application/pdf') {
5168 - $out[] = $att_id;
5169 - }
5170 - // An array node that represents one attachment doesn't contain other
5171 - // attachments inside it — done with this branch.
5172 - return;
5173 - }
5174 -
5175 - // Recurse: repeater rows, flexible-content layouts, groups, etc.
5176 - foreach ($value as $sub) {
5177 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($sub, $out, $depth + 1);
5178 - }
5179 - return;
5180 - }
5181 -
5182 - // Plain numeric attachment ID (ACF File field set to "Return: ID")
5183 - if (is_numeric($value)) {
5184 - $att_id = (int) $value;
5185 - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') {
5186 - $out[] = $att_id;
5187 - }
5188 - return;
5189 - }
5190 -
5191 - // Plain string — URL pointing at a PDF (ACF File field set to "Return: URL", or a custom URL/text field)
5192 - if (is_string($value)) {
5193 - $trimmed = trim($value);
5194 - if ($trimmed !== '' && $this->mxchat_url_looks_like_pdf($trimmed)) {
5195 - $att_id = (int) attachment_url_to_postid($trimmed);
5196 - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') {
5197 - $out[] = $att_id;
5198 - }
5199 - }
5200 - return;
5201 - }
5202 -}
5203 -
5204 -/**
5205 - * Heuristic: does this URL/string look like a PDF reference?
5206 - * Tolerates query strings and fragments (#page=2).
5207 - */
5208 -private function mxchat_url_looks_like_pdf($url) {
5209 - if (!is_string($url) || $url === '') {
5210 - return false;
5211 - }
5212 - // Strip query + fragment before checking extension
5213 - $path = preg_replace('/[?#].*$/', '', $url);
5214 - return (bool) preg_match('/\.pdf$/i', $path);
5215 -}
5216 -
5217 -/**
5218 - * Extract text from a PDF attachment by ID using the bundled Smalot parser.
5219 - * Reads the file directly from disk via get_attached_file (no HTTP fetch).
5220 - * Result is cached on the attachment as post_meta keyed by file mtime so we
5221 - * only parse the same PDF once unless the file changes on disk.
5222 - *
5223 - * @param int $attachment_id
5224 - * @return string Extracted plain text, or '' on failure.
5225 - */
5226 -private function mxchat_extract_pdf_text_by_attachment_id($attachment_id) {
5227 - $attachment_id = (int) $attachment_id;
5228 - if ($attachment_id <= 0) {
5229 - return '';
5230 - }
5231 - if (get_post_mime_type($attachment_id) !== 'application/pdf') {
5232 - return '';
5233 - }
5234 -
5235 - $pdf_path = get_attached_file($attachment_id);
5236 - if (empty($pdf_path) || !file_exists($pdf_path) || !is_readable($pdf_path)) {
5237 - return '';
5238 - }
5239 -
5240 - // Raw-file size cap. Parsing very large PDFs can OOM the request; skip with a log entry
5241 - // and let the rest of the ACF content land in the KB. Filterable for users who need it bigger.
5242 - $default_max_bytes = 25 * 1024 * 1024;
5243 - $max_bytes = (int) apply_filters('mxchat_acf_pdf_max_bytes', $default_max_bytes, $attachment_id, $pdf_path);
5244 - if ($max_bytes > 0) {
5245 - $file_size = @filesize($pdf_path);
5246 - if ($file_size !== false && $file_size > $max_bytes) {
5247 - error_log(sprintf(
5248 - '[mxchat] ACF PDF skipped (over size cap): attachment %d "%s" %d bytes > cap %d',
5249 - $attachment_id,
5250 - basename($pdf_path),
5251 - $file_size,
5252 - $max_bytes
5253 - ));
5254 - return '';
5255 - }
5256 - }
5257 -
5258 - $mtime = @filemtime($pdf_path);
5259 - $cache_meta_key = '_mxchat_acf_pdf_text_v1';
5260 - $cached = get_post_meta($attachment_id, $cache_meta_key, true);
5261 - if (is_array($cached) && isset($cached['mtime'], $cached['text']) && (int) $cached['mtime'] === (int) $mtime) {
5262 - return (string) $cached['text'];
5263 - }
5264 -
5265 - $text = '';
5266 - try {
5267 - if (function_exists('mxchat_load_pdf_parser')) {
5268 - mxchat_load_pdf_parser();
5269 - }
5270 - if (!class_exists('\\Smalot\\PdfParser\\Parser')) {
5271 - return '';
5272 - }
5273 - $parser = new \Smalot\PdfParser\Parser();
5274 - $pdf = $parser->parseFile($pdf_path);
5275 - $pages = $pdf->getPages();
5276 - $page_texts = array();
5277 - foreach ($pages as $page) {
5278 - $page_text = '';
5279 - try {
5280 - $page_text = $page->getText();
5281 - } catch (\Exception $e) {
5282 - $page_text = '';
5283 - }
5284 - if (!empty($page_text)) {
5285 - $page_texts[] = $page_text;
5286 - }
5287 - }
5288 - $text = trim(implode("\n\n", $page_texts));
5289 - } catch (\Exception $e) {
5290 - error_log('[mxchat] ACF PDF extraction failed for attachment ' . $attachment_id . ': ' . $e->getMessage());
5291 - return '';
5292 - } catch (\Throwable $e) {
5293 - error_log('[mxchat] ACF PDF extraction error for attachment ' . $attachment_id . ': ' . $e->getMessage());
5294 - return '';
5295 - }
5296 -
5297 - // Cap per-PDF text to avoid blowing up the embedding payload on enormous PDFs.
5298 - // The chunker downstream will still split this into multiple vectors.
5299 - $max_len = (int) apply_filters('mxchat_acf_pdf_text_max_length', 50000);
5300 - if ($max_len > 0 && strlen($text) > $max_len) {
5301 - $text = substr($text, 0, $max_len);
5302 - }
5303 -
5304 - update_post_meta($attachment_id, $cache_meta_key, array(
5305 - 'mtime' => (int) $mtime,
5306 - 'text' => $text,
5307 - ));
5308 -
5309 - return $text;
5310 -}
5311 -
5312 -/**
5313 5059 * Handle ACF save - fires after ACF fields are saved
5314 5060 * This ensures ACF field data is available when syncing to knowledge base
5315 5061 */
5316 5062 public function mxchat_handle_acf_save($post_id) {
@@ -5418,33 +5164,40 @@
5418 5164 // If the post was previously published but is now not published, remove from knowledge base
5419 5165 if ($previous_status === 'publish' && $post->post_status !== 'publish') {
5420 5166 // Use the stored URL from when it was published, or fall back to current permalink
5421 5167 $source_url = $previous_url ?: get_permalink($post_id);
5168 +
5169 + if ($source_url) {
5170 + // Check if Pinecone is enabled
5171 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5172 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5422 5173
5423 - if ($source_url) {
5424 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5425 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
5174 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5175 + // Delete from Pinecone
5176 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
5177 + } else {
5178 + // Delete from WordPress DB
5179 + global $wpdb;
5180 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5181 +
5182 + $result = $wpdb->delete(
5183 + $table_name,
5184 + array('source_url' => $source_url),
5185 + array('%s')
5186 + );
5187 + }
5426 5188 }
5427 -
5189 +
5428 5190 // Clean up the transients and exit early
5429 5191 delete_transient($previous_status_key);
5430 5192 delete_transient($previous_url_key);
5431 5193 return;
5432 5194 }
5433 -
5434 - // Slug/permalink rename while still published: delete the old vectors before upserting new ones.
5435 - // Without this, md5(old_url) vectors (base + chunks) would be orphaned under the stale URL.
5436 - if ($post->post_status === 'publish' && !empty($previous_url)) {
5437 - $current_url = get_permalink($post_id);
5438 - if ($current_url && $current_url !== $previous_url) {
5439 - MxChat_Utils::delete_chunks_for_url($previous_url, 'default');
5440 - }
5441 - }
5442 -
5195 +
5443 5196 // Store the current status for next time (if this is an update)
5444 5197 if ($update) {
5445 5198 set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
5446 -
5199 +
5447 5200 // If the post is currently published, also store its URL
5448 5201 if ($post->post_status === 'publish') {
5449 5202 $current_url = get_permalink($post_id);
5450 5203 set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
@@ -5553,9 +5306,8 @@
5553 5306 // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
5554 5307 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5555 5308 if (!empty($acf_fields)) {
5556 5309 $acf_content_parts = array();
5557 - $pdf_attachment_ids = array();
5558 5310
5559 5311 foreach ($acf_fields as $field_name => $field_value) {
5560 5312 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
5561 5313 if (!empty($formatted_value)) {
@@ -5562,44 +5314,13 @@
5562 5314 // Convert field name to readable label
5563 5315 $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
5564 5316 $acf_content_parts[] = $field_label . ": " . $formatted_value;
5565 5317 }
5566 -
5567 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids);
5568 5318 }
5569 5319
5570 5320 if (!empty($acf_content_parts)) {
5571 5321 $final_content .= "\n\n" . implode("\n", $acf_content_parts);
5572 5322 }
5573 -
5574 - // Gate the auto-sync PDF-extraction loop behind an opt-in option.
5575 - // Mirrors the per-batch checkbox the manual content selector has; the
5576 - // 25 MB size cap lives in the shared extractor so it applies in both
5577 - // paths regardless. Default OFF — re-parsing every ACF PDF on every
5578 - // editor save is expensive and most sites don't want it.
5579 - $autosync_extract_acf_pdfs = get_option('mxchat_auto_sync_acf_pdfs', '0') === '1';
5580 - if ($autosync_extract_acf_pdfs && !empty($pdf_attachment_ids)) {
5581 - $pdf_attachment_ids = array_unique(array_filter(array_map('intval', $pdf_attachment_ids)));
5582 - $pdf_sections = array();
5583 - foreach ($pdf_attachment_ids as $att_id) {
5584 - $pdf_text = $this->mxchat_extract_pdf_text_by_attachment_id($att_id);
5585 - if (!empty($pdf_text)) {
5586 - $pdf_title = get_the_title($att_id);
5587 - $pdf_url = wp_get_attachment_url($att_id);
5588 - $header = 'PDF Attachment';
5589 - if (!empty($pdf_title)) {
5590 - $header .= ': ' . $pdf_title;
5591 - }
5592 - if (!empty($pdf_url)) {
5593 - $header .= ' (' . $pdf_url . ')';
5594 - }
5595 - $pdf_sections[] = $header . "\n" . $pdf_text;
5596 - }
5597 - }
5598 - if (!empty($pdf_sections)) {
5599 - $final_content .= "\n\nAttached PDFs:\n" . implode("\n\n", $pdf_sections);
5600 - }
5601 - }
5602 5323 }
5603 5324
5604 5325 // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
5605 5326 $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
@@ -5706,14 +5427,12 @@
5706 5427 if (!$should_sync) {
5707 5428 return;
5708 5429 }
5709 5430
5710 - // Resolve the pre-trash URL. wp_trash_post renames the slug with "__trashed" before firing
5711 - // this hook, so get_permalink() here would return the trashed URL and md5() would miss the
5712 - // real vector IDs stored under the original URL.
5713 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
5431 + // Get the URL before post is deleted
5432 + $source_url = get_permalink($post_id);
5714 5433 if (!$source_url) {
5715 - //error_log('MXChat: Failed to resolve source URL for post ' . $post_id);
5434 + //error_log('MXChat: Failed to get permalink for post ' . $post_id);
5716 5435 return;
5717 5436 }
5718 5437
5719 5438 // Use chunk-aware deletion (handles both chunked and non-chunked content)
@@ -5721,36 +5440,56 @@
5721 5440
5722 5441 if (is_wp_error($delete_result)) {
5723 5442 //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
5724 5443 }
5444 +}
5725 5445
5726 - delete_transient('mxchat_prev_url_' . $post_id);
5727 - delete_transient('mxchat_prev_status_' . $post_id);
5728 -}
5729 5446
5730 -/**
5731 - * Resolve the source URL for a post being trashed/deleted.
5732 - *
5733 - * Why: wp_trash_post appends "__trashed" to the slug before the wp_trash_post action fires, so
5734 - * get_permalink() returns a URL whose md5() won't match the vector IDs stored in Pinecone or
5735 - * the source_url rows in the WP DB. Prefer the URL captured by mxchat_store_pre_update_status
5736 - * (runs on pre_post_update, before the rename); fall back to stripping the __trashed suffix.
5737 - */
5738 -private function mxchat_resolve_pre_trash_url($post_id) {
5739 - $previous_url = get_transient('mxchat_prev_url_' . $post_id);
5740 - if (!empty($previous_url)) {
5741 - return $previous_url;
5742 - }
5447 + /**
5448 + * Deletes data from Pinecone using a source URL
5449 + */
5450 + public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
5451 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
5452 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
5743 5453
5744 - $current = get_permalink($post_id);
5745 - if (!$current) {
5746 - return '';
5747 - }
5748 - return preg_replace('#__trashed(/?)$#', '$1', $current);
5749 -}
5454 + if (empty($host) || empty($api_key)) {
5455 + //error_log('MXChat: Pinecone deletion failed - missing configuration');
5456 + return false;
5457 + }
5750 5458
5459 + $api_endpoint = "https://{$host}/vectors/delete";
5460 + $vector_id = md5($source_url);
5751 5461
5462 + $request_body = array(
5463 + 'ids' => array($vector_id)
5464 + );
5752 5465
5466 + $response = wp_remote_post($api_endpoint, array(
5467 + 'headers' => array(
5468 + 'Api-Key' => $api_key,
5469 + 'accept' => 'application/json',
5470 + 'content-type' => 'application/json'
5471 + ),
5472 + 'body' => wp_json_encode($request_body),
5473 + 'timeout' => 30
5474 + ));
5475 +
5476 + if (is_wp_error($response)) {
5477 + //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
5478 + return false;
5479 + }
5480 +
5481 + $response_code = wp_remote_retrieve_response_code($response);
5482 + if ($response_code !== 200) {
5483 + //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
5484 + return false;
5485 + }
5486 +
5487 + return true;
5488 + }
5489 +
5490 +
5491 +
5753 5492 public function mxchat_handle_product_change($post_id, $post, $update) {
5754 5493 if ($post->post_type !== 'product') {
5755 5494 return;
5756 5495 }
@@ -5902,18 +5641,28 @@
5902 5641 if (get_post_type($post_id) !== 'product') {
5903 5642 return;
5904 5643 }
5905 5644
5906 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
5907 - if (!$source_url) {
5908 - return;
5909 - }
5645 + $source_url = get_permalink($post_id);
5910 5646
5911 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5912 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
5647 + // Check if Pinecone is enabled
5648 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5649 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5913 5650
5914 - delete_transient('mxchat_prev_url_' . $post_id);
5915 - delete_transient('mxchat_prev_status_' . $post_id);
5651 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5652 + // Delete from Pinecone
5653 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
5654 + } else {
5655 + // Delete from WordPress DB
5656 + global $wpdb;
5657 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5658 +
5659 + $wpdb->delete(
5660 + $table_name,
5661 + array('source_url' => $source_url),
5662 + array('%s')
5663 + );
5664 + }
5916 5665 }
5917 5666
5918 5667 /**
5919 5668 * Handle individual Pinecone content deletion