Completed
Push — update/check-node-version ( cabdb0...7f66bd )
by
unknown
11:21
created

Jetpack_Search::__wakeup()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for improperly formatted assignments.

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

To illustrate:

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

will have no issues, while

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

will report issues in lines 1 and 2.

Loading history...
398
399
400
		/**
401
		 * Modify the search query parameters, such as controlling the post_type.
402
		 *
403
		 * These arguments are in the format of WP_Query arguments
404
		 *
405
		 * @module search
406
		 *
407
		 * @since 5.0.0
408
		 *
409
		 * @param array $es_wp_query_args The current query args, in WP_Query format
410
		 * @param WP_Query $query The original query object
411
		 */
412
		$es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
413
414
		// If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
415
		// capped at $this->max_offset, so a high page would always return the last page of results otherwise
416
		if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $this->max_offset ) {
417
			$query->set_404();
418
419
			return;
420
		}
421
422
		// If there were no post types returned, then 404 to avoid querying against non-public post types, which could
423
		// happen if we don't add the post type restriction to the ES query
424
		if ( empty( $es_wp_query_args['post_type'] ) ) {
425
			$query->set_404();
426
427
			return;
428
		}
429
430
		// Convert the WP-style args into ES args
431
		$es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
432
433
		//Only trust ES to give us IDs, not the content since it is a mirror
434
		$es_query_args['fields'] = array(
435
			'post_id',
436
		);
437
438
		/**
439
		 * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
440
		 *
441
		 * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
442
		 *
443
		 * @module search
444
		 *
445
		 * @since 5.0.0
446
		 *
447
		 * @param array $es_query_args The raw ES query args
448
		 * @param WP_Query $query The original query object
449
		 */
450
		$es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
451
452
		// Do the actual search query!
453
		$this->search_result = $this->search( $es_query_args );
454
455
		if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
456
			$this->found_posts = 0;
457
458
			return;
459
		}
460
461
		// If we have aggregations, fix the ordering to match the input order (ES doesn't guarantee the return order)
462
		if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
463
			$this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
464
		}
465
466
		// Total number of results for paging purposes. Capped at $this->>max_offset + $posts_per_page, as deep paging
467
		// gets quite expensive
468
		$this->found_posts = min( $this->search_result['results']['total'], $this->max_offset + $posts_per_page );
469
470
		return;
471
	}
472
473
	/**
474
	 * If the query has already been run before filters have been updated, then we need to re-run the query
475
	 * to get the latest aggregations.
476
	 *
477
	 * This is especially useful for supporting widget management in the customizer.
478
	 *
479
	 * @return bool Whether the query was successful or not.
480
	 */
481
	public function update_search_results_aggregations() {
482
		if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
483
			return false;
484
		}
485
486
		$es_args = $this->last_query_info['args'];
487
		$builder = new Jetpack_WPES_Query_Builder();
488
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
489
		$es_args['aggregations'] = $builder->build_aggregation();
490
491
		$this->search_result = $this->search( $es_args );
492
493
		return ! is_wp_error( $this->search_result );
494
	}
495
496
	/**
497
	 * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style ES term arguments for the search
498
	 *
499
	 * @module search
500
	 *
501
	 * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query
502
	 *
503
	 * @return array The new WP-style ES arguments (that will be converted into 'real' ES arguments)
504
	 */
505
	public function get_es_wp_query_terms_for_query( WP_Query $query ) {
506
		$args = array();
507
508
		$the_tax_query = $query->tax_query;
509
510
		if ( ! $the_tax_query ) {
511
			return $args;
512
		}
513
514
515
		if ( ! $the_tax_query instanceof WP_Tax_Query || empty( $the_tax_query->queried_terms ) || ! is_array( $the_tax_query->queried_terms ) ) {
0 ignored issues
show
Bug introduced by
The class WP_Tax_Query does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
516
			return $args;
517
		}
518
519
		$args = array();
520
521
		foreach ( $the_tax_query->queries as $tax_query ) {
522
			// Right now we only support slugs...see note above
523
			if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
524
				continue;
525
			}
526
527
			$taxonomy = $tax_query['taxonomy'];
528
529 View Code Duplication
			if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
530
				$args[ $taxonomy ] = array();
531
			}
532
533
			$args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
534
		}
535
536
		return $args;
537
	}
538
539
	/**
540
	 * Parse out the post type from a WP_Query
541
	 *
542
	 * Only allows post types that are not marked as 'exclude_from_search'
543
	 *
544
	 * @module search
545
	 *
546
	 * @param WP_Query $query Original WP_Query object
547
	 *
548
	 * @return array Array of searchable post types corresponding to the original query
549
	 */
550
	public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
551
		$post_types = $query->get( 'post_type' );
552
553
		// If we're searching 'any', we want to only pass searchable post types to ES
554
		if ( 'any' === $post_types ) {
555
			$post_types = array_values( get_post_types( array(
556
				'exclude_from_search' => false,
557
			) ) );
558
		}
559
560
		if ( ! is_array( $post_types ) ) {
561
			$post_types = array( $post_types );
562
		}
563
564
		$post_types = array_unique( $post_types );
565
566
		$sanitized_post_types = array();
567
568
		// Make sure the post types are queryable
569
		foreach ( $post_types as $post_type ) {
570
			if ( ! $post_type ) {
571
				continue;
572
			}
573
574
			$post_type_object = get_post_type_object( $post_type );
575
			if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
576
				continue;
577
			}
578
579
			$sanitized_post_types[] = $post_type;
580
		}
581
582
		return $sanitized_post_types;
583
	}
584
585
	/**
586
	 * Initialize widgets for the Search module
587
	 *
588
	 * @module search
589
	 */
590
	public function action__widgets_init() {
591
		require_once( dirname( __FILE__ ) . '/class.jetpack-search-widget-filters.php' );
592
593
		register_widget( 'Jetpack_Search_Widget_Filters' );
594
	}
595
596
	/**
597
	 * Get the Elasticsearch result
598
	 *
599
	 * @module search
600
	 *
601
	 * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response
602
	 *
603
	 * @return array|bool The search results, or false if there was a failure
604
	 */
605
	public function get_search_result( $raw = false ) {
606
		if ( $raw ) {
607
			return $this->search_result;
608
		}
609
610
		return ( ! empty( $this->search_result ) && ! is_wp_error( $this->search_result ) && is_array( $this->search_result ) && ! empty( $this->search_result['results'] ) ) ? $this->search_result['results'] : false;
611
	}
612
613
	/**
614
	 * Add the date portion of a WP_Query onto the query args
615
	 *
616
	 * @param array    $es_wp_query_args
617
	 * @param WP_Query $query The original WP_Query
618
	 *
619
	 * @return array The es wp query args, with date filters added (as needed)
620
	 */
621
	public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
622
		if ( $query->get( 'year' ) ) {
623
			if ( $query->get( 'monthnum' ) ) {
624
				// Padding
625
				$date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
626
627
				if ( $query->get( 'day' ) ) {
628
					// Padding
629
					$date_day = sprintf( '%02d', $query->get( 'day' ) );
630
631
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
632
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
633
				} else {
634
					$days_in_month = date( 't', mktime( 0, 0, 0, $query->get( 'monthnum' ), 14, $query->get( 'year' ) ) ); // 14 = middle of the month so no chance of DST issues
635
636
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
637
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
638
				}
639
			} else {
640
				$date_start = $query->get( 'year' ) . '-01-01 00:00:00';
641
				$date_end   = $query->get( 'year' ) . '-12-31 23:59:59';
642
			}
643
644
			$es_wp_query_args['date_range'] = array(
645
				'field' => 'date',
646
				'gte'   => $date_start,
647
				'lte'   => $date_end,
648
			);
649
		}
650
651
		return $es_wp_query_args;
652
	}
653
654
	/**
655
	 * Converts WP_Query style args to ES args
656
	 *
657
	 * @module search
658
	 *
659
	 * @param array $args Array of WP_Query style arguments
660
	 *
661
	 * @return array Array of ES style query arguments
662
	 */
663
	function convert_wp_es_to_es_args( array $args ) {
664
		jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-parser' );
665
666
		$defaults = array(
667
			'blog_id'        => get_current_blog_id(),
668
669
			'query'          => null,    // Search phrase
670
			'query_fields'   => array( 'title', 'content', 'author', 'tag.name', 'category.name' ),
671
672
			'post_type'      => null,  // string or an array
673
			'terms'          => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) )
674
675
			'author'         => null,    // id or an array of ids
676
			'author_name'    => array(), // string or an array
677
678
			'date_range'     => null,    // array( 'field' => 'date', 'gt' => 'YYYY-MM-dd', 'lte' => 'YYYY-MM-dd' ); date formats: 'YYYY-MM-dd' or 'YYYY-MM-dd HH:MM:SS'
679
680
			'orderby'        => null,    // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
681
			'order'          => 'DESC',
682
683
			'posts_per_page' => 10,
684
685
			'offset'         => null,
686
			'paged'          => null,
687
688
			/**
689
			 * Aggregations. Examples:
690
			 * array(
691
			 *     'Tag'       => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
692
			 *     'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
693
			 * );
694
			 */
695
			'aggregations'   => null,
696
		);
697
698
		$args = wp_parse_args( $args, $defaults );
699
700
		$parser = new Jetpack_WPES_Search_Query_Parser( $args['query'], array( get_locale() ) );
701
702
		$match_content_fields = $parser->merge_ml_fields(
703
			array(
704
				'title'    => 0.2,
705
				'content'  => 0.2,
706
				'author'   => 0.1,
707
				'tag'      => 0.1,
708
				'category' => 0.1,
709
			),
710
			array()
711
		);
712
713
		$boost_content_fields = $parser->merge_ml_fields(
714
			array(
715
				'title'       => 2,
716
				'description' => 1,
717
				'tags'        => 1,
718
			),
719
			array(
720
				'author_login^2',
721
				'author^2',
722
			)
723
		);
724
725
		$parser->phrase_filter( array(
726
			'must_query_fields'  => $match_content_fields,
727
			'boost_query_fields' => $boost_content_fields,
728
		) );
729
		$parser->remaining_query( array(
730
			'must_query_fields'  => $match_content_fields,
731
			'boost_query_fields' => $boost_content_fields,
732
		) );
733
734
		// Boost on phrases
735
		$parser->remaining_query( array(
736
			'boost_query_fields' => $boost_content_fields,
737
			'boost_query_type'   => 'phrase',
738
		) );
739
740
		// Newer content gets weighted slightly higher
741
		$parser->add_decay( 'gauss', array(
742
			'date_gmt' => array(
743
				'origin' => date( 'Y-m-d' ),
744
				'scale'  => '360d',
745
				'decay'  => 0.9,
746
			),
747
		));
748
749
		$es_query_args = array(
750
			'blog_id' => absint( $args['blog_id'] ),
751
			'size'    => absint( $args['posts_per_page'] ),
752
		);
753
754
		// ES "from" arg (offset)
755
		if ( $args['offset'] ) {
756
			$es_query_args['from'] = absint( $args['offset'] );
757
		} elseif ( $args['paged'] ) {
758
			$es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
759
		}
760
761
		// Limit the offset to $this->max_offset posts, as deep pages get exponentially slower
762
		// See https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html
763
		$es_query_args['from'] = min( $es_query_args['from'], $this->max_offset );
764
765
		if ( ! is_array( $args['author_name'] ) ) {
766
			$args['author_name'] = array( $args['author_name'] );
767
		}
768
769
		// ES stores usernames, not IDs, so transform
770
		if ( ! empty( $args['author'] ) ) {
771
			if ( ! is_array( $args['author'] ) ) {
772
				$args['author'] = array( $args['author'] );
773
			}
774
775
			foreach ( $args['author'] as $author ) {
776
				$user = get_user_by( 'id', $author );
777
778
				if ( $user && ! empty( $user->user_login ) ) {
779
					$args['author_name'][] = $user->user_login;
780
				}
781
			}
782
		}
783
784
		//////////////////////////////////////////////////
785
		// Build the filters from the query elements.
786
		// Filters rock because they are cached from one query to the next
787
		// but they are cached as individual filters, rather than all combined together.
788
		// May get performance boost by also caching the top level boolean filter too.
789
790
		if ( $args['post_type'] ) {
791
			if ( ! is_array( $args['post_type'] ) ) {
792
				$args['post_type'] = array( $args['post_type'] );
793
			}
794
795
			$parser->add_filter( array(
796
				'terms' => array(
797
					'post_type' => $args['post_type'],
798
				),
799
			) );
800
		}
801
802
		if ( $args['author_name'] ) {
803
			$parser->add_filter( array(
804
				'terms' => array(
805
					'author_login' => $args['author_name'],
806
				),
807
			) );
808
		}
809
810
		if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
811
			$field = $args['date_range']['field'];
812
813
			unset( $args['date_range']['field'] );
814
815
			$parser->add_filter( array(
816
				'range' => array(
817
					$field => $args['date_range'],
818
				),
819
			) );
820
		}
821
822
		if ( is_array( $args['terms'] ) ) {
823
			foreach ( $args['terms'] as $tax => $terms ) {
824
				$terms = (array) $terms;
825
826
				if ( count( $terms ) && mb_strlen( $tax ) ) {
827 View Code Duplication
					switch ( $tax ) {
828
						case 'post_tag':
829
							$tax_fld = 'tag.slug';
830
831
							break;
832
833
						case 'category':
834
							$tax_fld = 'category.slug';
835
836
							break;
837
838
						default:
839
							$tax_fld = 'taxonomy.' . $tax . '.slug';
840
841
							break;
842
					}
843
844
					foreach ( $terms as $term ) {
845
						$parser->add_filter( array(
846
							'term' => array(
847
								$tax_fld => $term,
848
							),
849
						) );
850
					}
851
				}
852
			}
853
		}
854
855
		if ( ! $args['orderby'] ) {
856
			if ( $args['query'] ) {
857
				$args['orderby'] = array( 'relevance' );
858
			} else {
859
				$args['orderby'] = array( 'date' );
860
			}
861
		}
862
863
		// Validate the "order" field
864
		switch ( strtolower( $args['order'] ) ) {
865
			case 'asc':
866
				$args['order'] = 'asc';
867
				break;
868
869
			case 'desc':
870
			default:
871
				$args['order'] = 'desc';
872
				break;
873
		}
874
875
		$es_query_args['sort'] = array();
876
877
		foreach ( (array) $args['orderby'] as $orderby ) {
878
			// Translate orderby from WP field to ES field
879
			switch ( $orderby ) {
880
				case 'relevance' :
881
					//never order by score ascending
882
					$es_query_args['sort'][] = array(
883
						'_score' => array(
884
							'order' => 'desc',
885
						),
886
					);
887
888
					break;
889
890 View Code Duplication
				case 'date' :
891
					$es_query_args['sort'][] = array(
892
						'date' => array(
893
							'order' => $args['order'],
894
						),
895
					);
896
897
					break;
898
899 View Code Duplication
				case 'ID' :
900
					$es_query_args['sort'][] = array(
901
						'id' => array(
902
							'order' => $args['order'],
903
						),
904
					);
905
906
					break;
907
908
				case 'author' :
909
					$es_query_args['sort'][] = array(
910
						'author.raw' => array(
911
							'order' => $args['order'],
912
						),
913
					);
914
915
					break;
916
			} // End switch().
917
		} // End foreach().
918
919
		if ( empty( $es_query_args['sort'] ) ) {
920
			unset( $es_query_args['sort'] );
921
		}
922
923
		$es_query_args['filter'] = $parser->build_filter();
924
		$es_query_args['query']  = $parser->build_query();
925
926
		// Aggregations
927
		if ( ! empty( $args['aggregations'] ) ) {
928
			$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...
929
930
			$es_query_args['aggregations'] = $parser->build_aggregation();
931
		}
932
933
		return $es_query_args;
934
	}
935
936
	/**
937
	 * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in ES
938
	 *
939
	 * @module search
940
	 *
941
	 * @param array $aggregations Array of Aggregations (filters) to add to the Jetpack_WPES_Query_Builder
942
	 *
943
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
944
	 */
945
	public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
946
		foreach ( $aggregations as $label => $aggregation ) {
947
			switch ( $aggregation['type'] ) {
948
				case 'taxonomy':
949
					$this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
950
951
					break;
952
953
				case 'post_type':
954
					$this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
955
956
					break;
957
958
				case 'date_histogram':
959
					$this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
960
961
					break;
962
			}
963
		}
964
	}
965
966
	/**
967
	 * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
968
	 *
969
	 * @module search
970
	 *
971
	 * @param array $aggregation The aggregation to add to the query builder
972
	 * @param string $label The 'label' (unique id) for this aggregation
973
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
974
	 */
975
	public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
976
		$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...
977
978
		switch ( $aggregation['taxonomy'] ) {
979
			case 'post_tag':
980
				$field = 'tag';
981
				break;
982
983
			case 'category':
984
				$field = 'category';
985
				break;
986
987
			default:
988
				$field = 'taxonomy.' . $aggregation['taxonomy'];
989
				break;
990
		}
991
992
		$builder->add_aggs( $label, array(
993
			'terms' => array(
994
				'field' => $field . '.slug',
995
				'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
996
			),
997
		));
998
	}
999
1000
	/**
1001
	 * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
1002
	 *
1003
	 * @module search
1004
	 *
1005
	 * @param array $aggregation The aggregation to add to the query builder
1006
	 * @param string $label The 'label' (unique id) for this aggregation
1007
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
1008
	 */
1009
	public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1010
		$builder->add_aggs( $label, array(
1011
			'terms' => array(
1012
				'field' => 'post_type',
1013
				'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1014
			),
1015
		));
1016
	}
1017
1018
	/**
1019
	 * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in ES
1020
	 *
1021
	 * @module search
1022
	 *
1023
	 * @param array $aggregation The aggregation to add to the query builder
1024
	 * @param string $label The 'label' (unique id) for this aggregation
1025
	 * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the ES query
1026
	 */
1027
	public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1028
		$args = array(
1029
			'interval' => $aggregation['interval'],
1030
			'field'    => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' == $aggregation['field'] ) ? 'date_gmt' : 'date',
1031
		);
1032
1033
		if ( isset( $aggregation['min_doc_count'] ) ) {
1034
			$args['min_doc_count'] = intval( $aggregation['min_doc_count'] );
1035
		} else {
1036
			$args['min_doc_count'] = 1;
1037
		}
1038
1039
		$builder->add_aggs( $label, array(
1040
			'date_histogram' => $args,
1041
		));
1042
	}
1043
1044
	/**
1045
	 * And an existing filter object with a list of additional filters.
1046
	 *
1047
	 * Attempts to optimize the filters somewhat.
1048
	 *
1049
	 * @module search
1050
	 *
1051
	 * @param array $curr_filter The existing filters to build upon
1052
	 * @param array $filters The new filters to add
1053
	 *
1054
	 * @return array The resulting merged filters
1055
	 */
1056
	public static function and_es_filters( array $curr_filter, array $filters ) {
1057
		if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
1058
			if ( 1 === count( $filters ) ) {
1059
				return $filters[0];
1060
			}
1061
1062
			return array(
1063
				'and' => $filters,
1064
			);
1065
		}
1066
1067
		return array(
1068
			'and' => array_merge( array( $curr_filter ), $filters ),
1069
		);
1070
	}
1071
1072
	/**
1073
	 * Set the available filters for the search
1074
	 *
1075
	 * These get rendered via the Jetpack_Search_Widget_Filters() widget
1076
	 *
1077
	 * Behind the scenes, these are implemented using Elasticsearch Aggregations.
1078
	 *
1079
	 * If you do not require counts of how many documents match each filter, please consider using regular WP Query
1080
	 * arguments instead, such as via the jetpack_search_es_wp_query_args filter
1081
	 *
1082
	 * @module search
1083
	 *
1084
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1085
	 *
1086
	 * @param array $aggregations Array of filters (aggregations) to apply to the search
1087
	 */
1088
	public function set_filters( array $aggregations ) {
1089
		foreach ( (array) $aggregations as $key => $agg ) {
1090
			if ( empty( $agg['name'] ) ) {
1091
				$aggregations[ $key ]['name'] = $key;
1092
			}
1093
		}
1094
		$this->aggregations = $aggregations;
1095
	}
1096
1097
	/**
1098
	 * Set the search's facets (deprecated)
1099
	 *
1100
	 * @module search
1101
	 *
1102
	 * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead
1103
	 *
1104
	 * @see Jetpack_Search::set_filters()
1105
	 *
1106
	 * @param array $facets Array of facets to apply to the search
1107
	 */
1108
	public function set_facets( array $facets ) {
1109
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
1110
1111
		$this->set_filters( $facets );
1112
	}
1113
1114
	/**
1115
	 * Get the raw Aggregation results from the ES response
1116
	 *
1117
	 * @module search
1118
	 *
1119
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1120
	 *
1121
	 * @return array Array of Aggregations performed on the search
1122
	 */
1123
	public function get_search_aggregations_results() {
1124
		$aggregations = array();
1125
1126
		$search_result = $this->get_search_result();
1127
1128
		if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
1129
			$aggregations = $search_result['aggregations'];
1130
		}
1131
1132
		return $aggregations;
1133
	}
1134
1135
	/**
1136
	 * Get the raw Facet results from the ES response
1137
	 *
1138
	 * @module search
1139
	 *
1140
	 * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead
1141
	 *
1142
	 * @see Jetpack_Search::get_search_aggregations_results()
1143
	 *
1144
	 * @return array Array of Facets performed on the search
1145
	 */
1146
	public function get_search_facets() {
1147
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
1148
1149
		return $this->get_search_aggregations_results();
1150
	}
1151
1152
	/**
1153
	 * Get the results of the Filters performed, including the number of matching documents
1154
	 *
1155
	 * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
1156
	 * matching buckets, the url for applying/removing each bucket, etc.
1157
	 *
1158
	 * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
1159
	 * member if you need to access the raw filters set in Jetpack_Search::set_filters()
1160
	 *
1161
	 * @module search
1162
	 *
1163
	 * @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...
1164
	 *
1165
	 * @return array Array of Filters applied and info about them
1166
	 */
1167
	public function get_filters( WP_Query $query = null ) {
1168
		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...
1169
			global $wp_query;
1170
1171
			$query = $wp_query;
1172
		}
1173
1174
		$aggregation_data = $this->aggregations;
1175
1176
		if ( empty( $aggregation_data ) ) {
1177
			return $aggregation_data;
1178
		}
1179
1180
		$aggregation_results = $this->get_search_aggregations_results();
1181
1182
		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...
1183
			return $aggregation_data;
1184
		}
1185
1186
		// NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES
1187
		foreach ( $aggregation_results as $label => $aggregation ) {
1188
			if ( empty( $aggregation ) ) {
1189
				continue;
1190
			}
1191
1192
			$type = $this->aggregations[ $label ]['type'];
1193
1194
			$aggregation_data[ $label ]['buckets'] = array();
1195
1196
			$existing_term_slugs = array();
1197
1198
			$tax_query_var = null;
1199
1200
			// Figure out which terms are active in the query, for this taxonomy
1201
			if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
1202
				$tax_query_var = $this->get_taxonomy_query_var(  $this->aggregations[ $label ]['taxonomy'] );
1203
1204
				if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
1205
					foreach( $query->tax_query->queries as $tax_query ) {
1206
						if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
1207
						     'slug' === $tax_query['field'] &&
1208
						     is_array( $tax_query['terms'] ) ) {
1209
							$existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
1210
						}
1211
					}
1212
				}
1213
			}
1214
1215
			// Now take the resulting found aggregation items and generate the additional info about them, such as
1216
			// activation/deactivation url, name, count, etc
1217
			$buckets = array();
1218
1219
			if ( ! empty( $aggregation['buckets'] ) ) {
1220
				$buckets = (array) $aggregation['buckets'];
1221
			}
1222
1223
			if ( 'date_histogram' == $type ) {
1224
				//re-order newest to oldest
1225
				$buckets = array_reverse( $buckets );
1226
			}
1227
1228
			// Some aggregation types like date_histogram don't support the max results parameter
1229
			if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
1230
				$buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
1231
			}
1232
1233
			foreach ( $buckets as $item ) {
1234
				$query_vars = array();
1235
				$active     = false;
1236
				$remove_url = null;
1237
				$name       = '';
1238
1239
				// What type was the original aggregation?
1240
				switch ( $type ) {
1241
					case 'taxonomy':
1242
						$taxonomy = $this->aggregations[ $label ]['taxonomy'];
1243
1244
						$term = get_term_by( 'slug', $item['key'], $taxonomy );
1245
1246
						if ( ! $term || ! $tax_query_var ) {
1247
							continue 2; // switch() is considered a looping structure
1248
						}
1249
1250
						$query_vars = array(
1251
							$tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
1252
						);
1253
1254
						$name = $term->name;
1255
1256
						// Let's determine if this term is active or not
1257
1258
						if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
1259
							$active = true;
1260
1261
							$slug_count = count( $existing_term_slugs );
1262
1263
							if ( $slug_count > 1 ) {
1264
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1265
									$tax_query_var,
1266
									urlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) )
0 ignored issues
show
Documentation introduced by
urlencode(implode('+', a... array($item['key'])))) is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1267
								);
1268
							} else {
1269
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( $tax_query_var );
1270
							}
1271
						}
1272
1273
						break;
1274
1275
					case 'post_type':
1276
						$post_type = get_post_type_object( $item['key'] );
1277
1278
						if ( ! $post_type || $post_type->exclude_from_search ) {
1279
							continue 2;  // switch() is considered a looping structure
1280
						}
1281
1282
						$query_vars = array(
1283
							'post_type' => $item['key'],
1284
						);
1285
1286
						$name = $post_type->labels->singular_name;
1287
1288
						// Is this post type active on this search?
1289
						$post_types = $query->get( 'post_type' );
1290
1291
						if ( ! is_array( $post_types ) ) {
1292
							$post_types = array( $post_types );
1293
						}
1294
1295
						if ( in_array( $item['key'], $post_types ) ) {
1296
							$active = true;
1297
1298
							$post_type_count = count( $post_types );
1299
1300
							// 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
1301
							if ( $post_type_count > 1 ) {
1302
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1303
									'post_type',
1304
									implode( ',',  array_diff( $post_types, array( $item['key'] ) ) )
0 ignored issues
show
Documentation introduced by
implode(',', array_diff(..., array($item['key']))) is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1305
								);
1306
							} else {
1307
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
1308
							}
1309
						}
1310
1311
						break;
1312
1313
					case 'date_histogram':
1314
						$timestamp = $item['key'] / 1000;
1315
1316
						$current_year  = $query->get( 'year' );
1317
						$current_month = $query->get( 'monthnum' );
1318
						$current_day   = $query->get( 'day' );
1319
1320
						switch ( $this->aggregations[ $label ]['interval'] ) {
1321
							case 'year':
1322
								$year = (int) date( 'Y', $timestamp );
1323
1324
								$query_vars = array(
1325
									'year'     => $year,
1326
									'monthnum' => false,
1327
									'day'      => false,
1328
								);
1329
1330
								$name = $year;
1331
1332
								// Is this year currently selected?
1333
								if ( ! empty( $current_year ) && (int) $current_year === $year ) {
1334
									$active = true;
1335
1336
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
1337
								}
1338
1339
								break;
1340
1341
							case 'month':
1342
								$year  = (int) date( 'Y', $timestamp );
1343
								$month = (int) date( 'n', $timestamp );
1344
1345
								$query_vars = array(
1346
									'year'     => $year,
1347
									'monthnum' => $month,
1348
									'day'      => false,
1349
								);
1350
1351
								$name = date( 'F Y', $timestamp );
1352
1353
								// Is this month currently selected?
1354
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1355
								     ! empty( $current_month ) && (int) $current_month === $month ) {
1356
									$active = true;
1357
1358
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
1359
								}
1360
1361
								break;
1362
1363
							case 'day':
1364
								$year  = (int) date( 'Y', $timestamp );
1365
								$month = (int) date( 'n', $timestamp );
1366
								$day   = (int) date( 'j', $timestamp );
1367
1368
								$query_vars = array(
1369
									'year'     => $year,
1370
									'monthnum' => $month,
1371
									'day'      => $day,
1372
								);
1373
1374
								$name = date( 'F jS, Y', $timestamp );
1375
1376
								// Is this day currently selected?
1377
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1378
								     ! empty( $current_month ) && (int) $current_month === $month &&
1379
								     ! empty( $current_day ) && (int) $current_day === $day ) {
1380
									$active = true;
1381
1382
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
1383
								}
1384
1385
								break;
1386
1387
							default:
1388
								continue 3; // switch() is considered a looping structure
1389
						} // End switch().
1390
1391
						break;
1392
1393
					default:
1394
						//continue 2; // switch() is considered a looping structure
1395
				} // End switch().
1396
1397
				// Need to urlencode param values since add_query_arg doesn't
1398
				$url_params = urlencode_deep( $query_vars );
1399
1400
				$aggregation_data[ $label ]['buckets'][] = array(
1401
					'url'        => Jetpack_Search_Helpers::add_query_arg( $url_params ),
1402
					'query_vars' => $query_vars,
1403
					'name'       => $name,
1404
					'count'      => $item['doc_count'],
1405
					'active'     => $active,
1406
					'remove_url' => $remove_url,
1407
					'type'       => $type,
1408
					'type_label' => $aggregation_data[ $label ]['name'],
1409
					'widget_id'  => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0
1410
				);
1411
			} // End foreach().
1412
		} // End foreach().
1413
1414
		return $aggregation_data;
1415
	}
1416
1417
	/**
1418
	 * Get the results of the Facets performed
1419
	 *
1420
	 * @module search
1421
	 *
1422
	 * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead
1423
	 *
1424
	 * @see Jetpack_Search::get_filters()
1425
	 *
1426
	 * @return array $facets Array of Facets applied and info about them
1427
	 */
1428
	public function get_search_facet_data() {
1429
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
1430
1431
		return $this->get_filters();
1432
	}
1433
1434
	/**
1435
	 * Get the Filters that are currently applied to this search
1436
	 *
1437
	 * @module search
1438
	 *
1439
	 * @return array Array if Filters that were applied
1440
	 */
1441
	public function get_active_filter_buckets() {
1442
		$active_buckets = array();
1443
1444
		$filters = $this->get_filters();
1445
1446
		if ( ! is_array( $filters ) ) {
1447
			return $active_buckets;
1448
		}
1449
1450
		foreach( $filters as $filter ) {
1451
			if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
1452
				foreach( $filter['buckets'] as $item ) {
1453
					if ( isset( $item['active'] ) && $item['active'] ) {
1454
						$active_buckets[] = $item;
1455
					}
1456
				}
1457
			}
1458
		}
1459
1460
		return $active_buckets;
1461
	}
1462
1463
	/**
1464
	 * Get the Filters that are currently applied to this search
1465
	 *
1466
	 * @module search
1467
	 *
1468
	 * @return array Array if Filters that were applied
1469
	 */
1470
	public function get_current_filters() {
1471
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
1472
1473
		return $this->get_active_filter_buckets();
1474
	}
1475
1476
	/**
1477
	 * Calculate the right query var to use for a given taxonomy
1478
	 *
1479
	 * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter
1480
	 *
1481
	 * @module search
1482
	 *
1483
	 * @param string $taxonomy_name The name of the taxonomy for which to get the query var
1484
	 *
1485
	 * @return bool|string The query var to use for this taxonomy, or false if none found
1486
	 */
1487
	public function get_taxonomy_query_var( $taxonomy_name ) {
1488
		$taxonomy = get_taxonomy( $taxonomy_name );
1489
1490
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1491
			return false;
1492
		}
1493
1494
		/**
1495
		 * Modify the query var to use for a given taxonomy
1496
		 *
1497
		 * @module search
1498
		 *
1499
		 * @since 5.0.0
1500
		 *
1501
		 * @param string $query_var The current query_var for the taxonomy
1502
		 * @param string $taxonomy_name The taxonomy name
1503
		 */
1504
		return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
1505
	}
1506
1507
	/**
1508
	 * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
1509
	 * which is the input order
1510
	 *
1511
	 * Necessary because ES does not always return Aggs in the same order that you pass them in, and it should be possible
1512
	 * to control the display order easily
1513
	 *
1514
	 * @module search
1515
	 *
1516
	 * @param array $aggregations Agg results to be reordered
1517
	 * @param array $desired Array with keys representing the desired ordering
1518
	 *
1519
	 * @return array A new array with reordered keys, matching those in $desired
1520
	 */
1521
	public function fix_aggregation_ordering( array $aggregations, array $desired ) {
1522
		if ( empty( $aggregations ) || empty( $desired ) ) {
1523
			return $aggregations;
1524
		}
1525
1526
		$reordered = array();
1527
1528
		foreach( array_keys( $desired ) as $agg_name ) {
1529
			if ( isset( $aggregations[ $agg_name ] ) ) {
1530
				$reordered[ $agg_name ] = $aggregations[ $agg_name ];
1531
			}
1532
		}
1533
1534
		return $reordered;
1535
	}
1536
1537
	public function track_widget_updates( $option, $old_value, $new_value ) {
1538
		if ( 'widget_jetpack-search-filters' !== $option ) {
1539
			return;
1540
		}
1541
1542
		$event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value );
1543
		if ( ! $event ) {
1544
			return;
1545
		}
1546
1547
		jetpack_tracks_record_event(
1548
			wp_get_current_user(),
1549
			sprintf( 'jetpack_search_widget_%s', $event['action'] ),
1550
			$event['widget']
1551
		);
1552
	}
1553
}
1554