Completed
Push — add/layout-for-widgetless-over... ( 1fed47...f2b0fc )
by
unknown
10:50 queued 03:04
created

Jetpack_Instant_Search::action__parse_query()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 1
dl 0
loc 25
rs 9.52
c 0
b 0
f 0
1
<?php
2
/**
3
 * Jetpack Search: Instant Front-End Search and Filtering
4
 *
5
 * @since 8.3.0
6
 * @package jetpack
7
 */
8
9
use Automattic\Jetpack\Connection\Client;
10
use Automattic\Jetpack\Constants;
11
12
/**
13
 * Class to load Instant Search experience on the site.
14
 *
15
 * @since 8.3.0
16
 */
17
class Jetpack_Instant_Search extends Jetpack_Search {
18
19
	/**
20
	 * Loads the php for this version of search
21
	 *
22
	 * @since 8.3.0
23
	 */
24
	public function load_php() {
25
		$this->base_load_php();
26
27
		if ( class_exists( 'WP_Customize_Manager' ) ) {
28
			require_once dirname( __FILE__ ) . '/class-jetpack-search-customize.php';
29
			new Jetpack_Search_Customize();
30
		}
31
	}
32
33
	/**
34
	 * Setup the various hooks needed for the plugin to take over search duties.
35
	 *
36
	 * @since 5.0.0
37
	 */
38 View Code Duplication
	public function init_hooks() {
39
		if ( ! is_admin() ) {
40
			add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 );
41
			add_action( 'parse_query', array( $this, 'action__parse_query' ), 10, 1 );
42
43
			add_action( 'init', array( $this, 'set_filters_from_widgets' ) );
44
45
			add_action( 'wp_enqueue_scripts', array( $this, 'load_assets' ) );
46
			add_action( 'wp_footer', array( $this, 'print_instant_search_sidebar' ) );
47
		} else {
48
			add_action( 'update_option', array( $this, 'track_widget_updates' ), 10, 3 );
49
		}
50
51
		add_action( 'widgets_init', array( $this, 'register_jetpack_instant_sidebar' ) );
52
		add_action( 'jetpack_deactivate_module_search', array( $this, 'move_search_widgets_to_inactive' ) );
53
	}
54
55
	/**
56
	 * Loads assets for Jetpack Instant Search Prototype featuring Search As You Type experience.
57
	 */
58
	public function load_assets() {
59
		$script_relative_path = '_inc/build/instant-search/jp-search.bundle.js';
60
		$style_relative_path  = '_inc/build/instant-search/instant-search.min.css';
61
		if ( ! file_exists( JETPACK__PLUGIN_DIR . $script_relative_path ) || ! file_exists( JETPACK__PLUGIN_DIR . $style_relative_path ) ) {
62
			return;
63
		}
64
65
		$script_version = Jetpack_Search_Helpers::get_asset_version( $script_relative_path );
66
		$script_path    = plugins_url( $script_relative_path, JETPACK__PLUGIN_FILE );
67
		wp_enqueue_script( 'jetpack-instant-search', $script_path, array(), $script_version, true );
68
		wp_set_script_translations( 'jetpack-instant-search', 'jetpack' );
69
		$this->load_and_initialize_tracks();
70
		$this->inject_javascript_options();
71
72
		$style_version = Jetpack_Search_Helpers::get_asset_version( $style_relative_path );
73
		$style_path    = plugins_url( $style_relative_path, JETPACK__PLUGIN_FILE );
74
		wp_enqueue_style( 'jetpack-instant-search', $style_path, array(), $style_version );
75
	}
76
77
	/**
78
	 * Passes all options to the JS app.
79
	 */
80
	protected function inject_javascript_options() {
81
		$widget_options = Jetpack_Search_Helpers::get_widgets_from_option();
82
		if ( is_array( $widget_options ) ) {
83
			$widget_options = end( $widget_options );
0 ignored issues
show
Unused Code introduced by
$widget_options 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...
84
		}
85
86
		$overlay_widget_ids      = is_active_sidebar( 'jetpack-instant-search-sidebar' ) ?
87
			wp_get_sidebars_widgets()['jetpack-instant-search-sidebar'] : array();
88
		$filters                 = Jetpack_Search_Helpers::get_filters_from_widgets();
89
		$widgets                 = array();
90
		$widgets_outside_overlay = array();
91
		foreach ( $filters as $key => &$filter ) {
92
			$filter['filter_id'] = $key;
93
94
			if ( in_array( $filter['widget_id'], $overlay_widget_ids, true ) ) {
95 View Code Duplication
				if ( ! isset( $widgets[ $filter['widget_id'] ] ) ) {
96
					$widgets[ $filter['widget_id'] ]['filters']   = array();
97
					$widgets[ $filter['widget_id'] ]['widget_id'] = $filter['widget_id'];
98
				}
99
				$widgets[ $filter['widget_id'] ]['filters'][] = $filter;
100
			} else {
101 View Code Duplication
				if ( ! isset( $widgets_outside_overlay[ $filter['widget_id'] ] ) ) {
102
					$widgets_outside_overlay[ $filter['widget_id'] ]['filters']   = array();
103
					$widgets_outside_overlay[ $filter['widget_id'] ]['widget_id'] = $filter['widget_id'];
104
				}
105
				$widgets_outside_overlay[ $filter['widget_id'] ]['filters'][] = $filter;
106
			}
107
		}
108
		unset( $filter );
109
110
		$post_type_objs   = get_post_types( array( 'exclude_from_search' => false ), 'objects' );
111
		$post_type_labels = array();
112
		foreach ( $post_type_objs as $key => $obj ) {
113
			$post_type_labels[ $key ] = array(
114
				'singular_name' => $obj->labels->singular_name,
115
				'name'          => $obj->labels->name,
116
			);
117
		}
118
119
		$prefix         = Jetpack_Search_Options::OPTION_PREFIX;
120
		$posts_per_page = (int) get_option( 'posts_per_page' );
121
		if ( ( $posts_per_page > 20 ) || ( $posts_per_page <= 0 ) ) {
122
			$posts_per_page = 20;
123
		}
124
125
		$options = array(
126
			'overlayOptions'        => array(
127
				'colorTheme'      => get_option( $prefix . 'color_theme', 'light' ),
128
				'enableInfScroll' => get_option( $prefix . 'inf_scroll', '1' ) === '1',
129
				'enableSort'      => get_option( $prefix . 'enable_sort', '1' ) === '1',
130
				'highlightColor'  => get_option( $prefix . 'highlight_color', '#FFC' ),
131
				'opacity'         => (int) get_option( $prefix . 'opacity', 97 ),
132
				'overlayTrigger'  => get_option( $prefix . 'overlay_trigger', 'immediate' ),
133
				'showPoweredBy'   => get_option( $prefix . 'show_powered_by', '1' ) === '1',
134
			),
135
136
			// core config.
137
			'homeUrl'               => home_url(),
138
			'locale'                => str_replace( '_', '-', Jetpack_Search_Helpers::is_valid_locale( get_locale() ) ? get_locale() : 'en_US' ),
139
			'postsPerPage'          => $posts_per_page,
140
			'siteId'                => $this->jetpack_blog_id,
141
			'postTypes'             => $post_type_labels,
142
143
			// search options.
144
			'defaultSort'           => get_option( $prefix . 'default_sort', 'relevance' ),
145
146
			// widget info.
147
			'hasOverlayWidgets'     => count( $overlay_widget_ids ) > 0,
148
			'widgets'               => array_values( $widgets ),
149
			'widgetsOutsideOverlay' => array_values( $widgets_outside_overlay ),
150
		);
151
152
		/**
153
		 * Customize Instant Search Options.
154
		 *
155
		 * @module search
156
		 *
157
		 * @since 7.7.0
158
		 *
159
		 * @param array $options Array of parameters used in Instant Search queries.
160
		 */
161
		$options = apply_filters( 'jetpack_instant_search_options', $options );
162
163
		// Use wp_add_inline_script instead of wp_localize_script, see https://core.trac.wordpress.org/ticket/25280.
164
		wp_add_inline_script( 'jetpack-instant-search', 'var JetpackInstantSearchOptions=JSON.parse(decodeURIComponent("' . rawurlencode( wp_json_encode( $options ) ) . '"));' );
165
	}
166
167
	/**
168
	 * Registers a widget sidebar for Instant Search.
169
	 */
170
	public function register_jetpack_instant_sidebar() {
171
		$args = array(
172
			'name'          => __( 'Jetpack Search Sidebar', 'jetpack' ),
173
			'id'            => 'jetpack-instant-search-sidebar',
174
			'description'   => __( 'Customize the sidebar inside the Jetpack Search overlay', 'jetpack' ),
175
			'class'         => '',
176
			'before_widget' => '<div id="%1$s" class="widget %2$s">',
177
			'after_widget'  => '</div>',
178
			'before_title'  => '<h2 class="widgettitle">',
179
			'after_title'   => '</h2>',
180
		);
181
		register_sidebar( $args );
182
	}
183
184
	/**
185
	 * Prints Instant Search sidebar.
186
	 */
187
	public function print_instant_search_sidebar() {
188
		?>
189
		<div class="jetpack-instant-search__widget-area" style="display: none">
190
			<?php if ( is_active_sidebar( 'jetpack-instant-search-sidebar' ) ) { ?>
191
				<?php dynamic_sidebar( 'jetpack-instant-search-sidebar' ); ?>
192
			<?php } ?>
193
		</div>
194
		<?php
195
	}
196
197
	/**
198
	 * Loads scripts for Tracks analytics library
199
	 */
200
	public function load_and_initialize_tracks() {
201
		wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
202
	}
203
204
	/**
205
	 * Bypass the normal Search query since we will run it with instant search.
206
	 *
207
	 * @since 8.3.0
208
	 *
209
	 * @param array    $posts Current array of posts (still pre-query).
210
	 * @param WP_Query $query The WP_Query being filtered.
211
	 *
212
	 * @return array Array of matching posts.
213
	 */
214
	public function filter__posts_pre_query( $posts, $query ) {
215
		if ( ! $this->should_handle_query( $query ) ) {
216
			// Intentionally not adding the 'jetpack_search_abort' action since this should fire for every request except for search.
217
			return $posts;
218
		}
219
220
		/**
221
		 * Bypass the main query and return dummy data
222
		 *  WP Core doesn't call the set_found_posts and its filters when filtering
223
		 *  posts_pre_query like we do, so need to do these manually.
224
		 */
225
		$query->found_posts   = 1;
226
		$query->max_num_pages = 1;
227
228
		return array();
229
	}
230
231
	/**
232
	 * Run the aggregations API query for any filtering
233
	 *
234
	 * @since 8.3.0
235
	 *
236
	 * @param WP_Query $query The WP_Query being filtered.
237
	 */
238
	public function action__parse_query( $query ) {
239
		if ( ! empty( $this->search_result ) ) {
240
			return;
241
		}
242
243
		if ( is_admin() ) {
244
			return;
245
		}
246
247
		if ( empty( $this->aggregations ) ) {
248
			return;
249
		}
250
251
		jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-builder' );
252
253
		$builder = new Jetpack_WPES_Query_Builder();
254
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
255
		$this->search_result = $this->instant_api(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->instant_api(array...ze' => 0, 'from' => 0)) 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...
256
			array(
257
				'aggregations' => $builder->build_aggregation(),
258
				'size'         => 0,
259
				'from'         => 0,
260
			)
261
		);
262
	}
263
264
	/**
265
	 * Run an instant search on the WordPress.com public API.
266
	 *
267
	 * @since 8.3.0
268
	 *
269
	 * @param array $args Args conforming to the WP.com v1.3/sites/<blog_id>/search endpoint.
270
	 *
271
	 * @return object|WP_Error The response from the public API, or a WP_Error.
272
	 */
273
	public function instant_api( array $args ) {
274
		global $wp_version;
275
		$start_time = microtime( true );
276
277
		// Cache locally to avoid remote request slowing the page.
278
		$transient_name = 'jetpack_instant_search_cache_' . md5( wp_json_encode( $args ) );
279
		$cache          = get_transient( $transient_name );
280
		if ( false !== $cache ) {
281
			return $cache;
282
		}
283
284
		$service_url = add_query_arg(
285
			$args,
286
			sprintf(
287
				'https://public-api.wordpress.com/rest/v1.3/sites/%d/search',
288
				$this->jetpack_blog_id
289
			)
290
		);
291
292
		$request_args = array(
293
			'timeout'    => 10,
294
			'user-agent' => "WordPress/{$wp_version} | Jetpack/" . constant( 'JETPACK__VERSION' ),
295
		);
296
297
		$request  = wp_remote_get( esc_url_raw( $service_url ), $request_args );
298
		$end_time = microtime( true );
299
300
		if ( is_wp_error( $request ) ) {
301
			return $request;
302
		}
303
304
		$response_code = wp_remote_retrieve_response_code( $request );
305
		$response      = json_decode( wp_remote_retrieve_body( $request ), true );
306
307 View Code Duplication
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
308
			/**
309
			 * Fires after a search query request has failed
310
			 *
311
			 * @module search
312
			 *
313
			 * @since  5.6.0
314
			 *
315
			 * @param array Array containing the response code and response from the failed search query
316
			 */
317
			do_action(
318
				'failed_jetpack_search_query',
319
				array(
320
					'response_code' => $response_code,
321
					'json'          => $response,
322
				)
323
			);
324
325
			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...
326
		}
327
328
		$took = is_array( $response ) && ! empty( $response['took'] )
329
			? $response['took']
330
			: null;
331
332
		$query = array(
333
			'args'          => $args,
334
			'response'      => $response,
335
			'response_code' => $response_code,
336
			'elapsed_time'  => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms.
337
			'es_time'       => $took,
338
			'url'           => $service_url,
339
		);
340
341
		/**
342
		 * Fires after a search request has been performed.
343
		 *
344
		 * Includes the following info in the $query parameter:
345
		 *
346
		 * array args Array of Elasticsearch arguments for the search
347
		 * array response Raw API response, JSON decoded
348
		 * int response_code HTTP response code of the request
349
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
350
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
351
		 * string url API url that was queried
352
		 *
353
		 * @module search
354
		 *
355
		 * @since  5.0.0
356
		 * @since  5.8.0 This action now fires on all queries instead of just successful queries.
357
		 *
358
		 * @param array $query Array of information about the query performed
359
		 */
360
		do_action( 'did_jetpack_search_query', $query );
361
362
		// Update local cache.
363
		set_transient( $transient_name, $response, 1 * HOUR_IN_SECONDS );
364
365
		return $response;
366
	}
367
368
	/**
369
	 * Get the raw Aggregation results from the Elasticsearch response.
370
	 *
371
	 * @since  8.4.0
372
	 *
373
	 * @return array Array of Aggregations performed on the search.
374
	 */
375
	public function get_search_aggregations_results() {
376
		if ( empty( $this->search_result ) || is_wp_error( $this->search_result ) || ! isset( $this->search_result['aggregations'] ) ) {
377
			return array();
378
		}
379
380
		return $this->search_result['aggregations'];
381
	}
382
383
	/**
384
	 * Autoconfig search by adding filter widgets
385
	 *
386
	 * @since  8.3.0
387
	 */
388
	public function auto_config_search() {
389
		if ( ! current_user_can( 'edit_theme_options' ) ) {
390
			return;
391
		}
392
393
		global $wp_registered_sidebars;
394
		$sidebars = get_option( 'sidebars_widgets', array() );
395
		$slug     = Jetpack_Search_Helpers::FILTER_WIDGET_BASE;
396
397
		if ( isset( $sidebars['jetpack-instant-search-sidebar'] ) ) {
398
			foreach ( (array) $sidebars['jetpack-instant-search-sidebar'] as $widget_id ) {
399
				if ( 0 === strpos( $widget_id, $slug ) ) {
400
					// Already configured.
401
					return;
402
				}
403
			}
404
		}
405
406
		$has_sidebar           = isset( $wp_registered_sidebars['sidebar-1'] );
407
		$sidebar_id            = false;
408
		$sidebar_searchbox_idx = false;
409
		if ( $has_sidebar ) {
410
			if ( empty( $sidebars['sidebar-1'] ) ) {
411
				// Adding to an empty sidebar is generally a bad idea.
412
				$has_sidebar = false;
413
			}
414
			foreach ( (array) $sidebars['sidebar-1'] as $idx => $widget_id ) {
415
				if ( 0 === strpos( $widget_id, 'search-' ) ) {
416
					$sidebar_searchbox_idx = $idx;
417
				}
418
				if ( 0 === strpos( $widget_id, $slug ) ) {
419
					$sidebar_id = (int) str_replace( Jetpack_Search_Helpers::FILTER_WIDGET_BASE . '-', '', $widget_id );
420
					break;
421
				}
422
			}
423
		}
424
425
		$next_id         = 1;
426
		$widget_opt_name = Jetpack_Search_Helpers::get_widget_option_name();
427
		$widget_options  = get_option( $widget_opt_name, array() );
428
		foreach ( $widget_options as $id => $w ) {
429
			if ( $id >= $next_id ) {
430
				$next_id = $id + 1;
431
			}
432
		}
433
434
		// Copy sidebar settings to overlay.
435
		if ( ( false !== $sidebar_id ) && isset( $widget_options[ $sidebar_id ] ) ) {
436
			$widget_options[ $next_id ] = $widget_options[ $sidebar_id ];
437
			update_option( $widget_opt_name, $widget_options );
438
439
			if ( ! isset( $sidebars['jetpack-instant-search-sidebar'] ) ) {
440
				$sidebars['jetpack-instant-search-sidebar'] = array();
441
			}
442
			array_unshift( $sidebars['jetpack-instant-search-sidebar'], Jetpack_Search_Helpers::build_widget_id( $next_id ) );
443
			update_option( 'sidebars_widgets', $sidebars );
444
445
			return;
446
		}
447
448
		// Configure overlay and sidebar (if it exists).
449
		$preconfig_opts = $this->get_preconfig_widget_options();
450
		if ( ! isset( $sidebars['jetpack-instant-search-sidebar'] ) ) {
451
			$sidebars['jetpack-instant-search-sidebar'] = array();
452
		}
453
		if ( $has_sidebar ) {
454
			$widget_options[ $next_id ] = $preconfig_opts;
455
			if ( false !== $sidebar_searchbox_idx ) {
456
				// Replace Core search box.
457
				$sidebars['sidebar-1'][ $sidebar_searchbox_idx ] = Jetpack_Search_Helpers::build_widget_id( $next_id );
458
			} else {
459
				// Add to top.
460
				array_unshift( $sidebars['sidebar-1'], Jetpack_Search_Helpers::build_widget_id( $next_id ) );
461
			}
462
			$next_id++;
463
		}
464
		$widget_options[ $next_id ] = $preconfig_opts;
465
		array_unshift( $sidebars['jetpack-instant-search-sidebar'], Jetpack_Search_Helpers::build_widget_id( $next_id ) );
466
467
		update_option( $widget_opt_name, $widget_options );
468
		update_option( 'sidebars_widgets', $sidebars );
469
	}
470
471
	/**
472
	 * Autoconfig search by adding filter widgets
473
	 *
474
	 * @since  8.4.0
475
	 *
476
	 * @return array Array of config settings for search widget.
477
	 */
478
	protected function get_preconfig_widget_options() {
479
		$settings = array(
480
			'title'              => '',
481
			'search_box_enabled' => 1,
482
			'user_sort_enabled'  => 0,
483
			'filters'            => array(),
484
		);
485
486
		$post_types = get_post_types(
487
			array(
488
				'public'   => true,
489
				'_builtin' => false,
490
			)
491
		);
492
493
		if ( ! empty( $post_types ) ) {
494
			$settings['filters'][] = array(
495
				array(
496
					'name'  => '',
497
					'type'  => 'post_type',
498
					'count' => 5,
499
				),
500
			);
501
		}
502
503
		$taxonomies = get_taxonomies(
504
			array(
505
				'public'   => true,
506
				'_builtin' => false,
507
			)
508
		);
509
510
		foreach ( $taxonomies as $t ) {
511
			$settings['filters'][] = array(
512
				'name'     => '',
513
				'type'     => 'taxonomy',
514
				'taxonomy' => $t,
515
				'count'    => 5,
516
			);
517
		}
518
519
		$settings['filters'][] = array(
520
			'name'     => '',
521
			'type'     => 'taxonomy',
522
			'taxonomy' => 'category',
523
			'count'    => 5,
524
		);
525
		$settings['filters'][] = array(
526
			'name'     => '',
527
			'type'     => 'taxonomy',
528
			'taxonomy' => 'post_tag',
529
			'count'    => 5,
530
		);
531
		$settings['filters'][] = array(
532
			'name'     => '',
533
			'type'     => 'date_histogram',
534
			'count'    => 5,
535
			'field'    => 'post_date',
536
			'interval' => 'year',
537
		);
538
539
		return $settings;
540
	}
541
542
}
543