Changeset 3330738
- Timestamp:
- 07/19/2025 07:10:13 PM (9 months ago)
- Location:
- muchat-ai
- Files:
-
- 24 edited
- 1 copied
-
tags/2.0.38 (copied) (copied from muchat-ai/trunk)
-
tags/2.0.38/assets/js/admin.js (modified) (7 diffs)
-
tags/2.0.38/includes/Admin/Settings.php (modified) (5 diffs)
-
tags/2.0.38/includes/Api/Middleware/AuthMiddleware.php (modified) (2 diffs)
-
tags/2.0.38/includes/Core/Plugin.php (modified) (3 diffs)
-
tags/2.0.38/includes/Frontend/Widget.php (modified) (3 diffs)
-
tags/2.0.38/includes/Models/Page.php (modified) (6 diffs)
-
tags/2.0.38/includes/Models/Post.php (modified) (1 diff)
-
tags/2.0.38/includes/Models/Product.php (modified) (4 diffs)
-
tags/2.0.38/muchat-ai.php (modified) (2 diffs)
-
tags/2.0.38/readme.txt (modified) (2 diffs)
-
tags/2.0.38/templates/admin/settings.php (modified) (1 diff)
-
tags/2.0.38/templates/admin/widget-settings.php (modified) (1 diff)
-
trunk/assets/js/admin.js (modified) (7 diffs)
-
trunk/includes/Admin/Settings.php (modified) (5 diffs)
-
trunk/includes/Api/Middleware/AuthMiddleware.php (modified) (2 diffs)
-
trunk/includes/Core/Plugin.php (modified) (3 diffs)
-
trunk/includes/Frontend/Widget.php (modified) (3 diffs)
-
trunk/includes/Models/Page.php (modified) (6 diffs)
-
trunk/includes/Models/Post.php (modified) (1 diff)
-
trunk/includes/Models/Product.php (modified) (4 diffs)
-
trunk/muchat-ai.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/templates/admin/settings.php (modified) (1 diff)
-
trunk/templates/admin/widget-settings.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
muchat-ai/tags/2.0.38/assets/js/admin.js
r3300525 r3330738 144 144 145 145 $(document).ready(function() { 146 // Initial Messages Management 147 var container = document.getElementById('muchat-initial-messages-list'); 148 var addBtn = document.getElementById('muchat-add-initial-message'); 149 var hiddenInput = document.getElementById('muchat_ai_chatbot_interface_initial_messages'); 150 151 if (container && addBtn && hiddenInput) { 152 var initial = []; 146 // --- Reusable function for Initial Messages Management --- 147 function initializeMessageList(containerId, buttonId, inputId) { 148 var container = document.getElementById(containerId); 149 var addBtn = document.getElementById(buttonId); 150 var hiddenInput = document.getElementById(inputId); 151 152 if (!container || !addBtn || !hiddenInput) { 153 return; // Exit if elements are not found 154 } 155 156 var messages = []; 153 157 154 158 // Parse initial messages with error handling … … 156 160 if (hiddenInput.value && hiddenInput.value.trim() !== '') { 157 161 try { 158 initial= JSON.parse(hiddenInput.value);159 if (!Array.isArray( initial)) {162 messages = JSON.parse(hiddenInput.value); 163 if (!Array.isArray(messages)) { 160 164 console.warn('Initial messages not in expected array format, converting...'); 161 initial= hiddenInput.value.split(',').map(function(x) {165 messages = hiddenInput.value.split(',').map(function(x) { 162 166 return x.trim(); 163 167 }); 164 168 } 165 169 } catch (e) { 166 console.warn('Failed to parse JSON, falling back to comma separation ', e);167 initial= hiddenInput.value.split(',').map(function(x) {170 console.warn('Failed to parse JSON, falling back to comma separation for ' + inputId, e); 171 messages = hiddenInput.value.split(',').map(function(x) { 168 172 return x.trim(); 169 173 }); … … 171 175 } 172 176 } catch (e) { 173 console.error('Error processing initial messages :', e);174 initial= [];177 console.error('Error processing initial messages for ' + inputId + ':', e); 178 messages = []; 175 179 } 176 180 177 181 function render() { 178 182 container.innerHTML = ''; 179 initial.forEach(function(msg, idx) {183 messages.forEach(function(msg, idx) { 180 184 var div = document.createElement('div'); 181 185 div.className = 'initial-message-row'; … … 183 187 input.type = 'text'; 184 188 input.className = 'regular-text'; 185 input.value = msg || ''; // Prevent undefined values189 input.value = msg || ''; 186 190 input.placeholder = 'Message...'; 187 191 input.oninput = function() { 188 initial[idx] = input.value;192 messages[idx] = input.value; 189 193 updateHidden(); 190 194 }; … … 195 199 remove.title = 'Remove message'; 196 200 remove.onclick = function() { 197 initial.splice(idx, 1);201 messages.splice(idx, 1); 198 202 render(); 199 203 updateHidden(); … … 208 212 function updateHidden() { 209 213 try { 210 var filtered = initial.map(function(x) {214 var filtered = messages.map(function(x) { 211 215 return (x || '').trim(); 212 216 }).filter(Boolean); 213 217 hiddenInput.value = JSON.stringify(filtered); 214 218 } catch (e) { 215 console.error('Error updating hidden input :', e);219 console.error('Error updating hidden input for ' + inputId + ':', e); 216 220 } 217 221 } 218 222 219 223 addBtn.onclick = function() { 220 initial.push('');224 messages.push(''); 221 225 render(); 222 226 }; … … 224 228 render(); 225 229 } 230 231 // Initialize both message lists 232 initializeMessageList('muchat-initial-messages-list', 'muchat-add-initial-message', 'muchat_ai_chatbot_interface_initial_messages'); 233 initializeMessageList('muchat-guest-initial-messages-list', 'muchat-add-guest-initial-message', 'muchat_ai_chatbot_guest_initial_messages'); 234 235 // --- Toggle for Guest Initial Messages --- 236 var guestToggleCheckbox = document.getElementById('muchat_ai_chatbot_enable_guest_messages'); 237 var guestMessagesRow = document.getElementById('muchat-guest-initial-messages-row'); 238 239 if (guestToggleCheckbox && guestMessagesRow) { 240 function toggleGuestMessages() { 241 guestMessagesRow.style.display = guestToggleCheckbox.checked ? 'table-row' : 'none'; 242 } 243 244 // Set initial state on page load 245 toggleGuestMessages(); 246 247 // Add event listener for changes 248 guestToggleCheckbox.addEventListener('change', toggleGuestMessages); 249 } 250 226 251 227 252 // Schedule Settings Toggle -
muchat-ai/tags/2.0.38/includes/Admin/Settings.php
r3309949 r3330738 248 248 $interface_primary_color = get_option('muchat_ai_chatbot_interface_primary_color', '#145dee'); 249 249 $interface_initial_messages = get_option('muchat_ai_chatbot_interface_initial_messages', ''); 250 $enable_guest_messages = get_option('muchat_ai_chatbot_enable_guest_messages', '0'); 251 $guest_initial_messages = get_option('muchat_ai_chatbot_guest_initial_messages', ''); 250 252 $load_strategy = get_option('muchat_ai_chatbot_load_strategy', 'FAST'); 251 253 $use_logged_in_user_info = get_option('muchat_ai_chatbot_use_logged_in_user_info', ''); … … 544 546 public function register_settings() 545 547 { 548 // Handle cache clearing action 549 if ( 550 isset($_GET['action']) && 551 $_GET['action'] === 'muchat_refresh_meta' && 552 isset($_GET['_wpnonce']) && 553 wp_verify_nonce(sanitize_key($_GET['_wpnonce']), 'muchat_refresh_meta_action') 554 ) { 555 delete_transient('muchat_product_meta_fields_cache'); 556 add_settings_error( 557 'muchat_ai_chatbot_plugin_messages', 558 'meta_cache_cleared', 559 __('Product meta fields cache has been cleared successfully.', 'muchat-ai'), 560 'updated' 561 ); 562 } 563 546 564 // Register plugin settings 547 565 $this->register_plugin_settings(); … … 550 568 $this->register_plugin_onboarding(); 551 569 552 register_setting('muchat_ai_chatbot_plugin_settings', 'muchat_ai_chatbot_plugin_options', array( 553 'sanitize_callback' => function ($input) { 554 // If input is not an array, return an empty array 555 if (!is_array($input)) { 556 return array(); 570 // Register the main settings group for the plugin 571 register_setting( 572 'muchat_ai_chatbot_plugin_settings', 573 'muchat_ai_chatbot_plugin_options', 574 array( 575 'sanitize_callback' => function ($input) { 576 // If input is not an array, return an empty array 577 if (!is_array($input)) { 578 return array(); 579 } 580 581 // Sanitize meta field selections (array of values) 582 if (isset($input['product_meta_fields']) && is_array($input['product_meta_fields'])) { 583 $input['product_meta_fields'] = array_map('sanitize_text_field', $input['product_meta_fields']); 584 } 585 586 // Sanitize meta labels (associative array) 587 if (isset($input['meta_labels']) && is_array($input['meta_labels'])) { 588 foreach ($input['meta_labels'] as $key => $value) { 589 $input['meta_labels'][$key] = sanitize_text_field($value); 590 } 591 } 592 593 return $input; 557 594 } 558 559 // Sanitize meta field selections (array of values) 560 if (isset($input['product_meta_fields']) && is_array($input['product_meta_fields'])) { 561 $input['product_meta_fields'] = array_map('sanitize_text_field', $input['product_meta_fields']); 562 } 563 564 // Sanitize meta labels (associative array) 565 if (isset($input['meta_labels']) && is_array($input['meta_labels'])) { 566 foreach ($input['meta_labels'] as $key => $value) { 567 $input['meta_labels'][$key] = sanitize_text_field($value); 568 } 569 } 570 571 return $input; 572 } 573 )); 595 ) 596 ); 574 597 575 598 // Product Meta Section (in a separate page section) … … 734 757 'default' => '' 735 758 )); 759 register_setting('muchat-settings-group', 'muchat_ai_chatbot_enable_guest_messages', array( 760 'type' => 'string', 761 'sanitize_callback' => 'sanitize_text_field', 762 'default' => '0' 763 )); 764 register_setting('muchat-settings-group', 'muchat_ai_chatbot_guest_initial_messages', array( 765 'type' => 'string', 766 'sanitize_callback' => 'sanitize_textarea_field', 767 'default' => '' 768 )); 736 769 register_setting('muchat-settings-group', 'muchat_ai_chatbot_load_strategy', array( 737 770 'type' => 'string', … … 806 839 add_option('muchat_ai_chatbot_onboarding', false); 807 840 841 // Add default options for the new guest message fields 842 add_option('muchat_ai_chatbot_enable_guest_messages', '0'); 843 add_option('muchat_ai_chatbot_guest_initial_messages', ''); 844 808 845 // Default load strategy 809 846 add_option('muchat_ai_chatbot_load_strategy', 'FAST'); -
muchat-ai/tags/2.0.38/includes/Api/Middleware/AuthMiddleware.php
r3300525 r3330738 50 50 private function validate_token($token) 51 51 { 52 $transient_key = 'muchat_token_' . md5($token); 53 54 // Check cache first 55 if (get_transient($transient_key)) { 56 return true; 57 } 58 52 59 global $wp_filter; 53 60 … … 105 112 } 106 113 114 // If validation is successful, store it in cache for 1 hour 115 set_transient($transient_key, true, HOUR_IN_SECONDS); 116 107 117 return true; 108 118 } finally { -
muchat-ai/tags/2.0.38/includes/Core/Plugin.php
r3300592 r3330738 18 18 */ 19 19 protected $version; 20 21 /** 22 * Settings class instance 23 * 24 * @var \Muchat\Api\Admin\Settings 25 */ 26 private $settings_instance; 20 27 21 28 /** … … 56 63 57 64 /** 65 * Get (and instantiate if necessary) the settings admin class 66 * 67 * @return \Muchat\Api\Admin\Settings 68 */ 69 public function get_settings_instance() 70 { 71 if (null === $this->settings_instance) { 72 $this->settings_instance = new \Muchat\Api\Admin\Settings(); 73 } 74 return $this->settings_instance; 75 } 76 77 /** 58 78 * Register admin hooks 59 79 * … … 62 82 private function setup_admin() 63 83 { 64 $admin = new \Muchat\Api\Admin\Settings(); 65 66 $this->loader->add_action('admin_menu', $admin, 'add_menu_page'); 67 $this->loader->add_action('admin_init', $admin, 'register_settings'); 68 $this->loader->add_action('admin_enqueue_scripts', $admin, 'enqueue_styles'); 69 $this->loader->add_action('admin_enqueue_scripts', $admin, 'enqueue_scripts'); 84 $this->loader->add_action('admin_menu', $this, 'add_admin_menu'); 85 $this->loader->add_action('admin_init', $this, 'register_admin_settings'); 86 $this->loader->add_action('admin_enqueue_scripts', $this, 'enqueue_admin_assets'); 87 88 } 89 90 91 /** 92 * Wrapper for adding the admin menu 93 */ 94 public function add_admin_menu() 95 { 96 $this->get_settings_instance()->add_menu_page(); 97 } 98 99 /** 100 * Wrapper for registering settings 101 */ 102 public function register_admin_settings() 103 { 104 $this->get_settings_instance()->register_settings(); 105 } 106 107 /** 108 * Wrapper for enqueuing admin assets 109 * 110 * @param string $hook The current admin page hook. 111 */ 112 public function enqueue_admin_assets($hook) 113 { 114 $this->get_settings_instance()->enqueue_styles($hook); 115 $this->get_settings_instance()->enqueue_scripts($hook); 70 116 } 71 117 -
muchat-ai/tags/2.0.38/includes/Frontend/Widget.php
r3300525 r3330738 80 80 /** 81 81 * Determines if the chatbot should be displayed on the current page 82 * based on visibility settings 82 * based on visibility settings. This is the primary logic gate. 83 83 */ 84 84 private function should_display() 85 85 { 86 // Check if widget is enabled 87 $widget_enabled = get_option('muchat_ai_chatbot_widget_enabled', '1'); 88 if ($widget_enabled !== '1') { 86 // 1. Check if widget is globally enabled 87 if (get_option('muchat_ai_chatbot_widget_enabled', '1') !== '1') { 89 88 return false; 90 89 } 91 90 92 // Check scheduling if enabled 93 $schedule_enabled = get_option('muchat_ai_chatbot_schedule_enabled', '0'); 94 if ($schedule_enabled === '1') { 91 // 2. Check scheduling if enabled 92 if (get_option('muchat_ai_chatbot_schedule_enabled', '0') === '1') { 95 93 if (!$this->is_within_schedule()) { 96 94 return false; … … 98 96 } 99 97 100 // Get visibility settings98 // 3. Check page visibility rules 101 99 $visibility_mode = get_option('muchat_ai_chatbot_visibility_mode', 'all'); 102 100 $visibility_pages = get_option('muchat_ai_chatbot_visibility_pages', ''); 103 101 104 // If visibility pages is empty... 105 if (empty($visibility_pages)) { 106 // For "show everywhere except" mode, show on all pages 107 if ($visibility_mode === 'all') { 108 return true; 109 } 110 // For "only show on listed pages" mode, don't show anywhere if list is empty 111 else { 112 return false; 113 } 114 } 115 116 // If wildcard is specified, show everywhere for 'all' mode 117 if (trim($visibility_pages) === '*') { 102 // Rule: 'Disabled Everywhere' 103 if ($visibility_mode === 'none') { 104 return false; 105 } 106 107 // Rule: 'Show on all pages' (and no exceptions are listed) 108 if ($visibility_mode === 'all' && empty(trim($visibility_pages))) { 118 109 return true; 119 110 } 120 111 121 // Get current path 122 $current_path = isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : ''; 123 124 // Check if this is the homepage 125 $is_front = is_front_page() || $current_path === '/' || $current_path === '/index.php'; 126 127 // Convert path patterns to array 128 $patterns = array_filter(explode("\n", $visibility_pages)); 129 $matches = false; 112 // Rule: 'Only show on listed pages' (and the list is empty) 113 if ($visibility_mode === 'specific' && empty(trim($visibility_pages))) { 114 return false; 115 } 116 117 // The logic now depends on matching the current page against the list. 118 $matches = $this->is_path_match($visibility_pages); 119 120 // Rule: 'Show on all pages EXCEPT those listed' 121 if ($visibility_mode === 'all') { 122 return !$matches; 123 } 124 125 // Rule: 'Only show on pages listed' 126 if ($visibility_mode === 'specific') { 127 return $matches; 128 } 129 130 // Default fallback, should ideally not be reached. 131 return false; 132 } 133 134 /** 135 * Checks if the current request path matches any of the given patterns. 136 * Handles exact, wildcard, UTF-8, percent-encoded, and <front> patterns. 137 * 138 * @param string $patterns_string A newline-separated string of URL patterns. 139 * @return bool True if the path matches one of the patterns, false otherwise. 140 */ 141 private function is_path_match($patterns_string) 142 { 143 // 1. Get and normalize the current page's path. 144 // We get the raw URI, strip any query parameters, and then decode it. 145 $request_uri = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : ''; 146 $path_only = strtok($request_uri, '?'); 147 $current_path = rtrim(urldecode($path_only), '/'); 148 149 // Treat an empty path (which can be the homepage) as '/'. 150 if (empty($current_path)) { 151 $current_path = '/'; 152 } 153 154 // 2. Use WordPress's reliable function to determine if it's the front page. 155 $is_front = is_front_page(); 156 157 // 3. Handle the global wildcard match immediately. 158 if (trim($patterns_string) === '*') { 159 return true; 160 } 161 162 // 4. Process the list of patterns. 163 $patterns = explode("\n", $patterns_string); 130 164 131 165 foreach ($patterns as $pattern) { 132 166 $pattern = trim($pattern); 133 134 // Skip empty lines135 167 if (empty($pattern)) { 136 168 continue; 137 169 } 138 170 139 // Check for front page 140 if ($pattern === '<front>' && $is_front) { 141 $matches = true; 142 break; 143 } 144 145 // Check for exact matches 146 if ($pattern === $current_path) { 147 $matches = true; 148 break; 149 } 150 151 // Check for wildcard matches 152 if (strpos($pattern, '*') !== false) { 153 $regex_pattern = '@^' . str_replace('*', '.*', $pattern) . '$@'; 154 if (preg_match($regex_pattern, $current_path)) { 155 $matches = true; 156 break; 171 // Also decode the user-provided pattern in case it's percent-encoded. 172 $decoded_pattern = urldecode($pattern); 173 174 // A. Check for the special '<front>' tag. 175 if ($decoded_pattern === '<front>') { 176 if ($is_front) { 177 return true; // Match found. 157 178 } 158 } 159 } 160 161 // Return based on visibility mode 162 if ($visibility_mode === 'all') { 163 // Show on all pages EXCEPT those listed 164 return !$matches; 165 } else { 166 // Only show on pages listed 167 return $matches; 168 } 179 continue; // Not the front page, so check next pattern. 180 } 181 182 // B. Normalize the pattern for comparison. 183 $normalized_pattern = rtrim($decoded_pattern, '/'); 184 if (empty($normalized_pattern)) { 185 $normalized_pattern = '/'; 186 } 187 188 // C. Check for wildcard matches. 189 if (strpos($normalized_pattern, '*') !== false) { 190 // Escape regex characters, then replace our wildcard `*` with `.*`. 191 // The 'u' modifier is crucial for correct UTF-8 pattern matching. 192 $regex = '@^' . str_replace('\*', '.*', preg_quote($normalized_pattern, '@')) . '$@u'; 193 if (preg_match($regex, $current_path)) { 194 return true; // Match found. 195 } 196 } 197 // D. Check for exact matches (only if no wildcard). 198 else { 199 if ($normalized_pattern === $current_path) { 200 return true; // Match found. 201 } 202 // Also check if the pattern is for the homepage and it is the homepage 203 if ($normalized_pattern === '/' && $is_front) { 204 return true; 205 } 206 } 207 } 208 209 // No patterns matched the current path. 210 return false; 169 211 } 170 212 … … 341 383 342 384 /** 343 * Process and get initial messages 385 * Get initial messages based on user login status 386 * 387 * @return array 344 388 */ 345 389 private function get_initial_messages() 346 390 { 347 $interface_initial_messages = get_option('muchat_ai_chatbot_interface_initial_messages', ''); 348 if (empty($interface_initial_messages)) { 349 return []; 350 } 351 352 // Parse initial messages 353 $initial_messages = []; 391 // Get guest-specific message settings 392 $enable_guest_messages = get_option('muchat_ai_chatbot_enable_guest_messages', '0'); 393 $guest_initial_messages_json = get_option('muchat_ai_chatbot_guest_initial_messages', ''); 394 395 // Check if we should use guest messages 396 if ('1' === $enable_guest_messages && !is_user_logged_in() && !empty($guest_initial_messages_json)) { 397 try { 398 $guest_messages = json_decode($guest_initial_messages_json, true); 399 if (is_array($guest_messages) && !empty($guest_messages)) { 400 return $guest_messages; 401 } 402 } catch (\Exception $e) { 403 // If JSON is invalid, fall through to default messages 404 } 405 } 406 407 // --- Fallback to default messages --- 408 $initial_messages_json = get_option('muchat_ai_chatbot_interface_initial_messages', ''); 409 if (empty($initial_messages_json)) { 410 return []; // No messages configured 411 } 412 354 413 try { 355 $decoded = json_decode($interface_initial_messages, true); 356 if (is_array($decoded)) { 357 $initial_messages = $decoded; 358 } else { 359 // Fallback to comma-separated 360 $initial_messages = array_map('trim', explode(',', $interface_initial_messages)); 414 $initial_messages = json_decode($initial_messages_json, true); 415 // Ensure it's a non-empty array 416 if (is_array($initial_messages) && !empty($initial_messages)) { 417 return $initial_messages; 361 418 } 362 419 } catch (\Exception $e) { 363 // Fallback to empty array if parsing fails 364 return []; 365 } 366 367 // Replace variables 368 $firstName = ''; 369 $lastName = ''; 370 if (is_user_logged_in()) { 371 $current_user = wp_get_current_user(); 372 $firstName = $current_user->user_firstname; 373 $lastName = $current_user->user_lastname; 374 } else { 375 $firstName = get_option('muchat_ai_chatbot_contact_first_name', ''); 376 $lastName = get_option('muchat_ai_chatbot_contact_last_name', ''); 377 } 378 379 foreach ($initial_messages as &$msg) { 380 $msg = str_replace(['$name', '$lastname'], [$firstName, $lastName], $msg); 381 } 382 383 return array_filter($initial_messages); 420 // JSON might be malformed, or it might be a simple comma-separated string 421 } 422 423 // Fallback for old comma-separated format 424 $messages = array_map('trim', explode(',', $initial_messages_json)); 425 return array_filter($messages); 384 426 } 385 427 -
muchat-ai/tags/2.0.38/includes/Models/Page.php
r3300525 r3330738 63 63 } 64 64 65 // Get all valid page IDs first (for accurate counting and filtering) 66 $all_pages_query = new \WP_Query(array_merge( 67 $args, 68 ['posts_per_page' => -1] 69 )); 70 71 // Filter to only get valid pages 72 $valid_page_ids = []; 73 foreach ($all_pages_query->posts as $page_id) { 74 $page = get_post($page_id); 75 if ($this->is_valid_page($page)) { 76 $valid_page_ids[] = $page_id; 77 } 78 } 79 80 // Get total count from valid pages 81 $total_count = count($valid_page_ids); 82 83 // Get requested pages (with pagination) 65 // Get pages to exclude 66 $pages_to_exclude = $this->get_pages_to_exclude(); 67 if (!empty($pages_to_exclude)) { 68 $args['post__not_in'] = $pages_to_exclude; 69 } 70 71 // Add a WHERE clause to filter out empty content 72 add_filter('posts_where', function ($where) { 73 global $wpdb; 74 $where .= " AND {$wpdb->posts}.post_content != ''"; 75 return $where; 76 }, 10, 1); 77 78 79 // Set pagination parameters 84 80 $requested_offset = $params['skip'] ?? 0; 85 81 $requested_limit = $params['take'] ?? 10; 86 82 87 // Get the slice of page IDs for this request 88 $paginated_ids = array_slice($valid_page_ids, $requested_offset, $requested_limit); 83 $args['posts_per_page'] = $requested_limit; 84 $args['offset'] = $requested_offset; 85 86 // Execute the final query with pagination 87 $query = new \WP_Query($args); 88 89 // Get total count from the same query 90 $total_count = (int) $query->found_posts; 89 91 90 92 // Format the pages 91 $pages = []; 92 foreach ($paginated_ids as $page_id) { 93 $page = get_post($page_id); 94 $pages[] = $this->format_page($page); 95 } 93 $pages = array_map(function ($page_id) { 94 return $this->format_page(get_post($page_id)); 95 }, $query->posts); 96 97 // Remove the temporary WHERE filter to avoid affecting other queries 98 remove_filter('posts_where', '__return_true'); 99 96 100 97 101 $plugin = new \Muchat\Api\Core\Plugin(); … … 108 112 109 113 /** 110 * Check if a page is valid (not a WooCommerce page and has content) 111 * 112 * @param \WP_Post $page 113 * @return bool 114 */ 115 private function is_valid_page($page) 116 { 117 if (empty($page) || empty($page->post_content)) { 118 return false; 119 } 120 114 * Get an array of page IDs to exclude 115 * 116 * @return array 117 */ 118 private function get_pages_to_exclude() 119 { 121 120 $woocommerce_pages = []; 122 123 121 // Only check WooCommerce pages if WooCommerce is active 124 122 if (class_exists('WooCommerce')) { 125 // Main WooCommerce pages 126 $woocommerce_pages = [ 123 $woocommerce_pages = array_filter([ 127 124 wc_get_page_id('cart'), 128 125 wc_get_page_id('checkout'), … … 130 127 wc_get_page_id('shop'), 131 128 wc_get_page_id('terms'), 132 ]; 133 } 134 129 ]); 130 } 135 131 // Additional pages to exclude regardless of WooCommerce 136 $additional_pages = [ 137 // Check by slug (different variations) 132 $additional_pages = array_filter([ 138 133 $this->get_page_id_by_slug('wishlist'), 139 134 $this->get_page_id_by_slug('compare'), … … 142 137 $this->get_page_id_by_slug('my-wishlist'), 143 138 $this->get_page_id_by_slug('my-compare'), 144 145 // Check by title (different variations)146 139 $this->get_page_id_by_title('Wishlist'), 147 140 $this->get_page_id_by_title('Compare'), … … 151 144 $this->get_page_id_by_title('My Compare'), 152 145 $this->get_page_id_by_title('علاقهمندیها'), // For Persian sites 153 $this->get_page_id_by_title('مقایسه') // For Persian sites 154 ]; 155 156 $pages_to_exclude = array_merge($woocommerce_pages, $additional_pages); 146 $this->get_page_id_by_title('مقایسه') // For Persian sites 147 ]); 148 return array_unique(array_merge($woocommerce_pages, $additional_pages)); 149 } 150 151 152 /** 153 * Check if a page is valid (not a WooCommerce page and has content) 154 * 155 * @param \WP_Post $page 156 * @return bool 157 */ 158 private function is_valid_page($page) 159 { 160 if (empty($page) || empty($page->post_content)) { 161 return false; 162 } 163 164 $pages_to_exclude = $this->get_pages_to_exclude(); 157 165 158 166 // If the page is one of the excluded system pages, return false … … 208 216 } 209 217 210 $woocommerce_pages = []; 211 212 // Only check WooCommerce pages if WooCommerce is active 213 if (class_exists('WooCommerce')) { 214 // Main WooCommerce pages 215 $woocommerce_pages = [ 216 wc_get_page_id('cart'), 217 wc_get_page_id('checkout'), 218 wc_get_page_id('myaccount'), 219 wc_get_page_id('shop'), 220 wc_get_page_id('terms'), 221 ]; 222 } 223 224 // Additional pages to exclude regardless of WooCommerce 225 $additional_pages = [ 226 // Check by slug (different variations) 227 $this->get_page_id_by_slug('wishlist'), 228 $this->get_page_id_by_slug('compare'), 229 $this->get_page_id_by_slug('wish-list'), 230 $this->get_page_id_by_slug('compare-list'), 231 $this->get_page_id_by_slug('my-wishlist'), 232 $this->get_page_id_by_slug('my-compare'), 233 234 // Check by title (different variations) 235 $this->get_page_id_by_title('Wishlist'), 236 $this->get_page_id_by_title('Compare'), 237 $this->get_page_id_by_title('Wish List'), 238 $this->get_page_id_by_title('Compare List'), 239 $this->get_page_id_by_title('My Wishlist'), 240 $this->get_page_id_by_title('My Compare'), 241 $this->get_page_id_by_title('علاقهمندیها'), // For Persian sites 242 $this->get_page_id_by_title('مقایسه') // For Persian sites 243 ]; 244 245 $pages_to_exclude = array_merge($woocommerce_pages, $additional_pages); 218 $pages_to_exclude = $this->get_pages_to_exclude(); 246 219 247 220 // If the page is one of the excluded system pages, return null -
muchat-ai/tags/2.0.38/includes/Models/Post.php
r3300525 r3330738 63 63 } 64 64 65 // Get all valid post IDs first (for accurate counting and filtering) 66 $all_posts_query = new \WP_Query(array_merge( 67 $args, 68 ['posts_per_page' => -1] 69 )); 70 71 // Filter to only get valid posts 72 $valid_post_ids = []; 73 foreach ($all_posts_query->posts as $post_id) { 74 $post = get_post($post_id); 75 if ($this->is_valid_post($post)) { 76 $valid_post_ids[] = $post_id; 77 } 78 } 79 80 // Get total count from valid posts 81 $total_count = count($valid_post_ids); 82 83 // Get requested posts (with pagination) 65 // Add a meta query to exclude posts with empty content 66 $args['meta_query'] = [ 67 [ 68 'key' => '_wp_old_slug', // A non-existent key to satisfy the structure, the real filtering is done by the where filter. 69 ], 70 ]; 71 72 // Add a WHERE clause to filter out empty content. This is more efficient than a meta_query on post_content. 73 add_filter('posts_where', function ($where) { 74 global $wpdb; 75 $where .= " AND {$wpdb->posts}.post_content != ''"; 76 return $where; 77 }); 78 79 // Set pagination parameters 84 80 $requested_offset = $params['skip'] ?? 0; 85 81 $requested_limit = $params['take'] ?? 10; 86 82 87 // Get the slice of post IDs for this request 88 $paginated_ids = array_slice($valid_post_ids, $requested_offset, $requested_limit); 83 // Update the query with pagination parameters 84 $args['posts_per_page'] = $requested_limit; 85 $args['offset'] = $requested_offset; 86 87 // Execute the final query with pagination 88 $query = new \WP_Query($args); 89 90 // Get total count from the same query 91 $total_count = (int) $query->found_posts; 89 92 90 93 // Format the posts 91 $posts = []; 92 foreach ($paginated_ids as $post_id) { 93 $post = get_post($post_id); 94 $posts[] = $this->format_post($post); 95 } 94 $posts = array_map(function ($post_id) { 95 return $this->format_post(get_post($post_id)); 96 }, $query->posts); 97 96 98 97 99 $plugin = new \Muchat\Api\Core\Plugin(); -
muchat-ai/tags/2.0.38/includes/Models/Product.php
r3300525 r3330738 81 81 } 82 82 83 // Get all valid product IDs first (for accurate counting and filtering) 84 $all_products_query = new \WP_Query(array_merge( 85 $args, 86 ['posts_per_page' => -1] 87 )); 88 89 // Get total count from valid products 90 $total_count = count($all_products_query->posts); 91 92 // Get requested products (with pagination) 83 // Set pagination parameters 93 84 $requested_offset = $params['skip'] ?? 0; 94 85 $requested_limit = $params['take'] ?? 30; … … 100 91 // Execute the final query with pagination 101 92 $query = new \WP_Query($args); 93 94 // Get total count from the same query 95 $total_count = (int) $query->found_posts; 102 96 103 97 // Format the products … … 657 651 public function get_product_meta_fields() 658 652 { 653 $cache_key = 'muchat_product_meta_fields_cache'; 654 $cached_fields = get_transient($cache_key); 655 656 if (false !== $cached_fields) { 657 return $cached_fields; 658 } 659 659 660 global $wpdb; 660 661 … … 768 769 }); 769 770 770 return $meta_fields; 771 } 772 773 /** 774 * Get sample values for a meta key 771 // Combine standard meta fields and ACF fields 772 $all_fields = array_values($meta_fields); 773 774 // Store the result in cache for 6 hours 775 set_transient($cache_key, $all_fields, 6 * HOUR_IN_SECONDS); 776 777 return $all_fields; 778 } 779 780 /** 781 * Get sample values for a meta key from the database 775 782 * 776 783 * @param string $meta_key -
muchat-ai/tags/2.0.38/muchat-ai.php
r3309949 r3330738 5 5 * Plugin URI: https://mu.chat 6 6 * Description: Muchat, a powerful tool for customer support using artificial intelligence 7 * Version: 2.0.3 77 * Version: 2.0.38 8 8 * Author: Muchat 9 9 * Text Domain: muchat-ai … … 27 27 28 28 // Define plugin constants with unique prefix 29 define('MUCHAT_AI_CHATBOT_PLUGIN_VERSION', '2.0.3 7');29 define('MUCHAT_AI_CHATBOT_PLUGIN_VERSION', '2.0.38'); 30 30 // define('MUCHAT_AI_CHATBOT_CACHE_DURATION', HOUR_IN_SECONDS); 31 31 define('MUCHAT_AI_CHATBOT_PLUGIN_FILE', __FILE__); -
muchat-ai/tags/2.0.38/readme.txt
r3309949 r3330738 4 4 Requires at least: 5.0 5 5 Tested up to: 6.8 6 Stable tag: 2.0.3 76 Stable tag: 2.0.38 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later … … 76 76 == Changelog == 77 77 78 = 2.0.38 = 79 - Feature: Added an option for different initial messages for guest (non-logged-in) users. 80 - Performance: Major enhancements to API and admin pages. 81 - Performance: Optimized database queries for products, posts, and pages to reduce memory usage on large sites. 82 - Performance: Implemented caching for API token validation to prevent server slowdowns. 83 - Performance: Cached heavy queries on the plugin's settings page to improve load times. 84 - Bugfix: Corrected an "undefined variable" warning on the widget settings page. 85 78 86 = 2.0.37 = 79 87 * Fixed issue with `<front>` tag being removed when saving display rules -
muchat-ai/tags/2.0.38/templates/admin/settings.php
r3300525 r3330738 57 57 <?php submit_button(__('Save Changes', 'muchat-ai'), 'primary', 'submit', false, ['style' => 'margin-top: 20px;']); ?> 58 58 </form> 59 60 <form method="get" style="display: inline-block; margin-left: 10px; margin-top: 20px;"> 61 <input type="hidden" name="page" value="muchat-woocommerce-api"> 62 <input type="hidden" name="action" value="muchat_refresh_meta"> 63 <?php wp_nonce_field('muchat_refresh_meta_action'); ?> 64 <?php submit_button(__('Refresh Fields', 'muchat-ai'), 'secondary', 'refresh_meta', false); ?> 65 </form> 59 66 </div> -
muchat-ai/tags/2.0.38/templates/admin/widget-settings.php
r3309949 r3330738 150 150 </td> 151 151 </tr> 152 <tr valign="top"> 153 <th scope="row"><?php esc_html_e('Different Messages for Guests', 'muchat-ai'); ?></th> 154 <td> 155 <label> 156 <input type="checkbox" id="muchat_ai_chatbot_enable_guest_messages" name="muchat_ai_chatbot_enable_guest_messages" value="1" <?php checked($enable_guest_messages, '1'); ?> /> 157 <?php esc_html_e('Enable different initial messages for non-logged-in users.', 'muchat-ai'); ?> 158 </label> 159 </td> 160 </tr> 161 <tr valign="top" id="muchat-guest-initial-messages-row" style="display: none;"> 162 <th scope="row"><?php esc_html_e('Guest Initial Messages', 'muchat-ai'); ?></th> 163 <td> 164 <div id="muchat-guest-initial-messages-list"></div> 165 <button type="button" class="button" id="muchat-add-guest-initial-message"> 166 <span class="dashicons dashicons-plus-alt2" style="vertical-align: text-bottom;"></span> 167 <?php esc_html_e('Add Message', 'muchat-ai'); ?> 168 </button> 169 <input type="hidden" id="muchat_ai_chatbot_guest_initial_messages" name="muchat_ai_chatbot_guest_initial_messages" value="<?php echo esc_attr(is_array($guest_initial_messages) ? '' : $guest_initial_messages); ?>" /> 170 <p class="description"> 171 <?php esc_html_e('These messages will be shown to visitors who are not logged in. If left empty, the default messages above will be used.', 'muchat-ai'); ?> 172 </p> 173 </td> 174 </tr> 152 175 <tr valign="top"> 153 176 <th scope="row"><?php esc_html_e('Load Strategy', 'muchat-ai'); ?></th> -
muchat-ai/trunk/assets/js/admin.js
r3300525 r3330738 144 144 145 145 $(document).ready(function() { 146 // Initial Messages Management 147 var container = document.getElementById('muchat-initial-messages-list'); 148 var addBtn = document.getElementById('muchat-add-initial-message'); 149 var hiddenInput = document.getElementById('muchat_ai_chatbot_interface_initial_messages'); 150 151 if (container && addBtn && hiddenInput) { 152 var initial = []; 146 // --- Reusable function for Initial Messages Management --- 147 function initializeMessageList(containerId, buttonId, inputId) { 148 var container = document.getElementById(containerId); 149 var addBtn = document.getElementById(buttonId); 150 var hiddenInput = document.getElementById(inputId); 151 152 if (!container || !addBtn || !hiddenInput) { 153 return; // Exit if elements are not found 154 } 155 156 var messages = []; 153 157 154 158 // Parse initial messages with error handling … … 156 160 if (hiddenInput.value && hiddenInput.value.trim() !== '') { 157 161 try { 158 initial= JSON.parse(hiddenInput.value);159 if (!Array.isArray( initial)) {162 messages = JSON.parse(hiddenInput.value); 163 if (!Array.isArray(messages)) { 160 164 console.warn('Initial messages not in expected array format, converting...'); 161 initial= hiddenInput.value.split(',').map(function(x) {165 messages = hiddenInput.value.split(',').map(function(x) { 162 166 return x.trim(); 163 167 }); 164 168 } 165 169 } catch (e) { 166 console.warn('Failed to parse JSON, falling back to comma separation ', e);167 initial= hiddenInput.value.split(',').map(function(x) {170 console.warn('Failed to parse JSON, falling back to comma separation for ' + inputId, e); 171 messages = hiddenInput.value.split(',').map(function(x) { 168 172 return x.trim(); 169 173 }); … … 171 175 } 172 176 } catch (e) { 173 console.error('Error processing initial messages :', e);174 initial= [];177 console.error('Error processing initial messages for ' + inputId + ':', e); 178 messages = []; 175 179 } 176 180 177 181 function render() { 178 182 container.innerHTML = ''; 179 initial.forEach(function(msg, idx) {183 messages.forEach(function(msg, idx) { 180 184 var div = document.createElement('div'); 181 185 div.className = 'initial-message-row'; … … 183 187 input.type = 'text'; 184 188 input.className = 'regular-text'; 185 input.value = msg || ''; // Prevent undefined values189 input.value = msg || ''; 186 190 input.placeholder = 'Message...'; 187 191 input.oninput = function() { 188 initial[idx] = input.value;192 messages[idx] = input.value; 189 193 updateHidden(); 190 194 }; … … 195 199 remove.title = 'Remove message'; 196 200 remove.onclick = function() { 197 initial.splice(idx, 1);201 messages.splice(idx, 1); 198 202 render(); 199 203 updateHidden(); … … 208 212 function updateHidden() { 209 213 try { 210 var filtered = initial.map(function(x) {214 var filtered = messages.map(function(x) { 211 215 return (x || '').trim(); 212 216 }).filter(Boolean); 213 217 hiddenInput.value = JSON.stringify(filtered); 214 218 } catch (e) { 215 console.error('Error updating hidden input :', e);219 console.error('Error updating hidden input for ' + inputId + ':', e); 216 220 } 217 221 } 218 222 219 223 addBtn.onclick = function() { 220 initial.push('');224 messages.push(''); 221 225 render(); 222 226 }; … … 224 228 render(); 225 229 } 230 231 // Initialize both message lists 232 initializeMessageList('muchat-initial-messages-list', 'muchat-add-initial-message', 'muchat_ai_chatbot_interface_initial_messages'); 233 initializeMessageList('muchat-guest-initial-messages-list', 'muchat-add-guest-initial-message', 'muchat_ai_chatbot_guest_initial_messages'); 234 235 // --- Toggle for Guest Initial Messages --- 236 var guestToggleCheckbox = document.getElementById('muchat_ai_chatbot_enable_guest_messages'); 237 var guestMessagesRow = document.getElementById('muchat-guest-initial-messages-row'); 238 239 if (guestToggleCheckbox && guestMessagesRow) { 240 function toggleGuestMessages() { 241 guestMessagesRow.style.display = guestToggleCheckbox.checked ? 'table-row' : 'none'; 242 } 243 244 // Set initial state on page load 245 toggleGuestMessages(); 246 247 // Add event listener for changes 248 guestToggleCheckbox.addEventListener('change', toggleGuestMessages); 249 } 250 226 251 227 252 // Schedule Settings Toggle -
muchat-ai/trunk/includes/Admin/Settings.php
r3309949 r3330738 248 248 $interface_primary_color = get_option('muchat_ai_chatbot_interface_primary_color', '#145dee'); 249 249 $interface_initial_messages = get_option('muchat_ai_chatbot_interface_initial_messages', ''); 250 $enable_guest_messages = get_option('muchat_ai_chatbot_enable_guest_messages', '0'); 251 $guest_initial_messages = get_option('muchat_ai_chatbot_guest_initial_messages', ''); 250 252 $load_strategy = get_option('muchat_ai_chatbot_load_strategy', 'FAST'); 251 253 $use_logged_in_user_info = get_option('muchat_ai_chatbot_use_logged_in_user_info', ''); … … 544 546 public function register_settings() 545 547 { 548 // Handle cache clearing action 549 if ( 550 isset($_GET['action']) && 551 $_GET['action'] === 'muchat_refresh_meta' && 552 isset($_GET['_wpnonce']) && 553 wp_verify_nonce(sanitize_key($_GET['_wpnonce']), 'muchat_refresh_meta_action') 554 ) { 555 delete_transient('muchat_product_meta_fields_cache'); 556 add_settings_error( 557 'muchat_ai_chatbot_plugin_messages', 558 'meta_cache_cleared', 559 __('Product meta fields cache has been cleared successfully.', 'muchat-ai'), 560 'updated' 561 ); 562 } 563 546 564 // Register plugin settings 547 565 $this->register_plugin_settings(); … … 550 568 $this->register_plugin_onboarding(); 551 569 552 register_setting('muchat_ai_chatbot_plugin_settings', 'muchat_ai_chatbot_plugin_options', array( 553 'sanitize_callback' => function ($input) { 554 // If input is not an array, return an empty array 555 if (!is_array($input)) { 556 return array(); 570 // Register the main settings group for the plugin 571 register_setting( 572 'muchat_ai_chatbot_plugin_settings', 573 'muchat_ai_chatbot_plugin_options', 574 array( 575 'sanitize_callback' => function ($input) { 576 // If input is not an array, return an empty array 577 if (!is_array($input)) { 578 return array(); 579 } 580 581 // Sanitize meta field selections (array of values) 582 if (isset($input['product_meta_fields']) && is_array($input['product_meta_fields'])) { 583 $input['product_meta_fields'] = array_map('sanitize_text_field', $input['product_meta_fields']); 584 } 585 586 // Sanitize meta labels (associative array) 587 if (isset($input['meta_labels']) && is_array($input['meta_labels'])) { 588 foreach ($input['meta_labels'] as $key => $value) { 589 $input['meta_labels'][$key] = sanitize_text_field($value); 590 } 591 } 592 593 return $input; 557 594 } 558 559 // Sanitize meta field selections (array of values) 560 if (isset($input['product_meta_fields']) && is_array($input['product_meta_fields'])) { 561 $input['product_meta_fields'] = array_map('sanitize_text_field', $input['product_meta_fields']); 562 } 563 564 // Sanitize meta labels (associative array) 565 if (isset($input['meta_labels']) && is_array($input['meta_labels'])) { 566 foreach ($input['meta_labels'] as $key => $value) { 567 $input['meta_labels'][$key] = sanitize_text_field($value); 568 } 569 } 570 571 return $input; 572 } 573 )); 595 ) 596 ); 574 597 575 598 // Product Meta Section (in a separate page section) … … 734 757 'default' => '' 735 758 )); 759 register_setting('muchat-settings-group', 'muchat_ai_chatbot_enable_guest_messages', array( 760 'type' => 'string', 761 'sanitize_callback' => 'sanitize_text_field', 762 'default' => '0' 763 )); 764 register_setting('muchat-settings-group', 'muchat_ai_chatbot_guest_initial_messages', array( 765 'type' => 'string', 766 'sanitize_callback' => 'sanitize_textarea_field', 767 'default' => '' 768 )); 736 769 register_setting('muchat-settings-group', 'muchat_ai_chatbot_load_strategy', array( 737 770 'type' => 'string', … … 806 839 add_option('muchat_ai_chatbot_onboarding', false); 807 840 841 // Add default options for the new guest message fields 842 add_option('muchat_ai_chatbot_enable_guest_messages', '0'); 843 add_option('muchat_ai_chatbot_guest_initial_messages', ''); 844 808 845 // Default load strategy 809 846 add_option('muchat_ai_chatbot_load_strategy', 'FAST'); -
muchat-ai/trunk/includes/Api/Middleware/AuthMiddleware.php
r3300525 r3330738 50 50 private function validate_token($token) 51 51 { 52 $transient_key = 'muchat_token_' . md5($token); 53 54 // Check cache first 55 if (get_transient($transient_key)) { 56 return true; 57 } 58 52 59 global $wp_filter; 53 60 … … 105 112 } 106 113 114 // If validation is successful, store it in cache for 1 hour 115 set_transient($transient_key, true, HOUR_IN_SECONDS); 116 107 117 return true; 108 118 } finally { -
muchat-ai/trunk/includes/Core/Plugin.php
r3300592 r3330738 18 18 */ 19 19 protected $version; 20 21 /** 22 * Settings class instance 23 * 24 * @var \Muchat\Api\Admin\Settings 25 */ 26 private $settings_instance; 20 27 21 28 /** … … 56 63 57 64 /** 65 * Get (and instantiate if necessary) the settings admin class 66 * 67 * @return \Muchat\Api\Admin\Settings 68 */ 69 public function get_settings_instance() 70 { 71 if (null === $this->settings_instance) { 72 $this->settings_instance = new \Muchat\Api\Admin\Settings(); 73 } 74 return $this->settings_instance; 75 } 76 77 /** 58 78 * Register admin hooks 59 79 * … … 62 82 private function setup_admin() 63 83 { 64 $admin = new \Muchat\Api\Admin\Settings(); 65 66 $this->loader->add_action('admin_menu', $admin, 'add_menu_page'); 67 $this->loader->add_action('admin_init', $admin, 'register_settings'); 68 $this->loader->add_action('admin_enqueue_scripts', $admin, 'enqueue_styles'); 69 $this->loader->add_action('admin_enqueue_scripts', $admin, 'enqueue_scripts'); 84 $this->loader->add_action('admin_menu', $this, 'add_admin_menu'); 85 $this->loader->add_action('admin_init', $this, 'register_admin_settings'); 86 $this->loader->add_action('admin_enqueue_scripts', $this, 'enqueue_admin_assets'); 87 88 } 89 90 91 /** 92 * Wrapper for adding the admin menu 93 */ 94 public function add_admin_menu() 95 { 96 $this->get_settings_instance()->add_menu_page(); 97 } 98 99 /** 100 * Wrapper for registering settings 101 */ 102 public function register_admin_settings() 103 { 104 $this->get_settings_instance()->register_settings(); 105 } 106 107 /** 108 * Wrapper for enqueuing admin assets 109 * 110 * @param string $hook The current admin page hook. 111 */ 112 public function enqueue_admin_assets($hook) 113 { 114 $this->get_settings_instance()->enqueue_styles($hook); 115 $this->get_settings_instance()->enqueue_scripts($hook); 70 116 } 71 117 -
muchat-ai/trunk/includes/Frontend/Widget.php
r3300525 r3330738 80 80 /** 81 81 * Determines if the chatbot should be displayed on the current page 82 * based on visibility settings 82 * based on visibility settings. This is the primary logic gate. 83 83 */ 84 84 private function should_display() 85 85 { 86 // Check if widget is enabled 87 $widget_enabled = get_option('muchat_ai_chatbot_widget_enabled', '1'); 88 if ($widget_enabled !== '1') { 86 // 1. Check if widget is globally enabled 87 if (get_option('muchat_ai_chatbot_widget_enabled', '1') !== '1') { 89 88 return false; 90 89 } 91 90 92 // Check scheduling if enabled 93 $schedule_enabled = get_option('muchat_ai_chatbot_schedule_enabled', '0'); 94 if ($schedule_enabled === '1') { 91 // 2. Check scheduling if enabled 92 if (get_option('muchat_ai_chatbot_schedule_enabled', '0') === '1') { 95 93 if (!$this->is_within_schedule()) { 96 94 return false; … … 98 96 } 99 97 100 // Get visibility settings98 // 3. Check page visibility rules 101 99 $visibility_mode = get_option('muchat_ai_chatbot_visibility_mode', 'all'); 102 100 $visibility_pages = get_option('muchat_ai_chatbot_visibility_pages', ''); 103 101 104 // If visibility pages is empty... 105 if (empty($visibility_pages)) { 106 // For "show everywhere except" mode, show on all pages 107 if ($visibility_mode === 'all') { 108 return true; 109 } 110 // For "only show on listed pages" mode, don't show anywhere if list is empty 111 else { 112 return false; 113 } 114 } 115 116 // If wildcard is specified, show everywhere for 'all' mode 117 if (trim($visibility_pages) === '*') { 102 // Rule: 'Disabled Everywhere' 103 if ($visibility_mode === 'none') { 104 return false; 105 } 106 107 // Rule: 'Show on all pages' (and no exceptions are listed) 108 if ($visibility_mode === 'all' && empty(trim($visibility_pages))) { 118 109 return true; 119 110 } 120 111 121 // Get current path 122 $current_path = isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : ''; 123 124 // Check if this is the homepage 125 $is_front = is_front_page() || $current_path === '/' || $current_path === '/index.php'; 126 127 // Convert path patterns to array 128 $patterns = array_filter(explode("\n", $visibility_pages)); 129 $matches = false; 112 // Rule: 'Only show on listed pages' (and the list is empty) 113 if ($visibility_mode === 'specific' && empty(trim($visibility_pages))) { 114 return false; 115 } 116 117 // The logic now depends on matching the current page against the list. 118 $matches = $this->is_path_match($visibility_pages); 119 120 // Rule: 'Show on all pages EXCEPT those listed' 121 if ($visibility_mode === 'all') { 122 return !$matches; 123 } 124 125 // Rule: 'Only show on pages listed' 126 if ($visibility_mode === 'specific') { 127 return $matches; 128 } 129 130 // Default fallback, should ideally not be reached. 131 return false; 132 } 133 134 /** 135 * Checks if the current request path matches any of the given patterns. 136 * Handles exact, wildcard, UTF-8, percent-encoded, and <front> patterns. 137 * 138 * @param string $patterns_string A newline-separated string of URL patterns. 139 * @return bool True if the path matches one of the patterns, false otherwise. 140 */ 141 private function is_path_match($patterns_string) 142 { 143 // 1. Get and normalize the current page's path. 144 // We get the raw URI, strip any query parameters, and then decode it. 145 $request_uri = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : ''; 146 $path_only = strtok($request_uri, '?'); 147 $current_path = rtrim(urldecode($path_only), '/'); 148 149 // Treat an empty path (which can be the homepage) as '/'. 150 if (empty($current_path)) { 151 $current_path = '/'; 152 } 153 154 // 2. Use WordPress's reliable function to determine if it's the front page. 155 $is_front = is_front_page(); 156 157 // 3. Handle the global wildcard match immediately. 158 if (trim($patterns_string) === '*') { 159 return true; 160 } 161 162 // 4. Process the list of patterns. 163 $patterns = explode("\n", $patterns_string); 130 164 131 165 foreach ($patterns as $pattern) { 132 166 $pattern = trim($pattern); 133 134 // Skip empty lines135 167 if (empty($pattern)) { 136 168 continue; 137 169 } 138 170 139 // Check for front page 140 if ($pattern === '<front>' && $is_front) { 141 $matches = true; 142 break; 143 } 144 145 // Check for exact matches 146 if ($pattern === $current_path) { 147 $matches = true; 148 break; 149 } 150 151 // Check for wildcard matches 152 if (strpos($pattern, '*') !== false) { 153 $regex_pattern = '@^' . str_replace('*', '.*', $pattern) . '$@'; 154 if (preg_match($regex_pattern, $current_path)) { 155 $matches = true; 156 break; 171 // Also decode the user-provided pattern in case it's percent-encoded. 172 $decoded_pattern = urldecode($pattern); 173 174 // A. Check for the special '<front>' tag. 175 if ($decoded_pattern === '<front>') { 176 if ($is_front) { 177 return true; // Match found. 157 178 } 158 } 159 } 160 161 // Return based on visibility mode 162 if ($visibility_mode === 'all') { 163 // Show on all pages EXCEPT those listed 164 return !$matches; 165 } else { 166 // Only show on pages listed 167 return $matches; 168 } 179 continue; // Not the front page, so check next pattern. 180 } 181 182 // B. Normalize the pattern for comparison. 183 $normalized_pattern = rtrim($decoded_pattern, '/'); 184 if (empty($normalized_pattern)) { 185 $normalized_pattern = '/'; 186 } 187 188 // C. Check for wildcard matches. 189 if (strpos($normalized_pattern, '*') !== false) { 190 // Escape regex characters, then replace our wildcard `*` with `.*`. 191 // The 'u' modifier is crucial for correct UTF-8 pattern matching. 192 $regex = '@^' . str_replace('\*', '.*', preg_quote($normalized_pattern, '@')) . '$@u'; 193 if (preg_match($regex, $current_path)) { 194 return true; // Match found. 195 } 196 } 197 // D. Check for exact matches (only if no wildcard). 198 else { 199 if ($normalized_pattern === $current_path) { 200 return true; // Match found. 201 } 202 // Also check if the pattern is for the homepage and it is the homepage 203 if ($normalized_pattern === '/' && $is_front) { 204 return true; 205 } 206 } 207 } 208 209 // No patterns matched the current path. 210 return false; 169 211 } 170 212 … … 341 383 342 384 /** 343 * Process and get initial messages 385 * Get initial messages based on user login status 386 * 387 * @return array 344 388 */ 345 389 private function get_initial_messages() 346 390 { 347 $interface_initial_messages = get_option('muchat_ai_chatbot_interface_initial_messages', ''); 348 if (empty($interface_initial_messages)) { 349 return []; 350 } 351 352 // Parse initial messages 353 $initial_messages = []; 391 // Get guest-specific message settings 392 $enable_guest_messages = get_option('muchat_ai_chatbot_enable_guest_messages', '0'); 393 $guest_initial_messages_json = get_option('muchat_ai_chatbot_guest_initial_messages', ''); 394 395 // Check if we should use guest messages 396 if ('1' === $enable_guest_messages && !is_user_logged_in() && !empty($guest_initial_messages_json)) { 397 try { 398 $guest_messages = json_decode($guest_initial_messages_json, true); 399 if (is_array($guest_messages) && !empty($guest_messages)) { 400 return $guest_messages; 401 } 402 } catch (\Exception $e) { 403 // If JSON is invalid, fall through to default messages 404 } 405 } 406 407 // --- Fallback to default messages --- 408 $initial_messages_json = get_option('muchat_ai_chatbot_interface_initial_messages', ''); 409 if (empty($initial_messages_json)) { 410 return []; // No messages configured 411 } 412 354 413 try { 355 $decoded = json_decode($interface_initial_messages, true); 356 if (is_array($decoded)) { 357 $initial_messages = $decoded; 358 } else { 359 // Fallback to comma-separated 360 $initial_messages = array_map('trim', explode(',', $interface_initial_messages)); 414 $initial_messages = json_decode($initial_messages_json, true); 415 // Ensure it's a non-empty array 416 if (is_array($initial_messages) && !empty($initial_messages)) { 417 return $initial_messages; 361 418 } 362 419 } catch (\Exception $e) { 363 // Fallback to empty array if parsing fails 364 return []; 365 } 366 367 // Replace variables 368 $firstName = ''; 369 $lastName = ''; 370 if (is_user_logged_in()) { 371 $current_user = wp_get_current_user(); 372 $firstName = $current_user->user_firstname; 373 $lastName = $current_user->user_lastname; 374 } else { 375 $firstName = get_option('muchat_ai_chatbot_contact_first_name', ''); 376 $lastName = get_option('muchat_ai_chatbot_contact_last_name', ''); 377 } 378 379 foreach ($initial_messages as &$msg) { 380 $msg = str_replace(['$name', '$lastname'], [$firstName, $lastName], $msg); 381 } 382 383 return array_filter($initial_messages); 420 // JSON might be malformed, or it might be a simple comma-separated string 421 } 422 423 // Fallback for old comma-separated format 424 $messages = array_map('trim', explode(',', $initial_messages_json)); 425 return array_filter($messages); 384 426 } 385 427 -
muchat-ai/trunk/includes/Models/Page.php
r3300525 r3330738 63 63 } 64 64 65 // Get all valid page IDs first (for accurate counting and filtering) 66 $all_pages_query = new \WP_Query(array_merge( 67 $args, 68 ['posts_per_page' => -1] 69 )); 70 71 // Filter to only get valid pages 72 $valid_page_ids = []; 73 foreach ($all_pages_query->posts as $page_id) { 74 $page = get_post($page_id); 75 if ($this->is_valid_page($page)) { 76 $valid_page_ids[] = $page_id; 77 } 78 } 79 80 // Get total count from valid pages 81 $total_count = count($valid_page_ids); 82 83 // Get requested pages (with pagination) 65 // Get pages to exclude 66 $pages_to_exclude = $this->get_pages_to_exclude(); 67 if (!empty($pages_to_exclude)) { 68 $args['post__not_in'] = $pages_to_exclude; 69 } 70 71 // Add a WHERE clause to filter out empty content 72 add_filter('posts_where', function ($where) { 73 global $wpdb; 74 $where .= " AND {$wpdb->posts}.post_content != ''"; 75 return $where; 76 }, 10, 1); 77 78 79 // Set pagination parameters 84 80 $requested_offset = $params['skip'] ?? 0; 85 81 $requested_limit = $params['take'] ?? 10; 86 82 87 // Get the slice of page IDs for this request 88 $paginated_ids = array_slice($valid_page_ids, $requested_offset, $requested_limit); 83 $args['posts_per_page'] = $requested_limit; 84 $args['offset'] = $requested_offset; 85 86 // Execute the final query with pagination 87 $query = new \WP_Query($args); 88 89 // Get total count from the same query 90 $total_count = (int) $query->found_posts; 89 91 90 92 // Format the pages 91 $pages = []; 92 foreach ($paginated_ids as $page_id) { 93 $page = get_post($page_id); 94 $pages[] = $this->format_page($page); 95 } 93 $pages = array_map(function ($page_id) { 94 return $this->format_page(get_post($page_id)); 95 }, $query->posts); 96 97 // Remove the temporary WHERE filter to avoid affecting other queries 98 remove_filter('posts_where', '__return_true'); 99 96 100 97 101 $plugin = new \Muchat\Api\Core\Plugin(); … … 108 112 109 113 /** 110 * Check if a page is valid (not a WooCommerce page and has content) 111 * 112 * @param \WP_Post $page 113 * @return bool 114 */ 115 private function is_valid_page($page) 116 { 117 if (empty($page) || empty($page->post_content)) { 118 return false; 119 } 120 114 * Get an array of page IDs to exclude 115 * 116 * @return array 117 */ 118 private function get_pages_to_exclude() 119 { 121 120 $woocommerce_pages = []; 122 123 121 // Only check WooCommerce pages if WooCommerce is active 124 122 if (class_exists('WooCommerce')) { 125 // Main WooCommerce pages 126 $woocommerce_pages = [ 123 $woocommerce_pages = array_filter([ 127 124 wc_get_page_id('cart'), 128 125 wc_get_page_id('checkout'), … … 130 127 wc_get_page_id('shop'), 131 128 wc_get_page_id('terms'), 132 ]; 133 } 134 129 ]); 130 } 135 131 // Additional pages to exclude regardless of WooCommerce 136 $additional_pages = [ 137 // Check by slug (different variations) 132 $additional_pages = array_filter([ 138 133 $this->get_page_id_by_slug('wishlist'), 139 134 $this->get_page_id_by_slug('compare'), … … 142 137 $this->get_page_id_by_slug('my-wishlist'), 143 138 $this->get_page_id_by_slug('my-compare'), 144 145 // Check by title (different variations)146 139 $this->get_page_id_by_title('Wishlist'), 147 140 $this->get_page_id_by_title('Compare'), … … 151 144 $this->get_page_id_by_title('My Compare'), 152 145 $this->get_page_id_by_title('علاقهمندیها'), // For Persian sites 153 $this->get_page_id_by_title('مقایسه') // For Persian sites 154 ]; 155 156 $pages_to_exclude = array_merge($woocommerce_pages, $additional_pages); 146 $this->get_page_id_by_title('مقایسه') // For Persian sites 147 ]); 148 return array_unique(array_merge($woocommerce_pages, $additional_pages)); 149 } 150 151 152 /** 153 * Check if a page is valid (not a WooCommerce page and has content) 154 * 155 * @param \WP_Post $page 156 * @return bool 157 */ 158 private function is_valid_page($page) 159 { 160 if (empty($page) || empty($page->post_content)) { 161 return false; 162 } 163 164 $pages_to_exclude = $this->get_pages_to_exclude(); 157 165 158 166 // If the page is one of the excluded system pages, return false … … 208 216 } 209 217 210 $woocommerce_pages = []; 211 212 // Only check WooCommerce pages if WooCommerce is active 213 if (class_exists('WooCommerce')) { 214 // Main WooCommerce pages 215 $woocommerce_pages = [ 216 wc_get_page_id('cart'), 217 wc_get_page_id('checkout'), 218 wc_get_page_id('myaccount'), 219 wc_get_page_id('shop'), 220 wc_get_page_id('terms'), 221 ]; 222 } 223 224 // Additional pages to exclude regardless of WooCommerce 225 $additional_pages = [ 226 // Check by slug (different variations) 227 $this->get_page_id_by_slug('wishlist'), 228 $this->get_page_id_by_slug('compare'), 229 $this->get_page_id_by_slug('wish-list'), 230 $this->get_page_id_by_slug('compare-list'), 231 $this->get_page_id_by_slug('my-wishlist'), 232 $this->get_page_id_by_slug('my-compare'), 233 234 // Check by title (different variations) 235 $this->get_page_id_by_title('Wishlist'), 236 $this->get_page_id_by_title('Compare'), 237 $this->get_page_id_by_title('Wish List'), 238 $this->get_page_id_by_title('Compare List'), 239 $this->get_page_id_by_title('My Wishlist'), 240 $this->get_page_id_by_title('My Compare'), 241 $this->get_page_id_by_title('علاقهمندیها'), // For Persian sites 242 $this->get_page_id_by_title('مقایسه') // For Persian sites 243 ]; 244 245 $pages_to_exclude = array_merge($woocommerce_pages, $additional_pages); 218 $pages_to_exclude = $this->get_pages_to_exclude(); 246 219 247 220 // If the page is one of the excluded system pages, return null -
muchat-ai/trunk/includes/Models/Post.php
r3300525 r3330738 63 63 } 64 64 65 // Get all valid post IDs first (for accurate counting and filtering) 66 $all_posts_query = new \WP_Query(array_merge( 67 $args, 68 ['posts_per_page' => -1] 69 )); 70 71 // Filter to only get valid posts 72 $valid_post_ids = []; 73 foreach ($all_posts_query->posts as $post_id) { 74 $post = get_post($post_id); 75 if ($this->is_valid_post($post)) { 76 $valid_post_ids[] = $post_id; 77 } 78 } 79 80 // Get total count from valid posts 81 $total_count = count($valid_post_ids); 82 83 // Get requested posts (with pagination) 65 // Add a meta query to exclude posts with empty content 66 $args['meta_query'] = [ 67 [ 68 'key' => '_wp_old_slug', // A non-existent key to satisfy the structure, the real filtering is done by the where filter. 69 ], 70 ]; 71 72 // Add a WHERE clause to filter out empty content. This is more efficient than a meta_query on post_content. 73 add_filter('posts_where', function ($where) { 74 global $wpdb; 75 $where .= " AND {$wpdb->posts}.post_content != ''"; 76 return $where; 77 }); 78 79 // Set pagination parameters 84 80 $requested_offset = $params['skip'] ?? 0; 85 81 $requested_limit = $params['take'] ?? 10; 86 82 87 // Get the slice of post IDs for this request 88 $paginated_ids = array_slice($valid_post_ids, $requested_offset, $requested_limit); 83 // Update the query with pagination parameters 84 $args['posts_per_page'] = $requested_limit; 85 $args['offset'] = $requested_offset; 86 87 // Execute the final query with pagination 88 $query = new \WP_Query($args); 89 90 // Get total count from the same query 91 $total_count = (int) $query->found_posts; 89 92 90 93 // Format the posts 91 $posts = []; 92 foreach ($paginated_ids as $post_id) { 93 $post = get_post($post_id); 94 $posts[] = $this->format_post($post); 95 } 94 $posts = array_map(function ($post_id) { 95 return $this->format_post(get_post($post_id)); 96 }, $query->posts); 97 96 98 97 99 $plugin = new \Muchat\Api\Core\Plugin(); -
muchat-ai/trunk/includes/Models/Product.php
r3300525 r3330738 81 81 } 82 82 83 // Get all valid product IDs first (for accurate counting and filtering) 84 $all_products_query = new \WP_Query(array_merge( 85 $args, 86 ['posts_per_page' => -1] 87 )); 88 89 // Get total count from valid products 90 $total_count = count($all_products_query->posts); 91 92 // Get requested products (with pagination) 83 // Set pagination parameters 93 84 $requested_offset = $params['skip'] ?? 0; 94 85 $requested_limit = $params['take'] ?? 30; … … 100 91 // Execute the final query with pagination 101 92 $query = new \WP_Query($args); 93 94 // Get total count from the same query 95 $total_count = (int) $query->found_posts; 102 96 103 97 // Format the products … … 657 651 public function get_product_meta_fields() 658 652 { 653 $cache_key = 'muchat_product_meta_fields_cache'; 654 $cached_fields = get_transient($cache_key); 655 656 if (false !== $cached_fields) { 657 return $cached_fields; 658 } 659 659 660 global $wpdb; 660 661 … … 768 769 }); 769 770 770 return $meta_fields; 771 } 772 773 /** 774 * Get sample values for a meta key 771 // Combine standard meta fields and ACF fields 772 $all_fields = array_values($meta_fields); 773 774 // Store the result in cache for 6 hours 775 set_transient($cache_key, $all_fields, 6 * HOUR_IN_SECONDS); 776 777 return $all_fields; 778 } 779 780 /** 781 * Get sample values for a meta key from the database 775 782 * 776 783 * @param string $meta_key -
muchat-ai/trunk/muchat-ai.php
r3309949 r3330738 5 5 * Plugin URI: https://mu.chat 6 6 * Description: Muchat, a powerful tool for customer support using artificial intelligence 7 * Version: 2.0.3 77 * Version: 2.0.38 8 8 * Author: Muchat 9 9 * Text Domain: muchat-ai … … 27 27 28 28 // Define plugin constants with unique prefix 29 define('MUCHAT_AI_CHATBOT_PLUGIN_VERSION', '2.0.3 7');29 define('MUCHAT_AI_CHATBOT_PLUGIN_VERSION', '2.0.38'); 30 30 // define('MUCHAT_AI_CHATBOT_CACHE_DURATION', HOUR_IN_SECONDS); 31 31 define('MUCHAT_AI_CHATBOT_PLUGIN_FILE', __FILE__); -
muchat-ai/trunk/readme.txt
r3309949 r3330738 4 4 Requires at least: 5.0 5 5 Tested up to: 6.8 6 Stable tag: 2.0.3 76 Stable tag: 2.0.38 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later … … 76 76 == Changelog == 77 77 78 = 2.0.38 = 79 - Feature: Added an option for different initial messages for guest (non-logged-in) users. 80 - Performance: Major enhancements to API and admin pages. 81 - Performance: Optimized database queries for products, posts, and pages to reduce memory usage on large sites. 82 - Performance: Implemented caching for API token validation to prevent server slowdowns. 83 - Performance: Cached heavy queries on the plugin's settings page to improve load times. 84 - Bugfix: Corrected an "undefined variable" warning on the widget settings page. 85 78 86 = 2.0.37 = 79 87 * Fixed issue with `<front>` tag being removed when saving display rules -
muchat-ai/trunk/templates/admin/settings.php
r3300525 r3330738 57 57 <?php submit_button(__('Save Changes', 'muchat-ai'), 'primary', 'submit', false, ['style' => 'margin-top: 20px;']); ?> 58 58 </form> 59 60 <form method="get" style="display: inline-block; margin-left: 10px; margin-top: 20px;"> 61 <input type="hidden" name="page" value="muchat-woocommerce-api"> 62 <input type="hidden" name="action" value="muchat_refresh_meta"> 63 <?php wp_nonce_field('muchat_refresh_meta_action'); ?> 64 <?php submit_button(__('Refresh Fields', 'muchat-ai'), 'secondary', 'refresh_meta', false); ?> 65 </form> 59 66 </div> -
muchat-ai/trunk/templates/admin/widget-settings.php
r3309949 r3330738 150 150 </td> 151 151 </tr> 152 <tr valign="top"> 153 <th scope="row"><?php esc_html_e('Different Messages for Guests', 'muchat-ai'); ?></th> 154 <td> 155 <label> 156 <input type="checkbox" id="muchat_ai_chatbot_enable_guest_messages" name="muchat_ai_chatbot_enable_guest_messages" value="1" <?php checked($enable_guest_messages, '1'); ?> /> 157 <?php esc_html_e('Enable different initial messages for non-logged-in users.', 'muchat-ai'); ?> 158 </label> 159 </td> 160 </tr> 161 <tr valign="top" id="muchat-guest-initial-messages-row" style="display: none;"> 162 <th scope="row"><?php esc_html_e('Guest Initial Messages', 'muchat-ai'); ?></th> 163 <td> 164 <div id="muchat-guest-initial-messages-list"></div> 165 <button type="button" class="button" id="muchat-add-guest-initial-message"> 166 <span class="dashicons dashicons-plus-alt2" style="vertical-align: text-bottom;"></span> 167 <?php esc_html_e('Add Message', 'muchat-ai'); ?> 168 </button> 169 <input type="hidden" id="muchat_ai_chatbot_guest_initial_messages" name="muchat_ai_chatbot_guest_initial_messages" value="<?php echo esc_attr(is_array($guest_initial_messages) ? '' : $guest_initial_messages); ?>" /> 170 <p class="description"> 171 <?php esc_html_e('These messages will be shown to visitors who are not logged in. If left empty, the default messages above will be used.', 'muchat-ai'); ?> 172 </p> 173 </td> 174 </tr> 152 175 <tr valign="top"> 153 176 <th scope="row"><?php esc_html_e('Load Strategy', 'muchat-ai'); ?></th>
Note: See TracChangeset
for help on using the changeset viewer.