Completed
Push — fix/jetpack-search-cats ( acd815...e4ad44 )
by
unknown
29:33 queued 21:36
created

Jetpack_Search::init_hooks()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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