Completed
Push — instant-search-master ( 6806c6...584d61 )
by
unknown
06:40
created

Jetpack_Search::load_php()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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