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 / SearchForm.php

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

310 lines 10.5 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 SearchForm 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 /**
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;
32
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 }
37
38 // Only modify if there's a search term
39 if ( empty( $query->query_vars['s'] ) ) {
40 return $search;
41 }
42
43 $search_term = $query->query_vars['s'];
44
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 ) . '%';
49
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 );
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;
76
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
96
97 $search .= $trp_search;
98 }
99 }
100 }
101
102
103 $search .= " ) ";
104 }
105
106 return $search;
107 }
108
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 }
143
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 }
153
154 public function get_style_depends() {
155 $handlers = [ 'betterdocs-search' ];
156 return $handlers;
157 }
158
159 public function get_script_depends() {
160 $handlers = [ 'betterdocs-search'];
161
162 if ( is_tax() ) {
163 $handlers[] = 'betterdocs-glossaries';
164 }
165 return $handlers;
166 }
167
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
176
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 }
189
190 // Don't build KB tax_query here - let the MultipleKB filter handle it
191 // We just pass kb_slug in the args
192
193 $term = get_term_by( 'slug', $search_cat );
194
195 $post_status = ['publish'];
196
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 }
309 }
310