Completed
Push — fix/gutenberg-7.2.0-classnames ( 3b3138...aca56b )
by Jeremy
63:47 queued 56:55
created

Jetpack_Search::__wakeup()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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