PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
4.9.2 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 All 200 releases
← All changes | includes/Shortcodes/SearchForm.php +274 -175 3.3.44.9.2 View file →
@@ -1,8 +1,12 @@
1 1 <?php
2 +namespace WPDeveloper\BetterDocs\Shortcodes;
2 3
3 -namespace WPDeveloper\BetterDocs\Shortcodes;
4 +if ( ! defined( 'ABSPATH' ) ) {
5 + exit;
6 +}
4 7
8 +
5 9 use WPDeveloper\BetterDocs\Core\Query;
6 10 use WPDeveloper\BetterDocs\Utils\Helper;
7 11 use WPDeveloper\BetterDocs\Core\Settings;
8 12 use WPDeveloper\BetterDocs\Core\Shortcode;
@@ -8,203 +12,298 @@
8 12 use WPDeveloper\BetterDocs\Core\Shortcode;
9 13 use WPDeveloper\BetterDocs\Admin\Customizer\Defaults;
10 14
11 15 class SearchForm extends Shortcode {
12 - public function __construct( Settings $settings, Query $query, Helper $helper, Defaults $defaults ) {
13 - parent::__construct( $settings, $query, $helper, $defaults );
16 + public function __construct( Settings $settings, Query $query, Helper $helper, Defaults $defaults ) {
17 + parent::__construct( $settings, $query, $helper, $defaults );
14 18
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 - }
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 + }
18 22
19 - public function get_style_depends() {
20 - return ['betterdocs-search'];
21 - }
23 + /**
24 + * Modify search query to properly handle non-English characters
25 + *
26 + * @param string $search The search SQL for WHERE clause
27 + * @param WP_Query $query The WP_Query instance
28 + * @return string Modified search SQL
29 + */
30 + public function improve_search_for_non_english( $search, $query ) {
31 + global $wpdb;
22 32
23 - public function get_script_depends() {
24 - return ['betterdocs-search'];
25 - }
33 + // Only modify our BetterDocs search queries
34 + if ( ! isset( $query->query_vars['post_type'] ) || $query->query_vars['post_type'] !== 'docs' ) {
35 + return $search;
36 + }
26 37
27 - public function get_search_results() {
28 - global $wpdb;
29 - $search_input = isset( $_POST['search_input'] ) ? sanitize_text_field( $_POST['search_input'] ) : '';
30 - $search_cat = isset( $_POST['search_cat'] ) ? wp_strip_all_tags( $_POST['search_cat'] ) : '';
31 - $lang = isset( $_POST['lang'] ) ? wp_strip_all_tags( $_POST['lang'] ) : '';
32 - $search_input = preg_replace( '/[^A-Za-z0-9_\- ][]]/', '', strtolower( $search_input ) );
38 + // Only modify if there's a search term
39 + if ( empty( $query->query_vars['s'] ) ) {
40 + return $search;
41 + }
33 42
34 - $tax_query = [];
35 - if ( $search_cat ) {
36 - $tax_query = [
37 - [
38 - 'taxonomy' => 'doc_category',
39 - 'field' => 'slug',
40 - 'terms' => $search_cat,
41 - 'operator' => 'AND',
42 - 'include_children' => true
43 - ]
44 - ];
45 - }
43 + $search_term = $query->query_vars['s'];
46 44
47 - $term = get_term_by( 'slug', $search_cat );
45 + // If the search term contains non-ASCII characters, we need to ensure proper UTF-8 handling
46 + if ( preg_match('/[^\x00-\x7F]/', $search_term) ) {
47 + // Get the search term with proper escaping
48 + $like = '%' . $wpdb->esc_like( $search_term ) . '%';
48 49
49 - $args = [
50 - 'term_id' => isset( $term->term_id ) ? $term->term_id : 0,
51 - 'post_type' => 'docs',
52 - 'post_status' => 'publish',
53 - 'posts_per_page' => -1,
54 - 'suppress_filters' => true,
55 - 's' => $search_input,
56 - 'tax_query' => $tax_query
57 - ];
50 + // Build a UTF-8 compatible search query
51 + // Search in post_title, post_content, and post_excerpt
52 + // Note: Removed COLLATE clause to avoid collation mismatch with TranslatePress tables
53 + $search = $wpdb->prepare(
54 + " AND (
55 + ({$wpdb->posts}.post_title LIKE %s)
56 + OR ({$wpdb->posts}.post_content LIKE %s)
57 + OR ({$wpdb->posts}.post_excerpt LIKE %s)",
58 + $like,
59 + $like,
60 + $like
61 + );
58 62
59 - if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
60 - $args['suppress_filters'] = false;
61 - $args['lang'] = ICL_LANGUAGE_CODE;
62 - }
63 + // If TranslatePress is active, also search in the translation dictionary
64 + if ( class_exists( '\TRP_Translate_Press' ) ) {
65 + // Get language codes
66 + $lang_codes = $this->get_trp_language_code();
67 + $default_lang = $lang_codes['default_language'];
68 + $current_lang = $lang_codes['current_language'];
69 +
70 + // Only search in translation table if current language is different from default
71 + if ( $default_lang !== $current_lang ) {
72 + $default_lang = preg_replace( '/[^a-z0-9_]/', '', $default_lang );
73 + $current_lang = preg_replace( '/[^a-z0-9_]/', '', $current_lang );
74 + // TranslatePress table naming: wp_trp_dictionary_{default_lang}_{current_lang}
75 + $trp_table = $wpdb->prefix . 'trp_dictionary_' . $default_lang . '_' . $current_lang;
63 76
64 - $search_results = $this->query->get_posts( $args );
77 + if ( $this->table_exists( $trp_table ) ) {
78 + // $trp_table is composed from $wpdb->prefix + sanitized lang slugs (preg_replace allowlist above);
79 + // $like is esc_like()-wrapped with intentional % wildcards; CONCAT() wildcards are query literals, not user input.
80 + // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
81 + $trp_search = $wpdb->prepare(
82 + " OR EXISTS (
83 + SELECT 1 FROM {$trp_table} trp
84 + WHERE (trp.original LIKE %s OR trp.translated LIKE %s)
85 + AND trp.status != 2
86 + AND (
87 + {$wpdb->posts}.post_title COLLATE utf8mb4_unicode_ci = trp.original COLLATE utf8mb4_unicode_ci
88 + OR {$wpdb->posts}.post_content COLLATE utf8mb4_unicode_ci LIKE CONCAT('%%', trp.original COLLATE utf8mb4_unicode_ci, '%%')
89 + OR {$wpdb->posts}.post_excerpt COLLATE utf8mb4_unicode_ci = trp.original COLLATE utf8mb4_unicode_ci
90 + )
91 + )",
92 + $like,
93 + $like
94 + );
95 + // phpcs:enable WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
65 96
66 - $response = [];
97 + $search .= $trp_search;
98 + }
99 + }
100 + }
67 101
68 - ob_start();
69 - betterdocs()->views->get( 'shortcode-parts/search-results', [
70 - 'search_results' => $search_results,
71 - 'search_input' => $search_input
72 - ] );
73 102
74 - $_output = ob_get_clean();
103 + $search .= " ) ";
104 + }
75 105
76 - $_input_not_found = '';
77 - if ( ! $search_results->have_posts() ) {
78 - $_input_not_found = $search_input;
79 - }
106 + return $search;
107 + }
80 108
81 - $response['post_lists'] = $_output;
109 + /**
110 + * Get TranslatePress language codes (default and current) for table name construction
111 + *
112 + * @return array Array with 'default_language' and 'current_language' keys
113 + */
114 + private function get_trp_language_code() {
115 + $result = [
116 + 'default_language' => 'en_US',
117 + 'current_language' => 'en_US'
118 + ];
119 +
120 + if ( class_exists( '\TRP_Translate_Press' ) ) {
121 + $trp = \TRP_Translate_Press::get_trp_instance();
122 + if ( isset( $trp ) && method_exists( $trp, 'get_component' ) ) {
123 + $trp_settings = $trp->get_component( 'settings' );
124 +
125 + // Get default language from settings
126 + if ( $trp_settings ) {
127 + $settings = $trp_settings->get_settings();
128 + if ( isset( $settings['default-language'] ) ) {
129 + $result['default_language'] = strtolower( $settings['default-language'] );
130 + }
131 + }
132 +
133 + // Get current language from global variable
134 + global $TRP_LANGUAGE;
135 + if ( isset( $TRP_LANGUAGE ) && ! empty( $TRP_LANGUAGE ) ) {
136 + $result['current_language'] = strtolower( $TRP_LANGUAGE );
137 + }
138 + }
139 + }
140 +
141 + return $result;
142 + }
82 143
83 - if ( $_output && strlen( $search_input ) >= 3 ) {
84 - $this->insert( $search_input, $_input_not_found );
85 - }
144 + /**
145 + * Check if a database table exists
146 + */
147 + private function table_exists( $table_name ) {
148 + global $wpdb;
149 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- schema check, caching would mask plugin-activation state.
150 + $result = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) );
151 + return $result === $table_name;
152 + }
86 153
87 - wp_reset_query();
88 - wp_reset_postdata();
154 + public function get_style_depends() {
155 + $handlers = [ 'betterdocs-search' ];
156 + return $handlers;
157 + }
89 158
90 - wp_send_json_success( $response );
91 - }
159 + public function get_script_depends() {
160 + $handlers = [ 'betterdocs-search'];
92 161
93 - private function insert( $search_input, $input_not_found ) {
94 - global $wpdb;
95 - $search = $wpdb->get_results(
96 - $wpdb->prepare(
97 - "SELECT *
98 - FROM {$wpdb->prefix}betterdocs_search_keyword
99 - WHERE keyword = %s",
100 - $search_input
101 - )
102 - );
162 + if ( is_tax() ) {
163 + $handlers[] = 'betterdocs-glossaries';
164 + }
165 + return $handlers;
166 + }
103 167
104 - if ( ! empty( $search ) ) {
105 - $search_log = $wpdb->get_results(
106 - $wpdb->prepare(
107 - "SELECT *
108 - FROM {$wpdb->prefix}betterdocs_search_log
109 - WHERE created_at = %s AND keyword_id = %d",
110 - date( 'Y-m-d' ), $search[0]->id
111 - )
112 - );
168 + public function get_search_results() {
169 + global $wpdb;
170 + // phpcs:disable WordPress.Security.NonceVerification.Missing -- public live-search endpoint, no state change.
171 + $search_input = isset( $_POST['search_input'] ) ? sanitize_text_field( wp_unslash( $_POST['search_input'] ) ) : '';
172 + $search_cat = isset( $_POST['search_cat'] ) ? wp_strip_all_tags( wp_unslash( $_POST['search_cat'] ) ) : '';
173 + $lang = isset( $_POST['lang'] ) ? wp_strip_all_tags( wp_unslash( $_POST['lang'] ) ) : '';
174 + $kb_slug = isset( $_POST['kb_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['kb_slug'] ) ) : '';
175 + // phpcs:enable WordPress.Security.NonceVerification.Missing
113 176
114 - if ( ! empty( $search_log ) ) {
115 - if ( ! empty( $input_not_found ) ) {
116 - $tbl_field = 'not_found_count';
117 - $count = $search_log[0]->not_found_count + 1;
118 - } else {
119 - $tbl_field = 'count';
120 - $count = $search_log[0]->count + 1;
121 - }
122 - $wpdb->query(
123 - $wpdb->prepare(
124 - "UPDATE {$wpdb->prefix}betterdocs_search_log
125 - SET " . $tbl_field . " = " . $count . "
126 - WHERE created_at = %s AND keyword_id = %d",
127 - $search_log[0]->created_at, $search_log[0]->keyword_id
128 - )
129 - );
130 - } else {
131 - if ( ! empty( $input_not_found ) ) {
132 - $count = 0;
133 - $not_found_count = 1;
134 - } else {
135 - $count = 1;
136 - $not_found_count = 0;
137 - }
138 - $wpdb->query(
139 - $wpdb->prepare(
140 - "INSERT INTO {$wpdb->prefix}betterdocs_search_log
141 - ( keyword_id, count, not_found_count, created_at )
142 - VALUES ( %d, %d, %d, %s )",
143 - [
144 - $search[0]->id,
145 - $count,
146 - $not_found_count,
147 - date( 'Y-m-d' )
148 - ]
149 - )
150 - );
151 - }
152 - } else {
153 - $insert = $wpdb->query(
154 - $wpdb->prepare(
155 - "INSERT INTO {$wpdb->prefix}betterdocs_search_keyword
156 - ( keyword )
157 - VALUES ( %s )",
158 - [
159 - $search_input
160 - ]
161 - )
162 - );
177 + $tax_query = [];
178 + if ( $search_cat ) {
179 + $tax_query = [
180 + [
181 + 'taxonomy' => 'doc_category',
182 + 'field' => 'slug',
183 + 'terms' => $search_cat,
184 + 'operator' => 'AND',
185 + 'include_children' => true
186 + ]
187 + ];
188 + }
163 189
164 - if ( $insert ) {
165 - if ( ! empty( $input_not_found ) ) {
166 - $count = 0;
167 - $not_found_count = 1;
168 - } else {
169 - $count = 1;
170 - $not_found_count = 0;
171 - }
172 - $wpdb->query(
173 - $wpdb->prepare(
174 - "INSERT INTO {$wpdb->prefix}betterdocs_search_log
175 - ( keyword_id, count, not_found_count, created_at )
176 - VALUES ( %d, %d, %d, %s )",
177 - [
178 - $wpdb->insert_id,
179 - $count,
180 - $not_found_count,
181 - date( 'Y-m-d' )
182 - ]
183 - )
184 - );
185 - }
186 - }
187 - }
190 + // Don't build KB tax_query here - let the MultipleKB filter handle it
191 + // We just pass kb_slug in the args
188 192
189 - public function get_name() {
190 - return 'betterdocs_search_form';
191 - }
193 + $term = get_term_by( 'slug', $search_cat );
192 194
193 - /**
194 - * Summary of default_attributes
195 - * @return array
196 - */
197 - public function default_attributes() {
198 - return apply_filters( 'betterdocs_search_form_attr', [
199 - 'placeholder' => __( 'Search', 'betterdocs' ),
200 - 'heading' => '',
201 - 'subheading' => '',
202 - 'heading_tag' => 'h2',
203 - 'subheading_tag' => 'h3'
204 - ] );
205 - }
195 + $post_status = ['publish'];
206 196
207 - public function render( $atts, $content = null ) {
208 - $this->views( 'shortcodes/search' );
209 - }
197 + if( current_user_can( 'read_private_docs' ) ) {
198 + array_push($post_status, 'private');
199 + }
200 +
201 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- search query supports user-selected category filter.
202 + $args = [
203 + 'term_id' => isset( $term->term_id ) ? $term->term_id : 0,
204 + 'post_type' => 'docs',
205 + 'post_status' => $post_status,
206 + 'posts_per_page' => -1,
207 + 'suppress_filters' => false, // Changed to false to allow posts_search filter
208 + 's' => $search_input,
209 + 'orderby' => 'relevance',
210 + // 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.
211 + 'tax_query' => $tax_query,
212 + 'kb_slug' => $kb_slug // Pass kb_slug for filter hooks
213 + ];
214 +
215 + // Handle WPML multilingual search
216 + if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
217 + // If search term contains non-ASCII characters (e.g., Chinese, Japanese, Bangla),
218 + // search across all languages to find translated posts
219 + if ( preg_match('/[^\x00-\x7F]/', $search_input) ) {
220 + // Non-ASCII search: bypass WPML language filtering but allow posts_search filter
221 + // This allows searching across all languages
222 + $args['suppress_filters'] = true; // phpcs:ignore WordPressVIPMinimum.Hooks.PreGetPosts.PreGetPosts,WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- non-ASCII search must reach all WPML translations.
223 + } else {
224 + // ASCII-only search (English), use WPML filters to restrict to current language
225 + $args['suppress_filters'] = false;
226 + $args['lang'] = ICL_LANGUAGE_CODE;
227 + }
228 + }
229 + // Handle TranslatePress - always allow posts_search filter for non-ASCII
230 + elseif ( class_exists( '\TRP_Translate_Press' ) && preg_match('/[^\x00-\x7F]/', $search_input) ) {
231 + // For TranslatePress, we need posts_search filter to run
232 + $args['suppress_filters'] = false;
233 + }
234 +
235 + // Add filter to improve search for non-English characters
236 + add_filter( 'posts_search', [ $this, 'improve_search_for_non_english' ], 10, 2 );
237 +
238 + $search_results = $this->query->get_posts( $args );
239 +
240 + // Remove filter after query to avoid affecting other queries
241 + remove_filter( 'posts_search', [ $this, 'improve_search_for_non_english' ], 10 );
242 +
243 + $response = [];
244 +
245 + ob_start();
246 + betterdocs()->views->get(
247 + 'shortcode-parts/search-results',
248 + [
249 + 'search_results' => $search_results,
250 + 'search_input' => $search_input
251 + ]
252 + );
253 +
254 + $_output = ob_get_clean();
255 +
256 + $_input_not_found = '';
257 + if ( ! $search_results->have_posts() ) {
258 + $_input_not_found = $search_input;
259 + }
260 +
261 + $response['post_lists'] = $_output;
262 +
263 + if ( $_output && strlen( $search_input ) >= 3 ) {
264 + betterdocs()->query->insert_search_keyword( $search_input, $_input_not_found );
265 + }
266 +
267 + wp_reset_postdata();
268 +
269 + wp_send_json_success( $response );
270 + }
271 +
272 + public function get_name() {
273 + return 'betterdocs_search_form';
274 + }
275 +
276 + /**
277 + * Summary of default_attributes
278 + * @return array
279 + */
280 + public function default_attributes() {
281 + return apply_filters(
282 + 'betterdocs_search_form_attr',
283 + [
284 + 'placeholder' => __( 'Search', 'betterdocs' ),
285 + 'heading' => '',
286 + 'subheading' => '',
287 + 'heading_tag' => 'h1',
288 + 'subheading_tag' => 'p',
289 + 'kb_based_search' => '' // KB slug to filter search results
290 + ]
291 + );
292 + }
293 +
294 + public function render( $atts, $content = null ) {
295 + // Get kb_based_search from shortcode attribute (KB slug)
296 + $kb_based_search = isset( $atts['kb_based_search'] ) ? sanitize_text_field( $atts['kb_based_search'] ) : '';
297 +
298 + betterdocs()->assets->localize(
299 + 'betterdocs-search',
300 + 'betterdocsSearchConfigTwo',
301 + [
302 + 'is_post_type_archive' => is_post_type_archive( 'docs' ),
303 + 'kb_based_search' => $kb_based_search,
304 + ]
305 + );
306 +
307 + $this->views( 'shortcodes/search' );
308 + }
210 309 }