| @@ -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( |