Completed
Push — add/jetpack-search-sorting-ui ( 0d0d66...3a3924 )
by
unknown
89:20 queued 80:50
created

Jetpack_Search::store_query_failure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
class Jetpack_Search {
4
5
	protected $found_posts = 0;
6
7
	/**
8
	 * The maximum offset ('from' param), since deep pages get exponentially slower.
9
	 *
10
	 * @see https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html
11
	 */
12
	protected $max_offset = 200;
13
14
	protected $search_result;
15
16
	protected $original_blog_id;
17
	protected $jetpack_blog_id;
18
19
	protected $aggregations = array();
20
	protected $max_aggregations_count = 100;
21
22
	// used to output query meta into page
23
	protected $last_query_info;
24
	protected $last_query_failure_info;
25
26
	protected static $instance;
27
28
	//Languages with custom analyzers, other languages are supported,
29
	// but are analyzed with the default analyzer.
30
	public static $analyzed_langs = array( 'ar', 'bg', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'eu', 'fa', 'fi', 'fr', 'he', 'hi', 'hu', 'hy', 'id', 'it', 'ja', 'ko', 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' );
31
32
	protected function __construct() {
33
		/* Don't do anything, needs to be initialized via instance() method */
34
	}
35
36
	public function __clone() {
37
		wp_die( "Please don't __clone Jetpack_Search" );
38
	}
39
40
	public function __wakeup() {
41
		wp_die( "Please don't __wakeup Jetpack_Search" );
42
	}
43
44
	/**
45
	 * Get singleton instance of Jetpack_Search
46
	 *
47
	 * Instantiates and sets up a new instance if needed, or returns the singleton
48
	 *
49
	 * @module search
50
	 *
51
	 * @return Jetpack_Search The Jetpack_Search singleton
52
	 */
53
	public static function instance() {
54
		if ( ! isset( self::$instance ) ) {
55
			self::$instance = new Jetpack_Search();
56
57
			self::$instance->setup();
58
		}
59
60
		return self::$instance;
61
	}
62
63
	/**
64
	 * Perform various setup tasks for the class
65
	 *
66
	 * Checks various pre-requisites and adds hooks
67
	 *
68
	 * @module search
69
	 */
70
	public function setup() {
71
		if ( ! Jetpack::is_active() || ! Jetpack::active_plan_supports( 'search' ) ) {
72
			return;
73
		}
74
75
		$this->jetpack_blog_id = Jetpack::get_option( 'id' );
76
77
		if ( ! $this->jetpack_blog_id ) {
78
			return;
79
		}
80
81
		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
			'order'     => $query->query_vars['order'],
340
			'orderby'     => $query->query_vars['orderby'],
341
		);
342
343
		$posts_query = new WP_Query( $args );
344
345
		// WP Core doesn't call the set_found_posts and its filters when filtering posts_pre_query like we do, so need to
346
		// do these manually
347
		$query->found_posts   = $this->found_posts;
348
		$query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
349
350
		return $posts_query->posts;
351
	}
352
353
	/**
354
	 * Build up the search, then run it against the Jetpack servers
355
	 *
356
	 * @param WP_Query $query The original WP_Query to use for the parameters of our search
357
	 */
358
	public function do_search( WP_Query $query ) {
359
		$page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
360
361
		$posts_per_page = $query->get( 'posts_per_page' );
362
363
		// ES API does not allow more than 15 results at a time
364
		if ( $posts_per_page > 15 ) {
365
			$posts_per_page = 15;
366
		}
367
368
		// Start building the WP-style search query args
369
		// They'll be translated to ES format args later
370
		$es_wp_query_args = array(
371
			'query'          => $query->get( 's' ),
372
			'posts_per_page' => $posts_per_page,
373
			'paged'          => $page,
374
			'orderby'        => $query->get( 'orderby' ),
375
			'order'          => $query->get( 'order' ),
376
		);
377
378
		if ( ! empty( $this->aggregations ) ) {
379
			$es_wp_query_args['aggregations'] = $this->aggregations;
380
		}
381
382
		// Did we query for authors?
383
		if ( $query->get( 'author_name' ) ) {
384
			$es_wp_query_args['author_name'] = $query->get( 'author_name' );
385
		}
386
387
		$es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
388
389
		$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...
390
391
392
		/**
393
		 * Modify the search query parameters, such as controlling the post_type.
394
		 *
395
		 * These arguments are in the format of WP_Query arguments
396
		 *
397
		 * @module search
398
		 *
399
		 * @since 5.0.0
400
		 *
401
		 * @param array $es_wp_query_args The current query args, in WP_Query format
402
		 * @param WP_Query $query The original query object
403
		 */
404
		$es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
405
406
		// If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
407
		// capped at $this->max_offset, so a high page would always return the last page of results otherwise
408
		if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $this->max_offset ) {
409
			$query->set_404();
410
411
			return;
412
		}
413
414
		// If there were no post types returned, then 404 to avoid querying against non-public post types, which could
415
		// happen if we don't add the post type restriction to the ES query
416
		if ( empty( $es_wp_query_args['post_type'] ) ) {
417
			$query->set_404();
418
419
			return;
420
		}
421
422
		// Convert the WP-style args into ES args
423
		$es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
424
425
		//Only trust ES to give us IDs, not the content since it is a mirror
426
		$es_query_args['fields'] = array(
427
			'post_id',
428
		);
429
430
		/**
431
		 * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
432
		 *
433
		 * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
434
		 *
435
		 * @module search
436
		 *
437
		 * @since 5.0.0
438
		 *
439
		 * @param array $es_query_args The raw ES query args
440
		 * @param WP_Query $query The original query object
441
		 */
442
		$es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
443
444
		// Do the actual search query!
445
		$this->search_result = $this->search( $es_query_args );
446
447
		if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
448
			$this->found_posts = 0;
449
450
			return;
451
		}
452
453
		// If we have aggregations, fix the ordering to match the input order (ES doesn't
454
		// guarantee the return order)
455
		if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
456
			$this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
457
		}
458
459
		// Total number of results for paging purposes. Capped at $this->>max_offset + $posts_per_page, as deep paging
460
		// gets quite expensive
461
		$this->found_posts = min( $this->search_result['results']['total'], $this->max_offset + $posts_per_page );
462
463
		return;
464
	}
465
466
	/**
467
	 * If the query has already been run before filters have been updated, then we need to re-run the query
468
	 * to get the latest aggregations.
469
	 *
470
	 * This is especially useful for supporting widget management in the customizer.
471
	 *
472
	 * @return bool Whether the query was successful or not.
473
	 */
474
	public function update_search_results_aggregations() {
475
		if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
476
			return false;
477
		}
478
479
		$es_args = $this->last_query_info['args'];
480
		$builder = new Jetpack_WPES_Query_Builder();
481
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
482
		$es_args['aggregations'] = $builder->build_aggregation();
483
484
		$this->search_result = $this->search( $es_args );
485
486
		return ! is_wp_error( $this->search_result );
487
	}
488
489
	/**
490
	 * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style ES term arguments for the search
491
	 *
492
	 * @module search
493
	 *
494
	 * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query
495
	 *
496
	 * @return array The new WP-style ES arguments (that will be converted into 'real' ES arguments)
497
	 */
498
	public function get_es_wp_query_terms_for_query( WP_Query $query ) {
499
		$args = array();
500
501
		$the_tax_query = $query->tax_query;
502
503
		if ( ! $the_tax_query ) {
504
			return $args;
505
		}
506
507
508
		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...
509
			return $args;
510
		}
511
512
		$args = array();
513
514
		foreach ( $the_tax_query->queries as $tax_query ) {
515
			// Right now we only support slugs...see note above
516
			if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
517
				continue;
518
			}
519
520
			$taxonomy = $tax_query['taxonomy'];
521
522 View Code Duplication
			if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
523
				$args[ $taxonomy ] = array();
524
			}
525
526
			$args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
527
		}
528
529
		return $args;
530
	}
531
532
	/**
533
	 * Parse out the post type from a WP_Query
534
	 *
535
	 * Only allows post types that are not marked as 'exclude_from_search'
536
	 *
537
	 * @module search
538
	 *
539
	 * @param WP_Query $query Original WP_Query object
540
	 *
541
	 * @return array Array of searchable post types corresponding to the original query
542
	 */
543
	public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
544
		$post_types = $query->get( 'post_type' );
545
546
		// If we're searching 'any', we want to only pass searchable post types to ES
547
		if ( 'any' === $post_types ) {
548
			$post_types = array_values( get_post_types( array(
549
				'exclude_from_search' => false,
550
			) ) );
551
		}
552
553
		if ( ! is_array( $post_types ) ) {
554
			$post_types = array( $post_types );
555
		}
556
557
		$post_types = array_unique( $post_types );
558
559
		$sanitized_post_types = array();
560
561
		// Make sure the post types are queryable
562
		foreach ( $post_types as $post_type ) {
563
			if ( ! $post_type ) {
564
				continue;
565
			}
566
567
			$post_type_object = get_post_type_object( $post_type );
568
			if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
569
				continue;
570
			}
571
572
			$sanitized_post_types[] = $post_type;
573
		}
574
575
		return $sanitized_post_types;
576
	}
577
578
	/**
579
	 * Initialze widgets for the Search module
580
	 *
581
	 * @module search
582
	 */
583
	public function action__widgets_init() {
584
		require_once( dirname( __FILE__ ) . '/class.jetpack-search-widget-filters.php' );
585
586
		register_widget( 'Jetpack_Search_Widget_Filters' );
587
	}
588
589
	/**
590
	 * Get the Elasticsearch result
591
	 *
592
	 * @module search
593
	 *
594
	 * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response
595
	 *
596
	 * @return array|bool The search results, or false if there was a failure
597
	 */
598
	public function get_search_result( $raw = false ) {
599
		if ( $raw ) {
600
			return $this->search_result;
601
		}
602
603
		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;
604
	}
605
606
	/**
607
	 * Add the date portion of a WP_Query onto the query args
608
	 *
609
	 * @param array    $es_wp_query_args
610
	 * @param WP_Query $query The original WP_Query
611
	 *
612
	 * @return array The es wp query args, with date filters added (as needed)
613
	 */
614
	public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
615
		if ( $query->get( 'year' ) ) {
616
			if ( $query->get( 'monthnum' ) ) {
617
				// Padding
618
				$date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
619
620
				if ( $query->get( 'day' ) ) {
621
					// Padding
622
					$date_day = sprintf( '%02d', $query->get( 'day' ) );
623
624
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
625
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
626
				} else {
627
					$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
628
629
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
630
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
631
				}
632
			} else {
633
				$date_start = $query->get( 'year' ) . '-01-01 00:00:00';
634
				$date_end   = $query->get( 'year' ) . '-12-31 23:59:59';
635
			}
636
637
			$es_wp_query_args['date_range'] = array(
638
				'field' => 'date',
639
				'gte'   => $date_start,
640
				'lte'   => $date_end,
641
			);
642
		}
643
644
		return $es_wp_query_args;
645
	}
646
647
	/**
648
	 * Converts WP_Query style args to ES args
649
	 *
650
	 * @module search
651
	 *
652
	 * @param array $args Array of WP_Query style arguments
653
	 *
654
	 * @return array Array of ES style query arguments
655
	 */
656
	function convert_wp_es_to_es_args( array $args ) {
657
		jetpack_require_lib( 'jetpack-wpes-query-builder' );
658
659
		$builder = new Jetpack_WPES_Query_Builder();
660
661
		$defaults = array(
662
			'blog_id'        => get_current_blog_id(),
663
664
			'query'          => null,    // Search phrase
665
			'query_fields'   => array( 'title', 'content', 'author', 'tag', 'category' ),
666
667
			'post_type'      => null,  // string or an array
668
			'terms'          => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) )
669
670
			'author'         => null,    // id or an array of ids
671
			'author_name'    => array(), // string or an array
672
673
			'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'
674
675
			'orderby'        => null,    // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
676
			'order'          => 'DESC',
677
678
			'posts_per_page' => 10,
679
680
			'offset'         => null,
681
			'paged'          => null,
682
683
			/**
684
			 * Aggregations. Examples:
685
			 * array(
686
			 *     'Tag'       => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
687
			 *     'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
688
			 * );
689
			 */
690
			'aggregations'         => null,
691
		);
692
693
		$args = wp_parse_args( $args, $defaults );
694
695
		$es_query_args = array(
696
			'blog_id' => absint( $args['blog_id'] ),
697
			'size'    => absint( $args['posts_per_page'] ),
698
		);
699
700
		// ES "from" arg (offset)
701
		if ( $args['offset'] ) {
702
			$es_query_args['from'] = absint( $args['offset'] );
703
		} elseif ( $args['paged'] ) {
704
			$es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
705
		}
706
707
		// Limit the offset to $this->max_offset posts, as deep pages get exponentially slower
708
		// See https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html
709
		$es_query_args['from'] = min( $es_query_args['from'], $this->max_offset );
710
711
		if ( ! is_array( $args['author_name'] ) ) {
712
			$args['author_name'] = array( $args['author_name'] );
713
		}
714
715
		// ES stores usernames, not IDs, so transform
716
		if ( ! empty( $args['author'] ) ) {
717
			if ( ! is_array( $args['author'] ) ) {
718
				$args['author'] = array( $args['author'] );
719
			}
720
721
			foreach ( $args['author'] as $author ) {
722
				$user = get_user_by( 'id', $author );
723
724
				if ( $user && ! empty( $user->user_login ) ) {
725
					$args['author_name'][] = $user->user_login;
726
				}
727
			}
728
		}
729
730
		//////////////////////////////////////////////////
731
		// Build the filters from the query elements.
732
		// Filters rock because they are cached from one query to the next
733
		// but they are cached as individual filters, rather than all combined together.
734
		// May get performance boost by also caching the top level boolean filter too.
735
		$filters = array();
736
737
		if ( $args['post_type'] ) {
738
			if ( ! is_array( $args['post_type'] ) ) {
739
				$args['post_type'] = array( $args['post_type'] );
740
			}
741
742
			$filters[] = array(
743
				'terms' => array(
744
					'post_type' => $args['post_type'],
745
				),
746
			);
747
		}
748
749
		if ( $args['author_name'] ) {
750
			$filters[] = array(
751
				'terms' => array(
752
					'author_login' => $args['author_name'],
753
				),
754
			);
755
		}
756
757
		if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
758
			$field = $args['date_range']['field'];
759
760
			unset( $args['date_range']['field'] );
761
762
			$filters[] = array(
763
				'range' => array(
764
					$field => $args['date_range'],
765
				),
766
			);
767
		}
768
769
		if ( is_array( $args['terms'] ) ) {
770
			foreach ( $args['terms'] as $tax => $terms ) {
771
				$terms = (array) $terms;
772
773
				if ( count( $terms ) && mb_strlen( $tax ) ) {
774 View Code Duplication
					switch ( $tax ) {
775
						case 'post_tag':
776
							$tax_fld = 'tag.slug';
777
778
							break;
779
780
						case 'category':
781
							$tax_fld = 'category.slug';
782
783
							break;
784
785
						default:
786
							$tax_fld = 'taxonomy.' . $tax . '.slug';
787
788
							break;
789
					}
790
791
					foreach ( $terms as $term ) {
792
						$filters[] = array(
793
							'term' => array(
794
								$tax_fld => $term,
795
							),
796
						);
797
					}
798
				}
799
			}
800
		}
801
802
		if ( $args['query'] ) {
803
			$query = array(
804
				'multi_match' => array(
805
					'query'    => $args['query'],
806
					'fields'   => $args['query_fields'],
807
					'operator' => 'and',
808
					'type'     => 'cross_fields',
809
				),
810
			);
811
812
			$builder->add_query( $query );
813
814
			Jetpack_Search::score_query_by_recency( $builder );
815
816
			if ( ! $args['orderby'] ) {
817
				$args['orderby'] = array( 'relevance' );
818
			}
819
		} else {
820
			if ( ! $args['orderby'] ) {
821
				$args['orderby'] = array( 'date' );
822
			}
823
		}
824
825
		// Validate the "order" field
826
		switch ( strtolower( $args['order'] ) ) {
827
			case 'asc':
828
				$args['order'] = 'asc';
829
				break;
830
831
			case 'desc':
832
			default:
833
				$args['order'] = 'desc';
834
				break;
835
		}
836
837
		$es_query_args['sort'] = array();
838
839
		foreach ( (array) $args['orderby'] as $orderby ) {
840
			// Translate orderby from WP field to ES field
841
			switch ( $orderby ) {
842
				case 'relevance' :
843
					//never order by score ascending
844
					$es_query_args['sort'][] = array(
845
						'_score' => array(
846
							'order' => 'desc',
847
						),
848
					);
849
850
					break;
851
852 View Code Duplication
				case 'date' :
853
					$es_query_args['sort'][] = array(
854
						'date' => array(
855
							'order' => $args['order'],
856
						),
857
					);
858
859
					break;
860
861 View Code Duplication
				case 'ID' :
862
					$es_query_args['sort'][] = array(
863
						'id' => array(
864
							'order' => $args['order'],
865
						),
866
					);
867
868
					break;
869
870
				case 'author' :
871
					$es_query_args['sort'][] = array(
872
						'author.raw' => array(
873
							'order' => $args['order'],
874
						),
875
					);
876
877
					break;
878
			} // End switch().
879
		} // End foreach().
880
881
		if ( empty( $es_query_args['sort'] ) ) {
882
			unset( $es_query_args['sort'] );
883
		}
884
885
		if ( ! empty( $filters ) && is_array( $filters ) ) {
886
			foreach ( $filters as $filter ) {
887
				$builder->add_filter( $filter );
888
			}
889
890
			$es_query_args['filter'] = $builder->build_filter();
891
		}
892
893
		$es_query_args['query'] = $builder->build_query();
894
895
		// Aggregations
896
		if ( ! empty( $args['aggregations'] ) ) {
897
			$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...
898
899
			$es_query_args['aggregations'] = $builder->build_aggregation();
900
		}
901
902
		return $es_query_args;
903
	}
904
905
	/**
906
	 * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in ES
907
	 *
908
	 * @module search
909
	 *
910
	 * @param array $aggregations Array of Aggregations (filters) to add to the Jetpack_WPES_Query_Builder
911
	 *
912
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
913
	 */
914
	public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
915
		foreach ( $aggregations as $label => $aggregation ) {
916
			switch ( $aggregation['type'] ) {
917
				case 'taxonomy':
918
					$this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
919
920
					break;
921
922
				case 'post_type':
923
					$this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
924
925
					break;
926
927
				case 'date_histogram':
928
					$this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
929
930
					break;
931
			}
932
		}
933
	}
934
935
	/**
936
	 * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
937
	 *
938
	 * @module search
939
	 *
940
	 * @param array $aggregation The aggregation to add to the query builder
941
	 * @param string $label The 'label' (unique id) for this aggregation
942
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
943
	 */
944
	public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
945
		$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...
946
947
		switch ( $aggregation['taxonomy'] ) {
948
			case 'post_tag':
949
				$field = 'tag';
950
				break;
951
952
			case 'category':
953
				$field = 'category';
954
				break;
955
956
			default:
957
				$field = 'taxonomy.' . $aggregation['taxonomy'];
958
				break;
959
		}
960
961
		$builder->add_aggs( $label, array(
962
			'terms' => array(
963
				'field' => $field . '.slug',
964
				'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
965
			),
966
		));
967
	}
968
969
	/**
970
	 * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
971
	 *
972
	 * @module search
973
	 *
974
	 * @param array $aggregation The aggregation to add to the query builder
975
	 * @param string $label The 'label' (unique id) for this aggregation
976
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
977
	 */
978
	public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
979
		$builder->add_aggs( $label, array(
980
			'terms' => array(
981
				'field' => 'post_type',
982
				'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
983
			),
984
		));
985
	}
986
987
	/**
988
	 * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
989
	 *
990
	 * @module search
991
	 *
992
	 * @param array $aggregation The aggregation to add to the query builder
993
	 * @param string $label The 'label' (unique id) for this aggregation
994
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
995
	 */
996
	public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
997
		$args = array(
998
			'interval' => $aggregation['interval'],
999
			'field'    => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' == $aggregation['field'] ) ? 'date_gmt' : 'date',
1000
		);
1001
1002
		if ( isset( $aggregation['min_doc_count'] ) ) {
1003
			$args['min_doc_count'] = intval( $aggregation['min_doc_count'] );
1004
		} else {
1005
			$args['min_doc_count'] = 1;
1006
		}
1007
1008
		$builder->add_aggs( $label, array(
1009
			'date_histogram' => $args,
1010
		));
1011
	}
1012
1013
	/**
1014
	 * And an existing filter object with a list of additional filters.
1015
	 *
1016
	 * Attempts to optimize the filters somewhat.
1017
	 *
1018
	 * @module search
1019
	 *
1020
	 * @param array $curr_filter The existing filters to build upon
1021
	 * @param array $filters The new filters to add
1022
	 *
1023
	 * @return array The resulting merged filters
1024
	 */
1025
	public static function and_es_filters( array $curr_filter, array $filters ) {
1026
		if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
1027
			if ( 1 === count( $filters ) ) {
1028
				return $filters[0];
1029
			}
1030
1031
			return array(
1032
				'and' => $filters,
1033
			);
1034
		}
1035
1036
		return array(
1037
			'and' => array_merge( array( $curr_filter ), $filters ),
1038
		);
1039
	}
1040
1041
	/**
1042
	 * Add a recency score to a given Jetpack_WPES_Query_Builder object, for emphasizing newer posts in results
1043
	 *
1044
	 * Internally uses a gauss decay function
1045
	 *
1046
	 * @module search
1047
	 *
1048
	 * @param Jetpack_WPES_Query_Builder $builder The Jetpack_WPES_Query_Builder to add the recency score to
1049
	 *
1050
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay
1051
	 */
1052
	public static function score_query_by_recency( Jetpack_WPES_Query_Builder &$builder ) {
1053
		//Newer content gets weighted slightly higher
1054
		$date_scale  = '360d';
1055
		$date_decay  = 0.9;
1056
		$date_origin = date( 'Y-m-d' );
1057
1058
		$builder->add_decay( 'gauss', array(
1059
			'date_gmt' => array(
1060
				'origin' => $date_origin,
1061
				'scale'  => $date_scale,
1062
				'decay'  => $date_decay,
1063
			),
1064
		));
1065
	}
1066
1067
	/**
1068
	 * Set the available filters for the search
1069
	 *
1070
	 * These get rendered via the Jetpack_Search_Widget_Filters() widget
1071
	 *
1072
	 * Behind the scenes, these are implemented using Elasticsearch Aggregations.
1073
	 *
1074
	 * If you do not require counts of how many documents match each filter, please consider using regular WP Query
1075
	 * arguments instead, such as via the jetpack_search_es_wp_query_args filter
1076
	 *
1077
	 * @module search
1078
	 *
1079
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1080
	 *
1081
	 * @param array $aggregations Array of filters (aggregations) to apply to the search
1082
	 */
1083
	public function set_filters( array $aggregations ) {
1084
		foreach ( (array) $aggregations as $key => $agg ) {
1085
			if ( empty( $agg['name'] ) ) {
1086
				$aggregations[ $key ]['name'] = $key;
1087
			}
1088
		}
1089
		$this->aggregations = $aggregations;
1090
	}
1091
1092
	/**
1093
	 * Set the search's facets (deprecated)
1094
	 *
1095
	 * @module search
1096
	 *
1097
	 * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead
1098
	 *
1099
	 * @see Jetpack_Search::set_filters()
1100
	 *
1101
	 * @param array $facets Array of facets to apply to the search
1102
	 */
1103
	public function set_facets( array $facets ) {
1104
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
1105
1106
		$this->set_filters( $facets );
1107
	}
1108
1109
	/**
1110
	 * Get the raw Aggregation results from the ES response
1111
	 *
1112
	 * @module search
1113
	 *
1114
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1115
	 *
1116
	 * @return array Array of Aggregations performed on the search
1117
	 */
1118
	public function get_search_aggregations_results() {
1119
		$aggregations = array();
1120
1121
		$search_result = $this->get_search_result();
1122
1123
		if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
1124
			$aggregations = $search_result['aggregations'];
1125
		}
1126
1127
		return $aggregations;
1128
	}
1129
1130
	/**
1131
	 * Get the raw Facet results from the ES response
1132
	 *
1133
	 * @module search
1134
	 *
1135
	 * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead
1136
	 *
1137
	 * @see Jetpack_Search::get_search_aggregations_results()
1138
	 *
1139
	 * @return array Array of Facets performed on the search
1140
	 */
1141
	public function get_search_facets() {
1142
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
1143
1144
		return $this->get_search_aggregations_results();
1145
	}
1146
1147
	/**
1148
	 * Get the results of the Filters performed, including the number of matching documents
1149
	 *
1150
	 * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
1151
	 * matching buckets, the url for applying/removing each bucket, etc.
1152
	 *
1153
	 * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
1154
	 * member if you need to access the raw filters set in Jetpack_Search::set_filters()
1155
	 *
1156
	 * @module search
1157
	 *
1158
	 * @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...
1159
	 *
1160
	 * @return array Array of Filters applied and info about them
1161
	 */
1162
	public function get_filters( WP_Query $query = null ) {
1163
		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...
1164
			global $wp_query;
1165
1166
			$query = $wp_query;
1167
		}
1168
1169
		$aggregation_data = $this->aggregations;
1170
1171
		if ( empty( $aggregation_data ) ) {
1172
			return $aggregation_data;
1173
		}
1174
1175
		$aggregation_results = $this->get_search_aggregations_results();
1176
1177
		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...
1178
			return $aggregation_data;
1179
		}
1180
1181
		// NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES
1182
		foreach ( $aggregation_results as $label => $aggregation ) {
1183
			if ( empty( $aggregation ) ) {
1184
				continue;
1185
			}
1186
1187
			$type = $this->aggregations[ $label ]['type'];
1188
1189
			$aggregation_data[ $label ]['buckets'] = array();
1190
1191
			$existing_term_slugs = array();
1192
1193
			$tax_query_var = null;
1194
1195
			// Figure out which terms are active in the query, for this taxonomy
1196
			if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
1197
				$tax_query_var = $this->get_taxonomy_query_var(  $this->aggregations[ $label ]['taxonomy'] );
1198
1199
				if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
1200
					foreach( $query->tax_query->queries as $tax_query ) {
1201
						if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
1202
						     'slug' === $tax_query['field'] &&
1203
						     is_array( $tax_query['terms'] ) ) {
1204
							$existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
1205
						}
1206
					}
1207
				}
1208
			}
1209
1210
			// Now take the resulting found aggregation items and generate the additional info about them, such as
1211
			// activation/deactivation url, name, count, etc
1212
			$buckets = array();
1213
1214
			if ( ! empty( $aggregation['buckets'] ) ) {
1215
				$buckets = (array) $aggregation['buckets'];
1216
			}
1217
1218
			if ( 'date_histogram' == $type ) {
1219
				//re-order newest to oldest
1220
				$buckets = array_reverse( $buckets );
1221
			}
1222
1223
			// Some aggregation types like date_histogram don't support the max results parameter
1224
			if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
1225
				$buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
1226
			}
1227
1228
			foreach ( $buckets as $item ) {
1229
				$query_vars = array();
1230
				$active     = false;
1231
				$remove_url = null;
1232
				$name       = '';
1233
1234
				// What type was the original aggregation?
1235
				switch ( $type ) {
1236
					case 'taxonomy':
1237
						$taxonomy = $this->aggregations[ $label ]['taxonomy'];
1238
1239
						$term = get_term_by( 'slug', $item['key'], $taxonomy );
1240
1241
						if ( ! $term || ! $tax_query_var ) {
1242
							continue 2; // switch() is considered a looping structure
1243
						}
1244
1245
						$query_vars = array(
1246
							$tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
1247
						);
1248
1249
						$name = $term->name;
1250
1251
						// Let's determine if this term is active or not
1252
1253
						if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
1254
							$active = true;
1255
1256
							$slug_count = count( $existing_term_slugs );
1257
1258
							if ( $slug_count > 1 ) {
1259
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1260
									$tax_query_var,
1261
									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...
1262
								);
1263
							} else {
1264
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( $tax_query_var );
1265
							}
1266
						}
1267
1268
						break;
1269
1270
					case 'post_type':
1271
						$post_type = get_post_type_object( $item['key'] );
1272
1273
						if ( ! $post_type || $post_type->exclude_from_search ) {
1274
							continue 2;  // switch() is considered a looping structure
1275
						}
1276
1277
						$query_vars = array(
1278
							'post_type' => $item['key'],
1279
						);
1280
1281
						$name = $post_type->labels->singular_name;
1282
1283
						// Is this post type active on this search?
1284
						$post_types = $query->get( 'post_type' );
1285
1286
						if ( ! is_array( $post_types ) ) {
1287
							$post_types = array( $post_types );
1288
						}
1289
1290
						if ( in_array( $item['key'], $post_types ) ) {
1291
							$active = true;
1292
1293
							$post_type_count = count( $post_types );
1294
1295
							// 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
1296
							if ( $post_type_count > 1 ) {
1297
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1298
									'post_type',
1299
									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...
1300
								);
1301
							} else {
1302
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
1303
							}
1304
						}
1305
1306
						break;
1307
1308
					case 'date_histogram':
1309
						$timestamp = $item['key'] / 1000;
1310
1311
						$current_year  = $query->get( 'year' );
1312
						$current_month = $query->get( 'monthnum' );
1313
						$current_day   = $query->get( 'day' );
1314
1315
						switch ( $this->aggregations[ $label ]['interval'] ) {
1316
							case 'year':
1317
								$year = (int) date( 'Y', $timestamp );
1318
1319
								$query_vars = array(
1320
									'year'     => $year,
1321
									'monthnum' => false,
1322
									'day'      => false,
1323
								);
1324
1325
								$name = $year;
1326
1327
								// Is this year currently selected?
1328
								if ( ! empty( $current_year ) && (int) $current_year === $year ) {
1329
									$active = true;
1330
1331
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
1332
								}
1333
1334
								break;
1335
1336
							case 'month':
1337
								$year  = (int) date( 'Y', $timestamp );
1338
								$month = (int) date( 'n', $timestamp );
1339
1340
								$query_vars = array(
1341
									'year'     => $year,
1342
									'monthnum' => $month,
1343
									'day'      => false,
1344
								);
1345
1346
								$name = date( 'F Y', $timestamp );
1347
1348
								// Is this month currently selected?
1349
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1350
								     ! empty( $current_month ) && (int) $current_month === $month ) {
1351
									$active = true;
1352
1353
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
1354
								}
1355
1356
								break;
1357
1358
							case 'day':
1359
								$year  = (int) date( 'Y', $timestamp );
1360
								$month = (int) date( 'n', $timestamp );
1361
								$day   = (int) date( 'j', $timestamp );
1362
1363
								$query_vars = array(
1364
									'year'     => $year,
1365
									'monthnum' => $month,
1366
									'day'      => $day,
1367
								);
1368
1369
								$name = date( 'F jS, Y', $timestamp );
1370
1371
								// Is this day currently selected?
1372
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1373
								     ! empty( $current_month ) && (int) $current_month === $month &&
1374
								     ! empty( $current_day ) && (int) $current_day === $day ) {
1375
									$active = true;
1376
1377
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
1378
								}
1379
1380
								break;
1381
1382
							default:
1383
								continue 3; // switch() is considered a looping structure
1384
						} // End switch().
1385
1386
						break;
1387
1388
					default:
1389
						//continue 2; // switch() is considered a looping structure
1390
				} // End switch().
1391
1392
				// Need to urlencode param values since add_query_arg doesn't
1393
				$url_params = urlencode_deep( $query_vars );
1394
1395
				$aggregation_data[ $label ]['buckets'][] = array(
1396
					'url'        => Jetpack_Search_Helpers::add_query_arg( $url_params ),
1397
					'query_vars' => $query_vars,
1398
					'name'       => $name,
1399
					'count'      => $item['doc_count'],
1400
					'active'     => $active,
1401
					'remove_url' => $remove_url,
1402
					'type'       => $type,
1403
					'type_label' => $aggregation_data[ $label ]['name'],
1404
					'widget_id'  => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0
1405
				);
1406
			} // End foreach().
1407
		} // End foreach().
1408
1409
		return $aggregation_data;
1410
	}
1411
1412
	/**
1413
	 * Get the results of the Facets performed
1414
	 *
1415
	 * @module search
1416
	 *
1417
	 * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead
1418
	 *
1419
	 * @see Jetpack_Search::get_filters()
1420
	 *
1421
	 * @return array $facets Array of Facets applied and info about them
1422
	 */
1423
	public function get_search_facet_data() {
1424
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
1425
1426
		return $this->get_filters();
1427
	}
1428
1429
	/**
1430
	 * Get the Filters that are currently applied to this search
1431
	 *
1432
	 * @module search
1433
	 *
1434
	 * @return array Array if Filters that were applied
1435
	 */
1436
	public function get_active_filter_buckets() {
1437
		$active_buckets = array();
1438
1439
		$filters = $this->get_filters();
1440
1441
		if ( ! is_array( $filters ) ) {
1442
			return $active_buckets;
1443
		}
1444
1445
		foreach( $filters as $filter ) {
1446
			if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
1447
				foreach( $filter['buckets'] as $item ) {
1448
					if ( isset( $item['active'] ) && $item['active'] ) {
1449
						$active_buckets[] = $item;
1450
					}
1451
				}
1452
			}
1453
		}
1454
1455
		return $active_buckets;
1456
	}
1457
1458
	/**
1459
	 * Get the Filters that are currently applied to this search
1460
	 *
1461
	 * @module search
1462
	 *
1463
	 * @return array Array if Filters that were applied
1464
	 */
1465
	public function get_current_filters() {
1466
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
1467
1468
		return $this->get_active_filter_buckets();
1469
	}
1470
1471
	/**
1472
	 * Calculate the right query var to use for a given taxonomy
1473
	 *
1474
	 * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter
1475
	 *
1476
	 * @module search
1477
	 *
1478
	 * @param string $taxonomy_name The name of the taxonomy for which to get the query var
1479
	 *
1480
	 * @return bool|string The query var to use for this taxonomy, or false if none found
1481
	 */
1482
	public function get_taxonomy_query_var( $taxonomy_name ) {
1483
		$taxonomy = get_taxonomy( $taxonomy_name );
1484
1485
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1486
			return false;
1487
		}
1488
1489
		/**
1490
		 * Modify the query var to use for a given taxonomy
1491
		 *
1492
		 * @module search
1493
		 *
1494
		 * @since 5.0.0
1495
		 *
1496
		 * @param string $query_var The current query_var for the taxonomy
1497
		 * @param string $taxonomy_name The taxonomy name
1498
		 */
1499
		return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
1500
	}
1501
1502
	/**
1503
	 * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
1504
	 * which is the input order
1505
	 *
1506
	 * Necessary because ES does not always return Aggs in the same order that you pass them in, and it should be possible
1507
	 * to control the display order easily
1508
	 *
1509
	 * @module search
1510
	 *
1511
	 * @param array $aggregations Agg results to be reordered
1512
	 * @param array $desired Array with keys representing the desired ordering
1513
	 *
1514
	 * @return array A new array with reordered keys, matching those in $desired
1515
	 */
1516
	public function fix_aggregation_ordering( array $aggregations, array $desired ) {
1517
		if ( empty( $aggregations ) || empty( $desired ) ) {
1518
			return $aggregations;
1519
		}
1520
1521
		$reordered = array();
1522
1523
		foreach( array_keys( $desired ) as $agg_name ) {
1524
			if ( isset( $aggregations[ $agg_name ] ) ) {
1525
				$reordered[ $agg_name ] = $aggregations[ $agg_name ];
1526
			}
1527
		}
1528
1529
		return $reordered;
1530
	}
1531
}
1532