Source: includes/classes/Feature/Autosuggest/Autosuggest.php

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