Changeset 3406576
- Timestamp:
- 12/01/2025 08:42:59 AM (4 months ago)
- Location:
- muchat-ai
- Files:
-
- 4 added
- 22 edited
- 1 copied
-
tags/2.0.51 (copied) (copied from muchat-ai/trunk)
-
tags/2.0.51/includes/Admin/Settings.php (modified) (5 diffs)
-
tags/2.0.51/includes/Api/Controllers/CustomPostTypeController.php (added)
-
tags/2.0.51/includes/Api/Middleware/AuthMiddleware.php (modified) (1 diff)
-
tags/2.0.51/includes/Api/Routes.php (modified) (5 diffs)
-
tags/2.0.51/includes/Frontend/Widget.php (modified) (12 diffs)
-
tags/2.0.51/includes/Models/CustomPostType.php (added)
-
tags/2.0.51/includes/Models/Page.php (modified) (1 diff)
-
tags/2.0.51/includes/Models/Post.php (modified) (1 diff)
-
tags/2.0.51/includes/Models/Product.php (modified) (6 diffs)
-
tags/2.0.51/muchat-ai.php (modified) (4 diffs)
-
tags/2.0.51/readme.txt (modified) (2 diffs)
-
tags/2.0.51/templates/admin/api-documentation.php (modified) (4 diffs)
-
tags/2.0.51/templates/admin/widget-settings.php (modified) (2 diffs)
-
trunk/includes/Admin/Settings.php (modified) (5 diffs)
-
trunk/includes/Api/Controllers/CustomPostTypeController.php (added)
-
trunk/includes/Api/Middleware/AuthMiddleware.php (modified) (1 diff)
-
trunk/includes/Api/Routes.php (modified) (5 diffs)
-
trunk/includes/Frontend/Widget.php (modified) (12 diffs)
-
trunk/includes/Models/CustomPostType.php (added)
-
trunk/includes/Models/Page.php (modified) (1 diff)
-
trunk/includes/Models/Post.php (modified) (1 diff)
-
trunk/includes/Models/Product.php (modified) (6 diffs)
-
trunk/muchat-ai.php (modified) (4 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/templates/admin/api-documentation.php (modified) (4 diffs)
-
trunk/templates/admin/widget-settings.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
muchat-ai/tags/2.0.51/includes/Admin/Settings.php
r3373032 r3406576 13 13 14 14 /** 15 * PERFORMANCE: Pre-encoded SVG icon to avoid Disk I/O on every page load 16 * This is the base64-encoded version of assets/images/icon.svg 17 * 18 * The SVG contains only safe elements (path, g) with no scripts or external references. 19 * Re-generate this constant if the icon file changes using: 20 * cat assets/images/icon.svg | base64 | tr -d '\n' 21 */ 22 private const MENU_ICON_BASE64 = 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMDggMTA4IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8ZyBmaWxsPSJjdXJyZW50Q29sb3IiIGZpbGwtcnVsZT0ibm9uemVybyI+CiAgICAgICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjYuNzY5NiwgNDAuODQ2OSkiPgogICAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgICAgZD0iTTYwLjExMTcyOTcsLTIuMzk4OTk4NTllLTI0IEM2MC4wMjA3NTY4LDQuNjM4NTUxMzUgNTguOTg3OTQ1OSw5LjAxNDg4NjQ5IDU3LjE1ODg1NDEsMTMuMDA1OTI0MyBDNjYuODAwOTE4OSwxNC42MTI0IDY2Ljg3OTA0ODYsMjIuNDY0OTczIDY2Ljg3OTA0ODYsMjYuMDQ1MDI3IEM2Ni44NzkwNDg2LDI5LjM2MzkzNTEgNjYuODc5MDQ4NiwzOS40MjM0MDU0IDUzLjUxMzUxMzUsMzkuNDIzNDA1NCBMNDAuMTIyMjkxOSwzOS40MjM0MDU0IEMzMS40MDcwODExLDM5LjQyMzQwNTQgMjMuNjA4MDIxNiw0My42MzA2Mzc4IDE4LjcyMjIzNzgsNTAuMTM2ODEwOCBDMTUuNDgyNTI5Nyw0Ny42ODA1NDA1IDEzLjM2NTUzNTEsNDMuNzg2ODk3MyAxMy4zNjU1MzUxLDM5LjQyMzQwNTQgTDEzLjM2NTUzNTEsMjYuMDQ1MDI3IEMxMy4zNjU1MzUxLDIzLjM5OTMxODkgMTMuODM1MzgzOCwyMS4zMTU1MDI3IDE0LjU2NzQ0ODYsMTkuNTk3NzE4OSBDMTQuMTc1NzI5NywxOS41MjYwMTA4IDEzLjc4NDAxMDgsMTkuMzU1ODM3OCAxMy4zNjU1MzUxLDE5LjM1NTgzNzggTDAuNjY2Nzc4Mzc4LDE5LjM1NTgzNzggQzAuMjM2NTI5NzMsMjEuNDI2ODEwOCAyLjM5ODk5ODU5ZS0yNCwyMy42NDEyIDIuMzk4OTk4NTllLTI0LDI2LjA0NTAyNyBMMi4zOTg5OTg1OWUtMjQsMzkuNDIzNDA1NCBDMi4zOTg5OTg1OWUtMjQsNTQuMjAwNjI3IDExLjk2Nzc2MjIsNjYuMTgwMTYyMiAyNi43NDM5MTM1LDY2LjE4MDE2MjIgQzI2Ljc0MzkxMzUsNTguNzg1NjY0OSAzMi43Mjc3OTQ2LDUyLjgwMTc4MzggNDAuMTIyMjkxOSw1Mi44MDE3ODM4IEw1My41MTM1MTM1LDUyLjgwMTc4MzggQzY2Ljg3OTA0ODYsNTIuODAxNzgzOCA4MC4yNTc0MjcsNDUuMjc2NzEzNSA4MC4yNTc0MjcsMjYuMDQ1MDI3IEM4MC4yNTc0MjcsOS42ODE2NjQ4NiA3MC45NTU3MDgxLDIuMDk3NzI5NzMgNjAuMTExNzI5NywtMi4zOTg5OTg1OWUtMjQgWiIgLz4KICAgICAgICA8L2c+CiAgICAgICAgPHBhdGgKICAgICAgICAgICAgZD0iTTUzLjUxMzUxMzUsMTMuMzc4Mzc4NCBDNTguNTA0MTgzOCwxMy4zNzgzNzg0IDY2Ljg5MTg5MTksMTUuMTE1NDI3IDY2Ljg5MTg5MTksMjYuNzU2NzU2OCBMNjYuODkxODkxOSw0MC4xMzUxMzUxIEM2Ni44OTE4OTE5LDQ0LjUxMjU0MDUgNjQuNzc0ODk3Myw0OC4zOTg2OTE5IDYxLjUzNTE4OTIsNTAuODQyMTE4OSBDNTYuNjQ5NDA1NCw0NC4zNDg3ODkyIDQ4Ljg2MjExODksNDAuMTM1MTM1MSA0MC4xMzUxMzUxLDQwLjEzNTEzNTEgTDI2Ljc2OTYsNDAuMTM1MTM1MSBDMTMuMzc4Mzc4NCw0MC4xMzUxMzUxIDEzLjM3ODM3ODQsMzAuMDY5MjQzMiAxMy4zNzgzNzg0LDI2Ljc1Njc1NjggQzEzLjM3ODM3ODQsMjIuNzU5Mjk3MyAxMy4zNzgzNzg0LDEzLjM3ODM3ODQgMjYuNzY5NiwxMy4zNzgzNzg0IEw1My41MTM1MTM1LDEzLjM3ODM3ODQgTTUzLjUxMzUxMzUsMCBMMjYuNzY5NiwwIEMxMy4zNzgzNzg0LDAgMCw3LjEwNzY2NDg2IDAsMjYuNzU2NzU2OCBDMCw0NS45ODg0NDMyIDEzLjM3ODM3ODQsNTMuNTEzNTEzNSAyNi43Njk2LDUzLjUxMzUxMzUgTDQwLjEzNTEzNTEsNTMuNTEzNTEzNSBDNDcuNTI5NjMyNCw1My41MTM1MTM1IDUzLjUxMzUxMzUsNTkuNTExMzA4MSA1My41MTM1MTM1LDY2Ljg5MTg5MTkgQzY4LjI5MDczNTEsNjYuODkxODkxOSA4MC4yODMxMTM1LDU0LjkxMjM1NjggODAuMjgzMTEzNSw0MC4xMzUxMzUxIEw4MC4yODMxMTM1LDI2Ljc1Njc1NjggQzgwLjI4MzExMzUsOC4yODkyNDMyNCA2Ni44OTE4OTE5LDAgNTMuNTEzNTEzNSwwIEw1My41MTM1MTM1LDAgWiIgLz4KICAgIDwvZz4KPC9zdmc+'; 23 24 /** 15 25 * Initialize the class 16 26 */ … … 20 30 add_action('admin_enqueue_scripts', [$this, 'enqueue_styles']); 21 31 add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']); 32 33 // Register AJAX handlers for API tester functionality 34 add_action('wp_ajax_muchat_api_search_content', [$this, 'ajax_search_content']); 35 add_action('wp_ajax_muchat_api_preview', [$this, 'ajax_preview_content']); 22 36 } 23 37 24 38 /** 25 39 * Get the SVG icon for the menu 40 * 41 * PERFORMANCE: Uses pre-encoded base64 constant to avoid disk I/O 42 * SECURITY: SVG content has been validated to contain only safe elements 43 * 44 * @return string Base64-encoded SVG data URI 26 45 */ 27 46 private function muchat_ai_chatbot_get_menu_icon() 28 47 { 29 $icon_path = MUCHAT_AI_CHATBOT_PLUGIN_PATH . 'assets/images/icon.svg'; 30 if (file_exists($icon_path)) { 31 $svg = file_get_contents($icon_path); 32 // Convert SVG to base64 and ensure it's properly encoded 33 $base64 = base64_encode($svg); 34 return 'data:image/svg+xml;base64,' . $base64; 35 } 36 return 'dashicons-format-chat'; 48 // Use pre-encoded constant for performance (no disk I/O) 49 return 'data:image/svg+xml;base64,' . self::MENU_ICON_BASE64; 37 50 } 38 51 … … 786 799 787 800 /** 801 * Get available custom post types 802 * 803 * @return array 804 */ 805 public function get_available_custom_post_types() 806 { 807 $excluded_types = ['post', 'page', 'product', 'attachment', 'revision', 'nav_menu_item']; 808 $post_types = get_post_types(['public' => true], 'objects'); 809 $custom_post_types = []; 810 811 foreach ($post_types as $post_type) { 812 // Skip excluded types 813 if (in_array($post_type->name, $excluded_types)) { 814 continue; 815 } 816 817 // Only include public post types or those explicitly set to show in REST API 818 if (!$post_type->public && !$post_type->show_in_rest) { 819 continue; 820 } 821 822 $custom_post_types[$post_type->name] = [ 823 'name' => $post_type->name, 824 'label' => $post_type->label, 825 'singular_label' => $post_type->labels->singular_name ?? $post_type->label, 826 'description' => $post_type->description ?? '', 827 ]; 828 } 829 830 return $custom_post_types; 831 } 832 833 /** 834 * Get default fields for custom post type 835 * 836 * @param string $post_type 837 * @return array 838 */ 839 public function get_default_fields_for_custom_post_type($post_type) 840 { 841 return [ 842 'id' => __('ID', 'muchat-ai'), 843 'title' => __('Title', 'muchat-ai'), 844 'content' => __('Content', 'muchat-ai'), 845 'excerpt' => __('Excerpt', 'muchat-ai'), 846 'modified_date' => __('Modified Date', 'muchat-ai'), 847 'permalink' => __('Permalink', 'muchat-ai'), 848 'featured_image' => __('Featured Image', 'muchat-ai'), 849 ]; 850 } 851 852 /** 788 853 * Register plugin settings 789 854 */ … … 856 921 register_setting('muchat-settings-group', 'muchat_ai_chatbot_visibility_pages', array( 857 922 'type' => 'string', 858 'sanitize_callback' => function ($input) { 859 // First replace <front> with a placeholder to protect it 860 $input = str_replace('<front>', '##FRONTPLACEHOLDER##', $input); 861 // Do regular sanitization 862 $sanitized = sanitize_textarea_field($input); 863 // Restore <front> tags 864 return str_replace('##FRONTPLACEHOLDER##', '<front>', $sanitized); 865 }, 923 'sanitize_callback' => array($this, 'sanitize_visibility_pages'), 866 924 'default' => '' 867 925 )); … … 949 1007 } 950 1008 } 1009 1010 /** 1011 * Sanitize visibility pages input. 1012 * Preserves Unicode characters (Persian, Arabic, etc.), percent-encoded URLs, 1013 * wildcards (*), and the special <front> tag. 1014 * 1015 * @param string $input The raw input from the textarea. 1016 * @return string The sanitized input. 1017 */ 1018 public function sanitize_visibility_pages($input) 1019 { 1020 if (empty($input)) { 1021 return ''; 1022 } 1023 1024 // Split input into lines 1025 $lines = explode("\n", $input); 1026 $sanitized_lines = array(); 1027 1028 foreach ($lines as $line) { 1029 $line = trim($line); 1030 1031 // Skip empty lines 1032 if (empty($line)) { 1033 continue; 1034 } 1035 1036 // Check for special <front> tag (case-insensitive) 1037 if (strtolower($line) === '<front>') { 1038 $sanitized_lines[] = '<front>'; 1039 continue; 1040 } 1041 1042 // Check for global wildcard 1043 if ($line === '*') { 1044 $sanitized_lines[] = '*'; 1045 continue; 1046 } 1047 1048 // Decode percent-encoded URLs to UTF-8 1049 // This handles URLs like %d8%aa%d8%b3%d8%aa (تست) 1050 $decoded_line = $line; 1051 1052 // Check if the line contains percent-encoded characters 1053 if (preg_match('/%[0-9A-Fa-f]{2}/', $line)) { 1054 $decoded = urldecode($line); 1055 // Only use decoded version if it's valid UTF-8 1056 if (mb_check_encoding($decoded, 'UTF-8')) { 1057 $decoded_line = $decoded; 1058 } 1059 } 1060 1061 // Normalize the path: 1062 // 1. Remove any HTML tags (except for security, we don't want scripts) 1063 $decoded_line = wp_strip_all_tags($decoded_line); 1064 1065 // 2. Ensure path starts with / 1066 if (!empty($decoded_line) && $decoded_line[0] !== '/' && $decoded_line[0] !== '*') { 1067 $decoded_line = '/' . $decoded_line; 1068 } 1069 1070 // 3. Remove trailing slash (unless it's just /) 1071 if (strlen($decoded_line) > 1) { 1072 $decoded_line = rtrim($decoded_line, '/'); 1073 } 1074 1075 // 4. Normalize Unicode to NFC form for consistent storage 1076 if (function_exists('normalizer_normalize')) { 1077 $normalized = normalizer_normalize($decoded_line, \Normalizer::FORM_C); 1078 if ($normalized !== false) { 1079 $decoded_line = $normalized; 1080 } 1081 } 1082 1083 // 5. Only allow safe characters: 1084 // - Unicode letters and numbers (including Persian/Arabic) 1085 // - Forward slash, hyphen, underscore, dot, asterisk (wildcard) 1086 // - Percent sign (for any remaining encoded chars) 1087 // Using preg_replace with 'u' modifier for UTF-8 support 1088 $decoded_line = preg_replace('/[^\p{L}\p{N}\/\-\_\.\*\%]/u', '', $decoded_line); 1089 1090 if (!empty($decoded_line)) { 1091 $sanitized_lines[] = $decoded_line; 1092 } 1093 } 1094 1095 return implode("\n", $sanitized_lines); 1096 } 1097 1098 /** 1099 * AJAX handler for searching content (products, posts, pages) 1100 * 1101 * SECURITY: Implements nonce verification and capability check 1102 */ 1103 public function ajax_search_content() 1104 { 1105 // SECURITY: Verify nonce 1106 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key($_POST['nonce']), 'muchat_api_nonce')) { 1107 wp_send_json_error(['message' => __('Security check failed', 'muchat-ai')], 403); 1108 } 1109 1110 // SECURITY: Check user capability 1111 if (!current_user_can('manage_options')) { 1112 wp_send_json_error(['message' => __('Permission denied', 'muchat-ai')], 403); 1113 } 1114 1115 // Sanitize input 1116 $query = isset($_POST['query']) ? sanitize_text_field(wp_unslash($_POST['query'])) : ''; 1117 $page = isset($_POST['page']) ? absint($_POST['page']) : 1; 1118 $per_page = 20; 1119 1120 if (strlen($query) < 2) { 1121 wp_send_json_error(['message' => __('Query too short', 'muchat-ai')], 400); 1122 } 1123 1124 $results = []; 1125 $total = 0; 1126 1127 // Search products (if WooCommerce is active) 1128 if (class_exists('WooCommerce')) { 1129 $product_args = [ 1130 'post_type' => 'product', 1131 'post_status' => 'publish', 1132 's' => $query, 1133 'posts_per_page' => $per_page, 1134 'paged' => $page, 1135 ]; 1136 1137 $product_query = new \WP_Query($product_args); 1138 1139 foreach ($product_query->posts as $post) { 1140 $results[] = [ 1141 'id' => $post->ID, 1142 'title' => esc_html($post->post_title), 1143 'type' => 'product', 1144 ]; 1145 } 1146 $total += $product_query->found_posts; 1147 } 1148 1149 // Search posts 1150 $post_args = [ 1151 'post_type' => 'post', 1152 'post_status' => 'publish', 1153 's' => $query, 1154 'posts_per_page' => $per_page, 1155 'paged' => $page, 1156 ]; 1157 1158 $post_query = new \WP_Query($post_args); 1159 1160 foreach ($post_query->posts as $post) { 1161 $results[] = [ 1162 'id' => $post->ID, 1163 'title' => esc_html($post->post_title), 1164 'type' => 'post', 1165 ]; 1166 } 1167 $total += $post_query->found_posts; 1168 1169 // Search pages 1170 $page_args = [ 1171 'post_type' => 'page', 1172 'post_status' => 'publish', 1173 's' => $query, 1174 'posts_per_page' => $per_page, 1175 'paged' => $page, 1176 ]; 1177 1178 $page_query = new \WP_Query($page_args); 1179 1180 foreach ($page_query->posts as $post) { 1181 $results[] = [ 1182 'id' => $post->ID, 1183 'title' => esc_html($post->post_title), 1184 'type' => 'page', 1185 ]; 1186 } 1187 $total += $page_query->found_posts; 1188 1189 // Sort results by relevance (title match priority) 1190 usort($results, function($a, $b) use ($query) { 1191 $a_match = stripos($a['title'], $query) !== false ? 0 : 1; 1192 $b_match = stripos($b['title'], $query) !== false ? 0 : 1; 1193 return $a_match - $b_match; 1194 }); 1195 1196 wp_send_json_success([ 1197 'items' => array_slice($results, 0, $per_page), 1198 'total' => $total, 1199 'more' => ($page * $per_page) < $total, 1200 ]); 1201 } 1202 1203 /** 1204 * AJAX handler for previewing content via API 1205 * 1206 * SECURITY: Implements nonce verification and capability check 1207 */ 1208 public function ajax_preview_content() 1209 { 1210 // SECURITY: Verify nonce 1211 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key($_POST['nonce']), 'muchat_api_nonce')) { 1212 wp_send_json_error(['message' => __('Security check failed', 'muchat-ai')], 403); 1213 } 1214 1215 // SECURITY: Check user capability 1216 if (!current_user_can('manage_options')) { 1217 wp_send_json_error(['message' => __('Permission denied', 'muchat-ai')], 403); 1218 } 1219 1220 // Sanitize input 1221 $content_type = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : ''; 1222 $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0; 1223 1224 // Validate content type 1225 $allowed_types = ['product', 'post', 'page']; 1226 if (!in_array($content_type, $allowed_types, true)) { 1227 wp_send_json_error(['message' => __('Invalid content type', 'muchat-ai')], 400); 1228 } 1229 1230 if ($item_id <= 0) { 1231 wp_send_json_error(['message' => __('Invalid item ID', 'muchat-ai')], 400); 1232 } 1233 1234 $data = null; 1235 1236 try { 1237 switch ($content_type) { 1238 case 'product': 1239 if (!class_exists('WooCommerce')) { 1240 wp_send_json_error(['message' => __('WooCommerce not active', 'muchat-ai')], 400); 1241 } 1242 $product_model = new \Muchat\Api\Models\Product(); 1243 $data = $product_model->get_product($item_id); 1244 break; 1245 1246 case 'post': 1247 $post_model = new \Muchat\Api\Models\Post(); 1248 $data = $post_model->get_post($item_id); 1249 break; 1250 1251 case 'page': 1252 $page_model = new \Muchat\Api\Models\Page(); 1253 $data = $page_model->get_page($item_id); 1254 break; 1255 } 1256 1257 if (empty($data)) { 1258 wp_send_json_error(['message' => __('Content not found', 'muchat-ai')], 404); 1259 } 1260 1261 wp_send_json_success($data); 1262 1263 } catch (\Exception $e) { 1264 wp_send_json_error(['message' => $e->getMessage()], 500); 1265 } 1266 } 951 1267 } -
muchat-ai/tags/2.0.51/includes/Api/Middleware/AuthMiddleware.php
r3330738 r3406576 14 14 { 15 15 // Development mode - return true 16 // return true;16 //return true; 17 17 18 18 // Production mode implementation: -
muchat-ai/tags/2.0.51/includes/Api/Routes.php
r3392161 r3406576 8 8 use Muchat\Api\Api\Controllers\PageController; 9 9 use Muchat\Api\Api\Controllers\OrderController; 10 use Muchat\Api\Api\Controllers\CustomPostTypeController; 10 11 use Muchat\Api\Api\Middleware\AuthMiddleware; 11 12 … … 41 42 */ 42 43 private $order_controller; 44 45 /** 46 * @var CustomPostTypeController 47 */ 48 private $custom_post_type_controller; 43 49 44 50 /** … … 65 71 $this->page_controller = $page_controller; 66 72 $this->order_controller = $order_controller; 73 $this->custom_post_type_controller = new CustomPostTypeController(); 67 74 $this->auth_middleware = new AuthMiddleware(); 68 75 } … … 121 128 'callback' => [$this->page_controller, 'get_pages'], 122 129 'methods' => \WP_REST_Server::CREATABLE 130 ], 131 [ 132 'path' => '/custom-post-types/(?P<post_type>[a-zA-Z0-9_-]+)', 133 'callback' => [$this->custom_post_type_controller, 'get_custom_post_type_items'], 134 'methods' => \WP_REST_Server::READABLE, 135 'public' => true, 123 136 ] 124 137 ]; … … 131 144 'methods' => $route['methods'], 132 145 'callback' => $route['callback'], 133 'permission_callback' => [$this->auth_middleware, 'verify_token'],146 'permission_callback' => !empty($route['public']) ? '__return_true' : [$this->auth_middleware, 'verify_token'], 134 147 'args' => $this->get_collection_params(), 135 148 // Add headers to prevent caching -
muchat-ai/tags/2.0.51/includes/Frontend/Widget.php
r3373032 r3406576 69 69 private function register_cache_clearing_hooks() 70 70 { 71 // Core widget settings 71 72 add_action('update_option_muchat_ai_chatbot_agent_id', [$this, 'clear_widget_cache']); 72 73 add_action('update_option_muchat_ai_chatbot_interface_initial_messages', [$this, 'clear_widget_cache']); … … 75 76 add_action('update_option_muchat_ai_chatbot_script_position', [$this, 'clear_widget_cache']); 76 77 add_action('update_option_muchat_ai_chatbot_widget_enabled', [$this, 'clear_widget_cache']); 78 79 // Display Rules settings - IMPORTANT for visibility to work correctly! 80 add_action('update_option_muchat_ai_chatbot_visibility_mode', [$this, 'clear_all_caches']); 81 add_action('update_option_muchat_ai_chatbot_visibility_pages', [$this, 'clear_all_caches']); 82 83 // Schedule settings 84 add_action('update_option_muchat_ai_chatbot_schedule_enabled', [$this, 'clear_all_caches']); 85 add_action('update_option_muchat_ai_chatbot_schedule_days', [$this, 'clear_all_caches']); 86 add_action('update_option_muchat_ai_chatbot_schedule_start_time', [$this, 'clear_all_caches']); 87 add_action('update_option_muchat_ai_chatbot_schedule_end_time', [$this, 'clear_all_caches']); 77 88 } 78 89 … … 81 92 */ 82 93 public function clear_widget_cache() 94 { 95 \Muchat\Api\Utils\Cache::clear_widget_cache(); 96 } 97 98 /** 99 * Clear internal caches when display rules or schedule settings change. 100 * Note: We only clear our own internal caches, not third-party cache plugins. 101 * Users should manually clear their site cache if display rules don't work correctly. 102 */ 103 public function clear_all_caches() 83 104 { 84 105 \Muchat\Api\Utils\Cache::clear_widget_cache(); … … 192 213 /** 193 214 * Checks if the current request path matches any of the given patterns. 194 * Handles exact, wildcard, UTF-8 , percent-encoded, and <front> patterns.215 * Handles exact, wildcard, UTF-8 (Persian, Arabic, etc.), percent-encoded, and <front> patterns. 195 216 * 196 217 * @param string $patterns_string A newline-separated string of URL patterns. … … 203 224 $request_uri = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : ''; 204 225 $path_only = strtok($request_uri, '?'); 205 $current_path = rtrim(urldecode($path_only), '/');226 $current_path = $this->normalize_path(urldecode($path_only)); 206 227 207 228 // Treat an empty path (which can be the homepage) as '/'. … … 231 252 232 253 // A. Check for the special '<front>' tag. 233 if ( $decoded_pattern=== '<front>') {254 if (strtolower($decoded_pattern) === '<front>') { 234 255 if ($is_front) { 235 256 return true; // Match found. … … 239 260 240 261 // B. Normalize the pattern for comparison. 241 $normalized_pattern = rtrim($decoded_pattern, '/');262 $normalized_pattern = $this->normalize_path($decoded_pattern); 242 263 if (empty($normalized_pattern)) { 243 264 $normalized_pattern = '/'; … … 248 269 // Escape regex characters, then replace our wildcard `*` with `.*`. 249 270 // The 'u' modifier is crucial for correct UTF-8 pattern matching. 250 $regex = '@^' . str_replace('\*', '.*', preg_quote($normalized_pattern, '@')) . '$@u'; 271 // The 'i' modifier makes it case-insensitive for Latin characters. 272 $regex = '@^' . str_replace('\*', '.*', preg_quote($normalized_pattern, '@')) . '$@ui'; 251 273 if (preg_match($regex, $current_path)) { 252 274 return true; // Match found. … … 255 277 // D. Check for exact matches (only if no wildcard). 256 278 else { 257 if ($normalized_pattern === $current_path) { 279 // Case-insensitive comparison for Latin characters, exact for non-Latin (Persian, etc.) 280 if ($this->paths_match($normalized_pattern, $current_path)) { 258 281 return true; // Match found. 259 282 } … … 266 289 267 290 // No patterns matched the current path. 291 return false; 292 } 293 294 /** 295 * Normalize a URL path for consistent comparison. 296 * Handles trailing slashes, Unicode normalization (NFC), and encoding issues. 297 * 298 * @param string $path The path to normalize. 299 * @return string The normalized path. 300 */ 301 private function normalize_path($path) 302 { 303 // Remove trailing slash (but keep leading slash) 304 $path = rtrim($path, '/'); 305 306 // Ensure path starts with / 307 if (!empty($path) && $path[0] !== '/') { 308 $path = '/' . $path; 309 } 310 311 // Normalize Unicode characters to NFC form (Canonical Composition) 312 // This ensures that characters like Persian/Arabic are consistently represented. 313 // For example: "ک" (Arabic Kaf U+0643) vs "ک" (Farsi Keh U+06A9) 314 // or combining characters like "ی" + diacritic vs precomposed form 315 if (function_exists('normalizer_normalize')) { 316 $normalized = normalizer_normalize($path, \Normalizer::FORM_C); 317 if ($normalized !== false) { 318 $path = $normalized; 319 } 320 } 321 322 // Handle double URL encoding that some servers/browsers might cause 323 // For example: %25D9%2585 (double-encoded Persian) -> %D9%85 -> م 324 $prev_path = ''; 325 $max_iterations = 3; // Prevent infinite loops 326 $iteration = 0; 327 while ($prev_path !== $path && $iteration < $max_iterations) { 328 $prev_path = $path; 329 $decoded = urldecode($path); 330 // Only update if decoding actually changed something and result is valid UTF-8 331 if ($decoded !== $path && mb_check_encoding($decoded, 'UTF-8')) { 332 $path = $decoded; 333 } else { 334 break; 335 } 336 $iteration++; 337 } 338 339 return $path; 340 } 341 342 /** 343 * Compare two paths for equality. 344 * Case-insensitive for ASCII/Latin characters, exact match for non-Latin (Persian, Arabic, etc.) 345 * 346 * @param string $pattern The pattern path. 347 * @param string $path The current path. 348 * @return bool True if paths match. 349 */ 350 private function paths_match($pattern, $path) 351 { 352 // First try exact match (fastest) 353 if ($pattern === $path) { 354 return true; 355 } 356 357 // Try case-insensitive match for paths with ASCII characters 358 // This handles /About vs /about for English URLs 359 // mb_strtolower with UTF-8 encoding preserves non-Latin characters correctly 360 $pattern_lower = mb_strtolower($pattern, 'UTF-8'); 361 $path_lower = mb_strtolower($path, 'UTF-8'); 362 363 if ($pattern_lower === $path_lower) { 364 return true; 365 } 366 268 367 return false; 269 368 } … … 512 611 /** 513 612 * Generate the script tag for the widget 613 * 614 * SECURITY: Uses wp_json_encode with security flags to prevent XSS: 615 * - JSON_HEX_TAG: Converts < and > to \u003C and \u003E 616 * - JSON_HEX_AMP: Converts & to \u0026 617 * - JSON_HEX_APOS: Converts ' to \u0027 618 * - JSON_HEX_QUOT: Converts " to \u0022 619 * - JSON_UNESCAPED_UNICODE: Keeps Unicode characters readable 514 620 */ 515 621 private function generate_widget_script($config) 516 622 { 623 // SECURITY: Sanitize all config values recursively before encoding 624 $config = $this->sanitize_config_recursive($config); 625 517 626 $json_config = []; 518 627 519 // Convert PHP array to JS object notation 628 // SECURITY FLAGS: Prevent XSS by encoding special HTML characters 629 $json_flags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE; 630 631 // Convert PHP array to JS object notation with secure encoding 520 632 foreach ($config as $key => $value) { 521 if (is_array($value)) { 522 $json_config[] = "$key: " . wp_json_encode($value, JSON_UNESCAPED_UNICODE); 523 } else { 524 $json_config[] = "$key: " . wp_json_encode($value, JSON_UNESCAPED_UNICODE); 525 } 633 // Sanitize key to only allow alphanumeric and underscore 634 $safe_key = preg_replace('/[^a-zA-Z0-9_]/', '', $key); 635 636 // Use wp_json_encode with security flags 637 $encoded_value = wp_json_encode($value, $json_flags); 638 639 // Skip if encoding failed (returns false on failure) 640 if ($encoded_value === false) { 641 continue; 642 } 643 644 $json_config[] = $safe_key . ": " . $encoded_value; 526 645 } 527 646 528 647 $config_string = implode(",\n ", $json_config); 648 649 // SECURITY: Escape plugin version for use in URL 650 $safe_version = esc_attr(MUCHAT_AI_CHATBOT_PLUGIN_VERSION); 529 651 530 652 // Create the script tag 531 653 $script = "\n<script type='module'>\n"; 532 $script .= " import Chatbox from 'https://cdn.mu.chat/embeds/dist/chatbox/index.js?v=" . MUCHAT_AI_CHATBOT_PLUGIN_VERSION. "';\n";654 $script .= " import Chatbox from 'https://cdn.mu.chat/embeds/dist/chatbox/index.js?v=" . $safe_version . "';\n"; 533 655 $script .= " const widget = await Chatbox.initBubble({\n"; 534 656 $script .= " " . $config_string . "\n"; … … 538 660 return apply_filters('muchat_widget_output', $script); 539 661 } 662 663 /** 664 * Recursively sanitize config values 665 * 666 * @param mixed $data Data to sanitize 667 * @return mixed Sanitized data 668 */ 669 private function sanitize_config_recursive($data) 670 { 671 if (is_array($data)) { 672 $sanitized = []; 673 foreach ($data as $key => $value) { 674 $safe_key = is_string($key) ? sanitize_text_field($key) : $key; 675 $sanitized[$safe_key] = $this->sanitize_config_recursive($value); 676 } 677 return $sanitized; 678 } elseif (is_string($data)) { 679 // Sanitize string values - remove any potential script injections 680 return sanitize_text_field($data); 681 } elseif (is_int($data) || is_float($data) || is_bool($data) || is_null($data)) { 682 return $data; 683 } 684 685 return ''; 686 } 540 687 } -
muchat-ai/tags/2.0.51/includes/Models/Page.php
r3380443 r3406576 102 102 'items' => array_values(array_filter($pages)) 103 103 ]; 104 } 105 106 /** 107 * Get single page by ID 108 * 109 * @param int $page_id 110 * @return array|null 111 */ 112 public function get_page($page_id) 113 { 114 $page = get_post($page_id); 115 116 if (!$page || $page->post_type !== 'page' || $page->post_status !== 'publish') { 117 return null; 118 } 119 120 return $this->format_page($page); 104 121 } 105 122 -
muchat-ai/tags/2.0.51/includes/Models/Post.php
r3380443 r3406576 97 97 98 98 /** 99 * Get single post by ID 100 * 101 * @param int $post_id 102 * @return array|null 103 */ 104 public function get_post($post_id) 105 { 106 $post = get_post($post_id); 107 108 if (!$post || $post->post_type !== 'post' || $post->post_status !== 'publish') { 109 return null; 110 } 111 112 return $this->format_post($post); 113 } 114 115 /** 99 116 * Check if a post is valid (has content) 100 117 * -
muchat-ai/tags/2.0.51/includes/Models/Product.php
r3392161 r3406576 31 31 32 32 // Handle ordering - Use custom SQL approach for reliability 33 $order_by_field = isset($params['order_by']) ? $params['order_by']: 'modified';34 $order_direction = isset($params['order']) ? strtoupper( $params['order']) : 'ASC';33 $order_by_field = isset($params['order_by']) ? sanitize_key($params['order_by']) : 'modified'; 34 $order_direction = isset($params['order']) ? strtoupper(sanitize_key($params['order'])) : 'ASC'; 35 35 36 // Validate order direction 37 if (!in_array($order_direction, ['ASC', 'DESC'])) { 36 // SECURITY: Whitelist for order direction - only allow ASC or DESC 37 $allowed_directions = ['ASC', 'DESC']; 38 if (!in_array($order_direction, $allowed_directions, true)) { 38 39 $order_direction = 'ASC'; 39 40 } 40 41 41 // Map API field names to WP post fields 42 $field_map = [ 42 // SECURITY: Whitelist for allowed order fields - prevents SQL injection 43 // Only these exact field names are allowed 44 $allowed_fields = [ 43 45 'modified' => 'post_modified', 44 46 'date' => 'post_date', … … 47 49 ]; 48 50 49 $sql_field = isset($field_map[$order_by_field]) ? $field_map[$order_by_field] : 'post_modified'; 51 // Validate field is in whitelist, default to post_modified if not 52 if (!isset($allowed_fields[$order_by_field])) { 53 $order_by_field = 'modified'; 54 } 55 $sql_field = $allowed_fields[$order_by_field]; 50 56 51 57 // Use a custom orderby filter to ensure correct SQL 52 add_filter('posts_orderby', function($orderby, $query) use ($sql_field, $order_direction) { 58 // Both $sql_field and $order_direction are now guaranteed to be from whitelists 59 add_filter('posts_orderby', function($orderby, $query) use ($sql_field, $order_direction, $allowed_fields, $allowed_directions) { 53 60 // Only apply to our specific query 54 61 if (isset($query->query_vars['muchat_custom_order']) && $query->query_vars['muchat_custom_order'] === true) { 55 62 global $wpdb; 63 64 // Double-check security: verify values are still in whitelists 65 // This prevents any bypass even if values were modified in memory 66 if (!in_array($sql_field, $allowed_fields, true) || !in_array($order_direction, $allowed_directions, true)) { 67 return $orderby; // Return default if validation fails 68 } 69 70 // Safe to use - values are from strict whitelists 56 71 return "{$wpdb->posts}.{$sql_field} {$order_direction}, {$wpdb->posts}.ID ASC"; 57 72 } … … 767 782 /** 768 783 * Get all product meta fields with their information 784 * 785 * PERFORMANCE OPTIMIZATION: 786 * - Limited to 500 most recent products to avoid full table scan 787 * - Cached for 24 hours to reduce database load 769 788 * 770 789 * @return array … … 784 803 $added_fields = []; // Track added fields to prevent duplication 785 804 786 // 1. Get regular meta fields 805 // PERFORMANCE: Limit to 500 most recent products to avoid heavy table scan 806 // This provides a representative sample while keeping the query efficient 807 $product_limit = 500; 808 809 // 1. Get regular meta fields from recent products only 810 // Using subquery to limit products first, then join meta 787 811 $regular_fields = $wpdb->get_results($wpdb->prepare(" 788 812 SELECT pm.meta_key, … … 791 815 MAX(CASE WHEN p.post_type = 'product_variation' THEN 1 ELSE 0 END) as is_variation 792 816 FROM {$wpdb->postmeta} pm 793 JOIN {$wpdb->posts} p ON p.ID = pm.post_id 794 WHERE p.post_type IN ('product', 'product_variation') 795 AND pm.meta_key NOT LIKE %s 817 JOIN ( 818 SELECT ID, post_type 819 FROM {$wpdb->posts} 820 WHERE post_type IN ('product', 'product_variation') 821 AND post_status = 'publish' 822 ORDER BY post_modified DESC 823 LIMIT %d 824 ) p ON p.ID = pm.post_id 825 WHERE pm.meta_key NOT LIKE %s 796 826 GROUP BY pm.meta_key 797 827 HAVING usage_count > 0 798 ", '\_%'));828 ", $product_limit, '\_%')); 799 829 800 830 // 2. Process regular fields … … 893 923 $all_fields = array_values($meta_fields); 894 924 895 // Store the result in cache for 6 hours 896 set_transient($cache_key, $all_fields, 6 * HOUR_IN_SECONDS); 925 // PERFORMANCE: Store the result in cache for 24 hours 926 // Since meta fields rarely change, longer cache is appropriate 927 set_transient($cache_key, $all_fields, 24 * HOUR_IN_SECONDS); 897 928 898 929 return $all_fields; -
muchat-ai/tags/2.0.51/muchat-ai.php
r3392161 r3406576 5 5 * Plugin URI: https://mu.chat 6 6 * Description: Muchat, a powerful tool for customer support using artificial intelligence 7 * Version: 2.0.5 07 * Version: 2.0.51 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.5 0');29 define('MUCHAT_AI_CHATBOT_PLUGIN_VERSION', '2.0.51'); 30 30 // define('MUCHAT_AI_CHATBOT_CACHE_DURATION', HOUR_IN_SECONDS); 31 31 define('MUCHAT_AI_CHATBOT_PLUGIN_FILE', __FILE__); … … 111 111 }); 112 112 113 // Initialize plugin with unique function name 114 function muchat_ai_chatbot_run_plugin() 115 { 116 $plugin = new Muchat\Api\Core\Plugin(); 113 /** 114 * This function is hooked into 'plugins_loaded' to ensure all necessary WordPress 115 * functionality is available. 116 */ 117 function muchat_ai_chatbot_run() { 118 $plugin = new \Muchat\Api\Core\Plugin(); 117 119 $plugin->run(); 118 120 } 119 121 120 // Initialize all plugin components on plugins_loaded hook for reliability 121 function muchat_ai_chatbot_initialize() 122 // Hook the run function to the 'plugins_loaded' action 123 add_action('plugins_loaded', 'muchat_ai_chatbot_run'); 124 125 // Initialize product change tracker for WooCommerce on plugins_loaded hook 126 function muchat_ai_chatbot_initialize_woocommerce() 122 127 { 123 // Run the main plugin124 muchat_ai_chatbot_run_plugin();125 126 128 // Initialize product change tracker for WooCommerce 127 129 if (class_exists('WooCommerce')) { … … 129 131 } 130 132 } 131 add_action('plugins_loaded', 'muchat_ai_chatbot_initialize ');133 add_action('plugins_loaded', 'muchat_ai_chatbot_initialize_woocommerce'); 132 134 133 135 // Hook for plugin activation -
muchat-ai/tags/2.0.51/readme.txt
r3392161 r3406576 5 5 Tested up to: 6.8 6 6 Requires PHP: 7.3 7 Stable tag: 2.0.5 07 Stable tag: 2.0.51 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 76 76 == Changelog == 77 77 78 = 2.0.51 = 79 * Feat: New Custom Post Types API endpoint (`/custom-post-types/{post_type}`) - public access, no authentication required. 80 * Feat: Full UTF-8/Persian/Arabic URL support in Display Rules for RTL websites. 81 * Feat: Added API content search and preview functionality in admin panel. 82 * Feat: Added single post/page retrieval methods for API preview. 83 * Security: Enhanced SQL injection prevention with strict whitelist approach for order parameters. 84 * Fix: Improved API error isolation to prevent other plugin conflicts from affecting API responses. 85 * Docs: Added comprehensive Custom Post Types documentation to API Documentation page. 86 78 87 = 2.0.50 = 79 88 * Feat: Add `muchat_date_modified` field to products for precise tracking of price/stock changes. -
muchat-ai/tags/2.0.51/templates/admin/api-documentation.php
r3300525 r3406576 43 43 </div> 44 44 <div class="inside"> 45 <p><?php echo esc_html__('All API requests require authentication. Use the Authorization header with a Bearer token.', 'muchat-ai'); ?></p> 46 <p class="description"><?php echo esc_html__('Example headers:', 'muchat-ai'); ?></p> 45 <p><?php echo esc_html__('Most API requests require authentication. Use the Authorization header with a Bearer token.', 'muchat-ai'); ?></p> 46 <p><strong><?php echo esc_html__('Note:', 'muchat-ai'); ?></strong> <?php echo esc_html__('The Custom Post Types endpoint (/custom-post-types/{post_type}) is public and does not require an authentication token.', 'muchat-ai'); ?></p> 47 <p class="description"><?php echo esc_html__('Example headers for protected endpoints:', 'muchat-ai'); ?></p> 47 48 <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace;">Authorization: Bearer YOUR_API_TOKEN 48 49 Content-Type: application/json</pre> … … 94 95 ], 95 96 [ 97 'path' => '/custom-post-types/{post_type}', 98 'description' => __('Retrieve all items from a custom post type with pagination', 'muchat-ai'), 99 'url' => rest_url('muchat-api/v1/custom-post-types/{post_type}') 100 ], 101 [ 96 102 'path' => '/orders/track', 97 103 'description' => __('Track an order by order ID and email', 'muchat-ai'), … … 104 110 <tr> 105 111 <td><code style="word-break: keep-all;"><?php echo esc_html($endpoint['path']); ?></code></td> 106 <td><span class="badge" style="background-color: #00 a32a; color: white; padding: 3px 8px; border-radius: 3px; display: inline-block;">POST</span></td>112 <td><span class="badge" style="background-color: #0073aa; color: white; padding: 3px 8px; border-radius: 3px; display: inline-block;">GET</span></td> 107 113 <td><?php echo esc_html($endpoint['description']); ?></td> 108 114 <td> … … 423 429 </div> 424 430 431 <!-- Custom Post Types Section --> 432 <div id="custom-post-types" class="postbox"> 433 <div class="postbox-header"> 434 <h2 class="hndle"><?php echo esc_html__('Custom Post Types', 'muchat-ai'); ?></h2> 435 </div> 436 <div class="inside"> 437 <p><?php echo esc_html__('The API supports retrieving data from custom post types. You can access any public custom post type using the following endpoint format:', 'muchat-ai'); ?></p> 438 439 <div class="wp-clearfix" style="margin: 15px 0;"> 440 <code style="display: inline-block; margin-right: 10px; padding: 8px; background: #f0f0f1; border-radius: 3px;"><?php echo esc_html(rest_url('muchat-api/v1/custom-post-types/{post_type}')); ?></code> 441 <button type="button" class="button copy-button" data-clipboard-text="<?php echo esc_url(rest_url('muchat-api/v1/custom-post-types/{post_type}')); ?>"> 442 <?php echo esc_html__('Copy', 'muchat-ai'); ?> 443 </button> 444 </div> 445 446 <h3><?php echo esc_html__('How to Use', 'muchat-ai'); ?></h3> 447 <p><?php echo esc_html__('Replace {post_type} with the name of your custom post type. For example, if you have a custom post type called "portfolio", you would use:', 'muchat-ai'); ?></p> 448 <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;"><?php echo esc_html(rest_url('muchat-api/v1/custom-post-types/portfolio')); ?></pre> 449 450 <h3><?php echo esc_html__('Example Request', 'muchat-ai'); ?></h3> 451 <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">curl -X GET \ 452 '<?php echo esc_html(rest_url('muchat-api/v1/custom-post-types/portfolio')); ?>?take=10&order_by=title&order=ASC'</pre> 453 454 <h3><?php echo esc_html__('Example Response', 'muchat-ai'); ?></h3> 455 <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">{ 456 "plugin_version": "1.0.0", 457 "post_type": "portfolio", 458 "offset": 0, 459 "limit": 30, 460 "total_count": 50, 461 "has_more": true, 462 "items": [ 463 { 464 "id": 123, 465 "title": "Portfolio Item Title", 466 "content": "Portfolio item content...", 467 "excerpt": "Portfolio item excerpt...", 468 "date_modified": "2023-01-15 14:30:45", 469 "url": "https://example.com/portfolio/item-slug", 470 "featured_image": "https://example.com/wp-content/uploads/image.jpg" 471 } 472 // More items... 473 ] 474 }</pre> 475 476 <h3><?php echo esc_html__('Supported Post Types', 'muchat-ai'); ?></h3> 477 <p><?php echo esc_html__('The API automatically detects and supports all public custom post types registered in WordPress. The following post types are excluded (they have their own dedicated endpoints):', 'muchat-ai'); ?></p> 478 <ul style="margin-left: 20px;"> 479 <li><code>post</code> - <?php echo esc_html__('Use /posts endpoint', 'muchat-ai'); ?></li> 480 <li><code>page</code> - <?php echo esc_html__('Use /pages endpoint', 'muchat-ai'); ?></li> 481 <li><code>product</code> - <?php echo esc_html__('Use /products endpoint', 'muchat-ai'); ?></li> 482 <li><code>attachment</code>, <code>revision</code>, <code>nav_menu_item</code> - <?php echo esc_html__('Not supported', 'muchat-ai'); ?></li> 483 </ul> 484 485 <h3><?php echo esc_html__('Custom Fields and Taxonomies', 'muchat-ai'); ?></h3> 486 <p><?php echo esc_html__('Custom post type items can include custom meta fields and taxonomy terms. These are automatically included in the response if they are configured in the plugin settings.', 'muchat-ai'); ?></p> 487 488 <h3><?php echo esc_html__('Available Parameters', 'muchat-ai'); ?></h3> 489 <p><?php echo esc_html__('The custom post types endpoint supports the same parameters as other endpoints:', 'muchat-ai'); ?></p> 490 <ul style="margin-left: 20px;"> 491 <li><code>skip</code> - <?php echo esc_html__('Number of items to skip (default: 0)', 'muchat-ai'); ?></li> 492 <li><code>take</code> - <?php echo esc_html__('Number of items to retrieve (default: 30, max: 100)', 'muchat-ai'); ?></li> 493 <li><code>order_by</code> - <?php echo esc_html__('Field to sort by (default: modified,ID)', 'muchat-ai'); ?></li> 494 <li><code>order</code> - <?php echo esc_html__('Sort order: ASC or DESC (default: ASC)', 'muchat-ai'); ?></li> 495 <li><code>modified_after</code> - <?php echo esc_html__('Filter items modified after this date (format: YYYY-MM-DD HH:MM:SS)', 'muchat-ai'); ?></li> 496 </ul> 497 </div> 498 </div> 499 425 500 <!-- Error Handling Section --> 426 501 <div id="error-handling" class="postbox"> -
muchat-ai/tags/2.0.51/templates/admin/widget-settings.php
r3373032 r3406576 277 277 <th scope="row"><?php esc_html_e('Page List', 'muchat-ai'); ?></th> 278 278 <td> 279 <textarea name="muchat_ai_chatbot_visibility_pages" rows="6" cols="50" class="large-text code" ><?php279 <textarea name="muchat_ai_chatbot_visibility_pages" rows="6" cols="50" class="large-text code" dir="auto"><?php 280 280 $visibility_pages = get_option('muchat_ai_chatbot_visibility_pages', ''); 281 281 echo esc_textarea(is_array($visibility_pages) ? '' : htmlspecialchars_decode($visibility_pages)); … … 285 285 </p> 286 286 <p class="description"> 287 <?php esc_html_e('Examples:', 'muchat-ai'); ?> <code>/about</code>, <code>/blog/*</code> 288 </p> 289 </td> 290 </tr> 291 </table> 287 <?php esc_html_e('English examples:', 'muchat-ai'); ?> <code>/about</code>, <code>/blog/*</code>, <code>/product/shoes</code> 288 </p> 289 <p class="description"> 290 <?php esc_html_e('Persian/RTL examples:', 'muchat-ai'); ?> <code>/محصول/کفش</code>, <code>/دسته-بندی/*</code>, <code>/تماس-با-ما</code> 291 </p> 292 <p class="description"> 293 <small><?php esc_html_e('URLs are case-insensitive (/About = /about). Persian/Arabic URLs are fully supported.', 'muchat-ai'); ?></small> 294 </p> 295 </td> 296 </tr> 297 </table> 298 299 <!-- Cache Notice --> 300 <div class="notice notice-info inline" style="margin: 15px 0 0; padding: 10px 15px;"> 301 <p style="margin: 0;"> 302 <span class="dashicons dashicons-info" style="margin-right: 5px;"></span> 303 <?php esc_html_e('If you use a caching plugin and display rules don\'t work correctly, please clear your site cache.', 'muchat-ai'); ?> 304 </p> 305 </div> 292 306 </div> 293 307 </div> -
muchat-ai/trunk/includes/Admin/Settings.php
r3373032 r3406576 13 13 14 14 /** 15 * PERFORMANCE: Pre-encoded SVG icon to avoid Disk I/O on every page load 16 * This is the base64-encoded version of assets/images/icon.svg 17 * 18 * The SVG contains only safe elements (path, g) with no scripts or external references. 19 * Re-generate this constant if the icon file changes using: 20 * cat assets/images/icon.svg | base64 | tr -d '\n' 21 */ 22 private const MENU_ICON_BASE64 = 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMDggMTA4IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8ZyBmaWxsPSJjdXJyZW50Q29sb3IiIGZpbGwtcnVsZT0ibm9uemVybyI+CiAgICAgICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjYuNzY5NiwgNDAuODQ2OSkiPgogICAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgICAgZD0iTTYwLjExMTcyOTcsLTIuMzk4OTk4NTllLTI0IEM2MC4wMjA3NTY4LDQuNjM4NTUxMzUgNTguOTg3OTQ1OSw5LjAxNDg4NjQ5IDU3LjE1ODg1NDEsMTMuMDA1OTI0MyBDNjYuODAwOTE4OSwxNC42MTI0IDY2Ljg3OTA0ODYsMjIuNDY0OTczIDY2Ljg3OTA0ODYsMjYuMDQ1MDI3IEM2Ni44NzkwNDg2LDI5LjM2MzkzNTEgNjYuODc5MDQ4NiwzOS40MjM0MDU0IDUzLjUxMzUxMzUsMzkuNDIzNDA1NCBMNDAuMTIyMjkxOSwzOS40MjM0MDU0IEMzMS40MDcwODExLDM5LjQyMzQwNTQgMjMuNjA4MDIxNiw0My42MzA2Mzc4IDE4LjcyMjIzNzgsNTAuMTM2ODEwOCBDMTUuNDgyNTI5Nyw0Ny42ODA1NDA1IDEzLjM2NTUzNTEsNDMuNzg2ODk3MyAxMy4zNjU1MzUxLDM5LjQyMzQwNTQgTDEzLjM2NTUzNTEsMjYuMDQ1MDI3IEMxMy4zNjU1MzUxLDIzLjM5OTMxODkgMTMuODM1MzgzOCwyMS4zMTU1MDI3IDE0LjU2NzQ0ODYsMTkuNTk3NzE4OSBDMTQuMTc1NzI5NywxOS41MjYwMTA4IDEzLjc4NDAxMDgsMTkuMzU1ODM3OCAxMy4zNjU1MzUxLDE5LjM1NTgzNzggTDAuNjY2Nzc4Mzc4LDE5LjM1NTgzNzggQzAuMjM2NTI5NzMsMjEuNDI2ODEwOCAyLjM5ODk5ODU5ZS0yNCwyMy42NDEyIDIuMzk4OTk4NTllLTI0LDI2LjA0NTAyNyBMMi4zOTg5OTg1OWUtMjQsMzkuNDIzNDA1NCBDMi4zOTg5OTg1OWUtMjQsNTQuMjAwNjI3IDExLjk2Nzc2MjIsNjYuMTgwMTYyMiAyNi43NDM5MTM1LDY2LjE4MDE2MjIgQzI2Ljc0MzkxMzUsNTguNzg1NjY0OSAzMi43Mjc3OTQ2LDUyLjgwMTc4MzggNDAuMTIyMjkxOSw1Mi44MDE3ODM4IEw1My41MTM1MTM1LDUyLjgwMTc4MzggQzY2Ljg3OTA0ODYsNTIuODAxNzgzOCA4MC4yNTc0MjcsNDUuMjc2NzEzNSA4MC4yNTc0MjcsMjYuMDQ1MDI3IEM4MC4yNTc0MjcsOS42ODE2NjQ4NiA3MC45NTU3MDgxLDIuMDk3NzI5NzMgNjAuMTExNzI5NywtMi4zOTg5OTg1OWUtMjQgWiIgLz4KICAgICAgICA8L2c+CiAgICAgICAgPHBhdGgKICAgICAgICAgICAgZD0iTTUzLjUxMzUxMzUsMTMuMzc4Mzc4NCBDNTguNTA0MTgzOCwxMy4zNzgzNzg0IDY2Ljg5MTg5MTksMTUuMTE1NDI3IDY2Ljg5MTg5MTksMjYuNzU2NzU2OCBMNjYuODkxODkxOSw0MC4xMzUxMzUxIEM2Ni44OTE4OTE5LDQ0LjUxMjU0MDUgNjQuNzc0ODk3Myw0OC4zOTg2OTE5IDYxLjUzNTE4OTIsNTAuODQyMTE4OSBDNTYuNjQ5NDA1NCw0NC4zNDg3ODkyIDQ4Ljg2MjExODksNDAuMTM1MTM1MSA0MC4xMzUxMzUxLDQwLjEzNTEzNTEgTDI2Ljc2OTYsNDAuMTM1MTM1MSBDMTMuMzc4Mzc4NCw0MC4xMzUxMzUxIDEzLjM3ODM3ODQsMzAuMDY5MjQzMiAxMy4zNzgzNzg0LDI2Ljc1Njc1NjggQzEzLjM3ODM3ODQsMjIuNzU5Mjk3MyAxMy4zNzgzNzg0LDEzLjM3ODM3ODQgMjYuNzY5NiwxMy4zNzgzNzg0IEw1My41MTM1MTM1LDEzLjM3ODM3ODQgTTUzLjUxMzUxMzUsMCBMMjYuNzY5NiwwIEMxMy4zNzgzNzg0LDAgMCw3LjEwNzY2NDg2IDAsMjYuNzU2NzU2OCBDMCw0NS45ODg0NDMyIDEzLjM3ODM3ODQsNTMuNTEzNTEzNSAyNi43Njk2LDUzLjUxMzUxMzUgTDQwLjEzNTEzNTEsNTMuNTEzNTEzNSBDNDcuNTI5NjMyNCw1My41MTM1MTM1IDUzLjUxMzUxMzUsNTkuNTExMzA4MSA1My41MTM1MTM1LDY2Ljg5MTg5MTkgQzY4LjI5MDczNTEsNjYuODkxODkxOSA4MC4yODMxMTM1LDU0LjkxMjM1NjggODAuMjgzMTEzNSw0MC4xMzUxMzUxIEw4MC4yODMxMTM1LDI2Ljc1Njc1NjggQzgwLjI4MzExMzUsOC4yODkyNDMyNCA2Ni44OTE4OTE5LDAgNTMuNTEzNTEzNSwwIEw1My41MTM1MTM1LDAgWiIgLz4KICAgIDwvZz4KPC9zdmc+'; 23 24 /** 15 25 * Initialize the class 16 26 */ … … 20 30 add_action('admin_enqueue_scripts', [$this, 'enqueue_styles']); 21 31 add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']); 32 33 // Register AJAX handlers for API tester functionality 34 add_action('wp_ajax_muchat_api_search_content', [$this, 'ajax_search_content']); 35 add_action('wp_ajax_muchat_api_preview', [$this, 'ajax_preview_content']); 22 36 } 23 37 24 38 /** 25 39 * Get the SVG icon for the menu 40 * 41 * PERFORMANCE: Uses pre-encoded base64 constant to avoid disk I/O 42 * SECURITY: SVG content has been validated to contain only safe elements 43 * 44 * @return string Base64-encoded SVG data URI 26 45 */ 27 46 private function muchat_ai_chatbot_get_menu_icon() 28 47 { 29 $icon_path = MUCHAT_AI_CHATBOT_PLUGIN_PATH . 'assets/images/icon.svg'; 30 if (file_exists($icon_path)) { 31 $svg = file_get_contents($icon_path); 32 // Convert SVG to base64 and ensure it's properly encoded 33 $base64 = base64_encode($svg); 34 return 'data:image/svg+xml;base64,' . $base64; 35 } 36 return 'dashicons-format-chat'; 48 // Use pre-encoded constant for performance (no disk I/O) 49 return 'data:image/svg+xml;base64,' . self::MENU_ICON_BASE64; 37 50 } 38 51 … … 786 799 787 800 /** 801 * Get available custom post types 802 * 803 * @return array 804 */ 805 public function get_available_custom_post_types() 806 { 807 $excluded_types = ['post', 'page', 'product', 'attachment', 'revision', 'nav_menu_item']; 808 $post_types = get_post_types(['public' => true], 'objects'); 809 $custom_post_types = []; 810 811 foreach ($post_types as $post_type) { 812 // Skip excluded types 813 if (in_array($post_type->name, $excluded_types)) { 814 continue; 815 } 816 817 // Only include public post types or those explicitly set to show in REST API 818 if (!$post_type->public && !$post_type->show_in_rest) { 819 continue; 820 } 821 822 $custom_post_types[$post_type->name] = [ 823 'name' => $post_type->name, 824 'label' => $post_type->label, 825 'singular_label' => $post_type->labels->singular_name ?? $post_type->label, 826 'description' => $post_type->description ?? '', 827 ]; 828 } 829 830 return $custom_post_types; 831 } 832 833 /** 834 * Get default fields for custom post type 835 * 836 * @param string $post_type 837 * @return array 838 */ 839 public function get_default_fields_for_custom_post_type($post_type) 840 { 841 return [ 842 'id' => __('ID', 'muchat-ai'), 843 'title' => __('Title', 'muchat-ai'), 844 'content' => __('Content', 'muchat-ai'), 845 'excerpt' => __('Excerpt', 'muchat-ai'), 846 'modified_date' => __('Modified Date', 'muchat-ai'), 847 'permalink' => __('Permalink', 'muchat-ai'), 848 'featured_image' => __('Featured Image', 'muchat-ai'), 849 ]; 850 } 851 852 /** 788 853 * Register plugin settings 789 854 */ … … 856 921 register_setting('muchat-settings-group', 'muchat_ai_chatbot_visibility_pages', array( 857 922 'type' => 'string', 858 'sanitize_callback' => function ($input) { 859 // First replace <front> with a placeholder to protect it 860 $input = str_replace('<front>', '##FRONTPLACEHOLDER##', $input); 861 // Do regular sanitization 862 $sanitized = sanitize_textarea_field($input); 863 // Restore <front> tags 864 return str_replace('##FRONTPLACEHOLDER##', '<front>', $sanitized); 865 }, 923 'sanitize_callback' => array($this, 'sanitize_visibility_pages'), 866 924 'default' => '' 867 925 )); … … 949 1007 } 950 1008 } 1009 1010 /** 1011 * Sanitize visibility pages input. 1012 * Preserves Unicode characters (Persian, Arabic, etc.), percent-encoded URLs, 1013 * wildcards (*), and the special <front> tag. 1014 * 1015 * @param string $input The raw input from the textarea. 1016 * @return string The sanitized input. 1017 */ 1018 public function sanitize_visibility_pages($input) 1019 { 1020 if (empty($input)) { 1021 return ''; 1022 } 1023 1024 // Split input into lines 1025 $lines = explode("\n", $input); 1026 $sanitized_lines = array(); 1027 1028 foreach ($lines as $line) { 1029 $line = trim($line); 1030 1031 // Skip empty lines 1032 if (empty($line)) { 1033 continue; 1034 } 1035 1036 // Check for special <front> tag (case-insensitive) 1037 if (strtolower($line) === '<front>') { 1038 $sanitized_lines[] = '<front>'; 1039 continue; 1040 } 1041 1042 // Check for global wildcard 1043 if ($line === '*') { 1044 $sanitized_lines[] = '*'; 1045 continue; 1046 } 1047 1048 // Decode percent-encoded URLs to UTF-8 1049 // This handles URLs like %d8%aa%d8%b3%d8%aa (تست) 1050 $decoded_line = $line; 1051 1052 // Check if the line contains percent-encoded characters 1053 if (preg_match('/%[0-9A-Fa-f]{2}/', $line)) { 1054 $decoded = urldecode($line); 1055 // Only use decoded version if it's valid UTF-8 1056 if (mb_check_encoding($decoded, 'UTF-8')) { 1057 $decoded_line = $decoded; 1058 } 1059 } 1060 1061 // Normalize the path: 1062 // 1. Remove any HTML tags (except for security, we don't want scripts) 1063 $decoded_line = wp_strip_all_tags($decoded_line); 1064 1065 // 2. Ensure path starts with / 1066 if (!empty($decoded_line) && $decoded_line[0] !== '/' && $decoded_line[0] !== '*') { 1067 $decoded_line = '/' . $decoded_line; 1068 } 1069 1070 // 3. Remove trailing slash (unless it's just /) 1071 if (strlen($decoded_line) > 1) { 1072 $decoded_line = rtrim($decoded_line, '/'); 1073 } 1074 1075 // 4. Normalize Unicode to NFC form for consistent storage 1076 if (function_exists('normalizer_normalize')) { 1077 $normalized = normalizer_normalize($decoded_line, \Normalizer::FORM_C); 1078 if ($normalized !== false) { 1079 $decoded_line = $normalized; 1080 } 1081 } 1082 1083 // 5. Only allow safe characters: 1084 // - Unicode letters and numbers (including Persian/Arabic) 1085 // - Forward slash, hyphen, underscore, dot, asterisk (wildcard) 1086 // - Percent sign (for any remaining encoded chars) 1087 // Using preg_replace with 'u' modifier for UTF-8 support 1088 $decoded_line = preg_replace('/[^\p{L}\p{N}\/\-\_\.\*\%]/u', '', $decoded_line); 1089 1090 if (!empty($decoded_line)) { 1091 $sanitized_lines[] = $decoded_line; 1092 } 1093 } 1094 1095 return implode("\n", $sanitized_lines); 1096 } 1097 1098 /** 1099 * AJAX handler for searching content (products, posts, pages) 1100 * 1101 * SECURITY: Implements nonce verification and capability check 1102 */ 1103 public function ajax_search_content() 1104 { 1105 // SECURITY: Verify nonce 1106 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key($_POST['nonce']), 'muchat_api_nonce')) { 1107 wp_send_json_error(['message' => __('Security check failed', 'muchat-ai')], 403); 1108 } 1109 1110 // SECURITY: Check user capability 1111 if (!current_user_can('manage_options')) { 1112 wp_send_json_error(['message' => __('Permission denied', 'muchat-ai')], 403); 1113 } 1114 1115 // Sanitize input 1116 $query = isset($_POST['query']) ? sanitize_text_field(wp_unslash($_POST['query'])) : ''; 1117 $page = isset($_POST['page']) ? absint($_POST['page']) : 1; 1118 $per_page = 20; 1119 1120 if (strlen($query) < 2) { 1121 wp_send_json_error(['message' => __('Query too short', 'muchat-ai')], 400); 1122 } 1123 1124 $results = []; 1125 $total = 0; 1126 1127 // Search products (if WooCommerce is active) 1128 if (class_exists('WooCommerce')) { 1129 $product_args = [ 1130 'post_type' => 'product', 1131 'post_status' => 'publish', 1132 's' => $query, 1133 'posts_per_page' => $per_page, 1134 'paged' => $page, 1135 ]; 1136 1137 $product_query = new \WP_Query($product_args); 1138 1139 foreach ($product_query->posts as $post) { 1140 $results[] = [ 1141 'id' => $post->ID, 1142 'title' => esc_html($post->post_title), 1143 'type' => 'product', 1144 ]; 1145 } 1146 $total += $product_query->found_posts; 1147 } 1148 1149 // Search posts 1150 $post_args = [ 1151 'post_type' => 'post', 1152 'post_status' => 'publish', 1153 's' => $query, 1154 'posts_per_page' => $per_page, 1155 'paged' => $page, 1156 ]; 1157 1158 $post_query = new \WP_Query($post_args); 1159 1160 foreach ($post_query->posts as $post) { 1161 $results[] = [ 1162 'id' => $post->ID, 1163 'title' => esc_html($post->post_title), 1164 'type' => 'post', 1165 ]; 1166 } 1167 $total += $post_query->found_posts; 1168 1169 // Search pages 1170 $page_args = [ 1171 'post_type' => 'page', 1172 'post_status' => 'publish', 1173 's' => $query, 1174 'posts_per_page' => $per_page, 1175 'paged' => $page, 1176 ]; 1177 1178 $page_query = new \WP_Query($page_args); 1179 1180 foreach ($page_query->posts as $post) { 1181 $results[] = [ 1182 'id' => $post->ID, 1183 'title' => esc_html($post->post_title), 1184 'type' => 'page', 1185 ]; 1186 } 1187 $total += $page_query->found_posts; 1188 1189 // Sort results by relevance (title match priority) 1190 usort($results, function($a, $b) use ($query) { 1191 $a_match = stripos($a['title'], $query) !== false ? 0 : 1; 1192 $b_match = stripos($b['title'], $query) !== false ? 0 : 1; 1193 return $a_match - $b_match; 1194 }); 1195 1196 wp_send_json_success([ 1197 'items' => array_slice($results, 0, $per_page), 1198 'total' => $total, 1199 'more' => ($page * $per_page) < $total, 1200 ]); 1201 } 1202 1203 /** 1204 * AJAX handler for previewing content via API 1205 * 1206 * SECURITY: Implements nonce verification and capability check 1207 */ 1208 public function ajax_preview_content() 1209 { 1210 // SECURITY: Verify nonce 1211 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key($_POST['nonce']), 'muchat_api_nonce')) { 1212 wp_send_json_error(['message' => __('Security check failed', 'muchat-ai')], 403); 1213 } 1214 1215 // SECURITY: Check user capability 1216 if (!current_user_can('manage_options')) { 1217 wp_send_json_error(['message' => __('Permission denied', 'muchat-ai')], 403); 1218 } 1219 1220 // Sanitize input 1221 $content_type = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : ''; 1222 $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0; 1223 1224 // Validate content type 1225 $allowed_types = ['product', 'post', 'page']; 1226 if (!in_array($content_type, $allowed_types, true)) { 1227 wp_send_json_error(['message' => __('Invalid content type', 'muchat-ai')], 400); 1228 } 1229 1230 if ($item_id <= 0) { 1231 wp_send_json_error(['message' => __('Invalid item ID', 'muchat-ai')], 400); 1232 } 1233 1234 $data = null; 1235 1236 try { 1237 switch ($content_type) { 1238 case 'product': 1239 if (!class_exists('WooCommerce')) { 1240 wp_send_json_error(['message' => __('WooCommerce not active', 'muchat-ai')], 400); 1241 } 1242 $product_model = new \Muchat\Api\Models\Product(); 1243 $data = $product_model->get_product($item_id); 1244 break; 1245 1246 case 'post': 1247 $post_model = new \Muchat\Api\Models\Post(); 1248 $data = $post_model->get_post($item_id); 1249 break; 1250 1251 case 'page': 1252 $page_model = new \Muchat\Api\Models\Page(); 1253 $data = $page_model->get_page($item_id); 1254 break; 1255 } 1256 1257 if (empty($data)) { 1258 wp_send_json_error(['message' => __('Content not found', 'muchat-ai')], 404); 1259 } 1260 1261 wp_send_json_success($data); 1262 1263 } catch (\Exception $e) { 1264 wp_send_json_error(['message' => $e->getMessage()], 500); 1265 } 1266 } 951 1267 } -
muchat-ai/trunk/includes/Api/Middleware/AuthMiddleware.php
r3330738 r3406576 14 14 { 15 15 // Development mode - return true 16 // return true;16 //return true; 17 17 18 18 // Production mode implementation: -
muchat-ai/trunk/includes/Api/Routes.php
r3392161 r3406576 8 8 use Muchat\Api\Api\Controllers\PageController; 9 9 use Muchat\Api\Api\Controllers\OrderController; 10 use Muchat\Api\Api\Controllers\CustomPostTypeController; 10 11 use Muchat\Api\Api\Middleware\AuthMiddleware; 11 12 … … 41 42 */ 42 43 private $order_controller; 44 45 /** 46 * @var CustomPostTypeController 47 */ 48 private $custom_post_type_controller; 43 49 44 50 /** … … 65 71 $this->page_controller = $page_controller; 66 72 $this->order_controller = $order_controller; 73 $this->custom_post_type_controller = new CustomPostTypeController(); 67 74 $this->auth_middleware = new AuthMiddleware(); 68 75 } … … 121 128 'callback' => [$this->page_controller, 'get_pages'], 122 129 'methods' => \WP_REST_Server::CREATABLE 130 ], 131 [ 132 'path' => '/custom-post-types/(?P<post_type>[a-zA-Z0-9_-]+)', 133 'callback' => [$this->custom_post_type_controller, 'get_custom_post_type_items'], 134 'methods' => \WP_REST_Server::READABLE, 135 'public' => true, 123 136 ] 124 137 ]; … … 131 144 'methods' => $route['methods'], 132 145 'callback' => $route['callback'], 133 'permission_callback' => [$this->auth_middleware, 'verify_token'],146 'permission_callback' => !empty($route['public']) ? '__return_true' : [$this->auth_middleware, 'verify_token'], 134 147 'args' => $this->get_collection_params(), 135 148 // Add headers to prevent caching -
muchat-ai/trunk/includes/Frontend/Widget.php
r3373032 r3406576 69 69 private function register_cache_clearing_hooks() 70 70 { 71 // Core widget settings 71 72 add_action('update_option_muchat_ai_chatbot_agent_id', [$this, 'clear_widget_cache']); 72 73 add_action('update_option_muchat_ai_chatbot_interface_initial_messages', [$this, 'clear_widget_cache']); … … 75 76 add_action('update_option_muchat_ai_chatbot_script_position', [$this, 'clear_widget_cache']); 76 77 add_action('update_option_muchat_ai_chatbot_widget_enabled', [$this, 'clear_widget_cache']); 78 79 // Display Rules settings - IMPORTANT for visibility to work correctly! 80 add_action('update_option_muchat_ai_chatbot_visibility_mode', [$this, 'clear_all_caches']); 81 add_action('update_option_muchat_ai_chatbot_visibility_pages', [$this, 'clear_all_caches']); 82 83 // Schedule settings 84 add_action('update_option_muchat_ai_chatbot_schedule_enabled', [$this, 'clear_all_caches']); 85 add_action('update_option_muchat_ai_chatbot_schedule_days', [$this, 'clear_all_caches']); 86 add_action('update_option_muchat_ai_chatbot_schedule_start_time', [$this, 'clear_all_caches']); 87 add_action('update_option_muchat_ai_chatbot_schedule_end_time', [$this, 'clear_all_caches']); 77 88 } 78 89 … … 81 92 */ 82 93 public function clear_widget_cache() 94 { 95 \Muchat\Api\Utils\Cache::clear_widget_cache(); 96 } 97 98 /** 99 * Clear internal caches when display rules or schedule settings change. 100 * Note: We only clear our own internal caches, not third-party cache plugins. 101 * Users should manually clear their site cache if display rules don't work correctly. 102 */ 103 public function clear_all_caches() 83 104 { 84 105 \Muchat\Api\Utils\Cache::clear_widget_cache(); … … 192 213 /** 193 214 * Checks if the current request path matches any of the given patterns. 194 * Handles exact, wildcard, UTF-8 , percent-encoded, and <front> patterns.215 * Handles exact, wildcard, UTF-8 (Persian, Arabic, etc.), percent-encoded, and <front> patterns. 195 216 * 196 217 * @param string $patterns_string A newline-separated string of URL patterns. … … 203 224 $request_uri = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : ''; 204 225 $path_only = strtok($request_uri, '?'); 205 $current_path = rtrim(urldecode($path_only), '/');226 $current_path = $this->normalize_path(urldecode($path_only)); 206 227 207 228 // Treat an empty path (which can be the homepage) as '/'. … … 231 252 232 253 // A. Check for the special '<front>' tag. 233 if ( $decoded_pattern=== '<front>') {254 if (strtolower($decoded_pattern) === '<front>') { 234 255 if ($is_front) { 235 256 return true; // Match found. … … 239 260 240 261 // B. Normalize the pattern for comparison. 241 $normalized_pattern = rtrim($decoded_pattern, '/');262 $normalized_pattern = $this->normalize_path($decoded_pattern); 242 263 if (empty($normalized_pattern)) { 243 264 $normalized_pattern = '/'; … … 248 269 // Escape regex characters, then replace our wildcard `*` with `.*`. 249 270 // The 'u' modifier is crucial for correct UTF-8 pattern matching. 250 $regex = '@^' . str_replace('\*', '.*', preg_quote($normalized_pattern, '@')) . '$@u'; 271 // The 'i' modifier makes it case-insensitive for Latin characters. 272 $regex = '@^' . str_replace('\*', '.*', preg_quote($normalized_pattern, '@')) . '$@ui'; 251 273 if (preg_match($regex, $current_path)) { 252 274 return true; // Match found. … … 255 277 // D. Check for exact matches (only if no wildcard). 256 278 else { 257 if ($normalized_pattern === $current_path) { 279 // Case-insensitive comparison for Latin characters, exact for non-Latin (Persian, etc.) 280 if ($this->paths_match($normalized_pattern, $current_path)) { 258 281 return true; // Match found. 259 282 } … … 266 289 267 290 // No patterns matched the current path. 291 return false; 292 } 293 294 /** 295 * Normalize a URL path for consistent comparison. 296 * Handles trailing slashes, Unicode normalization (NFC), and encoding issues. 297 * 298 * @param string $path The path to normalize. 299 * @return string The normalized path. 300 */ 301 private function normalize_path($path) 302 { 303 // Remove trailing slash (but keep leading slash) 304 $path = rtrim($path, '/'); 305 306 // Ensure path starts with / 307 if (!empty($path) && $path[0] !== '/') { 308 $path = '/' . $path; 309 } 310 311 // Normalize Unicode characters to NFC form (Canonical Composition) 312 // This ensures that characters like Persian/Arabic are consistently represented. 313 // For example: "ک" (Arabic Kaf U+0643) vs "ک" (Farsi Keh U+06A9) 314 // or combining characters like "ی" + diacritic vs precomposed form 315 if (function_exists('normalizer_normalize')) { 316 $normalized = normalizer_normalize($path, \Normalizer::FORM_C); 317 if ($normalized !== false) { 318 $path = $normalized; 319 } 320 } 321 322 // Handle double URL encoding that some servers/browsers might cause 323 // For example: %25D9%2585 (double-encoded Persian) -> %D9%85 -> م 324 $prev_path = ''; 325 $max_iterations = 3; // Prevent infinite loops 326 $iteration = 0; 327 while ($prev_path !== $path && $iteration < $max_iterations) { 328 $prev_path = $path; 329 $decoded = urldecode($path); 330 // Only update if decoding actually changed something and result is valid UTF-8 331 if ($decoded !== $path && mb_check_encoding($decoded, 'UTF-8')) { 332 $path = $decoded; 333 } else { 334 break; 335 } 336 $iteration++; 337 } 338 339 return $path; 340 } 341 342 /** 343 * Compare two paths for equality. 344 * Case-insensitive for ASCII/Latin characters, exact match for non-Latin (Persian, Arabic, etc.) 345 * 346 * @param string $pattern The pattern path. 347 * @param string $path The current path. 348 * @return bool True if paths match. 349 */ 350 private function paths_match($pattern, $path) 351 { 352 // First try exact match (fastest) 353 if ($pattern === $path) { 354 return true; 355 } 356 357 // Try case-insensitive match for paths with ASCII characters 358 // This handles /About vs /about for English URLs 359 // mb_strtolower with UTF-8 encoding preserves non-Latin characters correctly 360 $pattern_lower = mb_strtolower($pattern, 'UTF-8'); 361 $path_lower = mb_strtolower($path, 'UTF-8'); 362 363 if ($pattern_lower === $path_lower) { 364 return true; 365 } 366 268 367 return false; 269 368 } … … 512 611 /** 513 612 * Generate the script tag for the widget 613 * 614 * SECURITY: Uses wp_json_encode with security flags to prevent XSS: 615 * - JSON_HEX_TAG: Converts < and > to \u003C and \u003E 616 * - JSON_HEX_AMP: Converts & to \u0026 617 * - JSON_HEX_APOS: Converts ' to \u0027 618 * - JSON_HEX_QUOT: Converts " to \u0022 619 * - JSON_UNESCAPED_UNICODE: Keeps Unicode characters readable 514 620 */ 515 621 private function generate_widget_script($config) 516 622 { 623 // SECURITY: Sanitize all config values recursively before encoding 624 $config = $this->sanitize_config_recursive($config); 625 517 626 $json_config = []; 518 627 519 // Convert PHP array to JS object notation 628 // SECURITY FLAGS: Prevent XSS by encoding special HTML characters 629 $json_flags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE; 630 631 // Convert PHP array to JS object notation with secure encoding 520 632 foreach ($config as $key => $value) { 521 if (is_array($value)) { 522 $json_config[] = "$key: " . wp_json_encode($value, JSON_UNESCAPED_UNICODE); 523 } else { 524 $json_config[] = "$key: " . wp_json_encode($value, JSON_UNESCAPED_UNICODE); 525 } 633 // Sanitize key to only allow alphanumeric and underscore 634 $safe_key = preg_replace('/[^a-zA-Z0-9_]/', '', $key); 635 636 // Use wp_json_encode with security flags 637 $encoded_value = wp_json_encode($value, $json_flags); 638 639 // Skip if encoding failed (returns false on failure) 640 if ($encoded_value === false) { 641 continue; 642 } 643 644 $json_config[] = $safe_key . ": " . $encoded_value; 526 645 } 527 646 528 647 $config_string = implode(",\n ", $json_config); 648 649 // SECURITY: Escape plugin version for use in URL 650 $safe_version = esc_attr(MUCHAT_AI_CHATBOT_PLUGIN_VERSION); 529 651 530 652 // Create the script tag 531 653 $script = "\n<script type='module'>\n"; 532 $script .= " import Chatbox from 'https://cdn.mu.chat/embeds/dist/chatbox/index.js?v=" . MUCHAT_AI_CHATBOT_PLUGIN_VERSION. "';\n";654 $script .= " import Chatbox from 'https://cdn.mu.chat/embeds/dist/chatbox/index.js?v=" . $safe_version . "';\n"; 533 655 $script .= " const widget = await Chatbox.initBubble({\n"; 534 656 $script .= " " . $config_string . "\n"; … … 538 660 return apply_filters('muchat_widget_output', $script); 539 661 } 662 663 /** 664 * Recursively sanitize config values 665 * 666 * @param mixed $data Data to sanitize 667 * @return mixed Sanitized data 668 */ 669 private function sanitize_config_recursive($data) 670 { 671 if (is_array($data)) { 672 $sanitized = []; 673 foreach ($data as $key => $value) { 674 $safe_key = is_string($key) ? sanitize_text_field($key) : $key; 675 $sanitized[$safe_key] = $this->sanitize_config_recursive($value); 676 } 677 return $sanitized; 678 } elseif (is_string($data)) { 679 // Sanitize string values - remove any potential script injections 680 return sanitize_text_field($data); 681 } elseif (is_int($data) || is_float($data) || is_bool($data) || is_null($data)) { 682 return $data; 683 } 684 685 return ''; 686 } 540 687 } -
muchat-ai/trunk/includes/Models/Page.php
r3380443 r3406576 102 102 'items' => array_values(array_filter($pages)) 103 103 ]; 104 } 105 106 /** 107 * Get single page by ID 108 * 109 * @param int $page_id 110 * @return array|null 111 */ 112 public function get_page($page_id) 113 { 114 $page = get_post($page_id); 115 116 if (!$page || $page->post_type !== 'page' || $page->post_status !== 'publish') { 117 return null; 118 } 119 120 return $this->format_page($page); 104 121 } 105 122 -
muchat-ai/trunk/includes/Models/Post.php
r3380443 r3406576 97 97 98 98 /** 99 * Get single post by ID 100 * 101 * @param int $post_id 102 * @return array|null 103 */ 104 public function get_post($post_id) 105 { 106 $post = get_post($post_id); 107 108 if (!$post || $post->post_type !== 'post' || $post->post_status !== 'publish') { 109 return null; 110 } 111 112 return $this->format_post($post); 113 } 114 115 /** 99 116 * Check if a post is valid (has content) 100 117 * -
muchat-ai/trunk/includes/Models/Product.php
r3392161 r3406576 31 31 32 32 // Handle ordering - Use custom SQL approach for reliability 33 $order_by_field = isset($params['order_by']) ? $params['order_by']: 'modified';34 $order_direction = isset($params['order']) ? strtoupper( $params['order']) : 'ASC';33 $order_by_field = isset($params['order_by']) ? sanitize_key($params['order_by']) : 'modified'; 34 $order_direction = isset($params['order']) ? strtoupper(sanitize_key($params['order'])) : 'ASC'; 35 35 36 // Validate order direction 37 if (!in_array($order_direction, ['ASC', 'DESC'])) { 36 // SECURITY: Whitelist for order direction - only allow ASC or DESC 37 $allowed_directions = ['ASC', 'DESC']; 38 if (!in_array($order_direction, $allowed_directions, true)) { 38 39 $order_direction = 'ASC'; 39 40 } 40 41 41 // Map API field names to WP post fields 42 $field_map = [ 42 // SECURITY: Whitelist for allowed order fields - prevents SQL injection 43 // Only these exact field names are allowed 44 $allowed_fields = [ 43 45 'modified' => 'post_modified', 44 46 'date' => 'post_date', … … 47 49 ]; 48 50 49 $sql_field = isset($field_map[$order_by_field]) ? $field_map[$order_by_field] : 'post_modified'; 51 // Validate field is in whitelist, default to post_modified if not 52 if (!isset($allowed_fields[$order_by_field])) { 53 $order_by_field = 'modified'; 54 } 55 $sql_field = $allowed_fields[$order_by_field]; 50 56 51 57 // Use a custom orderby filter to ensure correct SQL 52 add_filter('posts_orderby', function($orderby, $query) use ($sql_field, $order_direction) { 58 // Both $sql_field and $order_direction are now guaranteed to be from whitelists 59 add_filter('posts_orderby', function($orderby, $query) use ($sql_field, $order_direction, $allowed_fields, $allowed_directions) { 53 60 // Only apply to our specific query 54 61 if (isset($query->query_vars['muchat_custom_order']) && $query->query_vars['muchat_custom_order'] === true) { 55 62 global $wpdb; 63 64 // Double-check security: verify values are still in whitelists 65 // This prevents any bypass even if values were modified in memory 66 if (!in_array($sql_field, $allowed_fields, true) || !in_array($order_direction, $allowed_directions, true)) { 67 return $orderby; // Return default if validation fails 68 } 69 70 // Safe to use - values are from strict whitelists 56 71 return "{$wpdb->posts}.{$sql_field} {$order_direction}, {$wpdb->posts}.ID ASC"; 57 72 } … … 767 782 /** 768 783 * Get all product meta fields with their information 784 * 785 * PERFORMANCE OPTIMIZATION: 786 * - Limited to 500 most recent products to avoid full table scan 787 * - Cached for 24 hours to reduce database load 769 788 * 770 789 * @return array … … 784 803 $added_fields = []; // Track added fields to prevent duplication 785 804 786 // 1. Get regular meta fields 805 // PERFORMANCE: Limit to 500 most recent products to avoid heavy table scan 806 // This provides a representative sample while keeping the query efficient 807 $product_limit = 500; 808 809 // 1. Get regular meta fields from recent products only 810 // Using subquery to limit products first, then join meta 787 811 $regular_fields = $wpdb->get_results($wpdb->prepare(" 788 812 SELECT pm.meta_key, … … 791 815 MAX(CASE WHEN p.post_type = 'product_variation' THEN 1 ELSE 0 END) as is_variation 792 816 FROM {$wpdb->postmeta} pm 793 JOIN {$wpdb->posts} p ON p.ID = pm.post_id 794 WHERE p.post_type IN ('product', 'product_variation') 795 AND pm.meta_key NOT LIKE %s 817 JOIN ( 818 SELECT ID, post_type 819 FROM {$wpdb->posts} 820 WHERE post_type IN ('product', 'product_variation') 821 AND post_status = 'publish' 822 ORDER BY post_modified DESC 823 LIMIT %d 824 ) p ON p.ID = pm.post_id 825 WHERE pm.meta_key NOT LIKE %s 796 826 GROUP BY pm.meta_key 797 827 HAVING usage_count > 0 798 ", '\_%'));828 ", $product_limit, '\_%')); 799 829 800 830 // 2. Process regular fields … … 893 923 $all_fields = array_values($meta_fields); 894 924 895 // Store the result in cache for 6 hours 896 set_transient($cache_key, $all_fields, 6 * HOUR_IN_SECONDS); 925 // PERFORMANCE: Store the result in cache for 24 hours 926 // Since meta fields rarely change, longer cache is appropriate 927 set_transient($cache_key, $all_fields, 24 * HOUR_IN_SECONDS); 897 928 898 929 return $all_fields; -
muchat-ai/trunk/muchat-ai.php
r3392161 r3406576 5 5 * Plugin URI: https://mu.chat 6 6 * Description: Muchat, a powerful tool for customer support using artificial intelligence 7 * Version: 2.0.5 07 * Version: 2.0.51 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.5 0');29 define('MUCHAT_AI_CHATBOT_PLUGIN_VERSION', '2.0.51'); 30 30 // define('MUCHAT_AI_CHATBOT_CACHE_DURATION', HOUR_IN_SECONDS); 31 31 define('MUCHAT_AI_CHATBOT_PLUGIN_FILE', __FILE__); … … 111 111 }); 112 112 113 // Initialize plugin with unique function name 114 function muchat_ai_chatbot_run_plugin() 115 { 116 $plugin = new Muchat\Api\Core\Plugin(); 113 /** 114 * This function is hooked into 'plugins_loaded' to ensure all necessary WordPress 115 * functionality is available. 116 */ 117 function muchat_ai_chatbot_run() { 118 $plugin = new \Muchat\Api\Core\Plugin(); 117 119 $plugin->run(); 118 120 } 119 121 120 // Initialize all plugin components on plugins_loaded hook for reliability 121 function muchat_ai_chatbot_initialize() 122 // Hook the run function to the 'plugins_loaded' action 123 add_action('plugins_loaded', 'muchat_ai_chatbot_run'); 124 125 // Initialize product change tracker for WooCommerce on plugins_loaded hook 126 function muchat_ai_chatbot_initialize_woocommerce() 122 127 { 123 // Run the main plugin124 muchat_ai_chatbot_run_plugin();125 126 128 // Initialize product change tracker for WooCommerce 127 129 if (class_exists('WooCommerce')) { … … 129 131 } 130 132 } 131 add_action('plugins_loaded', 'muchat_ai_chatbot_initialize ');133 add_action('plugins_loaded', 'muchat_ai_chatbot_initialize_woocommerce'); 132 134 133 135 // Hook for plugin activation -
muchat-ai/trunk/readme.txt
r3392161 r3406576 5 5 Tested up to: 6.8 6 6 Requires PHP: 7.3 7 Stable tag: 2.0.5 07 Stable tag: 2.0.51 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 76 76 == Changelog == 77 77 78 = 2.0.51 = 79 * Feat: New Custom Post Types API endpoint (`/custom-post-types/{post_type}`) - public access, no authentication required. 80 * Feat: Full UTF-8/Persian/Arabic URL support in Display Rules for RTL websites. 81 * Feat: Added API content search and preview functionality in admin panel. 82 * Feat: Added single post/page retrieval methods for API preview. 83 * Security: Enhanced SQL injection prevention with strict whitelist approach for order parameters. 84 * Fix: Improved API error isolation to prevent other plugin conflicts from affecting API responses. 85 * Docs: Added comprehensive Custom Post Types documentation to API Documentation page. 86 78 87 = 2.0.50 = 79 88 * Feat: Add `muchat_date_modified` field to products for precise tracking of price/stock changes. -
muchat-ai/trunk/templates/admin/api-documentation.php
r3300525 r3406576 43 43 </div> 44 44 <div class="inside"> 45 <p><?php echo esc_html__('All API requests require authentication. Use the Authorization header with a Bearer token.', 'muchat-ai'); ?></p> 46 <p class="description"><?php echo esc_html__('Example headers:', 'muchat-ai'); ?></p> 45 <p><?php echo esc_html__('Most API requests require authentication. Use the Authorization header with a Bearer token.', 'muchat-ai'); ?></p> 46 <p><strong><?php echo esc_html__('Note:', 'muchat-ai'); ?></strong> <?php echo esc_html__('The Custom Post Types endpoint (/custom-post-types/{post_type}) is public and does not require an authentication token.', 'muchat-ai'); ?></p> 47 <p class="description"><?php echo esc_html__('Example headers for protected endpoints:', 'muchat-ai'); ?></p> 47 48 <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace;">Authorization: Bearer YOUR_API_TOKEN 48 49 Content-Type: application/json</pre> … … 94 95 ], 95 96 [ 97 'path' => '/custom-post-types/{post_type}', 98 'description' => __('Retrieve all items from a custom post type with pagination', 'muchat-ai'), 99 'url' => rest_url('muchat-api/v1/custom-post-types/{post_type}') 100 ], 101 [ 96 102 'path' => '/orders/track', 97 103 'description' => __('Track an order by order ID and email', 'muchat-ai'), … … 104 110 <tr> 105 111 <td><code style="word-break: keep-all;"><?php echo esc_html($endpoint['path']); ?></code></td> 106 <td><span class="badge" style="background-color: #00 a32a; color: white; padding: 3px 8px; border-radius: 3px; display: inline-block;">POST</span></td>112 <td><span class="badge" style="background-color: #0073aa; color: white; padding: 3px 8px; border-radius: 3px; display: inline-block;">GET</span></td> 107 113 <td><?php echo esc_html($endpoint['description']); ?></td> 108 114 <td> … … 423 429 </div> 424 430 431 <!-- Custom Post Types Section --> 432 <div id="custom-post-types" class="postbox"> 433 <div class="postbox-header"> 434 <h2 class="hndle"><?php echo esc_html__('Custom Post Types', 'muchat-ai'); ?></h2> 435 </div> 436 <div class="inside"> 437 <p><?php echo esc_html__('The API supports retrieving data from custom post types. You can access any public custom post type using the following endpoint format:', 'muchat-ai'); ?></p> 438 439 <div class="wp-clearfix" style="margin: 15px 0;"> 440 <code style="display: inline-block; margin-right: 10px; padding: 8px; background: #f0f0f1; border-radius: 3px;"><?php echo esc_html(rest_url('muchat-api/v1/custom-post-types/{post_type}')); ?></code> 441 <button type="button" class="button copy-button" data-clipboard-text="<?php echo esc_url(rest_url('muchat-api/v1/custom-post-types/{post_type}')); ?>"> 442 <?php echo esc_html__('Copy', 'muchat-ai'); ?> 443 </button> 444 </div> 445 446 <h3><?php echo esc_html__('How to Use', 'muchat-ai'); ?></h3> 447 <p><?php echo esc_html__('Replace {post_type} with the name of your custom post type. For example, if you have a custom post type called "portfolio", you would use:', 'muchat-ai'); ?></p> 448 <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;"><?php echo esc_html(rest_url('muchat-api/v1/custom-post-types/portfolio')); ?></pre> 449 450 <h3><?php echo esc_html__('Example Request', 'muchat-ai'); ?></h3> 451 <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">curl -X GET \ 452 '<?php echo esc_html(rest_url('muchat-api/v1/custom-post-types/portfolio')); ?>?take=10&order_by=title&order=ASC'</pre> 453 454 <h3><?php echo esc_html__('Example Response', 'muchat-ai'); ?></h3> 455 <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">{ 456 "plugin_version": "1.0.0", 457 "post_type": "portfolio", 458 "offset": 0, 459 "limit": 30, 460 "total_count": 50, 461 "has_more": true, 462 "items": [ 463 { 464 "id": 123, 465 "title": "Portfolio Item Title", 466 "content": "Portfolio item content...", 467 "excerpt": "Portfolio item excerpt...", 468 "date_modified": "2023-01-15 14:30:45", 469 "url": "https://example.com/portfolio/item-slug", 470 "featured_image": "https://example.com/wp-content/uploads/image.jpg" 471 } 472 // More items... 473 ] 474 }</pre> 475 476 <h3><?php echo esc_html__('Supported Post Types', 'muchat-ai'); ?></h3> 477 <p><?php echo esc_html__('The API automatically detects and supports all public custom post types registered in WordPress. The following post types are excluded (they have their own dedicated endpoints):', 'muchat-ai'); ?></p> 478 <ul style="margin-left: 20px;"> 479 <li><code>post</code> - <?php echo esc_html__('Use /posts endpoint', 'muchat-ai'); ?></li> 480 <li><code>page</code> - <?php echo esc_html__('Use /pages endpoint', 'muchat-ai'); ?></li> 481 <li><code>product</code> - <?php echo esc_html__('Use /products endpoint', 'muchat-ai'); ?></li> 482 <li><code>attachment</code>, <code>revision</code>, <code>nav_menu_item</code> - <?php echo esc_html__('Not supported', 'muchat-ai'); ?></li> 483 </ul> 484 485 <h3><?php echo esc_html__('Custom Fields and Taxonomies', 'muchat-ai'); ?></h3> 486 <p><?php echo esc_html__('Custom post type items can include custom meta fields and taxonomy terms. These are automatically included in the response if they are configured in the plugin settings.', 'muchat-ai'); ?></p> 487 488 <h3><?php echo esc_html__('Available Parameters', 'muchat-ai'); ?></h3> 489 <p><?php echo esc_html__('The custom post types endpoint supports the same parameters as other endpoints:', 'muchat-ai'); ?></p> 490 <ul style="margin-left: 20px;"> 491 <li><code>skip</code> - <?php echo esc_html__('Number of items to skip (default: 0)', 'muchat-ai'); ?></li> 492 <li><code>take</code> - <?php echo esc_html__('Number of items to retrieve (default: 30, max: 100)', 'muchat-ai'); ?></li> 493 <li><code>order_by</code> - <?php echo esc_html__('Field to sort by (default: modified,ID)', 'muchat-ai'); ?></li> 494 <li><code>order</code> - <?php echo esc_html__('Sort order: ASC or DESC (default: ASC)', 'muchat-ai'); ?></li> 495 <li><code>modified_after</code> - <?php echo esc_html__('Filter items modified after this date (format: YYYY-MM-DD HH:MM:SS)', 'muchat-ai'); ?></li> 496 </ul> 497 </div> 498 </div> 499 425 500 <!-- Error Handling Section --> 426 501 <div id="error-handling" class="postbox"> -
muchat-ai/trunk/templates/admin/widget-settings.php
r3373032 r3406576 277 277 <th scope="row"><?php esc_html_e('Page List', 'muchat-ai'); ?></th> 278 278 <td> 279 <textarea name="muchat_ai_chatbot_visibility_pages" rows="6" cols="50" class="large-text code" ><?php279 <textarea name="muchat_ai_chatbot_visibility_pages" rows="6" cols="50" class="large-text code" dir="auto"><?php 280 280 $visibility_pages = get_option('muchat_ai_chatbot_visibility_pages', ''); 281 281 echo esc_textarea(is_array($visibility_pages) ? '' : htmlspecialchars_decode($visibility_pages)); … … 285 285 </p> 286 286 <p class="description"> 287 <?php esc_html_e('Examples:', 'muchat-ai'); ?> <code>/about</code>, <code>/blog/*</code> 288 </p> 289 </td> 290 </tr> 291 </table> 287 <?php esc_html_e('English examples:', 'muchat-ai'); ?> <code>/about</code>, <code>/blog/*</code>, <code>/product/shoes</code> 288 </p> 289 <p class="description"> 290 <?php esc_html_e('Persian/RTL examples:', 'muchat-ai'); ?> <code>/محصول/کفش</code>, <code>/دسته-بندی/*</code>, <code>/تماس-با-ما</code> 291 </p> 292 <p class="description"> 293 <small><?php esc_html_e('URLs are case-insensitive (/About = /about). Persian/Arabic URLs are fully supported.', 'muchat-ai'); ?></small> 294 </p> 295 </td> 296 </tr> 297 </table> 298 299 <!-- Cache Notice --> 300 <div class="notice notice-info inline" style="margin: 15px 0 0; padding: 10px 15px;"> 301 <p style="margin: 0;"> 302 <span class="dashicons dashicons-info" style="margin-right: 5px;"></span> 303 <?php esc_html_e('If you use a caching plugin and display rules don\'t work correctly, please clear your site cache.', 'muchat-ai'); ?> 304 </p> 305 </div> 292 306 </div> 293 307 </div>
Note: See TracChangeset
for help on using the changeset viewer.