PluginProbe
ElasticPress / 5.0.2
ElasticPress v5.0.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 5.0.2, at includes/classes/Feature/Autosuggest/Autosuggest.php

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