Completed
Push — add/business-hours-tests ( a06e6d...88b2e3 )
by Jeremy
68:17 queued 57:51
created

Jetpack_Search::get_search_facets()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3
/**
4
 * Jetpack Search: Main Jetpack_Search class
5
 *
6
 * @package    Jetpack
7
 * @subpackage Jetpack Search
8
 * @since      5.0.0
9
 */
10
11
use Automattic\Jetpack\Connection\Client;
12
13
require_once __DIR__ . '/class-jetpack-search-options.php';
14
15
/**
16
 * The main class for the Jetpack Search module.
17
 *
18
 * @since 5.0.0
19
 */
20
class Jetpack_Search {
21
22
	/**
23
	 * The number of found posts.
24
	 *
25
	 * @since 5.0.0
26
	 *
27
	 * @var int
28
	 */
29
	protected $found_posts = 0;
30
31
	/**
32
	 * The search result, as returned by the WordPress.com REST API.
33
	 *
34
	 * @since 5.0.0
35
	 *
36
	 * @var array
37
	 */
38
	protected $search_result;
39
40
	/**
41
	 * This site's blog ID on WordPress.com.
42
	 *
43
	 * @since 5.0.0
44
	 *
45
	 * @var int
46
	 */
47
	protected $jetpack_blog_id;
48
49
	/**
50
	 * The Elasticsearch aggregations (filters).
51
	 *
52
	 * @since 5.0.0
53
	 *
54
	 * @var array
55
	 */
56
	protected $aggregations = array();
57
58
	/**
59
	 * The maximum number of aggregations allowed.
60
	 *
61
	 * @since 5.0.0
62
	 *
63
	 * @var int
64
	 */
65
	protected $max_aggregations_count = 100;
66
67
	/**
68
	 * Statistics about the last Elasticsearch query.
69
	 *
70
	 * @since 5.6.0
71
	 *
72
	 * @var array
73
	 */
74
	protected $last_query_info = array();
75
76
	/**
77
	 * Statistics about the last Elasticsearch query failure.
78
	 *
79
	 * @since 5.6.0
80
	 *
81
	 * @var array
82
	 */
83
	protected $last_query_failure_info = array();
84
85
	/**
86
	 * The singleton instance of this class.
87
	 *
88
	 * @since 5.0.0
89
	 *
90
	 * @var Jetpack_Search
91
	 */
92
	protected static $instance;
93
94
	/**
95
	 * Languages with custom analyzers. Other languages are supported, but are analyzed with the default analyzer.
96
	 *
97
	 * @since 5.0.0
98
	 *
99
	 * @var array
100
	 */
101
	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' );
102
103
	/**
104
	 * Jetpack_Search constructor.
105
	 *
106
	 * @since 5.0.0
107
	 *
108
	 * Doesn't do anything. This class needs to be initialized via the instance() method instead.
109
	 */
110
	protected function __construct() {
111
	}
112
113
	/**
114
	 * Prevent __clone()'ing of this class.
115
	 *
116
	 * @since 5.0.0
117
	 */
118
	public function __clone() {
119
		wp_die( "Please don't __clone Jetpack_Search" );
120
	}
121
122
	/**
123
	 * Prevent __wakeup()'ing of this class.
124
	 *
125
	 * @since 5.0.0
126
	 */
127
	public function __wakeup() {
128
		wp_die( "Please don't __wakeup Jetpack_Search" );
129
	}
130
131
	/**
132
	 * Get singleton instance of Jetpack_Search.
133
	 *
134
	 * Instantiates and sets up a new instance if needed, or returns the singleton.
135
	 *
136
	 * @since 5.0.0
137
	 *
138
	 * @return Jetpack_Search The Jetpack_Search singleton.
139
	 */
140
	public static function instance() {
141
		if ( ! isset( self::$instance ) ) {
142
			if ( Jetpack_Search_Options::is_instant_enabled() ) {
143
				require_once __DIR__ . '/class-jetpack-instant-search.php';
144
				self::$instance = new Jetpack_Instant_Search();
145
			} else {
146
				self::$instance = new Jetpack_Search();
147
			}
148
149
			self::$instance->setup();
150
		}
151
152
		return self::$instance;
153
	}
154
155
	/**
156
	 * Perform various setup tasks for the class.
157
	 *
158
	 * Checks various pre-requisites and adds hooks.
159
	 *
160
	 * @since 5.0.0
161
	 */
162
	public function setup() {
163
		if ( ! Jetpack::is_active() || ! $this->is_search_supported() ) {
164
			/**
165
			 * Fires when the Jetpack Search fails and would fallback to MySQL.
166
			 *
167
			 * @module search
168
			 * @since 7.9.0
169
			 *
170
			 * @param string $reason Reason for Search fallback.
171
			 * @param mixed  $data   Data associated with the request, such as attempted search parameters.
172
			 */
173
			do_action( 'jetpack_search_abort', 'inactive', null );
174
			return;
175
		}
176
177
		$this->jetpack_blog_id = Jetpack::get_option( 'id' );
178
179
		if ( ! $this->jetpack_blog_id ) {
180
			/** This action is documented in modules/search/class.jetpack-search.php */
181
			do_action( 'jetpack_search_abort', 'no_blog_id', null );
182
			return;
183
		}
184
185
		$this->load_php();
186
		$this->init_hooks();
187
	}
188
189
	/**
190
	 * Loads the php for this version of search
191
	 *
192
	 * @since 8.3.0
193
	 */
194
	public function load_php() {
195
		$this->base_load_php();
196
	}
197
198
	/**
199
	 * Loads the PHP common to all search. Should be called from extending classes.
200
	 */
201
	protected function base_load_php() {
202
		require_once __DIR__ . '/class.jetpack-search-helpers.php';
203
		require_once __DIR__ . '/class.jetpack-search-template-tags.php';
204
		require_once JETPACK__PLUGIN_DIR . 'modules/widgets/search.php';
205
	}
206
207
	/**
208
	 * Setup the various hooks needed for the plugin to take over search duties.
209
	 *
210
	 * @since 5.0.0
211
	 */
212 View Code Duplication
	public function init_hooks() {
213
		if ( ! is_admin() ) {
214
			add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 );
215
216
			add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ), 10, 2 );
217
218
			add_action( 'did_jetpack_search_query', array( $this, 'store_last_query_info' ) );
219
			add_action( 'failed_jetpack_search_query', array( $this, 'store_query_failure' ) );
220
221
			add_action( 'init', array( $this, 'set_filters_from_widgets' ) );
222
223
			add_action( 'pre_get_posts', array( $this, 'maybe_add_post_type_as_var' ) );
224
		} else {
225
			add_action( 'update_option', array( $this, 'track_widget_updates' ), 10, 3 );
226
		}
227
228
		add_action( 'jetpack_deactivate_module_search', array( $this, 'move_search_widgets_to_inactive' ) );
229
	}
230
231
	/**
232
	 * Is search supported on the current plan
233
	 *
234
	 * @since 6.0
235
	 * Loads scripts for Tracks analytics library
236
	 */
237
	public function is_search_supported() {
238
		if ( method_exists( 'Jetpack_Plan', 'supports' ) ) {
239
			return Jetpack_Plan::supports( 'search' );
240
		}
241
		return false;
242
	}
243
244
	/**
245
	 * Does this site have a VIP index
246
	 * Get the version number to use when loading the file. Allows us to bypass cache when developing.
247
	 *
248
	 * @since 6.0
249
	 * @return string $script_version Version number.
250
	 */
251
	public function has_vip_index() {
252
		return defined( 'JETPACK_SEARCH_VIP_INDEX' ) && JETPACK_SEARCH_VIP_INDEX;
253
	}
254
255
	/**
256
	 * When an Elasticsearch query fails, this stores it and enqueues some debug information in the footer.
257
	 *
258
	 * @since 5.6.0
259
	 *
260
	 * @param array $meta Information about the failure.
261
	 */
262
	public function store_query_failure( $meta ) {
263
		$this->last_query_failure_info = $meta;
264
		add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
265
	}
266
267
	/**
268
	 * Outputs information about the last Elasticsearch failure.
269
	 *
270
	 * @since 5.6.0
271
	 */
272
	public function print_query_failure() {
273
		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...
274
			printf(
275
				'<!-- Jetpack Search failed with code %s: %s - %s -->',
276
				esc_html( $this->last_query_failure_info['response_code'] ),
277
				esc_html( $this->last_query_failure_info['json']['error'] ),
278
				esc_html( $this->last_query_failure_info['json']['message'] )
279
			);
280
		}
281
	}
282
283
	/**
284
	 * Stores information about the last Elasticsearch query and enqueues some debug information in the footer.
285
	 *
286
	 * @since 5.6.0
287
	 *
288
	 * @param array $meta Information about the query.
289
	 */
290
	public function store_last_query_info( $meta ) {
291
		$this->last_query_info = $meta;
292
		add_action( 'wp_footer', array( $this, 'print_query_success' ) );
293
	}
294
295
	/**
296
	 * Outputs information about the last Elasticsearch search.
297
	 *
298
	 * @since 5.6.0
299
	 */
300
	public function print_query_success() {
301
		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...
302
			printf(
303
				'<!-- Jetpack Search took %s ms, ES time %s ms -->',
304
				(int) $this->last_query_info['elapsed_time'],
305
				esc_html( $this->last_query_info['es_time'] )
306
			);
307
308
			if ( isset( $_GET['searchdebug'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
309
				printf(
310
					'<!-- Query response data: %s -->',
311
					esc_html( print_r( $this->last_query_info, 1 ) ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
312
				);
313
			}
314
		}
315
	}
316
317
	/**
318
	 * Returns the last query information, or false if no information was stored.
319
	 *
320
	 * @since 5.8.0
321
	 *
322
	 * @return bool|array
323
	 */
324
	public function get_last_query_info() {
325
		return empty( $this->last_query_info ) ? false : $this->last_query_info;
326
	}
327
328
	/**
329
	 * Returns the last query failure information, or false if no failure information was stored.
330
	 *
331
	 * @since 5.8.0
332
	 *
333
	 * @return bool|array
334
	 */
335
	public function get_last_query_failure_info() {
336
		return empty( $this->last_query_failure_info ) ? false : $this->last_query_failure_info;
337
	}
338
339
	/**
340
	 * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
341
	 * developers to disable filters supplied by the search widget. Useful if filters are
342
	 * being defined at the code level.
343
	 *
344
	 * @since      5.7.0
345
	 * @deprecated 5.8.0 Use Jetpack_Search_Helpers::are_filters_by_widget_disabled() directly.
346
	 *
347
	 * @return bool
348
	 */
349
	public function are_filters_by_widget_disabled() {
350
		return Jetpack_Search_Helpers::are_filters_by_widget_disabled();
351
	}
352
353
	/**
354
	 * Retrieves a list of known Jetpack search filters widget IDs, gets the filters for each widget,
355
	 * and applies those filters to this Jetpack_Search object.
356
	 *
357
	 * @since 5.7.0
358
	 */
359
	public function set_filters_from_widgets() {
360
		if ( Jetpack_Search_Helpers::are_filters_by_widget_disabled() ) {
361
			return;
362
		}
363
364
		$filters = Jetpack_Search_Helpers::get_filters_from_widgets();
365
366
		if ( ! empty( $filters ) ) {
367
			$this->set_filters( $filters );
368
		}
369
	}
370
371
	/**
372
	 * Restricts search results to certain post types via a GET argument.
373
	 *
374
	 * @since 5.8.0
375
	 *
376
	 * @param WP_Query $query A WP_Query instance.
377
	 */
378
	public function maybe_add_post_type_as_var( WP_Query $query ) {
379
		$post_type = ( ! empty( $_GET['post_type'] ) ) ? $_GET['post_type'] : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
380
		if ( $this->should_handle_query( $query ) && $post_type ) {
381
			$post_types = ( is_string( $post_type ) && false !== strpos( $post_type, ',' ) )
382
				? explode( ',', $post_type )
383
				: (array) $post_type;
384
			$post_types = array_map( 'sanitize_key', $post_types );
385
			$query->set( 'post_type', $post_types );
386
		}
387
	}
388
389
	/**
390
	 * Run a search on the WordPress.com public API.
391
	 *
392
	 * @since 5.0.0
393
	 *
394
	 * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint.
395
	 *
396
	 * @return object|WP_Error The response from the public API, or a WP_Error.
397
	 */
398
	public function search( array $es_args ) {
399
		$endpoint    = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
400
		$service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
401
402
		$do_authenticated_request = false;
403
404
		if ( class_exists( 'Automattic\\Jetpack\\Connection\\Client' ) &&
405
			isset( $es_args['authenticated_request'] ) &&
406
			true === $es_args['authenticated_request'] ) {
407
			$do_authenticated_request = true;
408
		}
409
410
		unset( $es_args['authenticated_request'] );
411
412
		$request_args = array(
413
			'headers'    => array(
414
				'Content-Type' => 'application/json',
415
			),
416
			'timeout'    => 10,
417
			'user-agent' => 'jetpack_search',
418
		);
419
420
		$request_body = wp_json_encode( $es_args );
421
422
		$start_time = microtime( true );
423
424
		if ( $do_authenticated_request ) {
425
			$request_args['method'] = 'POST';
426
427
			$request = Client::wpcom_json_api_request_as_blog( $endpoint, Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
0 ignored issues
show
Security Bug introduced by
It seems like $request_body defined by wp_json_encode($es_args) on line 420 can also be of type false; however, Automattic\Jetpack\Conne...n_api_request_as_blog() does only seem to accept string|null, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
428
		} else {
429
			$request_args = array_merge(
430
				$request_args,
431
				array(
432
					'body' => $request_body,
433
				)
434
			);
435
436
			$request = wp_remote_post( $service_url, $request_args );
437
		}
438
439
		$end_time = microtime( true );
440
441
		if ( is_wp_error( $request ) ) {
442
			return $request;
443
		}
444
		$response_code = wp_remote_retrieve_response_code( $request );
445
446
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
447
			return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_search_api_response'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
448
		}
449
450
		$response = json_decode( wp_remote_retrieve_body( $request ), true );
451
452
		$took = is_array( $response ) && ! empty( $response['took'] )
453
			? $response['took']
454
			: null;
455
456
		$query = array(
457
			'args'          => $es_args,
458
			'response'      => $response,
459
			'response_code' => $response_code,
460
			'elapsed_time'  => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms.
461
			'es_time'       => $took,
462
			'url'           => $service_url,
463
		);
464
465
		/**
466
		 * Fires after a search request has been performed.
467
		 *
468
		 * Includes the following info in the $query parameter:
469
		 *
470
		 * array args Array of Elasticsearch arguments for the search
471
		 * array response Raw API response, JSON decoded
472
		 * int response_code HTTP response code of the request
473
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
474
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
475
		 * string url API url that was queried
476
		 *
477
		 * @module search
478
		 *
479
		 * @since  5.0.0
480
		 * @since  5.8.0 This action now fires on all queries instead of just successful queries.
481
		 *
482
		 * @param array $query Array of information about the query performed
483
		 */
484
		do_action( 'did_jetpack_search_query', $query );
485
486 View Code Duplication
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
487
			/**
488
			 * Fires after a search query request has failed
489
			 *
490
			 * @module search
491
			 *
492
			 * @since  5.6.0
493
			 *
494
			 * @param array Array containing the response code and response from the failed search query
495
			 */
496
			do_action(
497
				'failed_jetpack_search_query',
498
				array(
499
					'response_code' => $response_code,
500
					'json'          => $response,
501
				)
502
			);
503
504
			return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_search_api_response'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
505
		}
506
507
		return $response;
508
	}
509
510
	/**
511
	 * Bypass the normal Search query and offload it to Jetpack servers.
512
	 *
513
	 * This is the main hook of the plugin and is responsible for returning the posts that match the search query.
514
	 *
515
	 * @since 5.0.0
516
	 *
517
	 * @param array    $posts Current array of posts (still pre-query).
518
	 * @param WP_Query $query The WP_Query being filtered.
519
	 *
520
	 * @return array Array of matching posts.
521
	 */
522
	public function filter__posts_pre_query( $posts, $query ) {
523
		if ( ! $this->should_handle_query( $query ) ) {
524
			// Intentionally not adding the 'jetpack_search_abort' action since this should fire for every request except for search.
525
			return $posts;
526
		}
527
528
		$this->do_search( $query );
529
530
		if ( ! is_array( $this->search_result ) ) {
531
			/** This action is documented in modules/search/class.jetpack-search.php */
532
			do_action( 'jetpack_search_abort', 'no_search_results_array', $this->search_result );
533
			return $posts;
534
		}
535
536
		// If no results, nothing to do.
537
		if ( ! count( $this->search_result['results']['hits'] ) ) {
538
			return array();
539
		}
540
541
		$post_ids = array();
542
543
		foreach ( $this->search_result['results']['hits'] as $result ) {
544
			$post_ids[] = (int) $result['fields']['post_id'];
545
		}
546
547
		// Query all posts now.
548
		$args = array(
549
			'post__in'            => $post_ids,
550
			'orderby'             => 'post__in',
551
			'perm'                => 'readable',
552
			'post_type'           => 'any',
553
			'ignore_sticky_posts' => true,
554
			'suppress_filters'    => true,
555
		);
556
557
		$posts_query = new WP_Query( $args );
558
559
		// 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.
560
		$query->found_posts   = $this->found_posts;
561
		$query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
562
563
		return $posts_query->posts;
564
	}
565
566
	/**
567
	 * Build up the search, then run it against the Jetpack servers.
568
	 *
569
	 * @since 5.0.0
570
	 *
571
	 * @param WP_Query $query The original WP_Query to use for the parameters of our search.
572
	 */
573
	public function do_search( WP_Query $query ) {
574
		if ( ! $this->should_handle_query( $query ) ) {
575
			// If we make it here, either 'filter__posts_pre_query' somehow allowed it or a different entry to do_search.
576
			/** This action is documented in modules/search/class.jetpack-search.php */
577
			do_action( 'jetpack_search_abort', 'search_attempted_non_search_query', $query );
578
			return;
579
		}
580
581
		$page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
582
583
		// Get maximum allowed offset and posts per page values for the API.
584
		$max_offset         = Jetpack_Search_Helpers::get_max_offset();
585
		$max_posts_per_page = Jetpack_Search_Helpers::get_max_posts_per_page();
586
587
		$posts_per_page = $query->get( 'posts_per_page' );
588
		if ( $posts_per_page > $max_posts_per_page ) {
589
			$posts_per_page = $max_posts_per_page;
590
		}
591
592
		// Start building the WP-style search query args.
593
		// They'll be translated to ES format args later.
594
		$es_wp_query_args = array(
595
			'query'          => $query->get( 's' ),
596
			'posts_per_page' => $posts_per_page,
597
			'paged'          => $page,
598
			'orderby'        => $query->get( 'orderby' ),
599
			'order'          => $query->get( 'order' ),
600
		);
601
602
		if ( ! empty( $this->aggregations ) ) {
603
			$es_wp_query_args['aggregations'] = $this->aggregations;
604
		}
605
606
		// Did we query for authors?
607
		if ( $query->get( 'author_name' ) ) {
608
			$es_wp_query_args['author_name'] = $query->get( 'author_name' );
609
		}
610
611
		$es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
612
		$es_wp_query_args['terms']     = $this->get_es_wp_query_terms_for_query( $query );
613
614
		/**
615
		 * Modify the search query parameters, such as controlling the post_type.
616
		 *
617
		 * These arguments are in the format of WP_Query arguments
618
		 *
619
		 * @module search
620
		 *
621
		 * @since  5.0.0
622
		 *
623
		 * @param array    $es_wp_query_args The current query args, in WP_Query format.
624
		 * @param WP_Query $query            The original WP_Query object.
625
		 */
626
		$es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $query.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
627
628
		// If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
629
		// capped at Jetpack_Search_Helpers::get_max_offset(), so a high page would always return the last page of results otherwise.
630
		if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $max_offset ) {
631
			$query->set_404();
632
633
			return;
634
		}
635
636
		// If there were no post types returned, then 404 to avoid querying against non-public post types, which could
637
		// happen if we don't add the post type restriction to the ES query.
638
		if ( empty( $es_wp_query_args['post_type'] ) ) {
639
			$query->set_404();
640
641
			return;
642
		}
643
644
		// Convert the WP-style args into ES args.
645
		$es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
646
647
		// Only trust ES to give us IDs, not the content since it is a mirror.
648
		$es_query_args['fields'] = array(
649
			'post_id',
650
		);
651
652
		/**
653
		 * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
654
		 *
655
		 * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
656
		 *
657
		 * @module search
658
		 *
659
		 * @since  5.0.0
660
		 *
661
		 * @param array    $es_query_args The raw Elasticsearch query args.
662
		 * @param WP_Query $query         The original WP_Query object.
663
		 */
664
		$es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $query.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
665
666
		// Do the actual search query!
667
		$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...
668
669
		if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
670
			$this->found_posts = 0;
671
672
			return;
673
		}
674
675
		// If we have aggregations, fix the ordering to match the input order (ES doesn't guarantee the return order).
676
		if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
677
			$this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
678
		}
679
680
		// Total number of results for paging purposes. Capped at $max_offset + $posts_per_page, as deep paging gets quite expensive.
681
		$this->found_posts = min( $this->search_result['results']['total'], $max_offset + $posts_per_page );
682
	}
683
684
	/**
685
	 * If the query has already been run before filters have been updated, then we need to re-run the query
686
	 * to get the latest aggregations.
687
	 *
688
	 * This is especially useful for supporting widget management in the customizer.
689
	 *
690
	 * @since 5.8.0
691
	 *
692
	 * @return bool Whether the query was successful or not.
693
	 */
694
	public function update_search_results_aggregations() {
695
		if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
696
			return false;
697
		}
698
699
		$es_args = $this->last_query_info['args'];
700
		$builder = new Jetpack_WPES_Query_Builder();
701
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
702
		$es_args['aggregations'] = $builder->build_aggregation();
703
704
		$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...
705
706
		return ! is_wp_error( $this->search_result );
707
	}
708
709
	/**
710
	 * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style Elasticsearch term arguments for the search.
711
	 *
712
	 * @since 5.0.0
713
	 *
714
	 * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query.
715
	 *
716
	 * @return array The new WP-style Elasticsearch arguments (that will be converted into 'real' Elasticsearch arguments).
717
	 */
718
	public function get_es_wp_query_terms_for_query( WP_Query $query ) {
719
		$args = array();
720
721
		$the_tax_query = $query->tax_query;
722
723
		if ( ! $the_tax_query ) {
724
			return $args;
725
		}
726
727
		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...
728
			return $args;
729
		}
730
731
		$args = array();
732
733
		foreach ( $the_tax_query->queries as $tax_query ) {
734
			// Right now we only support slugs...see note above.
735
			if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
736
				continue;
737
			}
738
739
			$taxonomy = $tax_query['taxonomy'];
740
741 View Code Duplication
			if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
742
				$args[ $taxonomy ] = array();
743
			}
744
745
			$args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
746
		}
747
748
		return $args;
749
	}
750
751
	/**
752
	 * Parse out the post type from a WP_Query.
753
	 *
754
	 * Only allows post types that are not marked as 'exclude_from_search'.
755
	 *
756
	 * @since 5.0.0
757
	 *
758
	 * @param WP_Query $query Original WP_Query object.
759
	 *
760
	 * @return array Array of searchable post types corresponding to the original query.
761
	 */
762
	public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
763
		$post_types = $query->get( 'post_type' );
764
765
		// If we're searching 'any', we want to only pass searchable post types to Elasticsearch.
766
		if ( 'any' === $post_types ) {
767
			$post_types = array_values(
768
				get_post_types(
769
					array(
770
						'exclude_from_search' => false,
771
					)
772
				)
773
			);
774
		}
775
776
		if ( ! is_array( $post_types ) ) {
777
			$post_types = array( $post_types );
778
		}
779
780
		$post_types = array_unique( $post_types );
781
782
		$sanitized_post_types = array();
783
784
		// Make sure the post types are queryable.
785
		foreach ( $post_types as $post_type ) {
786
			if ( ! $post_type ) {
787
				continue;
788
			}
789
790
			$post_type_object = get_post_type_object( $post_type );
791
			if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
792
				continue;
793
			}
794
795
			$sanitized_post_types[] = $post_type;
796
		}
797
798
		return $sanitized_post_types;
799
	}
800
801
	/**
802
	 * Initialize widgets for the Search module (on wp.com only).
803
	 *
804
	 * @module search
805
	 */
806
	public function action__widgets_init() {
807
		require_once __DIR__ . '/class.jetpack-search-widget-filters.php';
808
809
		register_widget( 'Jetpack_Search_Widget_Filters' );
810
	}
811
812
	/**
813
	 * Get the Elasticsearch result.
814
	 *
815
	 * @since 5.0.0
816
	 *
817
	 * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response.
818
	 *
819
	 * @return array|bool The search results, or false if there was a failure.
820
	 */
821
	public function get_search_result( $raw = false ) {
822
		if ( $raw ) {
823
			return $this->search_result;
824
		}
825
826
		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;
827
	}
828
829
	/**
830
	 * Add the date portion of a WP_Query onto the query args.
831
	 *
832
	 * @since 5.0.0
833
	 *
834
	 * @param array    $es_wp_query_args The Elasticsearch query arguments in WordPress form.
835
	 * @param WP_Query $query            The original WP_Query.
836
	 *
837
	 * @return array The es wp query args, with date filters added (as needed).
838
	 */
839
	public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
840
		if ( $query->get( 'year' ) ) {
841
			if ( $query->get( 'monthnum' ) ) {
842
				// Padding.
843
				$date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
844
845
				if ( $query->get( 'day' ) ) {
846
					// Padding.
847
					$date_day = sprintf( '%02d', $query->get( 'day' ) );
848
849
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
850
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
851
				} else {
852
					$days_in_month = gmdate( 't', mktime( 0, 0, 0, $query->get( 'monthnum' ), 14, $query->get( 'year' ) ) ); // 14 = middle of the month so no chance of DST issues
853
854
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
855
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
856
				}
857
			} else {
858
				$date_start = $query->get( 'year' ) . '-01-01 00:00:00';
859
				$date_end   = $query->get( 'year' ) . '-12-31 23:59:59';
860
			}
861
862
			$es_wp_query_args['date_range'] = array(
863
				'field' => 'date',
864
				'gte'   => $date_start,
865
				'lte'   => $date_end,
866
			);
867
		}
868
869
		return $es_wp_query_args;
870
	}
871
872
	/**
873
	 * Converts WP_Query style args to Elasticsearch args.
874
	 *
875
	 * @since 5.0.0
876
	 *
877
	 * @param array $args Array of WP_Query style arguments.
878
	 *
879
	 * @return array Array of ES style query arguments.
880
	 */
881
	public function convert_wp_es_to_es_args( array $args ) {
882
		jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-parser' );
883
884
		$defaults = array(
885
			'blog_id'        => get_current_blog_id(),
886
			'query'          => null,    // Search phrase.
887
			'query_fields'   => array(), // list of fields to search.
888
			'excess_boost'   => array(), // map of field to excess boost values (multiply).
889
			'post_type'      => null,    // string or an array.
890
			'terms'          => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) ). phpcs:ignore Squiz.PHP.CommentedOutCode.Found.
891
			'author'         => null,    // id or an array of ids.
892
			'author_name'    => array(), // string or an array.
893
			'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'. phpcs:ignore Squiz.PHP.CommentedOutCode.Found.
894
			'orderby'        => null,    // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
895
			'order'          => 'DESC',
896
			'posts_per_page' => 10,
897
			'offset'         => null,
898
			'paged'          => null,
899
			/**
900
			 * Aggregations. Examples:
901
			 * array(
902
			 *     'Tag'       => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
903
			 *     'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
904
			 * );
905
			 */
906
			'aggregations'   => null,
907
		);
908
909
		$args = wp_parse_args( $args, $defaults );
0 ignored issues
show
Documentation introduced by
$defaults is of type array<string,?,{"blog_id..."aggregations":"null"}>, but the function expects a string.

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...
910
911
		$parser = new Jetpack_WPES_Search_Query_Parser(
912
			$args['query'],
913
			/**
914
			 * Filter the languages used by Jetpack Search's Query Parser.
915
			 *
916
			 * @module search
917
			 *
918
			 * @since  7.9.0
919
			 *
920
			 * @param array $languages The array of languages. Default is value of get_locale().
921
			 */
922
			apply_filters( 'jetpack_search_query_languages', array( get_locale() ) )
923
		);
924
925
		if ( empty( $args['query_fields'] ) ) {
926
			if ( $this->has_vip_index() ) {
927
				// VIP indices do not have per language fields.
928
				$match_fields = $this->_get_caret_boosted_fields(
929
					array(
930
						'title'         => 0.1,
931
						'content'       => 0.1,
932
						'excerpt'       => 0.1,
933
						'tag.name'      => 0.1,
934
						'category.name' => 0.1,
935
						'author_login'  => 0.1,
936
						'author'        => 0.1,
937
					)
938
				);
939
940
				$boost_fields = $this->_get_caret_boosted_fields(
941
					$this->_apply_boosts_multiplier(
942
						array(
943
							'title'         => 2,
944
							'tag.name'      => 1,
945
							'category.name' => 1,
946
							'author_login'  => 1,
947
							'author'        => 1,
948
						),
949
						$args['excess_boost']
950
					)
951
				);
952
953
				$boost_phrase_fields = $this->_get_caret_boosted_fields(
954
					array(
955
						'title'         => 1,
956
						'content'       => 1,
957
						'excerpt'       => 1,
958
						'tag.name'      => 1,
959
						'category.name' => 1,
960
						'author'        => 1,
961
					)
962
				);
963
			} else {
964
				$match_fields = $parser->merge_ml_fields(
965
					array(
966
						'title'         => 0.1,
967
						'content'       => 0.1,
968
						'excerpt'       => 0.1,
969
						'tag.name'      => 0.1,
970
						'category.name' => 0.1,
971
					),
972
					$this->_get_caret_boosted_fields(
973
						array(
974
							'author_login' => 0.1,
975
							'author'       => 0.1,
976
						)
977
					)
978
				);
979
980
				$boost_fields = $parser->merge_ml_fields(
981
					$this->_apply_boosts_multiplier(
982
						array(
983
							'title'         => 2,
984
							'tag.name'      => 1,
985
							'category.name' => 1,
986
						),
987
						$args['excess_boost']
988
					),
989
					$this->_get_caret_boosted_fields(
990
						$this->_apply_boosts_multiplier(
991
							array(
992
								'author_login' => 1,
993
								'author'       => 1,
994
							),
995
							$args['excess_boost']
996
						)
997
					)
998
				);
999
1000
				$boost_phrase_fields = $parser->merge_ml_fields(
1001
					array(
1002
						'title'         => 1,
1003
						'content'       => 1,
1004
						'excerpt'       => 1,
1005
						'tag.name'      => 1,
1006
						'category.name' => 1,
1007
					),
1008
					$this->_get_caret_boosted_fields(
1009
						array(
1010
							'author' => 1,
1011
						)
1012
					)
1013
				);
1014
			}
1015
		} else {
1016
			// If code is overriding the fields, then use that. Important for backwards compatibility.
1017
			$match_fields        = $args['query_fields'];
1018
			$boost_phrase_fields = $match_fields;
1019
			$boost_fields        = null;
1020
		}
1021
1022
		$parser->phrase_filter(
1023
			array(
1024
				'must_query_fields'  => $match_fields,
1025
				'boost_query_fields' => null,
1026
			)
1027
		);
1028
		$parser->remaining_query(
1029
			array(
1030
				'must_query_fields'  => $match_fields,
1031
				'boost_query_fields' => $boost_fields,
1032
			)
1033
		);
1034
1035
		// Boost on phrase matches.
1036
		$parser->remaining_query(
1037
			array(
1038
				'boost_query_fields' => $boost_phrase_fields,
1039
				'boost_query_type'   => 'phrase',
1040
			)
1041
		);
1042
1043
		/**
1044
		 * Modify the recency decay parameters for the search query.
1045
		 *
1046
		 * The recency decay lowers the search scores based on the age of a post relative to an origin date. Basic adjustments:
1047
		 *  - origin: A date. Posts with this date will have the highest score and no decay applied. Default is today.
1048
		 *  - 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.
1049
		 *  - scale: The number of days/months/years from the origin+offset at which the decay will equal the decay param. Default 360d
1050
		 *  - decay: The amount of decay applied at offset+scale. Default 0.9.
1051
		 *
1052
		 * 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}
1053
		 *
1054
		 * @module search
1055
		 *
1056
		 * @since  5.8.0
1057
		 *
1058
		 * @param array $decay_params The decay parameters.
1059
		 * @param array $args         The WP query parameters.
1060
		 */
1061
		$decay_params = apply_filters(
1062
			'jetpack_search_recency_score_decay',
1063
			array(
1064
				'origin' => gmdate( 'Y-m-d' ),
1065
				'scale'  => '360d',
1066
				'decay'  => 0.9,
1067
			),
1068
			$args
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $args.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1069
		);
1070
1071
		if ( ! empty( $decay_params ) ) {
1072
			// Newer content gets weighted slightly higher.
1073
			$parser->add_decay(
1074
				'gauss',
1075
				array(
1076
					'date_gmt' => $decay_params,
1077
				)
1078
			);
1079
		}
1080
1081
		$es_query_args = array(
1082
			'blog_id' => absint( $args['blog_id'] ),
1083
			'size'    => absint( $args['posts_per_page'] ),
1084
		);
1085
1086
		// ES "from" arg (offset).
1087
		if ( $args['offset'] ) {
1088
			$es_query_args['from'] = absint( $args['offset'] );
1089
		} elseif ( $args['paged'] ) {
1090
			$es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
1091
		}
1092
1093
		$es_query_args['from'] = min( $es_query_args['from'], Jetpack_Search_Helpers::get_max_offset() );
1094
1095
		if ( ! is_array( $args['author_name'] ) ) {
1096
			$args['author_name'] = array( $args['author_name'] );
1097
		}
1098
1099
		// ES stores usernames, not IDs, so transform.
1100
		if ( ! empty( $args['author'] ) ) {
1101
			if ( ! is_array( $args['author'] ) ) {
1102
				$args['author'] = array( $args['author'] );
1103
			}
1104
1105
			foreach ( $args['author'] as $author ) {
1106
				$user = get_user_by( 'id', $author );
1107
1108
				if ( $user && ! empty( $user->user_login ) ) {
1109
					$args['author_name'][] = $user->user_login;
1110
				}
1111
			}
1112
		}
1113
1114
		/*
1115
		 * Build the filters from the query elements.
1116
		 * Filters rock because they are cached from one query to the next
1117
		 * but they are cached as individual filters, rather than all combined together.
1118
		 * May get performance boost by also caching the top level boolean filter too.
1119
		 */
1120
1121
		if ( $args['post_type'] ) {
1122
			if ( ! is_array( $args['post_type'] ) ) {
1123
				$args['post_type'] = array( $args['post_type'] );
1124
			}
1125
1126
			$parser->add_filter(
1127
				array(
1128
					'terms' => array(
1129
						'post_type' => $args['post_type'],
1130
					),
1131
				)
1132
			);
1133
		}
1134
1135
		if ( $args['author_name'] ) {
1136
			$parser->add_filter(
1137
				array(
1138
					'terms' => array(
1139
						'author_login' => $args['author_name'],
1140
					),
1141
				)
1142
			);
1143
		}
1144
1145
		if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
1146
			$field = $args['date_range']['field'];
1147
1148
			unset( $args['date_range']['field'] );
1149
1150
			$parser->add_filter(
1151
				array(
1152
					'range' => array(
1153
						$field => $args['date_range'],
1154
					),
1155
				)
1156
			);
1157
		}
1158
1159
		if ( is_array( $args['terms'] ) ) {
1160
			foreach ( $args['terms'] as $tax => $terms ) {
1161
				$terms = (array) $terms;
1162
1163
				if ( count( $terms ) && mb_strlen( $tax ) ) {
1164 View Code Duplication
					switch ( $tax ) {
1165
						case 'post_tag':
1166
							$tax_fld = 'tag.slug';
1167
1168
							break;
1169
1170
						case 'category':
1171
							$tax_fld = 'category.slug';
1172
1173
							break;
1174
1175
						default:
1176
							$tax_fld = 'taxonomy.' . $tax . '.slug';
1177
1178
							break;
1179
					}
1180
1181
					foreach ( $terms as $term ) {
1182
						$parser->add_filter(
1183
							array(
1184
								'term' => array(
1185
									$tax_fld => $term,
1186
								),
1187
							)
1188
						);
1189
					}
1190
				}
1191
			}
1192
		}
1193
1194
		if ( ! $args['orderby'] ) {
1195
			if ( $args['query'] ) {
1196
				$args['orderby'] = array( 'relevance' );
1197
			} else {
1198
				$args['orderby'] = array( 'date' );
1199
			}
1200
		}
1201
1202
		// Validate the "order" field.
1203
		switch ( strtolower( $args['order'] ) ) {
1204
			case 'asc':
1205
				$args['order'] = 'asc';
1206
				break;
1207
1208
			case 'desc':
1209
			default:
1210
				$args['order'] = 'desc';
1211
				break;
1212
		}
1213
1214
		$es_query_args['sort'] = array();
1215
1216
		foreach ( (array) $args['orderby'] as $orderby ) {
1217
			// Translate orderby from WP field to ES field.
1218
			switch ( $orderby ) {
1219
				case 'relevance':
1220
					// never order by score ascending.
1221
					$es_query_args['sort'][] = array(
1222
						'_score' => array(
1223
							'order' => 'desc',
1224
						),
1225
					);
1226
1227
					break;
1228
1229 View Code Duplication
				case 'date':
1230
					$es_query_args['sort'][] = array(
1231
						'date' => array(
1232
							'order' => $args['order'],
1233
						),
1234
					);
1235
1236
					break;
1237
1238 View Code Duplication
				case 'ID':
1239
					$es_query_args['sort'][] = array(
1240
						'id' => array(
1241
							'order' => $args['order'],
1242
						),
1243
					);
1244
1245
					break;
1246
1247
				case 'author':
1248
					$es_query_args['sort'][] = array(
1249
						'author.raw' => array(
1250
							'order' => $args['order'],
1251
						),
1252
					);
1253
1254
					break;
1255
			} // End switch.
1256
		} // End foreach.
1257
1258
		if ( empty( $es_query_args['sort'] ) ) {
1259
			unset( $es_query_args['sort'] );
1260
		}
1261
1262
		// Aggregations.
1263
		if ( ! empty( $args['aggregations'] ) ) {
1264
			$this->add_aggregations_to_es_query_builder( $args['aggregations'], $parser );
1265
		}
1266
1267
		$es_query_args['filter']       = $parser->build_filter();
1268
		$es_query_args['query']        = $parser->build_query();
1269
		$es_query_args['aggregations'] = $parser->build_aggregation();
1270
1271
		return $es_query_args;
1272
	}
1273
1274
	/**
1275
	 * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1276
	 *
1277
	 * @since 5.0.0
1278
	 *
1279
	 * @param array                      $aggregations Array of aggregations (filters) to add to the Jetpack_WPES_Query_Builder.
1280
	 * @param Jetpack_WPES_Query_Builder $builder      The builder instance that is creating the Elasticsearch query.
1281
	 */
1282
	public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
1283
		foreach ( $aggregations as $label => $aggregation ) {
1284
			if ( ! isset( $aggregation['type'] ) ) {
1285
				continue;
1286
			}
1287
			switch ( $aggregation['type'] ) {
1288
				case 'taxonomy':
1289
					$this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1290
1291
					break;
1292
1293
				case 'post_type':
1294
					$this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1295
1296
					break;
1297
1298
				case 'date_histogram':
1299
					$this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1300
1301
					break;
1302
			}
1303
		}
1304
	}
1305
1306
	/**
1307
	 * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1308
	 *
1309
	 * @since 5.0.0
1310
	 *
1311
	 * @param array                      $aggregation The aggregation to add to the query builder.
1312
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1313
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1314
	 */
1315
	public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1316
		$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...
1317
1318
		switch ( $aggregation['taxonomy'] ) {
1319
			case 'post_tag':
1320
				$field = 'tag';
1321
				break;
1322
1323
			case 'category':
1324
				$field = 'category';
1325
				break;
1326
1327
			default:
1328
				$field = 'taxonomy.' . $aggregation['taxonomy'];
1329
				break;
1330
		}
1331
1332
		$builder->add_aggs(
1333
			$label,
1334
			array(
1335
				'terms' => array(
1336
					'field' => $field . '.slug',
1337
					'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1338
				),
1339
			)
1340
		);
1341
	}
1342
1343
	/**
1344
	 * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1345
	 *
1346
	 * @since 5.0.0
1347
	 *
1348
	 * @param array                      $aggregation The aggregation to add to the query builder.
1349
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1350
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1351
	 */
1352
	public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1353
		$builder->add_aggs(
1354
			$label,
1355
			array(
1356
				'terms' => array(
1357
					'field' => 'post_type',
1358
					'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1359
				),
1360
			)
1361
		);
1362
	}
1363
1364
	/**
1365
	 * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1366
	 *
1367
	 * @since 5.0.0
1368
	 *
1369
	 * @param array                      $aggregation The aggregation to add to the query builder.
1370
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1371
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1372
	 */
1373
	public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1374
		$args = array(
1375
			'interval' => $aggregation['interval'],
1376
			'field'    => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' === $aggregation['field'] ) ? 'date_gmt' : 'date',
1377
		);
1378
1379
		if ( isset( $aggregation['min_doc_count'] ) ) {
1380
			$args['min_doc_count'] = (int) $aggregation['min_doc_count'];
1381
		} else {
1382
			$args['min_doc_count'] = 1;
1383
		}
1384
1385
		$builder->add_aggs(
1386
			$label,
1387
			array(
1388
				'date_histogram' => $args,
1389
			)
1390
		);
1391
	}
1392
1393
	/**
1394
	 * And an existing filter object with a list of additional filters.
1395
	 *
1396
	 * Attempts to optimize the filters somewhat.
1397
	 *
1398
	 * @since 5.0.0
1399
	 *
1400
	 * @param array $curr_filter The existing filters to build upon.
1401
	 * @param array $filters     The new filters to add.
1402
	 *
1403
	 * @return array The resulting merged filters.
1404
	 */
1405
	public static function and_es_filters( array $curr_filter, array $filters ) {
1406
		if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
1407
			if ( 1 === count( $filters ) ) {
1408
				return $filters[0];
1409
			}
1410
1411
			return array(
1412
				'and' => $filters,
1413
			);
1414
		}
1415
1416
		return array(
1417
			'and' => array_merge( array( $curr_filter ), $filters ),
1418
		);
1419
	}
1420
1421
	/**
1422
	 * Set the available filters for the search.
1423
	 *
1424
	 * These get rendered via the Jetpack_Search_Widget() widget.
1425
	 *
1426
	 * Behind the scenes, these are implemented using Elasticsearch Aggregations.
1427
	 *
1428
	 * If you do not require counts of how many documents match each filter, please consider using regular WP Query
1429
	 * arguments instead, such as via the jetpack_search_es_wp_query_args filter
1430
	 *
1431
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1432
	 *
1433
	 * @since  5.0.0
1434
	 *
1435
	 * @param array $aggregations Array of filters (aggregations) to apply to the search.
1436
	 */
1437
	public function set_filters( array $aggregations ) {
1438
		foreach ( (array) $aggregations as $key => $agg ) {
1439
			if ( empty( $agg['name'] ) ) {
1440
				$aggregations[ $key ]['name'] = $key;
1441
			}
1442
		}
1443
		$this->aggregations = $aggregations;
1444
	}
1445
1446
	/**
1447
	 * Set the search's facets (deprecated).
1448
	 *
1449
	 * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead.
1450
	 *
1451
	 * @see        Jetpack_Search::set_filters()
1452
	 *
1453
	 * @param array $facets Array of facets to apply to the search.
1454
	 */
1455
	public function set_facets( array $facets ) {
1456
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
1457
1458
		$this->set_filters( $facets );
1459
	}
1460
1461
	/**
1462
	 * Get the raw Aggregation results from the Elasticsearch response.
1463
	 *
1464
	 * @since  5.0.0
1465
	 *
1466
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1467
	 *
1468
	 * @return array Array of Aggregations performed on the search.
1469
	 */
1470
	public function get_search_aggregations_results() {
1471
		$aggregations = array();
1472
1473
		$search_result = $this->get_search_result();
1474
1475
		if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
1476
			$aggregations = $search_result['aggregations'];
1477
		}
1478
1479
		return $aggregations;
1480
	}
1481
1482
	/**
1483
	 * Get the raw Facet results from the Elasticsearch response.
1484
	 *
1485
	 * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead.
1486
	 *
1487
	 * @see        Jetpack_Search::get_search_aggregations_results()
1488
	 *
1489
	 * @return array Array of Facets performed on the search.
1490
	 */
1491
	public function get_search_facets() {
1492
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
1493
1494
		return $this->get_search_aggregations_results();
1495
	}
1496
1497
	/**
1498
	 * Get the results of the Filters performed, including the number of matching documents.
1499
	 *
1500
	 * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
1501
	 * matching buckets, the url for applying/removing each bucket, etc.
1502
	 *
1503
	 * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
1504
	 * member if you need to access the raw filters set in Jetpack_Search::set_filters().
1505
	 *
1506
	 * @since 5.0.0
1507
	 *
1508
	 * @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...
1509
	 *
1510
	 * @return array Array of filters applied and info about them.
1511
	 */
1512
	public function get_filters( WP_Query $query = null ) {
1513
		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...
1514
			global $wp_query;
1515
1516
			$query = $wp_query;
1517
		}
1518
1519
		$aggregation_data = $this->aggregations;
1520
1521
		if ( empty( $aggregation_data ) ) {
1522
			return $aggregation_data;
1523
		}
1524
1525
		$aggregation_results = $this->get_search_aggregations_results();
1526
1527
		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...
1528
			return $aggregation_data;
1529
		}
1530
1531
		// NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES.
1532
		foreach ( $aggregation_results as $label => $aggregation ) {
1533
			if ( empty( $aggregation ) ) {
1534
				continue;
1535
			}
1536
1537
			$type = $this->aggregations[ $label ]['type'];
1538
1539
			$aggregation_data[ $label ]['buckets'] = array();
1540
1541
			$existing_term_slugs = array();
1542
1543
			$tax_query_var = null;
1544
1545
			// Figure out which terms are active in the query, for this taxonomy.
1546
			if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
1547
				$tax_query_var = $this->get_taxonomy_query_var( $this->aggregations[ $label ]['taxonomy'] );
1548
1549
				if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
1550
					foreach ( $query->tax_query->queries as $tax_query ) {
1551
						if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
1552
							'slug' === $tax_query['field'] &&
1553
							is_array( $tax_query['terms'] ) ) {
1554
							$existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
1555
						}
1556
					}
1557
				}
1558
			}
1559
1560
			// Now take the resulting found aggregation items and generate the additional info about them, such as activation/deactivation url, name, count, etc.
1561
			$buckets = array();
1562
1563
			if ( ! empty( $aggregation['buckets'] ) ) {
1564
				$buckets = (array) $aggregation['buckets'];
1565
			}
1566
1567
			if ( 'date_histogram' === $type ) {
1568
				// re-order newest to oldest.
1569
				$buckets = array_reverse( $buckets );
1570
			}
1571
1572
			// Some aggregation types like date_histogram don't support the max results parameter.
1573
			if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
1574
				$buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
1575
			}
1576
1577
			foreach ( $buckets as $item ) {
1578
				$query_vars = array();
1579
				$active     = false;
1580
				$remove_url = null;
1581
				$name       = '';
1582
1583
				// What type was the original aggregation?
1584
				switch ( $type ) {
1585
					case 'taxonomy':
1586
						$taxonomy = $this->aggregations[ $label ]['taxonomy'];
1587
1588
						$term = get_term_by( 'slug', $item['key'], $taxonomy );
1589
1590
						if ( ! $term || ! $tax_query_var ) {
1591
							continue 2; // switch() is considered a looping structure.
1592
						}
1593
1594
						$query_vars = array(
1595
							$tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
1596
						);
1597
1598
						$name = $term->name;
1599
1600
						// Let's determine if this term is active or not.
1601
1602 View Code Duplication
						if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
1603
							$active = true;
1604
1605
							$slug_count = count( $existing_term_slugs );
1606
1607
							if ( $slug_count > 1 ) {
1608
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1609
									$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...
1610
									rawurlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) )
1611
								);
1612
							} else {
1613
								$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...
1614
							}
1615
						}
1616
1617
						break;
1618
1619
					case 'post_type':
1620
						$post_type = get_post_type_object( $item['key'] );
1621
1622
						if ( ! $post_type || $post_type->exclude_from_search ) {
1623
							continue 2;  // switch() is considered a looping structure.
1624
						}
1625
1626
						$query_vars = array(
1627
							'post_type' => $item['key'],
1628
						);
1629
1630
						$name = $post_type->labels->singular_name;
1631
1632
						// Is this post type active on this search?
1633
						$post_types = $query->get( 'post_type' );
1634
1635
						if ( ! is_array( $post_types ) ) {
1636
							$post_types = array( $post_types );
1637
						}
1638
1639 View Code Duplication
						if ( in_array( $item['key'], $post_types, true ) ) {
1640
							$active = true;
1641
1642
							$post_type_count = count( $post_types );
1643
1644
							// 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.
1645
							if ( $post_type_count > 1 ) {
1646
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1647
									'post_type',
1648
									rawurlencode( implode( ',', array_diff( $post_types, array( $item['key'] ) ) ) )
1649
								);
1650
							} else {
1651
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
1652
							}
1653
						}
1654
1655
						break;
1656
1657
					case 'date_histogram':
1658
						$timestamp = $item['key'] / 1000;
1659
1660
						$current_year  = $query->get( 'year' );
1661
						$current_month = $query->get( 'monthnum' );
1662
						$current_day   = $query->get( 'day' );
1663
1664
						switch ( $this->aggregations[ $label ]['interval'] ) {
1665
							case 'year':
1666
								$year = (int) gmdate( 'Y', $timestamp );
1667
1668
								$query_vars = array(
1669
									'year'     => $year,
1670
									'monthnum' => false,
1671
									'day'      => false,
1672
								);
1673
1674
								$name = $year;
1675
1676
								// Is this year currently selected?
1677
								if ( ! empty( $current_year ) && (int) $current_year === $year ) {
1678
									$active = true;
1679
1680
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
1681
								}
1682
1683
								break;
1684
1685
							case 'month':
1686
								$year  = (int) gmdate( 'Y', $timestamp );
1687
								$month = (int) gmdate( 'n', $timestamp );
1688
1689
								$query_vars = array(
1690
									'year'     => $year,
1691
									'monthnum' => $month,
1692
									'day'      => false,
1693
								);
1694
1695
								$name = gmdate( 'F Y', $timestamp );
1696
1697
								// Is this month currently selected?
1698
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1699
									! empty( $current_month ) && (int) $current_month === $month ) {
1700
									$active = true;
1701
1702
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
1703
								}
1704
1705
								break;
1706
1707
							case 'day':
1708
								$year  = (int) gmdate( 'Y', $timestamp );
1709
								$month = (int) gmdate( 'n', $timestamp );
1710
								$day   = (int) gmdate( 'j', $timestamp );
1711
1712
								$query_vars = array(
1713
									'year'     => $year,
1714
									'monthnum' => $month,
1715
									'day'      => $day,
1716
								);
1717
1718
								$name = gmdate( 'F jS, Y', $timestamp );
1719
1720
								// Is this day currently selected?
1721
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1722
									! empty( $current_month ) && (int) $current_month === $month &&
1723
									! empty( $current_day ) && (int) $current_day === $day ) {
1724
									$active = true;
1725
1726
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
1727
								}
1728
1729
								break;
1730
1731
							default:
1732
								continue 3; // switch() is considered a looping structure.
1733
						} // End switch.
1734
1735
						break;
1736
1737
					default:
1738
						// continue 2; // switch() is considered a looping structure.
1739
				} // End switch.
1740
1741
				// Need to urlencode param values since add_query_arg doesn't.
1742
				$url_params = urlencode_deep( $query_vars );
1743
1744
				$aggregation_data[ $label ]['buckets'][] = array(
1745
					'url'        => Jetpack_Search_Helpers::add_query_arg( $url_params ),
1746
					'query_vars' => $query_vars,
1747
					'name'       => $name,
1748
					'count'      => $item['doc_count'],
1749
					'active'     => $active,
1750
					'remove_url' => $remove_url,
1751
					'type'       => $type,
1752
					'type_label' => $aggregation_data[ $label ]['name'],
1753
					'widget_id'  => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0,
1754
				);
1755
			} // End foreach.
1756
		} // End foreach.
1757
1758
		/**
1759
		 * Modify the aggregation filters returned by get_filters().
1760
		 *
1761
		 * Useful if you are setting custom filters outside of the supported filters (taxonomy, post_type etc.) and
1762
		 * want to hook them up so they're returned when you call `get_filters()`.
1763
		 *
1764
		 * @module search
1765
		 *
1766
		 * @since  6.9.0
1767
		 *
1768
		 * @param array    $aggregation_data The array of filters keyed on label.
1769
		 * @param WP_Query $query            The WP_Query object.
1770
		 */
1771
		return apply_filters( 'jetpack_search_get_filters', $aggregation_data, $query );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $query.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1772
	}
1773
1774
	/**
1775
	 * Get the results of the facets performed.
1776
	 *
1777
	 * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead.
1778
	 *
1779
	 * @see        Jetpack_Search::get_filters()
1780
	 *
1781
	 * @return array $facets Array of facets applied and info about them.
1782
	 */
1783
	public function get_search_facet_data() {
1784
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
1785
1786
		return $this->get_filters();
1787
	}
1788
1789
	/**
1790
	 * Get the filters that are currently applied to this search.
1791
	 *
1792
	 * @since 5.0.0
1793
	 *
1794
	 * @return array Array of filters that were applied.
1795
	 */
1796
	public function get_active_filter_buckets() {
1797
		$active_buckets = array();
1798
1799
		$filters = $this->get_filters();
1800
1801
		if ( ! is_array( $filters ) ) {
1802
			return $active_buckets;
1803
		}
1804
1805
		foreach ( $filters as $filter ) {
1806
			if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
1807
				foreach ( $filter['buckets'] as $item ) {
1808
					if ( isset( $item['active'] ) && $item['active'] ) {
1809
						$active_buckets[] = $item;
1810
					}
1811
				}
1812
			}
1813
		}
1814
1815
		return $active_buckets;
1816
	}
1817
1818
	/**
1819
	 * Get the filters that are currently applied to this search.
1820
	 *
1821
	 * @deprecated 5.0 Please use Jetpack_Search::get_active_filter_buckets() instead.
1822
	 *
1823
	 * @see        Jetpack_Search::get_active_filter_buckets()
1824
	 *
1825
	 * @return array Array of filters that were applied.
1826
	 */
1827
	public function get_current_filters() {
1828
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
1829
1830
		return $this->get_active_filter_buckets();
1831
	}
1832
1833
	/**
1834
	 * Calculate the right query var to use for a given taxonomy.
1835
	 *
1836
	 * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter.
1837
	 *
1838
	 * @since 5.0.0
1839
	 *
1840
	 * @param string $taxonomy_name The name of the taxonomy for which to get the query var.
1841
	 *
1842
	 * @return bool|string The query var to use for this taxonomy, or false if none found.
1843
	 */
1844
	public function get_taxonomy_query_var( $taxonomy_name ) {
1845
		$taxonomy = get_taxonomy( $taxonomy_name );
1846
1847
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1848
			return false;
1849
		}
1850
1851
		/**
1852
		 * Modify the query var to use for a given taxonomy
1853
		 *
1854
		 * @module search
1855
		 *
1856
		 * @since  5.0.0
1857
		 *
1858
		 * @param string $query_var     The current query_var for the taxonomy
1859
		 * @param string $taxonomy_name The taxonomy name
1860
		 */
1861
		return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $taxonomy_name.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1862
	}
1863
1864
	/**
1865
	 * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
1866
	 * which is the input order.
1867
	 *
1868
	 * Necessary because ES does not always return aggregations in the same order that you pass them in,
1869
	 * and it should be possible to control the display order easily.
1870
	 *
1871
	 * @since 5.0.0
1872
	 *
1873
	 * @param array $aggregations Aggregation results to be reordered.
1874
	 * @param array $desired      Array with keys representing the desired ordering.
1875
	 *
1876
	 * @return array A new array with reordered keys, matching those in $desired.
1877
	 */
1878
	public function fix_aggregation_ordering( array $aggregations, array $desired ) {
1879
		if ( empty( $aggregations ) || empty( $desired ) ) {
1880
			return $aggregations;
1881
		}
1882
1883
		$reordered = array();
1884
1885
		foreach ( array_keys( $desired ) as $agg_name ) {
1886
			if ( isset( $aggregations[ $agg_name ] ) ) {
1887
				$reordered[ $agg_name ] = $aggregations[ $agg_name ];
1888
			}
1889
		}
1890
1891
		return $reordered;
1892
	}
1893
1894
	/**
1895
	 * Sends events to Tracks when a search filters widget is updated.
1896
	 *
1897
	 * @since 5.8.0
1898
	 *
1899
	 * @param string $option    The option name. Only "widget_jetpack-search-filters" is cared about.
1900
	 * @param array  $old_value The old option value.
1901
	 * @param array  $new_value The new option value.
1902
	 */
1903
	public function track_widget_updates( $option, $old_value, $new_value ) {
1904
		if ( 'widget_jetpack-search-filters' !== $option ) {
1905
			return;
1906
		}
1907
1908
		$event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value );
1909
		if ( ! $event ) {
1910
			return;
1911
		}
1912
1913
		$tracking = new Automattic\Jetpack\Tracking();
1914
		$tracking->tracks_record_event(
1915
			wp_get_current_user(),
1916
			sprintf( 'jetpack_search_widget_%s', $event['action'] ),
1917
			$event['widget']
1918
		);
1919
	}
1920
1921
	/**
1922
	 * Moves any active search widgets to the inactive category.
1923
	 *
1924
	 * @since 5.9.0
1925
	 */
1926
	public function move_search_widgets_to_inactive() {
1927
		if ( ! is_active_widget( false, false, Jetpack_Search_Helpers::FILTER_WIDGET_BASE, true ) ) {
1928
			return;
1929
		}
1930
1931
		$sidebars_widgets = wp_get_sidebars_widgets();
1932
1933
		if ( ! is_array( $sidebars_widgets ) ) {
1934
			return;
1935
		}
1936
1937
		$changed = false;
1938
1939
		foreach ( $sidebars_widgets as $sidebar => $widgets ) {
1940
			if ( 'wp_inactive_widgets' === $sidebar || 'orphaned_widgets' === substr( $sidebar, 0, 16 ) ) {
1941
				continue;
1942
			}
1943
1944
			if ( is_array( $widgets ) ) {
1945
				foreach ( $widgets as $key => $widget ) {
1946
					if ( _get_widget_id_base( $widget ) === Jetpack_Search_Helpers::FILTER_WIDGET_BASE ) {
1947
						$changed = true;
1948
1949
						array_unshift( $sidebars_widgets['wp_inactive_widgets'], $widget );
1950
						unset( $sidebars_widgets[ $sidebar ][ $key ] );
1951
					}
1952
				}
1953
			}
1954
		}
1955
1956
		if ( $changed ) {
1957
			wp_set_sidebars_widgets( $sidebars_widgets );
1958
		}
1959
	}
1960
1961
	/**
1962
	 * Determine whether a given WP_Query should be handled by ElasticSearch.
1963
	 *
1964
	 * @param WP_Query $query The WP_Query object.
1965
	 *
1966
	 * @return bool
1967
	 */
1968
	public function should_handle_query( $query ) {
1969
		/**
1970
		 * Determine whether a given WP_Query should be handled by ElasticSearch.
1971
		 *
1972
		 * @module search
1973
		 *
1974
		 * @since  5.6.0
1975
		 *
1976
		 * @param bool     $should_handle Should be handled by Jetpack Search.
1977
		 * @param WP_Query $query         The WP_Query object.
1978
		 */
1979
		return apply_filters( 'jetpack_search_should_handle_query', $query->is_main_query() && $query->is_search(), $query );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $query.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1980
	}
1981
1982
	/**
1983
	 * Transforms an array with fields name as keys and boosts as value into
1984
	 * shorthand "caret" format.
1985
	 *
1986
	 * @param array $fields_boost [ "title" => "2", "content" => "1" ].
1987
	 *
1988
	 * @return array [ "title^2", "content^1" ]
1989
	 */
1990
	private function _get_caret_boosted_fields( array $fields_boost ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
1991
		$caret_boosted_fields = array();
1992
		foreach ( $fields_boost as $field => $boost ) {
1993
			$caret_boosted_fields[] = "$field^$boost";
1994
		}
1995
		return $caret_boosted_fields;
1996
	}
1997
1998
	/**
1999
	 * Apply a multiplier to boost values.
2000
	 *
2001
	 * @param array $fields_boost [ "title" => 2, "content" => 1 ].
2002
	 * @param array $fields_boost_multiplier [ "title" => 0.1234 ].
2003
	 *
2004
	 * @return array [ "title" => "0.247", "content" => "1.000" ]
2005
	 */
2006
	private function _apply_boosts_multiplier( array $fields_boost, array $fields_boost_multiplier ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
2007
		foreach ( $fields_boost as $field_name => $field_boost ) {
2008
			if ( isset( $fields_boost_multiplier[ $field_name ] ) ) {
2009
				$fields_boost[ $field_name ] *= $fields_boost_multiplier[ $field_name ];
2010
			}
2011
2012
			// Set a floor and format the number as string.
2013
			$fields_boost[ $field_name ] = number_format(
2014
				max( 0.001, $fields_boost[ $field_name ] ),
2015
				3,
2016
				'.',
2017
				''
2018
			);
2019
		}
2020
2021
		return $fields_boost;
2022
	}
2023
}
2024