Completed
Push — update/wpcom-search-merge ( 77e11c )
by
unknown
55:20 queued 42:10
created

Jetpack_Search::action__widgets_init()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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