PluginProbe
Knowledge Base documentation & wiki plugin – BasePress Docs / trunk
Knowledge Base documentation & wiki plugin – BasePress Docs vtrunk
basepress / includes / class-basepress-search.php

class-basepress-search.php in Knowledge Base documentation & wiki plugin – BasePress Docs trunk, at includes/class-basepress-search.php

901 lines 40.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * This is the class that creates and handles the search bar
5 */
6 // Exit if accessed directly.
7 if ( !defined( 'ABSPATH' ) ) {
8 exit;
9 }
10 if ( !class_exists( 'Basepress_Search' ) ) {
11 class Basepress_Search {
12 private $product = null;
13
14 private $multibyte = false;
15
16 /**
17 * basepress_search constructor.
18 *
19 * @since 1.0.0
20 *
21 * @updated 1.4.0
22 */
23 public function __construct() {
24 //Define the ajax call for front end
25 add_action( 'wp_ajax_nopriv_basepress_smart_search', array($this, 'basepress_smart_search') );
26 add_action( 'wp_ajax_basepress_smart_search', array($this, 'basepress_smart_search') );
27 //Redirect page in case the request is coming from the search form
28 add_action( 'get_header', array($this, 'headers') );
29 //Modifies WordPress query for search pages in front end
30 if ( !is_admin() ) {
31 add_filter(
32 'posts_clauses',
33 array($this, 'search_page_query'),
34 20,
35 2
36 );
37 }
38 // register basepress-searchbar shortcode
39 add_shortcode( 'basepress-search', array($this, 'do_shortcode') );
40 //Enqueue scripts if shortcode is present
41 add_action( 'wp_enqueue_scripts', array($this, 'enqueue_shortcode_scripts'), 90 );
42 //Adds meta tags to prevent browsers caching search results pages
43 add_action( 'wp_head', array($this, 'prevent_browser_caching') );
44 $this->multibyte = function_exists( 'mb_strlen' ) && function_exists( 'mb_stripos' ) && function_exists( 'mb_substr' ) && function_exists( 'mb_strrpos' ) && function_exists( 'mb_strrpos' );
45 }
46
47 /**
48 * Redirects the page in case the request is coming from the search form
49 * Creating the correct permalink
50 *
51 * @since 1.0.0
52 */
53 public function headers() {
54 global $wp, $wp_rewrite, $basepress_utils;
55 $options = $basepress_utils->get_options();
56 if ( !isset( $_GET['s'] ) || isset( $options['search_use_url_parameters'] ) && $options['search_use_url_parameters'] ) {
57 return;
58 }
59 $rewrite_rules = get_option( 'rewrite_rules' );
60 //If plain permalink are used return
61 if ( empty( $rewrite_rules ) ) {
62 return;
63 }
64 if ( $basepress_utils->is_knowledgebase ) {
65 $tmp_url = home_url();
66 $query_string = parse_url( $tmp_url, PHP_URL_QUERY );
67 $home_url = untrailingslashit( strtok( $tmp_url, '?' ) );
68 $redirect_url = $home_url . '/' . $wp->request . '/' . $wp_rewrite->search_base . '/' . urlencode( sanitize_text_field( $_GET['s'] ) );
69 $redirect_url .= ( $query_string ? '?' . $query_string : '' );
70 /**
71 * Filters the redirect url
72 */
73 $redirect_url = apply_filters( 'basepress_search_redirect', $redirect_url );
74 wp_redirect( $redirect_url, 302, 'BasePress Search' );
75 exit;
76 }
77 }
78
79 /**
80 * Renders the search bar
81 *
82 * @since 1.0.0
83 *
84 * @updated 1.4.0
85 */
86 public function render_searchbar( $product = '' ) {
87 global $basepress_utils;
88 if ( !$product ) {
89 $product = $basepress_utils->get_product();
90 }
91 $options = $basepress_utils->get_options();
92 $placeholder = ( isset( $options['search_field_placeholder'] ) ? $options['search_field_placeholder'] : '' );
93 $search_terms = stripslashes( get_search_query() );
94 $kb_slug = $basepress_utils->get_kb_slug();
95 $entry_page = ( isset( $options['entry_page'] ) ? $options['entry_page'] : false );
96 /**
97 * Filter the kb_slug before generating the action link
98 * @since 1.5.0
99 */
100 $kb_slug = apply_filters( 'basepress_kb_slug', $kb_slug, $entry_page );
101 $permalink_opt = get_option( 'permalink_structure' );
102 //Search action if ugly permalinks are used
103 if ( '' == $permalink_opt ) {
104 $action = '';
105 } else {
106 //Search actions for all pretty permalinks
107 $action = esc_url( home_url( '/' ) );
108 //Find URL parameters
109 preg_match( '/.*(\\?.*)/', $action, $action_args );
110 if ( empty( $action_args ) ) {
111 $action_args = array(
112 1 => '',
113 );
114 } else {
115 //Remove URL parameters from URL. They will be added afterwords
116 $action = str_replace( $action_args[1], '', $action );
117 }
118 if ( isset( $options['single_product_mode'] ) ) {
119 $action = trailingslashit( $action . $kb_slug ) . $action_args[1];
120 } else {
121 $action = trailingslashit( $action . $kb_slug . '/' . $product->slug ) . $action_args[1];
122 }
123 }
124 //Creates extra classes for the search input field. At the moment we have only the class stating the presence of the submit button.
125 $input_classes = ( isset( $options['show_search_submit'] ) ? ' show-submit' : '' );
126 ?>
127 <div class="bpress-search">
128 <form class="bpress-search-form<?php
129 echo esc_attr( $input_classes );
130 ?>" method="get" action="<?php
131 echo esc_url( $action );
132 ?>">
133 <input type="text" class="bpress-search-field<?php
134 echo esc_attr( $input_classes );
135 ?>" placeholder="<?php
136 echo esc_attr( $placeholder );
137 ?>" autocomplete="off" value="<?php
138 echo esc_attr( $search_terms );
139 ?>" name="s" data-product="<?php
140 echo esc_attr( $product->slug );
141 ?>">
142 <?php
143 wp_nonce_field( 'bp-search-nonce', 'search-nonce' );
144 ?>
145 <?php
146 if ( '' == $permalink_opt ) {
147 ?>
148 <?php
149 if ( $product->slug && !isset( $options['single_product_mode'] ) ) {
150 ?>
151 <input type="hidden" name="knowledgebase_cat" value="<?php
152 echo esc_attr( $product->slug );
153 ?>">
154 <?php
155 } else {
156 ?>
157 <input type="hidden" name="post_type" value="knowledgebase">
158 <?php
159 }
160 }
161 /**
162 * This action can be used to add fields to the search form
163 */
164 do_action( 'basepress_search_fields', $product );
165 ?>
166
167 <?php
168 if ( isset( $options['show_search_submit'] ) ) {
169 $submit_txt = ( isset( $options['search_submit_text'] ) ? $options['search_submit_text'] : '' );
170 ?>
171
172 <span class="bpress-search-submit">
173 <input type="submit" value="<?php
174 echo esc_attr( $submit_txt );
175 ?>">
176 </span>
177
178 <?php
179 }
180 ?>
181
182 </form>
183 <?php
184 if ( isset( $options['show_search_suggest'] ) ) {
185 $min_screen = ( isset( $options['min_search_suggest_screen'] ) ? $options['min_search_suggest_screen'] : '' );
186 ?>
187
188 <div class="bpress-search-suggest" data-minscreen="<?php
189 echo esc_attr( $min_screen );
190 ?>"></div>
191 <?php
192 }
193 ?>
194 </div>
195
196 <?php
197 }
198
199 /**
200 * Retrieve search term if any
201 *
202 * @since 1.0.0
203 *
204 * @return string
205 */
206 public function get_search_term() {
207 $terms = stripslashes( get_search_query( false ) );
208 if ( empty( $terms ) && isset( $_REQUEST['action'] ) && 'basepress_smart_search' == $_REQUEST['action'] ) {
209 $terms = ( isset( $_REQUEST['terms'] ) ? stripslashes( sanitize_text_field( $_REQUEST['terms'] ) ) : '' );
210 }
211 return $terms;
212 }
213
214 /**
215 * Generates the results for the smart search
216 * Used by Ajax calls and by render_searchbar()
217 *
218 * @since 1.0.0
219 * @updated 2.5.0
220 *
221 * @param string $product
222 * @param string $terms
223 */
224 public function basepress_smart_search( $product = '', $terms = '' ) {
225 if ( !isset( $_GET['nonce'] ) || !wp_verify_nonce( sanitize_title_for_query( wp_unslash( $_GET['nonce'] ) ), 'bp-search-nonce' ) ) {
226 $response = '<ul>';
227 $response .= '<li>' . esc_html__( 'No results found.', 'basepress' ) . '</li>';
228 $response .= '</ul>';
229 wp_send_json( array(
230 'html' => $response,
231 'foundPosts' => 0,
232 ) );
233 wp_die();
234 }
235 global $wpdb, $basepress_utils;
236 $found_posts = 0;
237 //Get suggestion's limit from options
238 $options = $basepress_utils->get_options();
239 $limit = ( isset( $options['search_suggest_count'] ) && '' != $options['search_suggest_count'] ? $options['search_suggest_count'] : PHP_INT_MAX );
240 //Get the product and terms
241 $get_product = ( isset( $_GET['product'] ) ? sanitize_title_for_query( wp_unslash( $_GET['product'] ) ) : '' );
242 $product_slug = ( $product ? $product : $get_product );
243 $terms = $this->get_search_term();
244 $product = ( $this->product ? $this->product : get_term_by( 'slug', $product_slug, 'knowledgebase_cat' ) );
245 $run_search = true;
246 if ( $run_search ) {
247 $query_pieces = $this->query_pieces( $product, $terms );
248 if ( $query_pieces ) {
249 $query_string = "SELECT DISTINCT SQL_CALC_FOUND_ROWS *";
250 $query_string .= " FROM {$wpdb->posts}";
251 $query_string .= $query_pieces['join'];
252 $query_string .= ' WHERE 1=1' . $query_pieces['where'];
253 $query_string .= " GROUP BY {$wpdb->posts}.ID";
254 $query_string .= ' ORDER BY ';
255 $query_string .= $query_pieces['orderby'];
256 $query_string .= ' LIMIT ' . $limit;
257 $wpdb->hide_errors();
258 $search_posts = $wpdb->get_results( $query_string );
259 if ( is_array( $search_posts ) && empty( $wpdb->last_error ) ) {
260 $wpdb->query( "SELECT FOUND_ROWS() as found_posts" );
261 $found_posts = $wpdb->last_result[0]->found_posts;
262 }
263 }
264 } else {
265 }
266 //Generates results from the found articles
267 $results = array();
268 $disable_word_boundaries = $basepress_utils->get_option( 'search_disable_word_boundary' );
269 $word_boundary = ( $disable_word_boundaries ? '' : apply_filters( 'basepress_snippet_word_boundary', '\\b' ) );
270 $minimum_char_length = $basepress_utils->get_option( 'search_min_chars' );
271 $minimum_char_length = ( $minimum_char_length ? $minimum_char_length : 3 );
272 $live_search_template = $basepress_utils->get_theme_file_path( 'live-search.php' );
273 if ( !$live_search_template && !empty( $search_posts ) ) {
274 foreach ( $search_posts as $search_post ) {
275 $content = $search_post->post_content;
276 //Strip shortcodes from content
277 $content = $this->strip_shortcodes( $content );
278 //Strip html tags from content
279 $content = wp_strip_all_tags( $content );
280 //Get the snipped for the article
281 $snippet = $this->get_snippet( $this->filter_terms( $terms ), $content, apply_filters( 'basepress_smart_search_snippet_length', 200 ) );
282 //Highlight the found terms
283 $search_terms = explode( ' ', $terms );
284 foreach ( $search_terms as $term ) {
285 if ( strlen( $term ) >= apply_filters( 'basepress_search_min_char_length', $minimum_char_length ) ) {
286 $snippet = preg_replace( '@(' . $word_boundary . preg_quote( stripslashes( $term ), '@' ) . ')@i', "<b>\\1</b>", $snippet );
287 }
288 }
289 $results[] = array(
290 'title' => $search_post->post_title,
291 'text' => apply_filters( 'basepress_search_snippet', $snippet, $search_post ),
292 'link' => get_permalink( $search_post->ID ),
293 );
294 }
295 }
296 ob_start();
297 //Echo the results list
298 if ( !empty( $search_posts ) ) {
299 if ( $live_search_template ) {
300 $post_ids = wp_list_pluck( $search_posts, 'ID' );
301 query_posts( array(
302 'post_type' => 'knowledgebase',
303 'post__in' => $post_ids,
304 'orderby' => 'post__in',
305 ) );
306 include $live_search_template;
307 } else {
308 if ( !empty( $results ) ) {
309 echo '<ul>';
310 foreach ( $results as $result ) {
311 echo '<li>';
312 echo '<a href="' . esc_url( $result['link'] ) . '"><p>';
313 echo '<span class="bpress-search-suggest-title">' . esc_html( $result['title'] ) . '</span><br>';
314 echo wp_kses_post( $result['text'] );
315 echo '</p></a>';
316 echo '</li>';
317 }
318 echo '</ul>';
319 }
320 }
321 //Add the suggest more link
322 $suggest_more = ( isset( $options['search_suggest_more_text'] ) ? $options['search_suggest_more_text'] : '' );
323 $suggest_more = str_replace( '%number%', $found_posts, $suggest_more );
324 $suggest_more_class = ( '' == $suggest_more ? ' empty' : ' text' );
325 echo '<div class="bpress-search-suggest-more' . esc_attr( $suggest_more_class ) . '">';
326 echo '<span>' . esc_html( $suggest_more ) . '</span>';
327 echo '</div>';
328 } else {
329 if ( isset( $options['smartsearch_no_results_message'] ) && '' != $options['smartsearch_no_results_message'] ) {
330 echo '<ul>';
331 echo '<li>' . esc_html( $options['smartsearch_no_results_message'] ) . '</li>';
332 echo '</ul>';
333 }
334 }
335 $response = ob_get_clean();
336 //If it was an ajax request die
337 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
338 wp_send_json( array(
339 'html' => $response,
340 'foundPosts' => $found_posts,
341 ) );
342 } else {
343 echo wp_kses_post( $response );
344 }
345 }
346
347 /**
348 * Modifies WordPress main query for search pages
349 *
350 * @since 1.2.1
351 * @updated 1.7.5
352 *
353 * @param $pieces
354 * @return mixed
355 */
356 public function search_page_query( $pieces, $wp_query ) {
357 global $basepress_utils;
358 $is_main_query = isset( $wp_query ) && $wp_query->is_main_query();
359 if ( $is_main_query && $basepress_utils->is_search ) {
360 //Redirect
361 $this->headers();
362 $product_slug = ( isset( $wp_query->query['knowledgebase_cat'] ) ? $wp_query->query['knowledgebase_cat'] : '' );
363 //Get product term and save it for later use like in search bar
364 $product = get_term_by( 'slug', $product_slug, 'knowledgebase_cat' );
365 $this->product = $product;
366 $terms = $this->get_search_term();
367 $query_pieces = $this->query_pieces( $product, $terms, $pieces );
368 if ( $query_pieces ) {
369 $pieces['join'] = $query_pieces['join'];
370 $pieces['where'] = $query_pieces['where'];
371 $pieces['orderby'] = $query_pieces['orderby'];
372 $pieces['groupby'] = '';
373 } else {
374 $pieces['join'] = '';
375 $pieces['where'] = 'AND 1=2';
376 $pieces['orderby'] = '';
377 $pieces['groupby'] = '';
378 }
379 $GLOBALS['basepress_search_query'] = true;
380 $pieces['distinct'] = "DISTINCT";
381 do_action( 'basepress_made_search', $product, $terms );
382 }
383 return $pieces;
384 }
385
386 /**
387 * Generates the query pieces
388 *
389 * @since 1.2.1
390 *
391 * @param $product
392 * @param $terms
393 * @param array $pieces
394 * @return array|null|object
395 */
396 private function query_pieces( $product, $terms, $pieces = array() ) {
397 global $basepress_utils;
398 $quoted_search = false;
399 if ( preg_match( "/^([\"']).*\x01\$/m", $terms ) ) {
400 $quoted_search = true;
401 $terms = str_replace( array('"', "'"), '', $terms );
402 }
403 //remove unwanted characters
404 $terms = str_replace( array('"'), ' ', $terms );
405 if ( $basepress_utils->get_option( 'search_disable_word_boundary' ) || $quoted_search ) {
406 return $this->query_pieces_like(
407 $product,
408 $terms,
409 $pieces,
410 $quoted_search
411 );
412 }
413 return $this->query_pieces_regex( $product, $terms, $pieces );
414 }
415
416 /**
417 * Generate Query pieces using the LIKE clause
418 *
419 * @since 2.11.3
420 *
421 * @param $product
422 * @param $terms
423 * @param $pieces
424 * @return bool
425 */
426 private function query_pieces_like(
427 $product,
428 $terms,
429 $pieces,
430 $quoted_search = false
431 ) {
432 global $wpdb, $basepress_utils;
433 //Set exact matches terms before filtering the query
434 $exact_match_terms = $terms;
435 //Filter out unwanted terms
436 $terms = ( $quoted_search ? $terms : $this->filter_terms( $terms ) );
437 //If we have some terms proceed with the query
438 if ( $terms ) {
439 if ( !$product && isset( $options['single_product_mode'] ) ) {
440 $product_id = $basepress_utils->get_active_product_id();
441 } else {
442 $product_id = ( $product ? $product->term_id : 0 );
443 }
444 //Generates query clauses to limit results by product
445 $terms_args = array(
446 'taxonomy' => 'knowledgebase_cat',
447 'child_of' => $product_id,
448 'fields' => 'tt_ids',
449 );
450 $sections_ids = get_terms( $terms_args );
451 /**
452 * Filters the list of section IDs that should be used to limit the search to.
453 */
454 $sections_ids = apply_filters( 'basepress_search_tax_terms', $sections_ids, $product );
455 sort( $sections_ids );
456 $sections_ids = implode( ',', $sections_ids );
457 //Prepare matches portion of the query
458 $order_index = 1;
459 $orderby = '';
460 $order_fields = array("{$wpdb->posts}.post_title", "{$wpdb->posts}.post_content");
461 /*
462 * Exact matches
463 */
464 foreach ( $order_fields as $field ) {
465 $orderby .= $wpdb->prepare( " WHEN {$field} LIKE '%%%s%%' THEN %d", $wpdb->esc_like( $exact_match_terms ), $order_index );
466 $order_index++;
467 }
468 $sql_terms = explode( ' ', $terms );
469 /*
470 * AND logic
471 */
472 if ( count( $sql_terms ) > 1 && !$quoted_search ) {
473 foreach ( $order_fields as $field ) {
474 $orderby .= ' WHEN';
475 foreach ( $sql_terms as $index => $sql_term ) {
476 $orderby .= ( $index != 0 ? ' AND' : '' );
477 $orderby .= $wpdb->prepare( " {$field} LIKE '%%%s%%' ", $wpdb->esc_like( $sql_term ) );
478 }
479 $orderby .= $wpdb->prepare( " THEN %d ", $order_index );
480 $order_index++;
481 }
482 }
483 /*
484 * Close ORDER BY clause
485 */
486 $orderby = 'CASE ' . ltrim( $orderby, ' + ' ) . " ELSE {$order_index} END";
487 /*
488 * JOIN Clause
489 */
490 $pieces['join'] = " LEFT JOIN {$wpdb->term_relationships} AS t ON ({$wpdb->posts}.ID = t.object_id)";
491 /*
492 * WHERE Clause
493 */
494 $pieces['where'] = " AND t.term_taxonomy_id IN ({$sections_ids})";
495 if ( count( $sql_terms ) && !$quoted_search ) {
496 foreach ( $sql_terms as $index => $sql_term ) {
497 $pieces['where'] .= ( $index == 0 ? ' AND (' : ' OR' );
498 $pieces['where'] .= $wpdb->prepare( " {$wpdb->posts}.post_title LIKE '%%%s%%' OR {$wpdb->posts}.post_content LIKE '%%%s%%'", $wpdb->esc_like( $sql_term ), $wpdb->esc_like( $sql_term ) );
499 }
500 } else {
501 $pieces['where'] .= $wpdb->prepare( " AND ( {$wpdb->posts}.post_title LIKE '%%%s%%' OR {$wpdb->posts}.post_content LIKE '%%%s%%'", $wpdb->esc_like( $exact_match_terms ), $wpdb->esc_like( $exact_match_terms ) );
502 }
503 $pieces['where'] .= " )";
504 $pieces['where'] .= " AND {$wpdb->posts}.post_type = 'knowledgebase'";
505 $pieces['where'] .= " AND {$wpdb->posts}.post_status = 'publish'";
506 $pieces['orderby'] = ' ' . $orderby;
507 /**
508 * Filter to alter the query pieces for the search query
509 */
510 $pieces = apply_filters( 'basepress_search_query_pieces', $pieces, $terms );
511 $pieces['orderby'] .= ", {$wpdb->posts}.post_modified DESC";
512 return $pieces;
513 }
514 return false;
515 }
516
517 /**
518 * Generate query pieces using REGEXP
519 *
520 * @since 1.2.1
521 *
522 * @param $product
523 * @param $terms
524 * @param $pieces
525 * @return bool
526 */
527 private function query_pieces_regex( $product, $terms, $pieces ) {
528 global $wpdb, $basepress_utils;
529 $word_boundary = $basepress_utils->get_regex_word_boundary();
530 $word_boundary = apply_filters( 'basepress_search_word_boundary', $word_boundary );
531 $options = $basepress_utils->get_options();
532 $terms = stripslashes( $terms );
533 //Make all terms lowercase
534 if ( function_exists( 'mb_strtolower' ) ) {
535 $terms = mb_strtolower( $terms );
536 } else {
537 $terms = strtolower( $terms );
538 }
539 //Filter out unwanted special characters.
540 $terms = preg_replace( "/[~!@#\$%^&*='()_+\\/]+/u", '', $terms );
541 //Set exact matches terms before filtering the query
542 $exact_match_terms = $terms;
543 //Filter out unwanted terms
544 $terms = $this->filter_terms( $terms );
545 //If we have some terms proceed with the query
546 if ( $terms ) {
547 //Crates the multi-terms string to use in query
548 $multi_terms = $word_boundary . str_replace( ' ', '|' . $word_boundary, $terms );
549 if ( !$product && isset( $options['single_product_mode'] ) ) {
550 $product_id = $basepress_utils->get_active_product_id();
551 } else {
552 $product_id = ( $product ? $product->term_id : 0 );
553 }
554 //Generates query clauses to limit results by product
555 $terms_args = array(
556 'taxonomy' => 'knowledgebase_cat',
557 'child_of' => $product_id,
558 'fields' => 'tt_ids',
559 );
560 $sections_ids = get_terms( $terms_args );
561 /**
562 * Filters the list of section IDs that should be used to limit the search to.
563 */
564 $sections_ids = apply_filters( 'basepress_search_tax_terms', $sections_ids, $product );
565 sort( $sections_ids );
566 $sections_ids = implode( ',', $sections_ids );
567 //Prepare matches portion of the query
568 $order_index = 1;
569 $orderby = '';
570 $order_fields = array("{$wpdb->posts}.post_title", "{$wpdb->posts}.post_content");
571 /*
572 * Exact matches
573 */
574 foreach ( $order_fields as $field ) {
575 $orderby .= $wpdb->prepare( " WHEN LOWER({$field}) REGEXP '%s' THEN %d", $word_boundary . $exact_match_terms, $order_index );
576 $order_index++;
577 }
578 $sql_terms = explode( ' ', $terms );
579 /*
580 * AND logic
581 */
582 if ( count( $sql_terms ) > 1 ) {
583 foreach ( $order_fields as $field ) {
584 $orderby .= ' WHEN';
585 foreach ( $sql_terms as $index => $sql_term ) {
586 $orderby .= ( $index != 0 ? ' AND' : '' );
587 $orderby .= $wpdb->prepare( " LOWER({$field}) REGEXP '%s'", $word_boundary . $sql_term );
588 }
589 $orderby .= " THEN {$order_index}";
590 $order_index++;
591 }
592 }
593 /*
594 * Close ORDER BY clause
595 */
596 $orderby = 'CASE ' . ltrim( $orderby, ' + ' ) . " ELSE {$order_index} END";
597 /*
598 * JOIN Clause
599 */
600 $pieces['join'] = " LEFT JOIN {$wpdb->term_relationships} AS t ON ({$wpdb->posts}.ID = t.object_id)";
601 /*
602 * WHERE Clause
603 */
604 if ( !isset( $pieces['where'] ) && empty( $pieces['where'] ) ) {
605 $pieces['where'] = '';
606 }
607 if ( !empty( $sections_ids ) ) {
608 $pieces['where'] = " AND t.term_taxonomy_id IN ({$sections_ids})";
609 }
610 $pieces['where'] .= $wpdb->prepare( " AND ( LOWER({$wpdb->posts}.post_content) REGEXP '%s' OR LOWER({$wpdb->posts}.post_title) REGEXP '%s'", $multi_terms, $multi_terms );
611 $pieces['where'] .= " )";
612 $pieces['where'] .= " AND {$wpdb->posts}.post_type = 'knowledgebase'";
613 $pieces['where'] .= " AND {$wpdb->posts}.post_status = 'publish'";
614 $pieces['where'] = str_replace( '\\\\', '\\', $pieces['where'] );
615 $orderby = str_replace( '\\\\', '\\', $orderby );
616 $pieces['orderby'] = ' ' . $orderby;
617 /**
618 * Filter to alter the query pieces for the search query
619 */
620 $pieces = apply_filters( 'basepress_search_query_pieces', $pieces, $terms );
621 $pieces['orderby'] .= ", {$wpdb->posts}.post_modified DESC";
622 return $pieces;
623 }
624 return false;
625 }
626
627 /**
628 * Filters the terms removing non alphanumeric chars and terms shorter then 3 chars
629 *
630 * @since 1.2.1
631 *
632 * @param $terms
633 * @return mixed|string
634 */
635 public function filter_terms( $terms ) {
636 //Return if there are no terms
637 if ( !isset( $terms ) || !strlen( $terms ) ) {
638 return $terms;
639 }
640 global $basepress_utils;
641 $minimum_char_length = $basepress_utils->get_option( 'search_min_chars' );
642 $minimum_char_length = ( $minimum_char_length ? $minimum_char_length : 3 );
643 $terms = sanitize_text_field( $terms );
644 //Split the terms into array of terms
645 $single_terms = explode( ' ', $terms );
646 //Remove all terms shorter then 3 characters
647 foreach ( $single_terms as $key => $single_term ) {
648 if ( strlen( $single_term ) < apply_filters( 'basepress_search_min_char_length', $minimum_char_length ) ) {
649 unset($single_terms[$key]);
650 }
651 }
652 return implode( ' ', $single_terms );
653 }
654
655 /**
656 * Strip shortcodes from content
657 *
658 * @since 1.8.5
659 *
660 * @param $content
661 * @return null|string|string[]
662 */
663 public function strip_shortcodes( $content ) {
664 if ( apply_filters( 'basepress_search_strip_shortcodes', true ) ) {
665 if ( apply_filters( 'basepress_search_force_strip_shortcodes', false ) ) {
666 $content = preg_replace( '~(?:\\[/?)[^/\\]]+/?\\]~s', '', $content );
667 } else {
668 $content = strip_shortcodes( $content );
669 }
670 }
671 return $content;
672 }
673
674 /**
675 * Copyright (c) 2015, Ben Boyter All rights reserved.
676 * Redistribution and use in source and binary forms, with or without modification,
677 * are permitted provided that the following conditions are met:
678 * * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
679 * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
680 * * Neither the name of Ben Boyter nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
681 *
682 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
683 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
684 * IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
685 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
686 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
687 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
688 */
689 // find the locations of each of the words
690 // Nothing exciting here. The array_unique is required
691 // unless you decide to make the words unique before passing in
692 private function get_terms_locations( $terms, $content, $multibyte ) {
693 $locations = array();
694 $terms = explode( ' ', trim( $terms ) );
695 foreach ( $terms as $term ) {
696 $term_len = ( $multibyte ? mb_strlen( $term ) : strlen( $term ) );
697 $loc = ( $multibyte ? mb_stripos( $content, $term ) : stripos( $content, $term ) );
698 while ( false !== $loc ) {
699 $locations[] = $loc;
700 $loc = ( $multibyte ? mb_stripos( $content, $term, $loc + $term_len ) : stripos( $content, $term, $loc + $term_len ) );
701 }
702 }
703 $locations = array_unique( $locations );
704 sort( $locations );
705 return $locations;
706 }
707
708 // Work out which is the most relevant portion to display
709 // This is done by looping over each match and finding the smallest distance between two found
710 // strings. The idea being that the closer the terms are the better match the snippet would be.
711 // When checking for matches we only change the location if there is a better match.
712 // The only exception is where we have only two matches in which case we just take the
713 // first as will be equally distant.
714 private function get_best_match( $locations, $prevcount ) {
715 // If we only have 1 match we dont actually do the for loop so set to the first
716 $startpos = ( isset( $locations[0] ) ? $locations[0] : 0 );
717 $loccount = count( $locations );
718 $smallestdiff = PHP_INT_MAX;
719 // If we only have 2 skip as its probably equally relevant
720 if ( count( $locations ) > 2 ) {
721 // skip the first as we check 1 behind
722 for ($i = 1; $i < $loccount; $i++) {
723 if ( $i == $loccount - 1 ) {
724 // at the end
725 $diff = $locations[$i] - $locations[$i - 1];
726 } else {
727 $diff = $locations[$i + 1] - $locations[$i];
728 }
729 if ( $smallestdiff > $diff ) {
730 $smallestdiff = $diff;
731 $startpos = $locations[$i];
732 }
733 }
734 }
735 $startpos = ( $startpos > $prevcount ? $startpos - $prevcount : 0 );
736 return $startpos;
737 }
738
739 // 1/6 ratio on prevcount tends to work pretty well and puts the terms
740 // in the middle of the extract
741 public function get_snippet(
742 $terms,
743 $content,
744 $rellength = 300,
745 $prevcount = 50,
746 $indicator = '...'
747 ) {
748 $multibyte = $this->multibyte;
749 $textlength = ( $multibyte ? mb_strlen( $content ) : strlen( $content ) );
750 if ( $textlength <= $rellength ) {
751 return $content;
752 }
753 $locations = $this->get_terms_locations( $terms, $content, $multibyte );
754 $startpos = $this->get_best_match( $locations, $prevcount );
755 // if we are going to snip too much...
756 if ( $textlength - $startpos < $rellength ) {
757 $startpos = $startpos - ($textlength - $startpos) / 2;
758 }
759 $reltext = ( $multibyte ? mb_substr( $content, $startpos, $rellength ) : substr( $content, $startpos, $rellength ) );
760 // check to ensure we dont snip the last word if thats the match
761 if ( $startpos + $rellength < $textlength ) {
762 $last_word_pos = ( $multibyte ? mb_strrpos( $reltext, ' ' ) : strrpos( $reltext, ' ' ) );
763 if ( false !== $last_word_pos ) {
764 if ( $multibyte ) {
765 $reltext = mb_substr( $reltext, 0, mb_strrpos( $reltext, ' ' ) ) . $indicator;
766 // remove last word
767 } else {
768 $reltext = substr( $reltext, 0, strrpos( $reltext, ' ' ) ) . $indicator;
769 // remove last word
770 }
771 } else {
772 $reltext .= $indicator;
773 }
774 }
775 // If we trimmed from the front add ...
776 if ( 0 != $startpos ) {
777 if ( $multibyte ) {
778 $reltext = $indicator . mb_substr( $reltext, mb_strpos( $reltext, ' ' ) + 1 );
779 // remove first word
780 } else {
781 $reltext = $indicator . substr( $reltext, strpos( $reltext, ' ' ) + 1 );
782 // remove first word
783 }
784 }
785 return $reltext;
786 }
787
788 /**
789 * Renders search bar on shortcode
790 *
791 * @since 1.4.0
792 *
793 * @param $atts
794 * @return false|string|void
795 */
796 public function do_shortcode( $atts ) {
797 global $basepress_utils;
798 $kb_term = false;
799 $a = shortcode_atts( array(
800 'product' => '',
801 'kb' => '',
802 'width' => '',
803 ), $atts );
804 //Backwards compatibility with old products attribute
805 $kb = ( $a['kb'] ? $a['kb'] : $a['product'] );
806 if ( empty( $kb ) && $basepress_utils->get_option( 'single_product_mode' ) ) {
807 $kb_id = $basepress_utils->get_active_product_id();
808 $kb_term = get_term( $kb_id, 'knowledgebase_cat' );
809 $kb = ( is_a( $kb_term, 'WP_Term' ) ? $kb_term->slug : '' );
810 }
811 if ( !empty( $kb ) ) {
812 $kb_term = get_term_by( 'slug', $kb, 'knowledgebase_cat' );
813 }
814 $width = ( isset( $a['width'] ) && $a['width'] ? ' style="max-width:' . $a['width'] . '"' : '' );
815 ob_start();
816 $bpkb_knowledge_base = basepress_kb();
817 echo '<div class="bpress-search-shoOrtcode"' . esc_attr( $this->basepress_sanitize_width( $width ) ) . '>';
818 echo "<h2>" . esc_html( $bpkb_knowledge_base->name ) . "</h2>";
819 echo "<p class='bpdescription'>";
820 basepress_search_description();
821 echo "</p>";
822 $this->render_searchbar( $kb_term );
823 echo '</div>';
824 return ob_get_clean();
825 }
826
827 /**
828 * Sanitizes the width attribute for the search bar shortcode
829 *
830 * @since 2.17.1
831 *
832 * @param $width
833 * @return string
834 */
835 public function basepress_sanitize_width( $width ) {
836 // Allow only valid CSS width values: 100px, 80%, 40vw, auto
837 if ( preg_match( '/^(auto|\\d+(px|%|vw|vh|em|rem))$/', trim( $width ) ) ) {
838 return $width;
839 }
840 // Fallback
841 return '100%';
842 }
843
844 /**
845 * Enqueue the css file when search bar is rendered by shortcode
846 *
847 * @since 1.4.0
848 */
849 public function enqueue_shortcode_scripts() {
850 global $post, $basepress_utils;
851 $has_shortcode = ( isset( $post->post_content ) ? has_shortcode( $post->post_content, 'basepress-search' ) : false );
852 $has_block = ( function_exists( 'has_block' ) ? has_block( 'basepress-kb/searchbar-block', $post ) : false );
853 if ( is_singular() && ($has_shortcode || $has_block) ) {
854 $options = $basepress_utils->get_options();
855 $minimum_char_length = $options['search_min_chars'];
856 $minimum_char_length = ( $minimum_char_length ? $minimum_char_length : 3 );
857 $enqueue_style = isset( $options['searchbar_style'] );
858 /**
859 * This filter can be used to alter the enqueueing of styles
860 */
861 $enqueue_style = apply_filters( 'basepress_enqueue_shortcode_style', $enqueue_style );
862 if ( $enqueue_style ) {
863 wp_enqueue_style( 'basepress-styles' );
864 wp_enqueue_style( 'basepress-icons' );
865 }
866 wp_enqueue_script( 'basepress-js' );
867 wp_localize_script( 'basepress-js', 'basepress_vars', array(
868 'ajax_url' => admin_url( 'admin-ajax.php' ),
869 'basepress_ajax' => plugins_url() . '/basepress/public/smart-search.php',
870 'premium' => ( BASEPRESS_PLAN == 'premium' ? true : '' ),
871 'log_search' => isset( $options['activate_insights'] ),
872 'min_chars' => apply_filters( 'basepress_search_min_char_length', $minimum_char_length ),
873 ) );
874 }
875 }
876
877 /**
878 * Adds meta tags to prevent browsers caching search results pages
879 *
880 * @since 2.0.0
881 */
882 public function prevent_browser_caching() {
883 global $wp_query;
884 $is_search = is_search();
885 $is_kb_post_type = ( isset( $wp_query->query['post_type'] ) && 'knowledgebase' == $wp_query->query['post_type'] ? true : false );
886 $is_kb_tax = is_tax( 'knowledgebase_cat' );
887 if ( $is_search && ($is_kb_post_type || $is_kb_tax) ) {
888 ?>
889 <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
890 <meta http-equiv="Pragma" content="no-cache" />
891 <meta http-equiv="Expires" content="0" />
892 <?php
893 }
894 }
895
896 }
897
898 //Class End
899 global $basepress_search;
900 $basepress_search = new Basepress_Search();
901 }