Completed
Push — update/launch-price-ux ( af1100...50e28f )
by Marin
07:13 queued 27s
created

Jetpack_Search::action__widgets_init()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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