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

Jetpack_Search::load_instant_search_assets()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 71

Duplication

Lines 5
Ratio 7.04 %

Importance

Changes 0
Metric Value
cc 8
nc 7
nop 0
dl 5
loc 71
rs 7.3882
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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