Completed
Push — branch-5.6 ( bdd7a6...aec938 )
by Jeremy
08:43 queued 46s
created

Jetpack_Search::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
		$this->init_hooks();
82
	}
83
84
	/**
85
	 * Setup the various hooks needed for the plugin to take over Search duties
86
	 *
87
	 * @module search
88
	 */
89
	public function init_hooks() {
90
		add_action( 'widgets_init', array( $this, 'action__widgets_init' ) );
91
92
		if ( ! is_admin() ) {
93
			add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 );
94
95
			add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ),  10, 2 );
96
97
			add_action( 'did_jetpack_search_query', array( $this, 'store_query_success' ) );
98
			add_action( 'failed_jetpack_search_query', array( $this, 'store_query_failure' ) );
99
		}
100
	}
101
102
	/**
103
	 * Print query info as a HTML comment in the footer
104
	 */
105
106
	public function store_query_failure( $meta ) {
107
		$this->last_query_failure_info = $meta;
108
		add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
109
	}
110
111
	public function print_query_failure() {
112
		if ( $this->last_query_failure_info ) {
113
			echo '<!-- Jetpack Search failed with code ' . $this->last_query_failure_info['response_code'] . ': ' . $this->last_query_failure_info['json']['error'] . ' - ' . $this->last_query_failure_info['json']['message'] . ' -->';
114
		}
115
	}
116
117
	public function store_query_success( $meta ) {
118
		$this->last_query_info = $meta;
119
		add_action( 'wp_footer', array( $this, 'print_query_success' ) );
120
	}
121
122
	public function print_query_success() {
123
		if ( $this->last_query_info ) {
124
			echo '<!-- Jetpack Search took ' . intval( $this->last_query_info['elapsed_time'] ) . ' ms, ES time ' . $this->last_query_info['es_time'] . ' ms -->';
125
		}
126
	}
127
128
	/*
129
	 * Run a search on the WP.com public API.
130
	 *
131
	 * @module search
132
	 *
133
	 * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint
134
	 *
135
	 * @return object|WP_Error The response from the public api, or a WP_Error
136
	 */
137
	public function search( array $es_args ) {
138
		$endpoint    = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
139
		$service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
140
141
		$do_authenticated_request = false;
142
143
		if ( class_exists( 'Jetpack_Client' ) &&
144
			isset( $es_args['authenticated_request'] ) &&
145
			true === $es_args['authenticated_request'] ) {
146
			$do_authenticated_request = true;
147
		}
148
149
		unset( $es_args['authenticated_request'] );
150
151
		$request_args = array(
152
			'headers' => array(
153
				'Content-Type' => 'application/json',
154
			),
155
			'timeout'    => 10,
156
			'user-agent' => 'jetpack_search',
157
		);
158
159
		$request_body = json_encode( $es_args );
160
161
		$start_time = microtime( true );
162
163
		if ( $do_authenticated_request ) {
164
			$request_args['method'] = 'POST';
165
166
			$request = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint, Jetpack_Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
167
		} else {
168
			$request_args = array_merge( $request_args, array(
169
				'body' => $request_body,
170
			) );
171
172
			$request = wp_remote_post( $service_url, $request_args );
173
		}
174
175
		$end_time = microtime( true );
176
177
		if ( is_wp_error( $request ) ) {
178
			return $request;
179
		}
180
181
		$response_code = wp_remote_retrieve_response_code( $request );
182
		$response = json_decode( wp_remote_retrieve_body( $request ), true );
183
184
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
185
			/**
186
			 * Fires after a search query request has failed
187
			 *
188
			 * @module search
189
			 *
190
			 * @since 5.6.0
191
			 *
192
			 * @param array Array containing the response code and response from the failed search query
193
			 */
194
			do_action( 'failed_jetpack_search_query', array( 'response_code' => $response_code, 'json' => $response ) );
195
			return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
196
		}
197
198
		$took = is_array( $response ) && $response['took'] ? $response['took'] : null;
199
200
		$query = array(
201
			'args'          => $es_args,
202
			'response'      => $response,
203
			'response_code' => $response_code,
204
			'elapsed_time'   => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms
205
			'es_time'       => $took,
206
			'url'           => $service_url,
207
		);
208
209
		/**
210
		 * Fires after a search request has been performed
211
		 *
212
		 * Includes the following info in the $query parameter:
213
		 *
214
		 * array args Array of Elasticsearch arguments for the search
215
		 * array response Raw API response, JSON decoded
216
		 * int response_code HTTP response code of the request
217
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
218
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
219
		 * string url API url that was queried
220
		 *
221
		 * @module search
222
		 *
223
		 * @since 5.0.0
224
		 *
225
		 * @param array $query Array of information about the query performed
226
		 */
227
		do_action( 'did_jetpack_search_query', $query );
228
229
		return $response;
230
	}
231
232
	/**
233
	 * Bypass the normal Search query and offload it to Jetpack servers
234
	 *
235
	 * This is the main hook of the plugin and is responsible for returning the posts that match the search query
236
	 *
237
	 * @module search
238
	 *
239
	 * @param array $posts Current array of posts (still pre-query)
240
	 * @param WP_Query $query The WP_Query being filtered
241
	 *
242
	 * @return array Array of matching posts
243
	 */
244
	public function filter__posts_pre_query( $posts, $query ) {
245
		/**
246
		 * Determine whether a given WP_Query should be handled by ElasticSearch
247
		 *
248
		 * @module search
249
		 *
250
		 * @since 5.6.0
251
		 * @param bool $should_handle Should be handled by Jetpack Search
252
		 * @param WP_Query $query The wp_query object
253
		 */
254
		if ( ! apply_filters( 'jetpack_search_should_handle_query', ( $query->is_main_query() && $query->is_search() ), $query ) ) {
255
			return $posts;
256
		}
257
258
		$this->do_search( $query );
259
260
		if ( ! is_array( $this->search_result ) ) {
261
			return $posts;
262
		}
263
264
		// If no results, nothing to do
265
		if ( ! count( $this->search_result['results']['hits'] ) ) {
266
			return array();
267
		}
268
269
		$post_ids = array();
270
271
		foreach ( $this->search_result['results']['hits'] as $result ) {
272
			$post_ids[] = (int) $result['fields']['post_id'];
273
		}
274
275
		// Query all posts now
276
		$args = array(
277
			'post__in'  => $post_ids,
278
			'perm'      => 'readable',
279
			'post_type' => 'any',
280
		);
281
282
		$posts_query = new WP_Query( $args );
283
284
		// WP Core doesn't call the set_found_posts and its filters when filtering posts_pre_query like we do, so need to
285
		// do these manually
286
		$query->found_posts   = $this->found_posts;
287
		$query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
288
289
		return $posts_query->posts;
290
	}
291
292
	/**
293
	 * Build up the search, then run it against the Jetpack servers
294
	 *
295
	 * @param WP_Query $query The original WP_Query to use for the parameters of our search
296
	 */
297
	public function do_search( WP_Query $query ) {
298
		$page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
299
300
		$posts_per_page = $query->get( 'posts_per_page' );
301
302
		// ES API does not allow more than 15 results at a time
303
		if ( $posts_per_page > 15 ) {
304
			$posts_per_page = 15;
305
		}
306
307
		// Start building the WP-style search query args
308
		// They'll be translated to ES format args later
309
		$es_wp_query_args = array(
310
			'query'          => $query->get( 's' ),
311
			'posts_per_page' => $posts_per_page,
312
			'paged'          => $page,
313
			'orderby'        => $query->get( 'orderby' ),
314
			'order'          => $query->get( 'order' ),
315
		);
316
317
		if ( ! empty( $this->aggregations ) ) {
318
			$es_wp_query_args['aggregations'] = $this->aggregations;
319
		}
320
321
		// Did we query for authors?
322
		if ( $query->get( 'author_name' ) ) {
323
			$es_wp_query_args['author_name'] = $query->get( 'author_name' );
324
		}
325
326
		$es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
327
328
		$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...
329
330
331
		/**
332
		 * Modify the search query parameters, such as controlling the post_type.
333
		 *
334
		 * These arguments are in the format of WP_Query arguments
335
		 *
336
		 * @module search
337
		 *
338
		 * @since 5.0.0
339
		 *
340
		 * @param array $es_wp_query_args The current query args, in WP_Query format
341
		 * @param WP_Query $query The original query object
342
		 */
343
		$es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
344
345
		// If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
346
		// capped at $this->max_offset, so a high page would always return the last page of results otherwise
347
		if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $this->max_offset ) {
348
			$query->set_404();
349
350
			return;
351
		}
352
353
		// If there were no post types returned, then 404 to avoid querying against non-public post types, which could
354
		// happen if we don't add the post type restriction to the ES query
355
		if ( empty( $es_wp_query_args['post_type'] ) ) {
356
			$query->set_404();
357
358
			return;
359
		}
360
361
		// Convert the WP-style args into ES args
362
		$es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
363
364
		//Only trust ES to give us IDs, not the content since it is a mirror
365
		$es_query_args['fields'] = array(
366
			'post_id',
367
		);
368
369
		/**
370
		 * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
371
		 *
372
		 * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
373
		 *
374
		 * @module search
375
		 *
376
		 * @since 5.0.0
377
		 *
378
		 * @param array $es_query_args The raw ES query args
379
		 * @param WP_Query $query The original query object
380
		 */
381
		$es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
382
383
		// Do the actual search query!
384
		$this->search_result = $this->search( $es_query_args );
385
386
		if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
387
			$this->found_posts = 0;
388
389
			return;
390
		}
391
392
		// If we have aggregations, fix the ordering to match the input order (ES doesn't
393
		// guarantee the return order)
394
		if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
395
			$this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
396
		}
397
398
		// Total number of results for paging purposes. Capped at $this->>max_offset + $posts_per_page, as deep paging
399
		// gets quite expensive
400
		$this->found_posts = min( $this->search_result['results']['total'], $this->max_offset + $posts_per_page );
401
402
		return;
403
	}
404
405
	/**
406
	 * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style ES term arguments for the search
407
	 *
408
	 * @module search
409
	 *
410
	 * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query
411
	 *
412
	 * @return array The new WP-style ES arguments (that will be converted into 'real' ES arguments)
413
	 */
414
	public function get_es_wp_query_terms_for_query( WP_Query $query ) {
415
		$args = array();
416
417
		$the_tax_query = $query->tax_query;
418
419
		if ( ! $the_tax_query ) {
420
			return $args;
421
		}
422
423
424
		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...
425
			return $args;
426
		}
427
428
		$args = array();
429
430
		foreach ( $the_tax_query->queries as $tax_query ) {
431
			// Right now we only support slugs...see note above
432
			if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
433
				continue;
434
			}
435
436
			$taxonomy = $tax_query['taxonomy'];
437
438 View Code Duplication
			if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
439
				$args[ $taxonomy ] = array();
440
			}
441
442
			$args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
443
		}
444
445
		return $args;
446
	}
447
448
	/**
449
	 * Parse out the post type from a WP_Query
450
	 *
451
	 * Only allows post types that are not marked as 'exclude_from_search'
452
	 *
453
	 * @module search
454
	 *
455
	 * @param WP_Query $query Original WP_Query object
456
	 *
457
	 * @return array Array of searchable post types corresponding to the original query
458
	 */
459
	public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
460
		$post_types = $query->get( 'post_type' );
461
462
		// If we're searching 'any', we want to only pass searchable post types to ES
463
		if ( 'any' === $post_types ) {
464
			$post_types = array_values( get_post_types( array(
465
				'exclude_from_search' => false,
466
			) ) );
467
		}
468
469
		if ( ! is_array( $post_types ) ) {
470
			$post_types = array( $post_types );
471
		}
472
473
		$post_types = array_unique( $post_types );
474
475
		$sanitized_post_types = array();
476
477
		// Make sure the post types are queryable
478
		foreach ( $post_types as $post_type ) {
479
			if ( ! $post_type ) {
480
				continue;
481
			}
482
483
			$post_type_object = get_post_type_object( $post_type );
484
			if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
485
				continue;
486
			}
487
488
			$sanitized_post_types[] = $post_type;
489
		}
490
491
		return $sanitized_post_types;
492
	}
493
494
	/**
495
	 * Initialze widgets for the Search module
496
	 *
497
	 * @module search
498
	 */
499
	public function action__widgets_init() {
500
		require_once( dirname( __FILE__ ) . '/class.jetpack-search-widget-filters.php' );
501
502
		register_widget( 'Jetpack_Search_Widget_Filters' );
503
	}
504
505
	/**
506
	 * Get the Elasticsearch result
507
	 *
508
	 * @module search
509
	 *
510
	 * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response
511
	 *
512
	 * @return array|bool The search results, or false if there was a failure
513
	 */
514
	public function get_search_result( $raw = false ) {
515
		if ( $raw ) {
516
			return $this->search_result;
517
		}
518
519
		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;
520
	}
521
522
	/**
523
	 * Add the date portion of a WP_Query onto the query args
524
	 *
525
	 * @param array    $es_wp_query_args
526
	 * @param WP_Query $query The original WP_Query
527
	 *
528
	 * @return array The es wp query args, with date filters added (as needed)
529
	 */
530
	public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
531
		if ( $query->get( 'year' ) ) {
532
			if ( $query->get( 'monthnum' ) ) {
533
				// Padding
534
				$date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
535
536
				if ( $query->get( 'day' ) ) {
537
					// Padding
538
					$date_day = sprintf( '%02d', $query->get( 'day' ) );
539
540
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
541
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
542
				} else {
543
					$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
544
545
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
546
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
547
				}
548
			} else {
549
				$date_start = $query->get( 'year' ) . '-01-01 00:00:00';
550
				$date_end   = $query->get( 'year' ) . '-12-31 23:59:59';
551
			}
552
553
			$es_wp_query_args['date_range'] = array(
554
				'field' => 'date',
555
				'gte'   => $date_start,
556
				'lte'   => $date_end,
557
			);
558
		}
559
560
		return $es_wp_query_args;
561
	}
562
563
	/**
564
	 * Converts WP_Query style args to ES args
565
	 *
566
	 * @module search
567
	 *
568
	 * @param array $args Array of WP_Query style arguments
569
	 *
570
	 * @return array Array of ES style query arguments
571
	 */
572
	function convert_wp_es_to_es_args( array $args ) {
573
		jetpack_require_lib( 'jetpack-wpes-query-builder' );
574
575
		$builder = new Jetpack_WPES_Query_Builder();
576
577
		$defaults = array(
578
			'blog_id'        => get_current_blog_id(),
579
580
			'query'          => null,    // Search phrase
581
			'query_fields'   => array( 'title', 'content', 'author', 'tag', 'category' ),
582
583
			'post_type'      => null,  // string or an array
584
			'terms'          => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) )
585
586
			'author'         => null,    // id or an array of ids
587
			'author_name'    => array(), // string or an array
588
589
			'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'
590
591
			'orderby'        => null,    // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
592
			'order'          => 'DESC',
593
594
			'posts_per_page' => 10,
595
596
			'offset'         => null,
597
			'paged'          => null,
598
599
			/**
600
			 * Aggregations. Examples:
601
			 * array(
602
			 *     'Tag'       => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
603
			 *     'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
604
			 * );
605
			 */
606
			'aggregations'         => null,
607
		);
608
609
		$args = wp_parse_args( $args, $defaults );
610
611
		$es_query_args = array(
612
			'blog_id' => absint( $args['blog_id'] ),
613
			'size'    => absint( $args['posts_per_page'] ),
614
		);
615
616
		// ES "from" arg (offset)
617
		if ( $args['offset'] ) {
618
			$es_query_args['from'] = absint( $args['offset'] );
619
		} elseif ( $args['paged'] ) {
620
			$es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
621
		}
622
623
		// Limit the offset to $this->max_offset posts, as deep pages get exponentially slower
624
		// See https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html
625
		$es_query_args['from'] = min( $es_query_args['from'], $this->max_offset );
626
627
		if ( ! is_array( $args['author_name'] ) ) {
628
			$args['author_name'] = array( $args['author_name'] );
629
		}
630
631
		// ES stores usernames, not IDs, so transform
632
		if ( ! empty( $args['author'] ) ) {
633
			if ( ! is_array( $args['author'] ) ) {
634
				$args['author'] = array( $args['author'] );
635
			}
636
637
			foreach ( $args['author'] as $author ) {
638
				$user = get_user_by( 'id', $author );
639
640
				if ( $user && ! empty( $user->user_login ) ) {
641
					$args['author_name'][] = $user->user_login;
642
				}
643
			}
644
		}
645
646
		//////////////////////////////////////////////////
647
		// Build the filters from the query elements.
648
		// Filters rock because they are cached from one query to the next
649
		// but they are cached as individual filters, rather than all combined together.
650
		// May get performance boost by also caching the top level boolean filter too.
651
		$filters = array();
652
653
		if ( $args['post_type'] ) {
654
			if ( ! is_array( $args['post_type'] ) ) {
655
				$args['post_type'] = array( $args['post_type'] );
656
			}
657
658
			$filters[] = array(
659
				'terms' => array(
660
					'post_type' => $args['post_type'],
661
				),
662
			);
663
		}
664
665
		if ( $args['author_name'] ) {
666
			$filters[] = array(
667
				'terms' => array(
668
					'author_login' => $args['author_name'],
669
				),
670
			);
671
		}
672
673
		if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
674
			$field = $args['date_range']['field'];
675
676
			unset( $args['date_range']['field'] );
677
678
			$filters[] = array(
679
				'range' => array(
680
					$field => $args['date_range'],
681
				),
682
			);
683
		}
684
685
		if ( is_array( $args['terms'] ) ) {
686
			foreach ( $args['terms'] as $tax => $terms ) {
687
				$terms = (array) $terms;
688
689
				if ( count( $terms ) && mb_strlen( $tax ) ) {
690 View Code Duplication
					switch ( $tax ) {
691
						case 'post_tag':
692
							$tax_fld = 'tag.slug';
693
694
							break;
695
696
						case 'category':
697
							$tax_fld = 'category.slug';
698
699
							break;
700
701
						default:
702
							$tax_fld = 'taxonomy.' . $tax . '.slug';
703
704
							break;
705
					}
706
707
					foreach ( $terms as $term ) {
708
						$filters[] = array(
709
							'term' => array(
710
								$tax_fld => $term,
711
							),
712
						);
713
					}
714
				}
715
			}
716
		}
717
718
		if ( $args['query'] ) {
719
			$query = array(
720
				'multi_match' => array(
721
					'query'    => $args['query'],
722
					'fields'   => $args['query_fields'],
723
					'operator' => 'and',
724
					'type'     => 'cross_fields',
725
				),
726
			);
727
728
			$builder->add_query( $query );
729
730
			Jetpack_Search::score_query_by_recency( $builder );
731
732
			if ( ! $args['orderby'] ) {
733
				$args['orderby'] = array( 'relevance' );
734
			}
735
		} else {
736
			if ( ! $args['orderby'] ) {
737
				$args['orderby'] = array( 'date' );
738
			}
739
		}
740
741
		// Validate the "order" field
742
		switch ( strtolower( $args['order'] ) ) {
743
			case 'asc':
744
				$args['order'] = 'asc';
745
				break;
746
747
			case 'desc':
748
			default:
749
				$args['order'] = 'desc';
750
				break;
751
		}
752
753
		$es_query_args['sort'] = array();
754
755
		foreach ( (array) $args['orderby'] as $orderby ) {
756
			// Translate orderby from WP field to ES field
757
			switch ( $orderby ) {
758
				case 'relevance' :
759
					//never order by score ascending
760
					$es_query_args['sort'][] = array(
761
						'_score' => array(
762
							'order' => 'desc',
763
						),
764
					);
765
766
					break;
767
768 View Code Duplication
				case 'date' :
769
					$es_query_args['sort'][] = array(
770
						'date' => array(
771
							'order' => $args['order'],
772
						),
773
					);
774
775
					break;
776
777 View Code Duplication
				case 'ID' :
778
					$es_query_args['sort'][] = array(
779
						'id' => array(
780
							'order' => $args['order'],
781
						),
782
					);
783
784
					break;
785
786
				case 'author' :
787
					$es_query_args['sort'][] = array(
788
						'author.raw' => array(
789
							'order' => $args['order'],
790
						),
791
					);
792
793
					break;
794
			} // End switch().
795
		} // End foreach().
796
797
		if ( empty( $es_query_args['sort'] ) ) {
798
			unset( $es_query_args['sort'] );
799
		}
800
801
		if ( ! empty( $filters ) && is_array( $filters ) ) {
802
			foreach ( $filters as $filter ) {
803
				$builder->add_filter( $filter );
804
			}
805
806
			$es_query_args['filter'] = $builder->build_filter();
807
		}
808
809
		$es_query_args['query'] = $builder->build_query();
810
811
		// Aggregations
812
		if ( ! empty( $args['aggregations'] ) ) {
813
			$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...
814
815
			$es_query_args['aggregations'] = $builder->build_aggregation();
816
		}
817
818
		return $es_query_args;
819
	}
820
821
	/**
822
	 * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in ES
823
	 *
824
	 * @module search
825
	 *
826
	 * @param array $aggregations Array of Aggregations (filters) to add to the Jetpack_WPES_Query_Builder
827
	 *
828
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
829
	 */
830
	public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
831
		foreach ( $aggregations as $label => $aggregation ) {
832
			switch ( $aggregation['type'] ) {
833
				case 'taxonomy':
834
					$this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
835
836
					break;
837
838
				case 'post_type':
839
					$this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
840
841
					break;
842
843
				case 'date_histogram':
844
					$this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
845
846
					break;
847
			}
848
		}
849
	}
850
851
	/**
852
	 * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
853
	 *
854
	 * @module search
855
	 *
856
	 * @param array $aggregation The aggregation to add to the query builder
857
	 * @param string $label The 'label' (unique id) for this aggregation
858
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
859
	 */
860
	public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
861
		$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...
862
863
		switch ( $aggregation['taxonomy'] ) {
864
			case 'post_tag':
865
				$field = 'tag';
866
				break;
867
868
			case 'category':
869
				$field = 'category';
870
				break;
871
872
			default:
873
				$field = 'taxonomy.' . $aggregation['taxonomy'];
874
				break;
875
		}
876
877
		$builder->add_aggs( $label, array(
878
			'terms' => array(
879
				'field' => $field . '.slug',
880
				'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
881
			),
882
		));
883
	}
884
885
	/**
886
	 * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
887
	 *
888
	 * @module search
889
	 *
890
	 * @param array $aggregation The aggregation to add to the query builder
891
	 * @param string $label The 'label' (unique id) for this aggregation
892
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
893
	 */
894
	public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
895
		$builder->add_aggs( $label, array(
896
			'terms' => array(
897
				'field' => 'post_type',
898
				'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
899
			),
900
		));
901
	}
902
903
	/**
904
	 * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
905
	 *
906
	 * @module search
907
	 *
908
	 * @param array $aggregation The aggregation to add to the query builder
909
	 * @param string $label The 'label' (unique id) for this aggregation
910
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
911
	 */
912
	public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
913
		$args = array(
914
			'interval' => $aggregation['interval'],
915
			'field'    => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' == $aggregation['field'] ) ? 'date_gmt' : 'date',
916
		);
917
918
		if ( isset( $aggregation['min_doc_count'] ) ) {
919
			$args['min_doc_count'] = intval( $aggregation['min_doc_count'] );
920
		} else {
921
			$args['min_doc_count'] = 1;
922
		}
923
924
		$builder->add_aggs( $label, array(
925
			'date_histogram' => $args,
926
		));
927
	}
928
929
	/**
930
	 * And an existing filter object with a list of additional filters.
931
	 *
932
	 * Attempts to optimize the filters somewhat.
933
	 *
934
	 * @module search
935
	 *
936
	 * @param array $curr_filter The existing filters to build upon
937
	 * @param array $filters The new filters to add
938
	 *
939
	 * @return array The resulting merged filters
940
	 */
941
	public static function and_es_filters( array $curr_filter, array $filters ) {
942
		if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
943
			if ( 1 === count( $filters ) ) {
944
				return $filters[0];
945
			}
946
947
			return array(
948
				'and' => $filters,
949
			);
950
		}
951
952
		return array(
953
			'and' => array_merge( array( $curr_filter ), $filters ),
954
		);
955
	}
956
957
	/**
958
	 * Add a recency score to a given Jetpack_WPES_Query_Builder object, for emphasizing newer posts in results
959
	 *
960
	 * Internally uses a gauss decay function
961
	 *
962
	 * @module search
963
	 *
964
	 * @param Jetpack_WPES_Query_Builder $builder The Jetpack_WPES_Query_Builder to add the recency score to
965
	 *
966
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay
967
	 */
968
	public static function score_query_by_recency( Jetpack_WPES_Query_Builder &$builder ) {
969
		//Newer content gets weighted slightly higher
970
		$date_scale  = '360d';
971
		$date_decay  = 0.9;
972
		$date_origin = date( 'Y-m-d' );
973
974
		$builder->add_decay( 'gauss', array(
975
			'date_gmt' => array(
976
				'origin' => $date_origin,
977
				'scale'  => $date_scale,
978
				'decay'  => $date_decay,
979
			),
980
		));
981
	}
982
983
	/**
984
	 * Set the available filters for the search
985
	 *
986
	 * These get rendered via the Jetpack_Search_Widget_Filters() widget
987
	 *
988
	 * Behind the scenes, these are implemented using Elasticsearch Aggregations.
989
	 *
990
	 * If you do not require counts of how many documents match each filter, please consider using regular WP Query
991
	 * arguments instead, such as via the jetpack_search_es_wp_query_args filter
992
	 *
993
	 * @module search
994
	 *
995
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
996
	 *
997
	 * @param array $aggregations Array of filters (aggregations) to apply to the search
998
	 */
999
	public function set_filters( array $aggregations ) {
1000
		$this->aggregations = $aggregations;
1001
	}
1002
1003
	/**
1004
	 * Set the search's facets (deprecated)
1005
	 *
1006
	 * @module search
1007
	 *
1008
	 * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead
1009
	 *
1010
	 * @see Jetpack_Search::set_filters()
1011
	 *
1012
	 * @param array $facets Array of facets to apply to the search
1013
	 */
1014
	public function set_facets( array $facets ) {
1015
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
1016
1017
		$this->set_filters( $facets );
1018
	}
1019
1020
	/**
1021
	 * Get the raw Aggregation results from the ES response
1022
	 *
1023
	 * @module search
1024
	 *
1025
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1026
	 *
1027
	 * @return array Array of Aggregations performed on the search
1028
	 */
1029
	public function get_search_aggregations_results() {
1030
		$aggregations = array();
1031
1032
		$search_result = $this->get_search_result();
1033
1034
		if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
1035
			$aggregations = $search_result['aggregations'];
1036
		}
1037
1038
		return $aggregations;
1039
	}
1040
1041
	/**
1042
	 * Get the raw Facet results from the ES response
1043
	 *
1044
	 * @module search
1045
	 *
1046
	 * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead
1047
	 *
1048
	 * @see Jetpack_Search::get_search_aggregations_results()
1049
	 *
1050
	 * @return array Array of Facets performed on the search
1051
	 */
1052
	public function get_search_facets() {
1053
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
1054
1055
		return $this->get_search_aggregations_results();
1056
	}
1057
1058
	/**
1059
	 * Get the results of the Filters performed, including the number of matching documents
1060
	 *
1061
	 * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
1062
	 * matching buckets, the url for applying/removing each bucket, etc.
1063
	 *
1064
	 * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
1065
	 * member if you need to access the raw filters set in Jetpack_Search::set_filters()
1066
	 *
1067
	 * @module search
1068
	 *
1069
	 * @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...
1070
	 *
1071
	 * @return array Array of Filters applied and info about them
1072
	 */
1073
	public function get_filters( WP_Query $query = null ) {
1074
		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...
1075
			global $wp_query;
1076
1077
			$query = $wp_query;
1078
		}
1079
1080
		$aggregation_data = $this->aggregations;
1081
1082
		if ( empty( $aggregation_data ) ) {
1083
			return $aggregation_data;
1084
		}
1085
1086
		$aggregation_results = $this->get_search_aggregations_results();
1087
1088
		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...
1089
			return $aggregation_data;
1090
		}
1091
1092
		// NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES
1093
		foreach ( $aggregation_results as $label => $aggregation ) {
1094
			if ( empty( $aggregation ) ) {
1095
				continue;
1096
			}
1097
1098
			$type = $this->aggregations[ $label ]['type'];
1099
1100
			$aggregation_data[ $label ]['buckets'] = array();
1101
1102
			$existing_term_slugs = array();
1103
1104
			$tax_query_var = null;
1105
1106
			// Figure out which terms are active in the query, for this taxonomy
1107
			if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
1108
				$tax_query_var = $this->get_taxonomy_query_var(  $this->aggregations[ $label ]['taxonomy'] );
1109
1110
				if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
1111
					foreach( $query->tax_query->queries as $tax_query ) {
1112
						if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
1113
						     'slug' === $tax_query['field'] &&
1114
						     is_array( $tax_query['terms'] ) ) {
1115
							$existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
1116
						}
1117
					}
1118
				}
1119
			}
1120
1121
			// Now take the resulting found aggregation items and generate the additional info about them, such as
1122
			// activation/deactivation url, name, count, etc
1123
			$buckets = array();
1124
1125
			if ( ! empty( $aggregation['buckets'] ) ) {
1126
				$buckets = (array) $aggregation['buckets'];
1127
			}
1128
1129
			// Some aggregation types like date_histogram don't support the max results parameter
1130
			if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
1131
				$buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
1132
			}
1133
1134
			foreach ( $buckets as $item ) {
1135
				$query_vars = array();
1136
				$active     = false;
1137
				$remove_url = null;
1138
				$name       = '';
1139
1140
				// What type was the original aggregation?
1141
				switch ( $type ) {
1142
					case 'taxonomy':
1143
						$taxonomy = $this->aggregations[ $label ]['taxonomy'];
1144
1145
						$term = get_term_by( 'slug', $item['key'], $taxonomy );
1146
1147
						if ( ! $term || ! $tax_query_var ) {
1148
							continue 2; // switch() is considered a looping structure
1149
						}
1150
1151
						$query_vars = array(
1152
							$tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
1153
						);
1154
1155
						$name = $term->name;
1156
1157
						// Let's determine if this term is active or not
1158
1159
						if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
1160
							$active = true;
1161
1162
							$slug_count = count( $existing_term_slugs );
1163
1164 View Code Duplication
							if ( $slug_count > 1 ) {
1165
								$remove_url = add_query_arg( $tax_query_var, urlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) ) );
1166
							} else {
1167
								$remove_url = remove_query_arg( $tax_query_var );
1168
							}
1169
						}
1170
1171
						break;
1172
1173
					case 'post_type':
1174
						$post_type = get_post_type_object( $item['key'] );
1175
1176
						if ( ! $post_type || $post_type->exclude_from_search ) {
1177
							continue 2;  // switch() is considered a looping structure
1178
						}
1179
1180
						$query_vars = array(
1181
							'post_type' => $item['key'],
1182
						);
1183
1184
						$name = $post_type->labels->singular_name;
1185
1186
						// Is this post type active on this search?
1187
						$post_types = $query->get( 'post_type' );
1188
1189
						if ( ! is_array( $post_types ) ) {
1190
							$post_types = array( $post_types );
1191
						}
1192
1193
						if ( in_array( $item['key'], $post_types ) ) {
1194
							$active = true;
1195
1196
							$post_type_count = count( $post_types );
1197
1198
							// 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
1199 View Code Duplication
							if ( $post_type_count > 1 ) {
1200
								$remove_url = add_query_arg( 'post_type', urlencode_deep( array_diff( $post_types, array( $item['key'] ) ) ) );
1201
							} else {
1202
								$remove_url = remove_query_arg( 'post_type' );
1203
							}
1204
						}
1205
1206
						break;
1207
1208
					case 'date_histogram':
1209
						$timestamp = $item['key'] / 1000;
1210
1211
						$current_year  = $query->get( 'year' );
1212
						$current_month = $query->get( 'monthnum' );
1213
						$current_day   = $query->get( 'day' );
1214
1215
						switch ( $this->aggregations[ $label ]['interval'] ) {
1216
							case 'year':
1217
								$year = (int) date( 'Y', $timestamp );
1218
1219
								$query_vars = array(
1220
									'year'     => $year,
1221
									'monthnum' => false,
1222
									'day'      => false,
1223
								);
1224
1225
								$name = $year;
1226
1227
								// Is this year currently selected?
1228
								if ( ! empty( $current_year ) && (int) $current_year === $year ) {
1229
									$active = true;
1230
1231
									$remove_url = remove_query_arg( array( 'year', 'monthnum', 'day' ) );
1232
								}
1233
1234
								break;
1235
1236
							case 'month':
1237
								$year  = (int) date( 'Y', $timestamp );
1238
								$month = (int) date( 'n', $timestamp );
1239
1240
								$query_vars = array(
1241
									'year'     => $year,
1242
									'monthnum' => $month,
1243
									'day'      => false,
1244
								);
1245
1246
								$name = date( 'F Y', $timestamp );
1247
1248
								// Is this month currently selected?
1249
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1250
								     ! empty( $current_month ) && (int) $current_month === $month ) {
1251
									$active = true;
1252
1253
									$remove_url = remove_query_arg( array( 'monthnum', 'day' ) );
1254
								}
1255
1256
								break;
1257
1258
							case 'day':
1259
								$year  = (int) date( 'Y', $timestamp );
1260
								$month = (int) date( 'n', $timestamp );
1261
								$day   = (int) date( 'j', $timestamp );
1262
1263
								$query_vars = array(
1264
									'year'     => $year,
1265
									'monthnum' => $month,
1266
									'day'      => $day,
1267
								);
1268
1269
								$name = date( 'F jS, Y', $timestamp );
1270
1271
								// Is this day currently selected?
1272
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1273
								     ! empty( $current_month ) && (int) $current_month === $month &&
1274
								     ! empty( $current_day ) && (int) $current_day === $day ) {
1275
									$active = true;
1276
1277
									$remove_url = remove_query_arg( array( 'day' ) );
1278
								}
1279
1280
								break;
1281
1282
							default:
1283
								continue 3; // switch() is considered a looping structure
1284
						} // End switch().
1285
1286
						break;
1287
1288
					default:
1289
						//continue 2; // switch() is considered a looping structure
1290
				} // End switch().
1291
1292
				// Need to urlencode param values since add_query_arg doesn't
1293
				$url_params = urlencode_deep( $query_vars );
1294
1295
				$aggregation_data[ $label ]['buckets'][] = array(
1296
					'url'        => add_query_arg( $url_params ),
1297
					'query_vars' => $query_vars,
1298
					'name'       => $name,
1299
					'count'      => $item['doc_count'],
1300
					'active'     => $active,
1301
					'remove_url' => $remove_url,
1302
					'type'       => $type,
1303
					'type_label' => $label,
1304
				);
1305
			} // End foreach().
1306
		} // End foreach().
1307
1308
		return $aggregation_data;
1309
	}
1310
1311
	/**
1312
	 * Get the results of the Facets performed
1313
	 *
1314
	 * @module search
1315
	 *
1316
	 * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead
1317
	 *
1318
	 * @see Jetpack_Search::get_filters()
1319
	 *
1320
	 * @return array $facets Array of Facets applied and info about them
1321
	 */
1322
	public function get_search_facet_data() {
1323
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
1324
1325
		return $this->get_filters();
1326
	}
1327
1328
	/**
1329
	 * Get the Filters that are currently applied to this search
1330
	 *
1331
	 * @module search
1332
	 *
1333
	 * @return array Array if Filters that were applied
1334
	 */
1335
	public function get_active_filter_buckets() {
1336
		$active_buckets = array();
1337
1338
		$filters = $this->get_filters();
1339
1340
		if ( ! is_array( $filters ) ) {
1341
			return $active_buckets;
1342
		}
1343
1344
		foreach( $filters as $filter ) {
1345
			if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
1346
				foreach( $filter['buckets'] as $item ) {
1347
					if ( isset( $item['active'] ) && $item['active'] ) {
1348
						$active_buckets[] = $item;
1349
					}
1350
				}
1351
			}
1352
		}
1353
1354
		return $active_buckets;
1355
	}
1356
1357
	/**
1358
	 * Get the Filters that are currently applied to this search
1359
	 *
1360
	 * @module search
1361
	 *
1362
	 * @return array Array if Filters that were applied
1363
	 */
1364
	public function get_current_filters() {
1365
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
1366
1367
		return $this->get_active_filter_buckets();
1368
	}
1369
1370
	/**
1371
	 * Calculate the right query var to use for a given taxonomy
1372
	 *
1373
	 * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter
1374
	 *
1375
	 * @module search
1376
	 *
1377
	 * @param string $taxonomy_name The name of the taxonomy for which to get the query var
1378
	 *
1379
	 * @return bool|string The query var to use for this taxonomy, or false if none found
1380
	 */
1381
	public function get_taxonomy_query_var( $taxonomy_name ) {
1382
		$taxonomy = get_taxonomy( $taxonomy_name );
1383
1384
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1385
			return false;
1386
		}
1387
1388
		/**
1389
		 * Modify the query var to use for a given taxonomy
1390
		 *
1391
		 * @module search
1392
		 *
1393
		 * @since 5.0.0
1394
		 *
1395
		 * @param string $query_var The current query_var for the taxonomy
1396
		 * @param string $taxonomy_name The taxonomy name
1397
		 */
1398
		return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
1399
	}
1400
1401
	/**
1402
	 * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
1403
	 * which is the input order
1404
	 *
1405
	 * Necessary because ES does not always return Aggs in the same order that you pass them in, and it should be possible
1406
	 * to control the display order easily
1407
	 *
1408
	 * @module search
1409
	 *
1410
	 * @param array $aggregations Agg results to be reordered
1411
	 * @param array $desired Array with keys representing the desired ordering
1412
	 *
1413
	 * @return array A new array with reordered keys, matching those in $desired
1414
	 */
1415
	public function fix_aggregation_ordering( array $aggregations, array $desired ) {
1416
		if ( empty( $aggregations ) || empty( $desired ) ) {
1417
			return $aggregations;
1418
		}
1419
1420
		$reordered = array();
1421
1422
		foreach( array_keys( $desired ) as $agg_name ) {
1423
			if ( isset( $aggregations[ $agg_name ] ) ) {
1424
				$reordered[ $agg_name ] = $aggregations[ $agg_name ];
1425
			}
1426
		}
1427
1428
		return $reordered;
1429
	}
1430
}
1431