Completed
Push — instant-search-master ( d21855...05bf31 )
by
unknown
07:02
created

Jetpack_Search::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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