| @@ -176,13 +176,9 @@ | ||
| 176 | 176 | |
| 177 | 177 | $page = max(1, (int) $page); |
| 178 | 178 | $offset = ($page - 1) * self::EXPORT_SESSIONS_PER_PAGE; |
| 179 | 179 | |
| 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); | |
| 180 | + $session_ids = self::get_session_ids($email_address, self::EXPORT_SESSIONS_PER_PAGE, $offset); | |
| 185 | 181 | |
| 186 | 182 | $items = array(); |
| 187 | 183 | foreach ($session_ids as $sid) { |
| 188 | 184 | $rows = $wpdb->get_results($wpdb->prepare( |
| @@ -192,16 +188,8 @@ | ||
| 192 | 188 | ORDER BY timestamp ASC, id ASC", |
| 193 | 189 | $sid |
| 194 | 190 | )); |
| 195 | 191 | 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 | - } | |
| 204 | 192 | continue; |
| 205 | 193 | } |
| 206 | 194 | |
| 207 | 195 | $emails = array(); |
| @@ -243,9 +231,22 @@ | ||
| 243 | 231 | |
| 244 | 232 | // Lead-capture consent record (b062c4). If we hold a consent |
| 245 | 233 | // decision about the subject, the export must show it — same |
| 246 | 234 | // Article 15 parity rule as the click rows below. |
| 247 | - $data = array_merge($data, self::export_consent_fields($sid)); | |
| 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 | + } | |
| 248 | 249 | |
| 249 | 250 | foreach ($rows as $row) { |
| 250 | 251 | $data[] = array( |
| 251 | 252 | 'name' => sprintf('[%s] %s', $row->timestamp, self::role_label($row->role)), |
| @@ -254,142 +255,49 @@ | ||
| 254 | 255 | } |
| 255 | 256 | |
| 256 | 257 | // Link-click rows for this session (Article 15 parity with the |
| 257 | 258 | // eraser — same subject, same session set, plan 23c4a1). |
| 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); | |
| 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 | + ); | |
| 337 | 285 | } |
| 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 | - } | |
| 342 | 286 | } |
| 343 | - } | |
| 344 | 287 | |
| 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 | - } | |
| 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 | + ); | |
| 380 | 295 | } |
| 381 | 296 | |
| 382 | - return $fields; | |
| 383 | - } | |
| 384 | - | |
| 385 | - private static function export_item($sid, array $data) { | |
| 386 | 297 | return array( |
| 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, | |
| 298 | + 'data' => $items, | |
| 299 | + 'done' => count($session_ids) < self::EXPORT_SESSIONS_PER_PAGE, | |
| 392 | 300 | ); |
| 393 | 301 | } |
| 394 | 302 | |
| 395 | 303 | private static function role_label($role) { |