Completed
Push — add/tracks-for-search ( 03d31b...2054a9 )
by
unknown
35:04 queued 24:22
created

Jetpack_Search   D

Complexity

Total Complexity 221

Size/Duplication

Total Lines 1554
Duplicated Lines 2.25 %

Coupling/Cohesion

Components 2
Dependencies 4

Importance

Changes 0
Metric Value
dl 35
loc 1554
rs 4.4102
c 0
b 0
f 0
wmc 221
lcom 2
cbo 4

40 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A __clone() 0 3 1
A __wakeup() 0 3 1
A instance() 0 9 2
A store_query_failure() 0 4 1
A print_query_failure() 0 10 2
A store_query_success() 0 4 1
A print_query_success() 0 9 2
A are_filters_by_widget_disabled() 0 13 1
A setup() 0 15 4
A init_hooks() 0 18 2
A set_filters_from_widgets() 0 11 3
B maybe_add_post_type_as_var() 0 9 6
C search() 0 94 11
B filter__posts_pre_query() 0 55 8
D do_search() 0 107 13
A update_search_results_aggregations() 0 14 3
D get_es_wp_query_terms_for_query() 3 33 10
C get_es_wp_query_post_type_for_query() 0 34 7
A action__widgets_init() 0 5 1
B get_search_result() 0 7 6
B filter__add_date_filter_to_query() 0 32 4
F convert_wp_es_to_es_args() 32 248 36
B add_aggregations_to_es_query_builder() 0 20 5
B add_taxonomy_aggregation_to_es_query_builder() 0 24 3
A add_post_type_aggregation_to_es_query_builder() 0 8 1
A add_date_histogram_aggregation_to_es_query_builder() 0 16 4
A and_es_filters() 0 15 4
A score_query_by_recency() 0 14 1
A set_filters() 0 8 3
A set_facets() 0 5 1
A get_search_aggregations_results() 0 11 3
A get_search_facets() 0 5 1
F get_filters() 0 249 48
A get_search_facet_data() 0 5 1
B get_active_filter_buckets() 0 21 8
A get_current_filters() 0 5 1
A get_taxonomy_query_var() 0 19 3
B fix_aggregation_ordering() 0 15 5
A track_widget_updates() 0 16 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Jetpack_Search often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Jetpack_Search, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
class Jetpack_Search {
4
5
	protected $found_posts = 0;
6
7
	/**
8
	 * The maximum offset ('from' param), since deep pages get exponentially slower.
9
	 *
10
	 * @see https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html
11
	 */
12
	protected $max_offset = 200;
13
14
	protected $search_result;
15
16
	protected $original_blog_id;
17
	protected $jetpack_blog_id;
18
19
	protected $aggregations = array();
20
	protected $max_aggregations_count = 100;
21
22
	// used to output query meta into page
23
	protected $last_query_info;
24
	protected $last_query_failure_info;
25
26
	protected static $instance;
27
28
	//Languages with custom analyzers, other languages are supported,
29
	// but are analyzed with the default analyzer.
30
	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' );
31
32
	protected function __construct() {
33
		/* Don't do anything, needs to be initialized via instance() method */
34
	}
35
36
	public function __clone() {
37
		wp_die( "Please don't __clone Jetpack_Search" );
38
	}
39
40
	public function __wakeup() {
41
		wp_die( "Please don't __wakeup Jetpack_Search" );
42
	}
43
44
	/**
45
	 * Get singleton instance of Jetpack_Search
46
	 *
47
	 * Instantiates and sets up a new instance if needed, or returns the singleton
48
	 *
49
	 * @module search
50
	 *
51
	 * @return Jetpack_Search The Jetpack_Search singleton
52
	 */
53
	public static function instance() {
54
		if ( ! isset( self::$instance ) ) {
55
			self::$instance = new Jetpack_Search();
56
57
			self::$instance->setup();
58
		}
59
60
		return self::$instance;
61
	}
62
63
	/**
64
	 * Perform various setup tasks for the class
65
	 *
66
	 * Checks various pre-requisites and adds hooks
67
	 *
68
	 * @module search
69
	 */
70
	public function setup() {
71
		if ( ! Jetpack::is_active() || ! Jetpack::active_plan_supports( 'search' ) ) {
72
			return;
73
		}
74
75
		$this->jetpack_blog_id = Jetpack::get_option( 'id' );
76
77
		if ( ! $this->jetpack_blog_id ) {
78
			return;
79
		}
80
81
		require_once( dirname( __FILE__ ) . '/class.jetpack-search-helpers.php' );
82
83
		$this->init_hooks();
84
	}
85
86
	/**
87
	 * Setup the various hooks needed for the plugin to take over Search duties
88
	 *
89
	 * @module search
90
	 */
91
	public function init_hooks() {
92
		add_action( 'widgets_init', array( $this, 'action__widgets_init' ) );
93
94
		if ( ! is_admin() ) {
95
			add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 );
96
97
			add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ),  10, 2 );
98
99
			add_action( 'did_jetpack_search_query', array( $this, 'store_query_success' ) );
100
			add_action( 'failed_jetpack_search_query', array( $this, 'store_query_failure' ) );
101
102
			add_action( 'init', array( $this, 'set_filters_from_widgets' ) );
103
104
			add_action( 'pre_get_posts', array( $this, 'maybe_add_post_type_as_var' ) );
105
		} else {
106
			add_action( 'update_option', array( $this, 'track_widget_updates' ), 10, 3 );
107
		}
108
	}
109
110
	/**
111
	 * Print query info as a HTML comment in the footer
112
	 */
113
114
	public function store_query_failure( $meta ) {
115
		$this->last_query_failure_info = $meta;
116
		add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
117
	}
118
119
	public function print_query_failure() {
120
		if ( $this->last_query_failure_info ) {
121
			printf(
122
				'<!-- Jetpack Search failed with code %s: %s - %s -->',
123
				esc_html( $this->last_query_failure_info['response_code'] ),
124
				esc_html( $this->last_query_failure_info['json']['error'] ),
125
				esc_html( $this->last_query_failure_info['json']['message'] )
126
			);
127
		}
128
	}
129
130
	public function store_query_success( $meta ) {
131
		$this->last_query_info = $meta;
132
		add_action( 'wp_footer', array( $this, 'print_query_success' ) );
133
	}
134
135
	public function print_query_success() {
136
		if ( $this->last_query_info ) {
137
			printf(
138
				'<!-- Jetpack Search took %s ms, ES time %s ms -->',
139
				intval( $this->last_query_info['elapsed_time'] ),
140
				esc_html( $this->last_query_info['es_time'] )
141
			);
142
		}
143
	}
144
145
	function are_filters_by_widget_disabled() {
146
		/**
147
		 * Allows developers to disable filters being set by widget, in favor of manually
148
		 * setting filters via `Jetpack_Search::set_filters()`.
149
		 *
150
		 * @module search
151
		 *
152
		 * @since 5.7.0
153
		 *
154
		 * @param bool false
155
		 */
156
		return apply_filters( 'jetpack_search_disable_widget_filters', false );
157
	}
158
159
	/**
160
	 * Retrives a list of known Jetpack search filters widget IDs, gets the filters for each widget,
161
	 * and applies those filters to this Jetpack_Search object.
162
	 *
163
	 * @since 5.7.0
164
	 *
165
	 * @return void
166
	 */
167
	function set_filters_from_widgets() {
168
		if ( $this->are_filters_by_widget_disabled() ) {
169
			return;
170
		}
171
172
		$filters = Jetpack_Search_Helpers::get_filters_from_widgets();
173
174
		if ( ! empty( $filters ) ) {
175
			$this->set_filters( $filters );
176
		}
177
	}
178
179
	function maybe_add_post_type_as_var( $query ) {
180
		if ( $query->is_main_query() && $query->is_search && ! empty( $_GET['post_type'] ) ) {
181
			$post_types = ( is_string( $_GET['post_type'] ) && false !== strpos( $_GET['post_type'], ',' ) )
182
				? $post_type = explode( ',', $_GET['post_type'] )
183
				: (array) $_GET['post_type'];
184
			$post_types = array_map( 'sanitize_key', $post_types );
185
			$query->set('post_type', $post_types );
186
		}
187
	}
188
189
	/*
190
	 * Run a search on the WP.com public API.
191
	 *
192
	 * @module search
193
	 *
194
	 * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint
195
	 *
196
	 * @return object|WP_Error The response from the public api, or a WP_Error
197
	 */
198
	public function search( array $es_args ) {
199
		$endpoint    = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
200
		$service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
201
202
		$do_authenticated_request = false;
203
204
		if ( class_exists( 'Jetpack_Client' ) &&
205
			isset( $es_args['authenticated_request'] ) &&
206
			true === $es_args['authenticated_request'] ) {
207
			$do_authenticated_request = true;
208
		}
209
210
		unset( $es_args['authenticated_request'] );
211
212
		$request_args = array(
213
			'headers' => array(
214
				'Content-Type' => 'application/json',
215
			),
216
			'timeout'    => 10,
217
			'user-agent' => 'jetpack_search',
218
		);
219
220
		$request_body = json_encode( $es_args );
221
222
		$start_time = microtime( true );
223
224
		if ( $do_authenticated_request ) {
225
			$request_args['method'] = 'POST';
226
227
			$request = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint, Jetpack_Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
228
		} else {
229
			$request_args = array_merge( $request_args, array(
230
				'body' => $request_body,
231
			) );
232
233
			$request = wp_remote_post( $service_url, $request_args );
234
		}
235
236
		$end_time = microtime( true );
237
238
		if ( is_wp_error( $request ) ) {
239
			return $request;
240
		}
241
242
		$response_code = wp_remote_retrieve_response_code( $request );
243
		$response = json_decode( wp_remote_retrieve_body( $request ), true );
244
245
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
246
			/**
247
			 * Fires after a search query request has failed
248
			 *
249
			 * @module search
250
			 *
251
			 * @since 5.6.0
252
			 *
253
			 * @param array Array containing the response code and response from the failed search query
254
			 */
255
			do_action( 'failed_jetpack_search_query', array( 'response_code' => $response_code, 'json' => $response ) );
256
			return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
257
		}
258
259
		$took = is_array( $response ) && $response['took'] ? $response['took'] : null;
260
261
		$query = array(
262
			'args'          => $es_args,
263
			'response'      => $response,
264
			'response_code' => $response_code,
265
			'elapsed_time'   => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms
266
			'es_time'       => $took,
267
			'url'           => $service_url,
268
		);
269
270
		/**
271
		 * Fires after a search request has been performed
272
		 *
273
		 * Includes the following info in the $query parameter:
274
		 *
275
		 * array args Array of Elasticsearch arguments for the search
276
		 * array response Raw API response, JSON decoded
277
		 * int response_code HTTP response code of the request
278
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
279
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
280
		 * string url API url that was queried
281
		 *
282
		 * @module search
283
		 *
284
		 * @since 5.0.0
285
		 *
286
		 * @param array $query Array of information about the query performed
287
		 */
288
		do_action( 'did_jetpack_search_query', $query );
289
290
		return $response;
291
	}
292
293
	/**
294
	 * Bypass the normal Search query and offload it to Jetpack servers
295
	 *
296
	 * This is the main hook of the plugin and is responsible for returning the posts that match the search query
297
	 *
298
	 * @module search
299
	 *
300
	 * @param array $posts Current array of posts (still pre-query)
301
	 * @param WP_Query $query The WP_Query being filtered
302
	 *
303
	 * @return array Array of matching posts
304
	 */
305
	public function filter__posts_pre_query( $posts, $query ) {
306
		/**
307
		 * Determine whether a given WP_Query should be handled by ElasticSearch
308
		 *
309
		 * @module search
310
		 *
311
		 * @since 5.6.0
312
		 * @param bool $should_handle Should be handled by Jetpack Search
313
		 * @param WP_Query $query The wp_query object
314
		 */
315
		if ( ! apply_filters( 'jetpack_search_should_handle_query', ( $query->is_main_query() && $query->is_search() ), $query ) ) {
316
			return $posts;
317
		}
318
319
		$this->do_search( $query );
320
321
		if ( ! is_array( $this->search_result ) ) {
322
			return $posts;
323
		}
324
325
		// If no results, nothing to do
326
		if ( ! count( $this->search_result['results']['hits'] ) ) {
327
			return array();
328
		}
329
330
		$post_ids = array();
331
332
		foreach ( $this->search_result['results']['hits'] as $result ) {
333
			$post_ids[] = (int) $result['fields']['post_id'];
334
		}
335
336
		// Query all posts now
337
		$args = array(
338
			'post__in'  => $post_ids,
339
			'perm'      => 'readable',
340
			'post_type' => 'any'
341
		);
342
343
		if ( isset( $query->query_vars['order'] ) ) {
344
			$args['order'] = $query->query_vars['order'];
345
		}
346
347
		if ( isset( $query->query_vars['orderby'] ) ) {
348
			$args['orderby'] = $query->query_vars['orderby'];
349
		}
350
351
		$posts_query = new WP_Query( $args );
352
353
		// WP Core doesn't call the set_found_posts and its filters when filtering posts_pre_query like we do, so need to
354
		// do these manually
355
		$query->found_posts   = $this->found_posts;
356
		$query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
357
358
		return $posts_query->posts;
359
	}
360
361
	/**
362
	 * Build up the search, then run it against the Jetpack servers
363
	 *
364
	 * @param WP_Query $query The original WP_Query to use for the parameters of our search
365
	 */
366
	public function do_search( WP_Query $query ) {
367
		$page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
368
369
		$posts_per_page = $query->get( 'posts_per_page' );
370
371
		// ES API does not allow more than 15 results at a time
372
		if ( $posts_per_page > 15 ) {
373
			$posts_per_page = 15;
374
		}
375
376
		// Start building the WP-style search query args
377
		// They'll be translated to ES format args later
378
		$es_wp_query_args = array(
379
			'query'          => $query->get( 's' ),
380
			'posts_per_page' => $posts_per_page,
381
			'paged'          => $page,
382
			'orderby'        => $query->get( 'orderby' ),
383
			'order'          => $query->get( 'order' ),
384
		);
385
386
		if ( ! empty( $this->aggregations ) ) {
387
			$es_wp_query_args['aggregations'] = $this->aggregations;
388
		}
389
390
		// Did we query for authors?
391
		if ( $query->get( 'author_name' ) ) {
392
			$es_wp_query_args['author_name'] = $query->get( 'author_name' );
393
		}
394
395
		$es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
396
397
		$es_wp_query_args['terms']     = $this->get_es_wp_query_terms_for_query( $query );
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 5 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
398
399
400
		/**
401
		 * Modify the search query parameters, such as controlling the post_type.
402
		 *
403
		 * These arguments are in the format of WP_Query arguments
404
		 *
405
		 * @module search
406
		 *
407
		 * @since 5.0.0
408
		 *
409
		 * @param array $es_wp_query_args The current query args, in WP_Query format
410
		 * @param WP_Query $query The original query object
411
		 */
412
		$es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
413
414
		// If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
415
		// capped at $this->max_offset, so a high page would always return the last page of results otherwise
416
		if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $this->max_offset ) {
417
			$query->set_404();
418
419
			return;
420
		}
421
422
		// If there were no post types returned, then 404 to avoid querying against non-public post types, which could
423
		// happen if we don't add the post type restriction to the ES query
424
		if ( empty( $es_wp_query_args['post_type'] ) ) {
425
			$query->set_404();
426
427
			return;
428
		}
429
430
		// Convert the WP-style args into ES args
431
		$es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
432
433
		//Only trust ES to give us IDs, not the content since it is a mirror
434
		$es_query_args['fields'] = array(
435
			'post_id',
436
		);
437
438
		/**
439
		 * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
440
		 *
441
		 * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
442
		 *
443
		 * @module search
444
		 *
445
		 * @since 5.0.0
446
		 *
447
		 * @param array $es_query_args The raw ES query args
448
		 * @param WP_Query $query The original query object
449
		 */
450
		$es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
451
452
		// Do the actual search query!
453
		$this->search_result = $this->search( $es_query_args );
454
455
		if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
456
			$this->found_posts = 0;
457
458
			return;
459
		}
460
461
		// If we have aggregations, fix the ordering to match the input order (ES doesn't
462
		// guarantee the return order)
463
		if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
464
			$this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
465
		}
466
467
		// Total number of results for paging purposes. Capped at $this->>max_offset + $posts_per_page, as deep paging
468
		// gets quite expensive
469
		$this->found_posts = min( $this->search_result['results']['total'], $this->max_offset + $posts_per_page );
470
471
		return;
472
	}
473
474
	/**
475
	 * If the query has already been run before filters have been updated, then we need to re-run the query
476
	 * to get the latest aggregations.
477
	 *
478
	 * This is especially useful for supporting widget management in the customizer.
479
	 *
480
	 * @return bool Whether the query was successful or not.
481
	 */
482
	public function update_search_results_aggregations() {
483
		if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
484
			return false;
485
		}
486
487
		$es_args = $this->last_query_info['args'];
488
		$builder = new Jetpack_WPES_Query_Builder();
489
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
490
		$es_args['aggregations'] = $builder->build_aggregation();
491
492
		$this->search_result = $this->search( $es_args );
493
494
		return ! is_wp_error( $this->search_result );
495
	}
496
497
	/**
498
	 * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style ES term arguments for the search
499
	 *
500
	 * @module search
501
	 *
502
	 * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query
503
	 *
504
	 * @return array The new WP-style ES arguments (that will be converted into 'real' ES arguments)
505
	 */
506
	public function get_es_wp_query_terms_for_query( WP_Query $query ) {
507
		$args = array();
508
509
		$the_tax_query = $query->tax_query;
510
511
		if ( ! $the_tax_query ) {
512
			return $args;
513
		}
514
515
516
		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...
517
			return $args;
518
		}
519
520
		$args = array();
521
522
		foreach ( $the_tax_query->queries as $tax_query ) {
523
			// Right now we only support slugs...see note above
524
			if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
525
				continue;
526
			}
527
528
			$taxonomy = $tax_query['taxonomy'];
529
530 View Code Duplication
			if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
531
				$args[ $taxonomy ] = array();
532
			}
533
534
			$args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
535
		}
536
537
		return $args;
538
	}
539
540
	/**
541
	 * Parse out the post type from a WP_Query
542
	 *
543
	 * Only allows post types that are not marked as 'exclude_from_search'
544
	 *
545
	 * @module search
546
	 *
547
	 * @param WP_Query $query Original WP_Query object
548
	 *
549
	 * @return array Array of searchable post types corresponding to the original query
550
	 */
551
	public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
552
		$post_types = $query->get( 'post_type' );
553
554
		// If we're searching 'any', we want to only pass searchable post types to ES
555
		if ( 'any' === $post_types ) {
556
			$post_types = array_values( get_post_types( array(
557
				'exclude_from_search' => false,
558
			) ) );
559
		}
560
561
		if ( ! is_array( $post_types ) ) {
562
			$post_types = array( $post_types );
563
		}
564
565
		$post_types = array_unique( $post_types );
566
567
		$sanitized_post_types = array();
568
569
		// Make sure the post types are queryable
570
		foreach ( $post_types as $post_type ) {
571
			if ( ! $post_type ) {
572
				continue;
573
			}
574
575
			$post_type_object = get_post_type_object( $post_type );
576
			if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
577
				continue;
578
			}
579
580
			$sanitized_post_types[] = $post_type;
581
		}
582
583
		return $sanitized_post_types;
584
	}
585
586
	/**
587
	 * Initialze widgets for the Search module
588
	 *
589
	 * @module search
590
	 */
591
	public function action__widgets_init() {
592
		require_once( dirname( __FILE__ ) . '/class.jetpack-search-widget-filters.php' );
593
594
		register_widget( 'Jetpack_Search_Widget_Filters' );
595
	}
596
597
	/**
598
	 * Get the Elasticsearch result
599
	 *
600
	 * @module search
601
	 *
602
	 * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response
603
	 *
604
	 * @return array|bool The search results, or false if there was a failure
605
	 */
606
	public function get_search_result( $raw = false ) {
607
		if ( $raw ) {
608
			return $this->search_result;
609
		}
610
611
		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;
612
	}
613
614
	/**
615
	 * Add the date portion of a WP_Query onto the query args
616
	 *
617
	 * @param array    $es_wp_query_args
618
	 * @param WP_Query $query The original WP_Query
619
	 *
620
	 * @return array The es wp query args, with date filters added (as needed)
621
	 */
622
	public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
623
		if ( $query->get( 'year' ) ) {
624
			if ( $query->get( 'monthnum' ) ) {
625
				// Padding
626
				$date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
627
628
				if ( $query->get( 'day' ) ) {
629
					// Padding
630
					$date_day = sprintf( '%02d', $query->get( 'day' ) );
631
632
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
633
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
634
				} else {
635
					$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
636
637
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
638
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
639
				}
640
			} else {
641
				$date_start = $query->get( 'year' ) . '-01-01 00:00:00';
642
				$date_end   = $query->get( 'year' ) . '-12-31 23:59:59';
643
			}
644
645
			$es_wp_query_args['date_range'] = array(
646
				'field' => 'date',
647
				'gte'   => $date_start,
648
				'lte'   => $date_end,
649
			);
650
		}
651
652
		return $es_wp_query_args;
653
	}
654
655
	/**
656
	 * Converts WP_Query style args to ES args
657
	 *
658
	 * @module search
659
	 *
660
	 * @param array $args Array of WP_Query style arguments
661
	 *
662
	 * @return array Array of ES style query arguments
663
	 */
664
	function convert_wp_es_to_es_args( array $args ) {
665
		jetpack_require_lib( 'jetpack-wpes-query-builder' );
666
667
		$builder = new Jetpack_WPES_Query_Builder();
668
669
		$defaults = array(
670
			'blog_id'        => get_current_blog_id(),
671
672
			'query'          => null,    // Search phrase
673
			'query_fields'   => array( 'title', 'content', 'author', 'tag', 'category' ),
674
675
			'post_type'      => null,  // string or an array
676
			'terms'          => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) )
677
678
			'author'         => null,    // id or an array of ids
679
			'author_name'    => array(), // string or an array
680
681
			'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'
682
683
			'orderby'        => null,    // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
684
			'order'          => 'DESC',
685
686
			'posts_per_page' => 10,
687
688
			'offset'         => null,
689
			'paged'          => null,
690
691
			/**
692
			 * Aggregations. Examples:
693
			 * array(
694
			 *     'Tag'       => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
695
			 *     'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
696
			 * );
697
			 */
698
			'aggregations'         => null,
699
		);
700
701
		$args = wp_parse_args( $args, $defaults );
702
703
		$es_query_args = array(
704
			'blog_id' => absint( $args['blog_id'] ),
705
			'size'    => absint( $args['posts_per_page'] ),
706
		);
707
708
		// ES "from" arg (offset)
709
		if ( $args['offset'] ) {
710
			$es_query_args['from'] = absint( $args['offset'] );
711
		} elseif ( $args['paged'] ) {
712
			$es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
713
		}
714
715
		// Limit the offset to $this->max_offset posts, as deep pages get exponentially slower
716
		// See https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html
717
		$es_query_args['from'] = min( $es_query_args['from'], $this->max_offset );
718
719
		if ( ! is_array( $args['author_name'] ) ) {
720
			$args['author_name'] = array( $args['author_name'] );
721
		}
722
723
		// ES stores usernames, not IDs, so transform
724
		if ( ! empty( $args['author'] ) ) {
725
			if ( ! is_array( $args['author'] ) ) {
726
				$args['author'] = array( $args['author'] );
727
			}
728
729
			foreach ( $args['author'] as $author ) {
730
				$user = get_user_by( 'id', $author );
731
732
				if ( $user && ! empty( $user->user_login ) ) {
733
					$args['author_name'][] = $user->user_login;
734
				}
735
			}
736
		}
737
738
		//////////////////////////////////////////////////
739
		// Build the filters from the query elements.
740
		// Filters rock because they are cached from one query to the next
741
		// but they are cached as individual filters, rather than all combined together.
742
		// May get performance boost by also caching the top level boolean filter too.
743
		$filters = array();
744
745
		if ( $args['post_type'] ) {
746
			if ( ! is_array( $args['post_type'] ) ) {
747
				$args['post_type'] = array( $args['post_type'] );
748
			}
749
750
			$filters[] = array(
751
				'terms' => array(
752
					'post_type' => $args['post_type'],
753
				),
754
			);
755
		}
756
757
		if ( $args['author_name'] ) {
758
			$filters[] = array(
759
				'terms' => array(
760
					'author_login' => $args['author_name'],
761
				),
762
			);
763
		}
764
765
		if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
766
			$field = $args['date_range']['field'];
767
768
			unset( $args['date_range']['field'] );
769
770
			$filters[] = array(
771
				'range' => array(
772
					$field => $args['date_range'],
773
				),
774
			);
775
		}
776
777
		if ( is_array( $args['terms'] ) ) {
778
			foreach ( $args['terms'] as $tax => $terms ) {
779
				$terms = (array) $terms;
780
781
				if ( count( $terms ) && mb_strlen( $tax ) ) {
782 View Code Duplication
					switch ( $tax ) {
783
						case 'post_tag':
784
							$tax_fld = 'tag.slug';
785
786
							break;
787
788
						case 'category':
789
							$tax_fld = 'category.slug';
790
791
							break;
792
793
						default:
794
							$tax_fld = 'taxonomy.' . $tax . '.slug';
795
796
							break;
797
					}
798
799
					foreach ( $terms as $term ) {
800
						$filters[] = array(
801
							'term' => array(
802
								$tax_fld => $term,
803
							),
804
						);
805
					}
806
				}
807
			}
808
		}
809
810
		if ( $args['query'] ) {
811
			$query = array(
812
				'multi_match' => array(
813
					'query'    => $args['query'],
814
					'fields'   => $args['query_fields'],
815
					'operator' => 'and',
816
					'type'     => 'cross_fields',
817
				),
818
			);
819
820
			$builder->add_query( $query );
821
822
			Jetpack_Search::score_query_by_recency( $builder );
823
824
			if ( ! $args['orderby'] ) {
825
				$args['orderby'] = array( 'relevance' );
826
			}
827
		} else {
828
			if ( ! $args['orderby'] ) {
829
				$args['orderby'] = array( 'date' );
830
			}
831
		}
832
833
		// Validate the "order" field
834
		switch ( strtolower( $args['order'] ) ) {
835
			case 'asc':
836
				$args['order'] = 'asc';
837
				break;
838
839
			case 'desc':
840
			default:
841
				$args['order'] = 'desc';
842
				break;
843
		}
844
845
		$es_query_args['sort'] = array();
846
847
		foreach ( (array) $args['orderby'] as $orderby ) {
848
			// Translate orderby from WP field to ES field
849
			switch ( $orderby ) {
850
				case 'relevance' :
851
					//never order by score ascending
852
					$es_query_args['sort'][] = array(
853
						'_score' => array(
854
							'order' => 'desc',
855
						),
856
					);
857
858
					break;
859
860 View Code Duplication
				case 'date' :
861
					$es_query_args['sort'][] = array(
862
						'date' => array(
863
							'order' => $args['order'],
864
						),
865
					);
866
867
					break;
868
869 View Code Duplication
				case 'ID' :
870
					$es_query_args['sort'][] = array(
871
						'id' => array(
872
							'order' => $args['order'],
873
						),
874
					);
875
876
					break;
877
878
				case 'author' :
879
					$es_query_args['sort'][] = array(
880
						'author.raw' => array(
881
							'order' => $args['order'],
882
						),
883
					);
884
885
					break;
886
			} // End switch().
887
		} // End foreach().
888
889
		if ( empty( $es_query_args['sort'] ) ) {
890
			unset( $es_query_args['sort'] );
891
		}
892
893
		if ( ! empty( $filters ) && is_array( $filters ) ) {
894
			foreach ( $filters as $filter ) {
895
				$builder->add_filter( $filter );
896
			}
897
898
			$es_query_args['filter'] = $builder->build_filter();
899
		}
900
901
		$es_query_args['query'] = $builder->build_query();
902
903
		// Aggregations
904
		if ( ! empty( $args['aggregations'] ) ) {
905
			$this->add_aggregations_to_es_query_builder( $args['aggregations'], $builder );
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...
906
907
			$es_query_args['aggregations'] = $builder->build_aggregation();
908
		}
909
910
		return $es_query_args;
911
	}
912
913
	/**
914
	 * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in ES
915
	 *
916
	 * @module search
917
	 *
918
	 * @param array $aggregations Array of Aggregations (filters) to add to the Jetpack_WPES_Query_Builder
919
	 *
920
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
921
	 */
922
	public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
923
		foreach ( $aggregations as $label => $aggregation ) {
924
			switch ( $aggregation['type'] ) {
925
				case 'taxonomy':
926
					$this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
927
928
					break;
929
930
				case 'post_type':
931
					$this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
932
933
					break;
934
935
				case 'date_histogram':
936
					$this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
937
938
					break;
939
			}
940
		}
941
	}
942
943
	/**
944
	 * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
945
	 *
946
	 * @module search
947
	 *
948
	 * @param array $aggregation The aggregation to add to the query builder
949
	 * @param string $label The 'label' (unique id) for this aggregation
950
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
951
	 */
952
	public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
953
		$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...
954
955
		switch ( $aggregation['taxonomy'] ) {
956
			case 'post_tag':
957
				$field = 'tag';
958
				break;
959
960
			case 'category':
961
				$field = 'category';
962
				break;
963
964
			default:
965
				$field = 'taxonomy.' . $aggregation['taxonomy'];
966
				break;
967
		}
968
969
		$builder->add_aggs( $label, array(
970
			'terms' => array(
971
				'field' => $field . '.slug',
972
				'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
973
			),
974
		));
975
	}
976
977
	/**
978
	 * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
979
	 *
980
	 * @module search
981
	 *
982
	 * @param array $aggregation The aggregation to add to the query builder
983
	 * @param string $label The 'label' (unique id) for this aggregation
984
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
985
	 */
986
	public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
987
		$builder->add_aggs( $label, array(
988
			'terms' => array(
989
				'field' => 'post_type',
990
				'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
991
			),
992
		));
993
	}
994
995
	/**
996
	 * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
997
	 *
998
	 * @module search
999
	 *
1000
	 * @param array $aggregation The aggregation to add to the query builder
1001
	 * @param string $label The 'label' (unique id) for this aggregation
1002
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
1003
	 */
1004
	public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1005
		$args = array(
1006
			'interval' => $aggregation['interval'],
1007
			'field'    => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' == $aggregation['field'] ) ? 'date_gmt' : 'date',
1008
		);
1009
1010
		if ( isset( $aggregation['min_doc_count'] ) ) {
1011
			$args['min_doc_count'] = intval( $aggregation['min_doc_count'] );
1012
		} else {
1013
			$args['min_doc_count'] = 1;
1014
		}
1015
1016
		$builder->add_aggs( $label, array(
1017
			'date_histogram' => $args,
1018
		));
1019
	}
1020
1021
	/**
1022
	 * And an existing filter object with a list of additional filters.
1023
	 *
1024
	 * Attempts to optimize the filters somewhat.
1025
	 *
1026
	 * @module search
1027
	 *
1028
	 * @param array $curr_filter The existing filters to build upon
1029
	 * @param array $filters The new filters to add
1030
	 *
1031
	 * @return array The resulting merged filters
1032
	 */
1033
	public static function and_es_filters( array $curr_filter, array $filters ) {
1034
		if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
1035
			if ( 1 === count( $filters ) ) {
1036
				return $filters[0];
1037
			}
1038
1039
			return array(
1040
				'and' => $filters,
1041
			);
1042
		}
1043
1044
		return array(
1045
			'and' => array_merge( array( $curr_filter ), $filters ),
1046
		);
1047
	}
1048
1049
	/**
1050
	 * Add a recency score to a given Jetpack_WPES_Query_Builder object, for emphasizing newer posts in results
1051
	 *
1052
	 * Internally uses a gauss decay function
1053
	 *
1054
	 * @module search
1055
	 *
1056
	 * @param Jetpack_WPES_Query_Builder $builder The Jetpack_WPES_Query_Builder to add the recency score to
1057
	 *
1058
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay
1059
	 */
1060
	public static function score_query_by_recency( Jetpack_WPES_Query_Builder &$builder ) {
1061
		//Newer content gets weighted slightly higher
1062
		$date_scale  = '360d';
1063
		$date_decay  = 0.9;
1064
		$date_origin = date( 'Y-m-d' );
1065
1066
		$builder->add_decay( 'gauss', array(
1067
			'date_gmt' => array(
1068
				'origin' => $date_origin,
1069
				'scale'  => $date_scale,
1070
				'decay'  => $date_decay,
1071
			),
1072
		));
1073
	}
1074
1075
	/**
1076
	 * Set the available filters for the search
1077
	 *
1078
	 * These get rendered via the Jetpack_Search_Widget_Filters() widget
1079
	 *
1080
	 * Behind the scenes, these are implemented using Elasticsearch Aggregations.
1081
	 *
1082
	 * If you do not require counts of how many documents match each filter, please consider using regular WP Query
1083
	 * arguments instead, such as via the jetpack_search_es_wp_query_args filter
1084
	 *
1085
	 * @module search
1086
	 *
1087
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1088
	 *
1089
	 * @param array $aggregations Array of filters (aggregations) to apply to the search
1090
	 */
1091
	public function set_filters( array $aggregations ) {
1092
		foreach ( (array) $aggregations as $key => $agg ) {
1093
			if ( empty( $agg['name'] ) ) {
1094
				$aggregations[ $key ]['name'] = $key;
1095
			}
1096
		}
1097
		$this->aggregations = $aggregations;
1098
	}
1099
1100
	/**
1101
	 * Set the search's facets (deprecated)
1102
	 *
1103
	 * @module search
1104
	 *
1105
	 * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead
1106
	 *
1107
	 * @see Jetpack_Search::set_filters()
1108
	 *
1109
	 * @param array $facets Array of facets to apply to the search
1110
	 */
1111
	public function set_facets( array $facets ) {
1112
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
1113
1114
		$this->set_filters( $facets );
1115
	}
1116
1117
	/**
1118
	 * Get the raw Aggregation results from the ES response
1119
	 *
1120
	 * @module search
1121
	 *
1122
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1123
	 *
1124
	 * @return array Array of Aggregations performed on the search
1125
	 */
1126
	public function get_search_aggregations_results() {
1127
		$aggregations = array();
1128
1129
		$search_result = $this->get_search_result();
1130
1131
		if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
1132
			$aggregations = $search_result['aggregations'];
1133
		}
1134
1135
		return $aggregations;
1136
	}
1137
1138
	/**
1139
	 * Get the raw Facet results from the ES response
1140
	 *
1141
	 * @module search
1142
	 *
1143
	 * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead
1144
	 *
1145
	 * @see Jetpack_Search::get_search_aggregations_results()
1146
	 *
1147
	 * @return array Array of Facets performed on the search
1148
	 */
1149
	public function get_search_facets() {
1150
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
1151
1152
		return $this->get_search_aggregations_results();
1153
	}
1154
1155
	/**
1156
	 * Get the results of the Filters performed, including the number of matching documents
1157
	 *
1158
	 * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
1159
	 * matching buckets, the url for applying/removing each bucket, etc.
1160
	 *
1161
	 * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
1162
	 * member if you need to access the raw filters set in Jetpack_Search::set_filters()
1163
	 *
1164
	 * @module search
1165
	 *
1166
	 * @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...
1167
	 *
1168
	 * @return array Array of Filters applied and info about them
1169
	 */
1170
	public function get_filters( WP_Query $query = null ) {
1171
		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...
1172
			global $wp_query;
1173
1174
			$query = $wp_query;
1175
		}
1176
1177
		$aggregation_data = $this->aggregations;
1178
1179
		if ( empty( $aggregation_data ) ) {
1180
			return $aggregation_data;
1181
		}
1182
1183
		$aggregation_results = $this->get_search_aggregations_results();
1184
1185
		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...
1186
			return $aggregation_data;
1187
		}
1188
1189
		// NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES
1190
		foreach ( $aggregation_results as $label => $aggregation ) {
1191
			if ( empty( $aggregation ) ) {
1192
				continue;
1193
			}
1194
1195
			$type = $this->aggregations[ $label ]['type'];
1196
1197
			$aggregation_data[ $label ]['buckets'] = array();
1198
1199
			$existing_term_slugs = array();
1200
1201
			$tax_query_var = null;
1202
1203
			// Figure out which terms are active in the query, for this taxonomy
1204
			if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
1205
				$tax_query_var = $this->get_taxonomy_query_var(  $this->aggregations[ $label ]['taxonomy'] );
1206
1207
				if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
1208
					foreach( $query->tax_query->queries as $tax_query ) {
1209
						if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
1210
						     'slug' === $tax_query['field'] &&
1211
						     is_array( $tax_query['terms'] ) ) {
1212
							$existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
1213
						}
1214
					}
1215
				}
1216
			}
1217
1218
			// Now take the resulting found aggregation items and generate the additional info about them, such as
1219
			// activation/deactivation url, name, count, etc
1220
			$buckets = array();
1221
1222
			if ( ! empty( $aggregation['buckets'] ) ) {
1223
				$buckets = (array) $aggregation['buckets'];
1224
			}
1225
1226
			if ( 'date_histogram' == $type ) {
1227
				//re-order newest to oldest
1228
				$buckets = array_reverse( $buckets );
1229
			}
1230
1231
			// Some aggregation types like date_histogram don't support the max results parameter
1232
			if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
1233
				$buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
1234
			}
1235
1236
			foreach ( $buckets as $item ) {
1237
				$query_vars = array();
1238
				$active     = false;
1239
				$remove_url = null;
1240
				$name       = '';
1241
1242
				// What type was the original aggregation?
1243
				switch ( $type ) {
1244
					case 'taxonomy':
1245
						$taxonomy = $this->aggregations[ $label ]['taxonomy'];
1246
1247
						$term = get_term_by( 'slug', $item['key'], $taxonomy );
1248
1249
						if ( ! $term || ! $tax_query_var ) {
1250
							continue 2; // switch() is considered a looping structure
1251
						}
1252
1253
						$query_vars = array(
1254
							$tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
1255
						);
1256
1257
						$name = $term->name;
1258
1259
						// Let's determine if this term is active or not
1260
1261
						if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
1262
							$active = true;
1263
1264
							$slug_count = count( $existing_term_slugs );
1265
1266
							if ( $slug_count > 1 ) {
1267
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1268
									$tax_query_var,
1269
									urlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) )
0 ignored issues
show
Documentation introduced by
urlencode(implode('+', a... array($item['key'])))) is of type string, but the function expects a boolean.

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...
1270
								);
1271
							} else {
1272
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( $tax_query_var );
1273
							}
1274
						}
1275
1276
						break;
1277
1278
					case 'post_type':
1279
						$post_type = get_post_type_object( $item['key'] );
1280
1281
						if ( ! $post_type || $post_type->exclude_from_search ) {
1282
							continue 2;  // switch() is considered a looping structure
1283
						}
1284
1285
						$query_vars = array(
1286
							'post_type' => $item['key'],
1287
						);
1288
1289
						$name = $post_type->labels->singular_name;
1290
1291
						// Is this post type active on this search?
1292
						$post_types = $query->get( 'post_type' );
1293
1294
						if ( ! is_array( $post_types ) ) {
1295
							$post_types = array( $post_types );
1296
						}
1297
1298
						if ( in_array( $item['key'], $post_types ) ) {
1299
							$active = true;
1300
1301
							$post_type_count = count( $post_types );
1302
1303
							// 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
1304
							if ( $post_type_count > 1 ) {
1305
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1306
									'post_type',
1307
									implode( ',',  array_diff( $post_types, array( $item['key'] ) ) )
0 ignored issues
show
Documentation introduced by
implode(',', array_diff(..., array($item['key']))) is of type string, but the function expects a boolean.

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...
1308
								);
1309
							} else {
1310
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
1311
							}
1312
						}
1313
1314
						break;
1315
1316
					case 'date_histogram':
1317
						$timestamp = $item['key'] / 1000;
1318
1319
						$current_year  = $query->get( 'year' );
1320
						$current_month = $query->get( 'monthnum' );
1321
						$current_day   = $query->get( 'day' );
1322
1323
						switch ( $this->aggregations[ $label ]['interval'] ) {
1324
							case 'year':
1325
								$year = (int) date( 'Y', $timestamp );
1326
1327
								$query_vars = array(
1328
									'year'     => $year,
1329
									'monthnum' => false,
1330
									'day'      => false,
1331
								);
1332
1333
								$name = $year;
1334
1335
								// Is this year currently selected?
1336
								if ( ! empty( $current_year ) && (int) $current_year === $year ) {
1337
									$active = true;
1338
1339
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
1340
								}
1341
1342
								break;
1343
1344
							case 'month':
1345
								$year  = (int) date( 'Y', $timestamp );
1346
								$month = (int) date( 'n', $timestamp );
1347
1348
								$query_vars = array(
1349
									'year'     => $year,
1350
									'monthnum' => $month,
1351
									'day'      => false,
1352
								);
1353
1354
								$name = date( 'F Y', $timestamp );
1355
1356
								// Is this month currently selected?
1357
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1358
								     ! empty( $current_month ) && (int) $current_month === $month ) {
1359
									$active = true;
1360
1361
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
1362
								}
1363
1364
								break;
1365
1366
							case 'day':
1367
								$year  = (int) date( 'Y', $timestamp );
1368
								$month = (int) date( 'n', $timestamp );
1369
								$day   = (int) date( 'j', $timestamp );
1370
1371
								$query_vars = array(
1372
									'year'     => $year,
1373
									'monthnum' => $month,
1374
									'day'      => $day,
1375
								);
1376
1377
								$name = date( 'F jS, Y', $timestamp );
1378
1379
								// Is this day currently selected?
1380
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1381
								     ! empty( $current_month ) && (int) $current_month === $month &&
1382
								     ! empty( $current_day ) && (int) $current_day === $day ) {
1383
									$active = true;
1384
1385
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
1386
								}
1387
1388
								break;
1389
1390
							default:
1391
								continue 3; // switch() is considered a looping structure
1392
						} // End switch().
1393
1394
						break;
1395
1396
					default:
1397
						//continue 2; // switch() is considered a looping structure
1398
				} // End switch().
1399
1400
				// Need to urlencode param values since add_query_arg doesn't
1401
				$url_params = urlencode_deep( $query_vars );
1402
1403
				$aggregation_data[ $label ]['buckets'][] = array(
1404
					'url'        => Jetpack_Search_Helpers::add_query_arg( $url_params ),
1405
					'query_vars' => $query_vars,
1406
					'name'       => $name,
1407
					'count'      => $item['doc_count'],
1408
					'active'     => $active,
1409
					'remove_url' => $remove_url,
1410
					'type'       => $type,
1411
					'type_label' => $aggregation_data[ $label ]['name'],
1412
					'widget_id'  => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0
1413
				);
1414
			} // End foreach().
1415
		} // End foreach().
1416
1417
		return $aggregation_data;
1418
	}
1419
1420
	/**
1421
	 * Get the results of the Facets performed
1422
	 *
1423
	 * @module search
1424
	 *
1425
	 * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead
1426
	 *
1427
	 * @see Jetpack_Search::get_filters()
1428
	 *
1429
	 * @return array $facets Array of Facets applied and info about them
1430
	 */
1431
	public function get_search_facet_data() {
1432
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
1433
1434
		return $this->get_filters();
1435
	}
1436
1437
	/**
1438
	 * Get the Filters that are currently applied to this search
1439
	 *
1440
	 * @module search
1441
	 *
1442
	 * @return array Array if Filters that were applied
1443
	 */
1444
	public function get_active_filter_buckets() {
1445
		$active_buckets = array();
1446
1447
		$filters = $this->get_filters();
1448
1449
		if ( ! is_array( $filters ) ) {
1450
			return $active_buckets;
1451
		}
1452
1453
		foreach( $filters as $filter ) {
1454
			if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
1455
				foreach( $filter['buckets'] as $item ) {
1456
					if ( isset( $item['active'] ) && $item['active'] ) {
1457
						$active_buckets[] = $item;
1458
					}
1459
				}
1460
			}
1461
		}
1462
1463
		return $active_buckets;
1464
	}
1465
1466
	/**
1467
	 * Get the Filters that are currently applied to this search
1468
	 *
1469
	 * @module search
1470
	 *
1471
	 * @return array Array if Filters that were applied
1472
	 */
1473
	public function get_current_filters() {
1474
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
1475
1476
		return $this->get_active_filter_buckets();
1477
	}
1478
1479
	/**
1480
	 * Calculate the right query var to use for a given taxonomy
1481
	 *
1482
	 * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter
1483
	 *
1484
	 * @module search
1485
	 *
1486
	 * @param string $taxonomy_name The name of the taxonomy for which to get the query var
1487
	 *
1488
	 * @return bool|string The query var to use for this taxonomy, or false if none found
1489
	 */
1490
	public function get_taxonomy_query_var( $taxonomy_name ) {
1491
		$taxonomy = get_taxonomy( $taxonomy_name );
1492
1493
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1494
			return false;
1495
		}
1496
1497
		/**
1498
		 * Modify the query var to use for a given taxonomy
1499
		 *
1500
		 * @module search
1501
		 *
1502
		 * @since 5.0.0
1503
		 *
1504
		 * @param string $query_var The current query_var for the taxonomy
1505
		 * @param string $taxonomy_name The taxonomy name
1506
		 */
1507
		return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
1508
	}
1509
1510
	/**
1511
	 * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
1512
	 * which is the input order
1513
	 *
1514
	 * Necessary because ES does not always return Aggs in the same order that you pass them in, and it should be possible
1515
	 * to control the display order easily
1516
	 *
1517
	 * @module search
1518
	 *
1519
	 * @param array $aggregations Agg results to be reordered
1520
	 * @param array $desired Array with keys representing the desired ordering
1521
	 *
1522
	 * @return array A new array with reordered keys, matching those in $desired
1523
	 */
1524
	public function fix_aggregation_ordering( array $aggregations, array $desired ) {
1525
		if ( empty( $aggregations ) || empty( $desired ) ) {
1526
			return $aggregations;
1527
		}
1528
1529
		$reordered = array();
1530
1531
		foreach( array_keys( $desired ) as $agg_name ) {
1532
			if ( isset( $aggregations[ $agg_name ] ) ) {
1533
				$reordered[ $agg_name ] = $aggregations[ $agg_name ];
1534
			}
1535
		}
1536
1537
		return $reordered;
1538
	}
1539
1540
	public function track_widget_updates( $option, $old_value, $new_value ) {
1541
		if ( 'widget_jetpack-search-filters' !== $option ) {
1542
			return;
1543
		}
1544
1545
		$event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value );
1546
		if ( ! $event ) {
1547
			return;
1548
		}
1549
1550
		jetpack_tracks_record_event(
1551
			wp_get_current_user(),
1552
			sprintf( 'jetpack_search_widget_%s', $event['action'] ),
1553
			$event['widget']
1554
		);
1555
	}
1556
}
1557