PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.2
MxChat – AI Chatbot & Content Generation for WordPress v3.2.2
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 +4 -273 3.2.63.2.2 View file →
@@ -3482,22 +3482,9 @@
3482 3482 }
3483 3483
3484 3484 // Get bot_id from request
3485 3485 $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 -
3486 +
3500 3487 // Process only ONE post at a time to avoid request size issues
3501 3488 $post_id = reset($post_ids);
3502 3489 $post = get_post($post_id);
3503 3490
@@ -3603,57 +3590,23 @@
3603 3590 }
3604 3591
3605 3592 // ADD ACF FIELDS SUPPORT
3606 3593 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
3607 - $pdf_extracted_count = 0;
3608 3594 if (!empty($acf_fields)) {
3609 3595 $acf_content_parts = array();
3610 - $pdf_attachment_ids = array();
3611 -
3596 +
3612 3597 foreach ($acf_fields as $field_name => $field_value) {
3613 3598 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
3614 -
3599 +
3615 3600 if (!empty($formatted_value)) {
3616 3601 $field_label = ucwords(str_replace('_', ' ', $field_name));
3617 3602 $acf_content_parts[] = $field_label . ": " . $formatted_value;
3618 3603 }
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 3604 }
3627 -
3605 +
3628 3606 if (!empty($acf_content_parts)) {
3629 3607 $content .= "\n\n" . implode("\n", $acf_content_parts);
3630 3608 }
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 3609 }
3657 3610
3658 3611 // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)
3659 3612 $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id);
@@ -3783,9 +3736,8 @@
3783 3736 'title' => $post->post_title,
3784 3737 'operation_type' => $operation_type,
3785 3738 'vector_id' => $vector_id,
3786 3739 'acf_fields_found' => $acf_field_count,
3787 - 'pdf_extracted_count' => (int) $pdf_extracted_count,
3788 3740 'content_preview' => substr($content, 0, 100) . '...',
3789 3741 'bot_id' => $bot_id
3790 3742 ));
3791 3743 exit;
@@ -5120,197 +5072,8 @@
5120 5072 return implode(', ', array_filter($text_parts));
5121 5073 }
5122 5074
5123 5075 /**
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 5076 * Handle ACF save - fires after ACF fields are saved
5314 5077 * This ensures ACF field data is available when syncing to knowledge base
5315 5078 */
5316 5079 public function mxchat_handle_acf_save($post_id) {
@@ -5553,9 +5316,8 @@
5553 5316 // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior)
5554 5317 $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id);
5555 5318 if (!empty($acf_fields)) {
5556 5319 $acf_content_parts = array();
5557 - $pdf_attachment_ids = array();
5558 5320
5559 5321 foreach ($acf_fields as $field_name => $field_value) {
5560 5322 $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id);
5561 5323 if (!empty($formatted_value)) {
@@ -5562,43 +5324,12 @@
5562 5324 // Convert field name to readable label
5563 5325 $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name));
5564 5326 $acf_content_parts[] = $field_label . ": " . $formatted_value;
5565 5327 }
5566 -
5567 - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids);
5568 5328 }
5569 5329
5570 5330 if (!empty($acf_content_parts)) {
5571 5331 $final_content .= "\n\n" . implode("\n", $acf_content_parts);
5572 - }
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 5332 }
5602 5333 }
5603 5334
5604 5335 // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields)