Completed
Push — try/instant-search-overlay ( 90519f...65b7dc )
by
unknown
27:08 queued 20:32
created

class.jetpack-search.php ➔ disable_search()   A

Complexity

Conditions 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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