esc_url_raw( rest_url( 'super-ajax-search/v1/search' ) ), 'restNonce' => wp_create_nonce( 'wp_rest' ), 'noResultsText' => esc_html( get_option( 'super_ajax_search_no_results_text', __( 'No results found', 'super-ajax-search' ) ) ), ) ); } /** * Register REST API route. * * @return void */ public function register_rest_routes(): void { register_rest_route( 'super-ajax-search/v1', '/search', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'rest_search_callback' ), 'permission_callback' => '__return_true', 'args' => array( 's' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'category' => array( 'required' => false, 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint', ), ), ) ); } /** * REST search callback. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public function rest_search_callback( \WP_REST_Request $request ): \WP_REST_Response { $query = $request->get_param( 's' ); $category = (int) $request->get_param( 'category' ); $result = $this->perform_search( $query, $category ); if ( is_wp_error( $result ) ) { return new \WP_REST_Response( array( 'message' => $result->get_error_message() ), 400 ); } return new \WP_REST_Response( $result, 200 ); } /** * Legacy AJAX handler. * * @return void */ public function handle_ajax_search(): void { check_ajax_referer( 'super_ajax_search_nonce', 'nonce' ); $query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : ''; $category = isset( $_POST['category'] ) ? absint( $_POST['category'] ) : 0; $result = $this->perform_search( $query, $category ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success( $result ); } /** * Core search logic — shared by REST and AJAX. * * @param string $query Search string. * @param int $category Optional category ID. * @return array|\WP_Error */ private function perform_search( string $query, int $category = 0 ): array|\WP_Error { $query = trim( $query ); if ( empty( $query ) || mb_strlen( $query ) < 2 ) { return new \WP_Error( 'query_too_short', __( 'Please enter at least 2 characters.', 'super-ajax-search' ) ); } // Transient cache. $cache_ttl = absint( get_option( 'super_ajax_search_cache_duration', 3600 ) ); $cache_key = 'sas_' . md5( $query . '|' . $category ); if ( $cache_ttl > 0 ) { $cached = get_transient( $cache_key ); if ( false !== $cached ) { return $cached; } } // Settings. $post_types = get_option( 'super_ajax_search_post_types', array( 'post' ) ); $post_types = is_array( $post_types ) && ! empty( $post_types ) ? $post_types : array( 'post' ); $limit = absint( get_option( 'super_ajax_search_limit', 5 ) ); $show_thumbnail = '1' === get_option( 'super_ajax_search_show_thumbnail', '1' ); $show_excerpt = '1' === get_option( 'super_ajax_search_show_excerpt', '1' ); $group_by = sanitize_key( get_option( 'super_ajax_search_group_by', 'none' ) ); $args = array( 's' => $query, 'post_type' => array_map( 'sanitize_key', $post_types ), 'posts_per_page' => min( $limit, 50 ), 'post_status' => 'publish', 'no_found_rows' => true, ); if ( $category > 0 ) { $args['cat'] = $category; } $wp_query = new \WP_Query( $args ); $items = array(); if ( $wp_query->have_posts() ) { while ( $wp_query->have_posts() ) { $wp_query->the_post(); $post_id = get_the_ID(); $type_obj = get_post_type_object( get_post_type() ); $item = array( 'title' => esc_html( get_the_title() ), 'url' => esc_url( get_permalink() ), 'post_type' => get_post_type(), 'post_type_label' => $type_obj ? esc_html( $type_obj->labels->singular_name ) : get_post_type(), ); if ( $show_thumbnail ) { $thumb = get_the_post_thumbnail_url( $post_id, 'thumbnail' ); $item['thumbnail'] = $thumb ? esc_url( $thumb ) : ''; } if ( $show_excerpt ) { $item['excerpt'] = $this->build_smart_excerpt( get_the_content( '', false ), $query ); } if ( 'category' === $group_by ) { $cats = get_the_category( $post_id ); $item['category'] = ! empty( $cats ) ? esc_html( $cats[0]->name ) : esc_html__( 'Uncategorized', 'super-ajax-search' ); } $items[] = $item; } } wp_reset_postdata(); $grouped = $this->group_results( $items, $group_by ); if ( $cache_ttl > 0 ) { set_transient( $cache_key, $grouped, $cache_ttl ); } return $grouped; } /** * Build a short excerpt with the search keyword highlighted in bold. * * @param string $content Raw post content. * @param string $keyword Search keyword. * @param int $length Max excerpt length. * @return string Safe HTML with highlights. */ private function build_smart_excerpt( string $content, string $keyword, int $length = 140 ): string { $content = wp_strip_all_tags( strip_shortcodes( $content ) ); if ( empty( $content ) ) { return ''; } $pos = mb_stripos( $content, $keyword ); if ( false !== $pos ) { $start = max( 0, $pos - 50 ); $excerpt = mb_substr( $content, $start, $length ); if ( $start > 0 ) { $excerpt = '…' . $excerpt; } } else { $excerpt = mb_substr( $content, 0, $length ); } $excerpt = trim( $excerpt ); if ( mb_strlen( $content ) > $length ) { $excerpt .= '…'; } // Escape first, THEN highlight — prevents XSS while allowing . $safe = esc_html( $excerpt ); return preg_replace( '/(' . preg_quote( esc_html( $keyword ), '/' ) . ')/iu', '$1', $safe ); } /** * Group items by post_type or category. * * @param array $items Flat result items. * @param string $group_by 'none', 'post_type', or 'category'. * @return array */ private function group_results( array $items, string $group_by ): array { if ( 'none' === $group_by || empty( $items ) ) { return array( 'groups' => array( array( 'label' => '', 'items' => $items ) ) ); } $key = ( 'post_type' === $group_by ) ? 'post_type_label' : 'category'; $buckets = array(); foreach ( $items as $item ) { $label = $item[ $key ] ?? __( 'Other', 'super-ajax-search' ); $buckets[ $label ][] = $item; } $groups = array(); foreach ( $buckets as $label => $bucket ) { $groups[] = array( 'label' => $label, 'items' => $bucket ); } return array( 'groups' => $groups ); } /** * Render the [super_ajax_search] shortcode. * * @param array|string $atts Shortcode attributes. * @return string */ public function render_search_form( $atts = array() ): string { $atts = shortcode_atts( array( 'category_filter' => get_option( 'super_ajax_search_enable_category_filter', '0' ), 'placeholder' => get_option( 'super_ajax_search_placeholder', __( 'Search here…', 'super-ajax-search' ) ), ), $atts, 'super_ajax_search' ); self::$instance_count++; $show_filter = in_array( $atts['category_filter'], array( '1', 'true', 'yes' ), true ); ob_start(); ?>
esc_like( '_transient_sas_' ) . '%'; $like_o = $wpdb->esc_like( '_transient_timeout_sas_' ) . '%'; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $like_t, $like_o ) ); } }