PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.5.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.5.5
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.5, at includes/Shortcodes/SearchModal.php

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