Completed
Push — update/react-create-class ( 7f3c72...120f37 )
by
unknown
257:16 queued 246:39
created

Jetpack_Search::get_search_result()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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