Completed
Push — merge/site-search ( f0678c )
by
unknown
224:08 queued 214:28
created

Jetpack_Search::set_facets()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Jetpack Search: Main Jetpack_Search class
4
 *
5
 * @package    Jetpack
6
 * @subpackage Jetpack Search
7
 * @since      5.0.0
8
 */
9
10
/**
11
 * The main class for the Jetpack Search module.
12
 *
13
 * @since 5.0.0
14
 */
15
class Jetpack_Search {
16
17
	/**
18
	 * The number of found posts.
19
	 *
20
	 * @since 5.0.0
21
	 *
22
	 * @var int
23
	 */
24
	protected $found_posts = 0;
25
26
	/**
27
	 * The search result, as returned by the WordPress.com REST API.
28
	 *
29
	 * @since 5.0.0
30
	 *
31
	 * @var array
32
	 */
33
	protected $search_result;
34
35
	/**
36
	 * This site's blog ID on WordPress.com.
37
	 *
38
	 * @since 5.0.0
39
	 *
40
	 * @var int
41
	 */
42
	protected $jetpack_blog_id;
43
44
	/**
45
	 * The Elasticsearch aggregations (filters).
46
	 *
47
	 * @since 5.0.0
48
	 *
49
	 * @var array
50
	 */
51
	protected $aggregations = array();
52
53
	/**
54
	 * The maximum number of aggregations allowed.
55
	 *
56
	 * @since 5.0.0
57
	 *
58
	 * @var int
59
	 */
60
	protected $max_aggregations_count = 100;
61
62
	/**
63
	 * Statistics about the last Elasticsearch query.
64
	 *
65
	 * @since 5.6.0
66
	 *
67
	 * @var array
68
	 */
69
	protected $last_query_info = array();
70
71
	/**
72
	 * Statistics about the last Elasticsearch query failure.
73
	 *
74
	 * @since 5.6.0
75
	 *
76
	 * @var array
77
	 */
78
	protected $last_query_failure_info = array();
79
80
	/**
81
	 * The singleton instance of this class.
82
	 *
83
	 * @since 5.0.0
84
	 *
85
	 * @var Jetpack_Search
86
	 */
87
	protected static $instance;
88
89
	/**
90
	 * Languages with custom analyzers. Other languages are supported, but are analyzed with the default analyzer.
91
	 *
92
	 * @since 5.0.0
93
	 *
94
	 * @var array
95
	 */
96
	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' );
97
98
	/**
99
	 * Jetpack_Search constructor.
100
	 *
101
	 * @since 5.0.0
102
	 *
103
	 * Doesn't do anything. This class needs to be initialized via the instance() method instead.
104
	 */
105
	protected function __construct() {
106
	}
107
108
	/**
109
	 * Prevent __clone()'ing of this class.
110
	 *
111
	 * @since 5.0.0
112
	 */
113
	public function __clone() {
114
		wp_die( "Please don't __clone Jetpack_Search" );
115
	}
116
117
	/**
118
	 * Prevent __wakeup()'ing of this class.
119
	 *
120
	 * @since 5.0.0
121
	 */
122
	public function __wakeup() {
123
		wp_die( "Please don't __wakeup Jetpack_Search" );
124
	}
125
126
	/**
127
	 * Get singleton instance of Jetpack_Search.
128
	 *
129
	 * Instantiates and sets up a new instance if needed, or returns the singleton.
130
	 *
131
	 * @since 5.0.0
132
	 *
133
	 * @return Jetpack_Search The Jetpack_Search singleton.
134
	 */
135
	public static function instance() {
136
		if ( ! isset( self::$instance ) ) {
137
			self::$instance = new Jetpack_Search();
138
139
			self::$instance->setup();
140
		}
141
142
		return self::$instance;
143
	}
144
145
	/**
146
	 * Perform various setup tasks for the class.
147
	 *
148
	 * Checks various pre-requisites and adds hooks.
149
	 *
150
	 * @since 5.0.0
151
	 */
152
	public function setup() {
153
		if ( ! Jetpack::is_active() || ! Jetpack::active_plan_supports( 'search' ) ) {
154
			return;
155
		}
156
157
		$this->jetpack_blog_id = Jetpack::get_option( 'id' );
158
159
		if ( ! $this->jetpack_blog_id ) {
160
			return;
161
		}
162
163
		require_once( dirname( __FILE__ ) . '/class.jetpack-search-helpers.php' );
164
		require_once( dirname( __FILE__ ) . '/class.jetpack-search-template-tags.php' );
165
166
		$this->init_hooks();
167
	}
168
169
	/**
170
	 * Setup the various hooks needed for the plugin to take over search duties.
171
	 *
172
	 * @since 5.0.0
173
	 */
174
	public function init_hooks() {
175
		if ( ! is_admin() ) {
176
			add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 );
177
178
			add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ), 10, 2 );
179
180
			add_action( 'did_jetpack_search_query', array( $this, 'store_last_query_info' ) );
181
			add_action( 'failed_jetpack_search_query', array( $this, 'store_query_failure' ) );
182
183
			add_action( 'init', array( $this, 'set_filters_from_widgets' ) );
184
185
			add_action( 'pre_get_posts', array( $this, 'maybe_add_post_type_as_var' ) );
186
		} else {
187
			add_action( 'update_option', array( $this, 'track_widget_updates' ), 10, 3 );
188
		}
189
190
		add_action( 'jetpack_deactivate_module_search', array( $this, 'move_search_widgets_to_inactive' ) );
191
	}
192
193
	/**
194
	 * When an Elasticsearch query fails, this stores it and enqueues some debug information in the footer.
195
	 *
196
	 * @since 5.6.0
197
	 *
198
	 * @param array $meta Information about the failure.
199
	 */
200
	public function store_query_failure( $meta ) {
201
		$this->last_query_failure_info = $meta;
202
		add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
203
	}
204
205
	/**
206
	 * Outputs information about the last Elasticsearch failure.
207
	 *
208
	 * @since 5.6.0
209
	 */
210
	public function print_query_failure() {
211
		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...
212
			printf(
213
				'<!-- Jetpack Search failed with code %s: %s - %s -->',
214
				esc_html( $this->last_query_failure_info['response_code'] ),
215
				esc_html( $this->last_query_failure_info['json']['error'] ),
216
				esc_html( $this->last_query_failure_info['json']['message'] )
217
			);
218
		}
219
	}
220
221
	/**
222
	 * Stores information about the last Elasticsearch query and enqueues some debug information in the footer.
223
	 *
224
	 * @since 5.6.0
225
	 *
226
	 * @param array $meta Information about the query.
227
	 */
228
	public function store_last_query_info( $meta ) {
229
		$this->last_query_info = $meta;
230
		add_action( 'wp_footer', array( $this, 'print_query_success' ) );
231
	}
232
233
	/**
234
	 * Outputs information about the last Elasticsearch search.
235
	 *
236
	 * @since 5.6.0
237
	 */
238
	public function print_query_success() {
239
		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...
240
			printf(
241
				'<!-- Jetpack Search took %s ms, ES time %s ms -->',
242
				intval( $this->last_query_info['elapsed_time'] ),
243
				esc_html( $this->last_query_info['es_time'] )
244
			);
245
		}
246
	}
247
248
	/**
249
	 * Returns the last query information, or false if no information was stored.
250
	 *
251
	 * @since 5.8.0
252
	 *
253
	 * @return bool|array
254
	 */
255
	public function get_last_query_info() {
256
		return empty( $this->last_query_info ) ? false : $this->last_query_info;
257
	}
258
259
	/**
260
	 * Returns the last query failure information, or false if no failure information was stored.
261
	 *
262
	 * @since 5.8.0
263
	 *
264
	 * @return bool|array
265
	 */
266
	public function get_last_query_failure_info() {
267
		return empty( $this->last_query_failure_info ) ? false : $this->last_query_failure_info;
268
	}
269
270
	/**
271
	 * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
272
	 * developers to disable filters supplied by the search widget. Useful if filters are
273
	 * being defined at the code level.
274
	 *
275
	 * @since      5.7.0
276
	 * @deprecated 5.8.0 Use Jetpack_Search_Helpers::are_filters_by_widget_disabled() directly.
277
	 *
278
	 * @return bool
279
	 */
280
	public function are_filters_by_widget_disabled() {
281
		return Jetpack_Search_Helpers::are_filters_by_widget_disabled();
282
	}
283
284
	/**
285
	 * Retrieves a list of known Jetpack search filters widget IDs, gets the filters for each widget,
286
	 * and applies those filters to this Jetpack_Search object.
287
	 *
288
	 * @since 5.7.0
289
	 */
290
	public function set_filters_from_widgets() {
291
		if ( Jetpack_Search_Helpers::are_filters_by_widget_disabled() ) {
292
			return;
293
		}
294
295
		$filters = Jetpack_Search_Helpers::get_filters_from_widgets();
296
297
		if ( ! empty( $filters ) ) {
298
			$this->set_filters( $filters );
299
		}
300
	}
301
302
	/**
303
	 * Restricts search results to certain post types via a GET argument.
304
	 *
305
	 * @since 5.8.0
306
	 *
307
	 * @param WP_Query $query A WP_Query instance.
308
	 */
309
	public function maybe_add_post_type_as_var( WP_Query $query ) {
310
		if ( $query->is_main_query() && $query->is_search && ! empty( $_GET['post_type'] ) ) {
311
			$post_types = ( is_string( $_GET['post_type'] ) && false !== strpos( $_GET['post_type'], ',' ) )
312
				? $post_type = explode( ',', $_GET['post_type'] )
313
				: (array) $_GET['post_type'];
314
			$post_types = array_map( 'sanitize_key', $post_types );
315
			$query->set( 'post_type', $post_types );
316
		}
317
	}
318
319
	/*
320
	 * Run a search on the WordPress.com public API.
321
	 *
322
	 * @since 5.0.0
323
	 *
324
	 * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint.
325
	 *
326
	 * @return object|WP_Error The response from the public API, or a WP_Error.
327
	 */
328
	public function search( array $es_args ) {
329
		$endpoint    = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
330
		$service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
331
332
		$do_authenticated_request = false;
333
334
		if ( class_exists( 'Jetpack_Client' ) &&
335
			isset( $es_args['authenticated_request'] ) &&
336
			true === $es_args['authenticated_request'] ) {
337
			$do_authenticated_request = true;
338
		}
339
340
		unset( $es_args['authenticated_request'] );
341
342
		$request_args = array(
343
			'headers' => array(
344
				'Content-Type' => 'application/json',
345
			),
346
			'timeout'    => 10,
347
			'user-agent' => 'jetpack_search',
348
		);
349
350
		$request_body = wp_json_encode( $es_args );
351
352
		$start_time = microtime( true );
353
354
		if ( $do_authenticated_request ) {
355
			$request_args['method'] = 'POST';
356
357
			$request = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint, Jetpack_Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
358
		} else {
359
			$request_args = array_merge( $request_args, array(
360
				'body' => $request_body,
361
			) );
362
363
			$request = wp_remote_post( $service_url, $request_args );
364
		}
365
366
		$end_time = microtime( true );
367
368
		if ( is_wp_error( $request ) ) {
369
			return $request;
370
		}
371
372
		$response_code = wp_remote_retrieve_response_code( $request );
373
374
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
375
			return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
376
		}
377
378
		$response = json_decode( wp_remote_retrieve_body( $request ), true );
379
380
		$took = is_array( $response ) && ! empty( $response['took'] )
381
			? $response['took']
382
			: null;
383
384
		$query = array(
385
			'args'          => $es_args,
386
			'response'      => $response,
387
			'response_code' => $response_code,
388
			'elapsed_time'  => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms.
389
			'es_time'       => $took,
390
			'url'           => $service_url,
391
		);
392
393
		/**
394
		 * Fires after a search request has been performed.
395
		 *
396
		 * Includes the following info in the $query parameter:
397
		 *
398
		 * array args Array of Elasticsearch arguments for the search
399
		 * array response Raw API response, JSON decoded
400
		 * int response_code HTTP response code of the request
401
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
402
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
403
		 * string url API url that was queried
404
		 *
405
		 * @module search
406
		 *
407
		 * @since  5.0.0
408
		 * @since  5.8.0 This action now fires on all queries instead of just successful queries.
409
		 *
410
		 * @param array $query Array of information about the query performed
411
		 */
412
		do_action( 'did_jetpack_search_query', $query );
413
414
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
415
			/**
416
			 * Fires after a search query request has failed
417
			 *
418
			 * @module search
419
			 *
420
			 * @since  5.6.0
421
			 *
422
			 * @param array Array containing the response code and response from the failed search query
423
			 */
424
			do_action( 'failed_jetpack_search_query', array(
425
				'response_code' => $response_code,
426
				'json'          => $response,
427
			) );
428
429
			return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
430
		}
431
432
		return $response;
433
	}
434
435
	/**
436
	 * Bypass the normal Search query and offload it to Jetpack servers.
437
	 *
438
	 * This is the main hook of the plugin and is responsible for returning the posts that match the search query.
439
	 *
440
	 * @since 5.0.0
441
	 *
442
	 * @param array    $posts Current array of posts (still pre-query).
443
	 * @param WP_Query $query The WP_Query being filtered.
444
	 *
445
	 * @return array Array of matching posts.
446
	 */
447
	public function filter__posts_pre_query( $posts, $query ) {
448
		/**
449
		 * Determine whether a given WP_Query should be handled by ElasticSearch.
450
		 *
451
		 * @module search
452
		 *
453
		 * @since  5.6.0
454
		 *
455
		 * @param bool     $should_handle Should be handled by Jetpack Search.
456
		 * @param WP_Query $query         The WP_Query object.
457
		 */
458
		if ( ! apply_filters( 'jetpack_search_should_handle_query', ( $query->is_main_query() && $query->is_search() ), $query ) ) {
459
			return $posts;
460
		}
461
462
		$this->do_search( $query );
463
464
		if ( ! is_array( $this->search_result ) ) {
465
			return $posts;
466
		}
467
468
		// If no results, nothing to do
469
		if ( ! count( $this->search_result['results']['hits'] ) ) {
470
			return array();
471
		}
472
473
		$post_ids = array();
474
475
		foreach ( $this->search_result['results']['hits'] as $result ) {
476
			$post_ids[] = (int) $result['fields']['post_id'];
477
		}
478
479
		// Query all posts now
480
		$args = array(
481
			'post__in'            => $post_ids,
482
			'perm'                => 'readable',
483
			'post_type'           => 'any',
484
			'ignore_sticky_posts' => true,
485
			'suppress_filters'    => true,
486
		);
487
488
		if ( isset( $query->query_vars['order'] ) ) {
489
			$args['order'] = $query->query_vars['order'];
490
		}
491
492
		if ( isset( $query->query_vars['orderby'] ) ) {
493
			$args['orderby'] = $query->query_vars['orderby'];
494
		}
495
496
		$posts_query = new WP_Query( $args );
497
498
		// 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.
499
		$query->found_posts   = $this->found_posts;
500
		$query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
501
502
		return $posts_query->posts;
503
	}
504
505
	/**
506
	 * Build up the search, then run it against the Jetpack servers.
507
	 *
508
	 * @since 5.0.0
509
	 *
510
	 * @param WP_Query $query The original WP_Query to use for the parameters of our search.
511
	 */
512
	public function do_search( WP_Query $query ) {
513
		if ( ! $query->is_main_query() || ! $query->is_search() ) {
514
			return;
515
		}
516
517
		$page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
518
519
		// Get maximum allowed offset and posts per page values for the API.
520
		$max_offset         = Jetpack_Search_Helpers::get_max_offset();
521
		$max_posts_per_page = Jetpack_Search_Helpers::get_max_posts_per_page();
522
523
		$posts_per_page = $query->get( 'posts_per_page' );
524
		if ( $posts_per_page > $max_posts_per_page ) {
525
			$posts_per_page = $max_posts_per_page;
526
		}
527
528
		// Start building the WP-style search query args.
529
		// They'll be translated to ES format args later.
530
		$es_wp_query_args = array(
531
			'query'          => $query->get( 's' ),
532
			'posts_per_page' => $posts_per_page,
533
			'paged'          => $page,
534
			'orderby'        => $query->get( 'orderby' ),
535
			'order'          => $query->get( 'order' ),
536
		);
537
538
		if ( ! empty( $this->aggregations ) ) {
539
			$es_wp_query_args['aggregations'] = $this->aggregations;
540
		}
541
542
		// Did we query for authors?
543
		if ( $query->get( 'author_name' ) ) {
544
			$es_wp_query_args['author_name'] = $query->get( 'author_name' );
545
		}
546
547
		$es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
548
		$es_wp_query_args['terms']     = $this->get_es_wp_query_terms_for_query( $query );
549
550
		/**
551
		 * Modify the search query parameters, such as controlling the post_type.
552
		 *
553
		 * These arguments are in the format of WP_Query arguments
554
		 *
555
		 * @module search
556
		 *
557
		 * @since  5.0.0
558
		 *
559
		 * @param array    $es_wp_query_args The current query args, in WP_Query format.
560
		 * @param WP_Query $query            The original WP_Query object.
561
		 */
562
		$es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
563
564
		// If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
565
		// capped at Jetpack_Search_Helpers::get_max_offset(), so a high page would always return the last page of results otherwise.
566
		if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $max_offset ) {
567
			$query->set_404();
568
569
			return;
570
		}
571
572
		// If there were no post types returned, then 404 to avoid querying against non-public post types, which could
573
		// happen if we don't add the post type restriction to the ES query.
574
		if ( empty( $es_wp_query_args['post_type'] ) ) {
575
			$query->set_404();
576
577
			return;
578
		}
579
580
		// Convert the WP-style args into ES args.
581
		$es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
582
583
		//Only trust ES to give us IDs, not the content since it is a mirror
584
		$es_query_args['fields'] = array(
585
			'post_id',
586
		);
587
588
		/**
589
		 * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
590
		 *
591
		 * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
592
		 *
593
		 * @module search
594
		 *
595
		 * @since  5.0.0
596
		 *
597
		 * @param array    $es_query_args The raw Elasticsearch query args.
598
		 * @param WP_Query $query         The original WP_Query object.
599
		 */
600
		$es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
601
602
		// Do the actual search query!
603
		$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...
604
605
		if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
606
			$this->found_posts = 0;
607
608
			return;
609
		}
610
611
		// If we have aggregations, fix the ordering to match the input order (ES doesn't guarantee the return order).
612
		if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
613
			$this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
614
		}
615
616
		// Total number of results for paging purposes. Capped at $max_offset + $posts_per_page, as deep paging gets quite expensive.
617
		$this->found_posts = min( $this->search_result['results']['total'], $max_offset + $posts_per_page );
618
	}
619
620
	/**
621
	 * If the query has already been run before filters have been updated, then we need to re-run the query
622
	 * to get the latest aggregations.
623
	 *
624
	 * This is especially useful for supporting widget management in the customizer.
625
	 *
626
	 * @since 5.8.0
627
	 *
628
	 * @return bool Whether the query was successful or not.
629
	 */
630
	public function update_search_results_aggregations() {
631
		if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
632
			return false;
633
		}
634
635
		$es_args = $this->last_query_info['args'];
636
		$builder = new Jetpack_WPES_Query_Builder();
637
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
638
		$es_args['aggregations'] = $builder->build_aggregation();
639
640
		$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...
641
642
		return ! is_wp_error( $this->search_result );
643
	}
644
645
	/**
646
	 * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style Elasticsearch term arguments for the search.
647
	 *
648
	 * @since 5.0.0
649
	 *
650
	 * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query.
651
	 *
652
	 * @return array The new WP-style Elasticsearch arguments (that will be converted into 'real' Elasticsearch arguments).
653
	 */
654
	public function get_es_wp_query_terms_for_query( WP_Query $query ) {
655
		$args = array();
656
657
		$the_tax_query = $query->tax_query;
658
659
		if ( ! $the_tax_query ) {
660
			return $args;
661
		}
662
663
664
		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...
665
			return $args;
666
		}
667
668
		$args = array();
669
670
		foreach ( $the_tax_query->queries as $tax_query ) {
671
			// Right now we only support slugs...see note above
672
			if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
673
				continue;
674
			}
675
676
			$taxonomy = $tax_query['taxonomy'];
677
678 View Code Duplication
			if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
679
				$args[ $taxonomy ] = array();
680
			}
681
682
			$args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
683
		}
684
685
		return $args;
686
	}
687
688
	/**
689
	 * Parse out the post type from a WP_Query.
690
	 *
691
	 * Only allows post types that are not marked as 'exclude_from_search'.
692
	 *
693
	 * @since 5.0.0
694
	 *
695
	 * @param WP_Query $query Original WP_Query object.
696
	 *
697
	 * @return array Array of searchable post types corresponding to the original query.
698
	 */
699
	public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
700
		$post_types = $query->get( 'post_type' );
701
702
		// If we're searching 'any', we want to only pass searchable post types to Elasticsearch.
703
		if ( 'any' === $post_types ) {
704
			$post_types = array_values( get_post_types( array(
705
				'exclude_from_search' => false,
706
			) ) );
707
		}
708
709
		if ( ! is_array( $post_types ) ) {
710
			$post_types = array( $post_types );
711
		}
712
713
		$post_types = array_unique( $post_types );
714
715
		$sanitized_post_types = array();
716
717
		// Make sure the post types are queryable.
718
		foreach ( $post_types as $post_type ) {
719
			if ( ! $post_type ) {
720
				continue;
721
			}
722
723
			$post_type_object = get_post_type_object( $post_type );
724
			if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
725
				continue;
726
			}
727
728
			$sanitized_post_types[] = $post_type;
729
		}
730
731
		return $sanitized_post_types;
732
	}
733
734
	/**
735
	 * Initialze widgets for the Search module
736
	 *
737
	 * @module search
738
	 */
739
	public function action__widgets_init() {
740
		require_once( dirname( __FILE__ ) . '/class.jetpack-search-widget-filters.php' );
741
742
		register_widget( 'Jetpack_Search_Widget_Filters' );
743
	}
744
745
	/**
746
	 * Get the Elasticsearch result.
747
	 *
748
	 * @since 5.0.0
749
	 *
750
	 * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response.
751
	 *
752
	 * @return array|bool The search results, or false if there was a failure.
753
	 */
754
	public function get_search_result( $raw = false ) {
755
		if ( $raw ) {
756
			return $this->search_result;
757
		}
758
759
		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;
760
	}
761
762
	/**
763
	 * Add the date portion of a WP_Query onto the query args.
764
	 *
765
	 * @since 5.0.0
766
	 *
767
	 * @param array    $es_wp_query_args The Elasticsearch query arguments in WordPress form.
768
	 * @param WP_Query $query            The original WP_Query.
769
	 *
770
	 * @return array The es wp query args, with date filters added (as needed).
771
	 */
772
	public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
773
		if ( $query->get( 'year' ) ) {
774
			if ( $query->get( 'monthnum' ) ) {
775
				// Padding
776
				$date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
777
778
				if ( $query->get( 'day' ) ) {
779
					// Padding
780
					$date_day = sprintf( '%02d', $query->get( 'day' ) );
781
782
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
783
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
784
				} else {
785
					$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
786
787
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
788
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
789
				}
790
			} else {
791
				$date_start = $query->get( 'year' ) . '-01-01 00:00:00';
792
				$date_end   = $query->get( 'year' ) . '-12-31 23:59:59';
793
			}
794
795
			$es_wp_query_args['date_range'] = array(
796
				'field' => 'date',
797
				'gte'   => $date_start,
798
				'lte'   => $date_end,
799
			);
800
		}
801
802
		return $es_wp_query_args;
803
	}
804
805
	/**
806
	 * Converts WP_Query style args to Elasticsearch args.
807
	 *
808
	 * @since 5.0.0
809
	 *
810
	 * @param array $args Array of WP_Query style arguments.
811
	 *
812
	 * @return array Array of ES style query arguments.
813
	 */
814
	public function convert_wp_es_to_es_args( array $args ) {
815
		jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-parser' );
816
817
		$defaults = array(
818
			'blog_id'        => get_current_blog_id(),
819
			'query'          => null,    // Search phrase
820
			'query_fields'   => array(), //list of fields to search
821
			'post_type'      => null,    // string or an array
822
			'terms'          => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) )
823
			'author'         => null,    // id or an array of ids
824
			'author_name'    => array(), // string or an array
825
			'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'
826
			'orderby'        => null,    // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
827
			'order'          => 'DESC',
828
			'posts_per_page' => 10,
829
			'offset'         => null,
830
			'paged'          => null,
831
			/**
832
			 * Aggregations. Examples:
833
			 * array(
834
			 *     'Tag'       => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
835
			 *     'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
836
			 * );
837
			 */
838
			'aggregations'   => null,
839
		);
840
841
		$args = wp_parse_args( $args, $defaults );
842
843
		$parser = new Jetpack_WPES_Search_Query_Parser( $args['query'], array( get_locale() ) );
844
845
		if ( empty( $args['query_fields'] ) ) {
846
			if ( defined( 'JETPACK_SEARCH_VIP_INDEX' ) && JETPACK_SEARCH_VIP_INDEX ) {
847
				// VIP indices do not have per language fields
848
				$match_fields        = array(
849
					'title^0.1',
850
					'content^0.1',
851
					'excerpt^0.1',
852
					'tag.name^0.1',
853
					'category.name^0.1',
854
					'author_login^0.1',
855
					'author^0.1',
856
				);
857
				$boost_fields        = array(
858
					'title^2',
859
					'tag.name',
860
					'category.name',
861
					'author_login',
862
					'author',
863
				);
864
				$boost_phrase_fields = array(
865
					'title',
866
					'content',
867
					'excerpt',
868
					'tag.name',
869
					'category.name',
870
					'author',
871
				);
872
			} else {
873
				$match_fields = $parser->merge_ml_fields(
874
					array(
875
						'title'         => 0.1,
876
						'content'       => 0.1,
877
						'excerpt'       => 0.1,
878
						'tag.name'      => 0.1,
879
						'category.name' => 0.1,
880
					),
881
					array(
882
						'author_login^0.1',
883
						'author^0.1',
884
					)
885
				);
886
887
				$boost_fields = $parser->merge_ml_fields(
888
					array(
889
						'title'         => 2,
890
						'tag.name'      => 1,
891
						'category.name' => 1,
892
					),
893
					array(
894
						'author_login',
895
						'author',
896
					)
897
				);
898
899
				$boost_phrase_fields = $parser->merge_ml_fields(
900
					array(
901
						'title'         => 1,
902
						'content'       => 1,
903
						'excerpt'       => 1,
904
						'tag.name'      => 1,
905
						'category.name' => 1,
906
					),
907
					array(
908
						'author',
909
					)
910
				);
911
			}
912
		} else {
913
			// If code is overriding the fields, then use that. Important for backwards compatibility.
914
			$match_fields        = $args['query_fields'];
915
			$boost_phrase_fields = $match_fields;
916
			$boost_fields        = null;
917
		}
918
919
		$parser->phrase_filter( array(
920
			'must_query_fields'  => $match_fields,
921
			'boost_query_fields' => null,
922
		) );
923
		$parser->remaining_query( array(
924
			'must_query_fields'  => $match_fields,
925
			'boost_query_fields' => $boost_fields,
926
		) );
927
928
		// Boost on phrase matches
929
		$parser->remaining_query( array(
930
			'boost_query_fields' => $boost_phrase_fields,
931
			'boost_query_type'   => 'phrase',
932
		) );
933
934
		/**
935
		 * Modify the recency decay parameters for the search query.
936
		 *
937
		 * The recency decay lowers the search scores based on the age of a post relative to an origin date. Basic adjustments:
938
		 *  - origin: A date. Posts with this date will have the highest score and no decay applied. Default is today.
939
		 *  - 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.
940
		 *  - scale: The number of days/months/years from the origin+offset at which the decay will equal the decay param. Default 360d
941
		 *  - decay: The amount of decay applied at offset+scale. Default 0.9.
942
		 *
943
		 * 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}
944
		 *
945
		 * @module search
946
		 *
947
		 * @since  5.8.0
948
		 *
949
		 * @param array $decay_params The decay parameters.
950
		 * @param array $args         The WP query parameters.
951
		 */
952
		$decay_params = apply_filters(
953
			'jetpack_search_recency_score_decay',
954
			array(
955
				'origin' => date( 'Y-m-d' ),
956
				'scale'  => '360d',
957
				'decay'  => 0.9,
958
			),
959
			$args
960
		);
961
962
		if ( ! empty( $decay_params ) ) {
963
			// Newer content gets weighted slightly higher
964
			$parser->add_decay( 'gauss', array(
965
				'date_gmt' => $decay_params
966
			) );
967
		}
968
969
		$es_query_args = array(
970
			'blog_id' => absint( $args['blog_id'] ),
971
			'size'    => absint( $args['posts_per_page'] ),
972
		);
973
974
		// ES "from" arg (offset)
975
		if ( $args['offset'] ) {
976
			$es_query_args['from'] = absint( $args['offset'] );
977
		} elseif ( $args['paged'] ) {
978
			$es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
979
		}
980
981
		$es_query_args['from'] = min( $es_query_args['from'], Jetpack_Search_Helpers::get_max_offset() );
982
983
		if ( ! is_array( $args['author_name'] ) ) {
984
			$args['author_name'] = array( $args['author_name'] );
985
		}
986
987
		// ES stores usernames, not IDs, so transform
988
		if ( ! empty( $args['author'] ) ) {
989
			if ( ! is_array( $args['author'] ) ) {
990
				$args['author'] = array( $args['author'] );
991
			}
992
993
			foreach ( $args['author'] as $author ) {
994
				$user = get_user_by( 'id', $author );
995
996
				if ( $user && ! empty( $user->user_login ) ) {
997
					$args['author_name'][] = $user->user_login;
998
				}
999
			}
1000
		}
1001
1002
		//////////////////////////////////////////////////
1003
		// Build the filters from the query elements.
1004
		// Filters rock because they are cached from one query to the next
1005
		// but they are cached as individual filters, rather than all combined together.
1006
		// May get performance boost by also caching the top level boolean filter too.
1007
1008
		if ( $args['post_type'] ) {
1009
			if ( ! is_array( $args['post_type'] ) ) {
1010
				$args['post_type'] = array( $args['post_type'] );
1011
			}
1012
1013
			$parser->add_filter( array(
1014
				'terms' => array(
1015
					'post_type' => $args['post_type'],
1016
				),
1017
			) );
1018
		}
1019
1020
		if ( $args['author_name'] ) {
1021
			$parser->add_filter( array(
1022
				'terms' => array(
1023
					'author_login' => $args['author_name'],
1024
				),
1025
			) );
1026
		}
1027
1028
		if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
1029
			$field = $args['date_range']['field'];
1030
1031
			unset( $args['date_range']['field'] );
1032
1033
			$parser->add_filter( array(
1034
				'range' => array(
1035
					$field => $args['date_range'],
1036
				),
1037
			) );
1038
		}
1039
1040
		if ( is_array( $args['terms'] ) ) {
1041
			foreach ( $args['terms'] as $tax => $terms ) {
1042
				$terms = (array) $terms;
1043
1044
				if ( count( $terms ) && mb_strlen( $tax ) ) {
1045 View Code Duplication
					switch ( $tax ) {
1046
						case 'post_tag':
1047
							$tax_fld = 'tag.slug';
1048
1049
							break;
1050
1051
						case 'category':
1052
							$tax_fld = 'category.slug';
1053
1054
							break;
1055
1056
						default:
1057
							$tax_fld = 'taxonomy.' . $tax . '.slug';
1058
1059
							break;
1060
					}
1061
1062
					foreach ( $terms as $term ) {
1063
						$parser->add_filter( array(
1064
							'term' => array(
1065
								$tax_fld => $term,
1066
							),
1067
						) );
1068
					}
1069
				}
1070
			}
1071
		}
1072
1073
		if ( ! $args['orderby'] ) {
1074
			if ( $args['query'] ) {
1075
				$args['orderby'] = array( 'relevance' );
1076
			} else {
1077
				$args['orderby'] = array( 'date' );
1078
			}
1079
		}
1080
1081
		// Validate the "order" field
1082
		switch ( strtolower( $args['order'] ) ) {
1083
			case 'asc':
1084
				$args['order'] = 'asc';
1085
				break;
1086
1087
			case 'desc':
1088
			default:
1089
				$args['order'] = 'desc';
1090
				break;
1091
		}
1092
1093
		$es_query_args['sort'] = array();
1094
1095
		foreach ( (array) $args['orderby'] as $orderby ) {
1096
			// Translate orderby from WP field to ES field
1097
			switch ( $orderby ) {
1098
				case 'relevance' :
1099
					//never order by score ascending
1100
					$es_query_args['sort'][] = array(
1101
						'_score' => array(
1102
							'order' => 'desc',
1103
						),
1104
					);
1105
1106
					break;
1107
1108 View Code Duplication
				case 'date' :
1109
					$es_query_args['sort'][] = array(
1110
						'date' => array(
1111
							'order' => $args['order'],
1112
						),
1113
					);
1114
1115
					break;
1116
1117 View Code Duplication
				case 'ID' :
1118
					$es_query_args['sort'][] = array(
1119
						'id' => array(
1120
							'order' => $args['order'],
1121
						),
1122
					);
1123
1124
					break;
1125
1126
				case 'author' :
1127
					$es_query_args['sort'][] = array(
1128
						'author.raw' => array(
1129
							'order' => $args['order'],
1130
						),
1131
					);
1132
1133
					break;
1134
			} // End switch().
1135
		} // End foreach().
1136
1137
		if ( empty( $es_query_args['sort'] ) ) {
1138
			unset( $es_query_args['sort'] );
1139
		}
1140
1141
		if ( ! empty( $filters ) && is_array( $filters ) ) {
0 ignored issues
show
Bug introduced by
The variable $filters seems to never exist, and therefore empty should always return true. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
1142
			foreach ( $filters as $filter ) {
1143
				$builder->add_filter( $filter );
0 ignored issues
show
Bug introduced by
The variable $builder does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1144
			}
1145
		}
1146
1147
		$es_query_args['query'] = $builder->build_query();
1148
1149
		// Aggregations
1150
		if ( ! empty( $args['aggregations'] ) ) {
1151
			$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...
1152
		}
1153
1154
		$es_query_args['filter']       = $parser->build_filter();
1155
		$es_query_args['query']        = $parser->build_query();
1156
		$es_query_args['aggregations'] = $parser->build_aggregation();
1157
1158
		return $es_query_args;
1159
	}
1160
1161
	/**
1162
	 * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1163
	 *
1164
	 * @since 5.0.0
1165
	 *
1166
	 * @param array                      $aggregations Array of aggregations (filters) to add to the Jetpack_WPES_Query_Builder.
1167
	 * @param Jetpack_WPES_Query_Builder $builder      The builder instance that is creating the Elasticsearch query.
1168
	 */
1169
	public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
1170
		foreach ( $aggregations as $label => $aggregation ) {
1171
			switch ( $aggregation['type'] ) {
1172
				case 'taxonomy':
1173
					$this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1174
1175
					break;
1176
1177
				case 'post_type':
1178
					$this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1179
1180
					break;
1181
1182
				case 'date_histogram':
1183
					$this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1184
1185
					break;
1186
			}
1187
		}
1188
	}
1189
1190
	/**
1191
	 * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1192
	 *
1193
	 * @since 5.0.0
1194
	 *
1195
	 * @param array                      $aggregation The aggregation to add to the query builder.
1196
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1197
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1198
	 */
1199
	public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1200
		$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...
1201
1202
		switch ( $aggregation['taxonomy'] ) {
1203
			case 'post_tag':
1204
				$field = 'tag';
1205
				break;
1206
1207
			case 'category':
1208
				$field = 'category';
1209
				break;
1210
1211
			default:
1212
				$field = 'taxonomy.' . $aggregation['taxonomy'];
1213
				break;
1214
		}
1215
1216
		$builder->add_aggs( $label, array(
1217
			'terms' => array(
1218
				'field' => $field . '.slug',
1219
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1220
			),
1221
		) );
1222
	}
1223
1224
	/**
1225
	 * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1226
	 *
1227
	 * @since 5.0.0
1228
	 *
1229
	 * @param array                      $aggregation The aggregation to add to the query builder.
1230
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1231
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1232
	 */
1233
	public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1234
		$builder->add_aggs( $label, array(
1235
			'terms' => array(
1236
				'field' => 'post_type',
1237
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1238
			),
1239
		) );
1240
	}
1241
1242
	/**
1243
	 * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1244
	 *
1245
	 * @since 5.0.0
1246
	 *
1247
	 * @param array                      $aggregation The aggregation to add to the query builder.
1248
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1249
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1250
	 */
1251
	public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1252
		$args = array(
1253
			'interval' => $aggregation['interval'],
1254
			'field'    => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' == $aggregation['field'] ) ? 'date_gmt' : 'date',
1255
		);
1256
1257
		if ( isset( $aggregation['min_doc_count'] ) ) {
1258
			$args['min_doc_count'] = intval( $aggregation['min_doc_count'] );
1259
		} else {
1260
			$args['min_doc_count'] = 1;
1261
		}
1262
1263
		$builder->add_aggs( $label, array(
1264
			'date_histogram' => $args,
1265
		) );
1266
	}
1267
1268
	/**
1269
	 * And an existing filter object with a list of additional filters.
1270
	 *
1271
	 * Attempts to optimize the filters somewhat.
1272
	 *
1273
	 * @since 5.0.0
1274
	 *
1275
	 * @param array $curr_filter The existing filters to build upon.
1276
	 * @param array $filters     The new filters to add.
1277
	 *
1278
	 * @return array The resulting merged filters.
1279
	 */
1280
	public static function and_es_filters( array $curr_filter, array $filters ) {
1281
		if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
1282
			if ( 1 === count( $filters ) ) {
1283
				return $filters[0];
1284
			}
1285
1286
			return array(
1287
				'and' => $filters,
1288
			);
1289
		}
1290
1291
		return array(
1292
			'and' => array_merge( array( $curr_filter ), $filters ),
1293
		);
1294
	}
1295
1296
	/**
1297
	 * Set the available filters for the search.
1298
	 *
1299
	 * These get rendered via the Jetpack_Search_Widget() widget.
1300
	 *
1301
	 * Behind the scenes, these are implemented using Elasticsearch Aggregations.
1302
	 *
1303
	 * If you do not require counts of how many documents match each filter, please consider using regular WP Query
1304
	 * arguments instead, such as via the jetpack_search_es_wp_query_args filter
1305
	 *
1306
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1307
	 *
1308
	 * @since  5.0.0
1309
	 *
1310
	 * @param array $aggregations Array of filters (aggregations) to apply to the search
1311
	 */
1312
	public function set_filters( array $aggregations ) {
1313
		foreach ( (array) $aggregations as $key => $agg ) {
1314
			if ( empty( $agg['name'] ) ) {
1315
				$aggregations[ $key ]['name'] = $key;
1316
			}
1317
		}
1318
		$this->aggregations = $aggregations;
1319
	}
1320
1321
	/**
1322
	 * Set the search's facets (deprecated).
1323
	 *
1324
	 * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead.
1325
	 *
1326
	 * @see        Jetpack_Search::set_filters()
1327
	 *
1328
	 * @param array $facets Array of facets to apply to the search.
1329
	 */
1330
	public function set_facets( array $facets ) {
1331
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
1332
1333
		$this->set_filters( $facets );
1334
	}
1335
1336
	/**
1337
	 * Get the raw Aggregation results from the Elasticsearch response.
1338
	 *
1339
	 * @since  5.0.0
1340
	 *
1341
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1342
	 *
1343
	 * @return array Array of Aggregations performed on the search.
1344
	 */
1345
	public function get_search_aggregations_results() {
1346
		$aggregations = array();
1347
1348
		$search_result = $this->get_search_result();
1349
1350
		if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
1351
			$aggregations = $search_result['aggregations'];
1352
		}
1353
1354
		return $aggregations;
1355
	}
1356
1357
	/**
1358
	 * Get the raw Facet results from the Elasticsearch response.
1359
	 *
1360
	 * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead.
1361
	 *
1362
	 * @see        Jetpack_Search::get_search_aggregations_results()
1363
	 *
1364
	 * @return array Array of Facets performed on the search.
1365
	 */
1366
	public function get_search_facets() {
1367
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
1368
1369
		return $this->get_search_aggregations_results();
1370
	}
1371
1372
	/**
1373
	 * Get the results of the Filters performed, including the number of matching documents.
1374
	 *
1375
	 * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
1376
	 * matching buckets, the url for applying/removing each bucket, etc.
1377
	 *
1378
	 * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
1379
	 * member if you need to access the raw filters set in Jetpack_Search::set_filters().
1380
	 *
1381
	 * @since 5.0.0
1382
	 *
1383
	 * @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...
1384
	 *
1385
	 * @return array Array of filters applied and info about them.
1386
	 */
1387
	public function get_filters( WP_Query $query = null ) {
1388
		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...
1389
			global $wp_query;
1390
1391
			$query = $wp_query;
1392
		}
1393
1394
		$aggregation_data = $this->aggregations;
1395
1396
		if ( empty( $aggregation_data ) ) {
1397
			return $aggregation_data;
1398
		}
1399
1400
		$aggregation_results = $this->get_search_aggregations_results();
1401
1402
		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...
1403
			return $aggregation_data;
1404
		}
1405
1406
		// NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES
1407
		foreach ( $aggregation_results as $label => $aggregation ) {
1408
			if ( empty( $aggregation ) ) {
1409
				continue;
1410
			}
1411
1412
			$type = $this->aggregations[ $label ]['type'];
1413
1414
			$aggregation_data[ $label ]['buckets'] = array();
1415
1416
			$existing_term_slugs = array();
1417
1418
			$tax_query_var = null;
1419
1420
			// Figure out which terms are active in the query, for this taxonomy
1421
			if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
1422
				$tax_query_var = $this->get_taxonomy_query_var( $this->aggregations[ $label ]['taxonomy'] );
1423
1424
				if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
1425
					foreach ( $query->tax_query->queries as $tax_query ) {
1426
						if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
1427
						     'slug' === $tax_query['field'] &&
1428
						     is_array( $tax_query['terms'] ) ) {
1429
							$existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
1430
						}
1431
					}
1432
				}
1433
			}
1434
1435
			// Now take the resulting found aggregation items and generate the additional info about them, such as activation/deactivation url, name, count, etc.
1436
			$buckets = array();
1437
1438
			if ( ! empty( $aggregation['buckets'] ) ) {
1439
				$buckets = (array) $aggregation['buckets'];
1440
			}
1441
1442
			if ( 'date_histogram' == $type ) {
1443
				//re-order newest to oldest
1444
				$buckets = array_reverse( $buckets );
1445
			}
1446
1447
			// Some aggregation types like date_histogram don't support the max results parameter
1448
			if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
1449
				$buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
1450
			}
1451
1452
			foreach ( $buckets as $item ) {
1453
				$query_vars = array();
1454
				$active     = false;
1455
				$remove_url = null;
1456
				$name       = '';
1457
1458
				// What type was the original aggregation?
1459
				switch ( $type ) {
1460
					case 'taxonomy':
1461
						$taxonomy = $this->aggregations[ $label ]['taxonomy'];
1462
1463
						$term = get_term_by( 'slug', $item['key'], $taxonomy );
1464
1465
						if ( ! $term || ! $tax_query_var ) {
1466
							continue 2; // switch() is considered a looping structure
1467
						}
1468
1469
						$query_vars = array(
1470
							$tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
1471
						);
1472
1473
						$name = $term->name;
1474
1475
						// Let's determine if this term is active or not
1476
1477
						if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
1478
							$active = true;
1479
1480
							$slug_count = count( $existing_term_slugs );
1481
1482 View Code Duplication
							if ( $slug_count > 1 ) {
1483
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1484
									$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...
1485
									rawurlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) )
1486
								);
1487
							} else {
1488
								$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...
1489
							}
1490
						}
1491
1492
						break;
1493
1494
					case 'post_type':
1495
						$post_type = get_post_type_object( $item['key'] );
1496
1497
						if ( ! $post_type || $post_type->exclude_from_search ) {
1498
							continue 2;  // switch() is considered a looping structure
1499
						}
1500
1501
						$query_vars = array(
1502
							'post_type' => $item['key'],
1503
						);
1504
1505
						$name = $post_type->labels->singular_name;
1506
1507
						// Is this post type active on this search?
1508
						$post_types = $query->get( 'post_type' );
1509
1510
						if ( ! is_array( $post_types ) ) {
1511
							$post_types = array( $post_types );
1512
						}
1513
1514
						if ( in_array( $item['key'], $post_types ) ) {
1515
							$active = true;
1516
1517
							$post_type_count = count( $post_types );
1518
1519
							// 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
1520 View Code Duplication
							if ( $post_type_count > 1 ) {
1521
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1522
									'post_type',
1523
									rawurlencode( implode( ',', array_diff( $post_types, array( $item['key'] ) ) ) )
1524
								);
1525
							} else {
1526
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
1527
							}
1528
						}
1529
1530
						break;
1531
1532
					case 'date_histogram':
1533
						$timestamp = $item['key'] / 1000;
1534
1535
						$current_year  = $query->get( 'year' );
1536
						$current_month = $query->get( 'monthnum' );
1537
						$current_day   = $query->get( 'day' );
1538
1539
						switch ( $this->aggregations[ $label ]['interval'] ) {
1540
							case 'year':
1541
								$year = (int) date( 'Y', $timestamp );
1542
1543
								$query_vars = array(
1544
									'year'     => $year,
1545
									'monthnum' => false,
1546
									'day'      => false,
1547
								);
1548
1549
								$name = $year;
1550
1551
								// Is this year currently selected?
1552
								if ( ! empty( $current_year ) && (int) $current_year === $year ) {
1553
									$active = true;
1554
1555
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
1556
								}
1557
1558
								break;
1559
1560
							case 'month':
1561
								$year  = (int) date( 'Y', $timestamp );
1562
								$month = (int) date( 'n', $timestamp );
1563
1564
								$query_vars = array(
1565
									'year'     => $year,
1566
									'monthnum' => $month,
1567
									'day'      => false,
1568
								);
1569
1570
								$name = date( 'F Y', $timestamp );
1571
1572
								// Is this month currently selected?
1573
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1574
								     ! empty( $current_month ) && (int) $current_month === $month ) {
1575
									$active = true;
1576
1577
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
1578
								}
1579
1580
								break;
1581
1582
							case 'day':
1583
								$year  = (int) date( 'Y', $timestamp );
1584
								$month = (int) date( 'n', $timestamp );
1585
								$day   = (int) date( 'j', $timestamp );
1586
1587
								$query_vars = array(
1588
									'year'     => $year,
1589
									'monthnum' => $month,
1590
									'day'      => $day,
1591
								);
1592
1593
								$name = date( 'F jS, Y', $timestamp );
1594
1595
								// Is this day currently selected?
1596
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1597
								     ! empty( $current_month ) && (int) $current_month === $month &&
1598
								     ! empty( $current_day ) && (int) $current_day === $day ) {
1599
									$active = true;
1600
1601
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
1602
								}
1603
1604
								break;
1605
1606
							default:
1607
								continue 3; // switch() is considered a looping structure
1608
						} // End switch().
1609
1610
						break;
1611
1612
					default:
1613
						//continue 2; // switch() is considered a looping structure
1614
				} // End switch().
1615
1616
				// Need to urlencode param values since add_query_arg doesn't
1617
				$url_params = urlencode_deep( $query_vars );
1618
1619
				$aggregation_data[ $label ]['buckets'][] = array(
1620
					'url'        => Jetpack_Search_Helpers::add_query_arg( $url_params ),
1621
					'query_vars' => $query_vars,
1622
					'name'       => $name,
1623
					'count'      => $item['doc_count'],
1624
					'active'     => $active,
1625
					'remove_url' => $remove_url,
1626
					'type'       => $type,
1627
					'type_label' => $aggregation_data[ $label ]['name'],
1628
					'widget_id'  => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0
1629
				);
1630
			} // End foreach().
1631
		} // End foreach().
1632
1633
		return $aggregation_data;
1634
	}
1635
1636
	/**
1637
	 * Get the results of the facets performed.
1638
	 *
1639
	 * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead.
1640
	 *
1641
	 * @see        Jetpack_Search::get_filters()
1642
	 *
1643
	 * @return array $facets Array of facets applied and info about them.
1644
	 */
1645
	public function get_search_facet_data() {
1646
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
1647
1648
		return $this->get_filters();
1649
	}
1650
1651
	/**
1652
	 * Get the filters that are currently applied to this search.
1653
	 *
1654
	 * @since 5.0.0
1655
	 *
1656
	 * @return array Array of filters that were applied.
1657
	 */
1658
	public function get_active_filter_buckets() {
1659
		$active_buckets = array();
1660
1661
		$filters = $this->get_filters();
1662
1663
		if ( ! is_array( $filters ) ) {
1664
			return $active_buckets;
1665
		}
1666
1667
		foreach ( $filters as $filter ) {
1668
			if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
1669
				foreach ( $filter['buckets'] as $item ) {
1670
					if ( isset( $item['active'] ) && $item['active'] ) {
1671
						$active_buckets[] = $item;
1672
					}
1673
				}
1674
			}
1675
		}
1676
1677
		return $active_buckets;
1678
	}
1679
1680
	/**
1681
	 * Get the filters that are currently applied to this search.
1682
	 *
1683
	 * @deprecated 5.0 Please use Jetpack_Search::get_active_filter_buckets() instead.
1684
	 *
1685
	 * @see        Jetpack_Search::get_active_filter_buckets()
1686
	 *
1687
	 * @return array Array of filters that were applied.
1688
	 */
1689
	public function get_current_filters() {
1690
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
1691
1692
		return $this->get_active_filter_buckets();
1693
	}
1694
1695
	/**
1696
	 * Calculate the right query var to use for a given taxonomy.
1697
	 *
1698
	 * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter.
1699
	 *
1700
	 * @since 5.0.0
1701
	 *
1702
	 * @param string $taxonomy_name The name of the taxonomy for which to get the query var.
1703
	 *
1704
	 * @return bool|string The query var to use for this taxonomy, or false if none found.
1705
	 */
1706
	public function get_taxonomy_query_var( $taxonomy_name ) {
1707
		$taxonomy = get_taxonomy( $taxonomy_name );
1708
1709
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1710
			return false;
1711
		}
1712
1713
		/**
1714
		 * Modify the query var to use for a given taxonomy
1715
		 *
1716
		 * @module search
1717
		 *
1718
		 * @since  5.0.0
1719
		 *
1720
		 * @param string $query_var     The current query_var for the taxonomy
1721
		 * @param string $taxonomy_name The taxonomy name
1722
		 */
1723
		return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
1724
	}
1725
1726
	/**
1727
	 * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
1728
	 * which is the input order.
1729
	 *
1730
	 * Necessary because ES does not always return aggregations in the same order that you pass them in,
1731
	 * and it should be possible to control the display order easily.
1732
	 *
1733
	 * @since 5.0.0
1734
	 *
1735
	 * @param array $aggregations Aggregation results to be reordered.
1736
	 * @param array $desired      Array with keys representing the desired ordering.
1737
	 *
1738
	 * @return array A new array with reordered keys, matching those in $desired.
1739
	 */
1740
	public function fix_aggregation_ordering( array $aggregations, array $desired ) {
1741
		if ( empty( $aggregations ) || empty( $desired ) ) {
1742
			return $aggregations;
1743
		}
1744
1745
		$reordered = array();
1746
1747
		foreach ( array_keys( $desired ) as $agg_name ) {
1748
			if ( isset( $aggregations[ $agg_name ] ) ) {
1749
				$reordered[ $agg_name ] = $aggregations[ $agg_name ];
1750
			}
1751
		}
1752
1753
		return $reordered;
1754
	}
1755
1756
	/**
1757
	 * Sends events to Tracks when a search filters widget is updated.
1758
	 *
1759
	 * @since 5.8.0
1760
	 *
1761
	 * @param string $option    The option name. Only "widget_jetpack-search-filters" is cared about.
1762
	 * @param array  $old_value The old option value.
1763
	 * @param array  $new_value The new option value.
1764
	 */
1765
	public function track_widget_updates( $option, $old_value, $new_value ) {
1766
		if ( 'widget_jetpack-search-filters' !== $option ) {
1767
			return;
1768
		}
1769
1770
		$event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value );
1771
		if ( ! $event ) {
1772
			return;
1773
		}
1774
1775
		jetpack_tracks_record_event(
1776
			wp_get_current_user(),
1777
			sprintf( 'jetpack_search_widget_%s', $event['action'] ),
1778
			$event['widget']
1779
		);
1780
	}
1781
1782
	/**
1783
	 * Moves any active search widgets to the inactive category.
1784
	 *
1785
	 * @since 5.9.0
1786
	 *
1787
	 * @param string $module Unused. The Jetpack module being disabled.
1788
	 */
1789
	public function move_search_widgets_to_inactive( $module ) {
1790
		if ( ! is_active_widget( false, false, Jetpack_Search_Helpers::FILTER_WIDGET_BASE, true ) ) {
1791
			return;
1792
		}
1793
1794
		$sidebars_widgets = wp_get_sidebars_widgets();
1795
1796
		if ( ! is_array( $sidebars_widgets ) ) {
1797
			return;
1798
		}
1799
1800
		$changed = false;
1801
1802
		foreach ( $sidebars_widgets as $sidebar => $widgets ) {
1803
			if ( 'wp_inactive_widgets' === $sidebar || 'orphaned_widgets' === substr( $sidebar, 0, 16 ) ) {
1804
				continue;
1805
			}
1806
1807
			if ( is_array( $widgets ) ) {
1808
				foreach ( $widgets as $key => $widget ) {
1809
					if ( _get_widget_id_base( $widget ) == Jetpack_Search_Helpers::FILTER_WIDGET_BASE ) {
1810
						$changed = true;
1811
1812
						array_unshift( $sidebars_widgets['wp_inactive_widgets'], $widget );
1813
						unset( $sidebars_widgets[ $sidebar ][ $key ] );
1814
					}
1815
				}
1816
			}
1817
		}
1818
1819
		if ( $changed ) {
1820
			wp_set_sidebars_widgets( $sidebars_widgets );
1821
		}
1822
	}
1823
}
1824