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 | admin/class-ajax-handler.php +327 -17 3.2.16 → 3.2.25 View file →
@@ -27,8 +27,9 @@
27 27 private function mxchat_init_ajax_hooks() {
28 28 // Settings AJAX
29 29 add_action('wp_ajax_mxchat_save_setting', array($this, 'mxchat_save_setting_callback'));
30 30 add_action('wp_ajax_mxchat_save_prompts_setting', array($this, 'mxchat_save_prompts_setting_callback'));
31 + add_action('wp_ajax_mxchat_acf_toggle_group', array($this, 'mxchat_acf_toggle_group_callback'));
31 32 add_action('wp_ajax_migrate_pinecone_settings', array($this, 'ajax_migrate_pinecone_settings'));
32 33
33 34 // License AJAX
34 35 add_action('wp_ajax_mxchat_handle_activate_license', array($this, 'mxchat_handle_activate_license'));
@@ -419,8 +420,58 @@
419 420 }
420 421 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
421 422 return;
422 423
424 + // ACF→PDF import-time extraction (plan 11720c). STANDALONE option, same
425 + // pattern. Moved from a per-import modal checkbox to an install-level
426 + // setting on Knowledge → ACF Fields. Stored '1'/'0' to match the
427 + // knowledge page's sibling toggles. Default OFF.
428 + case 'mxchat_acf_pdf_extraction':
429 + $apx_value = ($value === 'on' || $value === '1') ? '1' : '0';
430 + update_option('mxchat_acf_pdf_extraction', $apx_value);
431 + wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
432 + return;
433 +
434 + // In-chat YouTube card: master switch + its own confidence floor
435 + // (plan f52492). STANDALONE options, same pattern as the cases above.
436 + // Default ON — the card already ships, so this is an opt-OUT.
437 + case 'mxchat_video_embed_enabled':
438 + $vce_value = ($value === 'on' || $value === '1') ? 'on' : 'off';
439 + update_option('mxchat_video_embed_enabled', $vce_value);
440 + wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
441 + return;
442 +
443 + // Page-cache purge on settings save (plan b76ed6, Support Ticket 468).
444 + // STANDALONE option, same pattern. Default ON — the purge already ships
445 + // (3.2.9), so this is an opt-OUT for large sites. Switching it off also
446 + // drops a deferred purge the debounce may have scheduled seconds ago,
447 + // so "off" takes effect at once rather than after one more purge.
448 + case 'mxchat_cache_purge_enabled':
449 + $cpe_value = ($value === 'on' || $value === '1') ? 'on' : 'off';
450 + update_option('mxchat_cache_purge_enabled', $cpe_value);
451 + if ($cpe_value === 'off' && class_exists('MxChat_Cache_Purge')) {
452 + wp_clear_scheduled_hook(MxChat_Cache_Purge::DEFERRED_EVENT);
453 + }
454 + wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
455 + return;
456 +
457 + // Clamped to the same 20-95 the field advertises. An out-of-range or
458 + // non-numeric POST is corrected rather than refused, and the corrected
459 + // value is echoed back so the field can reconcile — a silently stored
460 + // 0 here would put a video on every answer, which is the bug.
461 + case 'mxchat_video_embed_threshold':
462 + $vct_value = is_numeric($value)
463 + ? (int) $value
464 + : MXCHAT_VIDEO_EMBED_THRESHOLD_DEFAULT;
465 + if ($vct_value < 20) { $vct_value = 20; }
466 + if ($vct_value > 95) { $vct_value = 95; }
467 + update_option('mxchat_video_embed_threshold', $vct_value);
468 + wp_send_json_success([
469 + 'message' => esc_html__('Setting saved', 'mxchat'),
470 + 'value' => $vct_value,
471 + ]);
472 + return;
473 +
423 474 // Live-agent availability schedules (plans 8ccaa2 + 99d7a4). STANDALONE
424 475 // options, same reasoning as the Editor Assistant case above — nested
425 476 // structures that mxchat_sanitize() would strip on the next autosave of any
426 477 // other field. Each channel's value arrives as JSON from its own hidden
@@ -429,8 +480,9 @@
429 480 // browser still running pre-split cached admin JS: that UI edited "both
430 481 // channels" as one, so its save writes both.
431 482 case 'live_agent_schedule_slack':
432 483 case 'live_agent_schedule_telegram':
484 + case 'live_agent_schedule_webhook':
433 485 case 'live_agent_schedule':
434 486 if (!class_exists('MxChat_Live_Agent_Schedule')) {
435 487 wp_send_json_error(['message' => esc_html__('Schedule unavailable', 'mxchat')]);
436 488 return;
@@ -542,8 +594,13 @@
542 594 case 'name_field_placeholder':
543 595 //error_log('MXChat Save: Processing name_field_placeholder');
544 596 $options[$field_name] = sanitize_text_field($value);
545 597 break;
598 + case 'consent_checkbox_label':
599 + // b062c4 — same allowlist as the options.php save path and the
600 + // widget render, so the stored label always equals the shown label.
601 + $options[$field_name] = MxChat_Utils::sanitize_consent_label($value);
602 + break;
546 603 case 'similarity_threshold':
547 604 //error_log('MXChat Save: Processing similarity_threshold');
548 605 // Validate and save - enforce min 20, max 85
549 606 $threshold = intval($value);
@@ -570,8 +627,49 @@
570 627 //error_log('MXChat Save: Processing live_agent_status');
571 628 // Set the new value
572 629 $options[$field_name] = ($value === 'on') ? 'on' : 'off';
573 630 break;
631 + // Shared handoff channel (plan 1a2666): re-probe the channel's privacy
632 + // at configuration time so the settings screen can warn about private
633 + // channels (their inbound events arrive as message.groups, which the
634 + // documented app setup never subscribes to). Only id-shaped values can
635 + // be checked before the first handoff resolves a #name — the handoff
636 + // path probes those when it caches the resolved id.
637 + case 'live_agent_shared_channel':
638 + $options[$field_name] = sanitize_text_field($value);
639 + delete_option('mxchat_slack_shared_channel_privacy');
640 + $shared_channel_target = ltrim(trim((string) $options[$field_name]), '#');
641 + if ($shared_channel_target !== '' && preg_match('/^[CG][A-Z0-9]{6,}$/', $shared_channel_target)) {
642 + if (!class_exists('MxChat_Integrator')) {
643 + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-mxchat-integrator.php';
644 + }
645 + MxChat_Integrator::mxchat_probe_slack_channel_privacy(
646 + $options['live_agent_bot_token'] ?? '',
647 + $shared_channel_target,
648 + trim((string) $options[$field_name])
649 + );
650 + }
651 + break;
652 + // Webhook handoff destination (plan d88e22). Status normalized like the
653 + // other channel toggles; the URL is refused outright when it isn't
654 + // https so the admin hears about it at save time instead of the
655 + // handoff silently never firing.
656 + case 'webhook_handoff_status':
657 + $options[$field_name] = ($value === 'on') ? 'on' : 'off';
658 + break;
659 + case 'webhook_handoff_url':
660 + $wh_url = trim((string) $value);
661 + if ($wh_url === '') {
662 + $options[$field_name] = '';
663 + break;
664 + }
665 + $wh_clean = esc_url_raw($wh_url, array('https'));
666 + if ($wh_clean === '' || stripos($wh_clean, 'https://') !== 0) {
667 + wp_send_json_error(['message' => esc_html__('Webhook URL must start with https://', 'mxchat')]);
668 + return;
669 + }
670 + $options[$field_name] = $wh_clean;
671 + break;
574 672 case 'enable_web_search':
575 673 //error_log('MXChat Save: Processing enable_web_search');
576 674 $options[$field_name] = ($value === 'on') ? 'on' : 'off';
577 675 break;
@@ -619,9 +717,16 @@
619 717 $transcripts_options = array();
620 718 }
621 719
622 720 // Handle checkbox values (convert 'on'/'off' to 1/0)
623 - if ($value === 'on' || $value === '1') {
721 + if ($field_name === 'mxchat_retention_days') {
722 + // Number field, NOT a checkbox — without this branch a
723 + // value of '1' would hit the 'on'/'1' coercion below
724 + // (harmlessly) but nothing would clamp: this direct-DB
725 + // path bypasses the registered sanitiser and its
726 + // 0-3650 clamp entirely (plan-3c3338).
727 + $transcripts_options[$field_name] = max(0, min(3650, (int) $value));
728 + } else if ($value === 'on' || $value === '1') {
624 729 $transcripts_options[$field_name] = 1;
625 730 } else if ($value === 'off' || $value === '0' || $value === '') {
626 731 $transcripts_options[$field_name] = 0;
627 732 } else {
@@ -626,9 +731,20 @@
626 731 $transcripts_options[$field_name] = 0;
627 732 } else {
628 733 // For text/select fields, sanitize appropriately
629 734 if ($field_name === 'mxchat_notification_email') {
630 - $transcripts_options[$field_name] = sanitize_email($value);
735 + // sanitize_email() alone CANNOT validate this field: given
736 + // "[email protected], [email protected]" it returns the single concatenated
737 + // address "[email protected]", which is_email() then accepts.
738 + // That is how two addresses used to be stored as one dead
739 + // one, silently. MxChat_Utils validates the raw parts first
740 + // and refuses the whole list if any of them is bad.
741 + $parsed = MxChat_Utils::parse_notification_emails($value);
742 + if ($parsed['error'] !== '') {
743 + // Reject: the previously stored value stays untouched.
744 + wp_send_json_error(array('message' => $parsed['error']));
745 + }
746 + $transcripts_options[$field_name] = implode(', ', $parsed['emails']);
631 747 } else {
632 748 $transcripts_options[$field_name] = sanitize_text_field($value);
633 749 }
634 750 }
@@ -805,8 +921,10 @@
805 921 'contextual_awareness_toggle',
806 922 'citation_links_toggle',
807 923 'enable_email_block',
808 924 'enable_name_field',
925 + 'enable_consent_checkbox',
926 + 'consent_checkbox_required',
809 927 'custom_provider_for_embeddings',
810 928 'custom_provider_for_images',
811 929 'print_button_enabled',
812 930 'reset_chat_enabled'
@@ -953,8 +1071,42 @@
953 1071 if ($field_name === 'mxchat_pinecone_host') {
954 1072 $new_value = str_replace(['https://', 'http://'], '', $new_value);
955 1073 }
956 1074 break;
1075 + case 'mxchat_pinecone_index_type':
1076 + // 362c31: document only once the host has been checked (see sanitize_pinecone_settings).
1077 + $new_value = ($value === 'document') ? 'document' : 'vector';
1078 + if ($new_value === 'document' && strtolower(trim((string) ($current_options['mxchat_pinecone_host'] ?? ''), '/')) !== strtolower((string) ($current_options['mxchat_pinecone_docs_verified_host'] ?? ''))) {
1079 + wp_send_json_error(['message' => esc_html__('Check the document index host first (Check index or Create index for me).', 'mxchat')]);
1080 + }
1081 + // 3e83e4: same gate as the form save — no switch onto a half-copied index.
1082 + if ($new_value === 'document' && (($current_options['mxchat_pinecone_index_type'] ?? 'vector') !== 'document') && class_exists('MxChat_Pinecone_Documents')) {
1083 + $running = MxChat_Pinecone_Documents::migration_blocks_switch((string) ($current_options['mxchat_pinecone_host'] ?? ''));
1084 + if ($running !== null) {
1085 + wp_send_json_error(['message' => MxChat_Pinecone_Documents::switch_blocked_message($running)]);
1086 + }
1087 + }
1088 + break;
1089 + case 'mxchat_pinecone_vector_host':
1090 + $new_value = trim(str_replace(['https://', 'http://'], '', sanitize_text_field($value)), '/');
1091 + break;
1092 + case 'mxchat_pinecone_vector_index':
1093 + case 'mxchat_pinecone_docs_region':
1094 + $new_value = sanitize_text_field($value);
1095 + break;
1096 + case 'mxchat_pinecone_docs_cloud':
1097 + $new_value = in_array($value, array('aws', 'gcp', 'azure'), true) ? $value : 'aws';
1098 + break;
1099 + case 'mxchat_pinecone_docs_language':
1100 + $lang_value = sanitize_key($value);
1101 + $new_value = (class_exists('MxChat_Pinecone_Documents') && array_key_exists($lang_value, MxChat_Pinecone_Documents::languages())) ? $lang_value : 'en';
1102 + break;
1103 + case 'mxchat_pinecone_top_k':
1104 + // d0cae1: out-of-range and junk normalize to the default 50 —
1105 + // same clamp the read site applies.
1106 + $top_k = absint($value);
1107 + $new_value = (string) (($top_k >= 1 && $top_k <= 1000) ? $top_k : 50);
1108 + break;
957 1109 default:
958 1110 wp_send_json_error(['message' => esc_html__('Unknown Pinecone field', 'mxchat')]);
959 1111 }
960 1112
@@ -984,14 +1136,16 @@
984 1136 );
985 1137 //error_log('[MXCHAT-PROMPTS] Updated existing option, result: ' . ($save_result !== false ? 'SUCCESS' : 'FAILED'));
986 1138 } else {
987 1139 // Insert new option
1140 + // Credential option — must NOT autoload (holds the Pinecone secret;
1141 + // autoloaded rows are read into memory on every request).
988 1142 $save_result = $wpdb->insert(
989 1143 $wpdb->options,
990 1144 array(
991 1145 'option_name' => 'mxchat_pinecone_addon_options',
992 1146 'option_value' => $serialized_options,
993 - 'autoload' => 'yes'
1147 + 'autoload' => 'off'
994 1148 ),
995 1149 array('%s', '%s', '%s')
996 1150 );
997 1151 //error_log('[MXCHAT-PROMPTS] Inserted new option, result: ' . ($save_result !== false ? 'SUCCESS' : 'FAILED'));
@@ -1109,10 +1263,18 @@
1109 1263 }
1110 1264
1111 1265 // Handle ACF field exclusion toggles
1112 1266 if (strpos($name, 'mxchat_acf_field_') === 0) {
1113 - // Extract field name from the input name (e.g., mxchat_acf_field_private_notes -> private_notes)
1114 - $field_name = str_replace('mxchat_acf_field_', '', $name);
1267 + // The identifier after the prefix is the ACF field KEY (unique per
1268 + // field), not the field name — names are shared across groups and
1269 + // collide (plan 30e81f). Reject anything that isn't key-shaped so a
1270 + // stale pre-3.2.20 page (or its exit beacon) posting a bare name
1271 + // can't write junk into the key-based list.
1272 + $field_key = str_replace('mxchat_acf_field_', '', $name);
1273 + if (!preg_match('/^field_[A-Za-z0-9_\-]+$/', $field_key)) {
1274 + wp_send_json_error(['message' => esc_html__('Invalid ACF field identifier', 'mxchat')]);
1275 + return;
1276 + }
1115 1277 $is_enabled = ($value === 'on' || $value === '1');
1116 1278
1117 1279 // Get current excluded fields
1118 1280 $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
@@ -1121,13 +1283,19 @@
1121 1283 }
1122 1284
1123 1285 if ($is_enabled) {
1124 1286 // Remove from exclusion list (field should be included)
1125 - $excluded_fields = array_values(array_diff($excluded_fields, array($field_name)));
1287 + $excluded_fields = array_values(array_diff($excluded_fields, array($field_key)));
1288 + // Lazy legacy-name conversion: if this field's NAME is still
1289 + // stored (its group was inactive when the 30e81f migration
1290 + // ran), including this one field must not silently include
1291 + // its same-named twins — swap the name entry for the keys of
1292 + // every OTHER field currently wearing that name.
1293 + $excluded_fields = $this->mxchat_expand_legacy_acf_name_entry($excluded_fields, $field_key);
1126 1294 } else {
1127 1295 // Add to exclusion list (field should be excluded)
1128 - if (!in_array($field_name, $excluded_fields)) {
1129 - $excluded_fields[] = $field_name;
1296 + if (!in_array($field_key, $excluded_fields, true)) {
1297 + $excluded_fields[] = $field_key;
1130 1298 }
1131 1299 }
1132 1300
1133 1301 $updated = update_option('mxchat_acf_excluded_fields', $excluded_fields);
@@ -1134,10 +1302,10 @@
1134 1302
1135 1303 if ($updated || true) { // Always report success since the state may already be correct
1136 1304 wp_send_json_success([
1137 1305 'message' => $is_enabled
1138 - ? sprintf(esc_html__('Field "%s" will be included in imports', 'mxchat'), $field_name)
1139 - : sprintf(esc_html__('Field "%s" will be excluded from imports', 'mxchat'), $field_name)
1306 + ? esc_html__('Field will be included in imports', 'mxchat')
1307 + : esc_html__('Field will be excluded from imports', 'mxchat')
1140 1308 ]);
1141 1309 } else {
1142 1310 wp_send_json_error(['message' => esc_html__('Failed to save ACF field setting', 'mxchat')]);
1143 1311 }
@@ -1157,12 +1325,12 @@
1157 1325 }
1158 1326 return;
1159 1327 }
1160 1328
1161 - // Handle other prompts options
1329 + // Handle other prompts options (autoload false — can hold the Pinecone secret)
1162 1330 $options = get_option('mxchat_prompts_options', []);
1163 1331 $options[$name] = $value;
1164 - $updated = update_option('mxchat_prompts_options', $options);
1332 + $updated = update_option('mxchat_prompts_options', $options, false);
1165 1333
1166 1334 if ($updated) {
1167 1335 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
1168 1336 } else {
@@ -1169,10 +1337,130 @@
1169 1337 wp_send_json_error(['message' => esc_html__('No changes detected', 'mxchat')]);
1170 1338 }
1171 1339 }
1172 1340
1341 + /**
1342 + * If the field behind $included_key still has its NAME stored in the
1343 + * exclusion list (a legacy entry the 30e81f migration could not resolve
1344 + * because the group was inactive), replace that name with the keys of
1345 + * every OTHER current field wearing it. Including one field must never
1346 + * silently include its same-named twins — that would be the original
1347 + * collision bug in reverse, in the unsafe (privacy-losing) direction.
1348 + */
1349 + private function mxchat_expand_legacy_acf_name_entry($excluded_fields, $included_key) {
1350 + if (!function_exists('acf_get_field')) {
1351 + return $excluded_fields;
1352 + }
1353 + $field = acf_get_field($included_key);
1354 + if (!$field || empty($field['name'])) {
1355 + return $excluded_fields;
1356 + }
1357 + $field_name = $field['name'];
1358 + if (!in_array($field_name, $excluded_fields, true)) {
1359 + return $excluded_fields;
1360 + }
1361 + $excluded_fields = array_values(array_diff($excluded_fields, array($field_name)));
1362 + foreach ($this->mxchat_acf_keys_for_name($field_name) as $twin_key) {
1363 + if ($twin_key !== $included_key && !in_array($twin_key, $excluded_fields, true)) {
1364 + $excluded_fields[] = $twin_key;
1365 + }
1366 + }
1367 + return $excluded_fields;
1368 + }
1173 1369
1174 1370 /**
1371 + * Keys of every currently-registered top-level ACF field with this name.
1372 + */
1373 + private function mxchat_acf_keys_for_name($field_name) {
1374 + $keys = array();
1375 + if (!function_exists('acf_get_field_groups') || !function_exists('acf_get_fields')) {
1376 + return $keys;
1377 + }
1378 + foreach (acf_get_field_groups() as $group) {
1379 + $group_fields = acf_get_fields($group['key']);
1380 + if (empty($group_fields)) {
1381 + continue;
1382 + }
1383 + foreach ($group_fields as $field) {
1384 + if (isset($field['name'], $field['key']) && $field['name'] === $field_name) {
1385 + $keys[] = $field['key'];
1386 + }
1387 + }
1388 + }
1389 + return $keys;
1390 + }
1391 +
1392 + /**
1393 + * Group-level ACF toggle (plan bf57e0): sets every field in one ACF field
1394 + * group included or excluded in a SINGLE option write. The client must
1395 + * never loop the per-field endpoint for this — get_option → modify →
1396 + * update_option once per field from twenty concurrent requests is a
1397 + * lost-update race that silently drops most of the group.
1398 + */
1399 + public function mxchat_acf_toggle_group_callback() {
1400 + check_ajax_referer('mxchat_prompts_setting_nonce');
1401 +
1402 + if (!current_user_can('manage_options')) {
1403 + wp_send_json_error(['message' => esc_html__('Insufficient permissions', 'mxchat')], 403);
1404 + return;
1405 + }
1406 +
1407 + if (!function_exists('acf_get_fields')) {
1408 + wp_send_json_error(['message' => esc_html__('ACF is not active', 'mxchat')]);
1409 + return;
1410 + }
1411 +
1412 + $group_key = isset($_POST['group_key']) ? sanitize_text_field(wp_unslash($_POST['group_key'])) : '';
1413 + if (!preg_match('/^group_[A-Za-z0-9_\-]+$/', $group_key)) {
1414 + wp_send_json_error(['message' => esc_html__('Invalid ACF group identifier', 'mxchat')]);
1415 + return;
1416 + }
1417 + $state = isset($_POST['state']) ? sanitize_text_field(wp_unslash($_POST['state'])) : '';
1418 + $include = ($state === 'on' || $state === '1');
1419 +
1420 + // Resolve the group's fields SERVER-side — a client-supplied key list
1421 + // is not trusted. This is the same call the settings UI lists from,
1422 + // so the toggle covers exactly the rendered set (top-level fields).
1423 + $group_fields = acf_get_fields($group_key);
1424 + if (empty($group_fields)) {
1425 + wp_send_json_error(['message' => esc_html__('No fields found for this group', 'mxchat')]);
1426 + return;
1427 + }
1428 +
1429 + $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
1430 + if (!is_array($excluded_fields)) {
1431 + $excluded_fields = array();
1432 + }
1433 +
1434 + $touched = array();
1435 + foreach ($group_fields as $field) {
1436 + if (empty($field['key'])) {
1437 + continue;
1438 + }
1439 + if ($include) {
1440 + $excluded_fields = array_values(array_diff($excluded_fields, array($field['key'])));
1441 + $excluded_fields = $this->mxchat_expand_legacy_acf_name_entry($excluded_fields, $field['key']);
1442 + } elseif (!in_array($field['key'], $excluded_fields, true)) {
1443 + $excluded_fields[] = $field['key'];
1444 + }
1445 + $touched[] = array(
1446 + 'name' => 'mxchat_acf_field_' . $field['key'],
1447 + 'value' => $include ? 'on' : 'off',
1448 + );
1449 + }
1450 +
1451 + // The one write — the whole point of this endpoint.
1452 + update_option('mxchat_acf_excluded_fields', array_values($excluded_fields));
1453 +
1454 + wp_send_json_success([
1455 + 'message' => $include
1456 + ? esc_html__('All fields in this group will be included in imports', 'mxchat')
1457 + : esc_html__('All fields in this group will be excluded from imports', 'mxchat'),
1458 + 'fields' => $touched,
1459 + ]);
1460 + }
1461 +
1462 + /**
1175 1463 * Handles AJAX request for Pinecone settings migration
1176 1464 */
1177 1465 public function ajax_migrate_pinecone_settings() {
1178 1466 // Verify nonce
@@ -1208,9 +1496,9 @@
1208 1496 'mxchat_pinecone_index' => sanitize_text_field($old_options['mxchat_pinecone_index'] ?? ''),
1209 1497 'mxchat_pinecone_environment' => sanitize_text_field($old_options['mxchat_pinecone_environment'] ?? '')
1210 1498 );
1211 1499
1212 - update_option('mxchat_pinecone_addon_options', $migrated_options);
1500 + update_option('mxchat_pinecone_addon_options', $migrated_options, false);
1213 1501
1214 1502 wp_send_json_success(array(
1215 1503 'migrated' => true,
1216 1504 'message' => 'Settings migrated successfully from Pinecone add-on'
@@ -1253,9 +1541,10 @@
1253 1541 }
1254 1542
1255 1543 $product_id = 'MxChatPRO';
1256 1544 $domain = parse_url(home_url(), PHP_URL_HOST); // Get the current domain
1257 -
1545 + $was_active = (get_option('mxchat_license_status', 'inactive') === 'active');
1546 +
1258 1547 // Call WooCommerce Software API for activation (not just validation)
1259 1548 $response = wp_remote_get(
1260 1549 add_query_arg(
1261 1550 array(
@@ -1301,9 +1590,16 @@
1301 1590 update_option('mxchat_license_status', 'active');
1302 1591 update_option('mxchat_pro_email', $customer_email);
1303 1592 update_option('mxchat_activation_key', $license_key);
1304 1593 delete_option('mxchat_license_error');
1305 -
1594 +
1595 + // A fresh activation starts the licence domain check (c93ede) from a
1596 + // clean slate: no grace streak carried over, first daily check at a
1597 + // random time within the day.
1598 + if (class_exists('MxChat_License_Check')) {
1599 + MxChat_License_Check::reset(true);
1600 + }
1601 +
1306 1602 // Also track on your website (this is your existing domain tracking)
1307 1603 $this->track_domain_on_website($license_key, $customer_email, $domain);
1308 1604
1309 1605 wp_send_json_success(array('message' => esc_html__('License activated successfully', 'mxchat')));
@@ -1310,9 +1606,17 @@
1310 1606 } else {
1311 1607 $error_message = isset($data->error) ? $data->error : esc_html__('Activation failed', 'mxchat');
1312 1608 update_option('mxchat_license_status', 'inactive');
1313 1609 update_option('mxchat_license_error', $error_message);
1314 -
1610 +
1611 + // c93ede: a refusal by the licence owner (domain switched off, new sites
1612 + // blocked) or a revoked key is an explicit "off" from mxchat.ai. Remember
1613 + // who controls it so Pro & Extensions says so, and keep checking so the
1614 + // owner's switch-on restores Pro without another click here.
1615 + if (class_exists('MxChat_License_Check')) {
1616 + MxChat_License_Check::note_activation_refusal(isset($data->code) ? (string) $data->code : '', (string) $error_message, $was_active);
1617 + }
1618 +
1315 1619 //error_log('MxChat Activation failed: ' . $error_message);
1316 1620 wp_send_json_error($error_message);
1317 1621 }
1318 1622 }
@@ -1397,9 +1701,15 @@
1397 1701 delete_option('mxchat_license_status');
1398 1702 delete_option('mxchat_pro_email');
1399 1703 delete_option('mxchat_activation_key');
1400 1704 delete_option('mxchat_license_error');
1401 -
1705 +
1706 + // Nothing left to check: drop the licence domain check's state and its
1707 + // scheduled event (c93ede).
1708 + if (class_exists('MxChat_License_Check')) {
1709 + MxChat_License_Check::reset(false);
1710 + }
1711 +
1402 1712 //error_log('MxChat deactivate: Local data cleared');
1403 1713
1404 1714 // Notify your website's API to properly deactivate
1405 1715 $response = wp_remote_post('https://mxchat.ai/mxchat-api/deactivate-license', array(