Completed
Push — instant-search-master ( 8be3b4...336413 )
by
unknown
06:37 queued 10s
created

Jetpack_Search::has_vip_index()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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