PluginProbe
ElasticPress / 4.7.2
ElasticPress v4.7.2
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 / Autosuggest / Autosuggest.php

Autosuggest.php in ElasticPress 4.7.2, at includes/classes/Feature/Autosuggest/Autosuggest.php

871 lines 27.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Autosuggest feature
4 *
5 * phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
6 *
7 * @package elasticpress
8 */
9
10 namespace ElasticPress\Feature\Autosuggest;
11
12 use ElasticPress\Elasticsearch;
13 use ElasticPress\Feature;
14 use ElasticPress\FeatureRequirementsStatus;
15 use ElasticPress\Features;
16 use ElasticPress\Indexables;
17 use ElasticPress\Utils;
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit; // Exit if accessed directly.
21 }
22
23 /**
24 * Autosuggest feature class
25 */
26 class Autosuggest extends Feature {
27
28 /**
29 * Autosuggest query generated by intercept_search_request
30 *
31 * @var array
32 */
33 public $autosuggest_query = [];
34
35 /**
36 * Initialize feature setting it's config
37 *
38 * @since 3.0
39 */
40 public function __construct() {
41 $this->slug = 'autosuggest';
42
43 $this->title = $this->get_title();
44
45 $this->short_title = esc_html__( 'Autosuggest', 'elasticpress' );
46
47 $this->summary = __( 'Suggest relevant content as text is entered into the search field.', 'elasticpress' );
48
49 $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#autosuggest', 'elasticpress' );
50
51 $this->requires_install_reindex = true;
52
53 $this->default_settings = [
54 'endpoint_url' => '',
55 'autosuggest_selector' => '',
56 'trigger_ga_event' => '0',
57 ];
58
59 $this->available_during_installation = true;
60
61 parent::__construct();
62 }
63
64 /**
65 * Output feature box long
66 *
67 * @since 2.4
68 */
69 public function output_feature_box_long() {
70 ?>
71 <p><?php esc_html_e( 'Input fields of type "search" or with the CSS class "search-field" or "ep-autosuggest" will be enhanced with autosuggest functionality. As text is entered into the search field, suggested content will appear below it, based on top search results for the text. Suggestions link directly to the content.', 'elasticpress' ); ?></p>
72 <?php
73 }
74
75 /**
76 * Setup feature functionality
77 *
78 * @since 2.4
79 */
80 public function setup() {
81 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
82 add_filter( 'ep_post_mapping', [ $this, 'mapping' ] );
83 add_filter( 'ep_post_sync_args', [ $this, 'filter_term_suggest' ], 10 );
84 add_filter( 'ep_post_fuzziness_arg', [ $this, 'set_fuzziness' ], 10, 3 );
85 add_filter( 'ep_weighted_query_for_post_type', [ $this, 'adjust_fuzzy_fields' ], 10, 3 );
86 add_filter( 'ep_saved_weighting_configuration', [ $this, 'epio_send_autosuggest_public_request' ] );
87 add_filter( 'wp', [ $this, 'epio_send_autosuggest_allowed' ] );
88 add_filter( 'ep_pre_sync_index', [ $this, 'epio_send_autosuggest_public_request' ] );
89 }
90
91 /**
92 * Display decaying settings on dashboard.
93 *
94 * @since 2.4
95 */
96 public function output_feature_box_settings() {
97 $settings = $this->get_settings();
98 ?>
99 <div class="field">
100 <div class="field-name status"><label for="feature_autosuggest_selector"><?php esc_html_e( 'Autosuggest Selector', 'elasticpress' ); ?></label></div>
101 <div class="input-wrap">
102 <input value="<?php echo empty( $settings['autosuggest_selector'] ) ? '.ep-autosuggest' : esc_attr( $settings['autosuggest_selector'] ); ?>" type="text" name="settings[autosuggest_selector]" id="feature_autosuggest_selector">
103 <p class="field-description"><?php esc_html_e( 'Input additional selectors where you would like to include autosuggest separated by a comma. Example: .custom-selector, #custom-id, input[type="text"]', 'elasticpress' ); ?></p>
104 </div>
105 </div>
106
107 <div class="field">
108 <div class="field-name status"><?php esc_html_e( 'Google Analytics Events', 'elasticpress' ); ?></div>
109 <div class="input-wrap">
110 <label><input name="settings[trigger_ga_event]" <?php checked( (bool) $settings['trigger_ga_event'] ); ?> type="radio" value="1"><?php esc_html_e( 'Enabled', 'elasticpress' ); ?></label><br>
111 <label><input name="settings[trigger_ga_event]" <?php checked( ! (bool) $settings['trigger_ga_event'] ); ?> type="radio" value="0"><?php esc_html_e( 'Disabled', 'elasticpress' ); ?></label>
112 <p class="field-description"><?php esc_html_e( 'When enabled, a gtag tracking event is fired when an autosuggest result is clicked.', 'elasticpress' ); ?></p>
113 </div>
114 </div>
115 <?php
116
117 if ( Utils\is_epio() ) {
118 $this->epio_allowed_parameters();
119 return;
120 }
121
122 $endpoint_url = ( defined( 'EP_AUTOSUGGEST_ENDPOINT' ) && EP_AUTOSUGGEST_ENDPOINT ) ? EP_AUTOSUGGEST_ENDPOINT : $settings['endpoint_url'];
123 ?>
124
125 <div class="field">
126 <div class="field-name status"><label for="feature_autosuggest_endpoint_url"><?php esc_html_e( 'Endpoint URL', 'elasticpress' ); ?></label></div>
127 <div class="input-wrap">
128 <input <?php disabled( defined( 'EP_AUTOSUGGEST_ENDPOINT' ) && EP_AUTOSUGGEST_ENDPOINT ); ?> value="<?php echo esc_url( $endpoint_url ); ?>" type="text" name="settings[endpoint_url]" id="feature_autosuggest_endpoint_url">
129
130 <?php if ( defined( 'EP_AUTOSUGGEST_ENDPOINT' ) && EP_AUTOSUGGEST_ENDPOINT ) : ?>
131 <p class="field-description"><?php esc_html_e( 'Your autosuggest endpoint is set in wp-config.php', 'elasticpress' ); ?></p>
132 <?php endif; ?>
133
134 <p class="field-description"><?php esc_html_e( 'This address will be exposed to the public.', 'elasticpress' ); ?></p>
135 </div>
136 </div>
137
138 <?php
139 }
140
141 /**
142 * Add mapping for suggest fields
143 *
144 * @param array $mapping ES mapping.
145 * @since 2.4
146 * @return array
147 */
148 public function mapping( $mapping ) {
149 $post_indexable = Indexables::factory()->get( 'post' );
150
151 $mapping = $post_indexable->add_ngram_analyzer( $mapping );
152 $mapping = $post_indexable->add_term_suggest_field( $mapping );
153
154 // Note the assignment by reference below.
155 if ( version_compare( (string) Elasticsearch::factory()->get_elasticsearch_version(), '7.0', '<' ) ) {
156 $mapping_properties = &$mapping['mappings']['post']['properties'];
157 } else {
158 $mapping_properties = &$mapping['mappings']['properties'];
159 }
160
161 $text_type = $mapping_properties['post_content']['type'];
162
163 $mapping_properties['post_title']['fields']['suggest'] = array(
164 'type' => $text_type,
165 'analyzer' => 'edge_ngram_analyzer',
166 'search_analyzer' => 'standard',
167 );
168
169 return $mapping;
170 }
171
172 /**
173 * Ensure both search and autosuggest use fuziness with type auto
174 *
175 * @param integer $fuzziness Fuzziness
176 * @param array $search_fields Search Fields
177 * @param array $args Array of ES args
178 * @return array
179 */
180 public function set_fuzziness( $fuzziness, $search_fields, $args ) {
181 if ( Utils\is_integrated_request( $this->slug, [ 'public' ] ) && ! empty( $args['s'] ) ) {
182 return 'auto';
183 }
184 return $fuzziness;
185 }
186
187 /**
188 * Handle ngram search fields for fuzziness fields
189 *
190 * @param array $query ES Query arguments
191 * @param string $post_type Post Type
192 * @param array $args WP_Query args
193 * @return array $query adjusted ES Query arguments
194 */
195 public function adjust_fuzzy_fields( $query, $post_type, $args ) {
196 if ( ! Utils\is_integrated_request( $this->slug, [ 'public' ] ) || empty( $args['s'] ) ) {
197 return $query;
198 }
199
200 if ( ! isset( $query['bool'] ) || ! isset( $query['bool']['must'] ) ) {
201 return $query;
202 }
203
204 /**
205 * Filter autosuggest ngram fields
206 *
207 * @hook ep_autosuggest_ngram_fields
208 * @param {array} $fields Fields available to ngram
209 * @return {array} New fields array
210 */
211 $ngram_fields = apply_filters(
212 'ep_autosuggest_ngram_fields',
213 [
214 'post_title' => 'post_title.suggest',
215 'terms\.(.+)\.name' => 'term_suggest',
216 ]
217 );
218
219 /**
220 * At this point, `$query` might look like this (using the 3.5 search algorithm):
221 *
222 * [
223 * [bool] => [
224 * [must] => [
225 * [0] => [
226 * [bool] => [
227 * [should] => [
228 * [0] => [
229 * [multi_match] => [
230 * [query] => ep_autosuggest_placeholder
231 * [type] => phrase
232 * [fields] => [
233 * [0] => post_title^1
234 * ...
235 * [n] => terms.category.name^27
236 * ]
237 * [boost] => 3
238 * ]
239 * ]
240 * [1] => [
241 * [multi_match] => [
242 * [query] => ep_autosuggest_placeholder
243 * [fields] => [ ... ]
244 * [type] => phrase
245 * [slop] => 5
246 * ]
247 * ]
248 * ]
249 * ]
250 * ]
251 * ]
252 * ]
253 * ...
254 * ]
255 *
256 * Also, note the usage of `&$must_query`. This means that by changing `$must_query`
257 * you will be actually changing `$query`.
258 */
259 foreach ( $query['bool']['must'] as &$must_query ) {
260 if ( ! isset( $must_query['bool'] ) || ! isset( $must_query['bool']['should'] ) ) {
261 continue;
262 }
263 foreach ( $must_query['bool']['should'] as &$current_bool_should ) {
264 if ( ! isset( $current_bool_should['multi_match'] ) || ! isset( $current_bool_should['multi_match']['fields'] ) ) {
265 continue;
266 }
267
268 /**
269 * `fuzziness` is used in the original algorithm.
270 * `slop` is used in `3.5`.
271 *
272 * @see \ElasticPress\Indexable\Post\Post::format_args()
273 */
274 if ( empty( $current_bool_should['multi_match']['fuzziness'] ) && empty( $current_bool_should['multi_match']['slop'] ) ) {
275 continue;
276 }
277
278 $fields_to_add = [];
279
280 /**
281 * If the regex used in `$ngram_fields` matches more than one field,
282 * like taxonomies, for example, we use the min value - 1.
283 */
284 foreach ( $current_bool_should['multi_match']['fields'] as $field ) {
285 foreach ( $ngram_fields as $regex => $ngram_field ) {
286 if ( preg_match( '/^(' . $regex . ')(\^(\d+))?$/', $field, $match ) ) {
287 $weight = 1;
288 if ( isset( $match[4] ) && $match[4] > 1 ) {
289 $weight = $match[4] - 1;
290 }
291
292 if ( isset( $fields_to_add[ $ngram_field ] ) ) {
293 $fields_to_add[ $ngram_field ] = min( $fields_to_add[ $ngram_field ], $weight );
294 } else {
295 $fields_to_add[ $ngram_field ] = $weight;
296 }
297 }
298 }
299 }
300
301 foreach ( $fields_to_add as $field => $weight ) {
302 $current_bool_should['multi_match']['fields'][] = "{$field}^{$weight}";
303 }
304 }
305 }
306
307 return $query;
308 }
309
310 /**
311 * Add term suggestions to be indexed
312 *
313 * @param array $post_args Array of ES args.
314 * @since 2.4
315 * @return array
316 */
317 public function filter_term_suggest( $post_args ) {
318 $suggest = [];
319
320 if ( ! empty( $post_args['terms'] ) ) {
321 foreach ( $post_args['terms'] as $taxonomy ) {
322 foreach ( $taxonomy as $term ) {
323 $suggest[] = $term['name'];
324 }
325 }
326 }
327
328 if ( ! empty( $suggest ) ) {
329 $post_args['term_suggest'] = $suggest;
330 }
331
332 return $post_args;
333 }
334
335 /**
336 * Enqueue our autosuggest script
337 *
338 * @since 2.4
339 */
340 public function enqueue_scripts() {
341 if ( Utils\is_indexing() ) {
342 return;
343 }
344
345 $host = Utils\get_host();
346 $settings = $this->get_settings();
347
348 if ( defined( 'EP_AUTOSUGGEST_ENDPOINT' ) && EP_AUTOSUGGEST_ENDPOINT ) {
349 $endpoint_url = EP_AUTOSUGGEST_ENDPOINT;
350 } else {
351 if ( Utils\is_epio() ) {
352 $endpoint_url = trailingslashit( $host ) . Indexables::factory()->get( 'post' )->get_index_name() . '/autosuggest';
353 } else {
354 $endpoint_url = $settings['endpoint_url'];
355 }
356 }
357
358 if ( empty( $endpoint_url ) ) {
359 return;
360 }
361
362 wp_enqueue_script(
363 'elasticpress-autosuggest',
364 EP_URL . 'dist/js/autosuggest-script.js',
365 Utils\get_asset_info( 'autosuggest-script', 'dependencies' ),
366 Utils\get_asset_info( 'autosuggest-script', 'version' ),
367 true
368 );
369
370 wp_set_script_translations( 'elasticpress-autosuggest', 'elasticpress' );
371
372 wp_enqueue_style(
373 'elasticpress-autosuggest',
374 EP_URL . 'dist/css/autosuggest-styles.css',
375 Utils\get_asset_info( 'autosuggest-styles', 'dependencies' ),
376 Utils\get_asset_info( 'autosuggest-styles', 'version' )
377 );
378
379 /** Features Class @var Features $features */
380 $features = Features::factory();
381
382 /** Search Feature @var Feature\Search\Search $search */
383 $search = $features->get_registered_feature( 'search' );
384
385 $query = $this->generate_search_query();
386
387 $epas_options = [
388 'query' => $query['body'],
389 'placeholder' => $query['placeholder'],
390 'endpointUrl' => esc_url( untrailingslashit( $endpoint_url ) ),
391 'selector' => empty( $settings['autosuggest_selector'] ) ? 'ep-autosuggest' : esc_html( $settings['autosuggest_selector'] ),
392 /**
393 * Filter autosuggest default selectors.
394 *
395 * @hook ep_autosuggest_default_selectors
396 * @since 3.6.0
397 * @param {string} $selectors Default selectors used to attach autosuggest.
398 * @return {string} Selectors used to attach autosuggest.
399 */
400 'defaultSelectors' => apply_filters( 'ep_autosuggest_default_selectors', '.ep-autosuggest, input[type="search"], .search-field' ),
401 'action' => 'navigate',
402 'mimeTypes' => [],
403 /**
404 * Filter autosuggest HTTP headers
405 *
406 * @hook ep_autosuggest_http_headers
407 * @param {array} $headers Autosuggest HTTP headers in name => value format
408 * @return {array} HTTP headers
409 */
410 'http_headers' => apply_filters( 'ep_autosuggest_http_headers', [] ),
411 'triggerAnalytics' => ! empty( $settings['trigger_ga_event'] ),
412 'addSearchTermHeader' => false,
413 'requestIdBase' => Utils\get_request_id_base(),
414 ];
415
416 if ( Utils\is_epio() ) {
417 $epas_options['addSearchTermHeader'] = true;
418 }
419
420 $search_settings = $search->get_settings();
421
422 if ( ! $search_settings ) {
423 $search_settings = [];
424 }
425
426 $search_settings = wp_parse_args( $search_settings, $search->default_settings );
427
428 if ( ! empty( $search_settings ) && $search_settings['highlight_enabled'] ) {
429 $epas_options['highlightingEnabled'] = true;
430 $epas_options['highlightingTag'] = apply_filters( 'ep_highlighting_tag', $search_settings['highlight_tag'] );
431 $epas_options['highlightingClass'] = apply_filters( 'ep_highlighting_class', 'ep-highlight' );
432 }
433
434 /**
435 * Output variables to use in Javascript
436 * index: the Elasticsearch index name
437 * endpointUrl: the Elasticsearch autosuggest endpoint url
438 * postType: which post types to use for suggestions
439 * action: the action to take when selecting an item. Possible values are "search" and "navigate".
440 */
441 wp_localize_script(
442 'elasticpress-autosuggest',
443 'epas',
444 /**
445 * Filter autosuggest JavaScript options
446 *
447 * @hook ep_autosuggest_options
448 * @param {array} $options Autosuggest options to be localized
449 * @return {array} New options
450 */
451 apply_filters(
452 'ep_autosuggest_options',
453 $epas_options
454 )
455 );
456 }
457
458 /**
459 * Build a default search request to pass to the autosuggest javascript.
460 * The request will include a placeholder that can then be replaced.
461 *
462 * @return array Generated ElasticSearch request array( 'placeholder'=> placeholderstring, 'body' => request body )
463 */
464 public function generate_search_query() {
465
466 /**
467 * Filter autosuggest query placeholder
468 *
469 * @hook ep_autosuggest_query_placeholder
470 * @param {string} $placeholder Autosuggest placeholder to be replaced later
471 * @return {string} New placeholder
472 */
473 $placeholder = apply_filters( 'ep_autosuggest_query_placeholder', 'ep_autosuggest_placeholder' );
474
475 /** Features Class @var Features $features */
476 $features = Features::factory();
477
478 $post_type = $features->get_registered_feature( 'search' )->get_searchable_post_types();
479
480 /**
481 * Filter post types available to autosuggest
482 *
483 * @hook ep_term_suggest_post_type
484 * @param {array} $post_types Post types
485 * @return {array} New post types
486 */
487 $post_type = apply_filters( 'ep_term_suggest_post_type', array_values( $post_type ) );
488
489 $post_status = get_post_stati(
490 [
491 'public' => true,
492 'exclude_from_search' => false,
493 ]
494 );
495
496 /**
497 * Filter post statuses available to autosuggest
498 *
499 * @hook ep_term_suggest_post_status
500 * @param {array} $post_statuses Post statuses
501 * @return {array} New post statuses
502 */
503 $post_status = apply_filters( 'ep_term_suggest_post_status', array_values( $post_status ) );
504
505 add_filter( 'ep_intercept_remote_request', [ $this, 'intercept_remote_request' ] );
506 add_filter( 'ep_weighting_configuration', [ $features->get_registered_feature( $this->slug ), 'apply_autosuggest_weighting' ] );
507
508 add_filter( 'ep_do_intercept_request', [ $features->get_registered_feature( $this->slug ), 'intercept_search_request' ], 10, 2 );
509
510 add_filter( 'posts_pre_query', [ $features->get_registered_feature( $this->slug ), 'return_empty_posts' ], 100, 1 ); // after ES Query to ensure we are not falling back to DB in any case
511
512 new \WP_Query(
513 /**
514 * Filter WP Query args of the autosuggest query template.
515 *
516 * If you want to display 20 posts in autosuggest:
517 *
518 * ```
519 * add_filter(
520 * 'ep_autosuggest_query_args',
521 * function( $args ) {
522 * $args['posts_per_page'] = 20;
523 * return $args;
524 * }
525 * );
526 * ```
527 *
528 * @since 4.4.0
529 * @hook ep_autosuggest_query_args
530 * @param {array} $args Query args
531 * @return {array} New query args
532 */
533 apply_filters(
534 'ep_autosuggest_query_args',
535 [
536 'post_type' => $post_type,
537 'post_status' => $post_status,
538 's' => $placeholder,
539 'ep_integrate' => true,
540 ]
541 )
542 );
543
544 remove_filter( 'posts_pre_query', [ $features->get_registered_feature( $this->slug ), 'return_empty_posts' ], 100 );
545
546 remove_filter( 'ep_do_intercept_request', [ $features->get_registered_feature( $this->slug ), 'intercept_search_request' ] );
547
548 remove_filter( 'ep_weighting_configuration', [ $features->get_registered_feature( $this->slug ), 'apply_autosuggest_weighting' ] );
549
550 remove_filter( 'ep_intercept_remote_request', [ $this, 'intercept_remote_request' ] );
551
552 return [
553 'body' => $this->autosuggest_query,
554 'placeholder' => $placeholder,
555 ];
556 }
557
558 /**
559 * Ensure we do not fallback to WPDB query for this request
560 *
561 * @param array $posts array of post objects
562 * @return array $posts
563 */
564 public function return_empty_posts( $posts = [] ) {
565 return [];
566 }
567
568 /**
569 * Allow applying custom weighting configuration for autosuggest
570 *
571 * @param array $config current configuration
572 * @return array $config desired configuration
573 */
574 public function apply_autosuggest_weighting( $config = [] ) {
575 /**
576 * Filter autosuggest weighting configuration
577 *
578 * @hook ep_weighting_configuration_for_autosuggest
579 * @param {array} $config Configuration
580 * @return {array} New config
581 */
582 $config = apply_filters( 'ep_weighting_configuration_for_autosuggest', $config );
583 return $config;
584 }
585
586 /**
587 * Store intercepted request value and return a fake successful request result
588 *
589 * @param array $response Response
590 * @param array $query ES Query
591 * @return array $response Response
592 */
593 public function intercept_search_request( $response, $query = [] ) {
594 $this->autosuggest_query = $query['args']['body'];
595
596 $message = wp_json_encode(
597 [
598 esc_html__( 'This is a fake request to build the ElasticPress Autosuggest query. It is not really sent.', 'elasticpress' ),
599 ]
600 );
601
602 return [
603 'is_ep_fake_request' => true,
604 'body' => $message,
605 'response' => [
606 'code' => 200,
607 'message' => $message,
608 ],
609 ];
610 }
611
612 /**
613 * Tell user whether requirements for feature are met or not.
614 *
615 * @return array $status Status array
616 * @since 2.4
617 */
618 public function requirements_status() {
619 $status = new FeatureRequirementsStatus( 0 );
620
621 $status->message = [];
622
623 $status->message[] = esc_html__( 'This feature modifies the site’s default user experience by presenting a list of suggestions below detected search fields as text is entered into the field.', 'elasticpress' );
624
625 if ( ! Utils\is_epio() ) {
626 $status->code = 1;
627 $status->message[] = wp_kses_post( __( "You aren't using <a href='https://elasticpress.io'>ElasticPress.io</a> so we can't be sure your host is properly secured. Autosuggest requires a publicly accessible endpoint, which can expose private content and allow data modification if improperly configured.", 'elasticpress' ) );
628 }
629
630 return $status;
631 }
632
633 /**
634 * Do a non-blocking search query to force the autosuggest hash to update.
635 *
636 * This request has to happen in a public environment, so all code testing if `is_admin()`
637 * are properly executed.
638 *
639 * @param bool $blocking If the request should block the execution or not.
640 */
641 public function epio_send_autosuggest_public_request( $blocking = false ) {
642 if ( ! Utils\is_epio() ) {
643 return;
644 }
645
646 $url = add_query_arg(
647 [
648 's' => 'search test',
649 'ep_epio_set_autosuggest' => 1,
650 'ep_epio_nonce' => wp_create_nonce( 'ep-epio-set-autosuggest' ),
651 'nocache' => time(), // Here just to avoid the request hitting a CDN.
652 ],
653 home_url( '/' )
654 );
655
656 // Pass the same cookies, so the same authenticated user is used (and we can check the nonce).
657 $cookies = [];
658 foreach ( $_COOKIE as $name => $value ) {
659 $cookies[] = new \WP_Http_Cookie(
660 [
661 'name' => $name,
662 'value' => $value,
663 ]
664 );
665 }
666
667 wp_remote_get(
668 $url,
669 [
670 'cookies' => $cookies,
671 'blocking' => (bool) $blocking,
672 ]
673 );
674 }
675
676 /**
677 * Send the allowed parameters for autosuggest to ElasticPress.io.
678 */
679 public function epio_send_autosuggest_allowed() {
680 if ( empty( $_REQUEST['ep_epio_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_REQUEST['ep_epio_nonce'] ), 'ep-epio-set-autosuggest' ) ) {
681 return;
682 }
683
684 if ( empty( $_GET['ep_epio_set_autosuggest'] ) ) {
685 return;
686 }
687
688 /**
689 * Fires before the request is sent to EP.io to set Autosuggest allowed values.
690 *
691 * @hook ep_epio_pre_send_autosuggest_allowed
692 * @since 3.5.x
693 */
694 do_action( 'ep_epio_pre_send_autosuggest_allowed' );
695
696 /**
697 * The same ES query sent by autosuggest.
698 *
699 * Sometimes it'll be a string, sometimes it'll be already an array.
700 */
701 $es_search_query = $this->generate_search_query()['body'];
702 $es_search_query = ( is_array( $es_search_query ) ) ? $es_search_query : json_decode( $es_search_query, true );
703
704 /**
705 * Filter autosuggest ES query
706 *
707 * @since 3.5.x
708 * @hook ep_epio_autosuggest_es_query
709 * @param {array} The ES Query.
710 */
711 $es_search_query = apply_filters( 'ep_epio_autosuggest_es_query', $es_search_query );
712
713 /**
714 * Here is a chance to short-circuit the execution. Also, during the sync
715 * the query will be empty anyway.
716 */
717 if ( empty( $es_search_query ) ) {
718 return;
719 }
720
721 $index = Indexables::factory()->get( 'post' )->get_index_name();
722
723 add_filter( 'ep_format_request_headers', [ $this, 'add_ep_set_autosuggest_header' ] );
724
725 Elasticsearch::factory()->query( $index, 'post', $es_search_query, [] );
726
727 remove_filter( 'ep_format_request_headers', [ $this, 'add_ep_set_autosuggest_header' ] );
728
729 /**
730 * Fires after the request is sent to EP.io to set Autosuggest allowed values.
731 *
732 * @hook ep_epio_sent_autosuggest_allowed
733 * @since 3.5.x
734 */
735 do_action( 'ep_epio_sent_autosuggest_allowed' );
736 }
737
738 /**
739 * Set a header so EP.io servers know this request contains the values
740 * that should be stored as allowed.
741 *
742 * @since 3.5.x
743 * @param array $headers The Request Headers.
744 * @return array
745 */
746 public function add_ep_set_autosuggest_header( $headers ) {
747 $headers['EP-Set-Autosuggest'] = true;
748 return $headers;
749 }
750
751 /**
752 * Retrieve the allowed parameters for autosuggest from ElasticPress.io.
753 *
754 * @return array
755 */
756 public function epio_retrieve_autosuggest_allowed() {
757 $response = Elasticsearch::factory()->remote_request(
758 Indexables::factory()->get( 'post' )->get_index_name() . '/get-autosuggest-allowed'
759 );
760
761 $body = wp_remote_retrieve_body( $response, true );
762 return json_decode( $body, true );
763 }
764
765 /**
766 * Output the current allowed parameters for autosuggest stored in ElasticPress.io.
767 */
768 public function epio_allowed_parameters() {
769 global $wp_version;
770
771 $allowed_params = $this->epio_autosuggest_set_and_get();
772 if ( empty( $allowed_params ) ) {
773 return;
774 }
775 ?>
776 <div class="field js-toggle-feature" data-feature="<?php echo esc_attr( $this->slug ); ?>">
777 <div class="field-name status"><?php esc_html_e( 'Connection', 'elasticpress' ); ?></div>
778 <div class="input-wrap">
779 <?php
780 $epio_link = 'https://elasticpress.io';
781 $epio_autosuggest_kb_link = 'https://elasticpress.zendesk.com/hc/en-us/articles/360055402791';
782 $status_report_link = defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ? network_admin_url( 'admin.php?page=elasticpress-status-report' ) : admin_url( 'admin.php?page=elasticpress-status-report' );
783
784 printf(
785 /* translators: 1: <a> tag (ElasticPress.io); 2. </a>; 3: <a> tag (KB article); 4. </a>; 5: <a> tag (Site Health Debug Section); 6. </a>; */
786 esc_html__( 'You are directly connected to %1$sElasticPress.io%2$s, ensuring the most performant Autosuggest experience. %3$sLearn more about what this means%4$s or %5$sclick here for debug information%6$s.', 'elasticpress' ),
787 '<a href="' . esc_url( $epio_link ) . '">',
788 '</a>',
789 '<a href="' . esc_url( $epio_autosuggest_kb_link ) . '">',
790 '</a>',
791 '<a href="' . esc_url( $status_report_link ) . '">',
792 '</a>'
793 );
794 ?>
795 </div>
796 </div>
797 <?php
798 }
799
800 /**
801 * Try to get the allowed parameters. If they are not set, set it and try to get them again.
802 *
803 * @since 3.5.x
804 * @return array
805 */
806 public function epio_autosuggest_set_and_get() {
807 $allowed_params = [];
808 $errors_count = 1;
809 for ( $i = 0; $i <= $errors_count; $i++ ) {
810 $allowed_params = $this->epio_retrieve_autosuggest_allowed();
811
812 if ( is_wp_error( $allowed_params ) || ( isset( $allowed_params['status'] ) && 200 !== $allowed_params['status'] ) ) {
813 $allowed_params = [];
814 break;
815 }
816
817 // We have what we need, no need to retry.
818 if ( ! empty( $allowed_params ) ) {
819 break;
820 }
821
822 // Send to EP.io what should be autosuggest's allowed values and try to get them again.
823 $this->epio_send_autosuggest_public_request( true );
824 }
825
826 return $allowed_params;
827 }
828
829 /**
830 * Returns the title.
831 *
832 * @since 4.4.1
833 * @return string
834 */
835 public function get_title() : string {
836 if ( ! Utils\is_epio() ) {
837 return esc_html__( 'Autosuggest', 'elasticpress' );
838 }
839
840 /* translators: 1. elasticpress.io logo; */
841 return sprintf( esc_html__( 'Autosuggest By %s', 'elasticpress' ), $this->get_epio_logo() );
842 }
843
844 /**
845 * Return true, so EP knows we want to intercept the remote request
846 *
847 * As we add and remove this function from `ep_intercept_remote_request`,
848 * using `__return_true` could remove a *real* `__return_true` added by someone else.
849 *
850 * @since 4.7.0
851 * @see https://github.com/10up/ElasticPress/issues/2887
852 * @return true
853 */
854 public function intercept_remote_request() {
855 return true;
856 }
857
858 /**
859 * DEPRECATED. Delete the cached query for autosuggest.
860 *
861 * @since 3.5.5
862 */
863 public function delete_cached_query() {
864 _doing_it_wrong(
865 __METHOD__,
866 esc_html__( 'This method should not be called anymore, as autosuggest requests are not sent regularly anymore.' ),
867 'ElasticPress 4.7.0'
868 );
869 }
870 }
871