Completed
Push — add/instant-search-blog-id ( 679e07...66a923 )
by
unknown
20:57 queued 14:27
created

Jetpack_Search::do_search()   C

Complexity

Conditions 14
Paths 81

Size

Total Lines 107

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
nc 81
nop 1
dl 0
loc 107
rs 5.0133
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
				$_blog_id = Jetpack::get_option( 'id' );
208
				// This is probably a temporary filter for testing the prototype.
209
				$options = array(
210
					'siteId' => $_blog_id,
211
				);
212
				$options = apply_filters( 'jetpack_instant_search_options', $options );
213
214
				wp_localize_script(
215
					'jetpack-instant-search',
216
					'jetpack_instant_search_options',
217
					$options
218
				);
219
			}
220
221
			$style_relative_path = '_inc/build/instant-search/instant-search.min.css';
222 View Code Duplication
			if ( file_exists( JETPACK__PLUGIN_DIR . $script_relative_path ) ) {
223
				$style_version = self::get_asset_version( $style_relative_path );
224
				$style_path    = plugins_url( $style_relative_path, JETPACK__PLUGIN_FILE );
225
				wp_enqueue_style( 'jetpack-instant-search', $style_path, array(), $style_version );
226
			}
227
		}
228
	}
229
230
	/**
231
	 * Get the version number to use when loading the file. Allows us to bypass cache when developing.
232
	 *
233
	 * @param string $file Path of the file we are looking for.
234
	 * @return string $script_version Version number.
235
	 */
236
	public static function get_asset_version( $file ) {
237
		return Jetpack::is_development_version() && file_exists( JETPACK__PLUGIN_DIR . $file )
238
			? filemtime( JETPACK__PLUGIN_DIR . $file )
239
			: JETPACK__VERSION;
240
	}
241
242
	/**
243
	 * When an Elasticsearch query fails, this stores it and enqueues some debug information in the footer.
244
	 *
245
	 * @since 5.6.0
246
	 *
247
	 * @param array $meta Information about the failure.
248
	 */
249
	public function store_query_failure( $meta ) {
250
		$this->last_query_failure_info = $meta;
251
		add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
252
	}
253
254
	/**
255
	 * Outputs information about the last Elasticsearch failure.
256
	 *
257
	 * @since 5.6.0
258
	 */
259
	public function print_query_failure() {
260
		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...
261
			printf(
262
				'<!-- Jetpack Search failed with code %s: %s - %s -->',
263
				esc_html( $this->last_query_failure_info['response_code'] ),
264
				esc_html( $this->last_query_failure_info['json']['error'] ),
265
				esc_html( $this->last_query_failure_info['json']['message'] )
266
			);
267
		}
268
	}
269
270
	/**
271
	 * Stores information about the last Elasticsearch query and enqueues some debug information in the footer.
272
	 *
273
	 * @since 5.6.0
274
	 *
275
	 * @param array $meta Information about the query.
276
	 */
277
	public function store_last_query_info( $meta ) {
278
		$this->last_query_info = $meta;
279
		add_action( 'wp_footer', array( $this, 'print_query_success' ) );
280
	}
281
282
	/**
283
	 * Outputs information about the last Elasticsearch search.
284
	 *
285
	 * @since 5.6.0
286
	 */
287
	public function print_query_success() {
288
		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...
289
			printf(
290
				'<!-- Jetpack Search took %s ms, ES time %s ms -->',
291
				intval( $this->last_query_info['elapsed_time'] ),
292
				esc_html( $this->last_query_info['es_time'] )
293
			);
294
295
			if ( isset( $_GET['searchdebug'] ) ) {
296
				printf(
297
					'<!-- Query response data: %s -->',
298
					esc_html( print_r( $this->last_query_info, 1 ) )
299
				);
300
			}
301
		}
302
	}
303
304
	/**
305
	 * Returns the last query information, or false if no information was stored.
306
	 *
307
	 * @since 5.8.0
308
	 *
309
	 * @return bool|array
310
	 */
311
	public function get_last_query_info() {
312
		return empty( $this->last_query_info ) ? false : $this->last_query_info;
313
	}
314
315
	/**
316
	 * Returns the last query failure information, or false if no failure information was stored.
317
	 *
318
	 * @since 5.8.0
319
	 *
320
	 * @return bool|array
321
	 */
322
	public function get_last_query_failure_info() {
323
		return empty( $this->last_query_failure_info ) ? false : $this->last_query_failure_info;
324
	}
325
326
	/**
327
	 * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
328
	 * developers to disable filters supplied by the search widget. Useful if filters are
329
	 * being defined at the code level.
330
	 *
331
	 * @since      5.7.0
332
	 * @deprecated 5.8.0 Use Jetpack_Search_Helpers::are_filters_by_widget_disabled() directly.
333
	 *
334
	 * @return bool
335
	 */
336
	public function are_filters_by_widget_disabled() {
337
		return Jetpack_Search_Helpers::are_filters_by_widget_disabled();
338
	}
339
340
	/**
341
	 * Retrieves a list of known Jetpack search filters widget IDs, gets the filters for each widget,
342
	 * and applies those filters to this Jetpack_Search object.
343
	 *
344
	 * @since 5.7.0
345
	 */
346
	public function set_filters_from_widgets() {
347
		if ( Jetpack_Search_Helpers::are_filters_by_widget_disabled() ) {
348
			return;
349
		}
350
351
		$filters = Jetpack_Search_Helpers::get_filters_from_widgets();
352
353
		if ( ! empty( $filters ) ) {
354
			$this->set_filters( $filters );
355
		}
356
	}
357
358
	/**
359
	 * Restricts search results to certain post types via a GET argument.
360
	 *
361
	 * @since 5.8.0
362
	 *
363
	 * @param WP_Query $query A WP_Query instance.
364
	 */
365
	public function maybe_add_post_type_as_var( WP_Query $query ) {
366
		if ( $this->should_handle_query( $query ) && ! empty( $_GET['post_type'] ) ) {
367
			$post_types = ( is_string( $_GET['post_type'] ) && false !== strpos( $_GET['post_type'], ',' ) )
368
				? $post_type = explode( ',', $_GET['post_type'] )
369
				: (array) $_GET['post_type'];
370
			$post_types = array_map( 'sanitize_key', $post_types );
371
			$query->set( 'post_type', $post_types );
372
		}
373
	}
374
375
	/*
376
	 * Run a search on the WordPress.com public API.
377
	 *
378
	 * @since 5.0.0
379
	 *
380
	 * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint.
381
	 *
382
	 * @return object|WP_Error The response from the public API, or a WP_Error.
383
	 */
384
	public function search( array $es_args ) {
385
		$endpoint    = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
386
		$service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
387
388
		$do_authenticated_request = false;
389
390
		if ( class_exists( 'Automattic\\Jetpack\\Connection\\Client' ) &&
391
			isset( $es_args['authenticated_request'] ) &&
392
			true === $es_args['authenticated_request'] ) {
393
			$do_authenticated_request = true;
394
		}
395
396
		unset( $es_args['authenticated_request'] );
397
398
		$request_args = array(
399
			'headers' => array(
400
				'Content-Type' => 'application/json',
401
			),
402
			'timeout'    => 10,
403
			'user-agent' => 'jetpack_search',
404
		);
405
406
		$request_body = wp_json_encode( $es_args );
407
408
		$start_time = microtime( true );
409
410
		if ( $do_authenticated_request ) {
411
			$request_args['method'] = 'POST';
412
413
			$request = Client::wpcom_json_api_request_as_blog( $endpoint, Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
414
		} else {
415
			$request_args = array_merge( $request_args, array(
416
				'body' => $request_body,
417
			) );
418
419
			$request = wp_remote_post( $service_url, $request_args );
420
		}
421
422
		$end_time = microtime( true );
423
424
		if ( is_wp_error( $request ) ) {
425
			return $request;
426
		}
427
428
		$response_code = wp_remote_retrieve_response_code( $request );
429
430
		$response = json_decode( wp_remote_retrieve_body( $request ), true );
431
432
		$took = is_array( $response ) && ! empty( $response['took'] )
433
			? $response['took']
434
			: null;
435
436
		$query = array(
437
			'args'          => $es_args,
438
			'response'      => $response,
439
			'response_code' => $response_code,
440
			'elapsed_time'  => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms.
441
			'es_time'       => $took,
442
			'url'           => $service_url,
443
		);
444
445
		/**
446
		 * Fires after a search request has been performed.
447
		 *
448
		 * Includes the following info in the $query parameter:
449
		 *
450
		 * array args Array of Elasticsearch arguments for the search
451
		 * array response Raw API response, JSON decoded
452
		 * int response_code HTTP response code of the request
453
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
454
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
455
		 * string url API url that was queried
456
		 *
457
		 * @module search
458
		 *
459
		 * @since  5.0.0
460
		 * @since  5.8.0 This action now fires on all queries instead of just successful queries.
461
		 *
462
		 * @param array $query Array of information about the query performed
463
		 */
464
		do_action( 'did_jetpack_search_query', $query );
465
466
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
467
			/**
468
			 * Fires after a search query request has failed
469
			 *
470
			 * @module search
471
			 *
472
			 * @since  5.6.0
473
			 *
474
			 * @param array Array containing the response code and response from the failed search query
475
			 */
476
			do_action( 'failed_jetpack_search_query', array(
477
				'response_code' => $response_code,
478
				'json'          => $response,
479
			) );
480
481
			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...
482
		}
483
484
		return $response;
485
	}
486
487
	/**
488
	 * Bypass the normal Search query and offload it to Jetpack servers.
489
	 *
490
	 * This is the main hook of the plugin and is responsible for returning the posts that match the search query.
491
	 *
492
	 * @since 5.0.0
493
	 *
494
	 * @param array    $posts Current array of posts (still pre-query).
495
	 * @param WP_Query $query The WP_Query being filtered.
496
	 *
497
	 * @return array Array of matching posts.
498
	 */
499
	public function filter__posts_pre_query( $posts, $query ) {
500
		if ( ! $this->should_handle_query( $query ) ) {
501
			return $posts;
502
		}
503
504
		$this->do_search( $query );
505
506
		if ( ! is_array( $this->search_result ) ) {
507
			return $posts;
508
		}
509
510
		// If no results, nothing to do
511
		if ( ! count( $this->search_result['results']['hits'] ) ) {
512
			return array();
513
		}
514
515
		$post_ids = array();
516
517
		foreach ( $this->search_result['results']['hits'] as $result ) {
518
			$post_ids[] = (int) $result['fields']['post_id'];
519
		}
520
521
		// Query all posts now
522
		$args = array(
523
			'post__in'            => $post_ids,
524
			'orderby'             => 'post__in',
525
			'perm'                => 'readable',
526
			'post_type'           => 'any',
527
			'ignore_sticky_posts' => true,
528
			'suppress_filters'    => true,
529
		);
530
531
		$posts_query = new WP_Query( $args );
532
533
		// 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.
534
		$query->found_posts   = $this->found_posts;
535
		$query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
536
537
		return $posts_query->posts;
538
	}
539
540
	/**
541
	 * Build up the search, then run it against the Jetpack servers.
542
	 *
543
	 * @since 5.0.0
544
	 *
545
	 * @param WP_Query $query The original WP_Query to use for the parameters of our search.
546
	 */
547
	public function do_search( WP_Query $query ) {
548
		if ( ! $this->should_handle_query( $query ) ) {
549
			return;
550
		}
551
552
		$page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
553
554
		// Get maximum allowed offset and posts per page values for the API.
555
		$max_offset         = Jetpack_Search_Helpers::get_max_offset();
556
		$max_posts_per_page = Jetpack_Search_Helpers::get_max_posts_per_page();
557
558
		$posts_per_page = $query->get( 'posts_per_page' );
559
		if ( $posts_per_page > $max_posts_per_page ) {
560
			$posts_per_page = $max_posts_per_page;
561
		}
562
563
		// Start building the WP-style search query args.
564
		// They'll be translated to ES format args later.
565
		$es_wp_query_args = array(
566
			'query'          => $query->get( 's' ),
567
			'posts_per_page' => $posts_per_page,
568
			'paged'          => $page,
569
			'orderby'        => $query->get( 'orderby' ),
570
			'order'          => $query->get( 'order' ),
571
		);
572
573
		if ( ! empty( $this->aggregations ) ) {
574
			$es_wp_query_args['aggregations'] = $this->aggregations;
575
		}
576
577
		// Did we query for authors?
578
		if ( $query->get( 'author_name' ) ) {
579
			$es_wp_query_args['author_name'] = $query->get( 'author_name' );
580
		}
581
582
		$es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
583
		$es_wp_query_args['terms']     = $this->get_es_wp_query_terms_for_query( $query );
584
585
		/**
586
		 * Modify the search query parameters, such as controlling the post_type.
587
		 *
588
		 * These arguments are in the format of WP_Query arguments
589
		 *
590
		 * @module search
591
		 *
592
		 * @since  5.0.0
593
		 *
594
		 * @param array    $es_wp_query_args The current query args, in WP_Query format.
595
		 * @param WP_Query $query            The original WP_Query object.
596
		 */
597
		$es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
598
599
		// If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
600
		// capped at Jetpack_Search_Helpers::get_max_offset(), so a high page would always return the last page of results otherwise.
601
		if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $max_offset ) {
602
			$query->set_404();
603
604
			return;
605
		}
606
607
		// If there were no post types returned, then 404 to avoid querying against non-public post types, which could
608
		// happen if we don't add the post type restriction to the ES query.
609
		if ( empty( $es_wp_query_args['post_type'] ) ) {
610
			$query->set_404();
611
612
			return;
613
		}
614
615
		// Convert the WP-style args into ES args.
616
		$es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
617
618
		//Only trust ES to give us IDs, not the content since it is a mirror
619
		$es_query_args['fields'] = array(
620
			'post_id',
621
		);
622
623
		/**
624
		 * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
625
		 *
626
		 * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
627
		 *
628
		 * @module search
629
		 *
630
		 * @since  5.0.0
631
		 *
632
		 * @param array    $es_query_args The raw Elasticsearch query args.
633
		 * @param WP_Query $query         The original WP_Query object.
634
		 */
635
		$es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
636
637
		// Do the actual search query!
638
		$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...
639
640
		if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
641
			$this->found_posts = 0;
642
643
			return;
644
		}
645
646
		// If we have aggregations, fix the ordering to match the input order (ES doesn't guarantee the return order).
647
		if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
648
			$this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
649
		}
650
651
		// Total number of results for paging purposes. Capped at $max_offset + $posts_per_page, as deep paging gets quite expensive.
652
		$this->found_posts = min( $this->search_result['results']['total'], $max_offset + $posts_per_page );
653
	}
654
655
	/**
656
	 * If the query has already been run before filters have been updated, then we need to re-run the query
657
	 * to get the latest aggregations.
658
	 *
659
	 * This is especially useful for supporting widget management in the customizer.
660
	 *
661
	 * @since 5.8.0
662
	 *
663
	 * @return bool Whether the query was successful or not.
664
	 */
665
	public function update_search_results_aggregations() {
666
		if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
667
			return false;
668
		}
669
670
		$es_args = $this->last_query_info['args'];
671
		$builder = new Jetpack_WPES_Query_Builder();
672
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
673
		$es_args['aggregations'] = $builder->build_aggregation();
674
675
		$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...
676
677
		return ! is_wp_error( $this->search_result );
678
	}
679
680
	/**
681
	 * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style Elasticsearch term arguments for the search.
682
	 *
683
	 * @since 5.0.0
684
	 *
685
	 * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query.
686
	 *
687
	 * @return array The new WP-style Elasticsearch arguments (that will be converted into 'real' Elasticsearch arguments).
688
	 */
689
	public function get_es_wp_query_terms_for_query( WP_Query $query ) {
690
		$args = array();
691
692
		$the_tax_query = $query->tax_query;
693
694
		if ( ! $the_tax_query ) {
695
			return $args;
696
		}
697
698
699
		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...
700
			return $args;
701
		}
702
703
		$args = array();
704
705
		foreach ( $the_tax_query->queries as $tax_query ) {
706
			// Right now we only support slugs...see note above
707
			if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
708
				continue;
709
			}
710
711
			$taxonomy = $tax_query['taxonomy'];
712
713 View Code Duplication
			if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
714
				$args[ $taxonomy ] = array();
715
			}
716
717
			$args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
718
		}
719
720
		return $args;
721
	}
722
723
	/**
724
	 * Parse out the post type from a WP_Query.
725
	 *
726
	 * Only allows post types that are not marked as 'exclude_from_search'.
727
	 *
728
	 * @since 5.0.0
729
	 *
730
	 * @param WP_Query $query Original WP_Query object.
731
	 *
732
	 * @return array Array of searchable post types corresponding to the original query.
733
	 */
734
	public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
735
		$post_types = $query->get( 'post_type' );
736
737
		// If we're searching 'any', we want to only pass searchable post types to Elasticsearch.
738
		if ( 'any' === $post_types ) {
739
			$post_types = array_values( get_post_types( array(
740
				'exclude_from_search' => false,
741
			) ) );
742
		}
743
744
		if ( ! is_array( $post_types ) ) {
745
			$post_types = array( $post_types );
746
		}
747
748
		$post_types = array_unique( $post_types );
749
750
		$sanitized_post_types = array();
751
752
		// Make sure the post types are queryable.
753
		foreach ( $post_types as $post_type ) {
754
			if ( ! $post_type ) {
755
				continue;
756
			}
757
758
			$post_type_object = get_post_type_object( $post_type );
759
			if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
760
				continue;
761
			}
762
763
			$sanitized_post_types[] = $post_type;
764
		}
765
766
		return $sanitized_post_types;
767
	}
768
769
	/**
770
	 * Get the Elasticsearch result.
771
	 *
772
	 * @since 5.0.0
773
	 *
774
	 * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response.
775
	 *
776
	 * @return array|bool The search results, or false if there was a failure.
777
	 */
778
	public function get_search_result( $raw = false ) {
779
		if ( $raw ) {
780
			return $this->search_result;
781
		}
782
783
		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;
784
	}
785
786
	/**
787
	 * Add the date portion of a WP_Query onto the query args.
788
	 *
789
	 * @since 5.0.0
790
	 *
791
	 * @param array    $es_wp_query_args The Elasticsearch query arguments in WordPress form.
792
	 * @param WP_Query $query            The original WP_Query.
793
	 *
794
	 * @return array The es wp query args, with date filters added (as needed).
795
	 */
796
	public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
797
		if ( $query->get( 'year' ) ) {
798
			if ( $query->get( 'monthnum' ) ) {
799
				// Padding
800
				$date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
801
802
				if ( $query->get( 'day' ) ) {
803
					// Padding
804
					$date_day = sprintf( '%02d', $query->get( 'day' ) );
805
806
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
807
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
808
				} else {
809
					$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
810
811
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
812
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
813
				}
814
			} else {
815
				$date_start = $query->get( 'year' ) . '-01-01 00:00:00';
816
				$date_end   = $query->get( 'year' ) . '-12-31 23:59:59';
817
			}
818
819
			$es_wp_query_args['date_range'] = array(
820
				'field' => 'date',
821
				'gte'   => $date_start,
822
				'lte'   => $date_end,
823
			);
824
		}
825
826
		return $es_wp_query_args;
827
	}
828
829
	/**
830
	 * Converts WP_Query style args to Elasticsearch args.
831
	 *
832
	 * @since 5.0.0
833
	 *
834
	 * @param array $args Array of WP_Query style arguments.
835
	 *
836
	 * @return array Array of ES style query arguments.
837
	 */
838
	public function convert_wp_es_to_es_args( array $args ) {
839
		jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-parser' );
840
841
		$defaults = array(
842
			'blog_id'        => get_current_blog_id(),
843
			'query'          => null,    // Search phrase
844
			'query_fields'   => array(), // list of fields to search
845
			'excess_boost'   => array(), // map of field to excess boost values (multiply)
846
			'post_type'      => null,    // string or an array
847
			'terms'          => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) )
848
			'author'         => null,    // id or an array of ids
849
			'author_name'    => array(), // string or an array
850
			'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'
851
			'orderby'        => null,    // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
852
			'order'          => 'DESC',
853
			'posts_per_page' => 10,
854
			'offset'         => null,
855
			'paged'          => null,
856
			/**
857
			 * Aggregations. Examples:
858
			 * array(
859
			 *     'Tag'       => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
860
			 *     'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
861
			 * );
862
			 */
863
			'aggregations'   => null,
864
		);
865
866
		$args = wp_parse_args( $args, $defaults );
867
868
		$parser = new Jetpack_WPES_Search_Query_Parser( $args['query'], array( get_locale() ) );
869
870
		if ( empty( $args['query_fields'] ) ) {
871
			if ( defined( 'JETPACK_SEARCH_VIP_INDEX' ) && JETPACK_SEARCH_VIP_INDEX ) {
872
				// VIP indices do not have per language fields
873
				$match_fields = $this->_get_caret_boosted_fields(
874
					array(
875
						'title'         => 0.1,
876
						'content'       => 0.1,
877
						'excerpt'       => 0.1,
878
						'tag.name'      => 0.1,
879
						'category.name' => 0.1,
880
						'author_login'  => 0.1,
881
						'author'        => 0.1,
882
					)
883
				);
884
885
				$boost_fields = $this->_get_caret_boosted_fields(
886
					$this->_apply_boosts_multiplier( array(
887
						'title'         => 2,
888
						'tag.name'      => 1,
889
						'category.name' => 1,
890
						'author_login'  => 1,
891
						'author'        => 1,
892
					), $args['excess_boost'] )
893
				);
894
895
				$boost_phrase_fields = $this->_get_caret_boosted_fields(
896
					array(
897
						'title'         => 1,
898
						'content'       => 1,
899
						'excerpt'       => 1,
900
						'tag.name'      => 1,
901
						'category.name' => 1,
902
						'author'        => 1,
903
					)
904
				);
905
			} else {
906
				$match_fields = $parser->merge_ml_fields(
907
					array(
908
						'title'         => 0.1,
909
						'content'       => 0.1,
910
						'excerpt'       => 0.1,
911
						'tag.name'      => 0.1,
912
						'category.name' => 0.1,
913
					),
914
					$this->_get_caret_boosted_fields( array(
915
						'author_login'  => 0.1,
916
						'author'        => 0.1,
917
					) )
918
				);
919
920
				$boost_fields = $parser->merge_ml_fields(
921
					$this->_apply_boosts_multiplier( array(
922
						'title'         => 2,
923
						'tag.name'      => 1,
924
						'category.name' => 1,
925
					), $args['excess_boost'] ),
926
					$this->_get_caret_boosted_fields( $this->_apply_boosts_multiplier( array(
927
						'author_login'  => 1,
928
						'author'        => 1,
929
					), $args['excess_boost'] ) )
930
				);
931
932
				$boost_phrase_fields = $parser->merge_ml_fields(
933
					array(
934
						'title'         => 1,
935
						'content'       => 1,
936
						'excerpt'       => 1,
937
						'tag.name'      => 1,
938
						'category.name' => 1,
939
					),
940
					$this->_get_caret_boosted_fields( array(
941
						'author'        => 1,
942
					) )
943
				);
944
			}
945
		} else {
946
			// If code is overriding the fields, then use that. Important for backwards compatibility.
947
			$match_fields        = $args['query_fields'];
948
			$boost_phrase_fields = $match_fields;
949
			$boost_fields        = null;
950
		}
951
952
		$parser->phrase_filter( array(
953
			'must_query_fields'  => $match_fields,
954
			'boost_query_fields' => null,
955
		) );
956
		$parser->remaining_query( array(
957
			'must_query_fields'  => $match_fields,
958
			'boost_query_fields' => $boost_fields,
959
		) );
960
961
		// Boost on phrase matches
962
		$parser->remaining_query( array(
963
			'boost_query_fields' => $boost_phrase_fields,
964
			'boost_query_type'   => 'phrase',
965
		) );
966
967
		/**
968
		 * Modify the recency decay parameters for the search query.
969
		 *
970
		 * The recency decay lowers the search scores based on the age of a post relative to an origin date. Basic adjustments:
971
		 *  - origin: A date. Posts with this date will have the highest score and no decay applied. Default is today.
972
		 *  - 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.
973
		 *  - scale: The number of days/months/years from the origin+offset at which the decay will equal the decay param. Default 360d
974
		 *  - decay: The amount of decay applied at offset+scale. Default 0.9.
975
		 *
976
		 * 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}
977
		 *
978
		 * @module search
979
		 *
980
		 * @since  5.8.0
981
		 *
982
		 * @param array $decay_params The decay parameters.
983
		 * @param array $args         The WP query parameters.
984
		 */
985
		$decay_params = apply_filters(
986
			'jetpack_search_recency_score_decay',
987
			array(
988
				'origin' => date( 'Y-m-d' ),
989
				'scale'  => '360d',
990
				'decay'  => 0.9,
991
			),
992
			$args
993
		);
994
995
		if ( ! empty( $decay_params ) ) {
996
			// Newer content gets weighted slightly higher
997
			$parser->add_decay( 'gauss', array(
998
				'date_gmt' => $decay_params
999
			) );
1000
		}
1001
1002
		$es_query_args = array(
1003
			'blog_id' => absint( $args['blog_id'] ),
1004
			'size'    => absint( $args['posts_per_page'] ),
1005
		);
1006
1007
		// ES "from" arg (offset)
1008
		if ( $args['offset'] ) {
1009
			$es_query_args['from'] = absint( $args['offset'] );
1010
		} elseif ( $args['paged'] ) {
1011
			$es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
1012
		}
1013
1014
		$es_query_args['from'] = min( $es_query_args['from'], Jetpack_Search_Helpers::get_max_offset() );
1015
1016
		if ( ! is_array( $args['author_name'] ) ) {
1017
			$args['author_name'] = array( $args['author_name'] );
1018
		}
1019
1020
		// ES stores usernames, not IDs, so transform
1021
		if ( ! empty( $args['author'] ) ) {
1022
			if ( ! is_array( $args['author'] ) ) {
1023
				$args['author'] = array( $args['author'] );
1024
			}
1025
1026
			foreach ( $args['author'] as $author ) {
1027
				$user = get_user_by( 'id', $author );
1028
1029
				if ( $user && ! empty( $user->user_login ) ) {
1030
					$args['author_name'][] = $user->user_login;
1031
				}
1032
			}
1033
		}
1034
1035
		//////////////////////////////////////////////////
1036
		// Build the filters from the query elements.
1037
		// Filters rock because they are cached from one query to the next
1038
		// but they are cached as individual filters, rather than all combined together.
1039
		// May get performance boost by also caching the top level boolean filter too.
1040
1041
		if ( $args['post_type'] ) {
1042
			if ( ! is_array( $args['post_type'] ) ) {
1043
				$args['post_type'] = array( $args['post_type'] );
1044
			}
1045
1046
			$parser->add_filter( array(
1047
				'terms' => array(
1048
					'post_type' => $args['post_type'],
1049
				),
1050
			) );
1051
		}
1052
1053
		if ( $args['author_name'] ) {
1054
			$parser->add_filter( array(
1055
				'terms' => array(
1056
					'author_login' => $args['author_name'],
1057
				),
1058
			) );
1059
		}
1060
1061
		if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
1062
			$field = $args['date_range']['field'];
1063
1064
			unset( $args['date_range']['field'] );
1065
1066
			$parser->add_filter( array(
1067
				'range' => array(
1068
					$field => $args['date_range'],
1069
				),
1070
			) );
1071
		}
1072
1073
		if ( is_array( $args['terms'] ) ) {
1074
			foreach ( $args['terms'] as $tax => $terms ) {
1075
				$terms = (array) $terms;
1076
1077
				if ( count( $terms ) && mb_strlen( $tax ) ) {
1078 View Code Duplication
					switch ( $tax ) {
1079
						case 'post_tag':
1080
							$tax_fld = 'tag.slug';
1081
1082
							break;
1083
1084
						case 'category':
1085
							$tax_fld = 'category.slug';
1086
1087
							break;
1088
1089
						default:
1090
							$tax_fld = 'taxonomy.' . $tax . '.slug';
1091
1092
							break;
1093
					}
1094
1095
					foreach ( $terms as $term ) {
1096
						$parser->add_filter( array(
1097
							'term' => array(
1098
								$tax_fld => $term,
1099
							),
1100
						) );
1101
					}
1102
				}
1103
			}
1104
		}
1105
1106
		if ( ! $args['orderby'] ) {
1107
			if ( $args['query'] ) {
1108
				$args['orderby'] = array( 'relevance' );
1109
			} else {
1110
				$args['orderby'] = array( 'date' );
1111
			}
1112
		}
1113
1114
		// Validate the "order" field
1115
		switch ( strtolower( $args['order'] ) ) {
1116
			case 'asc':
1117
				$args['order'] = 'asc';
1118
				break;
1119
1120
			case 'desc':
1121
			default:
1122
				$args['order'] = 'desc';
1123
				break;
1124
		}
1125
1126
		$es_query_args['sort'] = array();
1127
1128
		foreach ( (array) $args['orderby'] as $orderby ) {
1129
			// Translate orderby from WP field to ES field
1130
			switch ( $orderby ) {
1131
				case 'relevance' :
1132
					//never order by score ascending
1133
					$es_query_args['sort'][] = array(
1134
						'_score' => array(
1135
							'order' => 'desc',
1136
						),
1137
					);
1138
1139
					break;
1140
1141 View Code Duplication
				case 'date' :
1142
					$es_query_args['sort'][] = array(
1143
						'date' => array(
1144
							'order' => $args['order'],
1145
						),
1146
					);
1147
1148
					break;
1149
1150 View Code Duplication
				case 'ID' :
1151
					$es_query_args['sort'][] = array(
1152
						'id' => array(
1153
							'order' => $args['order'],
1154
						),
1155
					);
1156
1157
					break;
1158
1159
				case 'author' :
1160
					$es_query_args['sort'][] = array(
1161
						'author.raw' => array(
1162
							'order' => $args['order'],
1163
						),
1164
					);
1165
1166
					break;
1167
			} // End switch().
1168
		} // End foreach().
1169
1170
		if ( empty( $es_query_args['sort'] ) ) {
1171
			unset( $es_query_args['sort'] );
1172
		}
1173
1174
		// Aggregations
1175
		if ( ! empty( $args['aggregations'] ) ) {
1176
			$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...
1177
		}
1178
1179
		$es_query_args['filter']       = $parser->build_filter();
1180
		$es_query_args['query']        = $parser->build_query();
1181
		$es_query_args['aggregations'] = $parser->build_aggregation();
1182
1183
		return $es_query_args;
1184
	}
1185
1186
	/**
1187
	 * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1188
	 *
1189
	 * @since 5.0.0
1190
	 *
1191
	 * @param array                      $aggregations Array of aggregations (filters) to add to the Jetpack_WPES_Query_Builder.
1192
	 * @param Jetpack_WPES_Query_Builder $builder      The builder instance that is creating the Elasticsearch query.
1193
	 */
1194
	public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
1195
		foreach ( $aggregations as $label => $aggregation ) {
1196
			switch ( $aggregation['type'] ) {
1197
				case 'taxonomy':
1198
					$this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1199
1200
					break;
1201
1202
				case 'post_type':
1203
					$this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1204
1205
					break;
1206
1207
				case 'date_histogram':
1208
					$this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1209
1210
					break;
1211
			}
1212
		}
1213
	}
1214
1215
	/**
1216
	 * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1217
	 *
1218
	 * @since 5.0.0
1219
	 *
1220
	 * @param array                      $aggregation The aggregation to add to the query builder.
1221
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1222
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1223
	 */
1224
	public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1225
		$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...
1226
1227
		switch ( $aggregation['taxonomy'] ) {
1228
			case 'post_tag':
1229
				$field = 'tag';
1230
				break;
1231
1232
			case 'category':
1233
				$field = 'category';
1234
				break;
1235
1236
			default:
1237
				$field = 'taxonomy.' . $aggregation['taxonomy'];
1238
				break;
1239
		}
1240
1241
		$builder->add_aggs( $label, array(
1242
			'terms' => array(
1243
				'field' => $field . '.slug',
1244
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1245
			),
1246
		) );
1247
	}
1248
1249
	/**
1250
	 * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1251
	 *
1252
	 * @since 5.0.0
1253
	 *
1254
	 * @param array                      $aggregation The aggregation to add to the query builder.
1255
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1256
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1257
	 */
1258
	public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1259
		$builder->add_aggs( $label, array(
1260
			'terms' => array(
1261
				'field' => 'post_type',
1262
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1263
			),
1264
		) );
1265
	}
1266
1267
	/**
1268
	 * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1269
	 *
1270
	 * @since 5.0.0
1271
	 *
1272
	 * @param array                      $aggregation The aggregation to add to the query builder.
1273
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1274
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1275
	 */
1276
	public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1277
		$args = array(
1278
			'interval' => $aggregation['interval'],
1279
			'field'    => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' == $aggregation['field'] ) ? 'date_gmt' : 'date',
1280
		);
1281
1282
		if ( isset( $aggregation['min_doc_count'] ) ) {
1283
			$args['min_doc_count'] = intval( $aggregation['min_doc_count'] );
1284
		} else {
1285
			$args['min_doc_count'] = 1;
1286
		}
1287
1288
		$builder->add_aggs( $label, array(
1289
			'date_histogram' => $args,
1290
		) );
1291
	}
1292
1293
	/**
1294
	 * And an existing filter object with a list of additional filters.
1295
	 *
1296
	 * Attempts to optimize the filters somewhat.
1297
	 *
1298
	 * @since 5.0.0
1299
	 *
1300
	 * @param array $curr_filter The existing filters to build upon.
1301
	 * @param array $filters     The new filters to add.
1302
	 *
1303
	 * @return array The resulting merged filters.
1304
	 */
1305
	public static function and_es_filters( array $curr_filter, array $filters ) {
1306
		if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
1307
			if ( 1 === count( $filters ) ) {
1308
				return $filters[0];
1309
			}
1310
1311
			return array(
1312
				'and' => $filters,
1313
			);
1314
		}
1315
1316
		return array(
1317
			'and' => array_merge( array( $curr_filter ), $filters ),
1318
		);
1319
	}
1320
1321
	/**
1322
	 * Set the available filters for the search.
1323
	 *
1324
	 * These get rendered via the Jetpack_Search_Widget() widget.
1325
	 *
1326
	 * Behind the scenes, these are implemented using Elasticsearch Aggregations.
1327
	 *
1328
	 * If you do not require counts of how many documents match each filter, please consider using regular WP Query
1329
	 * arguments instead, such as via the jetpack_search_es_wp_query_args filter
1330
	 *
1331
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1332
	 *
1333
	 * @since  5.0.0
1334
	 *
1335
	 * @param array $aggregations Array of filters (aggregations) to apply to the search
1336
	 */
1337
	public function set_filters( array $aggregations ) {
1338
		foreach ( (array) $aggregations as $key => $agg ) {
1339
			if ( empty( $agg['name'] ) ) {
1340
				$aggregations[ $key ]['name'] = $key;
1341
			}
1342
		}
1343
		$this->aggregations = $aggregations;
1344
	}
1345
1346
	/**
1347
	 * Set the search's facets (deprecated).
1348
	 *
1349
	 * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead.
1350
	 *
1351
	 * @see        Jetpack_Search::set_filters()
1352
	 *
1353
	 * @param array $facets Array of facets to apply to the search.
1354
	 */
1355
	public function set_facets( array $facets ) {
1356
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
1357
1358
		$this->set_filters( $facets );
1359
	}
1360
1361
	/**
1362
	 * Get the raw Aggregation results from the Elasticsearch response.
1363
	 *
1364
	 * @since  5.0.0
1365
	 *
1366
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1367
	 *
1368
	 * @return array Array of Aggregations performed on the search.
1369
	 */
1370
	public function get_search_aggregations_results() {
1371
		$aggregations = array();
1372
1373
		$search_result = $this->get_search_result();
1374
1375
		if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
1376
			$aggregations = $search_result['aggregations'];
1377
		}
1378
1379
		return $aggregations;
1380
	}
1381
1382
	/**
1383
	 * Get the raw Facet results from the Elasticsearch response.
1384
	 *
1385
	 * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead.
1386
	 *
1387
	 * @see        Jetpack_Search::get_search_aggregations_results()
1388
	 *
1389
	 * @return array Array of Facets performed on the search.
1390
	 */
1391
	public function get_search_facets() {
1392
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
1393
1394
		return $this->get_search_aggregations_results();
1395
	}
1396
1397
	/**
1398
	 * Get the results of the Filters performed, including the number of matching documents.
1399
	 *
1400
	 * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
1401
	 * matching buckets, the url for applying/removing each bucket, etc.
1402
	 *
1403
	 * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
1404
	 * member if you need to access the raw filters set in Jetpack_Search::set_filters().
1405
	 *
1406
	 * @since 5.0.0
1407
	 *
1408
	 * @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...
1409
	 *
1410
	 * @return array Array of filters applied and info about them.
1411
	 */
1412
	public function get_filters( WP_Query $query = null ) {
1413
		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...
1414
			global $wp_query;
1415
1416
			$query = $wp_query;
1417
		}
1418
1419
		$aggregation_data = $this->aggregations;
1420
1421
		if ( empty( $aggregation_data ) ) {
1422
			return $aggregation_data;
1423
		}
1424
1425
		$aggregation_results = $this->get_search_aggregations_results();
1426
1427
		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...
1428
			return $aggregation_data;
1429
		}
1430
1431
		// NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES
1432
		foreach ( $aggregation_results as $label => $aggregation ) {
1433
			if ( empty( $aggregation ) ) {
1434
				continue;
1435
			}
1436
1437
			$type = $this->aggregations[ $label ]['type'];
1438
1439
			$aggregation_data[ $label ]['buckets'] = array();
1440
1441
			$existing_term_slugs = array();
1442
1443
			$tax_query_var = null;
1444
1445
			// Figure out which terms are active in the query, for this taxonomy
1446
			if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
1447
				$tax_query_var = $this->get_taxonomy_query_var( $this->aggregations[ $label ]['taxonomy'] );
1448
1449
				if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
1450
					foreach ( $query->tax_query->queries as $tax_query ) {
1451
						if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
1452
						     'slug' === $tax_query['field'] &&
1453
						     is_array( $tax_query['terms'] ) ) {
1454
							$existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
1455
						}
1456
					}
1457
				}
1458
			}
1459
1460
			// Now take the resulting found aggregation items and generate the additional info about them, such as activation/deactivation url, name, count, etc.
1461
			$buckets = array();
1462
1463
			if ( ! empty( $aggregation['buckets'] ) ) {
1464
				$buckets = (array) $aggregation['buckets'];
1465
			}
1466
1467
			if ( 'date_histogram' == $type ) {
1468
				//re-order newest to oldest
1469
				$buckets = array_reverse( $buckets );
1470
			}
1471
1472
			// Some aggregation types like date_histogram don't support the max results parameter
1473
			if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
1474
				$buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
1475
			}
1476
1477
			foreach ( $buckets as $item ) {
1478
				$query_vars = array();
1479
				$active     = false;
1480
				$remove_url = null;
1481
				$name       = '';
1482
1483
				// What type was the original aggregation?
1484
				switch ( $type ) {
1485
					case 'taxonomy':
1486
						$taxonomy = $this->aggregations[ $label ]['taxonomy'];
1487
1488
						$term = get_term_by( 'slug', $item['key'], $taxonomy );
1489
1490
						if ( ! $term || ! $tax_query_var ) {
1491
							continue 2; // switch() is considered a looping structure
1492
						}
1493
1494
						$query_vars = array(
1495
							$tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
1496
						);
1497
1498
						$name = $term->name;
1499
1500
						// Let's determine if this term is active or not
1501
1502
						if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
1503
							$active = true;
1504
1505
							$slug_count = count( $existing_term_slugs );
1506
1507 View Code Duplication
							if ( $slug_count > 1 ) {
1508
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1509
									$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...
1510
									rawurlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) )
1511
								);
1512
							} else {
1513
								$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...
1514
							}
1515
						}
1516
1517
						break;
1518
1519
					case 'post_type':
1520
						$post_type = get_post_type_object( $item['key'] );
1521
1522
						if ( ! $post_type || $post_type->exclude_from_search ) {
1523
							continue 2;  // switch() is considered a looping structure
1524
						}
1525
1526
						$query_vars = array(
1527
							'post_type' => $item['key'],
1528
						);
1529
1530
						$name = $post_type->labels->singular_name;
1531
1532
						// Is this post type active on this search?
1533
						$post_types = $query->get( 'post_type' );
1534
1535
						if ( ! is_array( $post_types ) ) {
1536
							$post_types = array( $post_types );
1537
						}
1538
1539
						if ( in_array( $item['key'], $post_types ) ) {
1540
							$active = true;
1541
1542
							$post_type_count = count( $post_types );
1543
1544
							// 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
1545 View Code Duplication
							if ( $post_type_count > 1 ) {
1546
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1547
									'post_type',
1548
									rawurlencode( implode( ',', array_diff( $post_types, array( $item['key'] ) ) ) )
1549
								);
1550
							} else {
1551
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
1552
							}
1553
						}
1554
1555
						break;
1556
1557
					case 'date_histogram':
1558
						$timestamp = $item['key'] / 1000;
1559
1560
						$current_year  = $query->get( 'year' );
1561
						$current_month = $query->get( 'monthnum' );
1562
						$current_day   = $query->get( 'day' );
1563
1564
						switch ( $this->aggregations[ $label ]['interval'] ) {
1565
							case 'year':
1566
								$year = (int) date( 'Y', $timestamp );
1567
1568
								$query_vars = array(
1569
									'year'     => $year,
1570
									'monthnum' => false,
1571
									'day'      => false,
1572
								);
1573
1574
								$name = $year;
1575
1576
								// Is this year currently selected?
1577
								if ( ! empty( $current_year ) && (int) $current_year === $year ) {
1578
									$active = true;
1579
1580
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
1581
								}
1582
1583
								break;
1584
1585
							case 'month':
1586
								$year  = (int) date( 'Y', $timestamp );
1587
								$month = (int) date( 'n', $timestamp );
1588
1589
								$query_vars = array(
1590
									'year'     => $year,
1591
									'monthnum' => $month,
1592
									'day'      => false,
1593
								);
1594
1595
								$name = date( 'F Y', $timestamp );
1596
1597
								// Is this month currently selected?
1598
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1599
								     ! empty( $current_month ) && (int) $current_month === $month ) {
1600
									$active = true;
1601
1602
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
1603
								}
1604
1605
								break;
1606
1607
							case 'day':
1608
								$year  = (int) date( 'Y', $timestamp );
1609
								$month = (int) date( 'n', $timestamp );
1610
								$day   = (int) date( 'j', $timestamp );
1611
1612
								$query_vars = array(
1613
									'year'     => $year,
1614
									'monthnum' => $month,
1615
									'day'      => $day,
1616
								);
1617
1618
								$name = date( 'F jS, Y', $timestamp );
1619
1620
								// Is this day currently selected?
1621
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1622
								     ! empty( $current_month ) && (int) $current_month === $month &&
1623
								     ! empty( $current_day ) && (int) $current_day === $day ) {
1624
									$active = true;
1625
1626
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
1627
								}
1628
1629
								break;
1630
1631
							default:
1632
								continue 3; // switch() is considered a looping structure
1633
						} // End switch().
1634
1635
						break;
1636
1637
					default:
1638
						//continue 2; // switch() is considered a looping structure
1639
				} // End switch().
1640
1641
				// Need to urlencode param values since add_query_arg doesn't
1642
				$url_params = urlencode_deep( $query_vars );
1643
1644
				$aggregation_data[ $label ]['buckets'][] = array(
1645
					'url'        => Jetpack_Search_Helpers::add_query_arg( $url_params ),
1646
					'query_vars' => $query_vars,
1647
					'name'       => $name,
1648
					'count'      => $item['doc_count'],
1649
					'active'     => $active,
1650
					'remove_url' => $remove_url,
1651
					'type'       => $type,
1652
					'type_label' => $aggregation_data[ $label ]['name'],
1653
					'widget_id'  => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0
1654
				);
1655
			} // End foreach().
1656
		} // End foreach().
1657
1658
		/**
1659
		 * Modify the aggregation filters returned by get_filters().
1660
		 *
1661
		 * Useful if you are setting custom filters outside of the supported filters (taxonomy, post_type etc.) and
1662
		 * want to hook them up so they're returned when you call `get_filters()`.
1663
		 *
1664
		 * @module search
1665
		 *
1666
		 * @since  6.9.0
1667
		 *
1668
		 * @param array    $aggregation_data The array of filters keyed on label.
1669
		 * @param WP_Query $query            The WP_Query object.
1670
		 */
1671
		return apply_filters( 'jetpack_search_get_filters', $aggregation_data, $query );
1672
	}
1673
1674
	/**
1675
	 * Get the results of the facets performed.
1676
	 *
1677
	 * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead.
1678
	 *
1679
	 * @see        Jetpack_Search::get_filters()
1680
	 *
1681
	 * @return array $facets Array of facets applied and info about them.
1682
	 */
1683
	public function get_search_facet_data() {
1684
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
1685
1686
		return $this->get_filters();
1687
	}
1688
1689
	/**
1690
	 * Get the filters that are currently applied to this search.
1691
	 *
1692
	 * @since 5.0.0
1693
	 *
1694
	 * @return array Array of filters that were applied.
1695
	 */
1696
	public function get_active_filter_buckets() {
1697
		$active_buckets = array();
1698
1699
		$filters = $this->get_filters();
1700
1701
		if ( ! is_array( $filters ) ) {
1702
			return $active_buckets;
1703
		}
1704
1705
		foreach ( $filters as $filter ) {
1706
			if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
1707
				foreach ( $filter['buckets'] as $item ) {
1708
					if ( isset( $item['active'] ) && $item['active'] ) {
1709
						$active_buckets[] = $item;
1710
					}
1711
				}
1712
			}
1713
		}
1714
1715
		return $active_buckets;
1716
	}
1717
1718
	/**
1719
	 * Get the filters that are currently applied to this search.
1720
	 *
1721
	 * @deprecated 5.0 Please use Jetpack_Search::get_active_filter_buckets() instead.
1722
	 *
1723
	 * @see        Jetpack_Search::get_active_filter_buckets()
1724
	 *
1725
	 * @return array Array of filters that were applied.
1726
	 */
1727
	public function get_current_filters() {
1728
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
1729
1730
		return $this->get_active_filter_buckets();
1731
	}
1732
1733
	/**
1734
	 * Calculate the right query var to use for a given taxonomy.
1735
	 *
1736
	 * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter.
1737
	 *
1738
	 * @since 5.0.0
1739
	 *
1740
	 * @param string $taxonomy_name The name of the taxonomy for which to get the query var.
1741
	 *
1742
	 * @return bool|string The query var to use for this taxonomy, or false if none found.
1743
	 */
1744
	public function get_taxonomy_query_var( $taxonomy_name ) {
1745
		$taxonomy = get_taxonomy( $taxonomy_name );
1746
1747
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1748
			return false;
1749
		}
1750
1751
		/**
1752
		 * Modify the query var to use for a given taxonomy
1753
		 *
1754
		 * @module search
1755
		 *
1756
		 * @since  5.0.0
1757
		 *
1758
		 * @param string $query_var     The current query_var for the taxonomy
1759
		 * @param string $taxonomy_name The taxonomy name
1760
		 */
1761
		return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
1762
	}
1763
1764
	/**
1765
	 * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
1766
	 * which is the input order.
1767
	 *
1768
	 * Necessary because ES does not always return aggregations in the same order that you pass them in,
1769
	 * and it should be possible to control the display order easily.
1770
	 *
1771
	 * @since 5.0.0
1772
	 *
1773
	 * @param array $aggregations Aggregation results to be reordered.
1774
	 * @param array $desired      Array with keys representing the desired ordering.
1775
	 *
1776
	 * @return array A new array with reordered keys, matching those in $desired.
1777
	 */
1778
	public function fix_aggregation_ordering( array $aggregations, array $desired ) {
1779
		if ( empty( $aggregations ) || empty( $desired ) ) {
1780
			return $aggregations;
1781
		}
1782
1783
		$reordered = array();
1784
1785
		foreach ( array_keys( $desired ) as $agg_name ) {
1786
			if ( isset( $aggregations[ $agg_name ] ) ) {
1787
				$reordered[ $agg_name ] = $aggregations[ $agg_name ];
1788
			}
1789
		}
1790
1791
		return $reordered;
1792
	}
1793
1794
	/**
1795
	 * Sends events to Tracks when a search filters widget is updated.
1796
	 *
1797
	 * @since 5.8.0
1798
	 *
1799
	 * @param string $option    The option name. Only "widget_jetpack-search-filters" is cared about.
1800
	 * @param array  $old_value The old option value.
1801
	 * @param array  $new_value The new option value.
1802
	 */
1803
	public function track_widget_updates( $option, $old_value, $new_value ) {
1804
		if ( 'widget_jetpack-search-filters' !== $option ) {
1805
			return;
1806
		}
1807
1808
		$event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value );
1809
		if ( ! $event ) {
1810
			return;
1811
		}
1812
1813
		$tracking = new Automattic\Jetpack\Tracking();
1814
		$tracking->tracks_record_event(
1815
			wp_get_current_user(),
1816
			sprintf( 'jetpack_search_widget_%s', $event['action'] ),
1817
			$event['widget']
1818
		);
1819
	}
1820
1821
	/**
1822
	 * Moves any active search widgets to the inactive category.
1823
	 *
1824
	 * @since 5.9.0
1825
	 *
1826
	 * @param string $module Unused. The Jetpack module being disabled.
1827
	 */
1828
	public function move_search_widgets_to_inactive( $module ) {
1829
		if ( ! is_active_widget( false, false, Jetpack_Search_Helpers::FILTER_WIDGET_BASE, true ) ) {
1830
			return;
1831
		}
1832
1833
		$sidebars_widgets = wp_get_sidebars_widgets();
1834
1835
		if ( ! is_array( $sidebars_widgets ) ) {
1836
			return;
1837
		}
1838
1839
		$changed = false;
1840
1841
		foreach ( $sidebars_widgets as $sidebar => $widgets ) {
1842
			if ( 'wp_inactive_widgets' === $sidebar || 'orphaned_widgets' === substr( $sidebar, 0, 16 ) ) {
1843
				continue;
1844
			}
1845
1846
			if ( is_array( $widgets ) ) {
1847
				foreach ( $widgets as $key => $widget ) {
1848
					if ( _get_widget_id_base( $widget ) == Jetpack_Search_Helpers::FILTER_WIDGET_BASE ) {
1849
						$changed = true;
1850
1851
						array_unshift( $sidebars_widgets['wp_inactive_widgets'], $widget );
1852
						unset( $sidebars_widgets[ $sidebar ][ $key ] );
1853
					}
1854
				}
1855
			}
1856
		}
1857
1858
		if ( $changed ) {
1859
			wp_set_sidebars_widgets( $sidebars_widgets );
1860
		}
1861
	}
1862
1863
	/**
1864
	 * Determine whether a given WP_Query should be handled by ElasticSearch.
1865
	 *
1866
	 * @param WP_Query $query The WP_Query object.
1867
	 *
1868
	 * @return bool
1869
	 */
1870
	public function should_handle_query( $query ) {
1871
		/**
1872
		 * Determine whether a given WP_Query should be handled by ElasticSearch.
1873
		 *
1874
		 * @module search
1875
		 *
1876
		 * @since  5.6.0
1877
		 *
1878
		 * @param bool     $should_handle Should be handled by Jetpack Search.
1879
		 * @param WP_Query $query         The WP_Query object.
1880
		 */
1881
		return apply_filters( 'jetpack_search_should_handle_query', $query->is_main_query() && $query->is_search(), $query );
1882
	}
1883
1884
	/**
1885
	 * Transforms an array with fields name as keys and boosts as value into
1886
	 * shorthand "caret" format.
1887
	 *
1888
	 * @param array $fields_boost [ "title" => "2", "content" => "1" ]
1889
	 *
1890
	 * @return array [ "title^2", "content^1" ]
1891
	 */
1892
	private function _get_caret_boosted_fields( array $fields_boost ) {
1893
		$caret_boosted_fields = array();
1894
		foreach ( $fields_boost as $field => $boost ) {
1895
			$caret_boosted_fields[] = "$field^$boost";
1896
		}
1897
		return $caret_boosted_fields;
1898
	}
1899
1900
	/**
1901
	 * Apply a multiplier to boost values.
1902
	 *
1903
	 * @param array $fields_boost [ "title" => 2, "content" => 1 ]
1904
	 * @param array $fields_boost_multiplier [ "title" => 0.1234 ]
1905
	 *
1906
	 * @return array [ "title" => "0.247", "content" => "1.000" ]
1907
	 */
1908
	private function _apply_boosts_multiplier( array $fields_boost, array $fields_boost_multiplier ) {
1909
		foreach( $fields_boost as $field_name => $field_boost ) {
1910
			if ( isset( $fields_boost_multiplier[ $field_name ] ) ) {
1911
				$fields_boost[ $field_name ] *= $fields_boost_multiplier[ $field_name ];
1912
			}
1913
1914
			// Set a floor and format the number as string
1915
			$fields_boost[ $field_name ] = number_format(
1916
				max( 0.001, $fields_boost[ $field_name ] ),
1917
				3, '.', ''
1918
			);
1919
		}
1920
1921
		return $fields_boost;
1922
	}
1923
}
1924