PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.25
MxChat – AI Chatbot & Content Generation for WordPress v3.2.25
3.2.25 3.2.24 3.2.23 3.2.22 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 All 156 releases
← All changes | includes/class-mxchat-privacy.php +152 -36 3.2.19 → 3.2.25 View file →
@@ -69,8 +69,11 @@
69 69 'mxchat_history_',
70 70 'mxchat_lead_del_email_',
71 71 'mxchat_lead_del_name_',
72 72 'mxchat_lead_del_ts_',
73 + 'mxchat_lead_del_consent_',
74 + 'mxchat_lead_del_consent_label_',
75 + 'mxchat_lead_del_consent_at_',
73 76 // NOTE: mxchat_session_owner_ (b64b77) and mxchat_email_ /
74 77 // mxchat_name_ / mxchat_agent_name_ (5658f2) moved out of wp_options
75 78 // into the mxchat_sessions table. The eraser calls
76 79 // MxChat_Session_Store::delete_session(), which drops the row AND the
@@ -173,9 +176,13 @@
173 176
174 177 $page = max(1, (int) $page);
175 178 $offset = ($page - 1) * self::EXPORT_SESSIONS_PER_PAGE;
176 179
177 - $session_ids = self::get_session_ids($email_address, self::EXPORT_SESSIONS_PER_PAGE, $offset);
180 + // Every session held for the subject: conversations first, then the
181 + // pre-chat captures that never became one (fb8877). Computed whole on
182 + // each call and sliced, so the pages stay stable.
183 + $all_session_ids = self::get_export_session_ids($email_address);
184 + $session_ids = array_slice($all_session_ids, $offset, self::EXPORT_SESSIONS_PER_PAGE);
178 185
179 186 $items = array();
180 187 foreach ($session_ids as $sid) {
181 188 $rows = $wpdb->get_results($wpdb->prepare(
@@ -185,8 +192,16 @@
185 192 ORDER BY timestamp ASC, id ASC",
186 193 $sid
187 194 ));
188 195 if (empty($rows)) {
196 + // No conversation: a visitor who submitted the pre-chat form
197 + // and never sent a message. The plugin still holds their
198 + // email, name and consent record (shown on the Leads tab,
199 + // removed by the eraser), so the export must disclose them.
200 + $item = self::export_store_only_item($sid);
201 + if ($item !== null) {
202 + $items[] = $item;
203 + }
189 204 continue;
190 205 }
191 206
192 207 $emails = array();
@@ -225,8 +240,13 @@
225 240 if (!empty($identifiers)) {
226 241 $data[] = array('name' => __('Visitor identifier', 'mxchat'), 'value' => implode(', ', array_keys($identifiers)));
227 242 }
228 243
244 + // Lead-capture consent record (b062c4). If we hold a consent
245 + // decision about the subject, the export must show it — same
246 + // Article 15 parity rule as the click rows below.
247 + $data = array_merge($data, self::export_consent_fields($sid));
248 +
229 249 foreach ($rows as $row) {
230 250 $data[] = array(
231 251 'name' => sprintf('[%s] %s', $row->timestamp, self::role_label($row->role)),
232 252 'value' => $row->message,
@@ -234,49 +254,142 @@
234 254 }
235 255
236 256 // Link-click rows for this session (Article 15 parity with the
237 257 // eraser — same subject, same session set, plan 23c4a1).
238 - if (self::url_clicks_table_exists()) {
239 - $clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
240 - $clicks = $wpdb->get_results($wpdb->prepare(
241 - "SELECT clicked_url, click_timestamp, user_ip, user_agent
242 - FROM {$clicks_table} WHERE session_id = %s
243 - ORDER BY click_timestamp ASC, id ASC",
244 - $sid
245 - ));
246 - foreach ($clicks as $click) {
247 - $details = $click->clicked_url;
248 - $meta = array();
249 - if (!empty($click->user_ip)) {
250 - /* translators: %s: IP address recorded with a link click */
251 - $meta[] = sprintf(__('IP address: %s', 'mxchat'), $click->user_ip);
252 - }
253 - if (!empty($click->user_agent)) {
254 - /* translators: %s: browser user-agent recorded with a link click */
255 - $meta[] = sprintf(__('Browser: %s', 'mxchat'), $click->user_agent);
256 - }
257 - if (!empty($meta)) {
258 - $details .= ' (' . implode('; ', $meta) . ')';
259 - }
260 - $data[] = array(
261 - 'name' => sprintf('[%s] %s', $click->click_timestamp, __('Link clicked', 'mxchat')),
262 - 'value' => $details,
263 - );
258 + $data = array_merge($data, self::export_click_fields($sid));
259 +
260 + $items[] = self::export_item($sid, $data);
261 + }
262 +
263 + return array(
264 + 'data' => $items,
265 + 'done' => ($offset + self::EXPORT_SESSIONS_PER_PAGE) >= count($all_session_ids),
266 + );
267 + }
268 +
269 + /**
270 + * The subject's full session set for the exporter: transcript sessions in
271 + * their existing order (oldest conversation first), then sessions that
272 + * exist only in the session store — a pre-chat capture with no messages —
273 + * ordered by session id. The eraser already unions the store the same way
274 + * (erase_lingering_options). The store-only set is bounded by lead count.
275 + */
276 + private static function get_export_session_ids($email_address) {
277 + $session_ids = self::get_session_ids($email_address, PHP_INT_MAX, 0);
278 + if (!is_array($session_ids)) {
279 + $session_ids = array();
280 + }
281 +
282 + if (class_exists('MxChat_Session_Store') && method_exists('MxChat_Session_Store', 'find_by_email')) {
283 + $store_only = array_diff(MxChat_Session_Store::find_by_email($email_address), $session_ids);
284 + $store_only = array_values(array_unique(array_map('strval', $store_only)));
285 + sort($store_only, SORT_STRING);
286 + $session_ids = array_merge($session_ids, $store_only);
287 + }
288 +
289 + return $session_ids;
290 + }
291 +
292 + /**
293 + * Export item for a session with no transcript rows. Only what is held:
294 + * the identity the visitor typed, the consent record, and any link-click
295 + * rows. No conversation dates (there is no conversation) and no capture
296 + * time of its own — the store does not expose one, and the consent record
297 + * carries its own timestamp. Null when the store holds nothing to show.
298 + */
299 + private static function export_store_only_item($sid) {
300 + if (!class_exists('MxChat_Session_Store')) {
301 + return null;
302 + }
303 +
304 + $email = MxChat_Session_Store::get($sid, 'email', '');
305 + $name = MxChat_Session_Store::get($sid, 'name', '');
306 +
307 + $data = array(
308 + array('name' => __('Session ID', 'mxchat'), 'value' => $sid),
309 + );
310 + if (!empty($email)) {
311 + $data[] = array('name' => __('Email on record', 'mxchat'), 'value' => (string) $email);
312 + }
313 + if (!empty($name)) {
314 + $data[] = array('name' => __('Name on record', 'mxchat'), 'value' => (string) $name);
315 + }
316 +
317 + $data = array_merge($data, self::export_consent_fields($sid), self::export_click_fields($sid));
318 +
319 + if (count($data) < 2) {
320 + return null;
321 + }
322 +
323 + return self::export_item($sid, $data);
324 + }
325 +
326 + /** Lead-capture consent fields for one session (empty when none recorded). */
327 + private static function export_consent_fields($sid) {
328 + $fields = array();
329 +
330 + if (class_exists('MxChat_Session_Store')) {
331 + $consent_state = MxChat_Session_Store::get($sid, 'consent', '');
332 + if ($consent_state !== '' && $consent_state !== false) {
333 + $fields[] = array('name' => __('Lead-capture consent', 'mxchat'), 'value' => (string) $consent_state);
334 + $consent_at = MxChat_Session_Store::get($sid, 'consent_at', '');
335 + if (!empty($consent_at)) {
336 + $fields[] = array('name' => __('Consent recorded', 'mxchat'), 'value' => (string) $consent_at);
264 337 }
338 + $consent_label = MxChat_Session_Store::get($sid, 'consent_label', '');
339 + if (!empty($consent_label)) {
340 + $fields[] = array('name' => __('Consent text shown', 'mxchat'), 'value' => wp_strip_all_tags((string) $consent_label));
341 + }
265 342 }
343 + }
266 344
267 - $items[] = array(
268 - 'group_id' => 'mxchat-chat-transcripts',
269 - 'group_label' => __('Chat Conversations (MxChat)', 'mxchat'),
270 - 'group_description' => __('Chatbot conversations recorded by the MxChat plugin.', 'mxchat'),
271 - 'item_id' => 'mxchat-session-' . $sid,
272 - 'data' => $data,
273 - );
345 + return $fields;
346 + }
347 +
348 + /** Link-click fields for one session (empty when none, or no clicks table). */
349 + private static function export_click_fields($sid) {
350 + global $wpdb;
351 + $fields = array();
352 +
353 + if (self::url_clicks_table_exists()) {
354 + $clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
355 + $clicks = $wpdb->get_results($wpdb->prepare(
356 + "SELECT clicked_url, click_timestamp, user_ip, user_agent
357 + FROM {$clicks_table} WHERE session_id = %s
358 + ORDER BY click_timestamp ASC, id ASC",
359 + $sid
360 + ));
361 + foreach ($clicks as $click) {
362 + $details = $click->clicked_url;
363 + $meta = array();
364 + if (!empty($click->user_ip)) {
365 + /* translators: %s: IP address recorded with a link click */
366 + $meta[] = sprintf(__('IP address: %s', 'mxchat'), $click->user_ip);
367 + }
368 + if (!empty($click->user_agent)) {
369 + /* translators: %s: browser user-agent recorded with a link click */
370 + $meta[] = sprintf(__('Browser: %s', 'mxchat'), $click->user_agent);
371 + }
372 + if (!empty($meta)) {
373 + $details .= ' (' . implode('; ', $meta) . ')';
374 + }
375 + $fields[] = array(
376 + 'name' => sprintf('[%s] %s', $click->click_timestamp, __('Link clicked', 'mxchat')),
377 + 'value' => $details,
378 + );
379 + }
274 380 }
275 381
382 + return $fields;
383 + }
384 +
385 + private static function export_item($sid, array $data) {
276 386 return array(
277 - 'data' => $items,
278 - 'done' => count($session_ids) < self::EXPORT_SESSIONS_PER_PAGE,
387 + 'group_id' => 'mxchat-chat-transcripts',
388 + 'group_label' => __('Chat Conversations (MxChat)', 'mxchat'),
389 + 'group_description' => __('Chatbot conversations recorded by the MxChat plugin.', 'mxchat'),
390 + 'item_id' => 'mxchat-session-' . $sid,
391 + 'data' => $data,
279 392 );
280 393 }
281 394
282 395 private static function role_label($role) {
@@ -451,8 +564,11 @@
451 564 $sid = substr($opt->option_name, strlen('mxchat_lead_del_email_'));
452 565 delete_option('mxchat_lead_del_email_' . $sid);
453 566 delete_option('mxchat_lead_del_name_' . $sid);
454 567 delete_option('mxchat_lead_del_ts_' . $sid);
568 + delete_option('mxchat_lead_del_consent_' . $sid);
569 + delete_option('mxchat_lead_del_consent_label_' . $sid);
570 + delete_option('mxchat_lead_del_consent_at_' . $sid);
455 571 } else {
456 572 $sid = substr($opt->option_name, strlen('mxchat_email_'));
457 573 delete_option('mxchat_email_' . $sid);
458 574 delete_option('mxchat_name_' . $sid);