PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.5.2
MxChat – AI Chatbot & Content Generation for WordPress v2.5.2
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / includes / class-mxchat-word-handler.php

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

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