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

865 lines 27.0 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\Feature as Feature;
13 use ElasticPress\Features as Features;
14 use ElasticPress\Utils as Utils;
15 use ElasticPress\FeatureRequirementsStatus as FeatureRequirementsStatus;
16 use ElasticPress\Indexables as Indexables;
17 use ElasticPress\Elasticsearch;
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 return [
597 'response' => [ 'code' => 200 ],
598 'body' => 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 }
605
606 /**
607 * Tell user whether requirements for feature are met or not.
608 *
609 * @return array $status Status array
610 * @since 2.4
611 */
612 public function requirements_status() {
613 $status = new FeatureRequirementsStatus( 0 );
614
615 $status->message = [];
616
617 $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' );
618
619 if ( ! Utils\is_epio() ) {
620 $status->code = 1;
621 $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' ) );
622 }
623
624 return $status;
625 }
626
627 /**
628 * Do a non-blocking search query to force the autosuggest hash to update.
629 *
630 * This request has to happen in a public environment, so all code testing if `is_admin()`
631 * are properly executed.
632 *
633 * @param bool $blocking If the request should block the execution or not.
634 */
635 public function epio_send_autosuggest_public_request( $blocking = false ) {
636 if ( ! Utils\is_epio() ) {
637 return;
638 }
639
640 $url = add_query_arg(
641 [
642 's' => 'search test',
643 'ep_epio_set_autosuggest' => 1,
644 'ep_epio_nonce' => wp_create_nonce( 'ep-epio-set-autosuggest' ),
645 'nocache' => time(), // Here just to avoid the request hitting a CDN.
646 ],
647 home_url( '/' )
648 );
649
650 // Pass the same cookies, so the same authenticated user is used (and we can check the nonce).
651 $cookies = [];
652 foreach ( $_COOKIE as $name => $value ) {
653 $cookies[] = new \WP_Http_Cookie(
654 [
655 'name' => $name,
656 'value' => $value,
657 ]
658 );
659 }
660
661 wp_remote_get(
662 $url,
663 [
664 'cookies' => $cookies,
665 'blocking' => (bool) $blocking,
666 ]
667 );
668 }
669
670 /**
671 * Send the allowed parameters for autosuggest to ElasticPress.io.
672 */
673 public function epio_send_autosuggest_allowed() {
674 if ( empty( $_REQUEST['ep_epio_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_REQUEST['ep_epio_nonce'] ), 'ep-epio-set-autosuggest' ) ) {
675 return;
676 }
677
678 if ( empty( $_GET['ep_epio_set_autosuggest'] ) ) {
679 return;
680 }
681
682 /**
683 * Fires before the request is sent to EP.io to set Autosuggest allowed values.
684 *
685 * @hook ep_epio_pre_send_autosuggest_allowed
686 * @since 3.5.x
687 */
688 do_action( 'ep_epio_pre_send_autosuggest_allowed' );
689
690 /**
691 * The same ES query sent by autosuggest.
692 *
693 * Sometimes it'll be a string, sometimes it'll be already an array.
694 */
695 $es_search_query = $this->generate_search_query()['body'];
696 $es_search_query = ( is_array( $es_search_query ) ) ? $es_search_query : json_decode( $es_search_query, true );
697
698 /**
699 * Filter autosuggest ES query
700 *
701 * @since 3.5.x
702 * @hook ep_epio_autosuggest_es_query
703 * @param {array} The ES Query.
704 */
705 $es_search_query = apply_filters( 'ep_epio_autosuggest_es_query', $es_search_query );
706
707 /**
708 * Here is a chance to short-circuit the execution. Also, during the sync
709 * the query will be empty anyway.
710 */
711 if ( empty( $es_search_query ) ) {
712 return;
713 }
714
715 $index = Indexables::factory()->get( 'post' )->get_index_name();
716
717 add_filter( 'ep_format_request_headers', [ $this, 'add_ep_set_autosuggest_header' ] );
718
719 Elasticsearch::factory()->query( $index, 'post', $es_search_query, [] );
720
721 remove_filter( 'ep_format_request_headers', [ $this, 'add_ep_set_autosuggest_header' ] );
722
723 /**
724 * Fires after the request is sent to EP.io to set Autosuggest allowed values.
725 *
726 * @hook ep_epio_sent_autosuggest_allowed
727 * @since 3.5.x
728 */
729 do_action( 'ep_epio_sent_autosuggest_allowed' );
730 }
731
732 /**
733 * Set a header so EP.io servers know this request contains the values
734 * that should be stored as allowed.
735 *
736 * @since 3.5.x
737 * @param array $headers The Request Headers.
738 * @return array
739 */
740 public function add_ep_set_autosuggest_header( $headers ) {
741 $headers['EP-Set-Autosuggest'] = true;
742 return $headers;
743 }
744
745 /**
746 * Retrieve the allowed parameters for autosuggest from ElasticPress.io.
747 *
748 * @return array
749 */
750 public function epio_retrieve_autosuggest_allowed() {
751 $response = Elasticsearch::factory()->remote_request(
752 Indexables::factory()->get( 'post' )->get_index_name() . '/get-autosuggest-allowed'
753 );
754
755 $body = wp_remote_retrieve_body( $response, true );
756 return json_decode( $body, true );
757 }
758
759 /**
760 * Output the current allowed parameters for autosuggest stored in ElasticPress.io.
761 */
762 public function epio_allowed_parameters() {
763 global $wp_version;
764
765 $allowed_params = $this->epio_autosuggest_set_and_get();
766 if ( empty( $allowed_params ) ) {
767 return;
768 }
769 ?>
770 <div class="field js-toggle-feature" data-feature="<?php echo esc_attr( $this->slug ); ?>">
771 <div class="field-name status"><?php esc_html_e( 'Connection', 'elasticpress' ); ?></div>
772 <div class="input-wrap">
773 <?php
774 $epio_link = 'https://elasticpress.io';
775 $epio_autosuggest_kb_link = 'https://elasticpress.zendesk.com/hc/en-us/articles/360055402791';
776 $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' );
777
778 printf(
779 /* translators: 1: <a> tag (ElasticPress.io); 2. </a>; 3: <a> tag (KB article); 4. </a>; 5: <a> tag (Site Health Debug Section); 6. </a>; */
780 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' ),
781 '<a href="' . esc_url( $epio_link ) . '">',
782 '</a>',
783 '<a href="' . esc_url( $epio_autosuggest_kb_link ) . '">',
784 '</a>',
785 '<a href="' . esc_url( $status_report_link ) . '">',
786 '</a>'
787 );
788 ?>
789 </div>
790 </div>
791 <?php
792 }
793
794 /**
795 * Try to get the allowed parameters. If they are not set, set it and try to get them again.
796 *
797 * @since 3.5.x
798 * @return array
799 */
800 public function epio_autosuggest_set_and_get() {
801 $allowed_params = [];
802 $errors_count = 1;
803 for ( $i = 0; $i <= $errors_count; $i++ ) {
804 $allowed_params = $this->epio_retrieve_autosuggest_allowed();
805
806 if ( is_wp_error( $allowed_params ) || ( isset( $allowed_params['status'] ) && 200 !== $allowed_params['status'] ) ) {
807 $allowed_params = [];
808 break;
809 }
810
811 // We have what we need, no need to retry.
812 if ( ! empty( $allowed_params ) ) {
813 break;
814 }
815
816 // Send to EP.io what should be autosuggest's allowed values and try to get them again.
817 $this->epio_send_autosuggest_public_request( true );
818 }
819
820 return $allowed_params;
821 }
822
823 /**
824 * Returns the title.
825 *
826 * @since 4.4.1
827 * @return string
828 */
829 public function get_title() : string {
830 if ( ! Utils\is_epio() ) {
831 return esc_html__( 'Autosuggest', 'elasticpress' );
832 }
833
834 /* translators: 1. elasticpress.io logo; */
835 return sprintf( esc_html__( 'Autosuggest By %s', 'elasticpress' ), $this->get_epio_logo() );
836 }
837
838 /**
839 * Return true, so EP knows we want to intercept the remote request
840 *
841 * As we add and remove this function from `ep_intercept_remote_request`,
842 * using `__return_true` could remove a *real* `__return_true` added by someone else.
843 *
844 * @since 4.7.0
845 * @see https://github.com/10up/ElasticPress/issues/2887
846 * @return true
847 */
848 public function intercept_remote_request() {
849 return true;
850 }
851
852 /**
853 * DEPRECATED. Delete the cached query for autosuggest.
854 *
855 * @since 3.5.5
856 */
857 public function delete_cached_query() {
858 _doing_it_wrong(
859 __METHOD__,
860 esc_html__( 'This method should not be called anymore, as autosuggest requests are not sent regularly anymore.' ),
861 'ElasticPress 4.7.0'
862 );
863 }
864 }
865