Completed
Push — fix/jetpack-search-sync ( b948ef...11e268 )
by
unknown
34:26 queued 26:19
created

Jetpack_Search::init_hooks()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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