Completed
Push — update/search-remove-front-bul... ( b294db...7a00b0 )
by
unknown
27:32 queued 19:15
created

Jetpack_Search::store_query_success()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
class Jetpack_Search {
4
5
	protected $found_posts = 0;
6
7
	/**
8
	 * The maximum offset ('from' param), since deep pages get exponentially slower.
9
	 *
10
	 * @see https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html
11
	 */
12
	protected $max_offset = 200;
13
14
	protected $search_result;
15
16
	protected $original_blog_id;
17
	protected $jetpack_blog_id;
18
19
	protected $aggregations = array();
20
	protected $max_aggregations_count = 100;
21
22
	// used to output query meta into page
23
	protected $last_query_info;
24
	protected $last_query_failure_info;
25
26
	protected static $instance;
27
28
	//Languages with custom analyzers, other languages are supported,
29
	// but are analyzed with the default analyzer.
30
	public static $analyzed_langs = array( 'ar', 'bg', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'eu', 'fa', 'fi', 'fr', 'he', 'hi', 'hu', 'hy', 'id', 'it', 'ja', 'ko', 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' );
31
32
	protected function __construct() {
33
		/* Don't do anything, needs to be initialized via instance() method */
34
	}
35
36
	public function __clone() {
37
		wp_die( "Please don't __clone Jetpack_Search" );
38
	}
39
40
	public function __wakeup() {
41
		wp_die( "Please don't __wakeup Jetpack_Search" );
42
	}
43
44
	/**
45
	 * Get singleton instance of Jetpack_Search
46
	 *
47
	 * Instantiates and sets up a new instance if needed, or returns the singleton
48
	 *
49
	 * @module search
50
	 *
51
	 * @return Jetpack_Search The Jetpack_Search singleton
52
	 */
53
	public static function instance() {
54
		if ( ! isset( self::$instance ) ) {
55
			self::$instance = new Jetpack_Search();
56
57
			self::$instance->setup();
58
		}
59
60
		return self::$instance;
61
	}
62
63
	/**
64
	 * Perform various setup tasks for the class
65
	 *
66
	 * Checks various pre-requisites and adds hooks
67
	 *
68
	 * @module search
69
	 */
70
	public function setup() {
71
		if ( ! Jetpack::is_active() || ! Jetpack::active_plan_supports( 'search' ) ) {
72
			return;
73
		}
74
75
		$this->jetpack_blog_id = Jetpack::get_option( 'id' );
76
77
		if ( ! $this->jetpack_blog_id ) {
78
			return;
79
		}
80
81
		require_once( dirname( __FILE__ ) . '/class.jetpack-search-helpers.php' );
82
83
		$this->init_hooks();
84
	}
85
86
	/**
87
	 * Setup the various hooks needed for the plugin to take over Search duties
88
	 *
89
	 * @module search
90
	 */
91
	public function init_hooks() {
92
		add_action( 'widgets_init', array( $this, 'action__widgets_init' ) );
93
94
		if ( ! is_admin() ) {
95
			add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 );
96
97
			add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ),  10, 2 );
98
99
			add_action( 'did_jetpack_search_query', array( $this, 'store_query_success' ) );
100
			add_action( 'failed_jetpack_search_query', array( $this, 'store_query_failure' ) );
101
102
			add_action( 'init', array( $this, 'set_filters_from_widgets' ) );
103
104
			add_action( 'pre_get_posts', array( $this, 'maybe_add_post_type_as_var' ) );
105
		} else {
106
			add_action( 'update_option', array( $this, 'track_widget_updates' ), 10, 3 );
107
		}
108
	}
109
110
	/**
111
	 * Print query info as a HTML comment in the footer
112
	 */
113
114
	public function store_query_failure( $meta ) {
115
		$this->last_query_failure_info = $meta;
116
		add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
117
	}
118
119
	public function print_query_failure() {
120
		if ( $this->last_query_failure_info ) {
121
			printf(
122
				'<!-- Jetpack Search failed with code %s: %s - %s -->',
123
				esc_html( $this->last_query_failure_info['response_code'] ),
124
				esc_html( $this->last_query_failure_info['json']['error'] ),
125
				esc_html( $this->last_query_failure_info['json']['message'] )
126
			);
127
		}
128
	}
129
130
	public function store_query_success( $meta ) {
131
		$this->last_query_info = $meta;
132
		add_action( 'wp_footer', array( $this, 'print_query_success' ) );
133
	}
134
135
	public function print_query_success() {
136
		if ( $this->last_query_info ) {
137
			printf(
138
				'<!-- Jetpack Search took %s ms, ES time %s ms -->',
139
				intval( $this->last_query_info['elapsed_time'] ),
140
				esc_html( $this->last_query_info['es_time'] )
141
			);
142
		}
143
	}
144
145
	function are_filters_by_widget_disabled() {
146
		/**
147
		 * Allows developers to disable filters being set by widget, in favor of manually
148
		 * setting filters via `Jetpack_Search::set_filters()`.
149
		 *
150
		 * @module search
151
		 *
152
		 * @since 5.7.0
153
		 *
154
		 * @param bool false
155
		 */
156
		return apply_filters( 'jetpack_search_disable_widget_filters', false );
157
	}
158
159
	/**
160
	 * Retrieves a list of known Jetpack search filters widget IDs, gets the filters for each widget,
161
	 * and applies those filters to this Jetpack_Search object.
162
	 *
163
	 * @since 5.7.0
164
	 *
165
	 * @return void
166
	 */
167
	function set_filters_from_widgets() {
168
		if ( $this->are_filters_by_widget_disabled() ) {
169
			return;
170
		}
171
172
		$filters = Jetpack_Search_Helpers::get_filters_from_widgets();
173
174
		if ( ! empty( $filters ) ) {
175
			$this->set_filters( $filters );
176
		}
177
	}
178
179
	function maybe_add_post_type_as_var( WP_Query $query ) {
180
		if ( $query->is_main_query() && $query->is_search && ! empty( $_GET['post_type'] ) ) {
181
			$post_types = ( is_string( $_GET['post_type'] ) && false !== strpos( $_GET['post_type'], ',' ) )
182
				? $post_type = explode( ',', $_GET['post_type'] )
183
				: (array) $_GET['post_type'];
184
			$post_types = array_map( 'sanitize_key', $post_types );
185
			$query->set('post_type', $post_types );
186
		}
187
	}
188
189
	/*
190
	 * Run a search on the WP.com public API.
191
	 *
192
	 * @module search
193
	 *
194
	 * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint
195
	 *
196
	 * @return object|WP_Error The response from the public api, or a WP_Error
197
	 */
198
	public function search( array $es_args ) {
199
		$endpoint    = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
200
		$service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
201
202
		$do_authenticated_request = false;
203
204
		if ( class_exists( 'Jetpack_Client' ) &&
205
			isset( $es_args['authenticated_request'] ) &&
206
			true === $es_args['authenticated_request'] ) {
207
			$do_authenticated_request = true;
208
		}
209
210
		unset( $es_args['authenticated_request'] );
211
212
		$request_args = array(
213
			'headers' => array(
214
				'Content-Type' => 'application/json',
215
			),
216
			'timeout'    => 10,
217
			'user-agent' => 'jetpack_search',
218
		);
219
220
		$request_body = json_encode( $es_args );
221
222
		$start_time = microtime( true );
223
224
		if ( $do_authenticated_request ) {
225
			$request_args['method'] = 'POST';
226
227
			$request = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint, Jetpack_Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
228
		} else {
229
			$request_args = array_merge( $request_args, array(
230
				'body' => $request_body,
231
			) );
232
233
			$request = wp_remote_post( $service_url, $request_args );
234
		}
235
236
		$end_time = microtime( true );
237
238
		if ( is_wp_error( $request ) ) {
239
			return $request;
240
		}
241
242
		$response_code = wp_remote_retrieve_response_code( $request );
243
		$response = json_decode( wp_remote_retrieve_body( $request ), true );
244
245
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
246
			/**
247
			 * Fires after a search query request has failed
248
			 *
249
			 * @module search
250
			 *
251
			 * @since 5.6.0
252
			 *
253
			 * @param array Array containing the response code and response from the failed search query
254
			 */
255
			do_action( 'failed_jetpack_search_query', array( 'response_code' => $response_code, 'json' => $response ) );
256
			return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
257
		}
258
259
		$took = is_array( $response ) && $response['took'] ? $response['took'] : null;
260
261
		$query = array(
262
			'args'          => $es_args,
263
			'response'      => $response,
264
			'response_code' => $response_code,
265
			'elapsed_time'   => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms
266
			'es_time'       => $took,
267
			'url'           => $service_url,
268
		);
269
270
		/**
271
		 * Fires after a search request has been performed
272
		 *
273
		 * Includes the following info in the $query parameter:
274
		 *
275
		 * array args Array of Elasticsearch arguments for the search
276
		 * array response Raw API response, JSON decoded
277
		 * int response_code HTTP response code of the request
278
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
279
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
280
		 * string url API url that was queried
281
		 *
282
		 * @module search
283
		 *
284
		 * @since 5.0.0
285
		 *
286
		 * @param array $query Array of information about the query performed
287
		 */
288
		do_action( 'did_jetpack_search_query', $query );
289
290
		return $response;
291
	}
292
293
	/**
294
	 * Bypass the normal Search query and offload it to Jetpack servers
295
	 *
296
	 * This is the main hook of the plugin and is responsible for returning the posts that match the search query
297
	 *
298
	 * @module search
299
	 *
300
	 * @param array $posts Current array of posts (still pre-query)
301
	 * @param WP_Query $query The WP_Query being filtered
302
	 *
303
	 * @return array Array of matching posts
304
	 */
305
	public function filter__posts_pre_query( $posts, $query ) {
306
		/**
307
		 * Determine whether a given WP_Query should be handled by ElasticSearch
308
		 *
309
		 * @module search
310
		 *
311
		 * @since 5.6.0
312
		 * @param bool $should_handle Should be handled by Jetpack Search
313
		 * @param WP_Query $query The wp_query object
314
		 */
315
		if ( ! apply_filters( 'jetpack_search_should_handle_query', ( $query->is_main_query() && $query->is_search() ), $query ) ) {
316
			return $posts;
317
		}
318
319
		$this->do_search( $query );
320
321
		if ( ! is_array( $this->search_result ) ) {
322
			return $posts;
323
		}
324
325
		// If no results, nothing to do
326
		if ( ! count( $this->search_result['results']['hits'] ) ) {
327
			return array();
328
		}
329
330
		$post_ids = array();
331
332
		foreach ( $this->search_result['results']['hits'] as $result ) {
333
			$post_ids[] = (int) $result['fields']['post_id'];
334
		}
335
336
		// Query all posts now
337
		$args = array(
338
			'post__in'            => $post_ids,
339
			'perm'                => 'readable',
340
			'post_type'           => 'any',
341
			'ignore_sticky_posts' => true,
342
		);
343
344
		if ( isset( $query->query_vars['order'] ) ) {
345
			$args['order'] = $query->query_vars['order'];
346
		}
347
348
		if ( isset( $query->query_vars['orderby'] ) ) {
349
			$args['orderby'] = $query->query_vars['orderby'];
350
		}
351
352
		$posts_query = new WP_Query( $args );
353
354
		// 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.
355
		$query->found_posts   = $this->found_posts;
356
		$query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
357
358
		return $posts_query->posts;
359
	}
360
361
	/**
362
	 * Build up the search, then run it against the Jetpack servers
363
	 *
364
	 * @param WP_Query $query The original WP_Query to use for the parameters of our search
365
	 */
366
	public function do_search( WP_Query $query ) {
367
		$page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
368
369
		$posts_per_page = $query->get( 'posts_per_page' );
370
371
		// ES API does not allow more than 15 results at a time
372
		if ( $posts_per_page > 15 ) {
373
			$posts_per_page = 15;
374
		}
375
376
		// Start building the WP-style search query args
377
		// They'll be translated to ES format args later
378
		$es_wp_query_args = array(
379
			'query'          => $query->get( 's' ),
380
			'posts_per_page' => $posts_per_page,
381
			'paged'          => $page,
382
			'orderby'        => $query->get( 'orderby' ),
383
			'order'          => $query->get( 'order' ),
384
		);
385
386
		if ( ! empty( $this->aggregations ) ) {
387
			$es_wp_query_args['aggregations'] = $this->aggregations;
388
		}
389
390
		// Did we query for authors?
391
		if ( $query->get( 'author_name' ) ) {
392
			$es_wp_query_args['author_name'] = $query->get( 'author_name' );
393
		}
394
395
		$es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
396
397
		$es_wp_query_args['terms']     = $this->get_es_wp_query_terms_for_query( $query );
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 5 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

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