mxchat_init_ajax_hooks(); } /** * Register all AJAX action hooks */ private function mxchat_init_ajax_hooks() { // Settings AJAX add_action('wp_ajax_mxchat_save_setting', array($this, 'mxchat_save_setting_callback')); add_action('wp_ajax_mxchat_save_prompts_setting', array($this, 'mxchat_save_prompts_setting_callback')); add_action('wp_ajax_migrate_pinecone_settings', array($this, 'ajax_migrate_pinecone_settings')); // License AJAX add_action('wp_ajax_mxchat_handle_activate_license', array($this, 'mxchat_handle_activate_license')); add_action('wp_ajax_mxchat_check_license_status', array($this, 'mxchat_check_license_status')); add_action('wp_ajax_mxchat_deactivate_license', array($this, 'mxchat_deactivate_license')); // Actions & Intents AJAX add_action('wp_ajax_mxchat_toggle_action', array($this, 'mxchat_toggle_action')); add_action('wp_ajax_mxchat_update_intent_threshold', array($this, 'mxchat_update_intent_threshold')); add_action('wp_ajax_mxchat_save_selected_bot', array($this, 'mxchat_save_selected_bot')); add_action('wp_ajax_mxchat_check_api_keys', array($this, 'mxchat_check_api_keys')); // Debug & Optimization AJAX add_action('wp_ajax_mxchat_toggle_debug_mode', array($this, 'mxchat_toggle_debug_mode_callback')); add_action('wp_ajax_mxchat_get_debug_log', array($this, 'mxchat_get_debug_log_callback')); add_action('wp_ajax_mxchat_clear_debug_log', array($this, 'mxchat_clear_debug_log_callback')); add_action('wp_ajax_mxchat_export_settings', array($this, 'mxchat_export_settings_callback')); add_action('wp_ajax_mxchat_reset_all_settings', array($this, 'mxchat_reset_all_settings_callback')); // Global rate-limit usage counter reset (admin-only, nonce-guarded) add_action('wp_ajax_mxchat_reset_global_rate_limit', array($this, 'mxchat_reset_global_rate_limit_callback')); // Custom (OpenAI-compatible) Provider connection test add_action('wp_ajax_mxchat_test_custom_provider', array($this, 'mxchat_test_custom_provider_callback')); } /** * Test connection to a Custom (OpenAI-compatible) provider by hitting its /models endpoint * with whichever auth scheme the user configured. Reports model count or a clean error. */ public function mxchat_test_custom_provider_callback() { check_ajax_referer('mxchat_test_custom_provider'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => esc_html__('Unauthorized', 'mxchat'))); } $options = get_option('mxchat_options', array()); $base_url = isset($options['custom_provider_base_url']) ? trim((string) $options['custom_provider_base_url']) : ''; $api_key = isset($options['custom_provider_api_key']) ? trim((string) $options['custom_provider_api_key']) : ''; $auth = isset($options['custom_provider_auth_scheme']) ? $options['custom_provider_auth_scheme'] : 'bearer'; $api_version = isset($options['custom_provider_api_version']) ? trim((string) $options['custom_provider_api_version']) : ''; if (empty($base_url)) { wp_send_json_error(array('message' => esc_html__('Base URL is empty. Save it first.', 'mxchat'))); } $url = rtrim($base_url, '/') . '/models'; if (!empty($api_version)) { $url = add_query_arg('api-version', $api_version, $url); } $headers = array('Content-Type' => 'application/json'); if (!empty($api_key)) { if ($auth === 'api-key') { $headers['api-key'] = $api_key; } else { $headers['Authorization'] = 'Bearer ' . $api_key; } } $response = wp_remote_get($url, array( 'headers' => $headers, 'timeout' => 10, )); if (is_wp_error($response)) { wp_send_json_error(array('message' => sprintf(esc_html__('Network error: %s', 'mxchat'), esc_html($response->get_error_message())))); } $code = (int) wp_remote_retrieve_response_code($response); if ($code === 401 || $code === 403) { wp_send_json_error(array('message' => sprintf(esc_html__('Auth rejected (HTTP %d). Check API key and auth scheme.', 'mxchat'), $code))); } if ($code === 404) { wp_send_json_error(array('message' => esc_html__('Endpoint not found (HTTP 404). Check the Base URL.', 'mxchat'))); } if ($code < 200 || $code >= 300) { wp_send_json_error(array('message' => sprintf(esc_html__('Upstream returned HTTP %d.', 'mxchat'), $code))); } $body = json_decode(wp_remote_retrieve_body($response), true); $count = 0; if (is_array($body)) { if (isset($body['data']) && is_array($body['data'])) { $count = count($body['data']); } elseif (isset($body['models']) && is_array($body['models'])) { $count = count($body['models']); } } wp_send_json_success(array( 'message' => sprintf(esc_html__('Connection OK — %d model(s) reported.', 'mxchat'), $count), 'count' => $count, )); } // ======================================== // SETTINGS AJAX HANDLERS // ======================================== /** * Validates and saves chat settings via AJAX request */ public function mxchat_save_setting_callback() { check_ajax_referer('mxchat_save_setting_nonce'); if (!current_user_can('manage_options')) { ('MXChat Save: Unauthorized access attempt'); wp_send_json_error(['message' => esc_html__('Unauthorized', 'mxchat')]); } $name = isset($_POST['name']) ? $_POST['name'] : ''; // Strip slashes from the value before saving $value = isset($_POST['value']) ? stripslashes($_POST['value']) : ''; //error_log('MXChat Save: Processing field name: ' . $name); //error_log('MXChat Save: Field value: ' . $value); if (empty($name)) { //error_log('MXChat Save: Empty field name detected'); wp_send_json_error(['message' => esc_html__('Invalid field name', 'mxchat')]); } // Load the full options array $options = get_option('mxchat_options', []); //error_log('MXChat Save: Current options array: ' . print_r($options, true)); // Extract field name from mxchat_options[field_name] format if present // But preserve the full name for special cases like rate_limits that need the full path $field_name = $name; if (preg_match('/^mxchat_options\[([^\[\]]+)\]$/', $name, $matches)) { $field_name = $matches[1]; } // Handle special cases switch ($field_name) { case 'model': //error_log('MXChat Save: Processing model selection'); //error_log('MXChat Save: Model value received: ' . $value); //error_log('MXChat Save: Value type: ' . gettype($value)); //error_log('MXChat Save: Value length: ' . strlen($value)); //error_log('MXChat Save: Value === "openrouter": ' . ($value === 'openrouter' ? 'YES' : 'NO')); // Allow 'openrouter' or validate against whitelist if ($value === 'openrouter') { //error_log('MXChat Save: Setting model to openrouter'); $options['model'] = 'openrouter'; } else { //error_log('MXChat Save: Checking against whitelist'); // Catalog refactor (plan-d14e89): canonical allowlist lives in // includes/class-mxchat-model-catalog.php. A new chat model // added there is automatically accepted by autosave. if (!class_exists('MxChat_Model_Catalog')) { require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-mxchat-model-catalog.php'; } $allowed_models = MxChat_Model_Catalog::chat_model_ids(); //error_log('MXChat Save: in_array result: ' . (in_array($value, $allowed_models) ? 'YES' : 'NO')); if (in_array($value, $allowed_models)) { //error_log('MXChat Save: Model is in whitelist, saving'); $options['model'] = sanitize_text_field($value); } else { //error_log('MXChat Save: Invalid model rejected: ' . $value); //error_log('MXChat Save: Allowed models: ' . print_r($allowed_models, true)); wp_send_json_error(['message' => esc_html__('Invalid model selected', 'mxchat')]); return; } } break; case 'openrouter_selected_model': //error_log('MXChat Save: Processing OpenRouter model: ' . $value); $options['openrouter_selected_model'] = sanitize_text_field($value); // Force immediate save for new keys //error_log('MXChat Save: OpenRouter model saved immediately'); break; case 'openrouter_selected_model_name': //error_log('MXChat Save: Processing OpenRouter model name: ' . $value); $options['openrouter_selected_model_name'] = sanitize_text_field($value); // Force immediate save for new keys //error_log('MXChat Save: OpenRouter model name saved immediately'); break; case 'openrouter_api_key': //error_log('MXChat Save: Processing OpenRouter API key'); $options['openrouter_api_key'] = sanitize_text_field($value); break; // REMOVED DUPLICATE case 'openrouter_selected_model_name' HERE! case 'additional_popular_questions': //error_log('MXChat Save: Processing additional_popular_questions'); $questions = json_decode($value, true); // No need for stripslashes here if (is_array($questions)) { $options[$field_name] = $questions; // Also update old option for backwards compatibility update_option('additional_popular_questions', $questions); //error_log('MXChat Save: Saved ' . count($questions) . ' additional questions'); } else { //error_log('MXChat Save: Failed to decode questions JSON'); } break; case 'email_blocker_header_content': //error_log('MXChat Save: Processing email_blocker_header_content'); // Allow HTML content but sanitize it safely $options[$field_name] = wp_kses_post($value); break; case 'email_blocker_button_text': //error_log('MXChat Save: Processing email_blocker_button_text'); $options[$field_name] = sanitize_text_field($value); break; case 'name_field_placeholder': //error_log('MXChat Save: Processing name_field_placeholder'); $options[$field_name] = sanitize_text_field($value); break; case 'similarity_threshold': //error_log('MXChat Save: Processing similarity_threshold'); // Validate and save - enforce min 20, max 85 $threshold = intval($value); if ($threshold < 20) $threshold = 20; if ($threshold > 85) $threshold = 85; $options[$field_name] = $threshold; break; case 'rag_sources_limit': //error_log('MXChat Save: Processing rag_sources_limit'); // Validate and save - enforce min 3, max 10, default 6 $rag_limit = intval($value); if ($rag_limit < 3) $rag_limit = 3; if ($rag_limit > 10) $rag_limit = 10; $options[$field_name] = $rag_limit; break; case 'rag_chunks_limit': // Validate and save - enforce min 8, max 20, default 15 $chunks_limit = intval($value); if ($chunks_limit < 8) $chunks_limit = 8; if ($chunks_limit > 20) $chunks_limit = 20; $options[$field_name] = $chunks_limit; break; case 'live_agent_status': //error_log('MXChat Save: Processing live_agent_status'); // Set the new value $options[$field_name] = ($value === 'on') ? 'on' : 'off'; break; case 'enable_web_search': //error_log('MXChat Save: Processing enable_web_search'); $options[$field_name] = ($value === 'on') ? 'on' : 'off'; break; case 'enable_woocommerce_integration': //error_log('MXChat Save: Processing enable_woocommerce_integration'); // Handle values that used to be 1/0 $options[$field_name] = ($value === 'on' || $value === '1') ? 'on' : 'off'; break; case 'post_type_visibility_mode': // Validate mode value $allowed_modes = array('all', 'include', 'exclude'); $options[$field_name] = in_array($value, $allowed_modes) ? $value : 'all'; break; case 'post_type_visibility_list': // Handle JSON array of post types $post_types = json_decode($value, true); if (is_array($post_types)) { // Sanitize each post type slug $options[$field_name] = array_map('sanitize_key', $post_types); } else { $options[$field_name] = array(); } break; case 'script_loading_strategy': // Validate script loading strategy value $allowed_strategies = array('default', 'defer', 'delay_1s', 'delay_3s', 'delay_5s', 'on_interaction'); $options[$field_name] = in_array($value, $allowed_strategies) ? $value : 'default'; break; case 'auto_retry_on_transient_error': // Boolean toggle — accept 1/0/on/off, default to '1' if any truthy value. $options[$field_name] = ($value === '1' || $value === 'on' || $value === 1 || $value === true) ? '1' : '0'; break; default: // Handle transcripts options if (strpos($name, 'mxchat_transcripts_options') !== false) { // Extract field name from mxchat_transcripts_options[field_name] if (preg_match('/mxchat_transcripts_options\[([^\]]+)\]/', $name, $matches)) { $field_name = $matches[1]; // Get current transcripts options $transcripts_options = get_option('mxchat_transcripts_options', array()); // Ensure it's an array if (!is_array($transcripts_options)) { $transcripts_options = array(); } // Handle checkbox values (convert 'on'/'off' to 1/0) if ($value === 'on' || $value === '1') { $transcripts_options[$field_name] = 1; } else if ($value === 'off' || $value === '0' || $value === '') { $transcripts_options[$field_name] = 0; } else { // For text/select fields, sanitize appropriately if ($field_name === 'mxchat_notification_email') { $transcripts_options[$field_name] = sanitize_email($value); } else { $transcripts_options[$field_name] = sanitize_text_field($value); } } // Use direct database update to bypass any filters global $wpdb; // Serialize the options array $serialized = maybe_serialize($transcripts_options); // Check if the option already exists in the database $existing = $wpdb->get_var("SELECT option_id FROM {$wpdb->options} WHERE option_name = 'mxchat_transcripts_options'"); if ($existing) { // Option exists, do an update $result = $wpdb->update( $wpdb->options, array('option_value' => $serialized), array('option_name' => 'mxchat_transcripts_options'), array('%s'), array('%s') ); } else { // Option doesn't exist (new install), do an insert $result = $wpdb->insert( $wpdb->options, array( 'option_name' => 'mxchat_transcripts_options', 'option_value' => $serialized, 'autoload' => 'yes' ), array('%s', '%s', '%s') ); } // Clear all caches after direct DB update wp_cache_delete('mxchat_transcripts_options', 'options'); wp_cache_delete('alloptions', 'options'); wp_cache_flush(); wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]); return; } } // Whole-chatbot global cap (sits in mxchat_options['rate_limits_global']). // Field names: mxchat_options[rate_limits_global][limit|timeframe|limit_custom] else if (strpos($name, 'mxchat_options[rate_limits_global]') !== false) { preg_match('/\[rate_limits_global\]\[(.*?)\]/', $name, $matches); if (isset($matches[1])) { $setting_key = $matches[1]; if (!isset($options['rate_limits_global']) || !is_array($options['rate_limits_global'])) { $options['rate_limits_global'] = array('limit' => 'unlimited', 'timeframe' => 'daily'); } if ($setting_key === 'limit') { // Selection from the preset dropdown. If __custom__, resolve from limit_custom; otherwise store directly. if ($value === '__custom__') { $custom = isset($options['rate_limits_global']['limit_custom']) ? (string) $options['rate_limits_global']['limit_custom'] : ''; if ($custom !== '' && ctype_digit($custom) && (int) $custom >= 1) { $options['rate_limits_global']['limit'] = $custom; } // else leave existing limit untouched until the custom value arrives } else { $options['rate_limits_global']['limit'] = $value; } } elseif ($setting_key === 'limit_custom') { $clean = preg_replace('/[^0-9]/', '', (string) $value); $options['rate_limits_global']['limit_custom'] = $clean; // Mirror a valid custom value into limit UNCONDITIONALLY (plan-74eb86). // The custom number input is only editable when the dropdown is on // "Custom…" (the toggle JS hides it for presets/unlimited) and autosave // sends one field per change event, so a limit_custom change only fires // in custom mode — there is no preset to clobber. The old guard required // limit to already be non-preset, which it isn't on a first-time custom // entry (the limit=__custom__ event arrives before limit_custom is set), // so the value never landed in limit on the first save and reverted on refresh. if ($clean !== '' && (int) $clean >= 1) { $options['rate_limits_global']['limit'] = $clean; } } elseif ($setting_key === 'timeframe') { $allowed_tf = array('hourly','daily','weekly','monthly'); $options['rate_limits_global']['timeframe'] = in_array($value, $allowed_tf, true) ? $value : 'daily'; } } } // First check for rate limits settings else if (strpos($name, 'mxchat_options[rate_limits]') !== false) { //error_log('MXChat Save: Detected rate_limits field: ' . $name); // Extract role ID and setting from the name preg_match('/\[rate_limits\]\[(.*?)\]\[(.*?)\]/', $name, $matches); //error_log('MXChat Save: Regex matches: ' . print_r($matches, true)); if (isset($matches[1]) && isset($matches[2])) { $role_id = $matches[1]; $setting_key = $matches[2]; // limit, timeframe, message, or limit_custom //error_log('MXChat Save: Role ID = ' . $role_id . ', Setting Key = ' . $setting_key); // Initialize rate_limits if it doesn't exist if (!isset($options['rate_limits'])) { // //error_log('MXChat Save: Initializing rate_limits array'); $options['rate_limits'] = []; } // Initialize role settings if it doesn't exist if (!isset($options['rate_limits'][$role_id])) { //error_log('MXChat Save: Initializing rate_limits for role: ' . $role_id); $options['rate_limits'][$role_id] = [ 'limit' => ($role_id === 'logged_out') ? '10' : '100', 'timeframe' => 'daily', 'message' => 'Rate limit exceeded. Please try again later.' ]; } if ($setting_key === 'limit') { if ($value === '__custom__') { // Pull the integer from limit_custom that may have arrived (or will arrive). $custom = isset($options['rate_limits'][$role_id]['limit_custom']) ? (string) $options['rate_limits'][$role_id]['limit_custom'] : ''; if ($custom !== '' && ctype_digit($custom) && (int) $custom >= 1) { $options['rate_limits'][$role_id]['limit'] = $custom; } } else { $options['rate_limits'][$role_id]['limit'] = $value; } } elseif ($setting_key === 'limit_custom') { $clean = preg_replace('/[^0-9]/', '', (string) $value); $options['rate_limits'][$role_id]['limit_custom'] = $clean; // Mirror a valid custom value into limit UNCONDITIONALLY — same reasoning // as the global branch above (plan-74eb86). The per-role custom input is // only editable in custom mode and autosave is one-field-per-change, so // this never clobbers a preset; it fixes the first-time-save revert. if ($clean !== '' && (int) $clean >= 1) { $options['rate_limits'][$role_id]['limit'] = $clean; } } else { // Update the specific setting (timeframe, message) $options['rate_limits'][$role_id][$setting_key] = $value; } //error_log('MXChat Save: Updated rate_limits[' . $role_id . '][' . $setting_key . '] = ' . $value); } else { //error_log('MXChat Save: Failed to parse rate_limits pattern: ' . $name); } } // Then check for role rate limits (old format) else if (strpos($name, 'mxchat_options[role_rate_limits]') !== false) { //error_log('MXChat Save: Processing role_rate_limits field: ' . $name); // Extract role ID from the name preg_match('/\[role_rate_limits\]\[(.*?)\]/', $name, $matches); //error_log('MXChat Save: Regex matches: ' . print_r($matches, true)); if (isset($matches[1])) { $role_id = $matches[1]; // Initialize role_rate_limits if it doesn't exist if (!isset($options['role_rate_limits'])) { //error_log('MXChat Save: Initializing role_rate_limits array'); $options['role_rate_limits'] = []; } // Update the specific role's rate limit $options['role_rate_limits'][$role_id] = sanitize_text_field($value); //error_log('MXChat Save: Updated role_rate_limits[' . $role_id . '] = ' . $value); } else { //error_log('MXChat Save: Failed to parse role_rate_limits pattern: ' . $name); } } // Handle toggles - check both extracted field_name and original name for toggle detection else if (strpos($field_name, 'toggle') !== false || in_array($field_name, [ 'chat_persistence_toggle', 'privacy_toggle', 'complianz_toggle', 'chat_toolbar_toggle', 'show_pdf_upload_button', 'show_word_upload_button', 'enable_streaming_toggle', 'contextual_awareness_toggle', 'citation_links_toggle', 'enable_email_block', 'enable_name_field', 'custom_provider_for_embeddings', 'custom_provider_for_images' ])) { //error_log('MXChat Save: Processing toggle: ' . $field_name); $options[$field_name] = ($value === 'on') ? 'on' : 'off'; } else { //error_log('MXChat Save: Processing standard field: ' . $field_name); // Store all other values directly using the extracted field name $options[$field_name] = $value; } break; } // Save all updates to the options array $updated = update_option('mxchat_options', $options); //error_log('MXChat Save: Update result: ' . ($updated ? 'success' : 'unchanged') . ' for field: ' . $name); //error_log('MXChat Save: Updated options array: ' . print_r($options, true)); // Log the save action if debug mode is enabled if ( class_exists( 'MxChat_Admin' ) ) { MxChat_Admin::mxchat_log_debug( 'settings_save', sprintf( 'Field saved: %s', $field_name ), array( 'field' => $field_name, 'updated' => $updated, ) ); } // Always return success even if WordPress says nothing changed // (which happens when the value is the same as before) wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]); } /** * Save the selected bot for knowledge base operations */ public function mxchat_save_selected_bot() { // Check nonce if (!wp_verify_nonce($_POST['nonce'] ?? '', 'mxchat_save_setting_nonce')) { wp_send_json_error('Invalid nonce'); } // Check permissions if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized'); } $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default'; // Save as user meta for the current user $user_id = get_current_user_id(); update_user_meta($user_id, 'mxchat_selected_knowledge_bot', $bot_id); // Also save as an option for site-wide default update_option('mxchat_current_knowledge_bot', $bot_id); // No cache clearing needed since we removed caching wp_send_json_success(array( 'message' => 'Bot selection saved', 'bot_id' => $bot_id )); } /** * Handles AJAX request for saving chat settings */ public function mxchat_save_prompts_setting_callback() { check_ajax_referer('mxchat_prompts_setting_nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => esc_html__('Unauthorized', 'mxchat')]); } $name = isset($_POST['name']) ? $_POST['name'] : ''; $value = isset($_POST['value']) ? stripslashes($_POST['value']) : ''; //error_log('[MXCHAT-PROMPTS] Saving setting: ' . $name . ' = ' . $value); if (empty($name)) { wp_send_json_error(['message' => esc_html__('Invalid field name', 'mxchat')]); } // Handle Pinecone settings - BYPASS WORDPRESS SANITIZATION if (strpos($name, 'mxchat_pinecone_addon_options') !== false) { //error_log('[MXCHAT-PROMPTS] Processing Pinecone setting: ' . $name); // Extract the field name if (preg_match('/mxchat_pinecone_addon_options\[([^\]]+)\]/', $name, $matches)) { $field_name = $matches[1]; //error_log('[MXCHAT-PROMPTS] Extracted field name: ' . $field_name); // Get current options directly from database - NO WordPress filters global $wpdb; $current_options_raw = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", 'mxchat_pinecone_addon_options' ) ); // FIX: Handle the case where the option doesn't exist yet if ($current_options_raw === null) { // Option doesn't exist, create it with default values $current_options = array( 'mxchat_use_pinecone' => '0', 'mxchat_pinecone_api_key' => '', 'mxchat_pinecone_host' => '', 'mxchat_pinecone_index' => '', 'mxchat_pinecone_environment' => '' ); //error_log('[MXCHAT-PROMPTS] Option does not exist, creating with defaults'); } else { // Unserialize the raw data $current_options = maybe_unserialize($current_options_raw); if (!is_array($current_options)) { // Fallback to defaults if unserialization fails $current_options = array( 'mxchat_use_pinecone' => '0', 'mxchat_pinecone_api_key' => '', 'mxchat_pinecone_host' => '', 'mxchat_pinecone_index' => '', 'mxchat_pinecone_environment' => '' ); //error_log('[MXCHAT-PROMPTS] Failed to unserialize, using defaults'); } } //error_log('[MXCHAT-PROMPTS] Current options from DB: ' . print_r($current_options, true)); // Update the specific field with proper sanitization switch ($field_name) { case 'mxchat_use_pinecone': $new_value = ($value === '1') ? '1' : '0'; break; case 'mxchat_pinecone_api_key': case 'mxchat_pinecone_host': case 'mxchat_pinecone_index': case 'mxchat_pinecone_environment': $new_value = sanitize_text_field($value); if ($field_name === 'mxchat_pinecone_host') { $new_value = str_replace(['https://', 'http://'], '', $new_value); } break; default: wp_send_json_error(['message' => esc_html__('Unknown Pinecone field', 'mxchat')]); } $current_options[$field_name] = $new_value; //error_log('[MXCHAT-PROMPTS] New value for ' . $field_name . ': "' . $new_value . '"'); //error_log('[MXCHAT-PROMPTS] Updated options: ' . print_r($current_options, true)); // Save directly to database to bypass WordPress sanitization $serialized_options = maybe_serialize($current_options); // FIX: Use INSERT ... ON DUPLICATE KEY UPDATE or separate INSERT/UPDATE logic $option_exists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name = %s", 'mxchat_pinecone_addon_options' ) ); if ($option_exists > 0) { // Update existing option $save_result = $wpdb->update( $wpdb->options, array('option_value' => $serialized_options), array('option_name' => 'mxchat_pinecone_addon_options'), array('%s'), array('%s') ); //error_log('[MXCHAT-PROMPTS] Updated existing option, result: ' . ($save_result !== false ? 'SUCCESS' : 'FAILED')); } else { // Insert new option $save_result = $wpdb->insert( $wpdb->options, array( 'option_name' => 'mxchat_pinecone_addon_options', 'option_value' => $serialized_options, 'autoload' => 'yes' ), array('%s', '%s', '%s') ); //error_log('[MXCHAT-PROMPTS] Inserted new option, result: ' . ($save_result !== false ? 'SUCCESS' : 'FAILED')); } // Clear any WordPress option cache to ensure get_option() returns fresh data wp_cache_delete('mxchat_pinecone_addon_options', 'options'); // IMPROVED VERIFICATION - Check if the database operation succeeded if ($save_result !== false) { // Double-check by reading fresh from database $verification_raw = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", 'mxchat_pinecone_addon_options' ) ); $verification_options = maybe_unserialize($verification_raw); $verified_value = isset($verification_options[$field_name]) ? $verification_options[$field_name] : 'NOT_FOUND'; //error_log('[MXCHAT-PROMPTS] Final verification - Expected: "' . $new_value . '", Got: "' . $verified_value . '"'); // Use loose comparison (==) instead of strict (===) to avoid type issues if ($verified_value == $new_value || $save_result > 0) { wp_send_json_success(['message' => esc_html__('Pinecone setting saved', 'mxchat')]); } else { // Still return success if the DB operation worked, even if verification is quirky //error_log('[MXCHAT-PROMPTS] Verification mismatch but DB operation succeeded'); wp_send_json_success(['message' => esc_html__('Pinecone setting saved (DB success)', 'mxchat')]); } } else { wp_send_json_error(['message' => esc_html__('Database save failed', 'mxchat')]); } } else { wp_send_json_error(['message' => esc_html__('Invalid field name format', 'mxchat')]); } return; // Exit here for Pinecone settings } // Handle auto-sync settings (existing functionality) if (strpos($name, 'mxchat_auto_sync_') === 0) { $value = ($value === 'on' || $value === '1') ? '1' : '0'; $updated = update_option($name, $value); if ($updated || get_option($name) === $value) { wp_send_json_success(['message' => esc_html__('Auto-sync setting saved', 'mxchat')]); } else { wp_send_json_error(['message' => esc_html__('No changes detected', 'mxchat')]); } } // Handle chunking settings - use direct DB access to bypass WordPress filters if (strpos($name, 'mxchat_chunk') === 0 || $name === 'mxchat_chunking_enabled') { global $wpdb; // Get current options directly from database $current_options_raw = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", 'mxchat_options' ) ); $options = $current_options_raw !== null ? maybe_unserialize($current_options_raw) : array(); if (!is_array($options)) { $options = array(); } // Update the specific chunking field if ($name === 'mxchat_chunking_enabled') { $options['chunking_enabled'] = in_array($value, array('on', '1', 'true', true), true); } elseif ($name === 'mxchat_chunk_size') { $options['chunk_size'] = max(1000, min(10000, intval($value))); } // Save directly to database $serialized_options = maybe_serialize($options); $option_exists = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name = %s", 'mxchat_options' ) ); if ($option_exists > 0) { $save_result = $wpdb->update( $wpdb->options, array('option_value' => $serialized_options), array('option_name' => 'mxchat_options'), array('%s'), array('%s') ); } else { $save_result = $wpdb->insert( $wpdb->options, array( 'option_name' => 'mxchat_options', 'option_value' => $serialized_options, 'autoload' => 'yes' ), array('%s', '%s', '%s') ); } // Clear object cache for this option wp_cache_delete('mxchat_options', 'options'); if ($save_result !== false) { wp_send_json_success(['message' => esc_html__('Chunking setting saved', 'mxchat')]); } else { wp_send_json_error(['message' => esc_html__('Failed to save chunking setting', 'mxchat')]); } return; } // Handle ACF field exclusion toggles if (strpos($name, 'mxchat_acf_field_') === 0) { // Extract field name from the input name (e.g., mxchat_acf_field_private_notes -> private_notes) $field_name = str_replace('mxchat_acf_field_', '', $name); $is_enabled = ($value === 'on' || $value === '1'); // Get current excluded fields $excluded_fields = get_option('mxchat_acf_excluded_fields', array()); if (!is_array($excluded_fields)) { $excluded_fields = array(); } if ($is_enabled) { // Remove from exclusion list (field should be included) $excluded_fields = array_values(array_diff($excluded_fields, array($field_name))); } else { // Add to exclusion list (field should be excluded) if (!in_array($field_name, $excluded_fields)) { $excluded_fields[] = $field_name; } } $updated = update_option('mxchat_acf_excluded_fields', $excluded_fields); if ($updated || true) { // Always report success since the state may already be correct wp_send_json_success([ 'message' => $is_enabled ? sprintf(esc_html__('Field "%s" will be included in imports', 'mxchat'), $field_name) : sprintf(esc_html__('Field "%s" will be excluded from imports', 'mxchat'), $field_name) ]); } else { wp_send_json_error(['message' => esc_html__('Failed to save ACF field setting', 'mxchat')]); } return; } // Handle custom post meta whitelist if ($name === 'mxchat_custom_meta_whitelist') { $updated = update_option('mxchat_custom_meta_whitelist', sanitize_textarea_field($value)); if ($updated || true) { // Always report success since the state may already be correct wp_send_json_success([ 'message' => esc_html__('Custom meta whitelist saved', 'mxchat') ]); } else { wp_send_json_error(['message' => esc_html__('Failed to save custom meta whitelist', 'mxchat')]); } return; } // Handle other prompts options $options = get_option('mxchat_prompts_options', []); $options[$name] = $value; $updated = update_option('mxchat_prompts_options', $options); if ($updated) { wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]); } else { wp_send_json_error(['message' => esc_html__('No changes detected', 'mxchat')]); } } /** * Handles AJAX request for Pinecone settings migration */ public function ajax_migrate_pinecone_settings() { // Verify nonce if (!wp_verify_nonce($_POST['_ajax_nonce'] ?? '', 'mxchat_save_setting_nonce')) { wp_send_json_error('Invalid nonce'); } // Check permissions if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized access'); } // Check if old Pinecone addon options exist $old_options = get_option('mxchat_pinecone_addon_options', array()); if (empty($old_options)) { wp_send_json_success(array('migrated' => false, 'message' => 'No old settings found')); } // Get current core plugin options $current_options = get_option('mxchat_pinecone_addon_options', array()); // Only migrate if core options are empty or if explicitly requested $should_migrate = empty($current_options) || (empty($current_options['mxchat_pinecone_api_key']) && !empty($old_options['mxchat_pinecone_api_key'])); if ($should_migrate) { // Migrate settings with proper sanitization $migrated_options = array( 'mxchat_use_pinecone' => $old_options['mxchat_use_pinecone'] ?? '0', 'mxchat_pinecone_api_key' => sanitize_text_field($old_options['mxchat_pinecone_api_key'] ?? ''), 'mxchat_pinecone_host' => sanitize_text_field($old_options['mxchat_pinecone_host'] ?? ''), 'mxchat_pinecone_index' => sanitize_text_field($old_options['mxchat_pinecone_index'] ?? ''), 'mxchat_pinecone_environment' => sanitize_text_field($old_options['mxchat_pinecone_environment'] ?? '') ); update_option('mxchat_pinecone_addon_options', $migrated_options); wp_send_json_success(array( 'migrated' => true, 'message' => 'Settings migrated successfully from Pinecone add-on' )); } else { wp_send_json_success(array( 'migrated' => false, 'message' => 'Settings already exist in core plugin' )); } } // ======================================== // LICENSE AJAX HANDLERS // ======================================== /** * Validates and activates chat license via AJAX */ public function mxchat_handle_activate_license() { // Check nonce if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) { wp_send_json_error(esc_html__('Invalid security token', 'mxchat')); return; } // Verify user capabilities if (!current_user_can('manage_options')) { wp_send_json_error(esc_html__('Unauthorized access', 'mxchat')); return; } $license_key = isset($_POST['mxchat_activation_key']) ? sanitize_text_field($_POST['mxchat_activation_key']) : ''; $customer_email = isset($_POST['mxchat_pro_email']) ? sanitize_email($_POST['mxchat_pro_email']) : ''; if (empty($license_key) || empty($customer_email)) { wp_send_json_error(esc_html__('Email or License Key is missing', 'mxchat')); return; } $product_id = 'MxChatPRO'; $domain = parse_url(home_url(), PHP_URL_HOST); // Get the current domain // Call WooCommerce Software API for activation (not just validation) $response = wp_remote_get( add_query_arg( array( 'wc-api' => 'software-api', 'request' => 'activation', 'email' => $customer_email, 'license_key' => $license_key, 'product_id' => $product_id, 'instance' => $domain, // THIS IS KEY - include the domain as instance 'platform' => 'wordpress' // Optional but good to include ), 'https://mxchat.ai/' ), array( 'timeout' => 60, 'sslverify' => true ) ); if (is_wp_error($response)) { $error_message = $response->get_error_message(); //error_log('MxChat License Activation Error: ' . $error_message); wp_send_json_error(esc_html__('Activation failed due to a server error: ', 'mxchat') . $error_message); return; } $response_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); // Log response for debugging //error_log('MxChat License Response Code: ' . $response_code); //error_log('MxChat License Response Body: ' . $body); if ($response_code !== 200) { wp_send_json_error(esc_html__('Server returned error code: ', 'mxchat') . $response_code); return; } $data = json_decode($body); if ($data && isset($data->activated) && $data->activated) { // Success - save local options update_option('mxchat_license_status', 'active'); update_option('mxchat_pro_email', $customer_email); update_option('mxchat_activation_key', $license_key); delete_option('mxchat_license_error'); // Also track on your website (this is your existing domain tracking) $this->track_domain_on_website($license_key, $customer_email, $domain); wp_send_json_success(array('message' => esc_html__('License activated successfully', 'mxchat'))); } else { $error_message = isset($data->error) ? $data->error : esc_html__('Activation failed', 'mxchat'); update_option('mxchat_license_status', 'inactive'); update_option('mxchat_license_error', $error_message); //error_log('MxChat Activation failed: ' . $error_message); wp_send_json_error($error_message); } } /** * Track domain on your website (separate from WooCommerce activation) */ private function track_domain_on_website($license_key, $email, $domain) { // This calls your website's tracking API wp_remote_post('https://mxchat.ai/mxchat-api/activate-license', array( 'body' => array( 'mxchat_pro_email' => $email, 'mxchat_activation_key' => $license_key, 'domain' => $domain ), 'timeout' => 10, 'sslverify' => true )); } /** * Validates license via AJAX with email and key */ public function mxchat_check_license_status() { // Verify nonce if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) { wp_send_json_error('Security check failed'); return; } // Add isset checks for safety $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : ''; $key = isset($_POST['key']) ? sanitize_text_field($_POST['key']) : ''; // Check if this license is actually active in your system $is_active = (get_option('mxchat_license_status') === 'active' && get_option('mxchat_pro_email') === $email && get_option('mxchat_activation_key') === $key); wp_send_json(array( 'is_active' => $is_active )); } /** * Handle license deactivation - Complete version for plugin */ function mxchat_deactivate_license() { // Add debugging //error_log('MxChat deactivate function called'); // Check nonce if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) { //error_log('MxChat deactivate: Nonce check failed'); wp_send_json_error('Security check failed.'); return; } //error_log('MxChat deactivate: Nonce check passed'); $license_key = get_option('mxchat_activation_key'); $email = get_option('mxchat_pro_email'); $domain = parse_url(home_url(), PHP_URL_HOST); //error_log('MxChat deactivate: License: ' . $license_key . ', Email: ' . $email . ', Domain: ' . $domain); if (empty($license_key) || empty($email)) { //error_log('MxChat deactivate: No active license found'); wp_send_json_error('No active license found.'); return; } // Clear local license data first delete_option('mxchat_license_status'); delete_option('mxchat_pro_email'); delete_option('mxchat_activation_key'); delete_option('mxchat_license_error'); //error_log('MxChat deactivate: Local data cleared'); // Notify your website's API to properly deactivate $response = wp_remote_post('https://mxchat.ai/mxchat-api/deactivate-license', array( 'body' => array( 'license_key' => $license_key, 'email' => $email, 'domain' => $domain ), 'timeout' => 15, 'sslverify' => true )); if (is_wp_error($response)) { //error_log('MxChat deactivate: Server error - ' . $response->get_error_message()); wp_send_json_success(array( 'message' => 'License deactivated locally. Server could not be contacted to free activation slot.', 'server_notified' => false )); return; } $response_body = wp_remote_retrieve_body($response); $response_data = json_decode($response_body, true); //error_log('MxChat deactivate: Server response - ' . $response_body); if (isset($response_data['success']) && $response_data['success']) { //error_log('MxChat deactivate: Success with server notification'); wp_send_json_success(array( 'message' => 'License deactivated successfully. Activation slot has been freed up.', 'server_notified' => true )); } else { //error_log('MxChat deactivate: Server responded but deactivation may have failed'); wp_send_json_success(array( 'message' => 'License deactivated locally. Please check your account dashboard to verify the activation was freed.', 'server_notified' => false )); } } // ======================================== // ACTIONS & INTENTS AJAX HANDLERS // ======================================== /** * Validates nonce and returns JSON error on failure */ public function mxchat_toggle_action() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_actions_nonce')) { wp_send_json_error(array('message' => 'Security check failed')); return; } // Check permissions if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'Permission denied')); return; } // Validate params $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0; $enabled = isset($_POST['enabled']) ? (bool)$_POST['enabled'] : false; if (!$intent_id) { wp_send_json_error(array('message' => 'Invalid action ID')); return; } // Update the intent/action status in the database global $wpdb; $table_name = $wpdb->prefix . 'mxchat_intents'; // Using the 'enabled' field - add this field if it doesn't exist $result = $wpdb->update( $table_name, array('enabled' => $enabled ? 1 : 0), array('id' => $intent_id), array('%d'), array('%d') ); if ($result === false) { wp_send_json_error(array('message' => 'Database error')); return; } wp_send_json_success(); } /** * Validates permissions for AJAX request handling */ public function mxchat_update_intent_threshold() { // Check permissions if (!current_user_can('manage_options')) { if (wp_doing_ajax()) { wp_send_json_error(array('message' => 'Unauthorized user')); return; } wp_die(esc_html__('Unauthorized user', 'mxchat')); } // Verify nonce check_admin_referer('mxchat_update_intent_threshold_nonce'); // Process the update if we have valid data if (isset($_POST['intent_id'], $_POST['intent_threshold'])) { global $wpdb; $table_name = $wpdb->prefix . 'mxchat_intents'; $intent_id = intval($_POST['intent_id']); $threshold_percentage = max(70, min(95, intval($_POST['intent_threshold']))); $similarity_threshold = $threshold_percentage / 100; $result = $wpdb->update( $table_name, ['similarity_threshold' => $similarity_threshold], ['id' => $intent_id], ['%f'], ['%d'] ); // Handle AJAX requests if (wp_doing_ajax()) { if ($result === false) { wp_send_json_error(array('message' => 'Failed to update threshold')); } else { wp_send_json_success(array('threshold' => $threshold_percentage)); } return; } } // Redirect for regular form submissions wp_safe_redirect(admin_url('admin.php?page=mxchat-actions&updated=true')); exit; } // ======================================== // HELPER METHODS // ======================================== /** * Returns a specific nonce action string */ private function mxchat_get_nonce_action() { return 'mxchat_license_nonce'; } /** * Check API key status for all providers */ public function mxchat_check_api_keys() { // Check nonce if (!wp_verify_nonce($_POST['nonce'] ?? '', 'mxchat_save_setting_nonce')) { wp_send_json_error('Invalid nonce'); } // Check permissions if (!current_user_can('manage_options')) { wp_send_json_error('Unauthorized'); } // Get current options $options = get_option('mxchat_options', array()); // Check which API keys are present $api_key_status = array( 'openai' => !empty($options['api_key']), 'claude' => !empty($options['claude_api_key']), 'xai' => !empty($options['xai_api_key']), 'deepseek' => !empty($options['deepseek_api_key']), 'gemini' => !empty($options['gemini_api_key']), 'openrouter' => !empty($options['openrouter_api_key']), 'voyage' => !empty($options['voyage_api_key']) ); wp_send_json_success($api_key_status); } // ======================================== // DEBUG & OPTIMIZATION AJAX HANDLERS // ======================================== /** * Toggle debug mode on/off */ public function mxchat_toggle_debug_mode_callback() { // Verify nonce if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) ); } // Check permissions if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) ); } $enabled = isset( $_POST['enabled'] ) && $_POST['enabled'] === 'on'; $options = get_option( 'mxchat_options', array() ); if ( $enabled ) { $options['debug_mode'] = 'on'; update_option( 'mxchat_options', $options ); MxChat_Admin::mxchat_log_debug( 'debug_mode', 'Debug mode enabled' ); } else { // Log before disabling MxChat_Admin::mxchat_log_debug( 'debug_mode', 'Debug mode disabled' ); $options['debug_mode'] = 'off'; update_option( 'mxchat_options', $options ); } wp_send_json_success( array( 'message' => $enabled ? esc_html__( 'Debug mode enabled', 'mxchat' ) : esc_html__( 'Debug mode disabled', 'mxchat' ), 'enabled' => $enabled, ) ); } /** * Get the debug log entries */ public function mxchat_get_debug_log_callback() { // Verify nonce if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) ); } // Check permissions if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) ); } $log = MxChat_Admin::mxchat_get_debug_log(); wp_send_json_success( array( 'log' => $log, 'count' => count( $log ), ) ); } /** * Clear the debug log */ public function mxchat_clear_debug_log_callback() { // Verify nonce if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) ); } // Check permissions if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) ); } MxChat_Admin::mxchat_clear_debug_log(); // Log that the log was cleared (this will be the first entry in the new log) MxChat_Admin::mxchat_log_debug( 'debug_log', 'Debug log cleared by user' ); wp_send_json_success( array( 'message' => esc_html__( 'Debug log cleared', 'mxchat' ) ) ); } /** * Export settings as JSON */ public function mxchat_export_settings_callback() { // Verify nonce if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) ); } // Check permissions if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) ); } $export = MxChat_Admin::mxchat_export_settings(); // Log the export MxChat_Admin::mxchat_log_debug( 'settings_export', 'Settings exported by user' ); wp_send_json_success( array( 'settings' => $export, 'filename' => 'mxchat-settings-' . gmdate( 'Y-m-d-His' ) . '.json', ) ); } /** * Reset all settings to defaults */ public function mxchat_reset_all_settings_callback() { // Verify nonce if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) ); } // Check permissions if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) ); } // Require confirmation code $confirmation = isset( $_POST['confirmation'] ) ? sanitize_text_field( wp_unslash( $_POST['confirmation'] ) ) : ''; if ( strtoupper( $confirmation ) !== 'RESET' ) { wp_send_json_error( array( 'message' => esc_html__( 'Invalid confirmation code. Please type RESET to confirm.', 'mxchat' ) ) ); } // Perform the reset MxChat_Admin::mxchat_reset_all_settings(); wp_send_json_success( array( 'message' => esc_html__( 'All settings have been reset to defaults. The page will reload.', 'mxchat' ) ) ); } /** * Reset the global rate-limit usage counter to zero on demand. * * Zeroes the WP option mxchat_chat_limit__global that the integrator * increments per message, then returns a freshly-formatted readout string * so the settings page can update without a reload. Does NOT change any * enforcement config — purely clears the running counter. */ public function mxchat_reset_global_rate_limit_callback() { // Verify nonce if ( ! check_ajax_referer( 'mxchat_reset_global_usage', '_ajax_nonce', false ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) ); } // Check permissions if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) ); } // Resolve the per-bot counter key the same way the integrator does. $bot_id = isset( $_POST['bot_id'] ) ? sanitize_key( wp_unslash( $_POST['bot_id'] ) ) : 'default'; $safe_bot = preg_replace( '/[^a-zA-Z0-9_]/', '_', $bot_id ); if ( $safe_bot === '' ) { $safe_bot = 'default'; } $option_key = 'mxchat_chat_limit_' . $safe_bot . '_global'; $now = time(); update_option( $option_key, array( 'count' => 0, 'timestamp' => $now ) ); // Recompute the display string so the front-end can update in place. $all_options = get_option( 'mxchat_options', array() ); $global_cfg = isset( $all_options['rate_limits_global'] ) && is_array( $all_options['rate_limits_global'] ) ? $all_options['rate_limits_global'] : array(); $limit_raw = isset( $global_cfg['limit'] ) ? (string) $global_cfg['limit'] : 'unlimited'; // Defensive: if a raw __custom__ ever slips through, fall back to the custom value. if ( ! ctype_digit( $limit_raw ) && isset( $global_cfg['limit_custom'] ) && ctype_digit( (string) $global_cfg['limit_custom'] ) ) { $limit_raw = (string) $global_cfg['limit_custom']; } $timeframe = isset( $global_cfg['timeframe'] ) ? (string) $global_cfg['timeframe'] : 'daily'; $windows = array( 'hourly' => 3600, 'daily' => 86400, 'weekly' => 604800, 'monthly' => 2592000 ); $window = isset( $windows[ $timeframe ] ) ? $windows[ $timeframe ] : 86400; $reset_at = $now + $window; $limit_int = ctype_digit( $limit_raw ) ? (int) $limit_raw : 0; $text = sprintf( /* translators: 1: used count, 2: limit, 3: remaining, 4: human-readable time until reset */ esc_html__( '%1$s of %2$s used · %3$s left · resets in %4$s', 'mxchat' ), number_format_i18n( 0 ), number_format_i18n( $limit_int ), number_format_i18n( $limit_int ), human_time_diff( $now, $reset_at ) ); wp_send_json_success( array( 'count' => 0, 'limit' => $limit_int, 'left' => $limit_int, 'reset_at' => $reset_at, 'pct' => 0, 'text' => $text, 'message' => esc_html__( 'Usage counter reset.', 'mxchat' ), ) ); } } // Initialize the AJAX handler new MxChat_Ajax_Handler();