Completed
Push — update/search/activate-search-... ( 688144...cfafec )
by Alex
08:19
created

update_search_results_aggregations()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 2
nop 0
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Jetpack Search: Main Jetpack_Search class
4
 *
5
 * @package    Jetpack
6
 * @subpackage Jetpack Search
7
 * @since      5.0.0
8
 */
9
10
/**
11
 * The main class for the Jetpack Search module.
12
 *
13
 * @since 5.0.0
14
 */
15
class Jetpack_Search {
16
17
	/**
18
	 * The number of found posts.
19
	 *
20
	 * @since 5.0.0
21
	 *
22
	 * @var int
23
	 */
24
	protected $found_posts = 0;
25
26
	/**
27
	 * The search result, as returned by the WordPress.com REST API.
28
	 *
29
	 * @since 5.0.0
30
	 *
31
	 * @var array
32
	 */
33
	protected $search_result;
34
35
	/**
36
	 * This site's blog ID on WordPress.com.
37
	 *
38
	 * @since 5.0.0
39
	 *
40
	 * @var int
41
	 */
42
	protected $jetpack_blog_id;
43
44
	/**
45
	 * The Elasticsearch aggregations (filters).
46
	 *
47
	 * @since 5.0.0
48
	 *
49
	 * @var array
50
	 */
51
	protected $aggregations = array();
52
53
	/**
54
	 * The maximum number of aggregations allowed.
55
	 *
56
	 * @since 5.0.0
57
	 *
58
	 * @var int
59
	 */
60
	protected $max_aggregations_count = 100;
61
62
	/**
63
	 * Statistics about the last Elasticsearch query.
64
	 *
65
	 * @since 5.6.0
66
	 *
67
	 * @var array
68
	 */
69
	protected $last_query_info = array();
70
71
	/**
72
	 * Statistics about the last Elasticsearch query failure.
73
	 *
74
	 * @since 5.6.0
75
	 *
76
	 * @var array
77
	 */
78
	protected $last_query_failure_info = array();
79
80
	/**
81
	 * The singleton instance of this class.
82
	 *
83
	 * @since 5.0.0
84
	 *
85
	 * @var Jetpack_Search
86
	 */
87
	protected static $instance;
88
89
	/**
90
	 * Languages with custom analyzers. Other languages are supported, but are analyzed with the default analyzer.
91
	 *
92
	 * @since 5.0.0
93
	 *
94
	 * @var array
95
	 */
96
	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' );
97
98
	/**
99
	 * Jetpack_Search constructor.
100
	 *
101
	 * @since 5.0.0
102
	 *
103
	 * Doesn't do anything. This class needs to be initialized via the instance() method instead.
104
	 */
105
	protected function __construct() {
106
	}
107
108
	/**
109
	 * Prevent __clone()'ing of this class.
110
	 *
111
	 * @since 5.0.0
112
	 */
113
	public function __clone() {
114
		wp_die( "Please don't __clone Jetpack_Search" );
115
	}
116
117
	/**
118
	 * Prevent __wakeup()'ing of this class.
119
	 *
120
	 * @since 5.0.0
121
	 */
122
	public function __wakeup() {
123
		wp_die( "Please don't __wakeup Jetpack_Search" );
124
	}
125
126
	/**
127
	 * Get singleton instance of Jetpack_Search.
128
	 *
129
	 * Instantiates and sets up a new instance if needed, or returns the singleton.
130
	 *
131
	 * @since 5.0.0
132
	 *
133
	 * @return Jetpack_Search The Jetpack_Search singleton.
134
	 */
135
	public static function instance() {
136
		if ( ! isset( self::$instance ) ) {
137
			self::$instance = new Jetpack_Search();
138
139
			self::$instance->setup();
140
		}
141
142
		return self::$instance;
143
	}
144
145
	/**
146
	 * Perform various setup tasks for the class.
147
	 *
148
	 * Checks various pre-requisites and adds hooks.
149
	 *
150
	 * @since 5.0.0
151
	 */
152
	public function setup() {
153
		if ( ! Jetpack::is_active() || ! Jetpack::active_plan_supports( 'search' ) ) {
154
			return;
155
		}
156
157
		$this->jetpack_blog_id = Jetpack::get_option( 'id' );
158
159
		if ( ! $this->jetpack_blog_id ) {
160
			return;
161
		}
162
163
		require_once( dirname( __FILE__ ) . '/class.jetpack-search-helpers.php' );
164
		require_once( dirname( __FILE__ ) . '/class.jetpack-search-template-tags.php' );
165
166
		$this->init_hooks();
167
	}
168
169
	/**
170
	 * Setup the various hooks needed for the plugin to take over search duties.
171
	 *
172
	 * @since 5.0.0
173
	 */
174
	public function init_hooks() {
175
		if ( ! is_admin() ) {
176
			add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 );
177
178
			add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ), 10, 2 );
179
180
			add_action( 'did_jetpack_search_query', array( $this, 'store_last_query_info' ) );
181
			add_action( 'failed_jetpack_search_query', array( $this, 'store_query_failure' ) );
182
183
			add_action( 'init', array( $this, 'set_filters_from_widgets' ) );
184
185
			add_action( 'pre_get_posts', array( $this, 'maybe_add_post_type_as_var' ) );
186
		} else {
187
			add_action( 'update_option', array( $this, 'track_widget_updates' ), 10, 3 );
188
		}
189
	}
190
191
	/**
192
	 * When an Elasticsearch query fails, this stores it and enqueues some debug information in the footer.
193
	 *
194
	 * @since 5.6.0
195
	 *
196
	 * @param array $meta Information about the failure.
197
	 */
198
	public function store_query_failure( $meta ) {
199
		$this->last_query_failure_info = $meta;
200
		add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
201
	}
202
203
	/**
204
	 * Outputs information about the last Elasticsearch failure.
205
	 *
206
	 * @since 5.6.0
207
	 */
208
	public function print_query_failure() {
209
		if ( $this->last_query_failure_info ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->last_query_failure_info 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...
210
			printf(
211
				'<!-- Jetpack Search failed with code %s: %s - %s -->',
212
				esc_html( $this->last_query_failure_info['response_code'] ),
213
				esc_html( $this->last_query_failure_info['json']['error'] ),
214
				esc_html( $this->last_query_failure_info['json']['message'] )
215
			);
216
		}
217
	}
218
219
	/**
220
	 * Stores information about the last Elasticsearch query and enqueues some debug information in the footer.
221
	 *
222
	 * @since 5.6.0
223
	 *
224
	 * @param array $meta Information about the query.
225
	 */
226
	public function store_last_query_info( $meta ) {
227
		$this->last_query_info = $meta;
228
		add_action( 'wp_footer', array( $this, 'print_query_success' ) );
229
	}
230
231
	/**
232
	 * Outputs information about the last Elasticsearch search.
233
	 *
234
	 * @since 5.6.0
235
	 */
236
	public function print_query_success() {
237
		if ( $this->last_query_info ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->last_query_info 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...
238
			printf(
239
				'<!-- Jetpack Search took %s ms, ES time %s ms -->',
240
				intval( $this->last_query_info['elapsed_time'] ),
241
				esc_html( $this->last_query_info['es_time'] )
242
			);
243
		}
244
	}
245
246
	/**
247
	 * Returns the last query information, or false if no information was stored.
248
	 *
249
	 * @since 5.8.0
250
	 *
251
	 * @return bool|array
252
	 */
253
	public function get_last_query_info() {
254
		return empty( $this->last_query_info ) ? false : $this->last_query_info;
255
	}
256
257
	/**
258
	 * Returns the last query failure information, or false if no failure information was stored.
259
	 *
260
	 * @since 5.8.0
261
	 *
262
	 * @return bool|array
263
	 */
264
	public function get_last_query_failure_info() {
265
		return empty( $this->last_query_failure_info ) ? false : $this->last_query_failure_info;
266
	}
267
268
	/**
269
	 * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
270
	 * developers to disable filters supplied by the search widget. Useful if filters are
271
	 * being defined at the code level.
272
	 *
273
	 * @since      5.7.0
274
	 * @deprecated 5.8.0 Use Jetpack_Search_Helpers::are_filters_by_widget_disabled() deprecated.
275
	 *
276
	 * @return bool
277
	 */
278
	public function are_filters_by_widget_disabled() {
279
		return Jetpack_Search_Helpers::are_filters_by_widget_disabled();
280
	}
281
282
	/**
283
	 * Retrieves a list of known Jetpack search filters widget IDs, gets the filters for each widget,
284
	 * and applies those filters to this Jetpack_Search object.
285
	 *
286
	 * @since 5.7.0
287
	 */
288
	public function set_filters_from_widgets() {
289
		if ( Jetpack_Search_Helpers::are_filters_by_widget_disabled() ) {
290
			return;
291
		}
292
293
		$filters = Jetpack_Search_Helpers::get_filters_from_widgets();
294
295
		if ( ! empty( $filters ) ) {
296
			$this->set_filters( $filters );
297
		}
298
	}
299
300
	/**
301
	 * Restricts search results to certain post types via a GET argument.
302
	 *
303
	 * @since 5.8.0
304
	 *
305
	 * @param WP_Query $query A WP_Query instance.
306
	 */
307
	public function maybe_add_post_type_as_var( WP_Query $query ) {
308
		if ( $query->is_main_query() && $query->is_search && ! empty( $_GET['post_type'] ) ) {
309
			$post_types = ( is_string( $_GET['post_type'] ) && false !== strpos( $_GET['post_type'], ',' ) )
310
				? $post_type = explode( ',', $_GET['post_type'] )
311
				: (array) $_GET['post_type'];
312
			$post_types = array_map( 'sanitize_key', $post_types );
313
			$query->set( 'post_type', $post_types );
314
		}
315
	}
316
317
	/*
318
	 * Run a search on the WordPress.com public API.
319
	 *
320
	 * @since 5.0.0
321
	 *
322
	 * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint.
323
	 *
324
	 * @return object|WP_Error The response from the public API, or a WP_Error.
325
	 */
326
	public function search( array $es_args ) {
327
		$endpoint    = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
328
		$service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
329
330
		$do_authenticated_request = false;
331
332
		if ( class_exists( 'Jetpack_Client' ) &&
333
		     isset( $es_args['authenticated_request'] ) &&
334
		     true === $es_args['authenticated_request'] ) {
335
			$do_authenticated_request = true;
336
		}
337
338
		unset( $es_args['authenticated_request'] );
339
340
		$request_args = array(
341
			'headers'    => array(
342
				'Content-Type' => 'application/json',
343
			),
344
			'timeout'    => 10,
345
			'user-agent' => 'jetpack_search',
346
		);
347
348
		$request_body = wp_json_encode( $es_args );
349
350
		$start_time = microtime( true );
351
352
		if ( $do_authenticated_request ) {
353
			$request_args['method'] = 'POST';
354
355
			$request = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint, Jetpack_Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
356
		} else {
357
			$request_args = array_merge( $request_args, array(
358
				'body' => $request_body,
359
			) );
360
361
			$request = wp_remote_post( $service_url, $request_args );
362
		}
363
364
		$end_time = microtime( true );
365
366
		if ( is_wp_error( $request ) ) {
367
			return $request;
368
		}
369
370
		$response_code = wp_remote_retrieve_response_code( $request );
371
		$response      = json_decode( wp_remote_retrieve_body( $request ), true );
372
373
		$took = is_array( $response ) && ! empty( $response['took'] )
374
			? $response['took']
375
			: null;
376
377
		$query = array(
378
			'args'          => $es_args,
379
			'response'      => $response,
380
			'response_code' => $response_code,
381
			'elapsed_time'  => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms.
382
			'es_time'       => $took,
383
			'url'           => $service_url,
384
		);
385
386
		/**
387
		 * Fires after a search request has been performed.
388
		 *
389
		 * Includes the following info in the $query parameter:
390
		 *
391
		 * array args Array of Elasticsearch arguments for the search
392
		 * array response Raw API response, JSON decoded
393
		 * int response_code HTTP response code of the request
394
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
395
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
396
		 * string url API url that was queried
397
		 *
398
		 * @module search
399
		 *
400
		 * @since  5.0.0
401
		 * @since  5.8.0 This action now fires on all queries instead of just successful queries.
402
		 *
403
		 * @param array $query Array of information about the query performed
404
		 */
405
		do_action( 'did_jetpack_search_query', $query );
406
407
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
408
			/**
409
			 * Fires after a search query request has failed
410
			 *
411
			 * @module search
412
			 *
413
			 * @since  5.6.0
414
			 *
415
			 * @param array Array containing the response code and response from the failed search query
416
			 */
417
			do_action( 'failed_jetpack_search_query', array(
418
				'response_code' => $response_code,
419
				'json'          => $response,
420
			) );
421
422
			return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
423
		}
424
425
		return $response;
426
	}
427
428
	/**
429
	 * Bypass the normal Search query and offload it to Jetpack servers.
430
	 *
431
	 * This is the main hook of the plugin and is responsible for returning the posts that match the search query.
432
	 *
433
	 * @since 5.0.0
434
	 *
435
	 * @param array    $posts Current array of posts (still pre-query).
436
	 * @param WP_Query $query The WP_Query being filtered.
437
	 *
438
	 * @return array Array of matching posts.
439
	 */
440
	public function filter__posts_pre_query( $posts, $query ) {
441
		/**
442
		 * Determine whether a given WP_Query should be handled by ElasticSearch.
443
		 *
444
		 * @module search
445
		 *
446
		 * @since  5.6.0
447
		 *
448
		 * @param bool     $should_handle Should be handled by Jetpack Search.
449
		 * @param WP_Query $query         The WP_Query object.
450
		 */
451
		if ( ! apply_filters( 'jetpack_search_should_handle_query', ( $query->is_main_query() && $query->is_search() ), $query ) ) {
452
			return $posts;
453
		}
454
455
		$this->do_search( $query );
456
457
		if ( ! is_array( $this->search_result ) ) {
458
			return $posts;
459
		}
460
461
		// If no results, nothing to do
462
		if ( ! count( $this->search_result['results']['hits'] ) ) {
463
			return array();
464
		}
465
466
		$post_ids = array();
467
468
		foreach ( $this->search_result['results']['hits'] as $result ) {
469
			$post_ids[] = (int) $result['fields']['post_id'];
470
		}
471
472
		// Query all posts now
473
		$args = array(
474
			'post__in'            => $post_ids,
475
			'perm'                => 'readable',
476
			'post_type'           => 'any',
477
			'ignore_sticky_posts' => true,
478
			'suppress_filters'    => true,
479
		);
480
481
		if ( isset( $query->query_vars['order'] ) ) {
482
			$args['order'] = $query->query_vars['order'];
483
		}
484
485
		if ( isset( $query->query_vars['orderby'] ) ) {
486
			$args['orderby'] = $query->query_vars['orderby'];
487
		}
488
489
		$posts_query = new WP_Query( $args );
490
491
		// 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.
492
		$query->found_posts   = $this->found_posts;
493
		$query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
494
495
		return $posts_query->posts;
496
	}
497
498
	/**
499
	 * Build up the search, then run it against the Jetpack servers.
500
	 *
501
	 * @since 5.0.0
502
	 *
503
	 * @param WP_Query $query The original WP_Query to use for the parameters of our search.
504
	 */
505
	public function do_search( WP_Query $query ) {
506
		$page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
507
508
		// Get maximum allowed offset and posts per page values for the API.
509
		$max_offset         = Jetpack_Search_Helpers::get_max_offset();
510
		$max_posts_per_page = Jetpack_Search_Helpers::get_max_posts_per_page();
511
512
		$posts_per_page = $query->get( 'posts_per_page' );
513
		if ( $posts_per_page > $max_posts_per_page ) {
514
			$posts_per_page = $max_posts_per_page;
515
		}
516
517
		// Start building the WP-style search query args.
518
		// They'll be translated to ES format args later.
519
		$es_wp_query_args = array(
520
			'query'          => $query->get( 's' ),
521
			'posts_per_page' => $posts_per_page,
522
			'paged'          => $page,
523
			'orderby'        => $query->get( 'orderby' ),
524
			'order'          => $query->get( 'order' ),
525
		);
526
527
		if ( ! empty( $this->aggregations ) ) {
528
			$es_wp_query_args['aggregations'] = $this->aggregations;
529
		}
530
531
		// Did we query for authors?
532
		if ( $query->get( 'author_name' ) ) {
533
			$es_wp_query_args['author_name'] = $query->get( 'author_name' );
534
		}
535
536
		$es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
537
		$es_wp_query_args['terms']     = $this->get_es_wp_query_terms_for_query( $query );
538
539
		/**
540
		 * Modify the search query parameters, such as controlling the post_type.
541
		 *
542
		 * These arguments are in the format of WP_Query arguments
543
		 *
544
		 * @module search
545
		 *
546
		 * @since  5.0.0
547
		 *
548
		 * @param array    $es_wp_query_args The current query args, in WP_Query format.
549
		 * @param WP_Query $query            The original WP_Query object.
550
		 */
551
		$es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
552
553
		// If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
554
		// capped at Jetpack_Search_Helpers::get_max_offset(), so a high page would always return the last page of results otherwise.
555
		if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $max_offset ) {
556
			$query->set_404();
557
558
			return;
559
		}
560
561
		// If there were no post types returned, then 404 to avoid querying against non-public post types, which could
562
		// happen if we don't add the post type restriction to the ES query.
563
		if ( empty( $es_wp_query_args['post_type'] ) ) {
564
			$query->set_404();
565
566
			return;
567
		}
568
569
		// Convert the WP-style args into ES args.
570
		$es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
571
572
		//Only trust ES to give us IDs, not the content since it is a mirror
573
		$es_query_args['fields'] = array(
574
			'post_id',
575
		);
576
577
		/**
578
		 * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
579
		 *
580
		 * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
581
		 *
582
		 * @module search
583
		 *
584
		 * @since  5.0.0
585
		 *
586
		 * @param array    $es_query_args The raw Elasticsearch query args.
587
		 * @param WP_Query $query         The original WP_Query object.
588
		 */
589
		$es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
590
591
		// Do the actual search query!
592
		$this->search_result = $this->search( $es_query_args );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->search($es_query_args) of type object is incompatible with the declared type array of property $search_result.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
593
594
		if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
595
			$this->found_posts = 0;
596
597
			return;
598
		}
599
600
		// If we have aggregations, fix the ordering to match the input order (ES doesn't guarantee the return order).
601
		if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
602
			$this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
603
		}
604
605
		// Total number of results for paging purposes. Capped at $max_offset + $posts_per_page, as deep paging gets quite expensive.
606
		$this->found_posts = min( $this->search_result['results']['total'], $max_offset + $posts_per_page );
607
	}
608
609
	/**
610
	 * If the query has already been run before filters have been updated, then we need to re-run the query
611
	 * to get the latest aggregations.
612
	 *
613
	 * This is especially useful for supporting widget management in the customizer.
614
	 *
615
	 * @since 5.8.0
616
	 *
617
	 * @return bool Whether the query was successful or not.
618
	 */
619
	public function update_search_results_aggregations() {
620
		if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
621
			return false;
622
		}
623
624
		$es_args = $this->last_query_info['args'];
625
		$builder = new Jetpack_WPES_Query_Builder();
626
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
627
		$es_args['aggregations'] = $builder->build_aggregation();
628
629
		$this->search_result = $this->search( $es_args );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->search($es_args) of type object is incompatible with the declared type array of property $search_result.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
630
631
		return ! is_wp_error( $this->search_result );
632
	}
633
634
	/**
635
	 * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style Elasticsearch term arguments for the search.
636
	 *
637
	 * @since 5.0.0
638
	 *
639
	 * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query.
640
	 *
641
	 * @return array The new WP-style Elasticsearch arguments (that will be converted into 'real' Elasticsearch arguments).
642
	 */
643
	public function get_es_wp_query_terms_for_query( WP_Query $query ) {
644
		$args = array();
645
646
		$the_tax_query = $query->tax_query;
647
648
		if ( ! $the_tax_query ) {
649
			return $args;
650
		}
651
652
653
		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...
654
			return $args;
655
		}
656
657
		$args = array();
658
659
		foreach ( $the_tax_query->queries as $tax_query ) {
660
			// Right now we only support slugs...see note above
661
			if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
662
				continue;
663
			}
664
665
			$taxonomy = $tax_query['taxonomy'];
666
667 View Code Duplication
			if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
668
				$args[ $taxonomy ] = array();
669
			}
670
671
			$args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
672
		}
673
674
		return $args;
675
	}
676
677
	/**
678
	 * Parse out the post type from a WP_Query.
679
	 *
680
	 * Only allows post types that are not marked as 'exclude_from_search'.
681
	 *
682
	 * @since 5.0.0
683
	 *
684
	 * @param WP_Query $query Original WP_Query object.
685
	 *
686
	 * @return array Array of searchable post types corresponding to the original query.
687
	 */
688
	public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
689
		$post_types = $query->get( 'post_type' );
690
691
		// If we're searching 'any', we want to only pass searchable post types to Elasticsearch.
692
		if ( 'any' === $post_types ) {
693
			$post_types = array_values( get_post_types( array(
694
				'exclude_from_search' => false,
695
			) ) );
696
		}
697
698
		if ( ! is_array( $post_types ) ) {
699
			$post_types = array( $post_types );
700
		}
701
702
		$post_types = array_unique( $post_types );
703
704
		$sanitized_post_types = array();
705
706
		// Make sure the post types are queryable.
707
		foreach ( $post_types as $post_type ) {
708
			if ( ! $post_type ) {
709
				continue;
710
			}
711
712
			$post_type_object = get_post_type_object( $post_type );
713
			if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
714
				continue;
715
			}
716
717
			$sanitized_post_types[] = $post_type;
718
		}
719
720
		return $sanitized_post_types;
721
	}
722
723
724
	/**
725
	 * Get the Elasticsearch result.
726
	 *
727
	 * @since 5.0.0
728
	 *
729
	 * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response.
730
	 *
731
	 * @return array|bool The search results, or false if there was a failure.
732
	 */
733
	public function get_search_result( $raw = false ) {
734
		if ( $raw ) {
735
			return $this->search_result;
736
		}
737
738
		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;
739
	}
740
741
	/**
742
	 * Add the date portion of a WP_Query onto the query args.
743
	 *
744
	 * @since 5.0.0
745
	 *
746
	 * @param array    $es_wp_query_args The Elasticsearch query arguments in WordPress form.
747
	 * @param WP_Query $query            The original WP_Query.
748
	 *
749
	 * @return array The es wp query args, with date filters added (as needed).
750
	 */
751
	public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
752
		if ( $query->get( 'year' ) ) {
753
			if ( $query->get( 'monthnum' ) ) {
754
				// Padding
755
				$date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
756
757
				if ( $query->get( 'day' ) ) {
758
					// Padding
759
					$date_day = sprintf( '%02d', $query->get( 'day' ) );
760
761
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
762
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
763
				} else {
764
					$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
765
766
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
767
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
768
				}
769
			} else {
770
				$date_start = $query->get( 'year' ) . '-01-01 00:00:00';
771
				$date_end   = $query->get( 'year' ) . '-12-31 23:59:59';
772
			}
773
774
			$es_wp_query_args['date_range'] = array(
775
				'field' => 'date',
776
				'gte'   => $date_start,
777
				'lte'   => $date_end,
778
			);
779
		}
780
781
		return $es_wp_query_args;
782
	}
783
784
	/**
785
	 * Converts WP_Query style args to Elasticsearch args.
786
	 *
787
	 * @since 5.0.0
788
	 *
789
	 * @param array $args Array of WP_Query style arguments.
790
	 *
791
	 * @return array Array of ES style query arguments.
792
	 */
793
	public function convert_wp_es_to_es_args( array $args ) {
794
		jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-parser' );
795
796
		$defaults = array(
797
			'blog_id'        => get_current_blog_id(),
798
			'query'          => null,    // Search phrase
799
			'query_fields'   => array(), //list of fields to search
800
			'post_type'      => null,    // string or an array
801
			'terms'          => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) )
802
			'author'         => null,    // id or an array of ids
803
			'author_name'    => array(), // string or an array
804
			'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'
805
			'orderby'        => null,    // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
806
			'order'          => 'DESC',
807
			'posts_per_page' => 10,
808
			'offset'         => null,
809
			'paged'          => null,
810
			/**
811
			 * Aggregations. Examples:
812
			 * array(
813
			 *     'Tag'       => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
814
			 *     'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
815
			 * );
816
			 */
817
			'aggregations'   => null,
818
		);
819
820
		$args = wp_parse_args( $args, $defaults );
821
822
		$parser = new Jetpack_WPES_Search_Query_Parser( $args['query'], array( get_locale() ) );
823
824
		if ( empty( $args['query_fields'] ) ) {
825
			if ( defined( 'JETPACK_SEARCH_VIP_INDEX' ) && JETPACK_SEARCH_VIP_INDEX ) {
826
				// VIP indices do not have per language fields
827
				$match_fields        = array(
828
					'title^0.1',
829
					'content^0.1',
830
					'excerpt^0.1',
831
					'tag.name^0.1',
832
					'category.name^0.1',
833
					'author_login^0.1',
834
					'author^0.1',
835
				);
836
				$boost_fields        = array(
837
					'title^2',
838
					'tag.name',
839
					'category.name',
840
					'author_login',
841
					'author',
842
				);
843
				$boost_phrase_fields = array(
844
					'title',
845
					'content',
846
					'excerpt',
847
					'tag.name',
848
					'category.name',
849
					'author',
850
				);
851
			} else {
852
				$match_fields = $parser->merge_ml_fields(
853
					array(
854
						'title'         => 0.1,
855
						'content'       => 0.1,
856
						'excerpt'       => 0.1,
857
						'tag.name'      => 0.1,
858
						'category.name' => 0.1,
859
					),
860
					array(
861
						'author_login^0.1',
862
						'author^0.1',
863
					)
864
				);
865
866
				$boost_fields = $parser->merge_ml_fields(
867
					array(
868
						'title'         => 2,
869
						'tag.name'      => 1,
870
						'category.name' => 1,
871
					),
872
					array(
873
						'author_login',
874
						'author',
875
					)
876
				);
877
878
				$boost_phrase_fields = $parser->merge_ml_fields(
879
					array(
880
						'title'         => 1,
881
						'content'       => 1,
882
						'excerpt'       => 1,
883
						'tag.name'      => 1,
884
						'category.name' => 1,
885
					),
886
					array(
887
						'author',
888
					)
889
				);
890
			}
891
		} else {
892
			// If code is overriding the fields, then use that. Important for backwards compatibility.
893
			$match_fields        = $args['query_fields'];
894
			$boost_phrase_fields = $match_fields;
895
			$boost_fields        = null;
896
		}
897
898
		$parser->phrase_filter( array(
899
			'must_query_fields'  => $match_fields,
900
			'boost_query_fields' => null,
901
		) );
902
		$parser->remaining_query( array(
903
			'must_query_fields'  => $match_fields,
904
			'boost_query_fields' => $boost_fields,
905
		) );
906
907
		// Boost on phrase matches
908
		$parser->remaining_query( array(
909
			'boost_query_fields' => $boost_phrase_fields,
910
			'boost_query_type'   => 'phrase',
911
		) );
912
913
		/**
914
		 * Modify the recency decay parameters for the search query.
915
		 *
916
		 * The recency decay lowers the search scores based on the age of a post relative to an origin date. Basic adjustments:
917
		 *  - origin: A date. Posts with this date will have the highest score and no decay applied. Default is today.
918
		 *  - 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 offset.
919
		 *  - scale: The number of days/months/years from the origin+offset at which the decay will equal the decay param. Default 360d
920
		 *  - decay: The amount of decay applied at offset+scale. Default 0.9.
921
		 *
922
		 * The curve applied is a Gaussian. More details available at {@see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay}
923
		 *
924
		 * @module search
925
		 *
926
		 * @since  5.8.0
927
		 *
928
		 * @param array $decay_params The decay parameters.
929
		 * @param array $args         The WP query parameters.
930
		 */
931
		$decay_params = apply_filters(
932
			'jetpack_search_recency_score_decay',
933
			array(
934
				'origin' => date( 'Y-m-d' ),
935
				'scale'  => '360d',
936
				'decay'  => 0.9,
937
			),
938
			$args
939
		);
940
941
		if ( ! empty( $decay_params ) ) {
942
			// Newer content gets weighted slightly higher
943
			$parser->add_decay( 'gauss', array(
944
				'date_gmt' => $decay_params
945
			) );
946
		}
947
948
		$es_query_args = array(
949
			'blog_id' => absint( $args['blog_id'] ),
950
			'size'    => absint( $args['posts_per_page'] ),
951
		);
952
953
		// ES "from" arg (offset)
954
		if ( $args['offset'] ) {
955
			$es_query_args['from'] = absint( $args['offset'] );
956
		} elseif ( $args['paged'] ) {
957
			$es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
958
		}
959
960
		$es_query_args['from'] = min( $es_query_args['from'], Jetpack_Search_Helpers::get_max_offset() );
961
962
		if ( ! is_array( $args['author_name'] ) ) {
963
			$args['author_name'] = array( $args['author_name'] );
964
		}
965
966
		// ES stores usernames, not IDs, so transform
967
		if ( ! empty( $args['author'] ) ) {
968
			if ( ! is_array( $args['author'] ) ) {
969
				$args['author'] = array( $args['author'] );
970
			}
971
972
			foreach ( $args['author'] as $author ) {
973
				$user = get_user_by( 'id', $author );
974
975
				if ( $user && ! empty( $user->user_login ) ) {
976
					$args['author_name'][] = $user->user_login;
977
				}
978
			}
979
		}
980
981
		//////////////////////////////////////////////////
982
		// Build the filters from the query elements.
983
		// Filters rock because they are cached from one query to the next
984
		// but they are cached as individual filters, rather than all combined together.
985
		// May get performance boost by also caching the top level boolean filter too.
986
987
		if ( $args['post_type'] ) {
988
			if ( ! is_array( $args['post_type'] ) ) {
989
				$args['post_type'] = array( $args['post_type'] );
990
			}
991
992
			$parser->add_filter( array(
993
				'terms' => array(
994
					'post_type' => $args['post_type'],
995
				),
996
			) );
997
		}
998
999
		if ( $args['author_name'] ) {
1000
			$parser->add_filter( array(
1001
				'terms' => array(
1002
					'author_login' => $args['author_name'],
1003
				),
1004
			) );
1005
		}
1006
1007
		if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
1008
			$field = $args['date_range']['field'];
1009
1010
			unset( $args['date_range']['field'] );
1011
1012
			$parser->add_filter( array(
1013
				'range' => array(
1014
					$field => $args['date_range'],
1015
				),
1016
			) );
1017
		}
1018
1019
		if ( is_array( $args['terms'] ) ) {
1020
			foreach ( $args['terms'] as $tax => $terms ) {
1021
				$terms = (array) $terms;
1022
1023
				if ( count( $terms ) && mb_strlen( $tax ) ) {
1024 View Code Duplication
					switch ( $tax ) {
1025
						case 'post_tag':
1026
							$tax_fld = 'tag.slug';
1027
1028
							break;
1029
1030
						case 'category':
1031
							$tax_fld = 'category.slug';
1032
1033
							break;
1034
1035
						default:
1036
							$tax_fld = 'taxonomy.' . $tax . '.slug';
1037
1038
							break;
1039
					}
1040
1041
					foreach ( $terms as $term ) {
1042
						$parser->add_filter( array(
1043
							'term' => array(
1044
								$tax_fld => $term,
1045
							),
1046
						) );
1047
					}
1048
				}
1049
			}
1050
		}
1051
1052
		if ( ! $args['orderby'] ) {
1053
			if ( $args['query'] ) {
1054
				$args['orderby'] = array( 'relevance' );
1055
			} else {
1056
				$args['orderby'] = array( 'date' );
1057
			}
1058
		}
1059
1060
		// Validate the "order" field
1061
		switch ( strtolower( $args['order'] ) ) {
1062
			case 'asc':
1063
				$args['order'] = 'asc';
1064
				break;
1065
1066
			case 'desc':
1067
			default:
1068
				$args['order'] = 'desc';
1069
				break;
1070
		}
1071
1072
		$es_query_args['sort'] = array();
1073
1074
		foreach ( (array) $args['orderby'] as $orderby ) {
1075
			// Translate orderby from WP field to ES field
1076
			switch ( $orderby ) {
1077
				case 'relevance' :
1078
					//never order by score ascending
1079
					$es_query_args['sort'][] = array(
1080
						'_score' => array(
1081
							'order' => 'desc',
1082
						),
1083
					);
1084
1085
					break;
1086
1087 View Code Duplication
				case 'date' :
1088
					$es_query_args['sort'][] = array(
1089
						'date' => array(
1090
							'order' => $args['order'],
1091
						),
1092
					);
1093
1094
					break;
1095
1096 View Code Duplication
				case 'ID' :
1097
					$es_query_args['sort'][] = array(
1098
						'id' => array(
1099
							'order' => $args['order'],
1100
						),
1101
					);
1102
1103
					break;
1104
1105
				case 'author' :
1106
					$es_query_args['sort'][] = array(
1107
						'author.raw' => array(
1108
							'order' => $args['order'],
1109
						),
1110
					);
1111
1112
					break;
1113
			} // End switch().
1114
		} // End foreach().
1115
1116
		if ( empty( $es_query_args['sort'] ) ) {
1117
			unset( $es_query_args['sort'] );
1118
		}
1119
1120
		if ( ! empty( $args['aggregations'] ) ) {
1121
			$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...
1122
		}
1123
1124
		$es_query_args['filter']       = $parser->build_filter();
1125
		$es_query_args['query']        = $parser->build_query();
1126
		$es_query_args['aggregations'] = $parser->build_aggregation();
1127
1128
		return $es_query_args;
1129
	}
1130
1131
	/**
1132
	 * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1133
	 *
1134
	 * @since 5.0.0
1135
	 *
1136
	 * @param array                      $aggregations Array of aggregations (filters) to add to the Jetpack_WPES_Query_Builder.
1137
	 * @param Jetpack_WPES_Query_Builder $builder      The builder instance that is creating the Elasticsearch query.
1138
	 */
1139
	public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
1140
		foreach ( $aggregations as $label => $aggregation ) {
1141
			switch ( $aggregation['type'] ) {
1142
				case 'taxonomy':
1143
					$this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1144
1145
					break;
1146
1147
				case 'post_type':
1148
					$this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1149
1150
					break;
1151
1152
				case 'date_histogram':
1153
					$this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1154
1155
					break;
1156
			}
1157
		}
1158
	}
1159
1160
	/**
1161
	 * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1162
	 *
1163
	 * @since 5.0.0
1164
	 *
1165
	 * @param array                      $aggregation The aggregation to add to the query builder.
1166
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1167
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1168
	 */
1169
	public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1170
		$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...
1171
1172
		switch ( $aggregation['taxonomy'] ) {
1173
			case 'post_tag':
1174
				$field = 'tag';
1175
				break;
1176
1177
			case 'category':
1178
				$field = 'category';
1179
				break;
1180
1181
			default:
1182
				$field = 'taxonomy.' . $aggregation['taxonomy'];
1183
				break;
1184
		}
1185
1186
		$builder->add_aggs( $label, array(
1187
			'terms' => array(
1188
				'field' => $field . '.slug',
1189
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1190
			),
1191
		) );
1192
	}
1193
1194
	/**
1195
	 * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1196
	 *
1197
	 * @since 5.0.0
1198
	 *
1199
	 * @param array                      $aggregation The aggregation to add to the query builder.
1200
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1201
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1202
	 */
1203
	public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1204
		$builder->add_aggs( $label, array(
1205
			'terms' => array(
1206
				'field' => 'post_type',
1207
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1208
			),
1209
		) );
1210
	}
1211
1212
	/**
1213
	 * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1214
	 *
1215
	 * @since 5.0.0
1216
	 *
1217
	 * @param array                      $aggregation The aggregation to add to the query builder.
1218
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1219
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1220
	 */
1221
	public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1222
		$args = array(
1223
			'interval' => $aggregation['interval'],
1224
			'field'    => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' == $aggregation['field'] ) ? 'date_gmt' : 'date',
1225
		);
1226
1227
		if ( isset( $aggregation['min_doc_count'] ) ) {
1228
			$args['min_doc_count'] = intval( $aggregation['min_doc_count'] );
1229
		} else {
1230
			$args['min_doc_count'] = 1;
1231
		}
1232
1233
		$builder->add_aggs( $label, array(
1234
			'date_histogram' => $args,
1235
		) );
1236
	}
1237
1238
	/**
1239
	 * And an existing filter object with a list of additional filters.
1240
	 *
1241
	 * Attempts to optimize the filters somewhat.
1242
	 *
1243
	 * @since 5.0.0
1244
	 *
1245
	 * @param array $curr_filter The existing filters to build upon.
1246
	 * @param array $filters     The new filters to add.
1247
	 *
1248
	 * @return array The resulting merged filters.
1249
	 */
1250
	public static function and_es_filters( array $curr_filter, array $filters ) {
1251
		if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
1252
			if ( 1 === count( $filters ) ) {
1253
				return $filters[0];
1254
			}
1255
1256
			return array(
1257
				'and' => $filters,
1258
			);
1259
		}
1260
1261
		return array(
1262
			'and' => array_merge( array( $curr_filter ), $filters ),
1263
		);
1264
	}
1265
1266
	/**
1267
	 * Set the available filters for the search.
1268
	 *
1269
	 * These get rendered via the Jetpack_Search_Widget() widget.
1270
	 *
1271
	 * Behind the scenes, these are implemented using Elasticsearch Aggregations.
1272
	 *
1273
	 * If you do not require counts of how many documents match each filter, please consider using regular WP Query
1274
	 * arguments instead, such as via the jetpack_search_es_wp_query_args filter
1275
	 *
1276
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1277
	 *
1278
	 * @since  5.0.0
1279
	 *
1280
	 * @param array $aggregations Array of filters (aggregations) to apply to the search
1281
	 */
1282
	public function set_filters( array $aggregations ) {
1283
		foreach ( (array) $aggregations as $key => $agg ) {
1284
			if ( empty( $agg['name'] ) ) {
1285
				$aggregations[ $key ]['name'] = $key;
1286
			}
1287
		}
1288
		$this->aggregations = $aggregations;
1289
	}
1290
1291
	/**
1292
	 * Set the search's facets (deprecated).
1293
	 *
1294
	 * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead.
1295
	 *
1296
	 * @see        Jetpack_Search::set_filters()
1297
	 *
1298
	 * @param array $facets Array of facets to apply to the search.
1299
	 */
1300
	public function set_facets( array $facets ) {
1301
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
1302
1303
		$this->set_filters( $facets );
1304
	}
1305
1306
	/**
1307
	 * Get the raw Aggregation results from the Elasticsearch response.
1308
	 *
1309
	 * @since  5.0.0
1310
	 *
1311
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1312
	 *
1313
	 * @return array Array of Aggregations performed on the search.
1314
	 */
1315
	public function get_search_aggregations_results() {
1316
		$aggregations = array();
1317
1318
		$search_result = $this->get_search_result();
1319
1320
		if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
1321
			$aggregations = $search_result['aggregations'];
1322
		}
1323
1324
		return $aggregations;
1325
	}
1326
1327
	/**
1328
	 * Get the raw Facet results from the Elasticsearch response.
1329
	 *
1330
	 * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead.
1331
	 *
1332
	 * @see        Jetpack_Search::get_search_aggregations_results()
1333
	 *
1334
	 * @return array Array of Facets performed on the search.
1335
	 */
1336
	public function get_search_facets() {
1337
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
1338
1339
		return $this->get_search_aggregations_results();
1340
	}
1341
1342
	/**
1343
	 * Get the results of the Filters performed, including the number of matching documents.
1344
	 *
1345
	 * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
1346
	 * matching buckets, the url for applying/removing each bucket, etc.
1347
	 *
1348
	 * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
1349
	 * member if you need to access the raw filters set in Jetpack_Search::set_filters().
1350
	 *
1351
	 * @since 5.0.0
1352
	 *
1353
	 * @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...
1354
	 *
1355
	 * @return array Array of filters applied and info about them.
1356
	 */
1357
	public function get_filters( WP_Query $query = null ) {
1358
		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...
1359
			global $wp_query;
1360
1361
			$query = $wp_query;
1362
		}
1363
1364
		$aggregation_data = $this->aggregations;
1365
1366
		if ( empty( $aggregation_data ) ) {
1367
			return $aggregation_data;
1368
		}
1369
1370
		$aggregation_results = $this->get_search_aggregations_results();
1371
1372
		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...
1373
			return $aggregation_data;
1374
		}
1375
1376
		// NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES
1377
		foreach ( $aggregation_results as $label => $aggregation ) {
1378
			if ( empty( $aggregation ) ) {
1379
				continue;
1380
			}
1381
1382
			$type = $this->aggregations[ $label ]['type'];
1383
1384
			$aggregation_data[ $label ]['buckets'] = array();
1385
1386
			$existing_term_slugs = array();
1387
1388
			$tax_query_var = null;
1389
1390
			// Figure out which terms are active in the query, for this taxonomy
1391
			if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
1392
				$tax_query_var = $this->get_taxonomy_query_var( $this->aggregations[ $label ]['taxonomy'] );
1393
1394
				if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
1395
					foreach ( $query->tax_query->queries as $tax_query ) {
1396
						if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
1397
						     'slug' === $tax_query['field'] &&
1398
						     is_array( $tax_query['terms'] ) ) {
1399
							$existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
1400
						}
1401
					}
1402
				}
1403
			}
1404
1405
			// Now take the resulting found aggregation items and generate the additional info about them, such as activation/deactivation url, name, count, etc.
1406
			$buckets = array();
1407
1408
			if ( ! empty( $aggregation['buckets'] ) ) {
1409
				$buckets = (array) $aggregation['buckets'];
1410
			}
1411
1412
			if ( 'date_histogram' == $type ) {
1413
				//re-order newest to oldest
1414
				$buckets = array_reverse( $buckets );
1415
			}
1416
1417
			// Some aggregation types like date_histogram don't support the max results parameter
1418
			if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
1419
				$buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
1420
			}
1421
1422
			foreach ( $buckets as $item ) {
1423
				$query_vars = array();
1424
				$active     = false;
1425
				$remove_url = null;
1426
				$name       = '';
1427
1428
				// What type was the original aggregation?
1429
				switch ( $type ) {
1430
					case 'taxonomy':
1431
						$taxonomy = $this->aggregations[ $label ]['taxonomy'];
1432
1433
						$term = get_term_by( 'slug', $item['key'], $taxonomy );
1434
1435
						if ( ! $term || ! $tax_query_var ) {
1436
							continue 2; // switch() is considered a looping structure
1437
						}
1438
1439
						$query_vars = array(
1440
							$tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
1441
						);
1442
1443
						$name = $term->name;
1444
1445
						// Let's determine if this term is active or not
1446
1447
						if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
1448
							$active = true;
1449
1450
							$slug_count = count( $existing_term_slugs );
1451
1452 View Code Duplication
							if ( $slug_count > 1 ) {
1453
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1454
									$tax_query_var,
0 ignored issues
show
Bug introduced by
It seems like $tax_query_var can also be of type boolean; however, Jetpack_Search_Helpers::add_query_arg() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1455
									rawurlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) )
1456
								);
1457
							} else {
1458
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( $tax_query_var );
0 ignored issues
show
Bug introduced by
It seems like $tax_query_var can also be of type boolean; however, Jetpack_Search_Helpers::remove_query_arg() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1459
							}
1460
						}
1461
1462
						break;
1463
1464
					case 'post_type':
1465
						$post_type = get_post_type_object( $item['key'] );
1466
1467
						if ( ! $post_type || $post_type->exclude_from_search ) {
1468
							continue 2;  // switch() is considered a looping structure
1469
						}
1470
1471
						$query_vars = array(
1472
							'post_type' => $item['key'],
1473
						);
1474
1475
						$name = $post_type->labels->singular_name;
1476
1477
						// Is this post type active on this search?
1478
						$post_types = $query->get( 'post_type' );
1479
1480
						if ( ! is_array( $post_types ) ) {
1481
							$post_types = array( $post_types );
1482
						}
1483
1484
						if ( in_array( $item['key'], $post_types ) ) {
1485
							$active = true;
1486
1487
							$post_type_count = count( $post_types );
1488
1489
							// 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
1490 View Code Duplication
							if ( $post_type_count > 1 ) {
1491
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1492
									'post_type',
1493
									rawurlencode( implode( ',', array_diff( $post_types, array( $item['key'] ) ) ) )
1494
								);
1495
							} else {
1496
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
1497
							}
1498
						}
1499
1500
						break;
1501
1502
					case 'date_histogram':
1503
						$timestamp = $item['key'] / 1000;
1504
1505
						$current_year  = $query->get( 'year' );
1506
						$current_month = $query->get( 'monthnum' );
1507
						$current_day   = $query->get( 'day' );
1508
1509
						switch ( $this->aggregations[ $label ]['interval'] ) {
1510
							case 'year':
1511
								$year = (int) date( 'Y', $timestamp );
1512
1513
								$query_vars = array(
1514
									'year'     => $year,
1515
									'monthnum' => false,
1516
									'day'      => false,
1517
								);
1518
1519
								$name = $year;
1520
1521
								// Is this year currently selected?
1522
								if ( ! empty( $current_year ) && (int) $current_year === $year ) {
1523
									$active = true;
1524
1525
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
1526
								}
1527
1528
								break;
1529
1530
							case 'month':
1531
								$year  = (int) date( 'Y', $timestamp );
1532
								$month = (int) date( 'n', $timestamp );
1533
1534
								$query_vars = array(
1535
									'year'     => $year,
1536
									'monthnum' => $month,
1537
									'day'      => false,
1538
								);
1539
1540
								$name = date( 'F Y', $timestamp );
1541
1542
								// Is this month currently selected?
1543
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1544
								     ! empty( $current_month ) && (int) $current_month === $month ) {
1545
									$active = true;
1546
1547
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
1548
								}
1549
1550
								break;
1551
1552
							case 'day':
1553
								$year  = (int) date( 'Y', $timestamp );
1554
								$month = (int) date( 'n', $timestamp );
1555
								$day   = (int) date( 'j', $timestamp );
1556
1557
								$query_vars = array(
1558
									'year'     => $year,
1559
									'monthnum' => $month,
1560
									'day'      => $day,
1561
								);
1562
1563
								$name = date( 'F jS, Y', $timestamp );
1564
1565
								// Is this day currently selected?
1566
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1567
								     ! empty( $current_month ) && (int) $current_month === $month &&
1568
								     ! empty( $current_day ) && (int) $current_day === $day ) {
1569
									$active = true;
1570
1571
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
1572
								}
1573
1574
								break;
1575
1576
							default:
1577
								continue 3; // switch() is considered a looping structure
1578
						} // End switch().
1579
1580
						break;
1581
1582
					default:
1583
						//continue 2; // switch() is considered a looping structure
1584
				} // End switch().
1585
1586
				// Need to urlencode param values since add_query_arg doesn't
1587
				$url_params = urlencode_deep( $query_vars );
1588
1589
				$aggregation_data[ $label ]['buckets'][] = array(
1590
					'url'        => Jetpack_Search_Helpers::add_query_arg( $url_params ),
1591
					'query_vars' => $query_vars,
1592
					'name'       => $name,
1593
					'count'      => $item['doc_count'],
1594
					'active'     => $active,
1595
					'remove_url' => $remove_url,
1596
					'type'       => $type,
1597
					'type_label' => $aggregation_data[ $label ]['name'],
1598
					'widget_id'  => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0
1599
				);
1600
			} // End foreach().
1601
		} // End foreach().
1602
1603
		return $aggregation_data;
1604
	}
1605
1606
	/**
1607
	 * Get the results of the facets performed.
1608
	 *
1609
	 * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead.
1610
	 *
1611
	 * @see        Jetpack_Search::get_filters()
1612
	 *
1613
	 * @return array $facets Array of facets applied and info about them.
1614
	 */
1615
	public function get_search_facet_data() {
1616
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
1617
1618
		return $this->get_filters();
1619
	}
1620
1621
	/**
1622
	 * Get the filters that are currently applied to this search.
1623
	 *
1624
	 * @since 5.0.0
1625
	 *
1626
	 * @return array Array of filters that were applied.
1627
	 */
1628
	public function get_active_filter_buckets() {
1629
		$active_buckets = array();
1630
1631
		$filters = $this->get_filters();
1632
1633
		if ( ! is_array( $filters ) ) {
1634
			return $active_buckets;
1635
		}
1636
1637
		foreach ( $filters as $filter ) {
1638
			if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
1639
				foreach ( $filter['buckets'] as $item ) {
1640
					if ( isset( $item['active'] ) && $item['active'] ) {
1641
						$active_buckets[] = $item;
1642
					}
1643
				}
1644
			}
1645
		}
1646
1647
		return $active_buckets;
1648
	}
1649
1650
	/**
1651
	 * Get the filters that are currently applied to this search.
1652
	 *
1653
	 * @deprecated 5.0 Please use Jetpack_Search::get_active_filter_buckets() instead.
1654
	 *
1655
	 * @see        Jetpack_Search::get_active_filter_buckets()
1656
	 *
1657
	 * @return array Array of filters that were applied.
1658
	 */
1659
	public function get_current_filters() {
1660
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
1661
1662
		return $this->get_active_filter_buckets();
1663
	}
1664
1665
	/**
1666
	 * Calculate the right query var to use for a given taxonomy.
1667
	 *
1668
	 * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter.
1669
	 *
1670
	 * @since 5.0.0
1671
	 *
1672
	 * @param string $taxonomy_name The name of the taxonomy for which to get the query var.
1673
	 *
1674
	 * @return bool|string The query var to use for this taxonomy, or false if none found.
1675
	 */
1676
	public function get_taxonomy_query_var( $taxonomy_name ) {
1677
		$taxonomy = get_taxonomy( $taxonomy_name );
1678
1679
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1680
			return false;
1681
		}
1682
1683
		/**
1684
		 * Modify the query var to use for a given taxonomy
1685
		 *
1686
		 * @module search
1687
		 *
1688
		 * @since  5.0.0
1689
		 *
1690
		 * @param string $query_var     The current query_var for the taxonomy
1691
		 * @param string $taxonomy_name The taxonomy name
1692
		 */
1693
		return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
1694
	}
1695
1696
	/**
1697
	 * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
1698
	 * which is the input order.
1699
	 *
1700
	 * Necessary because ES does not always return aggregations in the same order that you pass them in,
1701
	 * and it should be possible to control the display order easily.
1702
	 *
1703
	 * @since 5.0.0
1704
	 *
1705
	 * @param array $aggregations Aggregation results to be reordered.
1706
	 * @param array $desired      Array with keys representing the desired ordering.
1707
	 *
1708
	 * @return array A new array with reordered keys, matching those in $desired.
1709
	 */
1710
	public function fix_aggregation_ordering( array $aggregations, array $desired ) {
1711
		if ( empty( $aggregations ) || empty( $desired ) ) {
1712
			return $aggregations;
1713
		}
1714
1715
		$reordered = array();
1716
1717
		foreach ( array_keys( $desired ) as $agg_name ) {
1718
			if ( isset( $aggregations[ $agg_name ] ) ) {
1719
				$reordered[ $agg_name ] = $aggregations[ $agg_name ];
1720
			}
1721
		}
1722
1723
		return $reordered;
1724
	}
1725
1726
	/**
1727
	 * Sends events to Tracks when a search filters widget is updated.
1728
	 *
1729
	 * @since 5.8.0
1730
	 *
1731
	 * @param string $option    The option name. Only "widget_jetpack-search-filters" is cared about.
1732
	 * @param array  $old_value The old option value.
1733
	 * @param array  $new_value The new option value.
1734
	 */
1735
	public function track_widget_updates( $option, $old_value, $new_value ) {
1736
		if ( 'widget_jetpack-search-filters' !== $option ) {
1737
			return;
1738
		}
1739
1740
		$event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value );
1741
		if ( ! $event ) {
1742
			return;
1743
		}
1744
1745
		jetpack_tracks_record_event(
1746
			wp_get_current_user(),
1747
			sprintf( 'jetpack_search_widget_%s', $event['action'] ),
1748
			$event['widget']
1749
		);
1750
	}
1751
}
1752