PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.4
MxChat – AI Chatbot & Content Generation for WordPress v3.1.4
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 +123 -252 3.2.53.1.4 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;
@@ -5181,33 +5140,40 @@
5181 5140 // If the post was previously published but is now not published, remove from knowledge base
5182 5141 if ($previous_status === 'publish' && $post->post_status !== 'publish') {
5183 5142 // Use the stored URL from when it was published, or fall back to current permalink
5184 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';
5185 5149
5186 - if ($source_url) {
5187 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5188 - 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 + }
5189 5164 }
5190 -
5165 +
5191 5166 // Clean up the transients and exit early
5192 5167 delete_transient($previous_status_key);
5193 5168 delete_transient($previous_url_key);
5194 5169 return;
5195 5170 }
5196 -
5197 - // Slug/permalink rename while still published: delete the old vectors before upserting new ones.
5198 - // Without this, md5(old_url) vectors (base + chunks) would be orphaned under the stale URL.
5199 - if ($post->post_status === 'publish' && !empty($previous_url)) {
5200 - $current_url = get_permalink($post_id);
5201 - if ($current_url && $current_url !== $previous_url) {
5202 - MxChat_Utils::delete_chunks_for_url($previous_url, 'default');
5203 - }
5204 - }
5205 -
5171 +
5206 5172 // Store the current status for next time (if this is an update)
5207 5173 if ($update) {
5208 5174 set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
5209 -
5175 +
5210 5176 // If the post is currently published, also store its URL
5211 5177 if ($post->post_status === 'publish') {
5212 5178 $current_url = get_permalink($post_id);
5213 5179 set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
@@ -5437,14 +5403,12 @@
5437 5403 if (!$should_sync) {
5438 5404 return;
5439 5405 }
5440 5406
5441 - // Resolve the pre-trash URL. wp_trash_post renames the slug with "__trashed" before firing
5442 - // this hook, so get_permalink() here would return the trashed URL and md5() would miss the
5443 - // real vector IDs stored under the original URL.
5444 - $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);
5445 5409 if (!$source_url) {
5446 - //error_log('MXChat: Failed to resolve source URL for post ' . $post_id);
5410 + //error_log('MXChat: Failed to get permalink for post ' . $post_id);
5447 5411 return;
5448 5412 }
5449 5413
5450 5414 // Use chunk-aware deletion (handles both chunked and non-chunked content)
@@ -5452,36 +5416,56 @@
5452 5416
5453 5417 if (is_wp_error($delete_result)) {
5454 5418 //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
5455 5419 }
5420 +}
5456 5421
5457 - delete_transient('mxchat_prev_url_' . $post_id);
5458 - delete_transient('mxchat_prev_status_' . $post_id);
5459 -}
5460 5422
5461 -/**
5462 - * Resolve the source URL for a post being trashed/deleted.
5463 - *
5464 - * Why: wp_trash_post appends "__trashed" to the slug before the wp_trash_post action fires, so
5465 - * get_permalink() returns a URL whose md5() won't match the vector IDs stored in Pinecone or
5466 - * the source_url rows in the WP DB. Prefer the URL captured by mxchat_store_pre_update_status
5467 - * (runs on pre_post_update, before the rename); fall back to stripping the __trashed suffix.
5468 - */
5469 -private function mxchat_resolve_pre_trash_url($post_id) {
5470 - $previous_url = get_transient('mxchat_prev_url_' . $post_id);
5471 - if (!empty($previous_url)) {
5472 - return $previous_url;
5473 - }
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'] ?? '';
5474 5429
5475 - $current = get_permalink($post_id);
5476 - if (!$current) {
5477 - return '';
5478 - }
5479 - return preg_replace('#__trashed(/?)$#', '$1', $current);
5480 -}
5430 + if (empty($host) || empty($api_key)) {
5431 + //error_log('MXChat: Pinecone deletion failed - missing configuration');
5432 + return false;
5433 + }
5481 5434
5435 + $api_endpoint = "https://{$host}/vectors/delete";
5436 + $vector_id = md5($source_url);
5482 5437
5438 + $request_body = array(
5439 + 'ids' => array($vector_id)
5440 + );
5483 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 +
5484 5468 public function mxchat_handle_product_change($post_id, $post, $update) {
5485 5469 if ($post->post_type !== 'product') {
5486 5470 return;
5487 5471 }
@@ -5633,18 +5617,28 @@
5633 5617 if (get_post_type($post_id) !== 'product') {
5634 5618 return;
5635 5619 }
5636 5620
5637 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
5638 - if (!$source_url) {
5639 - return;
5640 - }
5621 + $source_url = get_permalink($post_id);
5641 5622
5642 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5643 - 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';
5644 5626
5645 - delete_transient('mxchat_prev_url_' . $post_id);
5646 - 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 + }
5647 5641 }
5648 5642
5649 5643 /**
5650 5644 * Handle individual Pinecone content deletion
@@ -7055,27 +7049,13 @@
7055 7049
7056 7050 $result = false;
7057 7051 $error_message = '';
7058 7052
7059 - // Read item directly from DB to get queue_id and preserve special chars in item_data
7060 - // (POST round-trip through JS mangles characters like apostrophes in URLs)
7061 - $db_item = $wpdb->get_row($wpdb->prepare(
7062 - "SELECT queue_id, item_data FROM $table_name WHERE id = %d",
7063 - $item_id
7064 - ));
7065 - $item_queue_id = $db_item ? $db_item->queue_id : '';
7066 - if ($db_item && !empty($db_item->item_data)) {
7067 - $db_data = json_decode($db_item->item_data, true);
7068 - if (is_array($db_data)) {
7069 - $item_data = $db_data;
7070 - }
7071 - }
7072 -
7073 7053 switch ($item_type) {
7074 7054 case 'url':
7075 - $result = $this->mxchat_process_queue_url($item_data, $bot_id, $item_queue_id);
7055 + $result = $this->mxchat_process_queue_url($item_data, $bot_id);
7076 7056 break;
7077 -
7057 +
7078 7058 case 'pdf_page':
7079 7059 $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id);
7080 7060 break;
7081 7061
@@ -7083,37 +7063,11 @@
7083 7063 throw new Exception('Unknown item type: ' . $item_type);
7084 7064 }
7085 7065
7086 7066 if (is_wp_error($result)) {
7087 - $error_code = $result->get_error_code();
7088 - // Content errors (empty page, sanitization) are permanent — retrying won't help
7089 - $permanent_codes = array('empty_page', 'empty_after_sanitization', 'no_api_key', 'page_not_found');
7090 - if (in_array($error_code, $permanent_codes)) {
7091 - // Mark as permanently failed — set attempts = max_attempts so it won't be retried
7092 - $current_item = $wpdb->get_row($wpdb->prepare(
7093 - "SELECT max_attempts FROM $table_name WHERE id = %d", $item_id
7094 - ));
7095 - $wpdb->update(
7096 - $table_name,
7097 - array(
7098 - 'status' => 'failed',
7099 - 'error_message' => $result->get_error_message(),
7100 - 'attempts' => $current_item ? $current_item->max_attempts : 3
7101 - ),
7102 - array('id' => $item_id),
7103 - array('%s', '%s', '%d'),
7104 - array('%d')
7105 - );
7106 - wp_send_json_error(array(
7107 - 'message' => $result->get_error_message(),
7108 - 'permanent_failure' => true,
7109 - 'item_id' => $item_id
7110 - ));
7111 - return;
7112 - }
7113 7067 throw new Exception($result->get_error_message());
7114 7068 }
7115 -
7069 +
7116 7070 if ($result === false) {
7117 7071 throw new Exception('Processing returned false - item may be empty or invalid');
7118 7072 }
7119 7073
@@ -7189,9 +7143,9 @@
7189 7143
7190 7144 /**
7191 7145 * Process a URL from the queue
7192 7146 */
7193 -private function mxchat_process_queue_url($item_data, $bot_id = 'default', $queue_id = '') {
7147 +private function mxchat_process_queue_url($item_data, $bot_id = 'default') {
7194 7148 $url = isset($item_data['url']) ? $item_data['url'] : '';
7195 7149
7196 7150 if (empty($url)) {
7197 7151 return new WP_Error('invalid_url', 'URL is empty');
@@ -7239,9 +7193,9 @@
7239 7193
7240 7194 // Fetch URL content (fallback for non-products or when WooCommerce extraction fails)
7241 7195 $is_likely_pdf = (strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION)) === 'pdf');
7242 7196 $response = wp_remote_get($url, array(
7243 - 'timeout' => $is_likely_pdf ? 120 : 30,
7197 + 'timeout' => $is_likely_pdf ? 60 : 30,
7244 7198 'redirection' => 5,
7245 7199 'user-agent' => 'MxChat/1.0'
7246 7200 ));
7247 7201
@@ -7250,14 +7204,14 @@
7250 7204 }
7251 7205
7252 7206 $response_code = wp_remote_retrieve_response_code($response);
7253 7207 if ($response_code !== 200) {
7254 - return new WP_Error('http_error', 'HTTP ' . $response_code . ' error for: ' . $url);
7208 + return new WP_Error('http_error', 'HTTP ' . $response_code . ' error');
7255 7209 }
7256 7210
7257 - // Check if URL is a PDF — expand into per-page queue items using the standard PDF pipeline
7211 + // Check if URL is a PDF — process through PDF pipeline instead of HTML
7258 7212 if ($this->mxchat_is_pdf_url($url, $response)) {
7259 - return $this->mxchat_expand_pdf_to_queue($url, $response, $bot_id, $queue_id);
7213 + return $this->mxchat_process_pdf_url_inline($url, $response, $api_key, $bot_id);
7260 7214 }
7261 7215
7262 7216 $html = wp_remote_retrieve_body($response);
7263 7217
@@ -7287,89 +7241,11 @@
7287 7241 return $result;
7288 7242 }
7289 7243
7290 7244 /**
7291 - * Expand a PDF URL into per-page queue items using the standard PDF pipeline.
7292 - * Called when a sitemap URL turns out to be a PDF — downloads, parses page count,
7293 - * and adds pdf_page items to the same queue so they process with full progress tracking.
7245 + * Process a PDF URL inline during sitemap queue processing.
7246 + * Downloads the PDF, extracts all pages, and submits each to the DB.
7294 7247 */
7295 -private function mxchat_expand_pdf_to_queue($pdf_url, $response, $bot_id = 'default', $queue_id = '') {
7296 - set_time_limit(120); // PDFs need extra time for download + parsing
7297 -
7298 - $upload_dir = wp_upload_dir();
7299 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
7300 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
7301 -
7302 - $response_body = wp_remote_retrieve_body($response);
7303 - if (empty($response_body)) {
7304 - return new WP_Error('empty_pdf', 'Empty PDF response for: ' . $pdf_url);
7305 - }
7306 -
7307 - if (!wp_mkdir_p(dirname($pdf_path))) {
7308 - return new WP_Error('dir_error', 'Failed to create upload directory');
7309 - }
7310 -
7311 - file_put_contents($pdf_path, $response_body);
7312 -
7313 - if (!file_exists($pdf_path)) {
7314 - return new WP_Error('save_error', 'Failed to save PDF file');
7315 - }
7316 -
7317 - try {
7318 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
7319 -
7320 - if ($total_pages === false || $total_pages < 1) {
7321 - wp_delete_file($pdf_path);
7322 - return new WP_Error('no_pages', 'PDF has no pages: ' . $pdf_url);
7323 - }
7324 -
7325 - // Build per-page items identical to mxchat_handle_pdf_for_knowledge_base
7326 - $pages = array();
7327 - for ($i = 1; $i <= $total_pages; $i++) {
7328 - $pages[] = array(
7329 - 'pdf_path' => $pdf_path,
7330 - 'pdf_url' => $pdf_url,
7331 - 'page_number' => $i,
7332 - 'total_pages' => $total_pages
7333 - );
7334 - }
7335 -
7336 - // Add pdf_page items to the SAME queue so the JS picks them up automatically
7337 - if (!empty($queue_id)) {
7338 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
7339 - } else {
7340 - // Fallback: create a new PDF queue (shouldn't happen in sitemap flow)
7341 - $new_queue_id = 'pdf_' . md5($pdf_url . time());
7342 - $queued_count = $this->mxchat_add_to_queue($new_queue_id, 'pdf_page', $pages, $bot_id);
7343 - $this->mxchat_set_queue_meta($new_queue_id, 'source_url', $pdf_url);
7344 - $this->mxchat_set_queue_meta($new_queue_id, 'queue_type', 'pdf');
7345 - $this->mxchat_set_queue_meta($new_queue_id, 'total_items', $total_pages);
7346 - $this->mxchat_set_queue_meta($new_queue_id, 'bot_id', $bot_id);
7347 - $this->mxchat_set_queue_meta($new_queue_id, 'pdf_path', $pdf_path);
7348 - $this->mxchat_set_queue_meta($new_queue_id, 'created_at', current_time('mysql'));
7349 - }
7350 -
7351 - if ($queued_count === 0) {
7352 - wp_delete_file($pdf_path);
7353 - return new WP_Error('queue_error', 'Failed to add PDF pages to queue');
7354 - }
7355 -
7356 - // Return true so the original URL item is marked complete
7357 - // The new pdf_page items will be processed in subsequent batches
7358 - return true;
7359 -
7360 - } catch (Exception $e) {
7361 - if (file_exists($pdf_path)) {
7362 - wp_delete_file($pdf_path);
7363 - }
7364 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7365 - }
7366 -}
7367 -
7368 -/**
7369 - * Legacy: Process a PDF URL inline during sitemap queue processing.
7370 - * @deprecated Use mxchat_expand_pdf_to_queue instead — kept for reference only.
7371 - */
7372 7248 private function mxchat_process_pdf_url_inline($pdf_url, $response, $api_key, $bot_id = 'default') {
7373 7249 set_time_limit(120); // PDFs need more time — downloading + parsing all pages
7374 7250
7375 7251 $upload_dir = wp_upload_dir();
@@ -7403,24 +7279,21 @@
7403 7279 return new WP_Error('no_pages', 'PDF has no pages');
7404 7280 }
7405 7281
7406 7282 $processed = 0;
7407 - $skipped_pages = array();
7408 7283
7409 7284 for ($i = 0; $i < $total_pages; $i++) {
7410 - $page_num = $i + 1;
7411 7285 $text = $pages[$i]->getText();
7412 7286 if (empty($text)) {
7413 - $skipped_pages[] = 'Page ' . $page_num . ': No text could be extracted — page may contain only images, links, or non-standard encoding';
7414 7287 continue;
7415 7288 }
7416 7289
7417 7290 $sanitized = $this->mxchat_sanitize_content_for_api($text);
7418 7291 if (empty($sanitized)) {
7419 - $skipped_pages[] = 'Page ' . $page_num . ': Text was extracted but contained only special characters, control codes, or unsupported content';
7420 7292 continue;
7421 7293 }
7422 7294
7295 + $page_num = $i + 1;
7423 7296 $metadata = array(
7424 7297 'document_type' => 'pdf',
7425 7298 'total_pages' => $total_pages,
7426 7299 'current_page' => $page_num,
@@ -7444,12 +7317,8 @@
7444 7317
7445 7318 // Clean up the temp PDF file
7446 7319 wp_delete_file($pdf_path);
7447 7320
7448 - if (!empty($skipped_pages)) {
7449 - error_log('MxChat PDF: Skipped ' . count($skipped_pages) . ' of ' . $total_pages . ' pages: ' . implode('; ', $skipped_pages));
7450 - }
7451 -
7452 7321 return $processed > 0 ? true : false;
7453 7322
7454 7323 } catch (Exception $e) {
7455 7324 if (file_exists($pdf_path)) {
@@ -7614,17 +7483,18 @@
7614 7483 return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF');
7615 7484 }
7616 7485
7617 7486 $text = $pages[$page_number - 1]->getText();
7618 -
7487 +
7619 7488 if (empty($text)) {
7620 - return new WP_Error('empty_page', 'Page ' . $page_number . ': No text could be extracted — page may contain only images, links, or non-standard encoding');
7489 + // Not an error - just an empty page
7490 + return false;
7621 7491 }
7622 -
7492 +
7623 7493 $sanitized = $this->mxchat_sanitize_content_for_api($text);
7624 -
7494 +
7625 7495 if (empty($sanitized)) {
7626 - 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');
7496 + return false;
7627 7497 }
7628 7498
7629 7499 // Create metadata
7630 7500 $metadata = array(
@@ -7728,16 +7598,17 @@
7728 7598
7729 7599 // Calculate percentage
7730 7600 $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0;
7731 7601
7732 - // Get failed items details (include all failed items, not just those that exhausted retries)
7602 + // Get failed items details
7733 7603 $failed_items = array();
7734 7604 if ($failed > 0) {
7735 7605 $failed_items = $wpdb->get_results($wpdb->prepare(
7736 - "SELECT item_type, item_data, error_message, attempts
7737 - FROM $table_name
7738 - WHERE queue_id = %s
7606 + "SELECT item_type, item_data, error_message, attempts
7607 + FROM $table_name
7608 + WHERE queue_id = %s
7739 7609 AND status = 'failed'
7610 + AND attempts >= max_attempts
7740 7611 ORDER BY id DESC
7741 7612 LIMIT 50",
7742 7613 $queue_id
7743 7614 ));