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 +142 -50 3.2.20 → 3.2.25 View file →
@@ -176,9 +176,13 @@
176 176
177 177 $page = max(1, (int) $page);
178 178 $offset = ($page - 1) * self::EXPORT_SESSIONS_PER_PAGE;
179 179
180 - $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);
181 185
182 186 $items = array();
183 187 foreach ($session_ids as $sid) {
184 188 $rows = $wpdb->get_results($wpdb->prepare(
@@ -188,8 +192,16 @@
188 192 ORDER BY timestamp ASC, id ASC",
189 193 $sid
190 194 ));
191 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 + }
192 204 continue;
193 205 }
194 206
195 207 $emails = array();
@@ -231,22 +243,9 @@
231 243
232 244 // Lead-capture consent record (b062c4). If we hold a consent
233 245 // decision about the subject, the export must show it — same
234 246 // Article 15 parity rule as the click rows below.
235 - if (class_exists('MxChat_Session_Store')) {
236 - $consent_state = MxChat_Session_Store::get($sid, 'consent', '');
237 - if ($consent_state !== '' && $consent_state !== false) {
238 - $data[] = array('name' => __('Lead-capture consent', 'mxchat'), 'value' => (string) $consent_state);
239 - $consent_at = MxChat_Session_Store::get($sid, 'consent_at', '');
240 - if (!empty($consent_at)) {
241 - $data[] = array('name' => __('Consent recorded', 'mxchat'), 'value' => (string) $consent_at);
242 - }
243 - $consent_label = MxChat_Session_Store::get($sid, 'consent_label', '');
244 - if (!empty($consent_label)) {
245 - $data[] = array('name' => __('Consent text shown', 'mxchat'), 'value' => wp_strip_all_tags((string) $consent_label));
246 - }
247 - }
248 - }
247 + $data = array_merge($data, self::export_consent_fields($sid));
249 248
250 249 foreach ($rows as $row) {
251 250 $data[] = array(
252 251 'name' => sprintf('[%s] %s', $row->timestamp, self::role_label($row->role)),
@@ -255,49 +254,142 @@
255 254 }
256 255
257 256 // Link-click rows for this session (Article 15 parity with the
258 257 // eraser — same subject, same session set, plan 23c4a1).
259 - if (self::url_clicks_table_exists()) {
260 - $clicks_table = $wpdb->prefix . 'mxchat_url_clicks';
261 - $clicks = $wpdb->get_results($wpdb->prepare(
262 - "SELECT clicked_url, click_timestamp, user_ip, user_agent
263 - FROM {$clicks_table} WHERE session_id = %s
264 - ORDER BY click_timestamp ASC, id ASC",
265 - $sid
266 - ));
267 - foreach ($clicks as $click) {
268 - $details = $click->clicked_url;
269 - $meta = array();
270 - if (!empty($click->user_ip)) {
271 - /* translators: %s: IP address recorded with a link click */
272 - $meta[] = sprintf(__('IP address: %s', 'mxchat'), $click->user_ip);
273 - }
274 - if (!empty($click->user_agent)) {
275 - /* translators: %s: browser user-agent recorded with a link click */
276 - $meta[] = sprintf(__('Browser: %s', 'mxchat'), $click->user_agent);
277 - }
278 - if (!empty($meta)) {
279 - $details .= ' (' . implode('; ', $meta) . ')';
280 - }
281 - $data[] = array(
282 - 'name' => sprintf('[%s] %s', $click->click_timestamp, __('Link clicked', 'mxchat')),
283 - 'value' => $details,
284 - );
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);
285 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 + }
286 342 }
343 + }
287 344
288 - $items[] = array(
289 - 'group_id' => 'mxchat-chat-transcripts',
290 - 'group_label' => __('Chat Conversations (MxChat)', 'mxchat'),
291 - 'group_description' => __('Chatbot conversations recorded by the MxChat plugin.', 'mxchat'),
292 - 'item_id' => 'mxchat-session-' . $sid,
293 - 'data' => $data,
294 - );
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 + }
295 380 }
296 381
382 + return $fields;
383 + }
384 +
385 + private static function export_item($sid, array $data) {
297 386 return array(
298 - 'data' => $items,
299 - '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,
300 392 );
301 393 }
302 394
303 395 private static function role_label($role) {