Plugin Directory

Changeset 3406576


Ignore:
Timestamp:
12/01/2025 08:42:59 AM (4 months ago)
Author:
muchatai
Message:

Update to version 2.0.51 from GitHub

Location:
muchat-ai
Files:
4 added
22 edited
1 copied

Legend:

Unmodified
Added
Removed
  • muchat-ai/tags/2.0.51/includes/Admin/Settings.php

    r3373032 r3406576  
    1313
    1414    /**
     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    /**
    1525     * Initialize the class
    1626     */
     
    2030        add_action('admin_enqueue_scripts', [$this, 'enqueue_styles']);
    2131        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']);
    2236    }
    2337
    2438    /**
    2539     * 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
    2645     */
    2746    private function muchat_ai_chatbot_get_menu_icon()
    2847    {
    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;
    3750    }
    3851
     
    786799
    787800    /**
     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    /**
    788853     * Register plugin settings
    789854     */
     
    856921        register_setting('muchat-settings-group', 'muchat_ai_chatbot_visibility_pages', array(
    857922            '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'),
    866924            'default' => ''
    867925        ));
     
    9491007        }
    9501008    }
     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    }
    9511267}
  • muchat-ai/tags/2.0.51/includes/Api/Middleware/AuthMiddleware.php

    r3330738 r3406576  
    1414    {
    1515        // Development mode - return true
    16         // return true;
     16        //return true;
    1717
    1818        // Production mode implementation:
  • muchat-ai/tags/2.0.51/includes/Api/Routes.php

    r3392161 r3406576  
    88use Muchat\Api\Api\Controllers\PageController;
    99use Muchat\Api\Api\Controllers\OrderController;
     10use Muchat\Api\Api\Controllers\CustomPostTypeController;
    1011use Muchat\Api\Api\Middleware\AuthMiddleware;
    1112
     
    4142     */
    4243    private $order_controller;
     44
     45    /**
     46     * @var CustomPostTypeController
     47     */
     48    private $custom_post_type_controller;
    4349
    4450    /**
     
    6571        $this->page_controller = $page_controller;
    6672        $this->order_controller = $order_controller;
     73        $this->custom_post_type_controller = new CustomPostTypeController();
    6774        $this->auth_middleware = new AuthMiddleware();
    6875    }
     
    121128                'callback' => [$this->page_controller, 'get_pages'],
    122129                '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,
    123136            ]
    124137        ];
     
    131144                    'methods' => $route['methods'],
    132145                    'callback' => $route['callback'],
    133                     'permission_callback' => [$this->auth_middleware, 'verify_token'],
     146                    'permission_callback' => !empty($route['public']) ? '__return_true' : [$this->auth_middleware, 'verify_token'],
    134147                    'args' => $this->get_collection_params(),
    135148                    // Add headers to prevent caching
  • muchat-ai/tags/2.0.51/includes/Frontend/Widget.php

    r3373032 r3406576  
    6969    private function register_cache_clearing_hooks()
    7070    {
     71        // Core widget settings
    7172        add_action('update_option_muchat_ai_chatbot_agent_id', [$this, 'clear_widget_cache']);
    7273        add_action('update_option_muchat_ai_chatbot_interface_initial_messages', [$this, 'clear_widget_cache']);
     
    7576        add_action('update_option_muchat_ai_chatbot_script_position', [$this, 'clear_widget_cache']);
    7677        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']);
    7788    }
    7889
     
    8192     */
    8293    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()
    83104    {
    84105        \Muchat\Api\Utils\Cache::clear_widget_cache();
     
    192213    /**
    193214     * 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.
    195216     *
    196217     * @param string $patterns_string A newline-separated string of URL patterns.
     
    203224        $request_uri = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '';
    204225        $path_only = strtok($request_uri, '?');
    205         $current_path = rtrim(urldecode($path_only), '/');
     226        $current_path = $this->normalize_path(urldecode($path_only));
    206227
    207228        // Treat an empty path (which can be the homepage) as '/'.
     
    231252
    232253            // A. Check for the special '<front>' tag.
    233             if ($decoded_pattern === '<front>') {
     254            if (strtolower($decoded_pattern) === '<front>') {
    234255                if ($is_front) {
    235256                    return true; // Match found.
     
    239260
    240261            // B. Normalize the pattern for comparison.
    241             $normalized_pattern = rtrim($decoded_pattern, '/');
     262            $normalized_pattern = $this->normalize_path($decoded_pattern);
    242263            if (empty($normalized_pattern)) {
    243264                $normalized_pattern = '/';
     
    248269                // Escape regex characters, then replace our wildcard `*` with `.*`.
    249270                // 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';
    251273                if (preg_match($regex, $current_path)) {
    252274                    return true; // Match found.
     
    255277            // D. Check for exact matches (only if no wildcard).
    256278            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)) {
    258281                    return true; // Match found.
    259282                }
     
    266289
    267290        // 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       
    268367        return false;
    269368    }
     
    512611    /**
    513612     * 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
    514620     */
    515621    private function generate_widget_script($config)
    516622    {
     623        // SECURITY: Sanitize all config values recursively before encoding
     624        $config = $this->sanitize_config_recursive($config);
     625       
    517626        $json_config = [];
    518627
    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
    520632        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;
    526645        }
    527646
    528647        $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);
    529651
    530652        // Create the script tag
    531653        $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";
    533655        $script .= "    const widget = await Chatbox.initBubble({\n";
    534656        $script .= "        " . $config_string . "\n";
     
    538660        return apply_filters('muchat_widget_output', $script);
    539661    }
     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    }
    540687}
  • muchat-ai/tags/2.0.51/includes/Models/Page.php

    r3380443 r3406576  
    102102            'items' => array_values(array_filter($pages))
    103103        ];
     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);
    104121    }
    105122
  • muchat-ai/tags/2.0.51/includes/Models/Post.php

    r3380443 r3406576  
    9797
    9898    /**
     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    /**
    99116     * Check if a post is valid (has content)
    100117     *
  • muchat-ai/tags/2.0.51/includes/Models/Product.php

    r3392161 r3406576  
    3131
    3232        // 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';
    3535       
    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)) {
    3839            $order_direction = 'ASC';
    3940        }
    4041       
    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 = [
    4345            'modified' => 'post_modified',
    4446            'date' => 'post_date',
     
    4749        ];
    4850       
    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];
    5056       
    5157        // 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) {
    5360            // Only apply to our specific query
    5461            if (isset($query->query_vars['muchat_custom_order']) && $query->query_vars['muchat_custom_order'] === true) {
    5562                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
    5671                return "{$wpdb->posts}.{$sql_field} {$order_direction}, {$wpdb->posts}.ID ASC";
    5772            }
     
    767782    /**
    768783     * 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
    769788     *
    770789     * @return array
     
    784803        $added_fields = []; // Track added fields to prevent duplication
    785804
    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
    787811        $regular_fields = $wpdb->get_results($wpdb->prepare("
    788812            SELECT pm.meta_key,
     
    791815                MAX(CASE WHEN p.post_type = 'product_variation' THEN 1 ELSE 0 END) as is_variation
    792816            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
    796826            GROUP BY pm.meta_key
    797827            HAVING usage_count > 0
    798         ", '\_%'));
     828        ", $product_limit, '\_%'));
    799829
    800830        // 2. Process regular fields
     
    893923        $all_fields = array_values($meta_fields);
    894924
    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);
    897928
    898929        return $all_fields;
  • muchat-ai/tags/2.0.51/muchat-ai.php

    r3392161 r3406576  
    55 * Plugin URI: https://mu.chat
    66 * Description: Muchat, a powerful tool for customer support using artificial intelligence
    7  * Version: 2.0.50
     7 * Version: 2.0.51
    88 * Author: Muchat
    99 * Text Domain: muchat-ai
     
    2727
    2828// Define plugin constants with unique prefix
    29 define('MUCHAT_AI_CHATBOT_PLUGIN_VERSION', '2.0.50');
     29define('MUCHAT_AI_CHATBOT_PLUGIN_VERSION', '2.0.51');
    3030// define('MUCHAT_AI_CHATBOT_CACHE_DURATION', HOUR_IN_SECONDS);
    3131define('MUCHAT_AI_CHATBOT_PLUGIN_FILE', __FILE__);
     
    111111});
    112112
    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 */
     117function muchat_ai_chatbot_run() {
     118    $plugin = new \Muchat\Api\Core\Plugin();
    117119    $plugin->run();
    118120}
    119121
    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
     123add_action('plugins_loaded', 'muchat_ai_chatbot_run');
     124
     125// Initialize product change tracker for WooCommerce on plugins_loaded hook
     126function muchat_ai_chatbot_initialize_woocommerce()
    122127{
    123     // Run the main plugin
    124     muchat_ai_chatbot_run_plugin();
    125 
    126128    // Initialize product change tracker for WooCommerce
    127129    if (class_exists('WooCommerce')) {
     
    129131    }
    130132}
    131 add_action('plugins_loaded', 'muchat_ai_chatbot_initialize');
     133add_action('plugins_loaded', 'muchat_ai_chatbot_initialize_woocommerce');
    132134
    133135// Hook for plugin activation
  • muchat-ai/tags/2.0.51/readme.txt

    r3392161 r3406576  
    55Tested up to: 6.8
    66Requires PHP: 7.3
    7 Stable tag: 2.0.50
     7Stable tag: 2.0.51
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    7676== Changelog ==
    7777
     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
    7887= 2.0.50 =
    7988* 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  
    4343            </div>
    4444            <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>
    4748                <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
    4849Content-Type: application/json</pre>
     
    9495                            ],
    9596                            [
     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                            [
    96102                                'path' => '/orders/track',
    97103                                'description' => __('Track an order by order ID and email', 'muchat-ai'),
     
    104110                            <tr>
    105111                                <td><code style="word-break: keep-all;"><?php echo esc_html($endpoint['path']); ?></code></td>
    106                                 <td><span class="badge" style="background-color: #00a32a; 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>
    107113                                <td><?php echo esc_html($endpoint['description']); ?></td>
    108114                                <td>
     
    423429        </div>
    424430
     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
    425500        <!-- Error Handling Section -->
    426501        <div id="error-handling" class="postbox">
  • muchat-ai/tags/2.0.51/templates/admin/widget-settings.php

    r3373032 r3406576  
    277277                                <th scope="row"><?php esc_html_e('Page List', 'muchat-ai'); ?></th>
    278278                                <td>
    279                                     <textarea name="muchat_ai_chatbot_visibility_pages" rows="6" cols="50" class="large-text code"><?php
     279                                    <textarea name="muchat_ai_chatbot_visibility_pages" rows="6" cols="50" class="large-text code" dir="auto"><?php
    280280                                                                                                                                    $visibility_pages = get_option('muchat_ai_chatbot_visibility_pages', '');
    281281                                                                                                                                    echo esc_textarea(is_array($visibility_pages) ? '' : htmlspecialchars_decode($visibility_pages));
     
    285285                                    </p>
    286286                                    <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>
    292306                    </div>
    293307                </div>
  • muchat-ai/trunk/includes/Admin/Settings.php

    r3373032 r3406576  
    1313
    1414    /**
     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    /**
    1525     * Initialize the class
    1626     */
     
    2030        add_action('admin_enqueue_scripts', [$this, 'enqueue_styles']);
    2131        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']);
    2236    }
    2337
    2438    /**
    2539     * 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
    2645     */
    2746    private function muchat_ai_chatbot_get_menu_icon()
    2847    {
    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;
    3750    }
    3851
     
    786799
    787800    /**
     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    /**
    788853     * Register plugin settings
    789854     */
     
    856921        register_setting('muchat-settings-group', 'muchat_ai_chatbot_visibility_pages', array(
    857922            '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'),
    866924            'default' => ''
    867925        ));
     
    9491007        }
    9501008    }
     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    }
    9511267}
  • muchat-ai/trunk/includes/Api/Middleware/AuthMiddleware.php

    r3330738 r3406576  
    1414    {
    1515        // Development mode - return true
    16         // return true;
     16        //return true;
    1717
    1818        // Production mode implementation:
  • muchat-ai/trunk/includes/Api/Routes.php

    r3392161 r3406576  
    88use Muchat\Api\Api\Controllers\PageController;
    99use Muchat\Api\Api\Controllers\OrderController;
     10use Muchat\Api\Api\Controllers\CustomPostTypeController;
    1011use Muchat\Api\Api\Middleware\AuthMiddleware;
    1112
     
    4142     */
    4243    private $order_controller;
     44
     45    /**
     46     * @var CustomPostTypeController
     47     */
     48    private $custom_post_type_controller;
    4349
    4450    /**
     
    6571        $this->page_controller = $page_controller;
    6672        $this->order_controller = $order_controller;
     73        $this->custom_post_type_controller = new CustomPostTypeController();
    6774        $this->auth_middleware = new AuthMiddleware();
    6875    }
     
    121128                'callback' => [$this->page_controller, 'get_pages'],
    122129                '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,
    123136            ]
    124137        ];
     
    131144                    'methods' => $route['methods'],
    132145                    'callback' => $route['callback'],
    133                     'permission_callback' => [$this->auth_middleware, 'verify_token'],
     146                    'permission_callback' => !empty($route['public']) ? '__return_true' : [$this->auth_middleware, 'verify_token'],
    134147                    'args' => $this->get_collection_params(),
    135148                    // Add headers to prevent caching
  • muchat-ai/trunk/includes/Frontend/Widget.php

    r3373032 r3406576  
    6969    private function register_cache_clearing_hooks()
    7070    {
     71        // Core widget settings
    7172        add_action('update_option_muchat_ai_chatbot_agent_id', [$this, 'clear_widget_cache']);
    7273        add_action('update_option_muchat_ai_chatbot_interface_initial_messages', [$this, 'clear_widget_cache']);
     
    7576        add_action('update_option_muchat_ai_chatbot_script_position', [$this, 'clear_widget_cache']);
    7677        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']);
    7788    }
    7889
     
    8192     */
    8293    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()
    83104    {
    84105        \Muchat\Api\Utils\Cache::clear_widget_cache();
     
    192213    /**
    193214     * 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.
    195216     *
    196217     * @param string $patterns_string A newline-separated string of URL patterns.
     
    203224        $request_uri = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '';
    204225        $path_only = strtok($request_uri, '?');
    205         $current_path = rtrim(urldecode($path_only), '/');
     226        $current_path = $this->normalize_path(urldecode($path_only));
    206227
    207228        // Treat an empty path (which can be the homepage) as '/'.
     
    231252
    232253            // A. Check for the special '<front>' tag.
    233             if ($decoded_pattern === '<front>') {
     254            if (strtolower($decoded_pattern) === '<front>') {
    234255                if ($is_front) {
    235256                    return true; // Match found.
     
    239260
    240261            // B. Normalize the pattern for comparison.
    241             $normalized_pattern = rtrim($decoded_pattern, '/');
     262            $normalized_pattern = $this->normalize_path($decoded_pattern);
    242263            if (empty($normalized_pattern)) {
    243264                $normalized_pattern = '/';
     
    248269                // Escape regex characters, then replace our wildcard `*` with `.*`.
    249270                // 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';
    251273                if (preg_match($regex, $current_path)) {
    252274                    return true; // Match found.
     
    255277            // D. Check for exact matches (only if no wildcard).
    256278            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)) {
    258281                    return true; // Match found.
    259282                }
     
    266289
    267290        // 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       
    268367        return false;
    269368    }
     
    512611    /**
    513612     * 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
    514620     */
    515621    private function generate_widget_script($config)
    516622    {
     623        // SECURITY: Sanitize all config values recursively before encoding
     624        $config = $this->sanitize_config_recursive($config);
     625       
    517626        $json_config = [];
    518627
    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
    520632        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;
    526645        }
    527646
    528647        $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);
    529651
    530652        // Create the script tag
    531653        $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";
    533655        $script .= "    const widget = await Chatbox.initBubble({\n";
    534656        $script .= "        " . $config_string . "\n";
     
    538660        return apply_filters('muchat_widget_output', $script);
    539661    }
     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    }
    540687}
  • muchat-ai/trunk/includes/Models/Page.php

    r3380443 r3406576  
    102102            'items' => array_values(array_filter($pages))
    103103        ];
     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);
    104121    }
    105122
  • muchat-ai/trunk/includes/Models/Post.php

    r3380443 r3406576  
    9797
    9898    /**
     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    /**
    99116     * Check if a post is valid (has content)
    100117     *
  • muchat-ai/trunk/includes/Models/Product.php

    r3392161 r3406576  
    3131
    3232        // 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';
    3535       
    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)) {
    3839            $order_direction = 'ASC';
    3940        }
    4041       
    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 = [
    4345            'modified' => 'post_modified',
    4446            'date' => 'post_date',
     
    4749        ];
    4850       
    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];
    5056       
    5157        // 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) {
    5360            // Only apply to our specific query
    5461            if (isset($query->query_vars['muchat_custom_order']) && $query->query_vars['muchat_custom_order'] === true) {
    5562                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
    5671                return "{$wpdb->posts}.{$sql_field} {$order_direction}, {$wpdb->posts}.ID ASC";
    5772            }
     
    767782    /**
    768783     * 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
    769788     *
    770789     * @return array
     
    784803        $added_fields = []; // Track added fields to prevent duplication
    785804
    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
    787811        $regular_fields = $wpdb->get_results($wpdb->prepare("
    788812            SELECT pm.meta_key,
     
    791815                MAX(CASE WHEN p.post_type = 'product_variation' THEN 1 ELSE 0 END) as is_variation
    792816            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
    796826            GROUP BY pm.meta_key
    797827            HAVING usage_count > 0
    798         ", '\_%'));
     828        ", $product_limit, '\_%'));
    799829
    800830        // 2. Process regular fields
     
    893923        $all_fields = array_values($meta_fields);
    894924
    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);
    897928
    898929        return $all_fields;
  • muchat-ai/trunk/muchat-ai.php

    r3392161 r3406576  
    55 * Plugin URI: https://mu.chat
    66 * Description: Muchat, a powerful tool for customer support using artificial intelligence
    7  * Version: 2.0.50
     7 * Version: 2.0.51
    88 * Author: Muchat
    99 * Text Domain: muchat-ai
     
    2727
    2828// Define plugin constants with unique prefix
    29 define('MUCHAT_AI_CHATBOT_PLUGIN_VERSION', '2.0.50');
     29define('MUCHAT_AI_CHATBOT_PLUGIN_VERSION', '2.0.51');
    3030// define('MUCHAT_AI_CHATBOT_CACHE_DURATION', HOUR_IN_SECONDS);
    3131define('MUCHAT_AI_CHATBOT_PLUGIN_FILE', __FILE__);
     
    111111});
    112112
    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 */
     117function muchat_ai_chatbot_run() {
     118    $plugin = new \Muchat\Api\Core\Plugin();
    117119    $plugin->run();
    118120}
    119121
    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
     123add_action('plugins_loaded', 'muchat_ai_chatbot_run');
     124
     125// Initialize product change tracker for WooCommerce on plugins_loaded hook
     126function muchat_ai_chatbot_initialize_woocommerce()
    122127{
    123     // Run the main plugin
    124     muchat_ai_chatbot_run_plugin();
    125 
    126128    // Initialize product change tracker for WooCommerce
    127129    if (class_exists('WooCommerce')) {
     
    129131    }
    130132}
    131 add_action('plugins_loaded', 'muchat_ai_chatbot_initialize');
     133add_action('plugins_loaded', 'muchat_ai_chatbot_initialize_woocommerce');
    132134
    133135// Hook for plugin activation
  • muchat-ai/trunk/readme.txt

    r3392161 r3406576  
    55Tested up to: 6.8
    66Requires PHP: 7.3
    7 Stable tag: 2.0.50
     7Stable tag: 2.0.51
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    7676== Changelog ==
    7777
     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
    7887= 2.0.50 =
    7988* 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  
    4343            </div>
    4444            <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>
    4748                <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
    4849Content-Type: application/json</pre>
     
    9495                            ],
    9596                            [
     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                            [
    96102                                'path' => '/orders/track',
    97103                                'description' => __('Track an order by order ID and email', 'muchat-ai'),
     
    104110                            <tr>
    105111                                <td><code style="word-break: keep-all;"><?php echo esc_html($endpoint['path']); ?></code></td>
    106                                 <td><span class="badge" style="background-color: #00a32a; 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>
    107113                                <td><?php echo esc_html($endpoint['description']); ?></td>
    108114                                <td>
     
    423429        </div>
    424430
     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
    425500        <!-- Error Handling Section -->
    426501        <div id="error-handling" class="postbox">
  • muchat-ai/trunk/templates/admin/widget-settings.php

    r3373032 r3406576  
    277277                                <th scope="row"><?php esc_html_e('Page List', 'muchat-ai'); ?></th>
    278278                                <td>
    279                                     <textarea name="muchat_ai_chatbot_visibility_pages" rows="6" cols="50" class="large-text code"><?php
     279                                    <textarea name="muchat_ai_chatbot_visibility_pages" rows="6" cols="50" class="large-text code" dir="auto"><?php
    280280                                                                                                                                    $visibility_pages = get_option('muchat_ai_chatbot_visibility_pages', '');
    281281                                                                                                                                    echo esc_textarea(is_array($visibility_pages) ? '' : htmlspecialchars_decode($visibility_pages));
     
    285285                                    </p>
    286286                                    <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>
    292306                    </div>
    293307                </div>
Note: See TracChangeset for help on using the changeset viewer.