'', 'enable_embed' => false, 'embed_code' => '', 'enable_woocommerce_sync' => false ); /** * Get all settings */ public static function get_settings() { $settings = array(); // Get each setting individually foreach (self::$setting_keys as $key) { $option_name = self::OPTION_PREFIX . $key; $default = isset(self::$default_settings[$key]) ? self::$default_settings[$key] : null; $settings[$key] = get_option($option_name, $default); } return $settings; } /** * Get specific setting */ public static function get_setting($key, $default = null) { $option_name = self::OPTION_PREFIX . $key; // Use provided default or get from defaults array if ($default === null && isset(self::$default_settings[$key])) { $default = self::$default_settings[$key]; } return get_option($option_name, $default); } /** * Update all settings */ public static function update_settings($settings) { $validated = self::validate_settings($settings); $updated = true; // Update each setting individually foreach ($validated as $key => $value) { $option_name = self::OPTION_PREFIX . $key; $result = update_option($option_name, $value); // If any update fails, mark the overall update as failed if (!$result) { $updated = false; } } return $updated; } /** * Update a single setting */ public static function update_setting($key, $value) { // Validate single setting $validated = self::validate_settings(array($key => $value)); if (isset($validated[$key])) { $option_name = self::OPTION_PREFIX . $key; return update_option($option_name, $validated[$key]); } return false; } /** * Delete settings */ public static function delete_settings() { $deleted = true; // Delete each setting individually foreach (self::$setting_keys as $key) { $option_name = self::OPTION_PREFIX . $key; $result = delete_option($option_name); // If any deletion fails, mark the overall deletion as failed if (!$result) { $deleted = false; } } return $deleted; } /** * Reset settings to default */ public static function reset_settings() { return self::update_settings(self::$default_settings); } /** * Validate settings */ public static function validate_settings($settings) { $validated = array(); // Validate API key if (isset($settings['api_key'])) { $validated['api_key'] = sanitize_text_field($settings['api_key']); } // Validate enable embed if (isset($settings['enable_embed'])) { $validated['enable_embed'] = (bool) $settings['enable_embed']; } // Validate embed code - safely handle script tags if (isset($settings['embed_code'])) { $validated['embed_code'] = self::sanitize_embed_code($settings['embed_code']); } // Validate enable_woocommerce_sync if (isset($settings['enable_woocommerce_sync'])) { $validated['enable_woocommerce_sync'] = (bool) $settings['enable_woocommerce_sync']; } // Validate website_id if (isset($settings['website_id'])) { $validated['website_id'] = sanitize_text_field($settings['website_id']); } return $validated; } /** * Public method to validate settings (wrapper for private validate_settings) */ public static function sanitize_settings($settings) { return self::validate_settings($settings); } /** * Safely sanitize embed code for Chatweb script tags * * @param string $embed_code Raw embed code from API * @return string Sanitized embed code */ public static function sanitize_embed_code($embed_code) { // Ensure it's a string if (!is_string($embed_code)) { return ''; } // Trim whitespace $embed_code = trim($embed_code); // Check if empty if (empty($embed_code)) { return ''; } // Validate that it's a Chatweb script tag if (!self::is_valid_aiflab_chatweb_embed($embed_code)) { return ''; } // Return sanitized embed code (WordPress handles escaping automatically) return $embed_code; } /** * Validate that embed code is a valid Chatweb script tag * * @param string $embed_code * @return bool */ private static function is_valid_aiflab_chatweb_embed($embed_code) { // Expected pattern: // Basic checks if (!str_contains($embed_code, '')) { return false; } // Check for Chatweb domain (support multiple domains and subdomains) // Accept: widget.chatweb.io, widget-dev.chatweb.to, chatweb.io, chatweb.to, etc. $valid_domains = array( 'widget.chatweb.io', 'widget-dev.chatweb.to', 'widget.chatweb.to', 'chatweb.io', 'chatweb.to' ); $has_valid_domain = false; foreach ($valid_domains as $domain) { if (str_contains($embed_code, $domain)) { $has_valid_domain = true; break; } } // Also check for any chatweb subdomain pattern if (!$has_valid_domain && preg_match('/[a-z0-9-]+\.chatweb\.(io|to)/i', $embed_code)) { $has_valid_domain = true; } if (!$has_valid_domain) { return false; } // Check for required attributes if (!str_contains($embed_code, 'data-token')) { return false; } // Additional security: ensure no dangerous content // Note: 'data-token' and 'data-cfasync' are valid attributes, so we need to be careful $dangerous_patterns = array( 'javascript:', // But allow data-token and data-cfasync 'data:url', // Only block data: URLs, not data-* attributes 'vbscript:', 'onload=', // Event handlers 'onerror=', 'onclick=', 'onmouseover=', ' self::API_BASE_URL, 'version' => self::API_VERSION, 'timeout' => self::API_TIMEOUT ); } /** * Check if embed is enabled */ public static function is_embed_enabled() { return (bool) self::get_setting('enable_embed', false); } /** * Get embed code safely with fallback methods */ public static function get_embed_code() { // Method 1: Lấy từ cài đặt thường $option_name = self::OPTION_PREFIX . 'embed_code'; // Clear cache to ensure we get fresh value from database wp_cache_delete($option_name, 'options'); $embed_code = get_option($option_name, ''); if (!empty($embed_code)) { // Validate again on retrieval for extra security if (!self::is_valid_aiflab_chatweb_embed($embed_code)) { self::clear_embed_code(); return ''; } return $embed_code; } // Method 2: Fallback to base64 method (only if main option is empty) return self::get_embed_code_with_fallback(); } /** * Set embed code safely with enhanced debugging */ public static function set_embed_code($embed_code) { // Log original embed code for debugging self::log_embed_code_debug('set_embed_code_input', array( 'original_embed_code' => $embed_code, 'length' => strlen($embed_code), 'is_string' => is_string($embed_code) )); // Sanitize embed code trước khi lưu $sanitized_embed_code = self::sanitize_embed_code($embed_code); // Log sanitization result self::log_embed_code_debug('sanitize_result', array( 'sanitized_embed_code' => $sanitized_embed_code, 'sanitized_length' => strlen($sanitized_embed_code), 'is_empty' => empty($sanitized_embed_code), 'original_length' => strlen($embed_code), 'matches_original' => ($sanitized_embed_code === $embed_code) )); if (empty($sanitized_embed_code)) { // Log why sanitization failed // Check validation manually $is_valid = self::is_valid_aiflab_chatweb_embed($embed_code); self::log_embed_code_debug('sanitize_failed', array( 'original_embed_code' => $embed_code, 'is_valid_aiflab_chatweb_embed' => $is_valid, 'has_script_tag' => strpos($embed_code, ' strpos($embed_code, '') !== false, 'has_widget_chatweb' => (strpos($embed_code, 'widget.chatweb.io') !== false || strpos($embed_code, 'widget-dev.chatweb.to') !== false || strpos($embed_code, 'chatweb.io') !== false || strpos($embed_code, 'chatweb.to') !== false), 'has_data_token' => strpos($embed_code, 'data-token') !== false )); return false; } // Lưu vào CẢ HAI options để đảm bảo có giá trị // Method 1: Lưu vào option chính $option_name = self::OPTION_PREFIX . 'embed_code'; $option_name_b64 = self::OPTION_PREFIX . 'embed_code_b64'; // Clear cache trước khi lưu wp_cache_delete($option_name, 'options'); wp_cache_delete($option_name_b64, 'options'); // Use $wpdb directly to ensure options are created/updated global $wpdb; // Check if options exist in database (not from cache) $option_exists = $wpdb->get_var($wpdb->prepare( "SELECT option_id FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $option_name )); $option_b64_exists = $wpdb->get_var($wpdb->prepare( "SELECT option_id FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $option_name_b64 )); // Encode to base64 for backup $encoded_embed_code = base64_encode($sanitized_embed_code); // Log before saving self::log_embed_code_debug('before_save', array( 'option_exists' => !empty($option_exists), 'option_b64_exists' => !empty($option_b64_exists), 'sanitized_length' => strlen($sanitized_embed_code), 'encoded_length' => strlen($encoded_embed_code) )); // Save main option using $wpdb directly $result_main = false; $wpdb_error_main = ''; if ($option_exists) { // Update existing option $update_result = $wpdb->update( $wpdb->options, array('option_value' => $sanitized_embed_code), array('option_name' => $option_name), array('%s'), array('%s') ); $result_main = ($update_result !== false); if ($update_result === false && !empty($wpdb->last_error)) { $wpdb_error_main = $wpdb->last_error; } } else { // Insert new option $insert_result = $wpdb->insert( $wpdb->options, array( 'option_name' => $option_name, 'option_value' => $sanitized_embed_code, 'autoload' => 'no' ), array('%s', '%s', '%s') ); $result_main = ($insert_result !== false); if ($insert_result === false && !empty($wpdb->last_error)) { $wpdb_error_main = $wpdb->last_error; } } // Save base64 option using $wpdb directly $result_b64 = false; $wpdb_error_b64 = ''; if ($option_b64_exists) { // Update existing option $update_result = $wpdb->update( $wpdb->options, array('option_value' => $encoded_embed_code), array('option_name' => $option_name_b64), array('%s'), array('%s') ); $result_b64 = ($update_result !== false); if ($update_result === false && !empty($wpdb->last_error)) { $wpdb_error_b64 = $wpdb->last_error; } } else { // Insert new option $insert_result = $wpdb->insert( $wpdb->options, array( 'option_name' => $option_name_b64, 'option_value' => $encoded_embed_code, 'autoload' => 'no' ), array('%s', '%s', '%s') ); $result_b64 = ($insert_result !== false); if ($insert_result === false && !empty($wpdb->last_error)) { $wpdb_error_b64 = $wpdb->last_error; } } // Log save results self::log_embed_code_debug('save_results', array( 'result_main' => $result_main, 'result_b64' => $result_b64, 'wpdb_error_main' => $wpdb_error_main, 'wpdb_error_b64' => $wpdb_error_b64, 'option_exists_before' => !empty($option_exists), 'option_b64_exists_before' => !empty($option_b64_exists) )); // Verify what was saved immediately by querying database directly wp_cache_delete($option_name, 'options'); wp_cache_delete($option_name_b64, 'options'); // Query database directly to verify (bypass cache) $saved_code = $wpdb->get_var($wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $option_name )); $saved_b64 = $wpdb->get_var($wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $option_name_b64 )); // If main option is empty but base64 is saved, try to restore from base64 if (empty($saved_code) && !empty($saved_b64)) { // Decode from base64 and verify $decoded = base64_decode($saved_b64); if (!empty($decoded) && $decoded === $sanitized_embed_code) { // Try using $wpdb directly to bypass WordPress filters // Check if option exists $option_exists_check = $wpdb->get_var($wpdb->prepare( "SELECT option_id FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $option_name )); if ($option_exists_check) { // Update existing option $wpdb->update( $wpdb->options, array('option_value' => $sanitized_embed_code), array('option_name' => $option_name), array('%s'), array('%s') ); } else { // Insert new option $wpdb->insert( $wpdb->options, array( 'option_name' => $option_name, 'option_value' => $sanitized_embed_code, 'autoload' => 'no' ), array('%s', '%s', '%s') ); } wp_cache_delete($option_name, 'options'); // Re-query to verify $saved_code = $wpdb->get_var($wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $option_name )); } } // Return success if at least one method succeeded // Check if values match what we tried to save $main_success = (!empty($saved_code) && $saved_code === $sanitized_embed_code); $b64_success = (!empty($saved_b64) && base64_decode($saved_b64) === $sanitized_embed_code); return $main_success || $b64_success; } /** * Clear embed code from database */ public static function clear_embed_code() { $option_name = self::OPTION_PREFIX . 'embed_code'; $option_name_b64 = self::OPTION_PREFIX . 'embed_code_b64'; // Clear cache wp_cache_delete($option_name, 'options'); wp_cache_delete($option_name_b64, 'options'); // Delete both options $result1 = delete_option($option_name); $result2 = delete_option($option_name_b64); return $result1 || $result2; } /** * Log embed code operations for debugging (static method) * * @param string $log_type Type of log entry * @param mixed $log_data Data to log * @return bool Success or failure */ private static function log_embed_code_debug($log_type, $log_data) { try { // Direct file logging (no dependency on Aiflab_Chatweb_API instance) $upload_dir = wp_upload_dir(); if (isset($upload_dir['error']) && $upload_dir['error'] !== false) { return false; } $logs_dir = $upload_dir['basedir'] . '/chatweb-logs'; if (!file_exists($logs_dir)) { $dir_created = wp_mkdir_p($logs_dir); if (!$dir_created) { return false; } // Create .htaccess to protect logs $htaccess_file = $logs_dir . '/.htaccess'; $htaccess_content = "Order deny,allow\nDeny from all"; @file_put_contents($htaccess_file, $htaccess_content); // Create index.php to prevent directory listing $index_file = $logs_dir . '/index.php'; $index_content = ' gmdate('Y-m-d H:i:s'), 'type' => $log_type, 'data' => $log_data ); // Write to log file with pretty print $log_line = json_encode($log_entry, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n---\n"; $bytes_written = @file_put_contents($log_file, $log_line, FILE_APPEND); return $bytes_written !== false; } catch (Exception $e) { // Catch any unexpected exceptions to prevent disrupting the main process return false; } } /** * Alternative method: Save embed code as base64 to bypass WordPress filtering */ public static function set_embed_code_base64($embed_code) { // Sanitize first $sanitized_embed_code = self::sanitize_embed_code($embed_code); if (empty($sanitized_embed_code)) { return false; } // Encode to base64 $encoded_embed_code = base64_encode($sanitized_embed_code); // Save with different option name $option_name = self::OPTION_PREFIX . 'embed_code_b64'; $result = update_option($option_name, $encoded_embed_code, false); // Verify $saved_encoded = get_option($option_name, ''); if (!empty($saved_encoded)) { $decoded = base64_decode($saved_encoded); // Also save to regular option for compatibility update_option(self::OPTION_PREFIX . 'embed_code', $decoded, false); return true; } return false; } /** * Get embed code with base64 fallback (no infinite loop) */ public static function get_embed_code_with_fallback() { // Try base64 method directly (don't call get_embed_code()) $option_name = self::OPTION_PREFIX . 'embed_code_b64'; $encoded_embed_code = get_option($option_name, ''); if (!empty($encoded_embed_code)) { $decoded = base64_decode($encoded_embed_code); // Validate decoded content if (self::is_valid_aiflab_chatweb_embed($decoded)) { return $decoded; } } return ''; } /** * Clear embed code (alias for clear_embed_code) */ public static function clear_cache() { return self::clear_embed_code(); } /** * Get plugin info */ public static function get_plugin_info() { return array( 'name' => 'AIFusionLab Chatbot for Chatweb', 'version' => AIFLAB_CHATWEB_PLUGIN_VERSION, 'url' => AIFLAB_CHATWEB_PLUGIN_URL, 'path' => AIFLAB_CHATWEB_PLUGIN_PATH, 'basename' => AIFLAB_CHATWEB_PLUGIN_BASENAME ); } /** * Hook into WordPress settings API to handle our settings * Note: Removed embed_code hook to prevent infinite loops */ public static function init_settings_hooks() { // Hook into each individual setting to update our config add_action('update_option_aiflab_chatweb_api_key', function($old_value, $new_value) { self::update_setting('api_key', $new_value); }, 10, 2); add_action('update_option_aiflab_chatweb_enable_embed', function($old_value, $new_value) { self::update_setting('enable_embed', $new_value); }, 10, 2); // Removed embed_code hook to prevent conflicts with set_embed_code method add_action('update_option_aiflab_chatweb_enable_woocommerce_sync', function($old_value, $new_value) { self::update_setting('enable_woocommerce_sync', $new_value); }, 10, 2); add_action('update_option_aiflab_chatweb_website_id', function($old_value, $new_value) { self::update_setting('website_id', $new_value); }, 10, 2); } /** * Clear caches from popular caching plugins */ public static function clear_plugin_caches() { // WP Super Cache if (function_exists('wp_cache_clear_cache')) { wp_cache_clear_cache(); } // W3 Total Cache if (function_exists('w3tc_flush_all')) { w3tc_flush_all(); } // WP Rocket if (function_exists('rocket_clean_domain')) { rocket_clean_domain(); } // WP Fastest Cache if (class_exists('WpFastestCache')) { $wpfc = new WpFastestCache(); if (method_exists($wpfc, 'deleteCache')) { $wpfc->deleteCache(true); } } // LiteSpeed Cache if (class_exists('LiteSpeed_Cache_API') && method_exists('LiteSpeed_Cache_API', 'purge_all')) { LiteSpeed_Cache_API::purge_all(); } // Autoptimize if (class_exists('autoptimizeCache') && method_exists('autoptimizeCache', 'clearall')) { autoptimizeCache::clearall(); } } }