Completed
Push — add/user-authentication ( 651ac4...684086 )
by
unknown
26:10 queued 18:04
created

Jetpack_Search::get_asset_version()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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