PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 3.5.3
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v3.5.3
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 +179 -273 4.9.23.5.3 View file →
@@ -1,12 +1,8 @@
1 1 <?php
2 +
2 3 namespace WPDeveloper\BetterDocs\Shortcodes;
3 4
4 -if ( ! defined( 'ABSPATH' ) ) {
5 - exit;
6 -}
7 -
8 -
9 5 use WPDeveloper\BetterDocs\Core\Query;
10 6 use WPDeveloper\BetterDocs\Utils\Helper;
11 7 use WPDeveloper\BetterDocs\Core\Settings;
12 8 use WPDeveloper\BetterDocs\Core\Shortcode;
@@ -12,298 +8,208 @@
12 8 use WPDeveloper\BetterDocs\Core\Shortcode;
13 9 use WPDeveloper\BetterDocs\Admin\Customizer\Defaults;
14 10
15 11 class SearchForm extends Shortcode {
16 - public function __construct( Settings $settings, Query $query, Helper $helper, Defaults $defaults ) {
17 - parent::__construct( $settings, $query, $helper, $defaults );
12 + public function __construct( Settings $settings, Query $query, Helper $helper, Defaults $defaults ) {
13 + parent::__construct( $settings, $query, $helper, $defaults );
18 14
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 - }
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 + }
22 18
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;
19 + public function get_style_depends() {
20 + return ['betterdocs-search'];
21 + }
32 22
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 - }
23 + public function get_script_depends() {
24 + return ['betterdocs-search'];
25 + }
37 26
38 - // Only modify if there's a search term
39 - if ( empty( $query->query_vars['s'] ) ) {
40 - return $search;
41 - }
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 ) );
42 33
43 - $search_term = $query->query_vars['s'];
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 + }
44 46
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 ) . '%';
47 + $term = get_term_by( 'slug', $search_cat );
49 48
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 - );
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 + 'orderby' => 'relevance',
57 + 'tax_query' => $tax_query
58 + ];
62 59
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;
60 + if ( is_plugin_active( 'sitepress-multilingual-cms/sitepress.php' ) ) {
61 + $args['suppress_filters'] = false;
62 + $args['lang'] = ICL_LANGUAGE_CODE;
63 + }
76 64
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 + $search_results = $this->query->get_posts( $args );
96 66
97 - $search .= $trp_search;
98 - }
99 - }
100 - }
67 + $response = [];
101 68
69 + ob_start();
70 + betterdocs()->views->get( 'shortcode-parts/search-results', [
71 + 'search_results' => $search_results,
72 + 'search_input' => $search_input
73 + ] );
102 74
103 - $search .= " ) ";
104 - }
75 + $_output = ob_get_clean();
105 76
106 - return $search;
107 - }
77 + $_input_not_found = '';
78 + if ( ! $search_results->have_posts() ) {
79 + $_input_not_found = $search_input;
80 + }
108 81
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 + $response['post_lists'] = $_output;
143 83
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 - }
84 + if ( $_output && strlen( $search_input ) >= 3 ) {
85 + $this->insert( $search_input, $_input_not_found );
86 + }
153 87
154 - public function get_style_depends() {
155 - $handlers = [ 'betterdocs-search' ];
156 - return $handlers;
157 - }
88 + wp_reset_query();
89 + wp_reset_postdata();
158 90
159 - public function get_script_depends() {
160 - $handlers = [ 'betterdocs-search'];
91 + wp_send_json_success( $response );
92 + }
161 93
162 - if ( is_tax() ) {
163 - $handlers[] = 'betterdocs-glossaries';
164 - }
165 - return $handlers;
166 - }
94 + private function insert( $search_input, $input_not_found ) {
95 + global $wpdb;
96 + $search = $wpdb->get_results(
97 + $wpdb->prepare(
98 + "SELECT *
99 + FROM {$wpdb->prefix}betterdocs_search_keyword
100 + WHERE keyword = %s",
101 + $search_input
102 + )
103 + );
167 104
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
105 + if ( ! empty( $search ) ) {
106 + $search_log = $wpdb->get_results(
107 + $wpdb->prepare(
108 + "SELECT *
109 + FROM {$wpdb->prefix}betterdocs_search_log
110 + WHERE created_at = %s AND keyword_id = %d",
111 + date( 'Y-m-d' ), $search[0]->id
112 + )
113 + );
176 114
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 - }
115 + if ( ! empty( $search_log ) ) {
116 + if ( ! empty( $input_not_found ) ) {
117 + $tbl_field = 'not_found_count';
118 + $count = $search_log[0]->not_found_count + 1;
119 + } else {
120 + $tbl_field = 'count';
121 + $count = $search_log[0]->count + 1;
122 + }
123 + $wpdb->query(
124 + $wpdb->prepare(
125 + "UPDATE {$wpdb->prefix}betterdocs_search_log
126 + SET " . $tbl_field . " = " . $count . "
127 + WHERE created_at = %s AND keyword_id = %d",
128 + $search_log[0]->created_at, $search_log[0]->keyword_id
129 + )
130 + );
131 + } else {
132 + if ( ! empty( $input_not_found ) ) {
133 + $count = 0;
134 + $not_found_count = 1;
135 + } else {
136 + $count = 1;
137 + $not_found_count = 0;
138 + }
139 + $wpdb->query(
140 + $wpdb->prepare(
141 + "INSERT INTO {$wpdb->prefix}betterdocs_search_log
142 + ( keyword_id, count, not_found_count, created_at )
143 + VALUES ( %d, %d, %d, %s )",
144 + [
145 + $search[0]->id,
146 + $count,
147 + $not_found_count,
148 + date( 'Y-m-d' )
149 + ]
150 + )
151 + );
152 + }
153 + } else {
154 + $insert = $wpdb->query(
155 + $wpdb->prepare(
156 + "INSERT INTO {$wpdb->prefix}betterdocs_search_keyword
157 + ( keyword )
158 + VALUES ( %s )",
159 + [
160 + $search_input
161 + ]
162 + )
163 + );
189 164
190 - // Don't build KB tax_query here - let the MultipleKB filter handle it
191 - // We just pass kb_slug in the args
165 + if ( $insert ) {
166 + if ( ! empty( $input_not_found ) ) {
167 + $count = 0;
168 + $not_found_count = 1;
169 + } else {
170 + $count = 1;
171 + $not_found_count = 0;
172 + }
173 + $wpdb->query(
174 + $wpdb->prepare(
175 + "INSERT INTO {$wpdb->prefix}betterdocs_search_log
176 + ( keyword_id, count, not_found_count, created_at )
177 + VALUES ( %d, %d, %d, %s )",
178 + [
179 + $wpdb->insert_id,
180 + $count,
181 + $not_found_count,
182 + date( 'Y-m-d' )
183 + ]
184 + )
185 + );
186 + }
187 + }
188 + }
192 189
193 - $term = get_term_by( 'slug', $search_cat );
190 + public function get_name() {
191 + return 'betterdocs_search_form';
192 + }
194 193
195 - $post_status = ['publish'];
194 + /**
195 + * Summary of default_attributes
196 + * @return array
197 + */
198 + public function default_attributes() {
199 + return apply_filters( 'betterdocs_search_form_attr', [
200 + 'placeholder' => __( 'Search', 'betterdocs' ),
201 + 'heading' => '',
202 + 'subheading' => '',
203 + 'heading_tag' => 'h2',
204 + 'subheading_tag' => 'h3'
205 + ] );
206 + }
196 207
197 - if( current_user_can( 'read_private_docs' ) ) {
198 - array_push($post_status, 'private');
199 - }
208 + public function render( $atts, $content = null ) {
209 + betterdocs()->assets->localize( 'betterdocs-search', 'betterdocsSearchConfigTwo', [
210 + 'is_post_type_archive' => is_post_type_archive( 'docs' ),
211 + ] );
200 212
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 - }
213 + $this->views( 'shortcodes/search' );
214 + }
309 215 }