PluginProbe
ElasticPress / 4.4.1
ElasticPress v4.4.1
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Feature / InstantResults / InstantResults.php

InstantResults.php in ElasticPress 4.4.1, at includes/classes/Feature/InstantResults/InstantResults.php

1,012 lines 28.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Instant Search feature
4 *
5 * @package elasticpress
6 */
7
8 namespace ElasticPress\Feature\InstantResults;
9
10 use ElasticPress\Elasticsearch as Elasticsearch;
11 use ElasticPress\Feature as Feature;
12 use ElasticPress\FeatureRequirementsStatus;
13 use ElasticPress\Features as Features;
14 use ElasticPress\Indexables as Indexables;
15 use ElasticPress\Utils as Utils;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit; // Exit if accessed directly.
19 }
20
21 /**
22 * Instant Results feature class.
23 *
24 * @since 4.0.0
25 */
26 class InstantResults extends Feature {
27 /**
28 * Elasticsearch index name.
29 *
30 * @var string
31 */
32 protected $index;
33
34 /**
35 * Host URL.
36 *
37 * @var string
38 */
39 protected $host;
40
41 /**
42 * WooCommerce is in use.
43 *
44 * @var boolean
45 */
46 protected $is_woocommerce;
47
48 /**
49 * Elasticsearch query template.
50 *
51 * @var string
52 */
53 protected $search_template = '';
54
55 /**
56 * Feature settings
57 *
58 * @var array
59 */
60 protected $settings = [];
61
62 /**
63 * Initialize feature.
64 *
65 * @return void
66 */
67 public function __construct() {
68 $this->slug = 'instant-results';
69
70 $this->title = $this->get_title();
71
72 $this->short_title = esc_html__( 'Instant Results', 'elasticpress' );
73
74 $this->summary = __( 'Search forms display results instantly after submission. A modal opens that populates results by querying ElasticPress directly.', 'elasticpress' );
75
76 $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#instant-results', 'elasticpress' );
77
78 $this->host = trailingslashit( Utils\get_host() );
79
80 $this->index = Indexables::factory()->get( 'post' )->get_index_name();
81
82 $this->is_woocommerce = function_exists( 'WC' );
83
84 $this->default_settings = [
85 'highlight_tag' => 'mark',
86 'facets' => 'post_type,category,post_tag',
87 'match_type' => 'all',
88 'term_count' => '1',
89 ];
90
91 $settings = $this->get_settings() ? $this->get_settings() : array();
92
93 $this->settings = wp_parse_args( $settings, $this->default_settings );
94
95 $this->requires_install_reindex = true;
96
97 $this->available_during_installation = true;
98
99 parent::__construct();
100 }
101
102 /**
103 * Output detailed feature description.
104 *
105 * @return void
106 */
107 public function output_feature_box_long() {
108 ?>
109 <p>
110 <?php
111 printf(
112 /* translators: %s: ElasticPress.io link. */
113 esc_html__( 'WordPress search forms will display results instantly. When the search query is submitted, a modal will open that populates results by querying ElasticPress directly, bypassing WordPress. As the user refines their search, results are refreshed. Requires an %s or a custom proxy to function.', 'elasticpress' ),
114 sprintf(
115 '<a href="%1$s" target="_blank">%2$s</a>',
116 'https://www.elasticpress.io/',
117 esc_html__( 'ElasticPress.io plan', 'elasticpress' )
118 )
119 );
120 ?>
121 </p>
122 <?php
123 }
124
125 /**
126 * Display feature settings.
127 *
128 * @return void
129 */
130 public function output_feature_box_settings() {
131 if ( ! $this->is_active() ) {
132 return;
133 }
134
135 $highlight_tags = array( 'mark', 'span', 'strong', 'em', 'i' );
136 ?>
137
138 <div class="field">
139 <label for="instant-results-highlight-tag" class="field-name status"><?php echo esc_html_e( 'Highlight tag ', 'elasticpress' ); ?></label>
140 <div class="input-wrap">
141 <select id="instant-results-highlight-tag" name="settings[highlight_tag]">
142 <option value=""><?php esc_html_e( 'None', 'elasticpress' ); ?></option>
143 <?php
144 foreach ( $highlight_tags as $highlight_tag ) {
145 printf(
146 '<option value="%1$s" %2$s>%3$s</option>',
147 esc_attr( $highlight_tag ),
148 selected( $this->settings['highlight_tag'], $highlight_tag, false ),
149 esc_html( $highlight_tag )
150 );
151 }
152 ?>
153 </select>
154 <p class="field-description"><?php esc_html_e( 'Highlight search terms in results with the selected HTML tag.', 'elasticpress' ); ?></p>
155 </div>
156 </div>
157 <div class="field">
158 <label for="feature_instant_results_facets" class="field-name status"><?php esc_html_e( 'Facets', 'elasticpress' ); ?></label>
159 <div class="input-wrap">
160 <input value="<?php echo esc_attr( $this->settings['facets'] ); ?>" type="text" name="settings[facets]" id="feature_instant_results_facets">
161 </div>
162 </div>
163 <div class="field">
164 <div class="field-name status"><?php esc_html_e( 'Match Type', 'elasticpress' ); ?></div>
165 <div class="input-wrap">
166 <label>
167 <input name="settings[match_type]" type="radio" <?php checked( $this->settings['match_type'], 'all' ); ?> value="all">
168 <?php echo wp_kses_post( __( 'Show any content tagged to <strong>all</strong> selected terms', 'elasticpress' ) ); ?>
169 </label><br>
170 <label>
171 <input name="settings[match_type]" type="radio" <?php checked( $this->settings['match_type'], 'any' ); ?> value="any">
172 <?php echo wp_kses_post( __( 'Show all content tagged to <strong>any</strong> selected term', 'elasticpress' ) ); ?>
173 </label>
174 <p class="field-description"><?php esc_html_e( '"All" will only show content that matches all facets. "Any" will show content that matches any facet.', 'elasticpress' ); ?></p>
175 </div>
176 </div>
177 <div class="field">
178 <div class="field-name status"><?php esc_html_e( 'Term Count', 'elasticpress' ); ?></div>
179 <div class="input-wrap">
180 <label>
181 <input name="settings[term_count]" <?php checked( (bool) $this->settings['term_count'] ); ?> type="radio" value="1"><?php esc_html_e( 'Enabled', 'elasticpress' ); ?>
182 </label><br>
183 <label>
184 <input name="settings[term_count]" <?php checked( ! (bool) $this->settings['term_count'] ); ?> type="radio" value="0"><?php esc_html_e( 'Disabled', 'elasticpress' ); ?>
185 </label>
186 <p class="field-description"><?php esc_html_e( 'When enabled, it will show the term count in the instant results widget.', 'elasticpress' ); ?></p>
187 </div>
188 </div>
189
190 <?php
191 }
192
193 /**
194 * Tell user whether requirements for feature are met or not.
195 *
196 * @return array $status Status array
197 */
198 public function requirements_status() {
199 $status = new FeatureRequirementsStatus( 2 );
200
201 $status->message = [];
202
203 if ( Utils\is_epio() ) {
204 $status->code = 1;
205
206 /**
207 * Whether the feature is available for non ElasticPress.io customers.
208 *
209 * Installations using self-hosted Elasticsearch will need to implement an API for
210 * handling search requests before making the feature available.
211 *
212 * @since 4.0.0
213 * @hook ep_instant_results_available
214 * @param {string} $available Whether the feature is available.
215 */
216 } elseif ( apply_filters( 'ep_instant_results_available', false ) ) {
217 $status->code = 1;
218 $status->message[] = esc_html__( 'You are using a custom proxy. Make sure you implement all security measures needed.', 'elasticpress' );
219 } else {
220 $status->message[] = wp_kses_post( __( "To use this feature you need to be an <a href='https://elasticpress.io'>ElasticPress.io</a> customer or implement a <a href='https://github.com/10up/elasticpress-proxy'>custom proxy</a>.", 'elasticpress' ) );
221 }
222
223 /**
224 * Display a warning if ElasticPress is network activated.
225 */
226 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
227 $status->message[] = wp_kses_post(
228 sprintf(
229 /* translators: Article URL */
230 __(
231 'ElasticPress is network activated. Additional steps are required to ensure Instant Results works for all sites on the network. See our article on <a href="%s" target="_blank">running ElasticPress in network mode</a> for more details.',
232 'elasticpress'
233 ),
234 'https://elasticpress.zendesk.com/hc/en-us/articles/10841087797901-Running-ElasticPress-in-a-WordPress-Multisite-Network-Mode-'
235 )
236 );
237 }
238
239 return $status;
240 }
241
242 /**
243 * Setup feature functionality.
244 *
245 * @return void
246 */
247 public function setup() {
248 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
249 add_filter( 'ep_after_update_feature', [ $this, 'after_update_feature' ], 10, 3 );
250 add_filter( 'ep_formatted_args', [ $this, 'maybe_apply_aggs_args' ], 10, 3 );
251 add_filter( 'ep_post_mapping', [ $this, 'add_mapping_properties' ] );
252 add_filter( 'ep_post_sync_args', [ $this, 'add_post_sync_args' ], 10, 2 );
253 add_filter( 'ep_after_sync_index', [ $this, 'epio_save_search_template' ] );
254 add_filter( 'ep_saved_weighting_configuration', [ $this, 'epio_save_search_template' ] );
255 add_filter( 'ep_bypass_exclusion_from_search', [ $this, 'maybe_bypass_post_exclusion' ], 10, 2 );
256 add_action( 'pre_get_posts', [ $this, 'maybe_apply_product_visibility' ] );
257 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_assets' ] );
258 add_action( 'wp_footer', [ $this, 'render' ] );
259 }
260
261 /**
262 * Output modal markup.
263 */
264 public function render() {
265 echo '<div id="ep-instant-results"></div>';
266 }
267
268 /**
269 * Enqueue our autosuggest script.
270 */
271 public function enqueue_frontend_assets() {
272 if ( Utils\is_indexing() ) {
273 return;
274 }
275
276 wp_enqueue_style(
277 'elasticpress-instant-results',
278 EP_URL . 'dist/css/instant-results-styles.css',
279 Utils\get_asset_info( 'instant-results-styles', 'dependencies' ),
280 Utils\get_asset_info( 'instant-results-styles', 'version' )
281 );
282
283 wp_enqueue_script(
284 'elasticpress-instant-results',
285 EP_URL . 'dist/js/instant-results-script.js',
286 Utils\get_asset_info( 'instant-results-script', 'dependencies' ),
287 Utils\get_asset_info( 'instant-results-script', 'version' ),
288 true
289 );
290
291 wp_set_script_translations( 'elasticpress-instant-results', 'elasticpress' );
292
293 /**
294 * The search API endpoint.
295 *
296 * @since 4.0.0
297 * @hook ep_instant_results_search_endpoint
298 * @param {string} $endpoint Endpoint path.
299 * @param {string} $index Elasticsearch index.
300 */
301 $api_endpoint = apply_filters( 'ep_instant_results_search_endpoint', "api/v1/search/posts/{$this->index}", $this->index );
302
303 wp_localize_script(
304 'elasticpress-instant-results',
305 'epInstantResults',
306 array(
307 'apiEndpoint' => $api_endpoint,
308 'apiHost' => ( 0 !== strpos( $api_endpoint, 'http' ) ) ? esc_url_raw( $this->host ) : '',
309 'argsSchema' => $this->get_args_schema(),
310 'currencyCode' => $this->is_woocommerce ? get_woocommerce_currency() : false,
311 'facets' => $this->get_facets_for_frontend(),
312 'highlightTag' => $this->settings['highlight_tag'],
313 'isWooCommerce' => $this->is_woocommerce,
314 'locale' => str_replace( '_', '-', get_locale() ),
315 'matchType' => $this->settings['match_type'],
316 'paramPrefix' => 'ep-',
317 'postTypeLabels' => $this->get_post_type_labels(),
318 'termCount' => $this->settings['term_count'],
319 )
320 );
321 }
322
323 /**
324 * Enqueue admin assets.
325 *
326 * @param string $hook_suffix The current admin page.
327 */
328 public function enqueue_admin_assets( $hook_suffix ) {
329 if ( 'toplevel_page_elasticpress' !== $hook_suffix ) {
330 return;
331 }
332
333 wp_enqueue_style( 'wp-edit-post' );
334
335 wp_enqueue_script(
336 'elasticpress-instant-results-admin',
337 EP_URL . 'dist/js/instant-results-admin-script.js',
338 Utils\get_asset_info( 'instant-results-admin-script', 'dependencies' ),
339 Utils\get_asset_info( 'instant-results-admin-script', 'version' ),
340 true
341 );
342
343 wp_set_script_translations( 'elasticpress-instant-results-admin', 'elasticpress' );
344
345 wp_localize_script(
346 'elasticpress-instant-results-admin',
347 'epInstantResultsAdmin',
348 array(
349 'facets' => $this->get_facets_for_admin(),
350 )
351 );
352 }
353
354 /**
355 * Save or delete the search template on ElasticPress.io based on whether
356 * the Instant Results feature is being activated or deactivated.
357 *
358 * @param string $feature Feature slug
359 * @param array $settings Feature settings
360 * @param array $data Feature activation data
361 *
362 * @return void
363 *
364 * @since 4.3.0
365 */
366 public function after_update_feature( $feature, $settings, $data ) {
367 if ( $feature !== $this->slug ) {
368 return;
369 }
370
371 if ( true === $data['active'] ) {
372 $this->epio_save_search_template();
373 } else {
374 $this->epio_delete_search_template();
375 }
376 }
377
378 /**
379 * Get the endpoint for the Instant Results search template.
380 *
381 * @return string Instant Results search template endpoint.
382 */
383 public function get_template_endpoint() {
384 /**
385 * Filters the search template API endpoint.
386 *
387 * @since 4.0.0
388 * @hook ep_instant_results_template_endpoint
389 * @param {string} $endpoint Endpoint path.
390 * @param {string} $index Elasticsearch index.
391 * @returns {string} Search template API endpoint.
392 */
393 return apply_filters( 'ep_instant_results_template_endpoint', "api/v1/search/posts/{$this->index}/template/", $this->index );
394 }
395
396 /**
397 * Save the search template to ElasticPress.io.
398 *
399 * @return void
400 */
401 public function epio_save_search_template() {
402 $endpoint = $this->get_template_endpoint();
403 $template = $this->get_search_template();
404
405 Elasticsearch::factory()->remote_request(
406 $endpoint,
407 [
408 'blocking' => false,
409 'body' => $template,
410 'method' => 'PUT',
411 ]
412 );
413
414 /**
415 * Fires after the request is sent the search template API endpoint.
416 *
417 * @since 4.0.0
418 * @hook ep_instant_results_template_saved
419 * @param {string} $template The search template (JSON).
420 * @param {string} $index Index name.
421 */
422 do_action( 'ep_instant_results_template_saved', $template, $this->index );
423 }
424
425 /**
426 * Delete the search template from ElasticPress.io.
427 *
428 * @return void
429 *
430 * @since 4.3.0
431 */
432 public function epio_delete_search_template() {
433 $endpoint = $this->get_template_endpoint();
434
435 Elasticsearch::factory()->remote_request(
436 $endpoint,
437 [
438 'blocking' => false,
439 'method' => 'DELETE',
440 ]
441 );
442
443 /**
444 * Fires after the request is sent the search template API endpoint.
445 *
446 * @since 4.3.0
447 * @hook ep_instant_results_template_deleted
448 * @param {string} $index Index name.
449 */
450 do_action( 'ep_instant_results_template_deleted', $this->index );
451 }
452
453 /**
454 * Get the saved search template from ElasticPress.io.
455 *
456 * @return string|WP_Error Search template if found, WP_Error on error.
457 *
458 * @since 4.4.0
459 */
460 public function epio_get_search_template() {
461 $endpoint = $this->get_template_endpoint();
462 $request = Elasticsearch::factory()->remote_request( $endpoint );
463
464 if ( is_wp_error( $request ) ) {
465 return $request;
466 }
467
468 $response = wp_remote_retrieve_body( $request );
469
470 return $response;
471 }
472
473 /**
474 * Generate a search template.
475 *
476 * A search template is the JSON for an Elasticsearch query with a
477 * placeholder search term. The template is sent to ElasticPress.io where
478 * it's used to make Elasticsearch queries using search terms sent from
479 * the front end.
480 *
481 * @return string The search template as JSON.
482 */
483 public function get_search_template() {
484 $post_types = Features::factory()->get_registered_feature( 'search' )->get_searchable_post_types();
485 $post_statuses = get_post_stati(
486 [
487 'public' => true,
488 'exclude_from_search' => false,
489 ]
490 );
491
492 /**
493 * The ID of the current user when generating the Instant Results
494 * search template.
495 *
496 * By default Instant Results sets the current user as anomnymous when
497 * generating the search template, so that any filters applied to
498 * queries for logged-in or specific users are not applied to the
499 * template. This filter supports setting a specific user as the
500 * current user while the template is generated.
501 *
502 * @since 4.1.0
503 * @hook ep_search_template_user_id
504 * @param {int} $user_id User ID to use.
505 * @return {int} New user ID to use.
506 */
507 $template_user_id = apply_filters( 'ep_search_template_user_id', 0 );
508 $original_user_id = get_current_user_id();
509
510 wp_set_current_user( $template_user_id );
511
512 add_filter( 'ep_intercept_remote_request', '__return_true' );
513 add_filter( 'ep_do_intercept_request', [ $this, 'intercept_search_request' ], 10, 4 );
514 add_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request' ], 10, 2 );
515
516 $query = new \WP_Query(
517 array(
518 'ep_integrate' => true,
519 'ep_search_template' => true,
520 'post_status' => array_values( $post_statuses ),
521 'post_type' => $post_types,
522 's' => '{{ep_placeholder}}',
523 )
524 );
525
526 remove_filter( 'ep_intercept_remote_request', '__return_true' );
527 remove_filter( 'ep_do_intercept_request', [ $this, 'intercept_search_request' ], 10 );
528 remove_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request' ], 10 );
529
530 wp_set_current_user( $original_user_id );
531
532 return $this->search_template;
533 }
534
535 /**
536 * Return true if a given feature is supported by Instant Results.
537 *
538 * Applied as a filter on Utils\is_integrated_request() so that features
539 * are enabled for the query that is used to generate the search template,
540 * regardless of the request type. This avoids the need to send a request
541 * to the front end.
542 *
543 * @param bool $is_integrated Whether queries for the request will be
544 * integrated.
545 * @param string $context Context for the original check. Usually the
546 * slug of the feature doing the check.
547 * @return bool True if the check is for a feature supported by instant
548 * search.
549 */
550 public function is_integrated_request( $is_integrated, $context ) {
551 $supported_contexts = [
552 'autosuggest',
553 'documents',
554 'search',
555 'weighting',
556 'woocommerce',
557 ];
558
559 return in_array( $context, $supported_contexts, true );
560 }
561
562 /**
563 * Store intercepted request body and return request result.
564 *
565 * @param object $response Response
566 * @param array $query Query
567 * @param array $args WP_Query argument array
568 * @param int $failures Count of failures in request loop
569 * @return object $response Response
570 */
571 public function intercept_search_request( $response, $query = [], $args = [], $failures = 0 ) {
572 $this->search_template = $query['args']['body'];
573
574 return wp_remote_request( $query['url'], $args );
575 }
576
577 /**
578 * If generating the search template query, do not bypass the post exclusion
579 *
580 * @since 4.4.0
581 * @param bool $bypass_exclusion_from_search Whether the post exclusion from search should be applied or not
582 * @param WP_Query $query The WP Query
583 * @return bool
584 */
585 public function maybe_bypass_post_exclusion( $bypass_exclusion_from_search, $query ) {
586 return true === $query->get( 'ep_search_template' ) ?
587 false : // not bypass, apply
588 $bypass_exclusion_from_search;
589 }
590
591 /**
592 * Apply product visibility taxonomy query to search template queries.
593 *
594 * @param \WP_Query $query Query instance.
595 * @return void
596 */
597 public function maybe_apply_product_visibility( $query ) {
598 if ( true !== $query->get( 'ep_search_template' ) ) {
599 return;
600 }
601
602 if ( ! $this->is_woocommerce ) {
603 return;
604 }
605
606 $this->apply_product_visibility( $query );
607 }
608
609 /**
610 * Apply product visibility taxonomy query.
611 *
612 * Applies filters to exclude products set to be excluded from search. Out
613 * of stock products will also be excluded if WooCommerce is configured to
614 * hide those products.
615 *
616 * Mimics the logic of WC_Query::get_tax_query().
617 *
618 * @param \WP_Query $query Query instance.
619 * @return void
620 */
621 public function apply_product_visibility( $query ) {
622 $product_visibility_terms = wc_get_product_visibility_term_ids();
623 $product_visibility_not_in = (array) $product_visibility_terms['exclude-from-search'];
624
625 if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
626 $product_visibility_not_in[] = $product_visibility_terms['outofstock'];
627 }
628
629 if ( ! empty( $product_visibility_not_in ) ) {
630 $tax_query = $query->get( 'tax_query', array() );
631
632 $tax_query[] = array(
633 'taxonomy' => 'product_visibility',
634 'field' => 'term_taxonomy_id',
635 'terms' => $product_visibility_not_in,
636 'operator' => 'NOT IN',
637 );
638
639 $query->set( 'tax_query', $tax_query );
640 }
641 }
642
643 /**
644 * Apply aggregation args to search templates.
645 *
646 * @param array $formatted_args Formatted Elasticsearch query.
647 * @param array $query_vars Query variables
648 * @param \WP_Query $query Query instance.
649 * @return array Formatted Elasticsearch query.
650 */
651 public function maybe_apply_aggs_args( $formatted_args, $query_vars, $query ) {
652 if ( true !== $query->get( 'ep_search_template' ) ) {
653 return $formatted_args;
654 }
655
656 return $this->apply_aggs_args( $formatted_args );
657 }
658
659 /**
660 * Add aggregation args to Elasticsearch query for facets.
661 *
662 * @param array $formatted_args Formatted Elasticsearch query.
663 * @return array Formatted Elasticsearch query.
664 */
665 public function apply_aggs_args( $formatted_args ) {
666 $filter = $formatted_args['post_filter'];
667 $facets = $this->get_facets();
668
669 foreach ( $facets as $key => $facet ) {
670 $formatted_args['aggs'][ $key ]['aggs'] = $facet['aggs'];
671
672 if ( $filter ) {
673 $formatted_args['aggs'][ $key ]['filter'] = $filter;
674 }
675 }
676
677 return $formatted_args;
678 }
679
680 /**
681 * Add additional fields to post mapping.
682 *
683 * @param array $mapping Post mapping.
684 * @return array Post mapping.
685 */
686 public function add_mapping_properties( $mapping ) {
687 $elasticsearch_version = Elasticsearch::factory()->get_elasticsearch_version();
688
689 $properties = array(
690 'post_content_plain' => array( 'type' => 'text' ),
691 'price_html' => array( 'type' => 'text' ),
692 );
693
694 if ( version_compare( $elasticsearch_version, '7.0', '<' ) ) {
695 $mapping['mappings']['post']['properties'] = array_merge(
696 $mapping['mappings']['post']['properties'],
697 $properties
698 );
699 } else {
700 $mapping['mappings']['properties'] = array_merge(
701 $mapping['mappings']['properties'],
702 $properties
703 );
704 }
705
706 return $mapping;
707 }
708
709 /**
710 * Add data for additional mapping properties.
711 *
712 * @param array $post_args Post arguments.
713 * @param integer $post_id Post ID.
714 * @return array Post sync args.
715 */
716 public function add_post_sync_args( $post_args, $post_id ) {
717 $post = get_post( $post_id );
718
719 $post_args['post_content_plain'] = $this->prepare_plain_content_arg( $post );
720 $post_args['price_html'] = $this->prepare_price_html_arg( $post );
721
722 return $post_args;
723 }
724
725
726 /**
727 * Get data for the plain post content.
728 *
729 * @param WP_Post $post Post object.
730 * @return string Post content.
731 */
732 public function prepare_plain_content_arg( $post ) {
733 $post_content = apply_filters( 'the_content', $post->post_content );
734
735 return wp_strip_all_tags( $post_content );
736 }
737
738 /**
739 * Get data for the price HTML arg.
740 *
741 * @param WP_Post $post Post object.
742 * @return string|null Price HTML.
743 */
744 public function prepare_price_html_arg( $post ) {
745 if ( 'product' !== $post->post_type ) {
746 return null;
747 }
748
749 if ( ! $this->is_woocommerce ) {
750 return null;
751 }
752
753 $product = wc_get_product( $post );
754
755 return $product->get_price_html();
756 }
757
758 /**
759 * Get post type labels.
760 *
761 * Only the post type slug is indexed, so we'll need the labels on the
762 * front end for display.
763 *
764 * @return array Array of post types and their labels.
765 */
766 public function get_post_type_labels() {
767 $labels = [];
768
769 $post_types = Features::factory()->get_registered_feature( 'search' )->get_searchable_post_types();
770
771 foreach ( $post_types as $post_type ) {
772 $post_type_object = get_post_type_object( $post_type );
773 $post_type_labels = get_post_type_labels( $post_type_object );
774
775 $labels[ $post_type ] = array(
776 'plural' => $post_type_labels->name,
777 'singular' => $post_type_labels->singular_name,
778 );
779 }
780
781 return $labels;
782 }
783
784 /**
785 * Get available facets.
786 *
787 * @return array Available facets.
788 */
789 public function get_facets() {
790 $facets = [];
791
792 /**
793 * Post type facet.
794 */
795 $facets['post_type'] = array(
796 'type' => 'post_type',
797 'post_types' => [],
798 'labels' => array(
799 'admin' => __( 'Post type', 'elasticpress' ),
800 'frontend' => __( 'Type', 'elasticpress' ),
801 ),
802 'aggs' => array(
803 'post_type' => array(
804 'terms' => array(
805 'field' => 'post_type.raw',
806 ),
807 ),
808 ),
809 /**
810 * The post_type arg needs to be supported regardless of whether
811 * the Post Type facet is present to be able to support setting the
812 * post type from the search form.
813 *
814 * @see ElasticPress\Feature\InstantResults::get_args_schema()
815 */
816 'args' => array(),
817 );
818
819 /**
820 * Taxonomy facets.
821 */
822 $taxonomies = get_taxonomies( array( 'public' => true ), 'object' );
823 $taxonomies = apply_filters( 'ep_facet_include_taxonomies', $taxonomies );
824
825 foreach ( $taxonomies as $slug => $taxonomy ) {
826 $name = 'tax-' . $slug;
827 $labels = get_taxonomy_labels( $taxonomy );
828
829 $admin_label = sprintf(
830 /* translators: $1$s: Taxonomy name. %2$s: Taxonomy slug. */
831 esc_html__( '%1$s (%2$s)' ),
832 $labels->singular_name,
833 $slug
834 );
835
836 $facets[ $name ] = array(
837 'type' => 'taxonomy',
838 'post_types' => $taxonomy->object_type,
839 'labels' => array(
840 'admin' => $admin_label,
841 'frontend' => $labels->singular_name,
842 ),
843 'aggs' => array(
844 $name => array(
845 'terms' => array(
846 'field' => 'terms.' . $slug . '.facet',
847 'size' => apply_filters( 'ep_facet_taxonomies_size', 10000, $taxonomy ),
848 ),
849 ),
850 ),
851 'args' => array(
852 $name => array(
853 'type' => 'strings',
854 ),
855 ),
856 );
857 }
858
859 /**
860 * Price facet.
861 */
862 if ( $this->is_woocommerce ) {
863 $facets['price_range'] = array(
864 'type' => 'price_range',
865 'post_types' => [ 'product' ],
866 'labels' => array(
867 'admin' => __( 'Price range', 'elasticpress' ),
868 'frontend' => __( 'Price', 'elasticpress' ),
869 ),
870 'aggs' => array(
871 'max_price' => array(
872 'max' => array(
873 'field' => 'meta._price.double',
874 ),
875 ),
876 'min_price' => array(
877 'min' => array(
878 'field' => 'meta._price.double',
879 ),
880 ),
881 ),
882 'args' => array(
883 'max_price' => array(
884 'type' => 'number',
885 ),
886 'min_price' => array(
887 'type' => 'number',
888 ),
889 ),
890 );
891 }
892
893 return $facets;
894 }
895
896 /**
897 * Get facet configuration for the front end.
898 *
899 * @return Array Facet configuration for the front end.
900 */
901 public function get_facets_for_frontend() {
902 $selected_facets = explode( ',', $this->settings['facets'] );
903 $available_facets = $this->get_facets();
904
905 $facets = [];
906
907 foreach ( $selected_facets as $key ) {
908 if ( isset( $available_facets[ $key ] ) ) {
909 $facet = $available_facets[ $key ];
910
911 $facets[] = array(
912 'name' => $key,
913 'label' => $facet['labels']['frontend'],
914 'type' => $facet['type'],
915 'postTypes' => $facet['post_types'],
916 );
917 }
918 }
919
920 return $facets;
921 }
922
923 /**
924 * Get facet configuration for the admin.
925 *
926 * @return Array Facet configuration for the admin.
927 */
928 public function get_facets_for_admin() {
929 $available_facets = $this->get_facets();
930
931 $facets = [];
932
933 foreach ( $available_facets as $key => $facet ) {
934 $facets[ $key ] = array(
935 'label' => $facet['labels']['admin'],
936 );
937 }
938
939 return $facets;
940 }
941
942 /**
943 * Get schema for search args.
944 *
945 * @return array Search args schema.
946 */
947 public function get_args_schema() {
948 $args = array(
949 'highlight' => array(
950 'type' => 'string',
951 'default' => $this->settings['highlight_tag'],
952 'allowedValues' => [ $this->settings['highlight_tag'] ],
953 ),
954 'offset' => array(
955 'type' => 'number',
956 'default' => 0,
957 ),
958 'orderby' => array(
959 'type' => 'string',
960 'default' => 'relevance',
961 'allowedValues' => [ 'date', 'price', 'relevance' ],
962 ),
963 'order' => array(
964 'type' => 'string',
965 'default' => 'desc',
966 'allowedValues' => [ 'asc', 'desc' ],
967 ),
968 'per_page' => array(
969 'type' => 'number',
970 'default' => 6,
971 ),
972 'post_type' => array(
973 'type' => 'strings',
974 ),
975 'search' => array(
976 'type' => 'string',
977 'default' => '',
978 ),
979 'relation' => array(
980 'type' => 'string',
981 'default' => 'and',
982 'allowedValues' => [ 'and', 'or' ],
983 ),
984 );
985
986 $selected_facets = explode( ',', $this->settings['facets'] );
987 $available_facets = $this->get_facets();
988
989 foreach ( $selected_facets as $key ) {
990 if ( isset( $available_facets[ $key ] ) ) {
991 $args = array_merge( $args, $available_facets[ $key ]['args'] );
992 }
993 }
994 return $args;
995 }
996
997 /**
998 * Returns the title.
999 *
1000 * @since 4.4.1
1001 * @return string
1002 */
1003 public function get_title() : string {
1004 if ( ! Utils\is_epio() ) {
1005 return esc_html__( 'Instant Results', 'elasticpress' );
1006 }
1007
1008 /* translators: 1. elasticpress.io logo; */
1009 return sprintf( esc_html__( 'Instant Results By %s', 'elasticpress' ), $this->get_epio_logo() );
1010 }
1011 }
1012