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

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

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