Completed
Push — fix/naming-search-customizer-l... ( 08095a...470f0d )
by
unknown
51:33 queued 42:57
created

update_search_results_aggregations()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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