Completed
Push — update/search/formatting-and-p... ( cb3630...02e684 )
by Alex
49:40 queued 38:52
created

Jetpack_Search::get_taxonomy_query_var()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 19
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 2
nop 1
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
class Jetpack_Search {
4
5
	protected $found_posts = 0;
6
7
	protected $search_result;
8
9
	protected $original_blog_id;
10
	protected $jetpack_blog_id;
11
12
	protected $aggregations = array();
13
	protected $max_aggregations_count = 100;
14
15
	// used to output query meta into page
16
	protected $last_query_info;
17
	protected $last_query_failure_info;
18
19
	protected static $instance;
20
21
	//Languages with custom analyzers, other languages are supported,
22
	// but are analyzed with the default analyzer.
23
	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' );
24
25
	protected function __construct() {
26
		/* Don't do anything, needs to be initialized via instance() method */
27
	}
28
29
	public function __clone() {
30
		wp_die( "Please don't __clone Jetpack_Search" );
31
	}
32
33
	public function __wakeup() {
34
		wp_die( "Please don't __wakeup Jetpack_Search" );
35
	}
36
37
	/**
38
	 * Get singleton instance of Jetpack_Search
39
	 *
40
	 * Instantiates and sets up a new instance if needed, or returns the singleton
41
	 *
42
	 * @module search
43
	 *
44
	 * @return Jetpack_Search The Jetpack_Search singleton
45
	 */
46
	public static function instance() {
47
		if ( ! isset( self::$instance ) ) {
48
			self::$instance = new Jetpack_Search();
49
50
			self::$instance->setup();
51
		}
52
53
		return self::$instance;
54
	}
55
56
	/**
57
	 * Perform various setup tasks for the class
58
	 *
59
	 * Checks various pre-requisites and adds hooks
60
	 *
61
	 * @module search
62
	 */
63
	public function setup() {
64
		if ( ! Jetpack::is_active() || ! Jetpack::active_plan_supports( 'search' ) ) {
65
			return;
66
		}
67
68
		$this->jetpack_blog_id = Jetpack::get_option( 'id' );
69
70
		if ( ! $this->jetpack_blog_id ) {
71
			return;
72
		}
73
74
		require_once( dirname( __FILE__ ) . '/class.jetpack-search-helpers.php' );
75
76
		$this->init_hooks();
77
	}
78
79
	/**
80
	 * Setup the various hooks needed for the plugin to take over Search duties
81
	 *
82
	 * @module search
83
	 */
84
	public function init_hooks() {
85
		add_action( 'widgets_init', array( $this, 'action__widgets_init' ) );
86
87
		if ( ! is_admin() ) {
88
			add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 );
89
90
			add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ), 10, 2 );
91
92
			add_action( 'did_jetpack_search_query', array( $this, 'store_last_query_info' ) );
93
			add_action( 'failed_jetpack_search_query', array( $this, 'store_query_failure' ) );
94
95
			add_action( 'init', array( $this, 'set_filters_from_widgets' ) );
96
97
			add_action( 'pre_get_posts', array( $this, 'maybe_add_post_type_as_var' ) );
98
		} else {
99
			add_action( 'update_option', array( $this, 'track_widget_updates' ), 10, 3 );
100
		}
101
	}
102
103
	/**
104
	 * Print query info as a HTML comment in the footer
105
	 */
106
107
	public function store_query_failure( $meta ) {
108
		$this->last_query_failure_info = $meta;
109
		add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
110
	}
111
112
	public function print_query_failure() {
113
		if ( $this->last_query_failure_info ) {
114
			printf(
115
				'<!-- Jetpack Search failed with code %s: %s - %s -->',
116
				esc_html( $this->last_query_failure_info['response_code'] ),
117
				esc_html( $this->last_query_failure_info['json']['error'] ),
118
				esc_html( $this->last_query_failure_info['json']['message'] )
119
			);
120
		}
121
	}
122
123
	public function store_last_query_info( $meta ) {
124
		$this->last_query_info = $meta;
125
		add_action( 'wp_footer', array( $this, 'print_query_success' ) );
126
	}
127
128
	public function print_query_success() {
129
		if ( $this->last_query_info ) {
130
			printf(
131
				'<!-- Jetpack Search took %s ms, ES time %s ms -->',
132
				intval( $this->last_query_info['elapsed_time'] ),
133
				esc_html( $this->last_query_info['es_time'] )
134
			);
135
		}
136
	}
137
138
	/**
139
	 * Returns the last query information, or false if no information was stored.
140
	 *
141
	 * @return bool|array
142
	 */
143
	public function get_last_query_info() {
144
		return empty( $this->last_query_info ) ? false : $this->last_query_info;
145
	}
146
147
	/**
148
	 * Returns the last query failure information, or false if no failure information was stored.
149
	 *
150
	 * @return bool|array
151
	 */
152
	public function get_last_query_failure_info() {
153
		return empty( $this->last_query_failure_info ) ? false : $this->last_query_failure_info;
154
	}
155
156
	function are_filters_by_widget_disabled() {
157
		/**
158
		 * Allows developers to disable filters being set by widget, in favor of manually
159
		 * setting filters via `Jetpack_Search::set_filters()`.
160
		 *
161
		 * @module search
162
		 *
163
		 * @since 5.7.0
164
		 *
165
		 * @param bool false
166
		 */
167
		return apply_filters( 'jetpack_search_disable_widget_filters', false );
168
	}
169
170
	/**
171
	 * Retrieves a list of known Jetpack search filters widget IDs, gets the filters for each widget,
172
	 * and applies those filters to this Jetpack_Search object.
173
	 *
174
	 * @since 5.7.0
175
	 *
176
	 * @return void
177
	 */
178
	function set_filters_from_widgets() {
179
		if ( $this->are_filters_by_widget_disabled() ) {
180
			return;
181
		}
182
183
		$filters = Jetpack_Search_Helpers::get_filters_from_widgets();
184
185
		if ( ! empty( $filters ) ) {
186
			$this->set_filters( $filters );
187
		}
188
	}
189
190
	function maybe_add_post_type_as_var( WP_Query $query ) {
191
		if ( $query->is_main_query() && $query->is_search && ! empty( $_GET['post_type'] ) ) {
192
			$post_types = ( is_string( $_GET['post_type'] ) && false !== strpos( $_GET['post_type'], ',' ) )
193
				? $post_type = explode( ',', $_GET['post_type'] )
194
				: (array) $_GET['post_type'];
195
			$post_types = array_map( 'sanitize_key', $post_types );
196
			$query->set('post_type', $post_types );
197
		}
198
	}
199
200
	/*
201
	 * Run a search on the WP.com public API.
202
	 *
203
	 * @module search
204
	 *
205
	 * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint
206
	 *
207
	 * @return object|WP_Error The response from the public api, or a WP_Error
208
	 */
209
	public function search( array $es_args ) {
210
		$endpoint    = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
211
		$service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
212
213
		$do_authenticated_request = false;
214
215
		if ( class_exists( 'Jetpack_Client' ) &&
216
			isset( $es_args['authenticated_request'] ) &&
217
			true === $es_args['authenticated_request'] ) {
218
			$do_authenticated_request = true;
219
		}
220
221
		unset( $es_args['authenticated_request'] );
222
223
		$request_args = array(
224
			'headers' => array(
225
				'Content-Type' => 'application/json',
226
			),
227
			'timeout'    => 10,
228
			'user-agent' => 'jetpack_search',
229
		);
230
231
		$request_body = json_encode( $es_args );
232
233
		$start_time = microtime( true );
234
235
		if ( $do_authenticated_request ) {
236
			$request_args['method'] = 'POST';
237
238
			$request = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint, Jetpack_Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
239
		} else {
240
			$request_args = array_merge( $request_args, array(
241
				'body' => $request_body,
242
			) );
243
244
			$request = wp_remote_post( $service_url, $request_args );
245
		}
246
247
		$end_time = microtime( true );
248
249
		if ( is_wp_error( $request ) ) {
250
			return $request;
251
		}
252
253
		$response_code = wp_remote_retrieve_response_code( $request );
254
		$response      = json_decode( wp_remote_retrieve_body( $request ), true );
255
256
		$took = is_array( $response ) && ! empty( $response['took'] )
257
			? $response['took']
258
			: null;
259
260
		$query = array(
261
			'args'          => $es_args,
262
			'response'      => $response,
263
			'response_code' => $response_code,
264
			'elapsed_time'  => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms.
265
			'es_time'       => $took,
266
			'url'           => $service_url,
267
		);
268
269
		/**
270
		 * Fires after a search request has been performed
271
		 *
272
		 * Includes the following info in the $query parameter:
273
		 *
274
		 * array args Array of Elasticsearch arguments for the search
275
		 * array response Raw API response, JSON decoded
276
		 * int response_code HTTP response code of the request
277
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
278
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
279
		 * string url API url that was queried
280
		 *
281
		 * @module search
282
		 *
283
		 * @since 5.0.0
284
		 * @since 5.8.0 This action now fires on all queries instead of just successful queries.
285
		 *
286
		 * @param array $query Array of information about the query performed
287
		 */
288
		do_action( 'did_jetpack_search_query', $query );
289
290
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
291
			/**
292
			 * Fires after a search query request has failed
293
			 *
294
			 * @module search
295
			 *
296
			 * @since 5.6.0
297
			 *
298
			 * @param array Array containing the response code and response from the failed search query
299
			 */
300
			do_action( 'failed_jetpack_search_query', array(
301
				'response_code' => $response_code,
302
				'json'          => $response,
303
			) );
304
			return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
305
		}
306
307
		return $response;
308
	}
309
310
	/**
311
	 * Bypass the normal Search query and offload it to Jetpack servers
312
	 *
313
	 * This is the main hook of the plugin and is responsible for returning the posts that match the search query
314
	 *
315
	 * @module search
316
	 *
317
	 * @param array $posts Current array of posts (still pre-query)
318
	 * @param WP_Query $query The WP_Query being filtered
319
	 *
320
	 * @return array Array of matching posts
321
	 */
322
	public function filter__posts_pre_query( $posts, $query ) {
323
		/**
324
		 * Determine whether a given WP_Query should be handled by ElasticSearch
325
		 *
326
		 * @module search
327
		 *
328
		 * @since 5.6.0
329
		 * @param bool $should_handle Should be handled by Jetpack Search
330
		 * @param WP_Query $query The wp_query object
331
		 */
332
		if ( ! apply_filters( 'jetpack_search_should_handle_query', ( $query->is_main_query() && $query->is_search() ), $query ) ) {
333
			return $posts;
334
		}
335
336
		$this->do_search( $query );
337
338
		if ( ! is_array( $this->search_result ) ) {
339
			return $posts;
340
		}
341
342
		// If no results, nothing to do
343
		if ( ! count( $this->search_result['results']['hits'] ) ) {
344
			return array();
345
		}
346
347
		$post_ids = array();
348
349
		foreach ( $this->search_result['results']['hits'] as $result ) {
350
			$post_ids[] = (int) $result['fields']['post_id'];
351
		}
352
353
		// Query all posts now
354
		$args = array(
355
			'post__in'            => $post_ids,
356
			'perm'                => 'readable',
357
			'post_type'           => 'any',
358
			'ignore_sticky_posts' => true,
359
		);
360
361
		if ( isset( $query->query_vars['order'] ) ) {
362
			$args['order'] = $query->query_vars['order'];
363
		}
364
365
		if ( isset( $query->query_vars['orderby'] ) ) {
366
			$args['orderby'] = $query->query_vars['orderby'];
367
		}
368
369
		$posts_query = new WP_Query( $args );
370
371
		// WP Core doesn't call the set_found_posts and its filters when filtering posts_pre_query like we do, so need to do these manually.
372
		$query->found_posts   = $this->found_posts;
373
		$query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
374
375
		return $posts_query->posts;
376
	}
377
378
	/**
379
	 * Build up the search, then run it against the Jetpack servers
380
	 *
381
	 * @param WP_Query $query The original WP_Query to use for the parameters of our search.
382
	 */
383
	public function do_search( WP_Query $query ) {
384
		$page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
385
386
		// Get maximum allowed offset and posts per page values for the API.
387
		$max_offset = Jetpack_Search_Helpers::get_max_offset();
388
		$max_posts_per_page = Jetpack_Search_Helpers::get_max_posts_per_page();
389
390
		$posts_per_page = $query->get( 'posts_per_page' );
391
		if ( $posts_per_page > $max_posts_per_page ) {
392
			$posts_per_page = $max_posts_per_page;
393
		}
394
395
		// Start building the WP-style search query args.
396
		// They'll be translated to ES format args later.
397
		$es_wp_query_args = array(
398
			'query'          => $query->get( 's' ),
399
			'posts_per_page' => $posts_per_page,
400
			'paged'          => $page,
401
			'orderby'        => $query->get( 'orderby' ),
402
			'order'          => $query->get( 'order' ),
403
		);
404
405
		if ( ! empty( $this->aggregations ) ) {
406
			$es_wp_query_args['aggregations'] = $this->aggregations;
407
		}
408
409
		// Did we query for authors?
410
		if ( $query->get( 'author_name' ) ) {
411
			$es_wp_query_args['author_name'] = $query->get( 'author_name' );
412
		}
413
414
		$es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
415
		$es_wp_query_args['terms']     = $this->get_es_wp_query_terms_for_query( $query );
416
417
		/**
418
		 * Modify the search query parameters, such as controlling the post_type.
419
		 *
420
		 * These arguments are in the format of WP_Query arguments
421
		 *
422
		 * @module search
423
		 *
424
		 * @since 5.0.0
425
		 *
426
		 * @param array $es_wp_query_args The current query args, in WP_Query format
427
		 * @param WP_Query $query The original query object
428
		 */
429
		$es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
430
431
		// If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
432
		// capped at Jetpack_Search_Helpers::get_max_offset(), so a high page would always return the last page of results otherwise.
433
		if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $max_offset ) {
434
			$query->set_404();
435
436
			return;
437
		}
438
439
		// If there were no post types returned, then 404 to avoid querying against non-public post types, which could
440
		// happen if we don't add the post type restriction to the ES query.
441
		if ( empty( $es_wp_query_args['post_type'] ) ) {
442
			$query->set_404();
443
444
			return;
445
		}
446
447
		// Convert the WP-style args into ES args.
448
		$es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
449
450
		//Only trust ES to give us IDs, not the content since it is a mirror
451
		$es_query_args['fields'] = array(
452
			'post_id',
453
		);
454
455
		/**
456
		 * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
457
		 *
458
		 * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
459
		 *
460
		 * @module search
461
		 *
462
		 * @since 5.0.0
463
		 *
464
		 * @param array $es_query_args The raw ES query args
465
		 * @param WP_Query $query The original query object
466
		 */
467
		$es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
468
469
		// Do the actual search query!
470
		$this->search_result = $this->search( $es_query_args );
471
472
		if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
473
			$this->found_posts = 0;
474
475
			return;
476
		}
477
478
		// If we have aggregations, fix the ordering to match the input order (ES doesn't guarantee the return order).
479
		if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
480
			$this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
481
		}
482
483
		// Total number of results for paging purposes. Capped at $max_offset + $posts_per_page, as deep paging gets quite expensive.
484
		$this->found_posts = min( $this->search_result['results']['total'], $max_offset + $posts_per_page );
485
486
		return;
487
	}
488
489
	/**
490
	 * If the query has already been run before filters have been updated, then we need to re-run the query
491
	 * to get the latest aggregations.
492
	 *
493
	 * This is especially useful for supporting widget management in the customizer.
494
	 *
495
	 * @return bool Whether the query was successful or not.
496
	 */
497
	public function update_search_results_aggregations() {
498
		if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
499
			return false;
500
		}
501
502
		$es_args = $this->last_query_info['args'];
503
		$builder = new Jetpack_WPES_Query_Builder();
504
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
505
		$es_args['aggregations'] = $builder->build_aggregation();
506
507
		$this->search_result = $this->search( $es_args );
508
509
		return ! is_wp_error( $this->search_result );
510
	}
511
512
	/**
513
	 * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style ES term arguments for the search
514
	 *
515
	 * @module search
516
	 *
517
	 * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query
518
	 *
519
	 * @return array The new WP-style ES arguments (that will be converted into 'real' ES arguments)
520
	 */
521
	public function get_es_wp_query_terms_for_query( WP_Query $query ) {
522
		$args = array();
523
524
		$the_tax_query = $query->tax_query;
525
526
		if ( ! $the_tax_query ) {
527
			return $args;
528
		}
529
530
531
		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...
532
			return $args;
533
		}
534
535
		$args = array();
536
537
		foreach ( $the_tax_query->queries as $tax_query ) {
538
			// Right now we only support slugs...see note above
539
			if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
540
				continue;
541
			}
542
543
			$taxonomy = $tax_query['taxonomy'];
544
545 View Code Duplication
			if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
546
				$args[ $taxonomy ] = array();
547
			}
548
549
			$args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
550
		}
551
552
		return $args;
553
	}
554
555
	/**
556
	 * Parse out the post type from a WP_Query
557
	 *
558
	 * Only allows post types that are not marked as 'exclude_from_search'
559
	 *
560
	 * @module search
561
	 *
562
	 * @param WP_Query $query Original WP_Query object
563
	 *
564
	 * @return array Array of searchable post types corresponding to the original query
565
	 */
566
	public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
567
		$post_types = $query->get( 'post_type' );
568
569
		// If we're searching 'any', we want to only pass searchable post types to ES
570
		if ( 'any' === $post_types ) {
571
			$post_types = array_values( get_post_types( array(
572
				'exclude_from_search' => false,
573
			) ) );
574
		}
575
576
		if ( ! is_array( $post_types ) ) {
577
			$post_types = array( $post_types );
578
		}
579
580
		$post_types = array_unique( $post_types );
581
582
		$sanitized_post_types = array();
583
584
		// Make sure the post types are queryable
585
		foreach ( $post_types as $post_type ) {
586
			if ( ! $post_type ) {
587
				continue;
588
			}
589
590
			$post_type_object = get_post_type_object( $post_type );
591
			if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
592
				continue;
593
			}
594
595
			$sanitized_post_types[] = $post_type;
596
		}
597
598
		return $sanitized_post_types;
599
	}
600
601
	/**
602
	 * Initialize widgets for the Search module
603
	 *
604
	 * @module search
605
	 */
606
	public function action__widgets_init() {
607
		require_once( dirname( __FILE__ ) . '/class.jetpack-search-widget-filters.php' );
608
609
		register_widget( 'Jetpack_Search_Widget_Filters' );
610
	}
611
612
	/**
613
	 * Get the Elasticsearch result
614
	 *
615
	 * @module search
616
	 *
617
	 * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response
618
	 *
619
	 * @return array|bool The search results, or false if there was a failure
620
	 */
621
	public function get_search_result( $raw = false ) {
622
		if ( $raw ) {
623
			return $this->search_result;
624
		}
625
626
		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;
627
	}
628
629
	/**
630
	 * Add the date portion of a WP_Query onto the query args
631
	 *
632
	 * @param array    $es_wp_query_args
633
	 * @param WP_Query $query The original WP_Query
634
	 *
635
	 * @return array The es wp query args, with date filters added (as needed)
636
	 */
637
	public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
638
		if ( $query->get( 'year' ) ) {
639
			if ( $query->get( 'monthnum' ) ) {
640
				// Padding
641
				$date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
642
643
				if ( $query->get( 'day' ) ) {
644
					// Padding
645
					$date_day = sprintf( '%02d', $query->get( 'day' ) );
646
647
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
648
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
649
				} else {
650
					$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
651
652
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
653
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
654
				}
655
			} else {
656
				$date_start = $query->get( 'year' ) . '-01-01 00:00:00';
657
				$date_end   = $query->get( 'year' ) . '-12-31 23:59:59';
658
			}
659
660
			$es_wp_query_args['date_range'] = array(
661
				'field' => 'date',
662
				'gte'   => $date_start,
663
				'lte'   => $date_end,
664
			);
665
		}
666
667
		return $es_wp_query_args;
668
	}
669
670
	/**
671
	 * Converts WP_Query style args to ES args
672
	 *
673
	 * @module search
674
	 *
675
	 * @param array $args Array of WP_Query style arguments
676
	 *
677
	 * @return array Array of ES style query arguments
678
	 */
679
	function convert_wp_es_to_es_args( array $args ) {
680
		jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-parser' );
681
682
		$defaults = array(
683
			'blog_id'        => get_current_blog_id(),
684
685
			'query'          => null,    // Search phrase
686
			'query_fields'   => array( ), //list of fields to search
687
688
			'post_type'      => null,  // string or an array
689
			'terms'          => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) )
690
691
			'author'         => null,    // id or an array of ids
692
			'author_name'    => array(), // string or an array
693
694
			'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'
695
696
			'orderby'        => null,    // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
697
			'order'          => 'DESC',
698
699
			'posts_per_page' => 10,
700
701
			'offset'         => null,
702
			'paged'          => null,
703
704
			/**
705
			 * Aggregations. Examples:
706
			 * array(
707
			 *     'Tag'       => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
708
			 *     'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
709
			 * );
710
			 */
711
			'aggregations'   => null,
712
		);
713
714
		$args = wp_parse_args( $args, $defaults );
715
716
		$parser = new Jetpack_WPES_Search_Query_Parser( $args['query'], array( get_locale() ) );
717
718
		if ( empty( $args['query_fields'] ) ) {
719
720
			if ( defined( 'JETPACK_SEARCH_VIP_INDEX' ) && JETPACK_SEARCH_VIP_INDEX ) {
721
				//VIP indices do not have per language fields
722
				$match_fields = array(
723
					'title^0.1',
724
					'content^0.1',
725
					'excerpt^0.1',
726
					'tag.name^0.1',
727
					'category.name^0.1',
728
					'author_login^0.1',
729
					'author^0.1',
730
				);
731
				$boost_fields = array(
732
					'title^2',
733
					'tag.name',
734
					'category.name',
735
					'author_login',
736
					'author',
737
				);
738
				$boost_phrase_fields = array(
739
					'title',
740
					'content',
741
					'excerpt',
742
					'tag.name',
743
					'category.name',
744
					'author',
745
				);
746
			} else {
747
				$match_fields = $parser->merge_ml_fields(
748
					array(
749
						'title'    => 0.1,
750
						'content'  => 0.1,
751
						'excerpt'  => 0.1,
752
						'tag.name'      => 0.1,
753
						'category.name' => 0.1,
754
					),
755
					array(
756
						'author_login^0.1',
757
						'author^0.1',
758
					)
759
				);
760
761
				$boost_fields = $parser->merge_ml_fields(
762
					array(
763
						'title'            => 2,
764
						'tag.name'         => 1,
765
						'category.name'    => 1,
766
					),
767
					array(
768
						'author_login',
769
						'author',
770
					)
771
				);
772
773
				$boost_phrase_fields = $parser->merge_ml_fields(
774
					array(
775
						'title'            => 1,
776
						'content'          => 1,
777
						'excerpt'          => 1,
778
						'tag.name'         => 1,
779
						'category.name'    => 1,
780
					),
781
					array(
782
						'author',
783
					)
784
				);
785
			}
786
		} else {
787
			//If code is overriding the fields, then use that
788
			// important for backwards compat
789
			$match_fields = $args['query_fields'];
790
			$boost_phrase_fields = $match_content_fields;
0 ignored issues
show
Bug introduced by
The variable $match_content_fields does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
791
			$boost_fields = null;
792
		}
793
794
		$parser->phrase_filter( array(
795
			'must_query_fields'  => $match_fields,
796
			'boost_query_fields' => null,
797
		) );
798
		$parser->remaining_query( array(
799
			'must_query_fields'  => $match_fields,
800
			'boost_query_fields' => $boost_fields,
801
		) );
802
803
		// Boost on phrase matches
804
		$parser->remaining_query( array(
805
			'boost_query_fields' => $boost_phrase_fields,
806
			'boost_query_type'   => 'phrase',
807
		) );
808
809
		/**
810
		 * Modify the recency decay parameters for the search query.
811
		 *
812
		 * The recency decay lowers the search scores based on the age of a post
813
		 * relative to an origin date. Basic adjustments:
814
		 *  origin: A date. Posts with this date will have the highest score and no decay applied. Default is today.
815
		 *  offset: Number of days/months/years (eg 30d). All posts within this time range of the origin (before and after) will have no decay applied. Default is no offet.
816
		 *  scale: The number of days/months/years from the origin+offset at which the decay will equay the decay param. Default 360d
817
		 *  decay: The amount of decay applied at offset+scale. Default 0.9
818
		 *
819
		 * The curve applied is a Gaussian. More details available at https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay
820
		 *
821
		 * @module search
822
		 *
823
		 * @since 5.8.0
824
		 *
825
		 * @param array $decay_params The decay parameters
826
		 * @param array $args The WP query parameters
827
		 */
828
		$decay_params = apply_filters(
829
			'jetpack_search_recency_score_decay',
830
			array(
831
				'origin' => date( 'Y-m-d' ),
832
				'scale'  => '360d',
833
				'decay'  => 0.9,
834
			),
835
			$args
836
		);
837
838
		if ( ! empty( $decay_params ) ) {
839
			// Newer content gets weighted slightly higher
840
			$parser->add_decay( 'gauss', array(
841
				'date_gmt' => $decay_params
842
			) );
843
		}
844
845
		$es_query_args = array(
846
			'blog_id' => absint( $args['blog_id'] ),
847
			'size'    => absint( $args['posts_per_page'] ),
848
		);
849
850
		// ES "from" arg (offset)
851
		if ( $args['offset'] ) {
852
			$es_query_args['from'] = absint( $args['offset'] );
853
		} elseif ( $args['paged'] ) {
854
			$es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
855
		}
856
857
		$es_query_args['from'] = min( $es_query_args['from'], Jetpack_Search_Helpers::get_max_offset() );
858
859
		if ( ! is_array( $args['author_name'] ) ) {
860
			$args['author_name'] = array( $args['author_name'] );
861
		}
862
863
		// ES stores usernames, not IDs, so transform
864
		if ( ! empty( $args['author'] ) ) {
865
			if ( ! is_array( $args['author'] ) ) {
866
				$args['author'] = array( $args['author'] );
867
			}
868
869
			foreach ( $args['author'] as $author ) {
870
				$user = get_user_by( 'id', $author );
871
872
				if ( $user && ! empty( $user->user_login ) ) {
873
					$args['author_name'][] = $user->user_login;
874
				}
875
			}
876
		}
877
878
		//////////////////////////////////////////////////
879
		// Build the filters from the query elements.
880
		// Filters rock because they are cached from one query to the next
881
		// but they are cached as individual filters, rather than all combined together.
882
		// May get performance boost by also caching the top level boolean filter too.
883
884
		if ( $args['post_type'] ) {
885
			if ( ! is_array( $args['post_type'] ) ) {
886
				$args['post_type'] = array( $args['post_type'] );
887
			}
888
889
			$parser->add_filter( array(
890
				'terms' => array(
891
					'post_type' => $args['post_type'],
892
				),
893
			) );
894
		}
895
896
		if ( $args['author_name'] ) {
897
			$parser->add_filter( array(
898
				'terms' => array(
899
					'author_login' => $args['author_name'],
900
				),
901
			) );
902
		}
903
904
		if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
905
			$field = $args['date_range']['field'];
906
907
			unset( $args['date_range']['field'] );
908
909
			$parser->add_filter( array(
910
				'range' => array(
911
					$field => $args['date_range'],
912
				),
913
			) );
914
		}
915
916
		if ( is_array( $args['terms'] ) ) {
917
			foreach ( $args['terms'] as $tax => $terms ) {
918
				$terms = (array) $terms;
919
920
				if ( count( $terms ) && mb_strlen( $tax ) ) {
921 View Code Duplication
					switch ( $tax ) {
922
						case 'post_tag':
923
							$tax_fld = 'tag.slug';
924
925
							break;
926
927
						case 'category':
928
							$tax_fld = 'category.slug';
929
930
							break;
931
932
						default:
933
							$tax_fld = 'taxonomy.' . $tax . '.slug';
934
935
							break;
936
					}
937
938
					foreach ( $terms as $term ) {
939
						$parser->add_filter( array(
940
							'term' => array(
941
								$tax_fld => $term,
942
							),
943
						) );
944
					}
945
				}
946
			}
947
		}
948
949
		if ( ! $args['orderby'] ) {
950
			if ( $args['query'] ) {
951
				$args['orderby'] = array( 'relevance' );
952
			} else {
953
				$args['orderby'] = array( 'date' );
954
			}
955
		}
956
957
		// Validate the "order" field
958
		switch ( strtolower( $args['order'] ) ) {
959
			case 'asc':
960
				$args['order'] = 'asc';
961
				break;
962
963
			case 'desc':
964
			default:
965
				$args['order'] = 'desc';
966
				break;
967
		}
968
969
		$es_query_args['sort'] = array();
970
971
		foreach ( (array) $args['orderby'] as $orderby ) {
972
			// Translate orderby from WP field to ES field
973
			switch ( $orderby ) {
974
				case 'relevance' :
975
					//never order by score ascending
976
					$es_query_args['sort'][] = array(
977
						'_score' => array(
978
							'order' => 'desc',
979
						),
980
					);
981
982
					break;
983
984 View Code Duplication
				case 'date' :
985
					$es_query_args['sort'][] = array(
986
						'date' => array(
987
							'order' => $args['order'],
988
						),
989
					);
990
991
					break;
992
993 View Code Duplication
				case 'ID' :
994
					$es_query_args['sort'][] = array(
995
						'id' => array(
996
							'order' => $args['order'],
997
						),
998
					);
999
1000
					break;
1001
1002
				case 'author' :
1003
					$es_query_args['sort'][] = array(
1004
						'author.raw' => array(
1005
							'order' => $args['order'],
1006
						),
1007
					);
1008
1009
					break;
1010
			} // End switch().
1011
		} // End foreach().
1012
1013
		if ( empty( $es_query_args['sort'] ) ) {
1014
			unset( $es_query_args['sort'] );
1015
		}
1016
1017
		if ( ! empty( $args['aggregations'] ) ) {
1018
			$this->add_aggregations_to_es_query_builder( $args['aggregations'], $parser );
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...
1019
		}
1020
1021
		$es_query_args['filter'] = $parser->build_filter();
1022
		$es_query_args['query']  = $parser->build_query();
1023
		$es_query_args['aggregations'] = $parser->build_aggregation();
1024
1025
		return $es_query_args;
1026
	}
1027
1028
	/**
1029
	 * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in ES
1030
	 *
1031
	 * @module search
1032
	 *
1033
	 * @param array $aggregations Array of Aggregations (filters) to add to the Jetpack_WPES_Query_Builder
1034
	 *
1035
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
1036
	 */
1037
	public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
1038
		foreach ( $aggregations as $label => $aggregation ) {
1039
			switch ( $aggregation['type'] ) {
1040
				case 'taxonomy':
1041
					$this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1042
1043
					break;
1044
1045
				case 'post_type':
1046
					$this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1047
1048
					break;
1049
1050
				case 'date_histogram':
1051
					$this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1052
1053
					break;
1054
			}
1055
		}
1056
	}
1057
1058
	/**
1059
	 * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
1060
	 *
1061
	 * @module search
1062
	 *
1063
	 * @param array $aggregation The aggregation to add to the query builder
1064
	 * @param string $label The 'label' (unique id) for this aggregation
1065
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
1066
	 */
1067
	public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1068
		$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...
1069
1070
		switch ( $aggregation['taxonomy'] ) {
1071
			case 'post_tag':
1072
				$field = 'tag';
1073
				break;
1074
1075
			case 'category':
1076
				$field = 'category';
1077
				break;
1078
1079
			default:
1080
				$field = 'taxonomy.' . $aggregation['taxonomy'];
1081
				break;
1082
		}
1083
1084
		$builder->add_aggs( $label, array(
1085
			'terms' => array(
1086
				'field' => $field . '.slug',
1087
				'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1088
			),
1089
		));
1090
	}
1091
1092
	/**
1093
	 * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
1094
	 *
1095
	 * @module search
1096
	 *
1097
	 * @param array $aggregation The aggregation to add to the query builder
1098
	 * @param string $label The 'label' (unique id) for this aggregation
1099
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
1100
	 */
1101
	public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1102
		$builder->add_aggs( $label, array(
1103
			'terms' => array(
1104
				'field' => 'post_type',
1105
				'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1106
			),
1107
		));
1108
	}
1109
1110
	/**
1111
	 * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
1112
	 *
1113
	 * @module search
1114
	 *
1115
	 * @param array $aggregation The aggregation to add to the query builder
1116
	 * @param string $label The 'label' (unique id) for this aggregation
1117
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
1118
	 */
1119
	public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1120
		$args = array(
1121
			'interval' => $aggregation['interval'],
1122
			'field'    => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' == $aggregation['field'] ) ? 'date_gmt' : 'date',
1123
		);
1124
1125
		if ( isset( $aggregation['min_doc_count'] ) ) {
1126
			$args['min_doc_count'] = intval( $aggregation['min_doc_count'] );
1127
		} else {
1128
			$args['min_doc_count'] = 1;
1129
		}
1130
1131
		$builder->add_aggs( $label, array(
1132
			'date_histogram' => $args,
1133
		));
1134
	}
1135
1136
	/**
1137
	 * And an existing filter object with a list of additional filters.
1138
	 *
1139
	 * Attempts to optimize the filters somewhat.
1140
	 *
1141
	 * @module search
1142
	 *
1143
	 * @param array $curr_filter The existing filters to build upon
1144
	 * @param array $filters The new filters to add
1145
	 *
1146
	 * @return array The resulting merged filters
1147
	 */
1148
	public static function and_es_filters( array $curr_filter, array $filters ) {
1149
		if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
1150
			if ( 1 === count( $filters ) ) {
1151
				return $filters[0];
1152
			}
1153
1154
			return array(
1155
				'and' => $filters,
1156
			);
1157
		}
1158
1159
		return array(
1160
			'and' => array_merge( array( $curr_filter ), $filters ),
1161
		);
1162
	}
1163
1164
	/**
1165
	 * Set the available filters for the search
1166
	 *
1167
	 * These get rendered via the Jetpack_Search_Widget_Filters() widget
1168
	 *
1169
	 * Behind the scenes, these are implemented using Elasticsearch Aggregations.
1170
	 *
1171
	 * If you do not require counts of how many documents match each filter, please consider using regular WP Query
1172
	 * arguments instead, such as via the jetpack_search_es_wp_query_args filter
1173
	 *
1174
	 * @module search
1175
	 *
1176
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1177
	 *
1178
	 * @param array $aggregations Array of filters (aggregations) to apply to the search
1179
	 */
1180
	public function set_filters( array $aggregations ) {
1181
		foreach ( (array) $aggregations as $key => $agg ) {
1182
			if ( empty( $agg['name'] ) ) {
1183
				$aggregations[ $key ]['name'] = $key;
1184
			}
1185
		}
1186
		$this->aggregations = $aggregations;
1187
	}
1188
1189
	/**
1190
	 * Set the search's facets (deprecated)
1191
	 *
1192
	 * @module search
1193
	 *
1194
	 * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead
1195
	 *
1196
	 * @see Jetpack_Search::set_filters()
1197
	 *
1198
	 * @param array $facets Array of facets to apply to the search
1199
	 */
1200
	public function set_facets( array $facets ) {
1201
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
1202
1203
		$this->set_filters( $facets );
1204
	}
1205
1206
	/**
1207
	 * Get the raw Aggregation results from the ES response
1208
	 *
1209
	 * @module search
1210
	 *
1211
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1212
	 *
1213
	 * @return array Array of Aggregations performed on the search
1214
	 */
1215
	public function get_search_aggregations_results() {
1216
		$aggregations = array();
1217
1218
		$search_result = $this->get_search_result();
1219
1220
		if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
1221
			$aggregations = $search_result['aggregations'];
1222
		}
1223
1224
		return $aggregations;
1225
	}
1226
1227
	/**
1228
	 * Get the raw Facet results from the ES response
1229
	 *
1230
	 * @module search
1231
	 *
1232
	 * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead
1233
	 *
1234
	 * @see Jetpack_Search::get_search_aggregations_results()
1235
	 *
1236
	 * @return array Array of Facets performed on the search
1237
	 */
1238
	public function get_search_facets() {
1239
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
1240
1241
		return $this->get_search_aggregations_results();
1242
	}
1243
1244
	/**
1245
	 * Get the results of the Filters performed, including the number of matching documents
1246
	 *
1247
	 * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
1248
	 * matching buckets, the url for applying/removing each bucket, etc.
1249
	 *
1250
	 * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
1251
	 * member if you need to access the raw filters set in Jetpack_Search::set_filters()
1252
	 *
1253
	 * @module search
1254
	 *
1255
	 * @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...
1256
	 *
1257
	 * @return array Array of Filters applied and info about them
1258
	 */
1259
	public function get_filters( WP_Query $query = null ) {
1260
		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...
1261
			global $wp_query;
1262
1263
			$query = $wp_query;
1264
		}
1265
1266
		$aggregation_data = $this->aggregations;
1267
1268
		if ( empty( $aggregation_data ) ) {
1269
			return $aggregation_data;
1270
		}
1271
1272
		$aggregation_results = $this->get_search_aggregations_results();
1273
1274
		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...
1275
			return $aggregation_data;
1276
		}
1277
1278
		// NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES
1279
		foreach ( $aggregation_results as $label => $aggregation ) {
1280
			if ( empty( $aggregation ) ) {
1281
				continue;
1282
			}
1283
1284
			$type = $this->aggregations[ $label ]['type'];
1285
1286
			$aggregation_data[ $label ]['buckets'] = array();
1287
1288
			$existing_term_slugs = array();
1289
1290
			$tax_query_var = null;
1291
1292
			// Figure out which terms are active in the query, for this taxonomy
1293
			if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
1294
				$tax_query_var = $this->get_taxonomy_query_var(  $this->aggregations[ $label ]['taxonomy'] );
1295
1296
				if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
1297
					foreach( $query->tax_query->queries as $tax_query ) {
1298
						if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
1299
						     'slug' === $tax_query['field'] &&
1300
						     is_array( $tax_query['terms'] ) ) {
1301
							$existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
1302
						}
1303
					}
1304
				}
1305
			}
1306
1307
			// Now take the resulting found aggregation items and generate the additional info about them, such as
1308
			// activation/deactivation url, name, count, etc
1309
			$buckets = array();
1310
1311
			if ( ! empty( $aggregation['buckets'] ) ) {
1312
				$buckets = (array) $aggregation['buckets'];
1313
			}
1314
1315
			if ( 'date_histogram' == $type ) {
1316
				//re-order newest to oldest
1317
				$buckets = array_reverse( $buckets );
1318
			}
1319
1320
			// Some aggregation types like date_histogram don't support the max results parameter
1321
			if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
1322
				$buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
1323
			}
1324
1325
			foreach ( $buckets as $item ) {
1326
				$query_vars = array();
1327
				$active     = false;
1328
				$remove_url = null;
1329
				$name       = '';
1330
1331
				// What type was the original aggregation?
1332
				switch ( $type ) {
1333
					case 'taxonomy':
1334
						$taxonomy = $this->aggregations[ $label ]['taxonomy'];
1335
1336
						$term = get_term_by( 'slug', $item['key'], $taxonomy );
1337
1338
						if ( ! $term || ! $tax_query_var ) {
1339
							continue 2; // switch() is considered a looping structure
1340
						}
1341
1342
						$query_vars = array(
1343
							$tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
1344
						);
1345
1346
						$name = $term->name;
1347
1348
						// Let's determine if this term is active or not
1349
1350
						if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
1351
							$active = true;
1352
1353
							$slug_count = count( $existing_term_slugs );
1354
1355
							if ( $slug_count > 1 ) {
1356
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1357
									$tax_query_var,
1358
									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...
1359
								);
1360
							} else {
1361
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( $tax_query_var );
1362
							}
1363
						}
1364
1365
						break;
1366
1367
					case 'post_type':
1368
						$post_type = get_post_type_object( $item['key'] );
1369
1370
						if ( ! $post_type || $post_type->exclude_from_search ) {
1371
							continue 2;  // switch() is considered a looping structure
1372
						}
1373
1374
						$query_vars = array(
1375
							'post_type' => $item['key'],
1376
						);
1377
1378
						$name = $post_type->labels->singular_name;
1379
1380
						// Is this post type active on this search?
1381
						$post_types = $query->get( 'post_type' );
1382
1383
						if ( ! is_array( $post_types ) ) {
1384
							$post_types = array( $post_types );
1385
						}
1386
1387
						if ( in_array( $item['key'], $post_types ) ) {
1388
							$active = true;
1389
1390
							$post_type_count = count( $post_types );
1391
1392
							// 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
1393
							if ( $post_type_count > 1 ) {
1394
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1395
									'post_type',
1396
									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...
1397
								);
1398
							} else {
1399
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
1400
							}
1401
						}
1402
1403
						break;
1404
1405
					case 'date_histogram':
1406
						$timestamp = $item['key'] / 1000;
1407
1408
						$current_year  = $query->get( 'year' );
1409
						$current_month = $query->get( 'monthnum' );
1410
						$current_day   = $query->get( 'day' );
1411
1412
						switch ( $this->aggregations[ $label ]['interval'] ) {
1413
							case 'year':
1414
								$year = (int) date( 'Y', $timestamp );
1415
1416
								$query_vars = array(
1417
									'year'     => $year,
1418
									'monthnum' => false,
1419
									'day'      => false,
1420
								);
1421
1422
								$name = $year;
1423
1424
								// Is this year currently selected?
1425
								if ( ! empty( $current_year ) && (int) $current_year === $year ) {
1426
									$active = true;
1427
1428
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
1429
								}
1430
1431
								break;
1432
1433
							case 'month':
1434
								$year  = (int) date( 'Y', $timestamp );
1435
								$month = (int) date( 'n', $timestamp );
1436
1437
								$query_vars = array(
1438
									'year'     => $year,
1439
									'monthnum' => $month,
1440
									'day'      => false,
1441
								);
1442
1443
								$name = date( 'F Y', $timestamp );
1444
1445
								// Is this month currently selected?
1446
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1447
								     ! empty( $current_month ) && (int) $current_month === $month ) {
1448
									$active = true;
1449
1450
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
1451
								}
1452
1453
								break;
1454
1455
							case 'day':
1456
								$year  = (int) date( 'Y', $timestamp );
1457
								$month = (int) date( 'n', $timestamp );
1458
								$day   = (int) date( 'j', $timestamp );
1459
1460
								$query_vars = array(
1461
									'year'     => $year,
1462
									'monthnum' => $month,
1463
									'day'      => $day,
1464
								);
1465
1466
								$name = date( 'F jS, Y', $timestamp );
1467
1468
								// Is this day currently selected?
1469
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1470
								     ! empty( $current_month ) && (int) $current_month === $month &&
1471
								     ! empty( $current_day ) && (int) $current_day === $day ) {
1472
									$active = true;
1473
1474
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
1475
								}
1476
1477
								break;
1478
1479
							default:
1480
								continue 3; // switch() is considered a looping structure
1481
						} // End switch().
1482
1483
						break;
1484
1485
					default:
1486
						//continue 2; // switch() is considered a looping structure
1487
				} // End switch().
1488
1489
				// Need to urlencode param values since add_query_arg doesn't
1490
				$url_params = urlencode_deep( $query_vars );
1491
1492
				$aggregation_data[ $label ]['buckets'][] = array(
1493
					'url'        => Jetpack_Search_Helpers::add_query_arg( $url_params ),
1494
					'query_vars' => $query_vars,
1495
					'name'       => $name,
1496
					'count'      => $item['doc_count'],
1497
					'active'     => $active,
1498
					'remove_url' => $remove_url,
1499
					'type'       => $type,
1500
					'type_label' => $aggregation_data[ $label ]['name'],
1501
					'widget_id'  => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0
1502
				);
1503
			} // End foreach().
1504
		} // End foreach().
1505
1506
		return $aggregation_data;
1507
	}
1508
1509
	/**
1510
	 * Get the results of the Facets performed
1511
	 *
1512
	 * @module search
1513
	 *
1514
	 * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead
1515
	 *
1516
	 * @see Jetpack_Search::get_filters()
1517
	 *
1518
	 * @return array $facets Array of Facets applied and info about them
1519
	 */
1520
	public function get_search_facet_data() {
1521
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
1522
1523
		return $this->get_filters();
1524
	}
1525
1526
	/**
1527
	 * Get the Filters that are currently applied to this search
1528
	 *
1529
	 * @module search
1530
	 *
1531
	 * @return array Array if Filters that were applied
1532
	 */
1533
	public function get_active_filter_buckets() {
1534
		$active_buckets = array();
1535
1536
		$filters = $this->get_filters();
1537
1538
		if ( ! is_array( $filters ) ) {
1539
			return $active_buckets;
1540
		}
1541
1542
		foreach( $filters as $filter ) {
1543
			if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
1544
				foreach( $filter['buckets'] as $item ) {
1545
					if ( isset( $item['active'] ) && $item['active'] ) {
1546
						$active_buckets[] = $item;
1547
					}
1548
				}
1549
			}
1550
		}
1551
1552
		return $active_buckets;
1553
	}
1554
1555
	/**
1556
	 * Get the Filters that are currently applied to this search
1557
	 *
1558
	 * @module search
1559
	 *
1560
	 * @return array Array if Filters that were applied
1561
	 */
1562
	public function get_current_filters() {
1563
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
1564
1565
		return $this->get_active_filter_buckets();
1566
	}
1567
1568
	/**
1569
	 * Calculate the right query var to use for a given taxonomy
1570
	 *
1571
	 * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter
1572
	 *
1573
	 * @module search
1574
	 *
1575
	 * @param string $taxonomy_name The name of the taxonomy for which to get the query var
1576
	 *
1577
	 * @return bool|string The query var to use for this taxonomy, or false if none found
1578
	 */
1579
	public function get_taxonomy_query_var( $taxonomy_name ) {
1580
		$taxonomy = get_taxonomy( $taxonomy_name );
1581
1582
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1583
			return false;
1584
		}
1585
1586
		/**
1587
		 * Modify the query var to use for a given taxonomy
1588
		 *
1589
		 * @module search
1590
		 *
1591
		 * @since 5.0.0
1592
		 *
1593
		 * @param string $query_var The current query_var for the taxonomy
1594
		 * @param string $taxonomy_name The taxonomy name
1595
		 */
1596
		return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
1597
	}
1598
1599
	/**
1600
	 * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
1601
	 * which is the input order
1602
	 *
1603
	 * Necessary because ES does not always return Aggs in the same order that you pass them in, and it should be possible
1604
	 * to control the display order easily
1605
	 *
1606
	 * @module search
1607
	 *
1608
	 * @param array $aggregations Agg results to be reordered
1609
	 * @param array $desired Array with keys representing the desired ordering
1610
	 *
1611
	 * @return array A new array with reordered keys, matching those in $desired
1612
	 */
1613
	public function fix_aggregation_ordering( array $aggregations, array $desired ) {
1614
		if ( empty( $aggregations ) || empty( $desired ) ) {
1615
			return $aggregations;
1616
		}
1617
1618
		$reordered = array();
1619
1620
		foreach( array_keys( $desired ) as $agg_name ) {
1621
			if ( isset( $aggregations[ $agg_name ] ) ) {
1622
				$reordered[ $agg_name ] = $aggregations[ $agg_name ];
1623
			}
1624
		}
1625
1626
		return $reordered;
1627
	}
1628
1629
	public function track_widget_updates( $option, $old_value, $new_value ) {
1630
		if ( 'widget_jetpack-search-filters' !== $option ) {
1631
			return;
1632
		}
1633
1634
		$event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value );
1635
		if ( ! $event ) {
1636
			return;
1637
		}
1638
1639
		jetpack_tracks_record_event(
1640
			wp_get_current_user(),
1641
			sprintf( 'jetpack_search_widget_%s', $event['action'] ),
1642
			$event['widget']
1643
		);
1644
	}
1645
}
1646