Completed
Push — merge/getty-shortcode ( ff72de...8d66c6 )
by
unknown
12:23
created

Jetpack_Search::set_facets()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1260
								);
1261
							} else {
1262
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( $tax_query_var );
1263
							}
1264
						}
1265
1266
						break;
1267
1268
					case 'post_type':
1269
						$post_type = get_post_type_object( $item['key'] );
1270
1271
						if ( ! $post_type || $post_type->exclude_from_search ) {
1272
							continue 2;  // switch() is considered a looping structure
1273
						}
1274
1275
						$query_vars = array(
1276
							'post_type' => $item['key'],
1277
						);
1278
1279
						$name = $post_type->labels->singular_name;
1280
1281
						// Is this post type active on this search?
1282
						$post_types = $query->get( 'post_type' );
1283
1284
						if ( ! is_array( $post_types ) ) {
1285
							$post_types = array( $post_types );
1286
						}
1287
1288
						if ( in_array( $item['key'], $post_types ) ) {
1289
							$active = true;
1290
1291
							$post_type_count = count( $post_types );
1292
1293
							// 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
1294
							if ( $post_type_count > 1 ) {
1295
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1296
									'post_type',
1297
									implode( ',',  array_diff( $post_types, array( $item['key'] ) ) )
0 ignored issues
show
Documentation introduced by
implode(',', array_diff(..., array($item['key']))) is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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