Completed
Push — add/instant-search-filters ( 6d9cd8 )
by
unknown
06:54
created

Jetpack_Search::store_last_query_info()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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