PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.3
MxChat – AI Chatbot & Content Generation for WordPress v3.2.3
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
mxchat-basic / includes / class-mxchat-word-handler.php

class-mxchat-word-handler.php in MxChat – AI Chatbot & Content Generation for WordPress 3.2.3, at includes/class-mxchat-word-handler.php

377 lines 13.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Word document handler and processor for MXChat
4 * Can be directly bundled in WordPress plugins
5 */
6 class MXChat_Word_Handler {
7 private $temp_dir;
8 private $options;
9
10 public function __construct($options) {
11 $this->options = $options;
12 $this->temp_dir = wp_upload_dir()['path'];
13 }
14
15 /**
16 * Handle Word document upload and processing
17 */
18 public function mxchat_handle_word_upload() {
19 check_ajax_referer('mxchat_chat_nonce', 'nonce');
20
21 if (!isset($_FILES['word_file']) || !isset($_POST['session_id'])) {
22 wp_send_json_error(esc_html__('Missing required parameters.', 'mxchat'));
23 return;
24 }
25
26 // SECURITY FIX: Check if Word uploads are enabled in settings
27 $options = get_option('mxchat_options', array());
28 $show_word_button = isset($options['show_word_upload_button']) ? $options['show_word_upload_button'] : 'on';
29
30 if ($show_word_button !== 'on') {
31 wp_send_json_error(esc_html__('Word document uploads are currently disabled.', 'mxchat'));
32 return;
33 }
34
35 $file = $_FILES['word_file'];
36 $session_id = sanitize_text_field($_POST['session_id']);
37 $original_filename = sanitize_text_field($file['name']);
38
39 // Update session owner if it changed (e.g. IP changed due to network switch)
40 $current_user_identifier = MxChat_User::mxchat_get_user_identifier();
41 $session_owner = get_option("mxchat_session_owner_{$session_id}");
42
43 if (!$session_owner || $session_owner !== $current_user_identifier) {
44 update_option("mxchat_session_owner_{$session_id}", $current_user_identifier, 'no');
45 }
46
47 // Check file type
48 $allowed_types = array(
49 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
50 );
51 $file_type = wp_check_filetype($file['name'], $allowed_types);
52
53 if (!$file_type['type']) {
54 wp_send_json_error(esc_html__('Invalid file type. Only .docx files are allowed.', 'mxchat'));
55 return;
56 }
57
58 // SECURITY FIX: Generate random filename without exposing session_id
59 $random_string = wp_generate_password(20, false, false); // 20 char alphanumeric string
60 $word_filename = 'mxchat_word_' . $random_string . '_' . time() . '.docx';
61 $word_path = $this->temp_dir . '/' . $word_filename;
62
63 if (!move_uploaded_file($file['tmp_name'], $word_path)) {
64 wp_send_json_error(esc_html__('Failed to upload file.', 'mxchat'));
65 return;
66 }
67
68 $this->mxchat_clear_word_transients($session_id);
69
70 // Process the document
71 $embeddings = $this->mxchat_process_word_document($word_path);
72
73 if ($embeddings === false || empty($embeddings)) {
74 unlink($word_path);
75 $error_message = $this->options['word_intent_error_text'] ??
76 esc_html__('The uploaded document appears to be empty or contains unsupported content.', 'mxchat');
77 wp_send_json_error($error_message);
78 return;
79 }
80
81 // Store the mapping between session and the random filename
82 set_transient('mxchat_word_url_' . $session_id, $word_path, HOUR_IN_SECONDS);
83 set_transient('mxchat_word_filename_' . $session_id, $original_filename, HOUR_IN_SECONDS);
84 set_transient('mxchat_word_embeddings_' . $session_id, $embeddings, HOUR_IN_SECONDS);
85 set_transient('mxchat_include_word_in_context_' . $session_id, true, HOUR_IN_SECONDS);
86
87 $success_message = $this->options['pdf_intent_success_text'] ??
88 __("I've processed the document. What questions do you have about it?", 'mxchat');
89
90 wp_send_json_success([
91 'message' => $success_message,
92 'filename' => $original_filename
93 ]);
94 }
95
96 /**
97 * Process Word document and generate embeddings
98 */
99 private function mxchat_process_word_document($file_path) {
100 // Get the maximum number of pages allowed from admin settings
101 $max_pages = isset($this->options['pdf_max_pages']) ? intval($this->options['pdf_max_pages']) : 69; // Use same setting as PDF
102
103 try {
104 $zip = new ZipArchive();
105 if ($zip->open($file_path) !== true) {
106 return false;
107 }
108
109 // Extract main document content
110 $content = $zip->getFromName('word/document.xml');
111 $zip->close();
112
113 if ($content === false) {
114 return false;
115 }
116
117 // Clean up the content
118 $text = $this->mxchat_clean_word_content($content);
119
120 // Count pages (roughly estimate based on paragraphs)
121 $paragraphs = explode("\n\n", $text);
122 $estimated_pages = ceil(count($paragraphs) / 3); // Assume ~3 paragraphs per page
123
124 if ($estimated_pages > $max_pages) {
125 return esc_html__('too_many_pages', 'mxchat');
126 }
127
128 // Split into chunks and continue processing...
129 $chunks = $this->mxchat_split_word_into_chunks($text, 1000);
130
131 $embeddings = [];
132 foreach ($chunks as $chunk_number => $chunk) {
133 if (empty(trim($chunk))) {
134 continue;
135 }
136
137 $embedding = $this->mxchat_generate_embedding_word(
138 esc_html__('Chunk ', 'mxchat') . ($chunk_number + 1) . ': ' . $chunk,
139 $this->options['api_key']
140 );
141
142 if ($embedding) {
143 $embeddings[] = [
144 'chunk_number' => $chunk_number + 1,
145 'embedding' => $embedding,
146 'text' => $chunk,
147 ];
148 }
149 }
150
151 return $embeddings;
152
153 } catch (Exception $e) {
154 return false;
155 }
156 }
157 /**
158 * Clean Word XML content
159 */
160 private function mxchat_clean_word_content($content) {
161 // Remove XML namespaces
162 $content = preg_replace('/xmlns[^=]*="[^"]*"/i', '', $content);
163
164 // Convert Word XML elements to text
165 $content = str_replace('</w:p>', "\n", $content);
166 $content = str_replace('</w:tr>', "\n", $content);
167
168 // Strip remaining XML tags
169 $content = strip_tags($content);
170
171 // Clean up whitespace
172 $content = preg_replace('/\s+/', ' ', $content);
173 $content = preg_replace('/\n\s*\n/', "\n\n", $content);
174
175 return trim($content);
176 }
177
178 /**
179 * Split text into manageable chunks
180 */
181 private function mxchat_split_word_into_chunks($text, $chunk_size) {
182 $chunks = [];
183 $paragraphs = explode("\n\n", $text);
184
185 $current_chunk = '';
186 foreach ($paragraphs as $paragraph) {
187 if (strlen($current_chunk) + strlen($paragraph) > $chunk_size) {
188 if (!empty($current_chunk)) {
189 $chunks[] = trim($current_chunk);
190 }
191 $current_chunk = $paragraph;
192 } else {
193 $current_chunk .= (!empty($current_chunk) ? "\n\n" : '') . $paragraph;
194 }
195 }
196
197 if (!empty($current_chunk)) {
198 $chunks[] = trim($current_chunk);
199 }
200
201 return $chunks;
202 }
203
204 /**
205 * Remove Word document and clean up transients
206 */
207 public function mxchat_handle_word_remove() {
208 check_ajax_referer('mxchat_chat_nonce', 'nonce');
209
210 if (empty($_POST['session_id'])) {
211 wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
212 return;
213 }
214
215 $session_id = sanitize_text_field($_POST['session_id']);
216 $word_path = get_transient('mxchat_word_url_' . $session_id);
217
218 if ($word_path && file_exists($word_path)) {
219 unlink($word_path);
220 }
221
222 $this->mxchat_clear_word_transients($session_id);
223
224 wp_send_json_success([
225 'message' => esc_html__('Document removed successfully.', 'mxchat')
226 ]);
227 }
228
229 /**
230 * Clear all Word-related transients
231 */
232 private function mxchat_clear_word_transients($session_id) {
233 delete_transient('mxchat_word_url_' . $session_id);
234 delete_transient('mxchat_word_filename_' . $session_id);
235 delete_transient('mxchat_word_embeddings_' . $session_id);
236 delete_transient('mxchat_include_word_in_context_' . $session_id);
237 }
238
239 /**
240 * Find relevant chunks from the Word document
241 */
242 public function mxchat_find_relevant_word_chunks($query_embedding, $embeddings) {
243 $most_relevant = null;
244 $highest_similarity = -INF;
245
246 foreach ($embeddings as $chunk_data) {
247 $similarity = $this->mxchat_calculate_cosine_similarity_word($query_embedding, $chunk_data['embedding']);
248
249 if ($similarity > $highest_similarity) {
250 $highest_similarity = $similarity;
251 $most_relevant = $chunk_data['chunk_number'];
252 }
253 }
254
255 if (!is_null($most_relevant)) {
256 $chunk_numbers = range(
257 max(1, $most_relevant - 1),
258 min(count($embeddings), $most_relevant + 1)
259 );
260 return array_filter($embeddings, function ($chunk) use ($chunk_numbers) {
261 return in_array($chunk['chunk_number'], $chunk_numbers);
262 });
263 }
264
265 return [];
266 }
267
268 /**
269 * Handle Word document discussion similar to PDF discussion
270 */
271 public function mxchat_handle_word_discussion($message, $user_id, $session_id) {
272 // Get stored embeddings for the session
273 $embeddings = get_transient('mxchat_word_embeddings_' . $session_id);
274 $word_path = get_transient('mxchat_word_url_' . $session_id);
275
276 if (!$embeddings || !$word_path) {
277 $trigger_text = $this->options['word_intent_trigger_text'] ??
278 __("Please upload a Word document (.docx) that you'd like to discuss.", 'mxchat');
279 set_transient('mxchat_waiting_for_word_' . $session_id, true, HOUR_IN_SECONDS);
280 $this->fallbackResponse['text'] = $trigger_text;
281 return;
282 }
283
284 // Set context flag for including Word content in conversation
285 set_transient('mxchat_include_word_in_context_' . $session_id, true, HOUR_IN_SECONDS);
286 $this->fallbackResponse['text'] = ''; // Proceed without additional message
287 }
288
289
290 private function mxchat_generate_embedding_word($text, $api_key) {
291 $endpoint = 'https://api.openai.com/v1/embeddings';
292
293 $body = wp_json_encode([
294 'input' => $text,
295 'model' => 'text-embedding-ada-002'
296 ]);
297
298 $args = [
299 'body' => $body,
300 'headers' => [
301 'Content-Type' => 'application/json',
302 'Authorization' => 'Bearer ' . $api_key,
303 ],
304 'timeout' => 60,
305 'redirection' => 5,
306 'blocking' => true,
307 'httpversion' => '1.0',
308 'sslverify' => true,
309 ];
310
311 $response = wp_remote_post($endpoint, $args);
312
313 if (is_wp_error($response)) {
314 return null;
315 }
316
317 $response_body = json_decode(wp_remote_retrieve_body($response), true);
318
319 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
320 return $response_body['data'][0]['embedding'];
321 } else {
322 return null;
323 }
324 }
325
326
327 private function mxchat_calculate_cosine_similarity_word($vectorA, $vectorB) {
328 if (!is_array($vectorA) || !is_array($vectorB) || empty($vectorA) || empty($vectorB)) {
329 return 0;
330 }
331
332 $dotProduct = array_sum(array_map(function ($a, $b) {
333 return $a * $b;
334 }, $vectorA, $vectorB));
335 $normA = sqrt(array_sum(array_map(function ($a) {
336 return $a * $a;
337 }, $vectorA)));
338 $normB = sqrt(array_sum(array_map(function ($b) {
339 return $b * $b;
340 }, $vectorB)));
341
342 if ($normA == 0 || $normB == 0) {
343 return 0;
344 }
345
346 return $dotProduct / ($normA * $normB);
347 }
348
349 /**
350 * Check the status of a Word document for the current session
351 */
352 public function mxchat_check_word_status() {
353 check_ajax_referer('mxchat_chat_nonce', 'nonce');
354
355 if (empty($_POST['session_id'])) {
356 wp_send_json_error(esc_html__('Session ID missing.', 'mxchat'));
357 return;
358 }
359
360 $session_id = sanitize_text_field($_POST['session_id']);
361 $word_path = get_transient('mxchat_word_url_' . $session_id);
362 $filename = get_transient('mxchat_word_filename_' . $session_id);
363
364 if ($word_path && file_exists($word_path) && $filename) {
365 wp_send_json_success([
366 'has_word' => true,
367 'filename' => $filename
368 ]);
369 } else {
370 wp_send_json_success([
371 'has_word' => false
372 ]);
373 }
374 }
375
376
377 }