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

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