Completed
Push — fix/search/deactive-widget-on-... ( 2ff184 )
by Alex
20:59 queued 11:28
created

Jetpack_Search::get_search_facets()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
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
		add_action( 'jetpack_deactivate_module_search', array( $this, 'move_search_widgets_to_inactive' ) );
191
	}
192
193
	/**
194
	 * When an Elasticsearch query fails, this stores it and enqueues some debug information in the footer.
195
	 *
196
	 * @since 5.6.0
197
	 *
198
	 * @param array $meta Information about the failure.
199
	 */
200
	public function store_query_failure( $meta ) {
201
		$this->last_query_failure_info = $meta;
202
		add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
203
	}
204
205
	/**
206
	 * Outputs information about the last Elasticsearch failure.
207
	 *
208
	 * @since 5.6.0
209
	 */
210
	public function print_query_failure() {
211
		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...
212
			printf(
213
				'<!-- Jetpack Search failed with code %s: %s - %s -->',
214
				esc_html( $this->last_query_failure_info['response_code'] ),
215
				esc_html( $this->last_query_failure_info['json']['error'] ),
216
				esc_html( $this->last_query_failure_info['json']['message'] )
217
			);
218
		}
219
	}
220
221
	/**
222
	 * Stores information about the last Elasticsearch query and enqueues some debug information in the footer.
223
	 *
224
	 * @since 5.6.0
225
	 *
226
	 * @param array $meta Information about the query.
227
	 */
228
	public function store_last_query_info( $meta ) {
229
		$this->last_query_info = $meta;
230
		add_action( 'wp_footer', array( $this, 'print_query_success' ) );
231
	}
232
233
	/**
234
	 * Outputs information about the last Elasticsearch search.
235
	 *
236
	 * @since 5.6.0
237
	 */
238
	public function print_query_success() {
239
		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...
240
			printf(
241
				'<!-- Jetpack Search took %s ms, ES time %s ms -->',
242
				intval( $this->last_query_info['elapsed_time'] ),
243
				esc_html( $this->last_query_info['es_time'] )
244
			);
245
		}
246
	}
247
248
	/**
249
	 * Returns the last query information, or false if no information was stored.
250
	 *
251
	 * @since 5.8.0
252
	 *
253
	 * @return bool|array
254
	 */
255
	public function get_last_query_info() {
256
		return empty( $this->last_query_info ) ? false : $this->last_query_info;
257
	}
258
259
	/**
260
	 * Returns the last query failure information, or false if no failure information was stored.
261
	 *
262
	 * @since 5.8.0
263
	 *
264
	 * @return bool|array
265
	 */
266
	public function get_last_query_failure_info() {
267
		return empty( $this->last_query_failure_info ) ? false : $this->last_query_failure_info;
268
	}
269
270
	/**
271
	 * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
272
	 * developers to disable filters supplied by the search widget. Useful if filters are
273
	 * being defined at the code level.
274
	 *
275
	 * @since      5.7.0
276
	 * @deprecated 5.8.0 Use Jetpack_Search_Helpers::are_filters_by_widget_disabled() directly.
277
	 *
278
	 * @return bool
279
	 */
280
	public function are_filters_by_widget_disabled() {
281
		return Jetpack_Search_Helpers::are_filters_by_widget_disabled();
282
	}
283
284
	/**
285
	 * Retrieves a list of known Jetpack search filters widget IDs, gets the filters for each widget,
286
	 * and applies those filters to this Jetpack_Search object.
287
	 *
288
	 * @since 5.7.0
289
	 */
290
	public function set_filters_from_widgets() {
291
		if ( Jetpack_Search_Helpers::are_filters_by_widget_disabled() ) {
292
			return;
293
		}
294
295
		$filters = Jetpack_Search_Helpers::get_filters_from_widgets();
296
297
		if ( ! empty( $filters ) ) {
298
			$this->set_filters( $filters );
299
		}
300
	}
301
302
	/**
303
	 * Restricts search results to certain post types via a GET argument.
304
	 *
305
	 * @since 5.8.0
306
	 *
307
	 * @param WP_Query $query A WP_Query instance.
308
	 */
309
	public function maybe_add_post_type_as_var( WP_Query $query ) {
310
		if ( $query->is_main_query() && $query->is_search && ! empty( $_GET['post_type'] ) ) {
311
			$post_types = ( is_string( $_GET['post_type'] ) && false !== strpos( $_GET['post_type'], ',' ) )
312
				? $post_type = explode( ',', $_GET['post_type'] )
313
				: (array) $_GET['post_type'];
314
			$post_types = array_map( 'sanitize_key', $post_types );
315
			$query->set( 'post_type', $post_types );
316
		}
317
	}
318
319
	/*
320
	 * Run a search on the WordPress.com public API.
321
	 *
322
	 * @since 5.0.0
323
	 *
324
	 * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint.
325
	 *
326
	 * @return object|WP_Error The response from the public API, or a WP_Error.
327
	 */
328
	public function search( array $es_args ) {
329
		$endpoint    = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
330
		$service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
331
332
		$do_authenticated_request = false;
333
334
		if ( class_exists( 'Jetpack_Client' ) &&
335
		     isset( $es_args['authenticated_request'] ) &&
336
		     true === $es_args['authenticated_request'] ) {
337
			$do_authenticated_request = true;
338
		}
339
340
		unset( $es_args['authenticated_request'] );
341
342
		$request_args = array(
343
			'headers'    => array(
344
				'Content-Type' => 'application/json',
345
			),
346
			'timeout'    => 10,
347
			'user-agent' => 'jetpack_search',
348
		);
349
350
		$request_body = wp_json_encode( $es_args );
351
352
		$start_time = microtime( true );
353
354
		if ( $do_authenticated_request ) {
355
			$request_args['method'] = 'POST';
356
357
			$request = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint, Jetpack_Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
358
		} else {
359
			$request_args = array_merge( $request_args, array(
360
				'body' => $request_body,
361
			) );
362
363
			$request = wp_remote_post( $service_url, $request_args );
364
		}
365
366
		$end_time = microtime( true );
367
368
		if ( is_wp_error( $request ) ) {
369
			return $request;
370
		}
371
372
		$response_code = wp_remote_retrieve_response_code( $request );
373
		$response      = json_decode( wp_remote_retrieve_body( $request ), true );
374
375
		$took = is_array( $response ) && ! empty( $response['took'] )
376
			? $response['took']
377
			: null;
378
379
		$query = array(
380
			'args'          => $es_args,
381
			'response'      => $response,
382
			'response_code' => $response_code,
383
			'elapsed_time'  => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms.
384
			'es_time'       => $took,
385
			'url'           => $service_url,
386
		);
387
388
		/**
389
		 * Fires after a search request has been performed.
390
		 *
391
		 * Includes the following info in the $query parameter:
392
		 *
393
		 * array args Array of Elasticsearch arguments for the search
394
		 * array response Raw API response, JSON decoded
395
		 * int response_code HTTP response code of the request
396
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
397
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
398
		 * string url API url that was queried
399
		 *
400
		 * @module search
401
		 *
402
		 * @since  5.0.0
403
		 * @since  5.8.0 This action now fires on all queries instead of just successful queries.
404
		 *
405
		 * @param array $query Array of information about the query performed
406
		 */
407
		do_action( 'did_jetpack_search_query', $query );
408
409
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
410
			/**
411
			 * Fires after a search query request has failed
412
			 *
413
			 * @module search
414
			 *
415
			 * @since  5.6.0
416
			 *
417
			 * @param array Array containing the response code and response from the failed search query
418
			 */
419
			do_action( 'failed_jetpack_search_query', array(
420
				'response_code' => $response_code,
421
				'json'          => $response,
422
			) );
423
424
			return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
425
		}
426
427
		return $response;
428
	}
429
430
	/**
431
	 * Bypass the normal Search query and offload it to Jetpack servers.
432
	 *
433
	 * This is the main hook of the plugin and is responsible for returning the posts that match the search query.
434
	 *
435
	 * @since 5.0.0
436
	 *
437
	 * @param array    $posts Current array of posts (still pre-query).
438
	 * @param WP_Query $query The WP_Query being filtered.
439
	 *
440
	 * @return array Array of matching posts.
441
	 */
442
	public function filter__posts_pre_query( $posts, $query ) {
443
		/**
444
		 * Determine whether a given WP_Query should be handled by ElasticSearch.
445
		 *
446
		 * @module search
447
		 *
448
		 * @since  5.6.0
449
		 *
450
		 * @param bool     $should_handle Should be handled by Jetpack Search.
451
		 * @param WP_Query $query         The WP_Query object.
452
		 */
453
		if ( ! apply_filters( 'jetpack_search_should_handle_query', ( $query->is_main_query() && $query->is_search() ), $query ) ) {
454
			return $posts;
455
		}
456
457
		$this->do_search( $query );
458
459
		if ( ! is_array( $this->search_result ) ) {
460
			return $posts;
461
		}
462
463
		// If no results, nothing to do
464
		if ( ! count( $this->search_result['results']['hits'] ) ) {
465
			return array();
466
		}
467
468
		$post_ids = array();
469
470
		foreach ( $this->search_result['results']['hits'] as $result ) {
471
			$post_ids[] = (int) $result['fields']['post_id'];
472
		}
473
474
		// Query all posts now
475
		$args = array(
476
			'post__in'            => $post_ids,
477
			'perm'                => 'readable',
478
			'post_type'           => 'any',
479
			'ignore_sticky_posts' => true,
480
			'suppress_filters'    => true,
481
		);
482
483
		if ( isset( $query->query_vars['order'] ) ) {
484
			$args['order'] = $query->query_vars['order'];
485
		}
486
487
		if ( isset( $query->query_vars['orderby'] ) ) {
488
			$args['orderby'] = $query->query_vars['orderby'];
489
		}
490
491
		$posts_query = new WP_Query( $args );
492
493
		// 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.
494
		$query->found_posts   = $this->found_posts;
495
		$query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
496
497
		return $posts_query->posts;
498
	}
499
500
	/**
501
	 * Build up the search, then run it against the Jetpack servers.
502
	 *
503
	 * @since 5.0.0
504
	 *
505
	 * @param WP_Query $query The original WP_Query to use for the parameters of our search.
506
	 */
507
	public function do_search( WP_Query $query ) {
508
		$page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
509
510
		// Get maximum allowed offset and posts per page values for the API.
511
		$max_offset         = Jetpack_Search_Helpers::get_max_offset();
512
		$max_posts_per_page = Jetpack_Search_Helpers::get_max_posts_per_page();
513
514
		$posts_per_page = $query->get( 'posts_per_page' );
515
		if ( $posts_per_page > $max_posts_per_page ) {
516
			$posts_per_page = $max_posts_per_page;
517
		}
518
519
		// Start building the WP-style search query args.
520
		// They'll be translated to ES format args later.
521
		$es_wp_query_args = array(
522
			'query'          => $query->get( 's' ),
523
			'posts_per_page' => $posts_per_page,
524
			'paged'          => $page,
525
			'orderby'        => $query->get( 'orderby' ),
526
			'order'          => $query->get( 'order' ),
527
		);
528
529
		if ( ! empty( $this->aggregations ) ) {
530
			$es_wp_query_args['aggregations'] = $this->aggregations;
531
		}
532
533
		// Did we query for authors?
534
		if ( $query->get( 'author_name' ) ) {
535
			$es_wp_query_args['author_name'] = $query->get( 'author_name' );
536
		}
537
538
		$es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
539
		$es_wp_query_args['terms']     = $this->get_es_wp_query_terms_for_query( $query );
540
541
		/**
542
		 * Modify the search query parameters, such as controlling the post_type.
543
		 *
544
		 * These arguments are in the format of WP_Query arguments
545
		 *
546
		 * @module search
547
		 *
548
		 * @since  5.0.0
549
		 *
550
		 * @param array    $es_wp_query_args The current query args, in WP_Query format.
551
		 * @param WP_Query $query            The original WP_Query object.
552
		 */
553
		$es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
554
555
		// If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
556
		// capped at Jetpack_Search_Helpers::get_max_offset(), so a high page would always return the last page of results otherwise.
557
		if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $max_offset ) {
558
			$query->set_404();
559
560
			return;
561
		}
562
563
		// If there were no post types returned, then 404 to avoid querying against non-public post types, which could
564
		// happen if we don't add the post type restriction to the ES query.
565
		if ( empty( $es_wp_query_args['post_type'] ) ) {
566
			$query->set_404();
567
568
			return;
569
		}
570
571
		// Convert the WP-style args into ES args.
572
		$es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
573
574
		//Only trust ES to give us IDs, not the content since it is a mirror
575
		$es_query_args['fields'] = array(
576
			'post_id',
577
		);
578
579
		/**
580
		 * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
581
		 *
582
		 * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
583
		 *
584
		 * @module search
585
		 *
586
		 * @since  5.0.0
587
		 *
588
		 * @param array    $es_query_args The raw Elasticsearch query args.
589
		 * @param WP_Query $query         The original WP_Query object.
590
		 */
591
		$es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
592
593
		// Do the actual search query!
594
		$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...
595
596
		if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
597
			$this->found_posts = 0;
598
599
			return;
600
		}
601
602
		// If we have aggregations, fix the ordering to match the input order (ES doesn't guarantee the return order).
603
		if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
604
			$this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
605
		}
606
607
		// Total number of results for paging purposes. Capped at $max_offset + $posts_per_page, as deep paging gets quite expensive.
608
		$this->found_posts = min( $this->search_result['results']['total'], $max_offset + $posts_per_page );
609
	}
610
611
	/**
612
	 * If the query has already been run before filters have been updated, then we need to re-run the query
613
	 * to get the latest aggregations.
614
	 *
615
	 * This is especially useful for supporting widget management in the customizer.
616
	 *
617
	 * @since 5.8.0
618
	 *
619
	 * @return bool Whether the query was successful or not.
620
	 */
621
	public function update_search_results_aggregations() {
622
		if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
623
			return false;
624
		}
625
626
		$es_args = $this->last_query_info['args'];
627
		$builder = new Jetpack_WPES_Query_Builder();
628
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
629
		$es_args['aggregations'] = $builder->build_aggregation();
630
631
		$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...
632
633
		return ! is_wp_error( $this->search_result );
634
	}
635
636
	/**
637
	 * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style Elasticsearch term arguments for the search.
638
	 *
639
	 * @since 5.0.0
640
	 *
641
	 * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query.
642
	 *
643
	 * @return array The new WP-style Elasticsearch arguments (that will be converted into 'real' Elasticsearch arguments).
644
	 */
645
	public function get_es_wp_query_terms_for_query( WP_Query $query ) {
646
		$args = array();
647
648
		$the_tax_query = $query->tax_query;
649
650
		if ( ! $the_tax_query ) {
651
			return $args;
652
		}
653
654
655
		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...
656
			return $args;
657
		}
658
659
		$args = array();
660
661
		foreach ( $the_tax_query->queries as $tax_query ) {
662
			// Right now we only support slugs...see note above
663
			if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
664
				continue;
665
			}
666
667
			$taxonomy = $tax_query['taxonomy'];
668
669 View Code Duplication
			if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
670
				$args[ $taxonomy ] = array();
671
			}
672
673
			$args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
674
		}
675
676
		return $args;
677
	}
678
679
	/**
680
	 * Parse out the post type from a WP_Query.
681
	 *
682
	 * Only allows post types that are not marked as 'exclude_from_search'.
683
	 *
684
	 * @since 5.0.0
685
	 *
686
	 * @param WP_Query $query Original WP_Query object.
687
	 *
688
	 * @return array Array of searchable post types corresponding to the original query.
689
	 */
690
	public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
691
		$post_types = $query->get( 'post_type' );
692
693
		// If we're searching 'any', we want to only pass searchable post types to Elasticsearch.
694
		if ( 'any' === $post_types ) {
695
			$post_types = array_values( get_post_types( array(
696
				'exclude_from_search' => false,
697
			) ) );
698
		}
699
700
		if ( ! is_array( $post_types ) ) {
701
			$post_types = array( $post_types );
702
		}
703
704
		$post_types = array_unique( $post_types );
705
706
		$sanitized_post_types = array();
707
708
		// Make sure the post types are queryable.
709
		foreach ( $post_types as $post_type ) {
710
			if ( ! $post_type ) {
711
				continue;
712
			}
713
714
			$post_type_object = get_post_type_object( $post_type );
715
			if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
716
				continue;
717
			}
718
719
			$sanitized_post_types[] = $post_type;
720
		}
721
722
		return $sanitized_post_types;
723
	}
724
725
726
	/**
727
	 * Get the Elasticsearch result.
728
	 *
729
	 * @since 5.0.0
730
	 *
731
	 * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response.
732
	 *
733
	 * @return array|bool The search results, or false if there was a failure.
734
	 */
735
	public function get_search_result( $raw = false ) {
736
		if ( $raw ) {
737
			return $this->search_result;
738
		}
739
740
		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;
741
	}
742
743
	/**
744
	 * Add the date portion of a WP_Query onto the query args.
745
	 *
746
	 * @since 5.0.0
747
	 *
748
	 * @param array    $es_wp_query_args The Elasticsearch query arguments in WordPress form.
749
	 * @param WP_Query $query            The original WP_Query.
750
	 *
751
	 * @return array The es wp query args, with date filters added (as needed).
752
	 */
753
	public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
754
		if ( $query->get( 'year' ) ) {
755
			if ( $query->get( 'monthnum' ) ) {
756
				// Padding
757
				$date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
758
759
				if ( $query->get( 'day' ) ) {
760
					// Padding
761
					$date_day = sprintf( '%02d', $query->get( 'day' ) );
762
763
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
764
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
765
				} else {
766
					$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
767
768
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
769
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
770
				}
771
			} else {
772
				$date_start = $query->get( 'year' ) . '-01-01 00:00:00';
773
				$date_end   = $query->get( 'year' ) . '-12-31 23:59:59';
774
			}
775
776
			$es_wp_query_args['date_range'] = array(
777
				'field' => 'date',
778
				'gte'   => $date_start,
779
				'lte'   => $date_end,
780
			);
781
		}
782
783
		return $es_wp_query_args;
784
	}
785
786
	/**
787
	 * Converts WP_Query style args to Elasticsearch args.
788
	 *
789
	 * @since 5.0.0
790
	 *
791
	 * @param array $args Array of WP_Query style arguments.
792
	 *
793
	 * @return array Array of ES style query arguments.
794
	 */
795
	public function convert_wp_es_to_es_args( array $args ) {
796
		jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-parser' );
797
798
		$defaults = array(
799
			'blog_id'        => get_current_blog_id(),
800
			'query'          => null,    // Search phrase
801
			'query_fields'   => array(), //list of fields to search
802
			'post_type'      => null,    // string or an array
803
			'terms'          => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) )
804
			'author'         => null,    // id or an array of ids
805
			'author_name'    => array(), // string or an array
806
			'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'
807
			'orderby'        => null,    // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
808
			'order'          => 'DESC',
809
			'posts_per_page' => 10,
810
			'offset'         => null,
811
			'paged'          => null,
812
			/**
813
			 * Aggregations. Examples:
814
			 * array(
815
			 *     'Tag'       => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
816
			 *     'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
817
			 * );
818
			 */
819
			'aggregations'   => null,
820
		);
821
822
		$args = wp_parse_args( $args, $defaults );
823
824
		$parser = new Jetpack_WPES_Search_Query_Parser( $args['query'], array( get_locale() ) );
825
826
		if ( empty( $args['query_fields'] ) ) {
827
			if ( defined( 'JETPACK_SEARCH_VIP_INDEX' ) && JETPACK_SEARCH_VIP_INDEX ) {
828
				// VIP indices do not have per language fields
829
				$match_fields        = array(
830
					'title^0.1',
831
					'content^0.1',
832
					'excerpt^0.1',
833
					'tag.name^0.1',
834
					'category.name^0.1',
835
					'author_login^0.1',
836
					'author^0.1',
837
				);
838
				$boost_fields        = array(
839
					'title^2',
840
					'tag.name',
841
					'category.name',
842
					'author_login',
843
					'author',
844
				);
845
				$boost_phrase_fields = array(
846
					'title',
847
					'content',
848
					'excerpt',
849
					'tag.name',
850
					'category.name',
851
					'author',
852
				);
853
			} else {
854
				$match_fields = $parser->merge_ml_fields(
855
					array(
856
						'title'         => 0.1,
857
						'content'       => 0.1,
858
						'excerpt'       => 0.1,
859
						'tag.name'      => 0.1,
860
						'category.name' => 0.1,
861
					),
862
					array(
863
						'author_login^0.1',
864
						'author^0.1',
865
					)
866
				);
867
868
				$boost_fields = $parser->merge_ml_fields(
869
					array(
870
						'title'         => 2,
871
						'tag.name'      => 1,
872
						'category.name' => 1,
873
					),
874
					array(
875
						'author_login',
876
						'author',
877
					)
878
				);
879
880
				$boost_phrase_fields = $parser->merge_ml_fields(
881
					array(
882
						'title'         => 1,
883
						'content'       => 1,
884
						'excerpt'       => 1,
885
						'tag.name'      => 1,
886
						'category.name' => 1,
887
					),
888
					array(
889
						'author',
890
					)
891
				);
892
			}
893
		} else {
894
			// If code is overriding the fields, then use that. Important for backwards compatibility.
895
			$match_fields        = $args['query_fields'];
896
			$boost_phrase_fields = $match_fields;
897
			$boost_fields        = null;
898
		}
899
900
		$parser->phrase_filter( array(
901
			'must_query_fields'  => $match_fields,
902
			'boost_query_fields' => null,
903
		) );
904
		$parser->remaining_query( array(
905
			'must_query_fields'  => $match_fields,
906
			'boost_query_fields' => $boost_fields,
907
		) );
908
909
		// Boost on phrase matches
910
		$parser->remaining_query( array(
911
			'boost_query_fields' => $boost_phrase_fields,
912
			'boost_query_type'   => 'phrase',
913
		) );
914
915
		/**
916
		 * Modify the recency decay parameters for the search query.
917
		 *
918
		 * The recency decay lowers the search scores based on the age of a post relative to an origin date. Basic adjustments:
919
		 *  - origin: A date. Posts with this date will have the highest score and no decay applied. Default is today.
920
		 *  - 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.
921
		 *  - scale: The number of days/months/years from the origin+offset at which the decay will equal the decay param. Default 360d
922
		 *  - decay: The amount of decay applied at offset+scale. Default 0.9.
923
		 *
924
		 * 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}
925
		 *
926
		 * @module search
927
		 *
928
		 * @since  5.8.0
929
		 *
930
		 * @param array $decay_params The decay parameters.
931
		 * @param array $args         The WP query parameters.
932
		 */
933
		$decay_params = apply_filters(
934
			'jetpack_search_recency_score_decay',
935
			array(
936
				'origin' => date( 'Y-m-d' ),
937
				'scale'  => '360d',
938
				'decay'  => 0.9,
939
			),
940
			$args
941
		);
942
943
		if ( ! empty( $decay_params ) ) {
944
			// Newer content gets weighted slightly higher
945
			$parser->add_decay( 'gauss', array(
946
				'date_gmt' => $decay_params
947
			) );
948
		}
949
950
		$es_query_args = array(
951
			'blog_id' => absint( $args['blog_id'] ),
952
			'size'    => absint( $args['posts_per_page'] ),
953
		);
954
955
		// ES "from" arg (offset)
956
		if ( $args['offset'] ) {
957
			$es_query_args['from'] = absint( $args['offset'] );
958
		} elseif ( $args['paged'] ) {
959
			$es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
960
		}
961
962
		$es_query_args['from'] = min( $es_query_args['from'], Jetpack_Search_Helpers::get_max_offset() );
963
964
		if ( ! is_array( $args['author_name'] ) ) {
965
			$args['author_name'] = array( $args['author_name'] );
966
		}
967
968
		// ES stores usernames, not IDs, so transform
969
		if ( ! empty( $args['author'] ) ) {
970
			if ( ! is_array( $args['author'] ) ) {
971
				$args['author'] = array( $args['author'] );
972
			}
973
974
			foreach ( $args['author'] as $author ) {
975
				$user = get_user_by( 'id', $author );
976
977
				if ( $user && ! empty( $user->user_login ) ) {
978
					$args['author_name'][] = $user->user_login;
979
				}
980
			}
981
		}
982
983
		//////////////////////////////////////////////////
984
		// Build the filters from the query elements.
985
		// Filters rock because they are cached from one query to the next
986
		// but they are cached as individual filters, rather than all combined together.
987
		// May get performance boost by also caching the top level boolean filter too.
988
989
		if ( $args['post_type'] ) {
990
			if ( ! is_array( $args['post_type'] ) ) {
991
				$args['post_type'] = array( $args['post_type'] );
992
			}
993
994
			$parser->add_filter( array(
995
				'terms' => array(
996
					'post_type' => $args['post_type'],
997
				),
998
			) );
999
		}
1000
1001
		if ( $args['author_name'] ) {
1002
			$parser->add_filter( array(
1003
				'terms' => array(
1004
					'author_login' => $args['author_name'],
1005
				),
1006
			) );
1007
		}
1008
1009
		if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
1010
			$field = $args['date_range']['field'];
1011
1012
			unset( $args['date_range']['field'] );
1013
1014
			$parser->add_filter( array(
1015
				'range' => array(
1016
					$field => $args['date_range'],
1017
				),
1018
			) );
1019
		}
1020
1021
		if ( is_array( $args['terms'] ) ) {
1022
			foreach ( $args['terms'] as $tax => $terms ) {
1023
				$terms = (array) $terms;
1024
1025
				if ( count( $terms ) && mb_strlen( $tax ) ) {
1026 View Code Duplication
					switch ( $tax ) {
1027
						case 'post_tag':
1028
							$tax_fld = 'tag.slug';
1029
1030
							break;
1031
1032
						case 'category':
1033
							$tax_fld = 'category.slug';
1034
1035
							break;
1036
1037
						default:
1038
							$tax_fld = 'taxonomy.' . $tax . '.slug';
1039
1040
							break;
1041
					}
1042
1043
					foreach ( $terms as $term ) {
1044
						$parser->add_filter( array(
1045
							'term' => array(
1046
								$tax_fld => $term,
1047
							),
1048
						) );
1049
					}
1050
				}
1051
			}
1052
		}
1053
1054
		if ( ! $args['orderby'] ) {
1055
			if ( $args['query'] ) {
1056
				$args['orderby'] = array( 'relevance' );
1057
			} else {
1058
				$args['orderby'] = array( 'date' );
1059
			}
1060
		}
1061
1062
		// Validate the "order" field
1063
		switch ( strtolower( $args['order'] ) ) {
1064
			case 'asc':
1065
				$args['order'] = 'asc';
1066
				break;
1067
1068
			case 'desc':
1069
			default:
1070
				$args['order'] = 'desc';
1071
				break;
1072
		}
1073
1074
		$es_query_args['sort'] = array();
1075
1076
		foreach ( (array) $args['orderby'] as $orderby ) {
1077
			// Translate orderby from WP field to ES field
1078
			switch ( $orderby ) {
1079
				case 'relevance' :
1080
					//never order by score ascending
1081
					$es_query_args['sort'][] = array(
1082
						'_score' => array(
1083
							'order' => 'desc',
1084
						),
1085
					);
1086
1087
					break;
1088
1089 View Code Duplication
				case 'date' :
1090
					$es_query_args['sort'][] = array(
1091
						'date' => array(
1092
							'order' => $args['order'],
1093
						),
1094
					);
1095
1096
					break;
1097
1098 View Code Duplication
				case 'ID' :
1099
					$es_query_args['sort'][] = array(
1100
						'id' => array(
1101
							'order' => $args['order'],
1102
						),
1103
					);
1104
1105
					break;
1106
1107
				case 'author' :
1108
					$es_query_args['sort'][] = array(
1109
						'author.raw' => array(
1110
							'order' => $args['order'],
1111
						),
1112
					);
1113
1114
					break;
1115
			} // End switch().
1116
		} // End foreach().
1117
1118
		if ( empty( $es_query_args['sort'] ) ) {
1119
			unset( $es_query_args['sort'] );
1120
		}
1121
1122
		if ( ! empty( $args['aggregations'] ) ) {
1123
			$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...
1124
		}
1125
1126
		$es_query_args['filter']       = $parser->build_filter();
1127
		$es_query_args['query']        = $parser->build_query();
1128
		$es_query_args['aggregations'] = $parser->build_aggregation();
1129
1130
		return $es_query_args;
1131
	}
1132
1133
	/**
1134
	 * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1135
	 *
1136
	 * @since 5.0.0
1137
	 *
1138
	 * @param array                      $aggregations Array of aggregations (filters) to add to the Jetpack_WPES_Query_Builder.
1139
	 * @param Jetpack_WPES_Query_Builder $builder      The builder instance that is creating the Elasticsearch query.
1140
	 */
1141
	public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
1142
		foreach ( $aggregations as $label => $aggregation ) {
1143
			switch ( $aggregation['type'] ) {
1144
				case 'taxonomy':
1145
					$this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1146
1147
					break;
1148
1149
				case 'post_type':
1150
					$this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1151
1152
					break;
1153
1154
				case 'date_histogram':
1155
					$this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1156
1157
					break;
1158
			}
1159
		}
1160
	}
1161
1162
	/**
1163
	 * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1164
	 *
1165
	 * @since 5.0.0
1166
	 *
1167
	 * @param array                      $aggregation The aggregation to add to the query builder.
1168
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1169
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1170
	 */
1171
	public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1172
		$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...
1173
1174
		switch ( $aggregation['taxonomy'] ) {
1175
			case 'post_tag':
1176
				$field = 'tag';
1177
				break;
1178
1179
			case 'category':
1180
				$field = 'category';
1181
				break;
1182
1183
			default:
1184
				$field = 'taxonomy.' . $aggregation['taxonomy'];
1185
				break;
1186
		}
1187
1188
		$builder->add_aggs( $label, array(
1189
			'terms' => array(
1190
				'field' => $field . '.slug',
1191
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1192
			),
1193
		) );
1194
	}
1195
1196
	/**
1197
	 * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1198
	 *
1199
	 * @since 5.0.0
1200
	 *
1201
	 * @param array                      $aggregation The aggregation to add to the query builder.
1202
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1203
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1204
	 */
1205
	public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1206
		$builder->add_aggs( $label, array(
1207
			'terms' => array(
1208
				'field' => 'post_type',
1209
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1210
			),
1211
		) );
1212
	}
1213
1214
	/**
1215
	 * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1216
	 *
1217
	 * @since 5.0.0
1218
	 *
1219
	 * @param array                      $aggregation The aggregation to add to the query builder.
1220
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1221
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1222
	 */
1223
	public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1224
		$args = array(
1225
			'interval' => $aggregation['interval'],
1226
			'field'    => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' == $aggregation['field'] ) ? 'date_gmt' : 'date',
1227
		);
1228
1229
		if ( isset( $aggregation['min_doc_count'] ) ) {
1230
			$args['min_doc_count'] = intval( $aggregation['min_doc_count'] );
1231
		} else {
1232
			$args['min_doc_count'] = 1;
1233
		}
1234
1235
		$builder->add_aggs( $label, array(
1236
			'date_histogram' => $args,
1237
		) );
1238
	}
1239
1240
	/**
1241
	 * And an existing filter object with a list of additional filters.
1242
	 *
1243
	 * Attempts to optimize the filters somewhat.
1244
	 *
1245
	 * @since 5.0.0
1246
	 *
1247
	 * @param array $curr_filter The existing filters to build upon.
1248
	 * @param array $filters     The new filters to add.
1249
	 *
1250
	 * @return array The resulting merged filters.
1251
	 */
1252
	public static function and_es_filters( array $curr_filter, array $filters ) {
1253
		if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
1254
			if ( 1 === count( $filters ) ) {
1255
				return $filters[0];
1256
			}
1257
1258
			return array(
1259
				'and' => $filters,
1260
			);
1261
		}
1262
1263
		return array(
1264
			'and' => array_merge( array( $curr_filter ), $filters ),
1265
		);
1266
	}
1267
1268
	/**
1269
	 * Set the available filters for the search.
1270
	 *
1271
	 * These get rendered via the Jetpack_Search_Widget() widget.
1272
	 *
1273
	 * Behind the scenes, these are implemented using Elasticsearch Aggregations.
1274
	 *
1275
	 * If you do not require counts of how many documents match each filter, please consider using regular WP Query
1276
	 * arguments instead, such as via the jetpack_search_es_wp_query_args filter
1277
	 *
1278
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1279
	 *
1280
	 * @since  5.0.0
1281
	 *
1282
	 * @param array $aggregations Array of filters (aggregations) to apply to the search
1283
	 */
1284
	public function set_filters( array $aggregations ) {
1285
		foreach ( (array) $aggregations as $key => $agg ) {
1286
			if ( empty( $agg['name'] ) ) {
1287
				$aggregations[ $key ]['name'] = $key;
1288
			}
1289
		}
1290
		$this->aggregations = $aggregations;
1291
	}
1292
1293
	/**
1294
	 * Set the search's facets (deprecated).
1295
	 *
1296
	 * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead.
1297
	 *
1298
	 * @see        Jetpack_Search::set_filters()
1299
	 *
1300
	 * @param array $facets Array of facets to apply to the search.
1301
	 */
1302
	public function set_facets( array $facets ) {
1303
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
1304
1305
		$this->set_filters( $facets );
1306
	}
1307
1308
	/**
1309
	 * Get the raw Aggregation results from the Elasticsearch response.
1310
	 *
1311
	 * @since  5.0.0
1312
	 *
1313
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1314
	 *
1315
	 * @return array Array of Aggregations performed on the search.
1316
	 */
1317
	public function get_search_aggregations_results() {
1318
		$aggregations = array();
1319
1320
		$search_result = $this->get_search_result();
1321
1322
		if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
1323
			$aggregations = $search_result['aggregations'];
1324
		}
1325
1326
		return $aggregations;
1327
	}
1328
1329
	/**
1330
	 * Get the raw Facet results from the Elasticsearch response.
1331
	 *
1332
	 * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead.
1333
	 *
1334
	 * @see        Jetpack_Search::get_search_aggregations_results()
1335
	 *
1336
	 * @return array Array of Facets performed on the search.
1337
	 */
1338
	public function get_search_facets() {
1339
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
1340
1341
		return $this->get_search_aggregations_results();
1342
	}
1343
1344
	/**
1345
	 * Get the results of the Filters performed, including the number of matching documents.
1346
	 *
1347
	 * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
1348
	 * matching buckets, the url for applying/removing each bucket, etc.
1349
	 *
1350
	 * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
1351
	 * member if you need to access the raw filters set in Jetpack_Search::set_filters().
1352
	 *
1353
	 * @since 5.0.0
1354
	 *
1355
	 * @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...
1356
	 *
1357
	 * @return array Array of filters applied and info about them.
1358
	 */
1359
	public function get_filters( WP_Query $query = null ) {
1360
		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...
1361
			global $wp_query;
1362
1363
			$query = $wp_query;
1364
		}
1365
1366
		$aggregation_data = $this->aggregations;
1367
1368
		if ( empty( $aggregation_data ) ) {
1369
			return $aggregation_data;
1370
		}
1371
1372
		$aggregation_results = $this->get_search_aggregations_results();
1373
1374
		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...
1375
			return $aggregation_data;
1376
		}
1377
1378
		// NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES
1379
		foreach ( $aggregation_results as $label => $aggregation ) {
1380
			if ( empty( $aggregation ) ) {
1381
				continue;
1382
			}
1383
1384
			$type = $this->aggregations[ $label ]['type'];
1385
1386
			$aggregation_data[ $label ]['buckets'] = array();
1387
1388
			$existing_term_slugs = array();
1389
1390
			$tax_query_var = null;
1391
1392
			// Figure out which terms are active in the query, for this taxonomy
1393
			if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
1394
				$tax_query_var = $this->get_taxonomy_query_var( $this->aggregations[ $label ]['taxonomy'] );
1395
1396
				if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
1397
					foreach ( $query->tax_query->queries as $tax_query ) {
1398
						if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
1399
						     'slug' === $tax_query['field'] &&
1400
						     is_array( $tax_query['terms'] ) ) {
1401
							$existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
1402
						}
1403
					}
1404
				}
1405
			}
1406
1407
			// Now take the resulting found aggregation items and generate the additional info about them, such as activation/deactivation url, name, count, etc.
1408
			$buckets = array();
1409
1410
			if ( ! empty( $aggregation['buckets'] ) ) {
1411
				$buckets = (array) $aggregation['buckets'];
1412
			}
1413
1414
			if ( 'date_histogram' == $type ) {
1415
				//re-order newest to oldest
1416
				$buckets = array_reverse( $buckets );
1417
			}
1418
1419
			// Some aggregation types like date_histogram don't support the max results parameter
1420
			if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
1421
				$buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
1422
			}
1423
1424
			foreach ( $buckets as $item ) {
1425
				$query_vars = array();
1426
				$active     = false;
1427
				$remove_url = null;
1428
				$name       = '';
1429
1430
				// What type was the original aggregation?
1431
				switch ( $type ) {
1432
					case 'taxonomy':
1433
						$taxonomy = $this->aggregations[ $label ]['taxonomy'];
1434
1435
						$term = get_term_by( 'slug', $item['key'], $taxonomy );
1436
1437
						if ( ! $term || ! $tax_query_var ) {
1438
							continue 2; // switch() is considered a looping structure
1439
						}
1440
1441
						$query_vars = array(
1442
							$tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
1443
						);
1444
1445
						$name = $term->name;
1446
1447
						// Let's determine if this term is active or not
1448
1449
						if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
1450
							$active = true;
1451
1452
							$slug_count = count( $existing_term_slugs );
1453
1454 View Code Duplication
							if ( $slug_count > 1 ) {
1455
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1456
									$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...
1457
									rawurlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) )
1458
								);
1459
							} else {
1460
								$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...
1461
							}
1462
						}
1463
1464
						break;
1465
1466
					case 'post_type':
1467
						$post_type = get_post_type_object( $item['key'] );
1468
1469
						if ( ! $post_type || $post_type->exclude_from_search ) {
1470
							continue 2;  // switch() is considered a looping structure
1471
						}
1472
1473
						$query_vars = array(
1474
							'post_type' => $item['key'],
1475
						);
1476
1477
						$name = $post_type->labels->singular_name;
1478
1479
						// Is this post type active on this search?
1480
						$post_types = $query->get( 'post_type' );
1481
1482
						if ( ! is_array( $post_types ) ) {
1483
							$post_types = array( $post_types );
1484
						}
1485
1486
						if ( in_array( $item['key'], $post_types ) ) {
1487
							$active = true;
1488
1489
							$post_type_count = count( $post_types );
1490
1491
							// 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
1492 View Code Duplication
							if ( $post_type_count > 1 ) {
1493
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1494
									'post_type',
1495
									rawurlencode( implode( ',', array_diff( $post_types, array( $item['key'] ) ) ) )
1496
								);
1497
							} else {
1498
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
1499
							}
1500
						}
1501
1502
						break;
1503
1504
					case 'date_histogram':
1505
						$timestamp = $item['key'] / 1000;
1506
1507
						$current_year  = $query->get( 'year' );
1508
						$current_month = $query->get( 'monthnum' );
1509
						$current_day   = $query->get( 'day' );
1510
1511
						switch ( $this->aggregations[ $label ]['interval'] ) {
1512
							case 'year':
1513
								$year = (int) date( 'Y', $timestamp );
1514
1515
								$query_vars = array(
1516
									'year'     => $year,
1517
									'monthnum' => false,
1518
									'day'      => false,
1519
								);
1520
1521
								$name = $year;
1522
1523
								// Is this year currently selected?
1524
								if ( ! empty( $current_year ) && (int) $current_year === $year ) {
1525
									$active = true;
1526
1527
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
1528
								}
1529
1530
								break;
1531
1532
							case 'month':
1533
								$year  = (int) date( 'Y', $timestamp );
1534
								$month = (int) date( 'n', $timestamp );
1535
1536
								$query_vars = array(
1537
									'year'     => $year,
1538
									'monthnum' => $month,
1539
									'day'      => false,
1540
								);
1541
1542
								$name = date( 'F Y', $timestamp );
1543
1544
								// Is this month currently selected?
1545
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1546
								     ! empty( $current_month ) && (int) $current_month === $month ) {
1547
									$active = true;
1548
1549
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
1550
								}
1551
1552
								break;
1553
1554
							case 'day':
1555
								$year  = (int) date( 'Y', $timestamp );
1556
								$month = (int) date( 'n', $timestamp );
1557
								$day   = (int) date( 'j', $timestamp );
1558
1559
								$query_vars = array(
1560
									'year'     => $year,
1561
									'monthnum' => $month,
1562
									'day'      => $day,
1563
								);
1564
1565
								$name = date( 'F jS, Y', $timestamp );
1566
1567
								// Is this day currently selected?
1568
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1569
								     ! empty( $current_month ) && (int) $current_month === $month &&
1570
								     ! empty( $current_day ) && (int) $current_day === $day ) {
1571
									$active = true;
1572
1573
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
1574
								}
1575
1576
								break;
1577
1578
							default:
1579
								continue 3; // switch() is considered a looping structure
1580
						} // End switch().
1581
1582
						break;
1583
1584
					default:
1585
						//continue 2; // switch() is considered a looping structure
1586
				} // End switch().
1587
1588
				// Need to urlencode param values since add_query_arg doesn't
1589
				$url_params = urlencode_deep( $query_vars );
1590
1591
				$aggregation_data[ $label ]['buckets'][] = array(
1592
					'url'        => Jetpack_Search_Helpers::add_query_arg( $url_params ),
1593
					'query_vars' => $query_vars,
1594
					'name'       => $name,
1595
					'count'      => $item['doc_count'],
1596
					'active'     => $active,
1597
					'remove_url' => $remove_url,
1598
					'type'       => $type,
1599
					'type_label' => $aggregation_data[ $label ]['name'],
1600
					'widget_id'  => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0
1601
				);
1602
			} // End foreach().
1603
		} // End foreach().
1604
1605
		return $aggregation_data;
1606
	}
1607
1608
	/**
1609
	 * Get the results of the facets performed.
1610
	 *
1611
	 * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead.
1612
	 *
1613
	 * @see        Jetpack_Search::get_filters()
1614
	 *
1615
	 * @return array $facets Array of facets applied and info about them.
1616
	 */
1617
	public function get_search_facet_data() {
1618
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
1619
1620
		return $this->get_filters();
1621
	}
1622
1623
	/**
1624
	 * Get the filters that are currently applied to this search.
1625
	 *
1626
	 * @since 5.0.0
1627
	 *
1628
	 * @return array Array of filters that were applied.
1629
	 */
1630
	public function get_active_filter_buckets() {
1631
		$active_buckets = array();
1632
1633
		$filters = $this->get_filters();
1634
1635
		if ( ! is_array( $filters ) ) {
1636
			return $active_buckets;
1637
		}
1638
1639
		foreach ( $filters as $filter ) {
1640
			if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
1641
				foreach ( $filter['buckets'] as $item ) {
1642
					if ( isset( $item['active'] ) && $item['active'] ) {
1643
						$active_buckets[] = $item;
1644
					}
1645
				}
1646
			}
1647
		}
1648
1649
		return $active_buckets;
1650
	}
1651
1652
	/**
1653
	 * Get the filters that are currently applied to this search.
1654
	 *
1655
	 * @deprecated 5.0 Please use Jetpack_Search::get_active_filter_buckets() instead.
1656
	 *
1657
	 * @see        Jetpack_Search::get_active_filter_buckets()
1658
	 *
1659
	 * @return array Array of filters that were applied.
1660
	 */
1661
	public function get_current_filters() {
1662
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
1663
1664
		return $this->get_active_filter_buckets();
1665
	}
1666
1667
	/**
1668
	 * Calculate the right query var to use for a given taxonomy.
1669
	 *
1670
	 * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter.
1671
	 *
1672
	 * @since 5.0.0
1673
	 *
1674
	 * @param string $taxonomy_name The name of the taxonomy for which to get the query var.
1675
	 *
1676
	 * @return bool|string The query var to use for this taxonomy, or false if none found.
1677
	 */
1678
	public function get_taxonomy_query_var( $taxonomy_name ) {
1679
		$taxonomy = get_taxonomy( $taxonomy_name );
1680
1681
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1682
			return false;
1683
		}
1684
1685
		/**
1686
		 * Modify the query var to use for a given taxonomy
1687
		 *
1688
		 * @module search
1689
		 *
1690
		 * @since  5.0.0
1691
		 *
1692
		 * @param string $query_var     The current query_var for the taxonomy
1693
		 * @param string $taxonomy_name The taxonomy name
1694
		 */
1695
		return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
1696
	}
1697
1698
	/**
1699
	 * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
1700
	 * which is the input order.
1701
	 *
1702
	 * Necessary because ES does not always return aggregations in the same order that you pass them in,
1703
	 * and it should be possible to control the display order easily.
1704
	 *
1705
	 * @since 5.0.0
1706
	 *
1707
	 * @param array $aggregations Aggregation results to be reordered.
1708
	 * @param array $desired      Array with keys representing the desired ordering.
1709
	 *
1710
	 * @return array A new array with reordered keys, matching those in $desired.
1711
	 */
1712
	public function fix_aggregation_ordering( array $aggregations, array $desired ) {
1713
		if ( empty( $aggregations ) || empty( $desired ) ) {
1714
			return $aggregations;
1715
		}
1716
1717
		$reordered = array();
1718
1719
		foreach ( array_keys( $desired ) as $agg_name ) {
1720
			if ( isset( $aggregations[ $agg_name ] ) ) {
1721
				$reordered[ $agg_name ] = $aggregations[ $agg_name ];
1722
			}
1723
		}
1724
1725
		return $reordered;
1726
	}
1727
1728
	/**
1729
	 * Sends events to Tracks when a search filters widget is updated.
1730
	 *
1731
	 * @since 5.8.0
1732
	 *
1733
	 * @param string $option    The option name. Only "widget_jetpack-search-filters" is cared about.
1734
	 * @param array  $old_value The old option value.
1735
	 * @param array  $new_value The new option value.
1736
	 */
1737
	public function track_widget_updates( $option, $old_value, $new_value ) {
1738
		if ( 'widget_jetpack-search-filters' !== $option ) {
1739
			return;
1740
		}
1741
1742
		$event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value );
1743
		if ( ! $event ) {
1744
			return;
1745
		}
1746
1747
		jetpack_tracks_record_event(
1748
			wp_get_current_user(),
1749
			sprintf( 'jetpack_search_widget_%s', $event['action'] ),
1750
			$event['widget']
1751
		);
1752
	}
1753
1754
	/**
1755
	 * Moves any active search widgets to the inactive category.
1756
	 *
1757
	 * @since 5.9.0
1758
	 *
1759
	 * @param string $module Unused. The Jetpack module being disabled.
1760
	 */
1761
	public function move_search_widgets_to_inactive( $module ) {
1762
		if ( ! is_active_widget( false, false, Jetpack_Search_Helpers::FILTER_WIDGET_BASE, true ) ) {
1763
			return;
1764
		}
1765
1766
		$sidebars_widgets = wp_get_sidebars_widgets();
1767
1768
		if ( ! is_array( $sidebars_widgets ) ) {
1769
			return;
1770
		}
1771
1772
		$changed = false;
1773
1774
		foreach ( $sidebars_widgets as $sidebar => $widgets ) {
1775
			if ( 'wp_inactive_widgets' === $sidebar || 'orphaned_widgets' === substr( $sidebar, 0, 16 ) ) {
1776
				continue;
1777
			}
1778
1779
			if ( is_array( $widgets ) ) {
1780
				foreach ( $widgets as $key => $widget ) {
1781
					if ( _get_widget_id_base( $widget ) == Jetpack_Search_Helpers::FILTER_WIDGET_BASE ) {
1782
						$changed = true;
1783
1784
						array_unshift( $sidebars_widgets['wp_inactive_widgets'], $widget );
1785
						unset( $sidebars_widgets[ $sidebar ][ $key ] );
1786
					}
1787
				}
1788
			}
1789
		}
1790
1791
		if ( $changed ) {
1792
			wp_set_sidebars_widgets( $sidebars_widgets );
1793
		}
1794
	}
1795
}
1796