| @@ -58,10 +58,12 @@ | ||
| 58 | 58 | add_action('wp_ajax_mxchat_refresh_pinecone_entries', array($this, 'ajax_mxchat_refresh_pinecone_entries')); |
| 59 | 59 | add_action('wp_ajax_mxchat_paginate_entries', array($this, 'ajax_mxchat_paginate_entries')); |
| 60 | 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 | - add_action('wp_ajax_mxchat_inspect_entry', array($this, 'ajax_mxchat_inspect_entry')); | |
| 63 | 62 | |
| 63 | + // Hook for content deletion | |
| 64 | + add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1); | |
| 65 | + | |
| 64 | 66 | // WordPress post management hooks |
| 65 | 67 | add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2); |
| 66 | 68 | add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3); |
| 67 | 69 | add_action('before_delete_post', array($this, 'mxchat_handle_post_delete')); |
| @@ -161,13 +163,9 @@ | ||
| 161 | 163 | public function mxchat_is_pdf_url($url, $response) { |
| 162 | 164 | $content_type = wp_remote_retrieve_header($response, 'content-type'); |
| 163 | 165 | $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION)); |
| 164 | 166 | |
| 165 | - // Check Content-Disposition header for .pdf filename (Google Drive sends this) | |
| 166 | - $disposition = wp_remote_retrieve_header($response, 'content-disposition'); | |
| 167 | - $has_pdf_disposition = ! empty($disposition) && stripos($disposition, '.pdf') !== false; | |
| 168 | - | |
| 169 | - return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf' || $has_pdf_disposition; | |
| 167 | + return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf'; | |
| 170 | 168 | } |
| 171 | 169 | |
| 172 | 170 | |
| 173 | 171 | public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') { |
| @@ -815,230 +813,8 @@ | ||
| 815 | 813 | ); |
| 816 | 814 | } |
| 817 | 815 | |
| 818 | 816 | /** |
| 819 | - * AJAX: Inspect a knowledge entry — returns the per-chunk stored text + metadata | |
| 820 | - * WITHOUT collapsing it, so a site owner can see exactly what was indexed for an | |
| 821 | - * entry (plan-mxchat-20260628-d8cb4b). READ-ONLY: never re-embeds or mutates. | |
| 822 | - */ | |
| 823 | -public function ajax_mxchat_inspect_entry() { | |
| 824 | - check_ajax_referer('mxchat_inspect_entry_nonce', 'nonce'); | |
| 825 | - | |
| 826 | - if ( ! current_user_can('manage_options') ) { | |
| 827 | - wp_send_json_error( array( 'message' => esc_html__('Permission denied.', 'mxchat') ) ); | |
| 828 | - } | |
| 829 | - | |
| 830 | - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : ''; | |
| 831 | - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0; | |
| 832 | - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress'; | |
| 833 | - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default'; | |
| 834 | - | |
| 835 | - if ( $data_source === 'pinecone' ) { | |
| 836 | - $result = $this->inspect_pinecone_entry( $source_url, $entry_id, $bot_id ); | |
| 837 | - } else { | |
| 838 | - $result = $this->inspect_wordpress_entry( $source_url, $entry_id ); | |
| 839 | - } | |
| 840 | - | |
| 841 | - if ( is_wp_error( $result ) ) { | |
| 842 | - wp_send_json_error( array( 'message' => $result->get_error_message() ) ); | |
| 843 | - } | |
| 844 | - | |
| 845 | - wp_send_json_success( $result ); | |
| 846 | -} | |
| 847 | - | |
| 848 | -/** | |
| 849 | - * Read-only inspector for WordPress-DB entries. Mirrors get_wordpress_entry_content() | |
| 850 | - * but returns each STORED chunk's exact text + length (no implode), plus the assembled | |
| 851 | - * embedded text. This shows what is actually in the index, not a re-derivation from the post. | |
| 852 | - */ | |
| 853 | -private function inspect_wordpress_entry( $source_url, $entry_id ) { | |
| 854 | - global $wpdb; | |
| 855 | - $table = $wpdb->prefix . 'mxchat_system_prompt_content'; | |
| 856 | - | |
| 857 | - $rows = array(); | |
| 858 | - | |
| 859 | - // Group by the real stored source_url — this INCLUDES "mxchat://" manual | |
| 860 | - // Direct Content entries (the spec's manual-entry case), which share one | |
| 861 | - // source_url across their chunk rows. Only the synthetic "_ungrouped_<id>" | |
| 862 | - // display key (invented by the table view for rows with no source_url) is | |
| 863 | - // excluded; those fall through to the entry_id lookup below. | |
| 864 | - if ( ! empty( $source_url ) && strpos( $source_url, '_ungrouped_' ) !== 0 ) { | |
| 865 | - $rows = $wpdb->get_results( $wpdb->prepare( | |
| 866 | - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE source_url = %s ORDER BY id ASC", | |
| 867 | - $source_url | |
| 868 | - ) ); | |
| 869 | - } | |
| 870 | - | |
| 871 | - // Fallback / manual "Direct Content" entries: fetch the single row by id. | |
| 872 | - if ( empty( $rows ) && $entry_id > 0 ) { | |
| 873 | - $row = $wpdb->get_row( $wpdb->prepare( | |
| 874 | - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE id = %d", | |
| 875 | - $entry_id | |
| 876 | - ) ); | |
| 877 | - if ( $row ) { | |
| 878 | - $rows = array( $row ); | |
| 879 | - } | |
| 880 | - } | |
| 881 | - | |
| 882 | - if ( empty( $rows ) ) { | |
| 883 | - return new WP_Error( 'not_found', esc_html__('Entry not found in the local knowledge database.', 'mxchat') ); | |
| 884 | - } | |
| 885 | - | |
| 886 | - $chunks = array(); | |
| 887 | - $content_type = ''; | |
| 888 | - foreach ( $rows as $row ) { | |
| 889 | - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content ); | |
| 890 | - $text = isset( $parsed['text'] ) ? $parsed['text'] : ''; | |
| 891 | - $index = isset( $parsed['metadata']['chunk_index'] ) ? intval( $parsed['metadata']['chunk_index'] ) : count( $chunks ); | |
| 892 | - $content_type = $row->content_type; | |
| 893 | - $chunks[] = array( | |
| 894 | - 'index' => $index, | |
| 895 | - 'text' => $text, | |
| 896 | - 'length' => function_exists('mb_strlen') ? mb_strlen( $text ) : strlen( $text ), | |
| 897 | - 'row_id' => intval( $row->id ), | |
| 898 | - ); | |
| 899 | - } | |
| 900 | - | |
| 901 | - usort( $chunks, function( $a, $b ) { return $a['index'] - $b['index']; } ); | |
| 902 | - | |
| 903 | - $assembled = implode( "\n\n", wp_list_pluck( $chunks, 'text' ) ); | |
| 904 | - | |
| 905 | - return array( | |
| 906 | - 'store' => 'wordpress', | |
| 907 | - 'source_url' => $source_url, | |
| 908 | - 'content_type' => $content_type, | |
| 909 | - 'is_chunked' => count( $chunks ) > 1, | |
| 910 | - 'chunk_count' => count( $chunks ), | |
| 911 | - 'assembled' => $assembled, | |
| 912 | - 'assembled_length' => function_exists('mb_strlen') ? mb_strlen( $assembled ) : strlen( $assembled ), | |
| 913 | - 'chunks' => array_values( $chunks ), | |
| 914 | - // WP-DB storage carries no separate vector metadata; surface that fact | |
| 915 | - // rather than letting the owner guess (the spec's taxonomy question). | |
| 916 | - 'metadata' => array(), | |
| 917 | - 'metadata_note' => esc_html__('Stored in the local WordPress database. Only the assembled text shown here is embedded — there are no separate vector metadata fields (e.g. taxonomy terms are not stored unless they were injected into the text itself).', 'mxchat'), | |
| 918 | - ); | |
| 919 | -} | |
| 920 | - | |
| 921 | -/** | |
| 922 | - * Read-only inspector for Pinecone entries. Mirrors get_pinecone_entry_content() | |
| 923 | - * but keeps each vector's text + metadata instead of imploding, so the owner can | |
| 924 | - * confirm exactly which metadata fields (text/source_url/type/last_updated/created_at/bot_id) | |
| 925 | - * are present per chunk. READ-ONLY. | |
| 926 | - */ | |
| 927 | -private function inspect_pinecone_entry( $source_url, $entry_id, $bot_id ) { | |
| 928 | - if ( ! class_exists('MxChat_Pinecone_Manager') ) { | |
| 929 | - return new WP_Error( 'pinecone_unavailable', esc_html__('Pinecone manager not available.', 'mxchat') ); | |
| 930 | - } | |
| 931 | - | |
| 932 | - if ( $bot_id === 'default' || ! class_exists('MxChat_Multi_Bot_Manager') ) { | |
| 933 | - $pinecone_options = get_option('mxchat_pinecone_addon_options'); | |
| 934 | - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? ''; | |
| 935 | - $host = $pinecone_options['mxchat_pinecone_host'] ?? ''; | |
| 936 | - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? ''; | |
| 937 | - } else { | |
| 938 | - $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id); | |
| 939 | - $api_key = $bot_config['api_key'] ?? ''; | |
| 940 | - $host = $bot_config['host'] ?? ''; | |
| 941 | - $namespace = $bot_config['namespace'] ?? ''; | |
| 942 | - } | |
| 943 | - | |
| 944 | - if ( empty($host) || empty($api_key) ) { | |
| 945 | - return new WP_Error( 'pinecone_config', esc_html__('Pinecone not configured.', 'mxchat') ); | |
| 946 | - } | |
| 947 | - | |
| 948 | - $base_id = md5( $source_url ); | |
| 949 | - $vector_ids = array( $base_id ); | |
| 950 | - | |
| 951 | - $list_url = "https://{$host}/vectors/list"; | |
| 952 | - $list_body = array( 'prefix' => $base_id . '_chunk_', 'limit' => 100 ); | |
| 953 | - if ( ! empty($namespace) ) { | |
| 954 | - $list_body['namespace'] = $namespace; | |
| 955 | - } | |
| 956 | - | |
| 957 | - $list_resp = wp_remote_post( $list_url, array( | |
| 958 | - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), | |
| 959 | - 'body' => wp_json_encode( $list_body ), | |
| 960 | - 'timeout' => 15, | |
| 961 | - ) ); | |
| 962 | - | |
| 963 | - if ( ! is_wp_error($list_resp) ) { | |
| 964 | - $list_data = json_decode( wp_remote_retrieve_body($list_resp), true ); | |
| 965 | - if ( ! empty($list_data['vectors']) ) { | |
| 966 | - foreach ( $list_data['vectors'] as $v ) { | |
| 967 | - $vector_ids[] = $v['id']; | |
| 968 | - } | |
| 969 | - } | |
| 970 | - } | |
| 971 | - | |
| 972 | - $fetch_url = "https://{$host}/vectors/fetch"; | |
| 973 | - $fetch_body = array( 'ids' => $vector_ids ); | |
| 974 | - if ( ! empty($namespace) ) { | |
| 975 | - $fetch_body['namespace'] = $namespace; | |
| 976 | - } | |
| 977 | - | |
| 978 | - $fetch_resp = wp_remote_post( $fetch_url, array( | |
| 979 | - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ), | |
| 980 | - 'body' => wp_json_encode( $fetch_body ), | |
| 981 | - 'timeout' => 15, | |
| 982 | - ) ); | |
| 983 | - | |
| 984 | - if ( is_wp_error($fetch_resp) ) { | |
| 985 | - return new WP_Error( 'pinecone_fetch', esc_html__('Failed to fetch from Pinecone.', 'mxchat') ); | |
| 986 | - } | |
| 987 | - | |
| 988 | - $fetch_data = json_decode( wp_remote_retrieve_body($fetch_resp), true ); | |
| 989 | - $vectors = $fetch_data['vectors'] ?? array(); | |
| 990 | - | |
| 991 | - if ( empty($vectors) ) { | |
| 992 | - return new WP_Error( 'not_found', esc_html__('Entry not found in Pinecone.', 'mxchat') ); | |
| 993 | - } | |
| 994 | - | |
| 995 | - // Whitelisted metadata fields the spec calls out — shown so devs can confirm | |
| 996 | - // what is (and is NOT) stored per vector. | |
| 997 | - $meta_fields = array( 'text', 'source_url', 'type', 'last_updated', 'created_at', 'bot_id', 'chunk_index', 'total_chunks' ); | |
| 998 | - $chunks = array(); | |
| 999 | - $content_type = ''; | |
| 1000 | - foreach ( $vectors as $vid => $vector ) { | |
| 1001 | - $meta = isset($vector['metadata']) && is_array($vector['metadata']) ? $vector['metadata'] : array(); | |
| 1002 | - $text = $meta['text'] ?? ''; | |
| 1003 | - $index = isset($meta['chunk_index']) ? intval($meta['chunk_index']) : count($chunks); | |
| 1004 | - $content_type = $meta['type'] ?? $content_type; | |
| 1005 | - | |
| 1006 | - $clean_meta = array(); | |
| 1007 | - foreach ( $meta_fields as $field ) { | |
| 1008 | - if ( array_key_exists( $field, $meta ) && $field !== 'text' ) { | |
| 1009 | - $clean_meta[ $field ] = is_scalar( $meta[ $field ] ) ? (string) $meta[ $field ] : wp_json_encode( $meta[ $field ] ); | |
| 1010 | - } | |
| 1011 | - } | |
| 1012 | - | |
| 1013 | - $chunks[] = array( | |
| 1014 | - 'index' => $index, | |
| 1015 | - 'text' => $text, | |
| 1016 | - 'length' => function_exists('mb_strlen') ? mb_strlen( $text ) : strlen( $text ), | |
| 1017 | - 'vector_id' => (string) $vid, | |
| 1018 | - 'metadata' => $clean_meta, | |
| 1019 | - ); | |
| 1020 | - } | |
| 1021 | - | |
| 1022 | - usort( $chunks, function( $a, $b ) { return $a['index'] - $b['index']; } ); | |
| 1023 | - | |
| 1024 | - $assembled = implode( "\n\n", wp_list_pluck( $chunks, 'text' ) ); | |
| 1025 | - | |
| 1026 | - return array( | |
| 1027 | - 'store' => 'pinecone', | |
| 1028 | - 'source_url' => $source_url, | |
| 1029 | - 'content_type' => $content_type, | |
| 1030 | - 'is_chunked' => count( $chunks ) > 1, | |
| 1031 | - 'chunk_count' => count( $chunks ), | |
| 1032 | - 'assembled' => $assembled, | |
| 1033 | - 'assembled_length' => function_exists('mb_strlen') ? mb_strlen( $assembled ) : strlen( $assembled ), | |
| 1034 | - 'chunks' => array_values( $chunks ), | |
| 1035 | - 'metadata' => array(), | |
| 1036 | - 'metadata_note' => esc_html__('Stored in Pinecone. Each chunk above lists the vector metadata fields actually present — if a field you expect (such as taxonomy terms) is missing here, it was not stored as metadata and is only searchable if it appears in the embedded text.', 'mxchat'), | |
| 1037 | - ); | |
| 1038 | -} | |
| 1039 | - | |
| 1040 | -/** | |
| 1041 | 817 | * AJAX: Save edited content — re-chunks and re-embeds as needed. |
| 1042 | 818 | * Works for both WordPress DB and Pinecone entries. |
| 1043 | 819 | */ |
| 1044 | 820 | public function ajax_mxchat_save_entry_content() { |
| @@ -1083,17 +859,10 @@ | ||
| 1083 | 859 | } |
| 1084 | 860 | |
| 1085 | 861 | // For manual entries (no source_url or mxchat:// prefix), delete the old entry by ID first |
| 1086 | 862 | // so submit_content_to_db creates a replacement instead of a duplicate |
| 1087 | - // Also treat legacy mxchat.ai source URLs as manual — old bug assigned the site URL to manual entries | |
| 1088 | - $is_legacy_manual = !empty($source_url) && strpos($source_url, 'mxchat.ai') !== false && strpos($source_url, 'mxchat://') !== 0; | |
| 1089 | - 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) ) { | |
| 1090 | 864 | $wpdb->delete( $table, array( 'id' => $entry_id ), array( '%d' ) ); |
| 1091 | - // Clear legacy URL so submit_content_to_db generates a unique mxchat:// identifier | |
| 1092 | - // instead of reusing the shared URL (which would mass-delete other entries with the same URL) | |
| 1093 | - if ( $is_legacy_manual ) { | |
| 1094 | - $source_url = ''; | |
| 1095 | - } | |
| 1096 | 865 | } |
| 1097 | 866 | |
| 1098 | 867 | // Use the existing submit_content_to_db which handles chunking, Pinecone, and WP DB |
| 1099 | 868 | $vector_id = ! empty($source_url) ? md5($source_url) : md5('mxchat_manual_' . $entry_id); |
| @@ -1170,22 +939,9 @@ | ||
| 1170 | 939 | exit; |
| 1171 | 940 | } |
| 1172 | 941 | |
| 1173 | 942 | $submitted_url = esc_url_raw($_POST['sitemap_url']); |
| 1174 | - | |
| 1175 | - // Convert Google Drive sharing URLs to direct download URLs | |
| 1176 | - if ( strpos($submitted_url, 'drive.google.com') !== false ) { | |
| 1177 | - $file_id = ''; | |
| 1178 | - if ( preg_match('/[?&]id=([a-zA-Z0-9_-]+)/', $submitted_url, $m) ) { | |
| 1179 | - $file_id = $m[1]; | |
| 1180 | - } elseif ( preg_match('#/file/d/([a-zA-Z0-9_-]+)#', $submitted_url, $m) ) { | |
| 1181 | - $file_id = $m[1]; | |
| 1182 | - } | |
| 1183 | - if ( ! empty($file_id) ) { | |
| 1184 | - $submitted_url = 'https://drive.google.com/uc?export=download&id=' . $file_id; | |
| 1185 | - } | |
| 1186 | - } | |
| 1187 | - | |
| 943 | + | |
| 1188 | 944 | // Get bot_id from form submission |
| 1189 | 945 | $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default'; |
| 1190 | 946 | |
| 1191 | 947 | // Get bot-specific options and validate API key |
| @@ -1213,23 +969,10 @@ | ||
| 1213 | 969 | wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts'))); |
| 1214 | 970 | exit; |
| 1215 | 971 | } |
| 1216 | 972 | |
| 1217 | - // Fetch URL — send an honest, versioned MXChat crawler UA (not a spoofed | |
| 1218 | - // browser). Stale browser UAs are exactly what WAFs like SiteGround's | |
| 1219 | - // ModSecurity flag as scrapers, 403-ing the fetch (including PDFs served | |
| 1220 | - // from the site's own media library, which route through this same call). | |
| 1221 | - // See mxchat_ingest_user_agent(). Accept is kept for content negotiation; | |
| 1222 | - // the browser-only Accept-Language fingerprint is dropped so it stays | |
| 1223 | - // coherent with a bot identity. | |
| 1224 | - $response = wp_remote_get($submitted_url, array( | |
| 1225 | - 'timeout' => 30, | |
| 1226 | - 'sslverify' => false, | |
| 1227 | - 'user-agent' => mxchat_ingest_user_agent(), | |
| 1228 | - 'headers' => array( | |
| 1229 | - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', | |
| 1230 | - ), | |
| 1231 | - )); | |
| 973 | + // Fetch URL | |
| 974 | + $response = wp_remote_get($submitted_url, array('timeout' => 30)); | |
| 1232 | 975 | |
| 1233 | 976 | if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { |
| 1234 | 977 | $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response); |
| 1235 | 978 | set_transient('mxchat_admin_notice_error', |
| @@ -3063,12 +2806,11 @@ | ||
| 3063 | 2806 | foreach ($primary_indexes as $path => $source) { |
| 3064 | 2807 | $url = trailingslashit($site_url) . $path; |
| 3065 | 2808 | |
| 3066 | 2809 | $response = wp_remote_head($url, array( |
| 3067 | - 'timeout' => 10, | |
| 2810 | + 'timeout' => 3, // Short timeout | |
| 3068 | 2811 | 'sslverify' => false, |
| 3069 | - 'redirection' => 1, | |
| 3070 | - 'user-agent' => mxchat_ingest_user_agent(), | |
| 2812 | + 'redirection' => 1 | |
| 3071 | 2813 | )); |
| 3072 | 2814 | |
| 3073 | 2815 | if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { |
| 3074 | 2816 | // Found a sitemap index - parse it to get sub-sitemaps |
| @@ -3126,14 +2868,10 @@ | ||
| 3126 | 2868 | private function parse_sitemap_index($url) { |
| 3127 | 2869 | $sub_sitemaps = array(); |
| 3128 | 2870 | |
| 3129 | 2871 | $response = wp_remote_get($url, array( |
| 3130 | - 'timeout' => 30, | |
| 3131 | - 'sslverify' => false, | |
| 3132 | - 'user-agent' => mxchat_ingest_user_agent(), | |
| 3133 | - 'headers' => array( | |
| 3134 | - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', | |
| 3135 | - ), | |
| 2872 | + 'timeout' => 5, | |
| 2873 | + 'sslverify' => false | |
| 3136 | 2874 | )); |
| 3137 | 2875 | |
| 3138 | 2876 | if (is_wp_error($response)) { |
| 3139 | 2877 | return $sub_sitemaps; |
| @@ -3184,14 +2922,10 @@ | ||
| 3184 | 2922 | * Get URL count from a sitemap |
| 3185 | 2923 | */ |
| 3186 | 2924 | private function get_sitemap_url_count($url) { |
| 3187 | 2925 | $response = wp_remote_get($url, array( |
| 3188 | - 'timeout' => 30, | |
| 3189 | - 'sslverify' => false, | |
| 3190 | - 'user-agent' => mxchat_ingest_user_agent(), | |
| 3191 | - 'headers' => array( | |
| 3192 | - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', | |
| 3193 | - ), | |
| 2926 | + 'timeout' => 10, | |
| 2927 | + 'sslverify' => false | |
| 3194 | 2928 | )); |
| 3195 | 2929 | |
| 3196 | 2930 | if (is_wp_error($response)) { |
| 3197 | 2931 | return 0; |
| @@ -3214,11 +2948,10 @@ | ||
| 3214 | 2948 | $sitemaps = array(); |
| 3215 | 2949 | $robots_url = trailingslashit($site_url) . 'robots.txt'; |
| 3216 | 2950 | |
| 3217 | 2951 | $response = wp_remote_get($robots_url, array( |
| 3218 | - 'timeout' => 15, | |
| 3219 | - 'sslverify' => false, | |
| 3220 | - 'user-agent' => mxchat_ingest_user_agent(), | |
| 2952 | + 'timeout' => 5, | |
| 2953 | + 'sslverify' => false | |
| 3221 | 2954 | )); |
| 3222 | 2955 | |
| 3223 | 2956 | if (is_wp_error($response)) { |
| 3224 | 2957 | return $sitemaps; |
| @@ -3708,22 +3441,9 @@ | ||
| 3708 | 3441 | } |
| 3709 | 3442 | |
| 3710 | 3443 | // Get bot_id from request |
| 3711 | 3444 | $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default'; |
| 3712 | - | |
| 3713 | - // ACF→PDF extraction is opt-in per import batch. Persist the last-used value so users | |
| 3714 | - // don't re-check on every batch; the default is OFF for installs that haven't set it. | |
| 3715 | - $extract_acf_pdfs = !empty($_POST['extract_acf_pdfs']) && $_POST['extract_acf_pdfs'] !== 'false'; | |
| 3716 | - $mxchat_options = get_option('mxchat_options', array()); | |
| 3717 | - if (!is_array($mxchat_options)) { | |
| 3718 | - $mxchat_options = array(); | |
| 3719 | - } | |
| 3720 | - $prior_default = !empty($mxchat_options['acf_pdf_extract_default']); | |
| 3721 | - if ($prior_default !== $extract_acf_pdfs) { | |
| 3722 | - $mxchat_options['acf_pdf_extract_default'] = $extract_acf_pdfs ? 1 : 0; | |
| 3723 | - update_option('mxchat_options', $mxchat_options); | |
| 3724 | - } | |
| 3725 | - | |
| 3445 | + | |
| 3726 | 3446 | // Process only ONE post at a time to avoid request size issues |
| 3727 | 3447 | $post_id = reset($post_ids); |
| 3728 | 3448 | $post = get_post($post_id); |
| 3729 | 3449 | |
| @@ -3829,57 +3549,23 @@ | ||
| 3829 | 3549 | } |
| 3830 | 3550 | |
| 3831 | 3551 | // ADD ACF FIELDS SUPPORT |
| 3832 | 3552 | $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id); |
| 3833 | - $pdf_extracted_count = 0; | |
| 3834 | 3553 | if (!empty($acf_fields)) { |
| 3835 | 3554 | $acf_content_parts = array(); |
| 3836 | - $pdf_attachment_ids = array(); | |
| 3837 | - | |
| 3555 | + | |
| 3838 | 3556 | foreach ($acf_fields as $field_name => $field_value) { |
| 3839 | 3557 | $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id); |
| 3840 | - | |
| 3558 | + | |
| 3841 | 3559 | if (!empty($formatted_value)) { |
| 3842 | 3560 | $field_label = ucwords(str_replace('_', ' ', $field_name)); |
| 3843 | 3561 | $acf_content_parts[] = $field_label . ": " . $formatted_value; |
| 3844 | 3562 | } |
| 3845 | - | |
| 3846 | - // Walk this field's value tree for any PDF attachment references and queue them for extraction. | |
| 3847 | - // Only when the user opted into ACF→PDF extraction for this batch; otherwise the ACF text | |
| 3848 | - // still lands in the KB but the heavier PDF parsing is skipped. | |
| 3849 | - if ($extract_acf_pdfs) { | |
| 3850 | - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids); | |
| 3851 | - } | |
| 3852 | 3563 | } |
| 3853 | - | |
| 3564 | + | |
| 3854 | 3565 | if (!empty($acf_content_parts)) { |
| 3855 | 3566 | $content .= "\n\n" . implode("\n", $acf_content_parts); |
| 3856 | 3567 | } |
| 3857 | - | |
| 3858 | - // Extract text from each unique PDF found in ACF fields and append as a labeled section | |
| 3859 | - if ($extract_acf_pdfs && !empty($pdf_attachment_ids)) { | |
| 3860 | - $pdf_attachment_ids = array_unique(array_filter(array_map('intval', $pdf_attachment_ids))); | |
| 3861 | - $pdf_sections = array(); | |
| 3862 | - foreach ($pdf_attachment_ids as $att_id) { | |
| 3863 | - $pdf_text = $this->mxchat_extract_pdf_text_by_attachment_id($att_id); | |
| 3864 | - if (!empty($pdf_text)) { | |
| 3865 | - $pdf_title = get_the_title($att_id); | |
| 3866 | - $pdf_url = wp_get_attachment_url($att_id); | |
| 3867 | - $header = 'PDF Attachment'; | |
| 3868 | - if (!empty($pdf_title)) { | |
| 3869 | - $header .= ': ' . $pdf_title; | |
| 3870 | - } | |
| 3871 | - if (!empty($pdf_url)) { | |
| 3872 | - $header .= ' (' . $pdf_url . ')'; | |
| 3873 | - } | |
| 3874 | - $pdf_sections[] = $header . "\n" . $pdf_text; | |
| 3875 | - $pdf_extracted_count++; | |
| 3876 | - } | |
| 3877 | - } | |
| 3878 | - if (!empty($pdf_sections)) { | |
| 3879 | - $content .= "\n\nAttached PDFs:\n" . implode("\n\n", $pdf_sections); | |
| 3880 | - } | |
| 3881 | - } | |
| 3882 | 3568 | } |
| 3883 | 3569 | |
| 3884 | 3570 | // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields) |
| 3885 | 3571 | $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id); |
| @@ -4009,9 +3695,8 @@ | ||
| 4009 | 3695 | 'title' => $post->post_title, |
| 4010 | 3696 | 'operation_type' => $operation_type, |
| 4011 | 3697 | 'vector_id' => $vector_id, |
| 4012 | 3698 | 'acf_fields_found' => $acf_field_count, |
| 4013 | - 'pdf_extracted_count' => (int) $pdf_extracted_count, | |
| 4014 | 3699 | 'content_preview' => substr($content, 0, 100) . '...', |
| 4015 | 3700 | 'bot_id' => $bot_id |
| 4016 | 3701 | )); |
| 4017 | 3702 | exit; |
| @@ -4426,20 +4111,9 @@ | ||
| 4426 | 4111 | |
| 4427 | 4112 | // Get bot-specific options |
| 4428 | 4113 | $bot_options = $this->get_bot_options($bot_id); |
| 4429 | 4114 | $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options'); |
| 4430 | - | |
| 4431 | - // Opt-in: when the custom provider is selected for embeddings, index through | |
| 4432 | - // the same custom endpoint the query path uses so stored vectors and query | |
| 4433 | - // vectors share a model. Returns the vector array on success, or an error | |
| 4434 | - // string on failure (this function's existing failure contract). | |
| 4435 | - if (isset($options['custom_provider_for_embeddings']) && $options['custom_provider_for_embeddings'] === 'on') { | |
| 4436 | - if (!class_exists('MxChat_Utils')) { | |
| 4437 | - require_once dirname(__FILE__) . '/../includes/class-mxchat-utils.php'; | |
| 4438 | - } | |
| 4439 | - return MxChat_Utils::generate_embedding_custom($text, $options); | |
| 4440 | - } | |
| 4441 | - | |
| 4115 | + | |
| 4442 | 4116 | $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002'; |
| 4443 | 4117 | //error_log('[MXCHAT-EMBED] Selected embedding model for bot ' . $bot_id . ': ' . $selected_model); |
| 4444 | 4118 | |
| 4445 | 4119 | // Determine provider and endpoint |
| @@ -5357,197 +5031,8 @@ | ||
| 5357 | 5031 | return implode(', ', array_filter($text_parts)); |
| 5358 | 5032 | } |
| 5359 | 5033 | |
| 5360 | 5034 | /** |
| 5361 | - * Walk an ACF field value tree and collect attachment IDs for any value that | |
| 5362 | - * resolves to a PDF in the WordPress media library. Handles the three shapes | |
| 5363 | - * ACF returns for File/Image/URL fields (array with ID+url, integer attachment ID, | |
| 5364 | - * plain URL string), and recurses through repeater/group/flexible content. | |
| 5365 | - * | |
| 5366 | - * @param mixed $value The ACF field value (any depth) | |
| 5367 | - * @param array $out Accumulator (passed by reference) for attachment IDs | |
| 5368 | - * @param int $depth Recursion guard | |
| 5369 | - */ | |
| 5370 | -private function mxchat_collect_pdf_attachment_ids_from_acf_value($value, &$out, $depth = 0) { | |
| 5371 | - if ($depth > 6) { | |
| 5372 | - return; // prevent runaway recursion on circular/very-deep structures | |
| 5373 | - } | |
| 5374 | - | |
| 5375 | - if (empty($value)) { | |
| 5376 | - return; | |
| 5377 | - } | |
| 5378 | - | |
| 5379 | - // Array shapes: ACF File/Image return value=array; repeaters/groups are arrays of arrays | |
| 5380 | - if (is_array($value)) { | |
| 5381 | - // Direct File/Image-style array (has 'url' and usually 'ID' + 'mime_type') | |
| 5382 | - $looks_like_attachment = isset($value['url']) || isset($value['ID']) || isset($value['id']); | |
| 5383 | - if ($looks_like_attachment) { | |
| 5384 | - $att_id = 0; | |
| 5385 | - if (!empty($value['ID']) && is_numeric($value['ID'])) { | |
| 5386 | - $att_id = (int) $value['ID']; | |
| 5387 | - } elseif (!empty($value['id']) && is_numeric($value['id'])) { | |
| 5388 | - $att_id = (int) $value['id']; | |
| 5389 | - } elseif (!empty($value['url']) && is_string($value['url'])) { | |
| 5390 | - $att_id = (int) attachment_url_to_postid($value['url']); | |
| 5391 | - } | |
| 5392 | - | |
| 5393 | - $is_pdf = false; | |
| 5394 | - if (!empty($value['mime_type']) && $value['mime_type'] === 'application/pdf') { | |
| 5395 | - $is_pdf = true; | |
| 5396 | - } elseif (!empty($value['subtype']) && strtolower((string) $value['subtype']) === 'pdf') { | |
| 5397 | - $is_pdf = true; | |
| 5398 | - } elseif (!empty($value['url']) && is_string($value['url']) && $this->mxchat_url_looks_like_pdf($value['url'])) { | |
| 5399 | - $is_pdf = true; | |
| 5400 | - } elseif ($att_id && get_post_mime_type($att_id) === 'application/pdf') { | |
| 5401 | - $is_pdf = true; | |
| 5402 | - } | |
| 5403 | - | |
| 5404 | - if ($is_pdf && $att_id && get_post_mime_type($att_id) === 'application/pdf') { | |
| 5405 | - $out[] = $att_id; | |
| 5406 | - } | |
| 5407 | - // An array node that represents one attachment doesn't contain other | |
| 5408 | - // attachments inside it — done with this branch. | |
| 5409 | - return; | |
| 5410 | - } | |
| 5411 | - | |
| 5412 | - // Recurse: repeater rows, flexible-content layouts, groups, etc. | |
| 5413 | - foreach ($value as $sub) { | |
| 5414 | - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($sub, $out, $depth + 1); | |
| 5415 | - } | |
| 5416 | - return; | |
| 5417 | - } | |
| 5418 | - | |
| 5419 | - // Plain numeric attachment ID (ACF File field set to "Return: ID") | |
| 5420 | - if (is_numeric($value)) { | |
| 5421 | - $att_id = (int) $value; | |
| 5422 | - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') { | |
| 5423 | - $out[] = $att_id; | |
| 5424 | - } | |
| 5425 | - return; | |
| 5426 | - } | |
| 5427 | - | |
| 5428 | - // Plain string — URL pointing at a PDF (ACF File field set to "Return: URL", or a custom URL/text field) | |
| 5429 | - if (is_string($value)) { | |
| 5430 | - $trimmed = trim($value); | |
| 5431 | - if ($trimmed !== '' && $this->mxchat_url_looks_like_pdf($trimmed)) { | |
| 5432 | - $att_id = (int) attachment_url_to_postid($trimmed); | |
| 5433 | - if ($att_id > 0 && get_post_mime_type($att_id) === 'application/pdf') { | |
| 5434 | - $out[] = $att_id; | |
| 5435 | - } | |
| 5436 | - } | |
| 5437 | - return; | |
| 5438 | - } | |
| 5439 | -} | |
| 5440 | - | |
| 5441 | -/** | |
| 5442 | - * Heuristic: does this URL/string look like a PDF reference? | |
| 5443 | - * Tolerates query strings and fragments (#page=2). | |
| 5444 | - */ | |
| 5445 | -private function mxchat_url_looks_like_pdf($url) { | |
| 5446 | - if (!is_string($url) || $url === '') { | |
| 5447 | - return false; | |
| 5448 | - } | |
| 5449 | - // Strip query + fragment before checking extension | |
| 5450 | - $path = preg_replace('/[?#].*$/', '', $url); | |
| 5451 | - return (bool) preg_match('/\.pdf$/i', $path); | |
| 5452 | -} | |
| 5453 | - | |
| 5454 | -/** | |
| 5455 | - * Extract text from a PDF attachment by ID using the bundled Smalot parser. | |
| 5456 | - * Reads the file directly from disk via get_attached_file (no HTTP fetch). | |
| 5457 | - * Result is cached on the attachment as post_meta keyed by file mtime so we | |
| 5458 | - * only parse the same PDF once unless the file changes on disk. | |
| 5459 | - * | |
| 5460 | - * @param int $attachment_id | |
| 5461 | - * @return string Extracted plain text, or '' on failure. | |
| 5462 | - */ | |
| 5463 | -private function mxchat_extract_pdf_text_by_attachment_id($attachment_id) { | |
| 5464 | - $attachment_id = (int) $attachment_id; | |
| 5465 | - if ($attachment_id <= 0) { | |
| 5466 | - return ''; | |
| 5467 | - } | |
| 5468 | - if (get_post_mime_type($attachment_id) !== 'application/pdf') { | |
| 5469 | - return ''; | |
| 5470 | - } | |
| 5471 | - | |
| 5472 | - $pdf_path = get_attached_file($attachment_id); | |
| 5473 | - if (empty($pdf_path) || !file_exists($pdf_path) || !is_readable($pdf_path)) { | |
| 5474 | - return ''; | |
| 5475 | - } | |
| 5476 | - | |
| 5477 | - // Raw-file size cap. Parsing very large PDFs can OOM the request; skip with a log entry | |
| 5478 | - // and let the rest of the ACF content land in the KB. Filterable for users who need it bigger. | |
| 5479 | - $default_max_bytes = 25 * 1024 * 1024; | |
| 5480 | - $max_bytes = (int) apply_filters('mxchat_acf_pdf_max_bytes', $default_max_bytes, $attachment_id, $pdf_path); | |
| 5481 | - if ($max_bytes > 0) { | |
| 5482 | - $file_size = @filesize($pdf_path); | |
| 5483 | - if ($file_size !== false && $file_size > $max_bytes) { | |
| 5484 | - error_log(sprintf( | |
| 5485 | - '[mxchat] ACF PDF skipped (over size cap): attachment %d "%s" %d bytes > cap %d', | |
| 5486 | - $attachment_id, | |
| 5487 | - basename($pdf_path), | |
| 5488 | - $file_size, | |
| 5489 | - $max_bytes | |
| 5490 | - )); | |
| 5491 | - return ''; | |
| 5492 | - } | |
| 5493 | - } | |
| 5494 | - | |
| 5495 | - $mtime = @filemtime($pdf_path); | |
| 5496 | - $cache_meta_key = '_mxchat_acf_pdf_text_v1'; | |
| 5497 | - $cached = get_post_meta($attachment_id, $cache_meta_key, true); | |
| 5498 | - if (is_array($cached) && isset($cached['mtime'], $cached['text']) && (int) $cached['mtime'] === (int) $mtime) { | |
| 5499 | - return (string) $cached['text']; | |
| 5500 | - } | |
| 5501 | - | |
| 5502 | - $text = ''; | |
| 5503 | - try { | |
| 5504 | - if (function_exists('mxchat_load_pdf_parser')) { | |
| 5505 | - mxchat_load_pdf_parser(); | |
| 5506 | - } | |
| 5507 | - if (!class_exists('\\Smalot\\PdfParser\\Parser')) { | |
| 5508 | - return ''; | |
| 5509 | - } | |
| 5510 | - $parser = new \Smalot\PdfParser\Parser(); | |
| 5511 | - $pdf = $parser->parseFile($pdf_path); | |
| 5512 | - $pages = $pdf->getPages(); | |
| 5513 | - $page_texts = array(); | |
| 5514 | - foreach ($pages as $page) { | |
| 5515 | - $page_text = ''; | |
| 5516 | - try { | |
| 5517 | - $page_text = $page->getText(); | |
| 5518 | - } catch (\Exception $e) { | |
| 5519 | - $page_text = ''; | |
| 5520 | - } | |
| 5521 | - if (!empty($page_text)) { | |
| 5522 | - $page_texts[] = $page_text; | |
| 5523 | - } | |
| 5524 | - } | |
| 5525 | - $text = trim(implode("\n\n", $page_texts)); | |
| 5526 | - } catch (\Exception $e) { | |
| 5527 | - error_log('[mxchat] ACF PDF extraction failed for attachment ' . $attachment_id . ': ' . $e->getMessage()); | |
| 5528 | - return ''; | |
| 5529 | - } catch (\Throwable $e) { | |
| 5530 | - error_log('[mxchat] ACF PDF extraction error for attachment ' . $attachment_id . ': ' . $e->getMessage()); | |
| 5531 | - return ''; | |
| 5532 | - } | |
| 5533 | - | |
| 5534 | - // Cap per-PDF text to avoid blowing up the embedding payload on enormous PDFs. | |
| 5535 | - // The chunker downstream will still split this into multiple vectors. | |
| 5536 | - $max_len = (int) apply_filters('mxchat_acf_pdf_text_max_length', 50000); | |
| 5537 | - if ($max_len > 0 && strlen($text) > $max_len) { | |
| 5538 | - $text = substr($text, 0, $max_len); | |
| 5539 | - } | |
| 5540 | - | |
| 5541 | - update_post_meta($attachment_id, $cache_meta_key, array( | |
| 5542 | - 'mtime' => (int) $mtime, | |
| 5543 | - 'text' => $text, | |
| 5544 | - )); | |
| 5545 | - | |
| 5546 | - return $text; | |
| 5547 | -} | |
| 5548 | - | |
| 5549 | -/** | |
| 5550 | 5035 | * Handle ACF save - fires after ACF fields are saved |
| 5551 | 5036 | * This ensures ACF field data is available when syncing to knowledge base |
| 5552 | 5037 | */ |
| 5553 | 5038 | public function mxchat_handle_acf_save($post_id) { |
| @@ -5655,33 +5140,40 @@ | ||
| 5655 | 5140 | // If the post was previously published but is now not published, remove from knowledge base |
| 5656 | 5141 | if ($previous_status === 'publish' && $post->post_status !== 'publish') { |
| 5657 | 5142 | // Use the stored URL from when it was published, or fall back to current permalink |
| 5658 | 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'; | |
| 5659 | 5149 | |
| 5660 | - if ($source_url) { | |
| 5661 | - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks) | |
| 5662 | - 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 | + } | |
| 5663 | 5164 | } |
| 5664 | - | |
| 5165 | + | |
| 5665 | 5166 | // Clean up the transients and exit early |
| 5666 | 5167 | delete_transient($previous_status_key); |
| 5667 | 5168 | delete_transient($previous_url_key); |
| 5668 | 5169 | return; |
| 5669 | 5170 | } |
| 5670 | - | |
| 5671 | - // Slug/permalink rename while still published: delete the old vectors before upserting new ones. | |
| 5672 | - // Without this, md5(old_url) vectors (base + chunks) would be orphaned under the stale URL. | |
| 5673 | - if ($post->post_status === 'publish' && !empty($previous_url)) { | |
| 5674 | - $current_url = get_permalink($post_id); | |
| 5675 | - if ($current_url && $current_url !== $previous_url) { | |
| 5676 | - MxChat_Utils::delete_chunks_for_url($previous_url, 'default'); | |
| 5677 | - } | |
| 5678 | - } | |
| 5679 | - | |
| 5171 | + | |
| 5680 | 5172 | // Store the current status for next time (if this is an update) |
| 5681 | 5173 | if ($update) { |
| 5682 | 5174 | set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS); |
| 5683 | - | |
| 5175 | + | |
| 5684 | 5176 | // If the post is currently published, also store its URL |
| 5685 | 5177 | if ($post->post_status === 'publish') { |
| 5686 | 5178 | $current_url = get_permalink($post_id); |
| 5687 | 5179 | set_transient($previous_url_key, $current_url, DAY_IN_SECONDS); |
| @@ -5790,9 +5282,8 @@ | ||
| 5790 | 5282 | // ADD ACF FIELDS SUPPORT (matches ajax_mxchat_process_selected_content behavior) |
| 5791 | 5283 | $acf_fields = $this->mxchat_get_acf_fields_for_post($post_id); |
| 5792 | 5284 | if (!empty($acf_fields)) { |
| 5793 | 5285 | $acf_content_parts = array(); |
| 5794 | - $pdf_attachment_ids = array(); | |
| 5795 | 5286 | |
| 5796 | 5287 | foreach ($acf_fields as $field_name => $field_value) { |
| 5797 | 5288 | $formatted_value = $this->mxchat_format_acf_field_value($field_value, $field_name, $post_id); |
| 5798 | 5289 | if (!empty($formatted_value)) { |
| @@ -5799,44 +5290,13 @@ | ||
| 5799 | 5290 | // Convert field name to readable label |
| 5800 | 5291 | $field_label = ucwords(str_replace(['_', '-'], ' ', $field_name)); |
| 5801 | 5292 | $acf_content_parts[] = $field_label . ": " . $formatted_value; |
| 5802 | 5293 | } |
| 5803 | - | |
| 5804 | - $this->mxchat_collect_pdf_attachment_ids_from_acf_value($field_value, $pdf_attachment_ids); | |
| 5805 | 5294 | } |
| 5806 | 5295 | |
| 5807 | 5296 | if (!empty($acf_content_parts)) { |
| 5808 | 5297 | $final_content .= "\n\n" . implode("\n", $acf_content_parts); |
| 5809 | 5298 | } |
| 5810 | - | |
| 5811 | - // Gate the auto-sync PDF-extraction loop behind an opt-in option. | |
| 5812 | - // Mirrors the per-batch checkbox the manual content selector has; the | |
| 5813 | - // 25 MB size cap lives in the shared extractor so it applies in both | |
| 5814 | - // paths regardless. Default OFF — re-parsing every ACF PDF on every | |
| 5815 | - // editor save is expensive and most sites don't want it. | |
| 5816 | - $autosync_extract_acf_pdfs = get_option('mxchat_auto_sync_acf_pdfs', '0') === '1'; | |
| 5817 | - if ($autosync_extract_acf_pdfs && !empty($pdf_attachment_ids)) { | |
| 5818 | - $pdf_attachment_ids = array_unique(array_filter(array_map('intval', $pdf_attachment_ids))); | |
| 5819 | - $pdf_sections = array(); | |
| 5820 | - foreach ($pdf_attachment_ids as $att_id) { | |
| 5821 | - $pdf_text = $this->mxchat_extract_pdf_text_by_attachment_id($att_id); | |
| 5822 | - if (!empty($pdf_text)) { | |
| 5823 | - $pdf_title = get_the_title($att_id); | |
| 5824 | - $pdf_url = wp_get_attachment_url($att_id); | |
| 5825 | - $header = 'PDF Attachment'; | |
| 5826 | - if (!empty($pdf_title)) { | |
| 5827 | - $header .= ': ' . $pdf_title; | |
| 5828 | - } | |
| 5829 | - if (!empty($pdf_url)) { | |
| 5830 | - $header .= ' (' . $pdf_url . ')'; | |
| 5831 | - } | |
| 5832 | - $pdf_sections[] = $header . "\n" . $pdf_text; | |
| 5833 | - } | |
| 5834 | - } | |
| 5835 | - if (!empty($pdf_sections)) { | |
| 5836 | - $final_content .= "\n\nAttached PDFs:\n" . implode("\n\n", $pdf_sections); | |
| 5837 | - } | |
| 5838 | - } | |
| 5839 | 5299 | } |
| 5840 | 5300 | |
| 5841 | 5301 | // ADD CUSTOM POST META SUPPORT (whitelisted non-ACF meta fields) |
| 5842 | 5302 | $custom_meta = $this->mxchat_get_whitelisted_post_meta($post_id); |
| @@ -5943,14 +5403,12 @@ | ||
| 5943 | 5403 | if (!$should_sync) { |
| 5944 | 5404 | return; |
| 5945 | 5405 | } |
| 5946 | 5406 | |
| 5947 | - // Resolve the pre-trash URL. wp_trash_post renames the slug with "__trashed" before firing | |
| 5948 | - // this hook, so get_permalink() here would return the trashed URL and md5() would miss the | |
| 5949 | - // real vector IDs stored under the original URL. | |
| 5950 | - $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); | |
| 5951 | 5409 | if (!$source_url) { |
| 5952 | - //error_log('MXChat: Failed to resolve source URL for post ' . $post_id); | |
| 5410 | + //error_log('MXChat: Failed to get permalink for post ' . $post_id); | |
| 5953 | 5411 | return; |
| 5954 | 5412 | } |
| 5955 | 5413 | |
| 5956 | 5414 | // Use chunk-aware deletion (handles both chunked and non-chunked content) |
| @@ -5958,36 +5416,56 @@ | ||
| 5958 | 5416 | |
| 5959 | 5417 | if (is_wp_error($delete_result)) { |
| 5960 | 5418 | //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message()); |
| 5961 | 5419 | } |
| 5420 | +} | |
| 5962 | 5421 | |
| 5963 | - delete_transient('mxchat_prev_url_' . $post_id); | |
| 5964 | - delete_transient('mxchat_prev_status_' . $post_id); | |
| 5965 | -} | |
| 5966 | 5422 | |
| 5967 | -/** | |
| 5968 | - * Resolve the source URL for a post being trashed/deleted. | |
| 5969 | - * | |
| 5970 | - * Why: wp_trash_post appends "__trashed" to the slug before the wp_trash_post action fires, so | |
| 5971 | - * get_permalink() returns a URL whose md5() won't match the vector IDs stored in Pinecone or | |
| 5972 | - * the source_url rows in the WP DB. Prefer the URL captured by mxchat_store_pre_update_status | |
| 5973 | - * (runs on pre_post_update, before the rename); fall back to stripping the __trashed suffix. | |
| 5974 | - */ | |
| 5975 | -private function mxchat_resolve_pre_trash_url($post_id) { | |
| 5976 | - $previous_url = get_transient('mxchat_prev_url_' . $post_id); | |
| 5977 | - if (!empty($previous_url)) { | |
| 5978 | - return $previous_url; | |
| 5979 | - } | |
| 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'] ?? ''; | |
| 5980 | 5429 | |
| 5981 | - $current = get_permalink($post_id); | |
| 5982 | - if (!$current) { | |
| 5983 | - return ''; | |
| 5984 | - } | |
| 5985 | - return preg_replace('#__trashed(/?)$#', '$1', $current); | |
| 5986 | -} | |
| 5430 | + if (empty($host) || empty($api_key)) { | |
| 5431 | + //error_log('MXChat: Pinecone deletion failed - missing configuration'); | |
| 5432 | + return false; | |
| 5433 | + } | |
| 5987 | 5434 | |
| 5435 | + $api_endpoint = "https://{$host}/vectors/delete"; | |
| 5436 | + $vector_id = md5($source_url); | |
| 5988 | 5437 | |
| 5438 | + $request_body = array( | |
| 5439 | + 'ids' => array($vector_id) | |
| 5440 | + ); | |
| 5989 | 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 | + | |
| 5990 | 5468 | public function mxchat_handle_product_change($post_id, $post, $update) { |
| 5991 | 5469 | if ($post->post_type !== 'product') { |
| 5992 | 5470 | return; |
| 5993 | 5471 | } |
| @@ -6139,18 +5617,28 @@ | ||
| 6139 | 5617 | if (get_post_type($post_id) !== 'product') { |
| 6140 | 5618 | return; |
| 6141 | 5619 | } |
| 6142 | 5620 | |
| 6143 | - $source_url = $this->mxchat_resolve_pre_trash_url($post_id); | |
| 6144 | - if (!$source_url) { | |
| 6145 | - return; | |
| 6146 | - } | |
| 5621 | + $source_url = get_permalink($post_id); | |
| 6147 | 5622 | |
| 6148 | - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks) | |
| 6149 | - 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'; | |
| 6150 | 5626 | |
| 6151 | - delete_transient('mxchat_prev_url_' . $post_id); | |
| 6152 | - 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 | + } | |
| 6153 | 5641 | } |
| 6154 | 5642 | |
| 6155 | 5643 | /** |
| 6156 | 5644 | * Handle individual Pinecone content deletion |
| @@ -6854,16 +6342,16 @@ | ||
| 6854 | 6342 | wp_send_json_error('Unauthorized access'); |
| 6855 | 6343 | exit; |
| 6856 | 6344 | } |
| 6857 | 6345 | |
| 6858 | - $tag_input = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : ''; | |
| 6346 | + $tag_slug = isset($_POST['tag_slug']) ? sanitize_text_field($_POST['tag_slug']) : ''; | |
| 6859 | 6347 | $role_restriction = isset($_POST['role_restriction']) ? sanitize_text_field($_POST['role_restriction']) : 'public'; |
| 6860 | - | |
| 6861 | - if (empty($tag_input)) { | |
| 6862 | - wp_send_json_error('Please enter a tag name or slug'); | |
| 6348 | + | |
| 6349 | + if (empty($tag_slug)) { | |
| 6350 | + wp_send_json_error('Tag slug is required'); | |
| 6863 | 6351 | exit; |
| 6864 | 6352 | } |
| 6865 | - | |
| 6353 | + | |
| 6866 | 6354 | // Validate role restriction |
| 6867 | 6355 | $valid_roles = array_keys($this->mxchat_get_role_options()); |
| 6868 | 6356 | if (!in_array($role_restriction, $valid_roles)) { |
| 6869 | 6357 | wp_send_json_error('Invalid role restriction'); |
| @@ -6868,27 +6356,16 @@ | ||
| 6868 | 6356 | if (!in_array($role_restriction, $valid_roles)) { |
| 6869 | 6357 | wp_send_json_error('Invalid role restriction'); |
| 6870 | 6358 | exit; |
| 6871 | 6359 | } |
| 6872 | - | |
| 6873 | - // Resolve the tag by slug first, then fall back to its display name, so users can | |
| 6874 | - // enter either "premium-content" or "Premium Content". (plan b8bcf5 — the field is | |
| 6875 | - // labeled by name but previously validated by slug only, producing the confusing | |
| 6876 | - // "Tag does not exist in WordPress" error when a real tag's name was typed.) | |
| 6877 | - $term = get_term_by('slug', $tag_input, 'post_tag'); | |
| 6360 | + | |
| 6361 | + // Check if tag exists in WordPress | |
| 6362 | + $term = get_term_by('slug', $tag_slug, 'post_tag'); | |
| 6878 | 6363 | if (!$term) { |
| 6879 | - $term = get_term_by('name', $tag_input, 'post_tag'); | |
| 6880 | - } | |
| 6881 | - if (!$term) { | |
| 6882 | - wp_send_json_error('No tag with that name or slug exists yet. Create it under Posts → Tags first, then enter its name or slug.'); | |
| 6364 | + wp_send_json_error('Tag does not exist in WordPress'); | |
| 6883 | 6365 | exit; |
| 6884 | 6366 | } |
| 6885 | - | |
| 6886 | - // Always key the mapping by the RESOLVED slug — apply_role_restriction_to_post() | |
| 6887 | - // compares against each post's tag slugs, so the stored key must be a slug, | |
| 6888 | - // never the raw (possibly display-name) input. | |
| 6889 | - $tag_slug = $term->slug; | |
| 6890 | - | |
| 6367 | + | |
| 6891 | 6368 | // Get existing mappings |
| 6892 | 6369 | $mappings = get_option('mxchat_tag_role_mappings', array()); |
| 6893 | 6370 | |
| 6894 | 6371 | // Check if mapping already exists |
| @@ -7572,27 +7049,13 @@ | ||
| 7572 | 7049 | |
| 7573 | 7050 | $result = false; |
| 7574 | 7051 | $error_message = ''; |
| 7575 | 7052 | |
| 7576 | - // Read item directly from DB to get queue_id and preserve special chars in item_data | |
| 7577 | - // (POST round-trip through JS mangles characters like apostrophes in URLs) | |
| 7578 | - $db_item = $wpdb->get_row($wpdb->prepare( | |
| 7579 | - "SELECT queue_id, item_data FROM $table_name WHERE id = %d", | |
| 7580 | - $item_id | |
| 7581 | - )); | |
| 7582 | - $item_queue_id = $db_item ? $db_item->queue_id : ''; | |
| 7583 | - if ($db_item && !empty($db_item->item_data)) { | |
| 7584 | - $db_data = json_decode($db_item->item_data, true); | |
| 7585 | - if (is_array($db_data)) { | |
| 7586 | - $item_data = $db_data; | |
| 7587 | - } | |
| 7588 | - } | |
| 7589 | - | |
| 7590 | 7053 | switch ($item_type) { |
| 7591 | 7054 | case 'url': |
| 7592 | - $result = $this->mxchat_process_queue_url($item_data, $bot_id, $item_queue_id); | |
| 7055 | + $result = $this->mxchat_process_queue_url($item_data, $bot_id); | |
| 7593 | 7056 | break; |
| 7594 | - | |
| 7057 | + | |
| 7595 | 7058 | case 'pdf_page': |
| 7596 | 7059 | $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id); |
| 7597 | 7060 | break; |
| 7598 | 7061 | |
| @@ -7600,37 +7063,11 @@ | ||
| 7600 | 7063 | throw new Exception('Unknown item type: ' . $item_type); |
| 7601 | 7064 | } |
| 7602 | 7065 | |
| 7603 | 7066 | if (is_wp_error($result)) { |
| 7604 | - $error_code = $result->get_error_code(); | |
| 7605 | - // Content errors (empty page, sanitization) are permanent — retrying won't help | |
| 7606 | - $permanent_codes = array('empty_page', 'empty_after_sanitization', 'no_api_key', 'page_not_found'); | |
| 7607 | - if (in_array($error_code, $permanent_codes)) { | |
| 7608 | - // Mark as permanently failed — set attempts = max_attempts so it won't be retried | |
| 7609 | - $current_item = $wpdb->get_row($wpdb->prepare( | |
| 7610 | - "SELECT max_attempts FROM $table_name WHERE id = %d", $item_id | |
| 7611 | - )); | |
| 7612 | - $wpdb->update( | |
| 7613 | - $table_name, | |
| 7614 | - array( | |
| 7615 | - 'status' => 'failed', | |
| 7616 | - 'error_message' => $result->get_error_message(), | |
| 7617 | - 'attempts' => $current_item ? $current_item->max_attempts : 3 | |
| 7618 | - ), | |
| 7619 | - array('id' => $item_id), | |
| 7620 | - array('%s', '%s', '%d'), | |
| 7621 | - array('%d') | |
| 7622 | - ); | |
| 7623 | - wp_send_json_error(array( | |
| 7624 | - 'message' => $result->get_error_message(), | |
| 7625 | - 'permanent_failure' => true, | |
| 7626 | - 'item_id' => $item_id | |
| 7627 | - )); | |
| 7628 | - return; | |
| 7629 | - } | |
| 7630 | 7067 | throw new Exception($result->get_error_message()); |
| 7631 | 7068 | } |
| 7632 | - | |
| 7069 | + | |
| 7633 | 7070 | if ($result === false) { |
| 7634 | 7071 | throw new Exception('Processing returned false - item may be empty or invalid'); |
| 7635 | 7072 | } |
| 7636 | 7073 | |
| @@ -7706,9 +7143,9 @@ | ||
| 7706 | 7143 | |
| 7707 | 7144 | /** |
| 7708 | 7145 | * Process a URL from the queue |
| 7709 | 7146 | */ |
| 7710 | -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') { | |
| 7711 | 7148 | $url = isset($item_data['url']) ? $item_data['url'] : ''; |
| 7712 | 7149 | |
| 7713 | 7150 | if (empty($url)) { |
| 7714 | 7151 | return new WP_Error('invalid_url', 'URL is empty'); |
| @@ -7756,11 +7193,11 @@ | ||
| 7756 | 7193 | |
| 7757 | 7194 | // Fetch URL content (fallback for non-products or when WooCommerce extraction fails) |
| 7758 | 7195 | $is_likely_pdf = (strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION)) === 'pdf'); |
| 7759 | 7196 | $response = wp_remote_get($url, array( |
| 7760 | - 'timeout' => $is_likely_pdf ? 120 : 30, | |
| 7197 | + 'timeout' => $is_likely_pdf ? 60 : 30, | |
| 7761 | 7198 | 'redirection' => 5, |
| 7762 | - 'user-agent' => mxchat_ingest_user_agent(), | |
| 7199 | + 'user-agent' => 'MxChat/1.0' | |
| 7763 | 7200 | )); |
| 7764 | 7201 | |
| 7765 | 7202 | if (is_wp_error($response)) { |
| 7766 | 7203 | return $response; |
| @@ -7767,14 +7204,14 @@ | ||
| 7767 | 7204 | } |
| 7768 | 7205 | |
| 7769 | 7206 | $response_code = wp_remote_retrieve_response_code($response); |
| 7770 | 7207 | if ($response_code !== 200) { |
| 7771 | - return new WP_Error('http_error', 'HTTP ' . $response_code . ' error for: ' . $url); | |
| 7208 | + return new WP_Error('http_error', 'HTTP ' . $response_code . ' error'); | |
| 7772 | 7209 | } |
| 7773 | 7210 | |
| 7774 | - // 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 | |
| 7775 | 7212 | if ($this->mxchat_is_pdf_url($url, $response)) { |
| 7776 | - 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); | |
| 7777 | 7214 | } |
| 7778 | 7215 | |
| 7779 | 7216 | $html = wp_remote_retrieve_body($response); |
| 7780 | 7217 | |
| @@ -7804,89 +7241,11 @@ | ||
| 7804 | 7241 | return $result; |
| 7805 | 7242 | } |
| 7806 | 7243 | |
| 7807 | 7244 | /** |
| 7808 | - * Expand a PDF URL into per-page queue items using the standard PDF pipeline. | |
| 7809 | - * Called when a sitemap URL turns out to be a PDF — downloads, parses page count, | |
| 7810 | - * 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. | |
| 7811 | 7247 | */ |
| 7812 | -private function mxchat_expand_pdf_to_queue($pdf_url, $response, $bot_id = 'default', $queue_id = '') { | |
| 7813 | - set_time_limit(120); // PDFs need extra time for download + parsing | |
| 7814 | - | |
| 7815 | - $upload_dir = wp_upload_dir(); | |
| 7816 | - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf'); | |
| 7817 | - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename; | |
| 7818 | - | |
| 7819 | - $response_body = wp_remote_retrieve_body($response); | |
| 7820 | - if (empty($response_body)) { | |
| 7821 | - return new WP_Error('empty_pdf', 'Empty PDF response for: ' . $pdf_url); | |
| 7822 | - } | |
| 7823 | - | |
| 7824 | - if (!wp_mkdir_p(dirname($pdf_path))) { | |
| 7825 | - return new WP_Error('dir_error', 'Failed to create upload directory'); | |
| 7826 | - } | |
| 7827 | - | |
| 7828 | - file_put_contents($pdf_path, $response_body); | |
| 7829 | - | |
| 7830 | - if (!file_exists($pdf_path)) { | |
| 7831 | - return new WP_Error('save_error', 'Failed to save PDF file'); | |
| 7832 | - } | |
| 7833 | - | |
| 7834 | - try { | |
| 7835 | - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path); | |
| 7836 | - | |
| 7837 | - if ($total_pages === false || $total_pages < 1) { | |
| 7838 | - wp_delete_file($pdf_path); | |
| 7839 | - return new WP_Error('no_pages', 'PDF has no pages: ' . $pdf_url); | |
| 7840 | - } | |
| 7841 | - | |
| 7842 | - // Build per-page items identical to mxchat_handle_pdf_for_knowledge_base | |
| 7843 | - $pages = array(); | |
| 7844 | - for ($i = 1; $i <= $total_pages; $i++) { | |
| 7845 | - $pages[] = array( | |
| 7846 | - 'pdf_path' => $pdf_path, | |
| 7847 | - 'pdf_url' => $pdf_url, | |
| 7848 | - 'page_number' => $i, | |
| 7849 | - 'total_pages' => $total_pages | |
| 7850 | - ); | |
| 7851 | - } | |
| 7852 | - | |
| 7853 | - // Add pdf_page items to the SAME queue so the JS picks them up automatically | |
| 7854 | - if (!empty($queue_id)) { | |
| 7855 | - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id); | |
| 7856 | - } else { | |
| 7857 | - // Fallback: create a new PDF queue (shouldn't happen in sitemap flow) | |
| 7858 | - $new_queue_id = 'pdf_' . md5($pdf_url . time()); | |
| 7859 | - $queued_count = $this->mxchat_add_to_queue($new_queue_id, 'pdf_page', $pages, $bot_id); | |
| 7860 | - $this->mxchat_set_queue_meta($new_queue_id, 'source_url', $pdf_url); | |
| 7861 | - $this->mxchat_set_queue_meta($new_queue_id, 'queue_type', 'pdf'); | |
| 7862 | - $this->mxchat_set_queue_meta($new_queue_id, 'total_items', $total_pages); | |
| 7863 | - $this->mxchat_set_queue_meta($new_queue_id, 'bot_id', $bot_id); | |
| 7864 | - $this->mxchat_set_queue_meta($new_queue_id, 'pdf_path', $pdf_path); | |
| 7865 | - $this->mxchat_set_queue_meta($new_queue_id, 'created_at', current_time('mysql')); | |
| 7866 | - } | |
| 7867 | - | |
| 7868 | - if ($queued_count === 0) { | |
| 7869 | - wp_delete_file($pdf_path); | |
| 7870 | - return new WP_Error('queue_error', 'Failed to add PDF pages to queue'); | |
| 7871 | - } | |
| 7872 | - | |
| 7873 | - // Return true so the original URL item is marked complete | |
| 7874 | - // The new pdf_page items will be processed in subsequent batches | |
| 7875 | - return true; | |
| 7876 | - | |
| 7877 | - } catch (Exception $e) { | |
| 7878 | - if (file_exists($pdf_path)) { | |
| 7879 | - wp_delete_file($pdf_path); | |
| 7880 | - } | |
| 7881 | - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage()); | |
| 7882 | - } | |
| 7883 | -} | |
| 7884 | - | |
| 7885 | -/** | |
| 7886 | - * Legacy: Process a PDF URL inline during sitemap queue processing. | |
| 7887 | - * @deprecated Use mxchat_expand_pdf_to_queue instead — kept for reference only. | |
| 7888 | - */ | |
| 7889 | 7248 | private function mxchat_process_pdf_url_inline($pdf_url, $response, $api_key, $bot_id = 'default') { |
| 7890 | 7249 | set_time_limit(120); // PDFs need more time — downloading + parsing all pages |
| 7891 | 7250 | |
| 7892 | 7251 | $upload_dir = wp_upload_dir(); |
| @@ -7920,24 +7279,21 @@ | ||
| 7920 | 7279 | return new WP_Error('no_pages', 'PDF has no pages'); |
| 7921 | 7280 | } |
| 7922 | 7281 | |
| 7923 | 7282 | $processed = 0; |
| 7924 | - $skipped_pages = array(); | |
| 7925 | 7283 | |
| 7926 | 7284 | for ($i = 0; $i < $total_pages; $i++) { |
| 7927 | - $page_num = $i + 1; | |
| 7928 | 7285 | $text = $pages[$i]->getText(); |
| 7929 | 7286 | if (empty($text)) { |
| 7930 | - $skipped_pages[] = 'Page ' . $page_num . ': No text could be extracted — page may contain only images, links, or non-standard encoding'; | |
| 7931 | 7287 | continue; |
| 7932 | 7288 | } |
| 7933 | 7289 | |
| 7934 | 7290 | $sanitized = $this->mxchat_sanitize_content_for_api($text); |
| 7935 | 7291 | if (empty($sanitized)) { |
| 7936 | - $skipped_pages[] = 'Page ' . $page_num . ': Text was extracted but contained only special characters, control codes, or unsupported content'; | |
| 7937 | 7292 | continue; |
| 7938 | 7293 | } |
| 7939 | 7294 | |
| 7295 | + $page_num = $i + 1; | |
| 7940 | 7296 | $metadata = array( |
| 7941 | 7297 | 'document_type' => 'pdf', |
| 7942 | 7298 | 'total_pages' => $total_pages, |
| 7943 | 7299 | 'current_page' => $page_num, |
| @@ -7961,12 +7317,8 @@ | ||
| 7961 | 7317 | |
| 7962 | 7318 | // Clean up the temp PDF file |
| 7963 | 7319 | wp_delete_file($pdf_path); |
| 7964 | 7320 | |
| 7965 | - if (!empty($skipped_pages)) { | |
| 7966 | - error_log('MxChat PDF: Skipped ' . count($skipped_pages) . ' of ' . $total_pages . ' pages: ' . implode('; ', $skipped_pages)); | |
| 7967 | - } | |
| 7968 | - | |
| 7969 | 7321 | return $processed > 0 ? true : false; |
| 7970 | 7322 | |
| 7971 | 7323 | } catch (Exception $e) { |
| 7972 | 7324 | if (file_exists($pdf_path)) { |
| @@ -8131,17 +7483,18 @@ | ||
| 8131 | 7483 | return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF'); |
| 8132 | 7484 | } |
| 8133 | 7485 | |
| 8134 | 7486 | $text = $pages[$page_number - 1]->getText(); |
| 8135 | - | |
| 7487 | + | |
| 8136 | 7488 | if (empty($text)) { |
| 8137 | - 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; | |
| 8138 | 7491 | } |
| 8139 | - | |
| 7492 | + | |
| 8140 | 7493 | $sanitized = $this->mxchat_sanitize_content_for_api($text); |
| 8141 | - | |
| 7494 | + | |
| 8142 | 7495 | if (empty($sanitized)) { |
| 8143 | - 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; | |
| 8144 | 7497 | } |
| 8145 | 7498 | |
| 8146 | 7499 | // Create metadata |
| 8147 | 7500 | $metadata = array( |
| @@ -8245,16 +7598,17 @@ | ||
| 8245 | 7598 | |
| 8246 | 7599 | // Calculate percentage |
| 8247 | 7600 | $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0; |
| 8248 | 7601 | |
| 8249 | - // Get failed items details (include all failed items, not just those that exhausted retries) | |
| 7602 | + // Get failed items details | |
| 8250 | 7603 | $failed_items = array(); |
| 8251 | 7604 | if ($failed > 0) { |
| 8252 | 7605 | $failed_items = $wpdb->get_results($wpdb->prepare( |
| 8253 | - "SELECT item_type, item_data, error_message, attempts | |
| 8254 | - FROM $table_name | |
| 8255 | - WHERE queue_id = %s | |
| 7606 | + "SELECT item_type, item_data, error_message, attempts | |
| 7607 | + FROM $table_name | |
| 7608 | + WHERE queue_id = %s | |
| 8256 | 7609 | AND status = 'failed' |
| 7610 | + AND attempts >= max_attempts | |
| 8257 | 7611 | ORDER BY id DESC |
| 8258 | 7612 | LIMIT 50", |
| 8259 | 7613 | $queue_id |
| 8260 | 7614 | )); |