Completed
Push — add/twentytwenty-compat ( 602168...cd9ba9 )
by Jeremy
26:39 queued 18:13
created

Jetpack_Search::load_assets()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 70

Duplication

Lines 5
Ratio 7.14 %

Importance

Changes 0
Metric Value
cc 8
nc 7
nop 0
dl 5
loc 70
rs 7.4101
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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