| @@ -42,12 +42,12 @@ | ||
| 42 | 42 | $original_filename = sanitize_text_field($file['name']); |
| 43 | 43 | |
| 44 | 44 | // Update session owner if it changed (e.g. IP changed due to network switch) |
| 45 | 45 | $current_user_identifier = MxChat_User::mxchat_get_user_identifier(); |
| 46 | - $session_owner = get_option("mxchat_session_owner_{$session_id}"); | |
| 46 | + $session_owner = MxChat_Session_Store::get($session_id, 'owner'); | |
| 47 | 47 | |
| 48 | 48 | if (!$session_owner || $session_owner !== $current_user_identifier) { |
| 49 | - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no'); | |
| 49 | + MxChat_Session_Store::set($session_id, 'owner', $current_user_identifier); | |
| 50 | 50 | } |
| 51 | 51 | |
| 52 | 52 | // Check file type |
| 53 | 53 | $allowed_types = array( |
| @@ -98,8 +98,33 @@ | ||
| 98 | 98 | ]); |
| 99 | 99 | } |
| 100 | 100 | |
| 101 | 101 | /** |
| 102 | + * Extract plain text from a .docx file. THE one .docx parser — the | |
| 103 | + * visitor-facing toolbar upload (via mxchat_process_word_document) and the | |
| 104 | + * admin knowledge importer (plan 0485e5) both call this; do not fork a | |
| 105 | + * second copy. Returns the cleaned text, or false on unreadable/empty | |
| 106 | + * files (not a Zip, no word/document.xml, no text content). | |
| 107 | + */ | |
| 108 | + public static function extract_docx_text($file_path) { | |
| 109 | + try { | |
| 110 | + $zip = new ZipArchive(); | |
| 111 | + if ($zip->open($file_path) !== true) { | |
| 112 | + return false; | |
| 113 | + } | |
| 114 | + $content = $zip->getFromName('word/document.xml'); | |
| 115 | + $zip->close(); | |
| 116 | + if ($content === false) { | |
| 117 | + return false; | |
| 118 | + } | |
| 119 | + $text = self::clean_word_xml($content); | |
| 120 | + return $text === '' ? false : $text; | |
| 121 | + } catch (Exception $e) { | |
| 122 | + return false; | |
| 123 | + } | |
| 124 | + } | |
| 125 | + | |
| 126 | + /** | |
| 102 | 127 | * Process Word document and generate embeddings |
| 103 | 128 | */ |
| 104 | 129 | private function mxchat_process_word_document($file_path) { |
| 105 | 130 | // Get the maximum number of pages allowed from admin settings |
| @@ -105,24 +130,13 @@ | ||
| 105 | 130 | // Get the maximum number of pages allowed from admin settings |
| 106 | 131 | $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69; // Use same setting as PDF |
| 107 | 132 | |
| 108 | 133 | try { |
| 109 | - $zip = new ZipArchive(); | |
| 110 | - if ($zip->open($file_path) !== true) { | |
| 134 | + $text = self::extract_docx_text($file_path); | |
| 135 | + if ($text === false) { | |
| 111 | 136 | return false; |
| 112 | 137 | } |
| 113 | 138 | |
| 114 | - // Extract main document content | |
| 115 | - $content = $zip->getFromName('word/document.xml'); | |
| 116 | - $zip->close(); | |
| 117 | - | |
| 118 | - if ($content === false) { | |
| 119 | - return false; | |
| 120 | - } | |
| 121 | - | |
| 122 | - // Clean up the content | |
| 123 | - $text = $this->mxchat_clean_word_content($content); | |
| 124 | - | |
| 125 | 139 | // Count pages (roughly estimate based on paragraphs) |
| 126 | 140 | $paragraphs = explode("\n\n", $text); |
| 127 | 141 | $estimated_pages = ceil(count($paragraphs) / 3); // Assume ~3 paragraphs per page |
| 128 | 142 | |
| @@ -159,25 +173,26 @@ | ||
| 159 | 173 | return false; |
| 160 | 174 | } |
| 161 | 175 | } |
| 162 | 176 | /** |
| 163 | - * Clean Word XML content | |
| 177 | + * Clean Word XML content (word/document.xml) to plain text. | |
| 178 | + * Static so both callers of extract_docx_text share one implementation. | |
| 164 | 179 | */ |
| 165 | - private function mxchat_clean_word_content($content) { | |
| 180 | + public static function clean_word_xml($content) { | |
| 166 | 181 | // Remove XML namespaces |
| 167 | 182 | $content = preg_replace('/xmlns[^=]*="[^"]*"/i', '', $content); |
| 168 | - | |
| 183 | + | |
| 169 | 184 | // Convert Word XML elements to text |
| 170 | 185 | $content = str_replace('</w:p>', "\n", $content); |
| 171 | 186 | $content = str_replace('</w:tr>', "\n", $content); |
| 172 | - | |
| 187 | + | |
| 173 | 188 | // Strip remaining XML tags |
| 174 | 189 | $content = strip_tags($content); |
| 175 | - | |
| 190 | + | |
| 176 | 191 | // Clean up whitespace |
| 177 | 192 | $content = preg_replace('/\s+/', ' ', $content); |
| 178 | 193 | $content = preg_replace('/\n\s*\n/', "\n\n", $content); |
| 179 | - | |
| 194 | + | |
| 180 | 195 | return trim($content); |
| 181 | 196 | } |
| 182 | 197 | |
| 183 | 198 | /** |
| @@ -227,11 +242,11 @@ | ||
| 227 | 242 | // history endpoint (plan-mxchat-20260731-d42bec). Possession of the session |
| 228 | 243 | // id is the credential, so this records/refreshes the owner rather than |
| 229 | 244 | // refusing a mismatch; see the fuller note in handle_pdf_remove(). |
| 230 | 245 | $current_user_identifier = MxChat_User::mxchat_get_user_identifier(); |
| 231 | - $session_owner = get_option("mxchat_session_owner_{$session_id}"); | |
| 246 | + $session_owner = MxChat_Session_Store::get($session_id, 'owner'); | |
| 232 | 247 | if (!$session_owner || $session_owner !== $current_user_identifier) { |
| 233 | - update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no'); | |
| 248 | + MxChat_Session_Store::set($session_id, 'owner', $current_user_identifier); | |
| 234 | 249 | } |
| 235 | 250 | |
| 236 | 251 | $word_path = get_transient('mxchat_word_url_' . $session_id); |
| 237 | 252 | |