Completed
Push — instant-search-master ( a4e056...eedd28 )
by
unknown
07:13
created

Jetpack_Search::print_query_success()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 0
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
1
<?php
2
/**
3
 * Jetpack Search: Main Jetpack_Search class
4
 *
5
 * @package    Jetpack
6
 * @subpackage Jetpack Search
7
 * @since      5.0.0
8
 */
9
10
use Automattic\Jetpack\Connection\Client;
11
12
/**
13
 * The main class for the Jetpack Search module.
14
 *
15
 * @since 5.0.0
16
 */
17
class Jetpack_Search {
18
19
	/**
20
	 * The number of found posts.
21
	 *
22
	 * @since 5.0.0
23
	 *
24
	 * @var int
25
	 */
26
	protected $found_posts = 0;
27
28
	/**
29
	 * The search result, as returned by the WordPress.com REST API.
30
	 *
31
	 * @since 5.0.0
32
	 *
33
	 * @var array
34
	 */
35
	protected $search_result;
36
37
	/**
38
	 * This site's blog ID on WordPress.com.
39
	 *
40
	 * @since 5.0.0
41
	 *
42
	 * @var int
43
	 */
44
	protected $jetpack_blog_id;
45
46
	/**
47
	 * The Elasticsearch aggregations (filters).
48
	 *
49
	 * @since 5.0.0
50
	 *
51
	 * @var array
52
	 */
53
	protected $aggregations = array();
54
55
	/**
56
	 * The maximum number of aggregations allowed.
57
	 *
58
	 * @since 5.0.0
59
	 *
60
	 * @var int
61
	 */
62
	protected $max_aggregations_count = 100;
63
64
	/**
65
	 * Statistics about the last Elasticsearch query.
66
	 *
67
	 * @since 5.6.0
68
	 *
69
	 * @var array
70
	 */
71
	protected $last_query_info = array();
72
73
	/**
74
	 * Statistics about the last Elasticsearch query failure.
75
	 *
76
	 * @since 5.6.0
77
	 *
78
	 * @var array
79
	 */
80
	protected $last_query_failure_info = array();
81
82
	/**
83
	 * The singleton instance of this class.
84
	 *
85
	 * @since 5.0.0
86
	 *
87
	 * @var Jetpack_Search
88
	 */
89
	protected static $instance;
90
91
	/**
92
	 * Languages with custom analyzers. Other languages are supported, but are analyzed with the default analyzer.
93
	 *
94
	 * @since 5.0.0
95
	 *
96
	 * @var array
97
	 */
98
	public static $analyzed_langs = array( 'ar', 'bg', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'eu', 'fa', 'fi', 'fr', 'he', 'hi', 'hu', 'hy', 'id', 'it', 'ja', 'ko', 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' );
99
100
	/**
101
	 * Jetpack_Search constructor.
102
	 *
103
	 * @since 5.0.0
104
	 *
105
	 * Doesn't do anything. This class needs to be initialized via the instance() method instead.
106
	 */
107
	protected function __construct() {
108
	}
109
110
	/**
111
	 * Prevent __clone()'ing of this class.
112
	 *
113
	 * @since 5.0.0
114
	 */
115
	public function __clone() {
116
		wp_die( "Please don't __clone Jetpack_Search" );
117
	}
118
119
	/**
120
	 * Prevent __wakeup()'ing of this class.
121
	 *
122
	 * @since 5.0.0
123
	 */
124
	public function __wakeup() {
125
		wp_die( "Please don't __wakeup Jetpack_Search" );
126
	}
127
128
	/**
129
	 * Get singleton instance of Jetpack_Search.
130
	 *
131
	 * Instantiates and sets up a new instance if needed, or returns the singleton.
132
	 *
133
	 * @since 5.0.0
134
	 *
135
	 * @return Jetpack_Search The Jetpack_Search singleton.
136
	 */
137
	public static function instance() {
138
		if ( ! isset( self::$instance ) ) {
139
			self::$instance = new Jetpack_Search();
140
141
			self::$instance->setup();
142
		}
143
144
		return self::$instance;
145
	}
146
147
	/**
148
	 * Perform various setup tasks for the class.
149
	 *
150
	 * Checks various pre-requisites and adds hooks.
151
	 *
152
	 * @since 5.0.0
153
	 */
154
	public function setup() {
155
		if ( ! Jetpack::is_active() || ! Jetpack_Plan::supports( 'search' ) ) {
156
			return;
157
		}
158
159
		$this->jetpack_blog_id = Jetpack::get_option( 'id' );
160
161
		if ( ! $this->jetpack_blog_id ) {
162
			return;
163
		}
164
165
		require_once dirname( __FILE__ ) . '/class.jetpack-search-helpers.php';
166
		require_once dirname( __FILE__ ) . '/class.jetpack-search-template-tags.php';
167
		require_once JETPACK__PLUGIN_DIR . 'modules/widgets/search.php';
168
169
		$this->init_hooks();
170
	}
171
172
	/**
173
	 * Setup the various hooks needed for the plugin to take over search duties.
174
	 *
175
	 * @since 5.0.0
176
	 */
177
	public function init_hooks() {
178
		if ( ! is_admin() ) {
179
			add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 );
180
181
			add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ), 10, 2 );
182
183
			add_action( 'did_jetpack_search_query', array( $this, 'store_last_query_info' ) );
184
			add_action( 'failed_jetpack_search_query', array( $this, 'store_query_failure' ) );
185
186
			add_action( 'init', array( $this, 'set_filters_from_widgets' ) );
187
188
			add_action( 'pre_get_posts', array( $this, 'maybe_add_post_type_as_var' ) );
189
			add_action( 'wp_enqueue_scripts', array( $this, 'load_assets' ) );
190
		} else {
191
			add_action( 'update_option', array( $this, 'track_widget_updates' ), 10, 3 );
192
		}
193
194
		add_action( 'jetpack_deactivate_module_search', array( $this, 'move_search_widgets_to_inactive' ) );
195
	}
196
197
	/**
198
	 * Loads assets for Jetpack Search Prototype featuring Search As You Type experience.
199
	 */
200
	public function load_assets() {
201
		if ( defined( 'JETPACK_SEARCH_PROTOTYPE' ) ) {
202
			$script_relative_path = '_inc/build/instant-search/jp-search.bundle.js';
203
			if ( file_exists( JETPACK__PLUGIN_DIR . $script_relative_path ) ) {
204
				$script_version = self::get_asset_version( $script_relative_path );
205
				$script_path    = plugins_url( $script_relative_path, JETPACK__PLUGIN_FILE );
206
				wp_enqueue_script( 'jetpack-instant-search', $script_path, array(), $script_version, true );
207
208
				$filters = Jetpack_Search_Helpers::get_filters_from_widgets();
209
				$widgets = array();
210
				foreach( $filters as $key => $filter ) {
211
					if ( ! isset( $widgets[$filter[ 'widget_id' ]] ) ) {
212
						$widgets[$filter[ 'widget_id' ]][ 'filters' ] = array();
213
						$widgets[$filter[ 'widget_id' ]][ 'widget_id' ] = $filter[ 'widget_id' ];
214
					}
215
					$new_filter = $filter;
216
					$new_filter[ 'filter_id' ] = $key;
217
					$widgets[$filter[ 'widget_id' ]][ 'filters' ][] = $new_filter;
218
				}
219
220
				// This is probably a temporary filter for testing the prototype.
221
				$options = array(
222
					'postTypes' => get_post_types(),
223
					'siteId'		=> Jetpack::get_option( 'id' ),
224
					'widgets' 	=> array_values( $widgets ),
225
				);
226
				/**
227
				 * Customize Instant Search Options.
228
				 *
229
				 * @module search
230
				 *
231
				 * @since 7.7.0
232
				 *
233
				 * @param array $options Array of parameters used in Instant Search queries.
234
				 */
235
				$options = apply_filters( 'jetpack_instant_search_options', $options );
236
237
				wp_localize_script(
238
					'jetpack-instant-search', 'JetpackInstantSearchOptions', $options
239
				);
240
			}
241
242
			$style_relative_path = '_inc/build/instant-search/instant-search.min.css';
243 View Code Duplication
			if ( file_exists( JETPACK__PLUGIN_DIR . $script_relative_path ) ) {
244
				$style_version = self::get_asset_version( $style_relative_path );
245
				$style_path    = plugins_url( $style_relative_path, JETPACK__PLUGIN_FILE );
246
				wp_enqueue_style( 'jetpack-instant-search', $style_path, array(), $style_version );
247
			}
248
		}
249
	}
250
251
	/**
252
	 * Get the version number to use when loading the file. Allows us to bypass cache when developing.
253
	 *
254
	 * @param string $file Path of the file we are looking for.
255
	 * @return string $script_version Version number.
256
	 */
257
	public static function get_asset_version( $file ) {
258
		return Jetpack::is_development_version() && file_exists( JETPACK__PLUGIN_DIR . $file )
259
			? filemtime( JETPACK__PLUGIN_DIR . $file )
260
			: JETPACK__VERSION;
261
	}
262
263
	/**
264
	 * When an Elasticsearch query fails, this stores it and enqueues some debug information in the footer.
265
	 *
266
	 * @since 5.6.0
267
	 *
268
	 * @param array $meta Information about the failure.
269
	 */
270
	public function store_query_failure( $meta ) {
271
		$this->last_query_failure_info = $meta;
272
		add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
273
	}
274
275
	/**
276
	 * Outputs information about the last Elasticsearch failure.
277
	 *
278
	 * @since 5.6.0
279
	 */
280
	public function print_query_failure() {
281
		if ( $this->last_query_failure_info ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->last_query_failure_info of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
282
			printf(
283
				'<!-- Jetpack Search failed with code %s: %s - %s -->',
284
				esc_html( $this->last_query_failure_info['response_code'] ),
285
				esc_html( $this->last_query_failure_info['json']['error'] ),
286
				esc_html( $this->last_query_failure_info['json']['message'] )
287
			);
288
		}
289
	}
290
291
	/**
292
	 * Stores information about the last Elasticsearch query and enqueues some debug information in the footer.
293
	 *
294
	 * @since 5.6.0
295
	 *
296
	 * @param array $meta Information about the query.
297
	 */
298
	public function store_last_query_info( $meta ) {
299
		$this->last_query_info = $meta;
300
		add_action( 'wp_footer', array( $this, 'print_query_success' ) );
301
	}
302
303
	/**
304
	 * Outputs information about the last Elasticsearch search.
305
	 *
306
	 * @since 5.6.0
307
	 */
308
	public function print_query_success() {
309
		if ( $this->last_query_info ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->last_query_info of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
310
			printf(
311
				'<!-- Jetpack Search took %s ms, ES time %s ms -->',
312
				intval( $this->last_query_info['elapsed_time'] ),
313
				esc_html( $this->last_query_info['es_time'] )
314
			);
315
316
			if ( isset( $_GET['searchdebug'] ) ) {
317
				printf(
318
					'<!-- Query response data: %s -->',
319
					esc_html( print_r( $this->last_query_info, 1 ) )
320
				);
321
			}
322
		}
323
	}
324
325
	/**
326
	 * Returns the last query information, or false if no information was stored.
327
	 *
328
	 * @since 5.8.0
329
	 *
330
	 * @return bool|array
331
	 */
332
	public function get_last_query_info() {
333
		return empty( $this->last_query_info ) ? false : $this->last_query_info;
334
	}
335
336
	/**
337
	 * Returns the last query failure information, or false if no failure information was stored.
338
	 *
339
	 * @since 5.8.0
340
	 *
341
	 * @return bool|array
342
	 */
343
	public function get_last_query_failure_info() {
344
		return empty( $this->last_query_failure_info ) ? false : $this->last_query_failure_info;
345
	}
346
347
	/**
348
	 * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
349
	 * developers to disable filters supplied by the search widget. Useful if filters are
350
	 * being defined at the code level.
351
	 *
352
	 * @since      5.7.0
353
	 * @deprecated 5.8.0 Use Jetpack_Search_Helpers::are_filters_by_widget_disabled() directly.
354
	 *
355
	 * @return bool
356
	 */
357
	public function are_filters_by_widget_disabled() {
358
		return Jetpack_Search_Helpers::are_filters_by_widget_disabled();
359
	}
360
361
	/**
362
	 * Retrieves a list of known Jetpack search filters widget IDs, gets the filters for each widget,
363
	 * and applies those filters to this Jetpack_Search object.
364
	 *
365
	 * @since 5.7.0
366
	 */
367
	public function set_filters_from_widgets() {
368
		if ( Jetpack_Search_Helpers::are_filters_by_widget_disabled() ) {
369
			return;
370
		}
371
372
		$filters = Jetpack_Search_Helpers::get_filters_from_widgets();
373
374
		if ( ! empty( $filters ) ) {
375
			$this->set_filters( $filters );
376
		}
377
	}
378
379
	/**
380
	 * Restricts search results to certain post types via a GET argument.
381
	 *
382
	 * @since 5.8.0
383
	 *
384
	 * @param WP_Query $query A WP_Query instance.
385
	 */
386
	public function maybe_add_post_type_as_var( WP_Query $query ) {
387
		if ( $this->should_handle_query( $query ) && ! empty( $_GET['post_type'] ) ) {
388
			$post_types = ( is_string( $_GET['post_type'] ) && false !== strpos( $_GET['post_type'], ',' ) )
389
				? $post_type = explode( ',', $_GET['post_type'] )
390
				: (array) $_GET['post_type'];
391
			$post_types = array_map( 'sanitize_key', $post_types );
392
			$query->set( 'post_type', $post_types );
393
		}
394
	}
395
396
	/*
397
	 * Run a search on the WordPress.com public API.
398
	 *
399
	 * @since 5.0.0
400
	 *
401
	 * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint.
402
	 *
403
	 * @return object|WP_Error The response from the public API, or a WP_Error.
404
	 */
405
	public function search( array $es_args ) {
406
		$endpoint    = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
407
		$service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
408
409
		$do_authenticated_request = false;
410
411
		if ( class_exists( 'Automattic\\Jetpack\\Connection\\Client' ) &&
412
			isset( $es_args['authenticated_request'] ) &&
413
			true === $es_args['authenticated_request'] ) {
414
			$do_authenticated_request = true;
415
		}
416
417
		unset( $es_args['authenticated_request'] );
418
419
		$request_args = array(
420
			'headers' => array(
421
				'Content-Type' => 'application/json',
422
			),
423
			'timeout'    => 10,
424
			'user-agent' => 'jetpack_search',
425
		);
426
427
		$request_body = wp_json_encode( $es_args );
428
429
		$start_time = microtime( true );
430
431
		if ( $do_authenticated_request ) {
432
			$request_args['method'] = 'POST';
433
434
			$request = Client::wpcom_json_api_request_as_blog( $endpoint, Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
435
		} else {
436
			$request_args = array_merge( $request_args, array(
437
				'body' => $request_body,
438
			) );
439
440
			$request = wp_remote_post( $service_url, $request_args );
441
		}
442
443
		$end_time = microtime( true );
444
445
		if ( is_wp_error( $request ) ) {
446
			return $request;
447
		}
448
449
		$response_code = wp_remote_retrieve_response_code( $request );
450
451
		$response = json_decode( wp_remote_retrieve_body( $request ), true );
452
453
		$took = is_array( $response ) && ! empty( $response['took'] )
454
			? $response['took']
455
			: null;
456
457
		$query = array(
458
			'args'          => $es_args,
459
			'response'      => $response,
460
			'response_code' => $response_code,
461
			'elapsed_time'  => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms.
462
			'es_time'       => $took,
463
			'url'           => $service_url,
464
		);
465
466
		/**
467
		 * Fires after a search request has been performed.
468
		 *
469
		 * Includes the following info in the $query parameter:
470
		 *
471
		 * array args Array of Elasticsearch arguments for the search
472
		 * array response Raw API response, JSON decoded
473
		 * int response_code HTTP response code of the request
474
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
475
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
476
		 * string url API url that was queried
477
		 *
478
		 * @module search
479
		 *
480
		 * @since  5.0.0
481
		 * @since  5.8.0 This action now fires on all queries instead of just successful queries.
482
		 *
483
		 * @param array $query Array of information about the query performed
484
		 */
485
		do_action( 'did_jetpack_search_query', $query );
486
487
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
488
			/**
489
			 * Fires after a search query request has failed
490
			 *
491
			 * @module search
492
			 *
493
			 * @since  5.6.0
494
			 *
495
			 * @param array Array containing the response code and response from the failed search query
496
			 */
497
			do_action( 'failed_jetpack_search_query', array(
498
				'response_code' => $response_code,
499
				'json'          => $response,
500
			) );
501
502
			return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_search_api_response'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
503
		}
504
505
		return $response;
506
	}
507
508
	/**
509
	 * Bypass the normal Search query and offload it to Jetpack servers.
510
	 *
511
	 * This is the main hook of the plugin and is responsible for returning the posts that match the search query.
512
	 *
513
	 * @since 5.0.0
514
	 *
515
	 * @param array    $posts Current array of posts (still pre-query).
516
	 * @param WP_Query $query The WP_Query being filtered.
517
	 *
518
	 * @return array Array of matching posts.
519
	 */
520
	public function filter__posts_pre_query( $posts, $query ) {
521
		if ( ! $this->should_handle_query( $query ) ) {
522
			return $posts;
523
		}
524
525
		$this->do_search( $query );
526
527
		if ( ! is_array( $this->search_result ) ) {
528
			return $posts;
529
		}
530
531
		// If no results, nothing to do
532
		if ( ! count( $this->search_result['results']['hits'] ) ) {
533
			return array();
534
		}
535
536
		$post_ids = array();
537
538
		foreach ( $this->search_result['results']['hits'] as $result ) {
539
			$post_ids[] = (int) $result['fields']['post_id'];
540
		}
541
542
		// Query all posts now
543
		$args = array(
544
			'post__in'            => $post_ids,
545
			'orderby'             => 'post__in',
546
			'perm'                => 'readable',
547
			'post_type'           => 'any',
548
			'ignore_sticky_posts' => true,
549
			'suppress_filters'    => true,
550
		);
551
552
		$posts_query = new WP_Query( $args );
553
554
		// WP Core doesn't call the set_found_posts and its filters when filtering posts_pre_query like we do, so need to do these manually.
555
		$query->found_posts   = $this->found_posts;
556
		$query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
557
558
		return $posts_query->posts;
559
	}
560
561
	/**
562
	 * Build up the search, then run it against the Jetpack servers.
563
	 *
564
	 * @since 5.0.0
565
	 *
566
	 * @param WP_Query $query The original WP_Query to use for the parameters of our search.
567
	 */
568
	public function do_search( WP_Query $query ) {
569
		if ( ! $this->should_handle_query( $query ) ) {
570
			return;
571
		}
572
573
		$page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
574
575
		// Get maximum allowed offset and posts per page values for the API.
576
		$max_offset         = Jetpack_Search_Helpers::get_max_offset();
577
		$max_posts_per_page = Jetpack_Search_Helpers::get_max_posts_per_page();
578
579
		$posts_per_page = $query->get( 'posts_per_page' );
580
		if ( $posts_per_page > $max_posts_per_page ) {
581
			$posts_per_page = $max_posts_per_page;
582
		}
583
584
		// Start building the WP-style search query args.
585
		// They'll be translated to ES format args later.
586
		$es_wp_query_args = array(
587
			'query'          => $query->get( 's' ),
588
			'posts_per_page' => $posts_per_page,
589
			'paged'          => $page,
590
			'orderby'        => $query->get( 'orderby' ),
591
			'order'          => $query->get( 'order' ),
592
		);
593
594
		if ( ! empty( $this->aggregations ) ) {
595
			$es_wp_query_args['aggregations'] = $this->aggregations;
596
		}
597
598
		// Did we query for authors?
599
		if ( $query->get( 'author_name' ) ) {
600
			$es_wp_query_args['author_name'] = $query->get( 'author_name' );
601
		}
602
603
		$es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
604
		$es_wp_query_args['terms']     = $this->get_es_wp_query_terms_for_query( $query );
605
606
		/**
607
		 * Modify the search query parameters, such as controlling the post_type.
608
		 *
609
		 * These arguments are in the format of WP_Query arguments
610
		 *
611
		 * @module search
612
		 *
613
		 * @since  5.0.0
614
		 *
615
		 * @param array    $es_wp_query_args The current query args, in WP_Query format.
616
		 * @param WP_Query $query            The original WP_Query object.
617
		 */
618
		$es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
619
620
		// If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
621
		// capped at Jetpack_Search_Helpers::get_max_offset(), so a high page would always return the last page of results otherwise.
622
		if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $max_offset ) {
623
			$query->set_404();
624
625
			return;
626
		}
627
628
		// If there were no post types returned, then 404 to avoid querying against non-public post types, which could
629
		// happen if we don't add the post type restriction to the ES query.
630
		if ( empty( $es_wp_query_args['post_type'] ) ) {
631
			$query->set_404();
632
633
			return;
634
		}
635
636
		// Convert the WP-style args into ES args.
637
		$es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
638
639
		//Only trust ES to give us IDs, not the content since it is a mirror
640
		$es_query_args['fields'] = array(
641
			'post_id',
642
		);
643
644
		/**
645
		 * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
646
		 *
647
		 * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
648
		 *
649
		 * @module search
650
		 *
651
		 * @since  5.0.0
652
		 *
653
		 * @param array    $es_query_args The raw Elasticsearch query args.
654
		 * @param WP_Query $query         The original WP_Query object.
655
		 */
656
		$es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
657
658
		// Do the actual search query!
659
		$this->search_result = $this->search( $es_query_args );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->search($es_query_args) of type object is incompatible with the declared type array of property $search_result.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
660
661
		if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
662
			$this->found_posts = 0;
663
664
			return;
665
		}
666
667
		// If we have aggregations, fix the ordering to match the input order (ES doesn't guarantee the return order).
668
		if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
669
			$this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
670
		}
671
672
		// Total number of results for paging purposes. Capped at $max_offset + $posts_per_page, as deep paging gets quite expensive.
673
		$this->found_posts = min( $this->search_result['results']['total'], $max_offset + $posts_per_page );
674
	}
675
676
	/**
677
	 * If the query has already been run before filters have been updated, then we need to re-run the query
678
	 * to get the latest aggregations.
679
	 *
680
	 * This is especially useful for supporting widget management in the customizer.
681
	 *
682
	 * @since 5.8.0
683
	 *
684
	 * @return bool Whether the query was successful or not.
685
	 */
686
	public function update_search_results_aggregations() {
687
		if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
688
			return false;
689
		}
690
691
		$es_args = $this->last_query_info['args'];
692
		$builder = new Jetpack_WPES_Query_Builder();
693
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
694
		$es_args['aggregations'] = $builder->build_aggregation();
695
696
		$this->search_result = $this->search( $es_args );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->search($es_args) of type object is incompatible with the declared type array of property $search_result.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
697
698
		return ! is_wp_error( $this->search_result );
699
	}
700
701
	/**
702
	 * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style Elasticsearch term arguments for the search.
703
	 *
704
	 * @since 5.0.0
705
	 *
706
	 * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query.
707
	 *
708
	 * @return array The new WP-style Elasticsearch arguments (that will be converted into 'real' Elasticsearch arguments).
709
	 */
710
	public function get_es_wp_query_terms_for_query( WP_Query $query ) {
711
		$args = array();
712
713
		$the_tax_query = $query->tax_query;
714
715
		if ( ! $the_tax_query ) {
716
			return $args;
717
		}
718
719
720
		if ( ! $the_tax_query instanceof WP_Tax_Query || empty( $the_tax_query->queried_terms ) || ! is_array( $the_tax_query->queried_terms ) ) {
0 ignored issues
show
Bug introduced by
The class WP_Tax_Query does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
721
			return $args;
722
		}
723
724
		$args = array();
725
726
		foreach ( $the_tax_query->queries as $tax_query ) {
727
			// Right now we only support slugs...see note above
728
			if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
729
				continue;
730
			}
731
732
			$taxonomy = $tax_query['taxonomy'];
733
734 View Code Duplication
			if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
735
				$args[ $taxonomy ] = array();
736
			}
737
738
			$args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
739
		}
740
741
		return $args;
742
	}
743
744
	/**
745
	 * Parse out the post type from a WP_Query.
746
	 *
747
	 * Only allows post types that are not marked as 'exclude_from_search'.
748
	 *
749
	 * @since 5.0.0
750
	 *
751
	 * @param WP_Query $query Original WP_Query object.
752
	 *
753
	 * @return array Array of searchable post types corresponding to the original query.
754
	 */
755
	public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
756
		$post_types = $query->get( 'post_type' );
757
758
		// If we're searching 'any', we want to only pass searchable post types to Elasticsearch.
759
		if ( 'any' === $post_types ) {
760
			$post_types = array_values( get_post_types( array(
761
				'exclude_from_search' => false,
762
			) ) );
763
		}
764
765
		if ( ! is_array( $post_types ) ) {
766
			$post_types = array( $post_types );
767
		}
768
769
		$post_types = array_unique( $post_types );
770
771
		$sanitized_post_types = array();
772
773
		// Make sure the post types are queryable.
774
		foreach ( $post_types as $post_type ) {
775
			if ( ! $post_type ) {
776
				continue;
777
			}
778
779
			$post_type_object = get_post_type_object( $post_type );
780
			if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
781
				continue;
782
			}
783
784
			$sanitized_post_types[] = $post_type;
785
		}
786
787
		return $sanitized_post_types;
788
	}
789
790
	/**
791
	 * Get the Elasticsearch result.
792
	 *
793
	 * @since 5.0.0
794
	 *
795
	 * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response.
796
	 *
797
	 * @return array|bool The search results, or false if there was a failure.
798
	 */
799
	public function get_search_result( $raw = false ) {
800
		if ( $raw ) {
801
			return $this->search_result;
802
		}
803
804
		return ( ! empty( $this->search_result ) && ! is_wp_error( $this->search_result ) && is_array( $this->search_result ) && ! empty( $this->search_result['results'] ) ) ? $this->search_result['results'] : false;
805
	}
806
807
	/**
808
	 * Add the date portion of a WP_Query onto the query args.
809
	 *
810
	 * @since 5.0.0
811
	 *
812
	 * @param array    $es_wp_query_args The Elasticsearch query arguments in WordPress form.
813
	 * @param WP_Query $query            The original WP_Query.
814
	 *
815
	 * @return array The es wp query args, with date filters added (as needed).
816
	 */
817
	public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
818
		if ( $query->get( 'year' ) ) {
819
			if ( $query->get( 'monthnum' ) ) {
820
				// Padding
821
				$date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
822
823
				if ( $query->get( 'day' ) ) {
824
					// Padding
825
					$date_day = sprintf( '%02d', $query->get( 'day' ) );
826
827
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
828
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
829
				} else {
830
					$days_in_month = date( 't', mktime( 0, 0, 0, $query->get( 'monthnum' ), 14, $query->get( 'year' ) ) ); // 14 = middle of the month so no chance of DST issues
831
832
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
833
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
834
				}
835
			} else {
836
				$date_start = $query->get( 'year' ) . '-01-01 00:00:00';
837
				$date_end   = $query->get( 'year' ) . '-12-31 23:59:59';
838
			}
839
840
			$es_wp_query_args['date_range'] = array(
841
				'field' => 'date',
842
				'gte'   => $date_start,
843
				'lte'   => $date_end,
844
			);
845
		}
846
847
		return $es_wp_query_args;
848
	}
849
850
	/**
851
	 * Converts WP_Query style args to Elasticsearch args.
852
	 *
853
	 * @since 5.0.0
854
	 *
855
	 * @param array $args Array of WP_Query style arguments.
856
	 *
857
	 * @return array Array of ES style query arguments.
858
	 */
859
	public function convert_wp_es_to_es_args( array $args ) {
860
		jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-parser' );
861
862
		$defaults = array(
863
			'blog_id'        => get_current_blog_id(),
864
			'query'          => null,    // Search phrase
865
			'query_fields'   => array(), // list of fields to search
866
			'excess_boost'   => array(), // map of field to excess boost values (multiply)
867
			'post_type'      => null,    // string or an array
868
			'terms'          => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) )
869
			'author'         => null,    // id or an array of ids
870
			'author_name'    => array(), // string or an array
871
			'date_range'     => null,    // array( 'field' => 'date', 'gt' => 'YYYY-MM-dd', 'lte' => 'YYYY-MM-dd' ); date formats: 'YYYY-MM-dd' or 'YYYY-MM-dd HH:MM:SS'
872
			'orderby'        => null,    // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
873
			'order'          => 'DESC',
874
			'posts_per_page' => 10,
875
			'offset'         => null,
876
			'paged'          => null,
877
			/**
878
			 * Aggregations. Examples:
879
			 * array(
880
			 *     'Tag'       => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
881
			 *     'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
882
			 * );
883
			 */
884
			'aggregations'   => null,
885
		);
886
887
		$args = wp_parse_args( $args, $defaults );
888
889
		$parser = new Jetpack_WPES_Search_Query_Parser( $args['query'], array( get_locale() ) );
890
891
		if ( empty( $args['query_fields'] ) ) {
892
			if ( defined( 'JETPACK_SEARCH_VIP_INDEX' ) && JETPACK_SEARCH_VIP_INDEX ) {
893
				// VIP indices do not have per language fields
894
				$match_fields = $this->_get_caret_boosted_fields(
895
					array(
896
						'title'         => 0.1,
897
						'content'       => 0.1,
898
						'excerpt'       => 0.1,
899
						'tag.name'      => 0.1,
900
						'category.name' => 0.1,
901
						'author_login'  => 0.1,
902
						'author'        => 0.1,
903
					)
904
				);
905
906
				$boost_fields = $this->_get_caret_boosted_fields(
907
					$this->_apply_boosts_multiplier( array(
908
						'title'         => 2,
909
						'tag.name'      => 1,
910
						'category.name' => 1,
911
						'author_login'  => 1,
912
						'author'        => 1,
913
					), $args['excess_boost'] )
914
				);
915
916
				$boost_phrase_fields = $this->_get_caret_boosted_fields(
917
					array(
918
						'title'         => 1,
919
						'content'       => 1,
920
						'excerpt'       => 1,
921
						'tag.name'      => 1,
922
						'category.name' => 1,
923
						'author'        => 1,
924
					)
925
				);
926
			} else {
927
				$match_fields = $parser->merge_ml_fields(
928
					array(
929
						'title'         => 0.1,
930
						'content'       => 0.1,
931
						'excerpt'       => 0.1,
932
						'tag.name'      => 0.1,
933
						'category.name' => 0.1,
934
					),
935
					$this->_get_caret_boosted_fields( array(
936
						'author_login'  => 0.1,
937
						'author'        => 0.1,
938
					) )
939
				);
940
941
				$boost_fields = $parser->merge_ml_fields(
942
					$this->_apply_boosts_multiplier( array(
943
						'title'         => 2,
944
						'tag.name'      => 1,
945
						'category.name' => 1,
946
					), $args['excess_boost'] ),
947
					$this->_get_caret_boosted_fields( $this->_apply_boosts_multiplier( array(
948
						'author_login'  => 1,
949
						'author'        => 1,
950
					), $args['excess_boost'] ) )
951
				);
952
953
				$boost_phrase_fields = $parser->merge_ml_fields(
954
					array(
955
						'title'         => 1,
956
						'content'       => 1,
957
						'excerpt'       => 1,
958
						'tag.name'      => 1,
959
						'category.name' => 1,
960
					),
961
					$this->_get_caret_boosted_fields( array(
962
						'author'        => 1,
963
					) )
964
				);
965
			}
966
		} else {
967
			// If code is overriding the fields, then use that. Important for backwards compatibility.
968
			$match_fields        = $args['query_fields'];
969
			$boost_phrase_fields = $match_fields;
970
			$boost_fields        = null;
971
		}
972
973
		$parser->phrase_filter( array(
974
			'must_query_fields'  => $match_fields,
975
			'boost_query_fields' => null,
976
		) );
977
		$parser->remaining_query( array(
978
			'must_query_fields'  => $match_fields,
979
			'boost_query_fields' => $boost_fields,
980
		) );
981
982
		// Boost on phrase matches
983
		$parser->remaining_query( array(
984
			'boost_query_fields' => $boost_phrase_fields,
985
			'boost_query_type'   => 'phrase',
986
		) );
987
988
		/**
989
		 * Modify the recency decay parameters for the search query.
990
		 *
991
		 * The recency decay lowers the search scores based on the age of a post relative to an origin date. Basic adjustments:
992
		 *  - origin: A date. Posts with this date will have the highest score and no decay applied. Default is today.
993
		 *  - offset: Number of days/months/years (eg 30d). All posts within this time range of the origin (before and after) will have no decay applied. Default is no offset.
994
		 *  - scale: The number of days/months/years from the origin+offset at which the decay will equal the decay param. Default 360d
995
		 *  - decay: The amount of decay applied at offset+scale. Default 0.9.
996
		 *
997
		 * The curve applied is a Gaussian. More details available at {@see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay}
998
		 *
999
		 * @module search
1000
		 *
1001
		 * @since  5.8.0
1002
		 *
1003
		 * @param array $decay_params The decay parameters.
1004
		 * @param array $args         The WP query parameters.
1005
		 */
1006
		$decay_params = apply_filters(
1007
			'jetpack_search_recency_score_decay',
1008
			array(
1009
				'origin' => date( 'Y-m-d' ),
1010
				'scale'  => '360d',
1011
				'decay'  => 0.9,
1012
			),
1013
			$args
1014
		);
1015
1016
		if ( ! empty( $decay_params ) ) {
1017
			// Newer content gets weighted slightly higher
1018
			$parser->add_decay( 'gauss', array(
1019
				'date_gmt' => $decay_params
1020
			) );
1021
		}
1022
1023
		$es_query_args = array(
1024
			'blog_id' => absint( $args['blog_id'] ),
1025
			'size'    => absint( $args['posts_per_page'] ),
1026
		);
1027
1028
		// ES "from" arg (offset)
1029
		if ( $args['offset'] ) {
1030
			$es_query_args['from'] = absint( $args['offset'] );
1031
		} elseif ( $args['paged'] ) {
1032
			$es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
1033
		}
1034
1035
		$es_query_args['from'] = min( $es_query_args['from'], Jetpack_Search_Helpers::get_max_offset() );
1036
1037
		if ( ! is_array( $args['author_name'] ) ) {
1038
			$args['author_name'] = array( $args['author_name'] );
1039
		}
1040
1041
		// ES stores usernames, not IDs, so transform
1042
		if ( ! empty( $args['author'] ) ) {
1043
			if ( ! is_array( $args['author'] ) ) {
1044
				$args['author'] = array( $args['author'] );
1045
			}
1046
1047
			foreach ( $args['author'] as $author ) {
1048
				$user = get_user_by( 'id', $author );
1049
1050
				if ( $user && ! empty( $user->user_login ) ) {
1051
					$args['author_name'][] = $user->user_login;
1052
				}
1053
			}
1054
		}
1055
1056
		//////////////////////////////////////////////////
1057
		// Build the filters from the query elements.
1058
		// Filters rock because they are cached from one query to the next
1059
		// but they are cached as individual filters, rather than all combined together.
1060
		// May get performance boost by also caching the top level boolean filter too.
1061
1062
		if ( $args['post_type'] ) {
1063
			if ( ! is_array( $args['post_type'] ) ) {
1064
				$args['post_type'] = array( $args['post_type'] );
1065
			}
1066
1067
			$parser->add_filter( array(
1068
				'terms' => array(
1069
					'post_type' => $args['post_type'],
1070
				),
1071
			) );
1072
		}
1073
1074
		if ( $args['author_name'] ) {
1075
			$parser->add_filter( array(
1076
				'terms' => array(
1077
					'author_login' => $args['author_name'],
1078
				),
1079
			) );
1080
		}
1081
1082
		if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
1083
			$field = $args['date_range']['field'];
1084
1085
			unset( $args['date_range']['field'] );
1086
1087
			$parser->add_filter( array(
1088
				'range' => array(
1089
					$field => $args['date_range'],
1090
				),
1091
			) );
1092
		}
1093
1094
		if ( is_array( $args['terms'] ) ) {
1095
			foreach ( $args['terms'] as $tax => $terms ) {
1096
				$terms = (array) $terms;
1097
1098
				if ( count( $terms ) && mb_strlen( $tax ) ) {
1099 View Code Duplication
					switch ( $tax ) {
1100
						case 'post_tag':
1101
							$tax_fld = 'tag.slug';
1102
1103
							break;
1104
1105
						case 'category':
1106
							$tax_fld = 'category.slug';
1107
1108
							break;
1109
1110
						default:
1111
							$tax_fld = 'taxonomy.' . $tax . '.slug';
1112
1113
							break;
1114
					}
1115
1116
					foreach ( $terms as $term ) {
1117
						$parser->add_filter( array(
1118
							'term' => array(
1119
								$tax_fld => $term,
1120
							),
1121
						) );
1122
					}
1123
				}
1124
			}
1125
		}
1126
1127
		if ( ! $args['orderby'] ) {
1128
			if ( $args['query'] ) {
1129
				$args['orderby'] = array( 'relevance' );
1130
			} else {
1131
				$args['orderby'] = array( 'date' );
1132
			}
1133
		}
1134
1135
		// Validate the "order" field
1136
		switch ( strtolower( $args['order'] ) ) {
1137
			case 'asc':
1138
				$args['order'] = 'asc';
1139
				break;
1140
1141
			case 'desc':
1142
			default:
1143
				$args['order'] = 'desc';
1144
				break;
1145
		}
1146
1147
		$es_query_args['sort'] = array();
1148
1149
		foreach ( (array) $args['orderby'] as $orderby ) {
1150
			// Translate orderby from WP field to ES field
1151
			switch ( $orderby ) {
1152
				case 'relevance' :
1153
					//never order by score ascending
1154
					$es_query_args['sort'][] = array(
1155
						'_score' => array(
1156
							'order' => 'desc',
1157
						),
1158
					);
1159
1160
					break;
1161
1162 View Code Duplication
				case 'date' :
1163
					$es_query_args['sort'][] = array(
1164
						'date' => array(
1165
							'order' => $args['order'],
1166
						),
1167
					);
1168
1169
					break;
1170
1171 View Code Duplication
				case 'ID' :
1172
					$es_query_args['sort'][] = array(
1173
						'id' => array(
1174
							'order' => $args['order'],
1175
						),
1176
					);
1177
1178
					break;
1179
1180
				case 'author' :
1181
					$es_query_args['sort'][] = array(
1182
						'author.raw' => array(
1183
							'order' => $args['order'],
1184
						),
1185
					);
1186
1187
					break;
1188
			} // End switch().
1189
		} // End foreach().
1190
1191
		if ( empty( $es_query_args['sort'] ) ) {
1192
			unset( $es_query_args['sort'] );
1193
		}
1194
1195
		// Aggregations
1196
		if ( ! empty( $args['aggregations'] ) ) {
1197
			$this->add_aggregations_to_es_query_builder( $args['aggregations'], $parser );
0 ignored issues
show
Documentation introduced by
$args['aggregations'] is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1198
		}
1199
1200
		$es_query_args['filter']       = $parser->build_filter();
1201
		$es_query_args['query']        = $parser->build_query();
1202
		$es_query_args['aggregations'] = $parser->build_aggregation();
1203
1204
		return $es_query_args;
1205
	}
1206
1207
	/**
1208
	 * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1209
	 *
1210
	 * @since 5.0.0
1211
	 *
1212
	 * @param array                      $aggregations Array of aggregations (filters) to add to the Jetpack_WPES_Query_Builder.
1213
	 * @param Jetpack_WPES_Query_Builder $builder      The builder instance that is creating the Elasticsearch query.
1214
	 */
1215
	public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
1216
		foreach ( $aggregations as $label => $aggregation ) {
1217
			switch ( $aggregation['type'] ) {
1218
				case 'taxonomy':
1219
					$this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1220
1221
					break;
1222
1223
				case 'post_type':
1224
					$this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1225
1226
					break;
1227
1228
				case 'date_histogram':
1229
					$this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1230
1231
					break;
1232
			}
1233
		}
1234
	}
1235
1236
	/**
1237
	 * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1238
	 *
1239
	 * @since 5.0.0
1240
	 *
1241
	 * @param array                      $aggregation The aggregation to add to the query builder.
1242
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1243
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1244
	 */
1245
	public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1246
		$field = null;
0 ignored issues
show
Unused Code introduced by
$field is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1247
1248
		switch ( $aggregation['taxonomy'] ) {
1249
			case 'post_tag':
1250
				$field = 'tag';
1251
				break;
1252
1253
			case 'category':
1254
				$field = 'category';
1255
				break;
1256
1257
			default:
1258
				$field = 'taxonomy.' . $aggregation['taxonomy'];
1259
				break;
1260
		}
1261
1262
		$builder->add_aggs( $label, array(
1263
			'terms' => array(
1264
				'field' => $field . '.slug',
1265
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1266
			),
1267
		) );
1268
	}
1269
1270
	/**
1271
	 * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1272
	 *
1273
	 * @since 5.0.0
1274
	 *
1275
	 * @param array                      $aggregation The aggregation to add to the query builder.
1276
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1277
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1278
	 */
1279
	public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1280
		$builder->add_aggs( $label, array(
1281
			'terms' => array(
1282
				'field' => 'post_type',
1283
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1284
			),
1285
		) );
1286
	}
1287
1288
	/**
1289
	 * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1290
	 *
1291
	 * @since 5.0.0
1292
	 *
1293
	 * @param array                      $aggregation The aggregation to add to the query builder.
1294
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1295
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1296
	 */
1297
	public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1298
		$args = array(
1299
			'interval' => $aggregation['interval'],
1300
			'field'    => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' == $aggregation['field'] ) ? 'date_gmt' : 'date',
1301
		);
1302
1303
		if ( isset( $aggregation['min_doc_count'] ) ) {
1304
			$args['min_doc_count'] = intval( $aggregation['min_doc_count'] );
1305
		} else {
1306
			$args['min_doc_count'] = 1;
1307
		}
1308
1309
		$builder->add_aggs( $label, array(
1310
			'date_histogram' => $args,
1311
		) );
1312
	}
1313
1314
	/**
1315
	 * And an existing filter object with a list of additional filters.
1316
	 *
1317
	 * Attempts to optimize the filters somewhat.
1318
	 *
1319
	 * @since 5.0.0
1320
	 *
1321
	 * @param array $curr_filter The existing filters to build upon.
1322
	 * @param array $filters     The new filters to add.
1323
	 *
1324
	 * @return array The resulting merged filters.
1325
	 */
1326
	public static function and_es_filters( array $curr_filter, array $filters ) {
1327
		if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
1328
			if ( 1 === count( $filters ) ) {
1329
				return $filters[0];
1330
			}
1331
1332
			return array(
1333
				'and' => $filters,
1334
			);
1335
		}
1336
1337
		return array(
1338
			'and' => array_merge( array( $curr_filter ), $filters ),
1339
		);
1340
	}
1341
1342
	/**
1343
	 * Set the available filters for the search.
1344
	 *
1345
	 * These get rendered via the Jetpack_Search_Widget() widget.
1346
	 *
1347
	 * Behind the scenes, these are implemented using Elasticsearch Aggregations.
1348
	 *
1349
	 * If you do not require counts of how many documents match each filter, please consider using regular WP Query
1350
	 * arguments instead, such as via the jetpack_search_es_wp_query_args filter
1351
	 *
1352
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1353
	 *
1354
	 * @since  5.0.0
1355
	 *
1356
	 * @param array $aggregations Array of filters (aggregations) to apply to the search
1357
	 */
1358
	public function set_filters( array $aggregations ) {
1359
		foreach ( (array) $aggregations as $key => $agg ) {
1360
			if ( empty( $agg['name'] ) ) {
1361
				$aggregations[ $key ]['name'] = $key;
1362
			}
1363
		}
1364
		$this->aggregations = $aggregations;
1365
	}
1366
1367
	/**
1368
	 * Set the search's facets (deprecated).
1369
	 *
1370
	 * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead.
1371
	 *
1372
	 * @see        Jetpack_Search::set_filters()
1373
	 *
1374
	 * @param array $facets Array of facets to apply to the search.
1375
	 */
1376
	public function set_facets( array $facets ) {
1377
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
1378
1379
		$this->set_filters( $facets );
1380
	}
1381
1382
	/**
1383
	 * Get the raw Aggregation results from the Elasticsearch response.
1384
	 *
1385
	 * @since  5.0.0
1386
	 *
1387
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1388
	 *
1389
	 * @return array Array of Aggregations performed on the search.
1390
	 */
1391
	public function get_search_aggregations_results() {
1392
		$aggregations = array();
1393
1394
		$search_result = $this->get_search_result();
1395
1396
		if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
1397
			$aggregations = $search_result['aggregations'];
1398
		}
1399
1400
		return $aggregations;
1401
	}
1402
1403
	/**
1404
	 * Get the raw Facet results from the Elasticsearch response.
1405
	 *
1406
	 * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead.
1407
	 *
1408
	 * @see        Jetpack_Search::get_search_aggregations_results()
1409
	 *
1410
	 * @return array Array of Facets performed on the search.
1411
	 */
1412
	public function get_search_facets() {
1413
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
1414
1415
		return $this->get_search_aggregations_results();
1416
	}
1417
1418
	/**
1419
	 * Get the results of the Filters performed, including the number of matching documents.
1420
	 *
1421
	 * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
1422
	 * matching buckets, the url for applying/removing each bucket, etc.
1423
	 *
1424
	 * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
1425
	 * member if you need to access the raw filters set in Jetpack_Search::set_filters().
1426
	 *
1427
	 * @since 5.0.0
1428
	 *
1429
	 * @param WP_Query $query The optional original WP_Query to use for determining which filters are active. Defaults to the main query.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $query not be null|WP_Query?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1430
	 *
1431
	 * @return array Array of filters applied and info about them.
1432
	 */
1433
	public function get_filters( WP_Query $query = null ) {
1434
		if ( ! $query instanceof WP_Query ) {
0 ignored issues
show
Bug introduced by
The class WP_Query does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1435
			global $wp_query;
1436
1437
			$query = $wp_query;
1438
		}
1439
1440
		$aggregation_data = $this->aggregations;
1441
1442
		if ( empty( $aggregation_data ) ) {
1443
			return $aggregation_data;
1444
		}
1445
1446
		$aggregation_results = $this->get_search_aggregations_results();
1447
1448
		if ( ! $aggregation_results ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $aggregation_results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1449
			return $aggregation_data;
1450
		}
1451
1452
		// NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES
1453
		foreach ( $aggregation_results as $label => $aggregation ) {
1454
			if ( empty( $aggregation ) ) {
1455
				continue;
1456
			}
1457
1458
			$type = $this->aggregations[ $label ]['type'];
1459
1460
			$aggregation_data[ $label ]['buckets'] = array();
1461
1462
			$existing_term_slugs = array();
1463
1464
			$tax_query_var = null;
1465
1466
			// Figure out which terms are active in the query, for this taxonomy
1467
			if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
1468
				$tax_query_var = $this->get_taxonomy_query_var( $this->aggregations[ $label ]['taxonomy'] );
1469
1470
				if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
1471
					foreach ( $query->tax_query->queries as $tax_query ) {
1472
						if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
1473
						     'slug' === $tax_query['field'] &&
1474
						     is_array( $tax_query['terms'] ) ) {
1475
							$existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
1476
						}
1477
					}
1478
				}
1479
			}
1480
1481
			// Now take the resulting found aggregation items and generate the additional info about them, such as activation/deactivation url, name, count, etc.
1482
			$buckets = array();
1483
1484
			if ( ! empty( $aggregation['buckets'] ) ) {
1485
				$buckets = (array) $aggregation['buckets'];
1486
			}
1487
1488
			if ( 'date_histogram' == $type ) {
1489
				//re-order newest to oldest
1490
				$buckets = array_reverse( $buckets );
1491
			}
1492
1493
			// Some aggregation types like date_histogram don't support the max results parameter
1494
			if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
1495
				$buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
1496
			}
1497
1498
			foreach ( $buckets as $item ) {
1499
				$query_vars = array();
1500
				$active     = false;
1501
				$remove_url = null;
1502
				$name       = '';
1503
1504
				// What type was the original aggregation?
1505
				switch ( $type ) {
1506
					case 'taxonomy':
1507
						$taxonomy = $this->aggregations[ $label ]['taxonomy'];
1508
1509
						$term = get_term_by( 'slug', $item['key'], $taxonomy );
1510
1511
						if ( ! $term || ! $tax_query_var ) {
1512
							continue 2; // switch() is considered a looping structure
1513
						}
1514
1515
						$query_vars = array(
1516
							$tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
1517
						);
1518
1519
						$name = $term->name;
1520
1521
						// Let's determine if this term is active or not
1522
1523
						if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
1524
							$active = true;
1525
1526
							$slug_count = count( $existing_term_slugs );
1527
1528 View Code Duplication
							if ( $slug_count > 1 ) {
1529
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1530
									$tax_query_var,
0 ignored issues
show
Bug introduced by
It seems like $tax_query_var can also be of type boolean; however, Jetpack_Search_Helpers::add_query_arg() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1531
									rawurlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) )
1532
								);
1533
							} else {
1534
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( $tax_query_var );
0 ignored issues
show
Bug introduced by
It seems like $tax_query_var can also be of type boolean; however, Jetpack_Search_Helpers::remove_query_arg() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1535
							}
1536
						}
1537
1538
						break;
1539
1540
					case 'post_type':
1541
						$post_type = get_post_type_object( $item['key'] );
1542
1543
						if ( ! $post_type || $post_type->exclude_from_search ) {
1544
							continue 2;  // switch() is considered a looping structure
1545
						}
1546
1547
						$query_vars = array(
1548
							'post_type' => $item['key'],
1549
						);
1550
1551
						$name = $post_type->labels->singular_name;
1552
1553
						// Is this post type active on this search?
1554
						$post_types = $query->get( 'post_type' );
1555
1556
						if ( ! is_array( $post_types ) ) {
1557
							$post_types = array( $post_types );
1558
						}
1559
1560
						if ( in_array( $item['key'], $post_types ) ) {
1561
							$active = true;
1562
1563
							$post_type_count = count( $post_types );
1564
1565
							// For the right 'remove filter' url, we need to remove the post type from the array, or remove the param entirely if it's the only one
1566 View Code Duplication
							if ( $post_type_count > 1 ) {
1567
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1568
									'post_type',
1569
									rawurlencode( implode( ',', array_diff( $post_types, array( $item['key'] ) ) ) )
1570
								);
1571
							} else {
1572
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
1573
							}
1574
						}
1575
1576
						break;
1577
1578
					case 'date_histogram':
1579
						$timestamp = $item['key'] / 1000;
1580
1581
						$current_year  = $query->get( 'year' );
1582
						$current_month = $query->get( 'monthnum' );
1583
						$current_day   = $query->get( 'day' );
1584
1585
						switch ( $this->aggregations[ $label ]['interval'] ) {
1586
							case 'year':
1587
								$year = (int) date( 'Y', $timestamp );
1588
1589
								$query_vars = array(
1590
									'year'     => $year,
1591
									'monthnum' => false,
1592
									'day'      => false,
1593
								);
1594
1595
								$name = $year;
1596
1597
								// Is this year currently selected?
1598
								if ( ! empty( $current_year ) && (int) $current_year === $year ) {
1599
									$active = true;
1600
1601
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
1602
								}
1603
1604
								break;
1605
1606
							case 'month':
1607
								$year  = (int) date( 'Y', $timestamp );
1608
								$month = (int) date( 'n', $timestamp );
1609
1610
								$query_vars = array(
1611
									'year'     => $year,
1612
									'monthnum' => $month,
1613
									'day'      => false,
1614
								);
1615
1616
								$name = date( 'F Y', $timestamp );
1617
1618
								// Is this month currently selected?
1619
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1620
								     ! empty( $current_month ) && (int) $current_month === $month ) {
1621
									$active = true;
1622
1623
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
1624
								}
1625
1626
								break;
1627
1628
							case 'day':
1629
								$year  = (int) date( 'Y', $timestamp );
1630
								$month = (int) date( 'n', $timestamp );
1631
								$day   = (int) date( 'j', $timestamp );
1632
1633
								$query_vars = array(
1634
									'year'     => $year,
1635
									'monthnum' => $month,
1636
									'day'      => $day,
1637
								);
1638
1639
								$name = date( 'F jS, Y', $timestamp );
1640
1641
								// Is this day currently selected?
1642
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1643
								     ! empty( $current_month ) && (int) $current_month === $month &&
1644
								     ! empty( $current_day ) && (int) $current_day === $day ) {
1645
									$active = true;
1646
1647
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
1648
								}
1649
1650
								break;
1651
1652
							default:
1653
								continue 3; // switch() is considered a looping structure
1654
						} // End switch().
1655
1656
						break;
1657
1658
					default:
1659
						//continue 2; // switch() is considered a looping structure
1660
				} // End switch().
1661
1662
				// Need to urlencode param values since add_query_arg doesn't
1663
				$url_params = urlencode_deep( $query_vars );
1664
1665
				$aggregation_data[ $label ]['buckets'][] = array(
1666
					'url'        => Jetpack_Search_Helpers::add_query_arg( $url_params ),
1667
					'query_vars' => $query_vars,
1668
					'name'       => $name,
1669
					'count'      => $item['doc_count'],
1670
					'active'     => $active,
1671
					'remove_url' => $remove_url,
1672
					'type'       => $type,
1673
					'type_label' => $aggregation_data[ $label ]['name'],
1674
					'widget_id'  => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0
1675
				);
1676
			} // End foreach().
1677
		} // End foreach().
1678
1679
		/**
1680
		 * Modify the aggregation filters returned by get_filters().
1681
		 *
1682
		 * Useful if you are setting custom filters outside of the supported filters (taxonomy, post_type etc.) and
1683
		 * want to hook them up so they're returned when you call `get_filters()`.
1684
		 *
1685
		 * @module search
1686
		 *
1687
		 * @since  6.9.0
1688
		 *
1689
		 * @param array    $aggregation_data The array of filters keyed on label.
1690
		 * @param WP_Query $query            The WP_Query object.
1691
		 */
1692
		return apply_filters( 'jetpack_search_get_filters', $aggregation_data, $query );
1693
	}
1694
1695
	/**
1696
	 * Get the results of the facets performed.
1697
	 *
1698
	 * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead.
1699
	 *
1700
	 * @see        Jetpack_Search::get_filters()
1701
	 *
1702
	 * @return array $facets Array of facets applied and info about them.
1703
	 */
1704
	public function get_search_facet_data() {
1705
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
1706
1707
		return $this->get_filters();
1708
	}
1709
1710
	/**
1711
	 * Get the filters that are currently applied to this search.
1712
	 *
1713
	 * @since 5.0.0
1714
	 *
1715
	 * @return array Array of filters that were applied.
1716
	 */
1717
	public function get_active_filter_buckets() {
1718
		$active_buckets = array();
1719
1720
		$filters = $this->get_filters();
1721
1722
		if ( ! is_array( $filters ) ) {
1723
			return $active_buckets;
1724
		}
1725
1726
		foreach ( $filters as $filter ) {
1727
			if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
1728
				foreach ( $filter['buckets'] as $item ) {
1729
					if ( isset( $item['active'] ) && $item['active'] ) {
1730
						$active_buckets[] = $item;
1731
					}
1732
				}
1733
			}
1734
		}
1735
1736
		return $active_buckets;
1737
	}
1738
1739
	/**
1740
	 * Get the filters that are currently applied to this search.
1741
	 *
1742
	 * @deprecated 5.0 Please use Jetpack_Search::get_active_filter_buckets() instead.
1743
	 *
1744
	 * @see        Jetpack_Search::get_active_filter_buckets()
1745
	 *
1746
	 * @return array Array of filters that were applied.
1747
	 */
1748
	public function get_current_filters() {
1749
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
1750
1751
		return $this->get_active_filter_buckets();
1752
	}
1753
1754
	/**
1755
	 * Calculate the right query var to use for a given taxonomy.
1756
	 *
1757
	 * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter.
1758
	 *
1759
	 * @since 5.0.0
1760
	 *
1761
	 * @param string $taxonomy_name The name of the taxonomy for which to get the query var.
1762
	 *
1763
	 * @return bool|string The query var to use for this taxonomy, or false if none found.
1764
	 */
1765
	public function get_taxonomy_query_var( $taxonomy_name ) {
1766
		$taxonomy = get_taxonomy( $taxonomy_name );
1767
1768
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1769
			return false;
1770
		}
1771
1772
		/**
1773
		 * Modify the query var to use for a given taxonomy
1774
		 *
1775
		 * @module search
1776
		 *
1777
		 * @since  5.0.0
1778
		 *
1779
		 * @param string $query_var     The current query_var for the taxonomy
1780
		 * @param string $taxonomy_name The taxonomy name
1781
		 */
1782
		return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
1783
	}
1784
1785
	/**
1786
	 * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
1787
	 * which is the input order.
1788
	 *
1789
	 * Necessary because ES does not always return aggregations in the same order that you pass them in,
1790
	 * and it should be possible to control the display order easily.
1791
	 *
1792
	 * @since 5.0.0
1793
	 *
1794
	 * @param array $aggregations Aggregation results to be reordered.
1795
	 * @param array $desired      Array with keys representing the desired ordering.
1796
	 *
1797
	 * @return array A new array with reordered keys, matching those in $desired.
1798
	 */
1799
	public function fix_aggregation_ordering( array $aggregations, array $desired ) {
1800
		if ( empty( $aggregations ) || empty( $desired ) ) {
1801
			return $aggregations;
1802
		}
1803
1804
		$reordered = array();
1805
1806
		foreach ( array_keys( $desired ) as $agg_name ) {
1807
			if ( isset( $aggregations[ $agg_name ] ) ) {
1808
				$reordered[ $agg_name ] = $aggregations[ $agg_name ];
1809
			}
1810
		}
1811
1812
		return $reordered;
1813
	}
1814
1815
	/**
1816
	 * Sends events to Tracks when a search filters widget is updated.
1817
	 *
1818
	 * @since 5.8.0
1819
	 *
1820
	 * @param string $option    The option name. Only "widget_jetpack-search-filters" is cared about.
1821
	 * @param array  $old_value The old option value.
1822
	 * @param array  $new_value The new option value.
1823
	 */
1824
	public function track_widget_updates( $option, $old_value, $new_value ) {
1825
		if ( 'widget_jetpack-search-filters' !== $option ) {
1826
			return;
1827
		}
1828
1829
		$event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value );
1830
		if ( ! $event ) {
1831
			return;
1832
		}
1833
1834
		$tracking = new Automattic\Jetpack\Tracking();
1835
		$tracking->tracks_record_event(
1836
			wp_get_current_user(),
1837
			sprintf( 'jetpack_search_widget_%s', $event['action'] ),
1838
			$event['widget']
1839
		);
1840
	}
1841
1842
	/**
1843
	 * Moves any active search widgets to the inactive category.
1844
	 *
1845
	 * @since 5.9.0
1846
	 *
1847
	 * @param string $module Unused. The Jetpack module being disabled.
1848
	 */
1849
	public function move_search_widgets_to_inactive( $module ) {
1850
		if ( ! is_active_widget( false, false, Jetpack_Search_Helpers::FILTER_WIDGET_BASE, true ) ) {
1851
			return;
1852
		}
1853
1854
		$sidebars_widgets = wp_get_sidebars_widgets();
1855
1856
		if ( ! is_array( $sidebars_widgets ) ) {
1857
			return;
1858
		}
1859
1860
		$changed = false;
1861
1862
		foreach ( $sidebars_widgets as $sidebar => $widgets ) {
1863
			if ( 'wp_inactive_widgets' === $sidebar || 'orphaned_widgets' === substr( $sidebar, 0, 16 ) ) {
1864
				continue;
1865
			}
1866
1867
			if ( is_array( $widgets ) ) {
1868
				foreach ( $widgets as $key => $widget ) {
1869
					if ( _get_widget_id_base( $widget ) == Jetpack_Search_Helpers::FILTER_WIDGET_BASE ) {
1870
						$changed = true;
1871
1872
						array_unshift( $sidebars_widgets['wp_inactive_widgets'], $widget );
1873
						unset( $sidebars_widgets[ $sidebar ][ $key ] );
1874
					}
1875
				}
1876
			}
1877
		}
1878
1879
		if ( $changed ) {
1880
			wp_set_sidebars_widgets( $sidebars_widgets );
1881
		}
1882
	}
1883
1884
	/**
1885
	 * Determine whether a given WP_Query should be handled by ElasticSearch.
1886
	 *
1887
	 * @param WP_Query $query The WP_Query object.
1888
	 *
1889
	 * @return bool
1890
	 */
1891
	public function should_handle_query( $query ) {
1892
		/**
1893
		 * Determine whether a given WP_Query should be handled by ElasticSearch.
1894
		 *
1895
		 * @module search
1896
		 *
1897
		 * @since  5.6.0
1898
		 *
1899
		 * @param bool     $should_handle Should be handled by Jetpack Search.
1900
		 * @param WP_Query $query         The WP_Query object.
1901
		 */
1902
		return apply_filters( 'jetpack_search_should_handle_query', $query->is_main_query() && $query->is_search(), $query );
1903
	}
1904
1905
	/**
1906
	 * Transforms an array with fields name as keys and boosts as value into
1907
	 * shorthand "caret" format.
1908
	 *
1909
	 * @param array $fields_boost [ "title" => "2", "content" => "1" ]
1910
	 *
1911
	 * @return array [ "title^2", "content^1" ]
1912
	 */
1913
	private function _get_caret_boosted_fields( array $fields_boost ) {
1914
		$caret_boosted_fields = array();
1915
		foreach ( $fields_boost as $field => $boost ) {
1916
			$caret_boosted_fields[] = "$field^$boost";
1917
		}
1918
		return $caret_boosted_fields;
1919
	}
1920
1921
	/**
1922
	 * Apply a multiplier to boost values.
1923
	 *
1924
	 * @param array $fields_boost [ "title" => 2, "content" => 1 ]
1925
	 * @param array $fields_boost_multiplier [ "title" => 0.1234 ]
1926
	 *
1927
	 * @return array [ "title" => "0.247", "content" => "1.000" ]
1928
	 */
1929
	private function _apply_boosts_multiplier( array $fields_boost, array $fields_boost_multiplier ) {
1930
		foreach( $fields_boost as $field_name => $field_boost ) {
1931
			if ( isset( $fields_boost_multiplier[ $field_name ] ) ) {
1932
				$fields_boost[ $field_name ] *= $fields_boost_multiplier[ $field_name ];
1933
			}
1934
1935
			// Set a floor and format the number as string
1936
			$fields_boost[ $field_name ] = number_format(
1937
				max( 0.001, $fields_boost[ $field_name ] ),
1938
				3, '.', ''
1939
			);
1940
		}
1941
1942
		return $fields_boost;
1943
	}
1944
}
1945