PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.5.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.5.0
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Shortcodes / SearchModal.php

SearchModal.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.5.0, at includes/Shortcodes/SearchModal.php

360 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\Shortcodes;
4
5 use WPDeveloper\BetterDocs\Core\Query;
6 use WPDeveloper\BetterDocs\Utils\Helper;
7 use WPDeveloper\BetterDocs\Core\Settings;
8 use WPDeveloper\BetterDocs\Core\Shortcode;
9 use WPDeveloper\BetterDocs\Admin\Customizer\Defaults;
10
11 class SearchModal extends Shortcode {
12 public function __construct( Settings $settings, Query $query, Helper $helper, Defaults $defaults ) {
13 parent::__construct( $settings, $query, $helper, $defaults );
14
15 add_action( 'wp_ajax_nopriv_betterdocs_get_search_result', [ $this, 'get_search_results' ] );
16 add_action( 'wp_ajax_betterdocs_get_search_result', [ $this, 'get_search_results' ] );
17 }
18
19 public function get_style_depends() {
20 return [ 'betterdocs-search-modal' ];
21 }
22
23 public function get_script_depends() {
24 return [ 'betterdocs-search-modal' ];
25 }
26
27 /**
28 * Modify search query to properly handle non-English characters
29 *
30 * @param string $search The search SQL for WHERE clause
31 * @param WP_Query $query The WP_Query instance
32 * @return string Modified search SQL
33 */
34 public function improve_search_for_non_english( $search, $query ) {
35 global $wpdb;
36
37 // Only modify our BetterDocs search queries
38 if ( ! isset( $query->query_vars['post_type'] ) || $query->query_vars['post_type'] !== 'docs' ) {
39 return $search;
40 }
41
42 // Only modify if there's a search term
43 if ( empty( $query->query_vars['s'] ) ) {
44 return $search;
45 }
46
47 $search_term = $query->query_vars['s'];
48
49 // If the search term contains non-ASCII characters, we need to ensure proper UTF-8 handling
50 if ( preg_match('/[^\x00-\x7F]/', $search_term) ) {
51 // Get the search term with proper escaping
52 $like = '%' . $wpdb->esc_like( $search_term ) . '%';
53
54 // Build a UTF-8 compatible search query
55 // Search in post_title, post_content, and post_excerpt
56 // Note: Removed COLLATE clause to avoid collation mismatch with TranslatePress tables
57 $search = $wpdb->prepare(
58 " AND (
59 ({$wpdb->posts}.post_title LIKE %s)
60 OR ({$wpdb->posts}.post_content LIKE %s)
61 OR ({$wpdb->posts}.post_excerpt LIKE %s)",
62 $like,
63 $like,
64 $like
65 );
66
67 // If TranslatePress is active, also search in the translation dictionary
68 if ( class_exists( '\TRP_Translate_Press' ) ) {
69 // Get language codes
70 $lang_codes = $this->get_trp_language_code();
71 $default_lang = $lang_codes['default_language'];
72 $current_lang = $lang_codes['current_language'];
73
74 // Only search in translation table if current language is different from default
75 if ( $default_lang !== $current_lang ) {
76 // TranslatePress table naming: wp_trp_dictionary_{default_lang}_{current_lang}
77 $trp_table = $wpdb->prefix . 'trp_dictionary_' . $default_lang . '_' . $current_lang;
78
79 if ( $this->table_exists( $trp_table ) ) {
80 $search .= $wpdb->prepare(
81 " OR EXISTS (
82 SELECT 1 FROM {$trp_table} trp
83 WHERE (trp.original LIKE %s OR trp.translated LIKE %s)
84 AND trp.status != 2
85 AND (
86 {$wpdb->posts}.post_title COLLATE utf8mb4_unicode_ci = trp.original COLLATE utf8mb4_unicode_ci
87 OR {$wpdb->posts}.post_content COLLATE utf8mb4_unicode_ci LIKE CONCAT('%%', trp.original COLLATE utf8mb4_unicode_ci, '%%')
88 OR {$wpdb->posts}.post_excerpt COLLATE utf8mb4_unicode_ci = trp.original COLLATE utf8mb4_unicode_ci
89 )
90 )",
91 $like,
92 $like
93 );
94 }
95 }
96 }
97 $search .= " ) ";
98 }
99
100 return $search;
101 }
102
103 /**
104 * Get TranslatePress language codes (default and current) for table name construction
105 *
106 * @return array Array with 'default_language' and 'current_language' keys
107 */
108 private function get_trp_language_code() {
109 $result = [
110 'default_language' => 'en_US',
111 'current_language' => 'en_US'
112 ];
113
114 if ( class_exists( '\TRP_Translate_Press' ) ) {
115 $trp = \TRP_Translate_Press::get_trp_instance();
116 if ( isset( $trp ) && method_exists( $trp, 'get_component' ) ) {
117 $trp_settings = $trp->get_component( 'settings' );
118
119 // Get default language from settings
120 if ( $trp_settings ) {
121 $settings = $trp_settings->get_settings();
122 if ( isset( $settings['default-language'] ) ) {
123 $result['default_language'] = strtolower( $settings['default-language'] );
124 }
125 }
126
127 // Get current language from global variable
128 global $TRP_LANGUAGE;
129 if ( isset( $TRP_LANGUAGE ) && ! empty( $TRP_LANGUAGE ) ) {
130 $result['current_language'] = strtolower( $TRP_LANGUAGE );
131 }
132 }
133 }
134
135 return $result;
136 }
137
138 /**
139 * Check if a database table exists
140 */
141 private function table_exists( $table_name ) {
142 global $wpdb;
143 $result = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) );
144 return $result === $table_name;
145 }
146
147 public function get_search_results() {
148 global $wpdb;
149 $search_input = isset( $_POST['search_input'] ) ? sanitize_text_field( $_POST['search_input'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
150 $search_cat = isset( $_POST['search_cat'] ) ? wp_strip_all_tags( $_POST['search_cat'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
151 $lang = isset( $_POST['lang'] ) ? wp_strip_all_tags( $_POST['lang'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
152 // Removed preg_replace that was stripping non-Latin characters - sanitize_text_field() already handles security
153
154 $tax_query = [];
155 if ( $search_cat ) {
156 $tax_query = [
157 [
158 'taxonomy' => 'doc_category',
159 'field' => 'slug',
160 'terms' => $search_cat,
161 'operator' => 'AND',
162 'include_children' => true
163 ]
164 ];
165 }
166
167 $term = get_term_by( 'slug', $search_cat );
168
169 $post_status = ['publish'];
170
171 if( current_user_can( 'read_private_docs' ) ) {
172 array_push($post_status, 'private');
173 }
174
175 $args = [
176 'term_id' => isset( $term->term_id ) ? $term->term_id : 0,
177 'post_type' => 'docs',
178 'post_status' => $post_status,
179 'posts_per_page' => -1,
180 'suppress_filters' => false, // Changed to false to allow posts_search filter
181 's' => $search_input,
182 'orderby' => 'relevance',
183 'tax_query' => $tax_query
184 ];
185
186 // Handle WPML multilingual search
187 if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
188 // If search term contains non-ASCII characters (e.g., Chinese, Japanese, Bangla),
189 // search across all languages to find translated posts
190 if ( preg_match('/[^\x00-\x7F]/', $search_input) ) {
191 // Non-ASCII search: bypass WPML language filtering but allow posts_search filter
192 // This allows searching across all languages
193 $args['suppress_filters'] = true;
194 } else {
195 // ASCII-only search (English), use WPML filters to restrict to current language
196 $args['suppress_filters'] = false;
197 $args['lang'] = ICL_LANGUAGE_CODE;
198 }
199 }
200 // Handle TranslatePress - always allow posts_search filter for non-ASCII
201 elseif ( class_exists( '\TRP_Translate_Press' ) && preg_match('/[^\x00-\x7F]/', $search_input) ) {
202 // For TranslatePress, we need posts_search filter to run
203 $args['suppress_filters'] = false;
204 }
205
206 // Add filter to improve search for non-English characters
207 add_filter( 'posts_search', [ $this, 'improve_search_for_non_english' ], 10, 2 );
208
209 $search_results = $this->query->get_posts( $args );
210
211 // Remove filter after query to avoid affecting other queries
212 remove_filter( 'posts_search', [ $this, 'improve_search_for_non_english' ], 10 );
213
214 $response = [];
215
216 ob_start();
217 betterdocs()->views->get(
218 'shortcode-parts/search-results',
219 [
220 'search_results' => $search_results,
221 'search_input' => $search_input
222 ]
223 );
224
225 $_output = ob_get_clean();
226
227 $_input_not_found = '';
228 if ( ! $search_results->have_posts() ) {
229 $_input_not_found = $search_input;
230 }
231
232 $response['post_lists'] = $_output;
233
234 if ( $_output && strlen( $search_input ) >= 3 ) {
235 betterdocs()->query->insert_search_keyword( $search_input, $_input_not_found );
236 }
237
238 wp_reset_postdata();
239
240 wp_send_json_success( $response );
241 }
242
243 public function get_name() {
244 return 'betterdocs_search_modal';
245 }
246
247 /**
248 * Summary of default_attributes
249 * @return array
250 */
251 public function default_attributes() {
252 return apply_filters(
253 'betterdocs_search_modal_default_attr',
254 [
255 'placeholder' => __( 'Search Doc', 'betterdocs' ),
256 'heading' => '',
257 'subheading' => '',
258 'heading_tag' => 'h1',
259 'subheading_tag' => 'p',
260 'number_of_docs' => '5',
261 'number_of_faqs' => '5',
262 'search_button_text' => __( 'Search', 'betterdocs' ),
263 'faq_categories_ids' => '',
264 'layout' => 'layout-1',
265 'doc_ids' => '',
266 'doc_categories_ids' => '',
267 'enable_docs_search' => true,
268 'enable_faq_search' => true,
269 'enable_ai_powered_search' => false,
270 'kb_based_search' => '' // KB slug to filter search results
271 ]
272 );
273 }
274
275 public function render( $atts, $content = null ) {
276 betterdocs()->assets->localize(
277 'betterdocs-search-modal',
278 'searchModalConfig',
279 [
280 'nonce' => wp_create_nonce( 'wp_rest' )
281 ]
282 );
283
284 $defaults_attrs = $this->default_attributes();
285
286 if ( isset( $atts['layout'] ) && $atts['layout'] == 'layout-1' ) {
287 $attributes = [
288 'placeholder' => isset( $atts['placeholder'] ) ? $atts['placeholder'] : $defaults_attrs['placeholder'],
289 'heading' => isset( $atts['heading'] ) ? $atts['heading'] : $defaults_attrs['heading'],
290 'subheading' => isset( $atts['subheading'] ) ? $atts['subheading'] : $defaults_attrs['subheading'],
291 'headingtag' => isset( $atts['heading_tag'] ) ? $atts['heading_tag'] : $defaults_attrs['heading_tag'],
292 'subheadingtag' => isset( $atts['subheading_tag'] ) ? $atts['subheading_tag'] : $defaults_attrs['subheading_tag'],
293 'buttontext' => isset( $atts['search_button_text'] ) ? $atts['search_button_text'] : '',
294 'numberofdocs' => isset( $atts['number_of_docs'] ) ? $atts['number_of_docs'] : 5,
295 'numberoffaqs' => isset( $atts['number_of_faqs'] ) ? $atts['number_of_faqs'] : 5,
296 'faq_categories_ids' => isset( $atts['faq_categories_ids'] ) ? $atts['faq_categories_ids'] : '',
297 'doc_ids' => isset( $atts['doc_ids'] ) ? $atts['doc_ids'] : '',
298 'doc_categories_ids' => isset( $atts['doc_categories_ids'] ) ? $atts['doc_categories_ids'] : '',
299 'enable_faq_search' => isset( $atts['enable_faq_search'] ) ? $atts['enable_faq_search'] : $defaults_attrs['enable_faq_search'],
300 'enable_docs_search' => isset( $atts['enable_docs_search'] ) ? $atts['enable_docs_search'] : $defaults_attrs['enable_docs_search'],
301 'enable_ai_powered_search' => isset( $atts['enable_ai_powered_search'] ) ? $atts['enable_ai_powered_search'] : $defaults_attrs['enable_ai_powered_search'],
302 'kb_based_search' => isset( $atts['kb_based_search'] ) ? $atts['kb_based_search'] : $defaults_attrs['kb_based_search']
303 ];
304 $attributes = apply_filters( 'betterdocs_search_modal_shortcode_attributes', $attributes );
305 echo '<div class="betterdocs-search-modal-layout-1" id="betterdocs-search-modal"';
306 foreach ( $attributes as $key => $value ) {
307 if ( ! empty( $value ) ) {
308 echo ' data-' . esc_attr( $key ) . '="' . esc_attr( $value ) . '"';
309 }
310 }
311 echo '></div>';
312 } else if ( isset( $atts['layout'] ) && $atts['layout'] == 'docs-archive' ) {
313 $attributes = [
314 'placeholder' => isset( $atts['placeholder'] ) ? $atts['placeholder'] : '',
315 'buttontext' => isset( $atts['search_button_text'] ) ? $atts['search_button_text'] : '',
316 'numberofdocs' => isset( $atts['number_of_docs'] ) ? $atts['number_of_docs'] : 5,
317 'numberoffaqs' => isset( $atts['number_of_faqs'] ) ? $atts['number_of_faqs'] : 5,
318 'faq_categories_ids' => isset( $atts['faq_categories_ids'] ) ? $atts['faq_categories_ids'] : '',
319 'doc_ids' => isset( $atts['doc_ids'] ) ? $atts['doc_ids'] : '',
320 'doc_categories_ids' => isset( $atts['doc_categories_ids'] ) ? $atts['doc_categories_ids'] : '',
321 'enable_faq_search' => isset( $atts['enable_faq_search'] ) ? $atts['enable_faq_search'] : $defaults_attrs['enable_faq_search'],
322 'enable_docs_search' => isset( $atts['enable_docs_search'] ) ? $atts['enable_docs_search'] : $defaults_attrs['enable_docs_search'],
323 'enable_ai_powered_search' => isset( $atts['enable_ai_powered_search'] ) ? $atts['enable_ai_powered_search'] : $defaults_attrs['enable_ai_powered_search'],
324 'kb_based_search' => isset( $atts['kb_based_search'] ) ? $atts['kb_based_search'] : $defaults_attrs['kb_based_search']
325 ];
326 $attributes = apply_filters( 'betterdocs_search_modal_shortcode_attributes', $attributes );
327
328 echo '<div class="betterdocs-search-modal-archive" id="betterdocs-search-modal"';
329 foreach ( $attributes as $key => $value ) {
330 if ( ! empty( $value ) ) {
331 echo ' data-' . esc_attr( $key ) . '="' . esc_attr( $value ) . '"';
332 }
333 }
334 echo '></div>';
335 } elseif ( isset( $atts['layout'] ) && $atts['layout'] == 'sidebar' ) {
336 $attributes = [
337 'placeholder' => $atts['placeholder'],
338 'numberofdocs' => isset( $atts['number_of_docs'] ) ? $atts['number_of_docs'] : 5,
339 'numberoffaqs' => isset( $atts['number_of_faqs'] ) ? $atts['number_of_faqs'] : 5,
340 'faq_categories_ids' => isset( $atts['faq_categories_ids'] ) ? $atts['faq_categories_ids'] : '',
341 'doc_ids' => isset( $atts['doc_ids'] ) ? $atts['doc_ids'] : '',
342 'doc_categories_ids' => isset( $atts['doc_categories_ids'] ) ? $atts['doc_categories_ids'] : '',
343 'enable_faq_search' => isset( $atts['enable_faq_search'] ) ? $atts['enable_faq_search'] : $defaults_attrs['enable_faq_search'],
344 'enable_docs_search' => isset( $atts['enable_docs_search'] ) ? $atts['enable_docs_search'] : $defaults_attrs['enable_docs_search'],
345 'enable_ai_powered_search' => isset( $atts['enable_ai_powered_search'] ) ? $atts['enable_ai_powered_search'] : $defaults_attrs['enable_ai_powered_search'],
346 'kb_based_search' => isset( $atts['kb_based_search'] ) ? $atts['kb_based_search'] : $defaults_attrs['kb_based_search']
347 ];
348 $attributes = apply_filters( 'betterdocs_search_modal_shortcode_attributes', $attributes );
349
350 echo '<div class="betterdocs-search-modal-sidebar" id="betterdocs-search-modal"';
351 foreach ( $attributes as $key => $value ) {
352 if ( ! empty( $value ) ) {
353 echo ' data-' . esc_attr( $key ) . '="' . esc_attr( $value ) . '"';
354 }
355 }
356 echo '></div>';
357 }
358 }
359 }
360