Completed
Push — update/instant-search-overlay-... ( 4dff47 )
by
unknown
157:48 queued 150:50
created

load_and_initialize_tracks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 3
rs 10
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
		if ( 'immediate' === get_option( $prefix . 'overlay_trigger' ) ) {
126
			update_option( $prefix . 'overlay_trigger', 'focus' );
127
		} elseif ( 'results' === get_option( $prefix . 'overlay_trigger' ) ) {
128
			update_option( $prefix . 'overlay_trigger', 'submit' );
129
		}
130
131
		$options = array(
132
			'overlayOptions'        => array(
133
				'colorTheme'      => get_option( $prefix . 'color_theme', 'light' ),
134
				'enableInfScroll' => get_option( $prefix . 'inf_scroll', '1' ) === '1',
135
				'enableSort'      => get_option( $prefix . 'enable_sort', '1' ) === '1',
136
				'highlightColor'  => get_option( $prefix . 'highlight_color', '#FFC' ),
137
				'opacity'         => (int) get_option( $prefix . 'opacity', 97 ),
138
				'overlayTrigger'  => get_option( $prefix . 'overlay_trigger', 'submit' ),
139
				'showPoweredBy'   => get_option( $prefix . 'show_powered_by', '1' ) === '1',
140
			),
141
142
			// core config.
143
			'homeUrl'               => home_url(),
144
			'locale'                => str_replace( '_', '-', Jetpack_Search_Helpers::is_valid_locale( get_locale() ) ? get_locale() : 'en_US' ),
145
			'postsPerPage'          => $posts_per_page,
146
			'siteId'                => $this->jetpack_blog_id,
147
			'postTypes'             => $post_type_labels,
148
149
			// search options.
150
			'defaultSort'           => get_option( $prefix . 'default_sort', 'relevance' ),
151
152
			// widget info.
153
			'hasOverlayWidgets'     => count( $overlay_widget_ids ) > 0,
154
			'widgets'               => array_values( $widgets ),
155
			'widgetsOutsideOverlay' => array_values( $widgets_outside_overlay ),
156
		);
157
158
		/**
159
		 * Customize Instant Search Options.
160
		 *
161
		 * @module search
162
		 *
163
		 * @since 7.7.0
164
		 *
165
		 * @param array $options Array of parameters used in Instant Search queries.
166
		 */
167
		$options = apply_filters( 'jetpack_instant_search_options', $options );
168
169
		// Use wp_add_inline_script instead of wp_localize_script, see https://core.trac.wordpress.org/ticket/25280.
170
		wp_add_inline_script( 'jetpack-instant-search', 'var JetpackInstantSearchOptions=JSON.parse(decodeURIComponent("' . rawurlencode( wp_json_encode( $options ) ) . '"));' );
171
	}
172
173
	/**
174
	 * Registers a widget sidebar for Instant Search.
175
	 */
176
	public function register_jetpack_instant_sidebar() {
177
		$args = array(
178
			'name'          => __( 'Jetpack Search Sidebar', 'jetpack' ),
179
			'id'            => 'jetpack-instant-search-sidebar',
180
			'description'   => __( 'Customize the sidebar inside the Jetpack Search overlay', 'jetpack' ),
181
			'class'         => '',
182
			'before_widget' => '<div id="%1$s" class="widget %2$s">',
183
			'after_widget'  => '</div>',
184
			'before_title'  => '<h2 class="widgettitle">',
185
			'after_title'   => '</h2>',
186
		);
187
		register_sidebar( $args );
188
	}
189
190
	/**
191
	 * Prints Instant Search sidebar.
192
	 */
193
	public function print_instant_search_sidebar() {
194
		?>
195
		<div class="jetpack-instant-search__widget-area" style="display: none">
196
			<?php if ( is_active_sidebar( 'jetpack-instant-search-sidebar' ) ) { ?>
197
				<?php dynamic_sidebar( 'jetpack-instant-search-sidebar' ); ?>
198
			<?php } ?>
199
		</div>
200
		<?php
201
	}
202
203
	/**
204
	 * Loads scripts for Tracks analytics library
205
	 */
206
	public function load_and_initialize_tracks() {
207
		wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
208
	}
209
210
	/**
211
	 * Bypass the normal Search query since we will run it with instant search.
212
	 *
213
	 * @since 8.3.0
214
	 *
215
	 * @param array    $posts Current array of posts (still pre-query).
216
	 * @param WP_Query $query The WP_Query being filtered.
217
	 *
218
	 * @return array Array of matching posts.
219
	 */
220
	public function filter__posts_pre_query( $posts, $query ) {
221
		if ( ! $this->should_handle_query( $query ) ) {
222
			// Intentionally not adding the 'jetpack_search_abort' action since this should fire for every request except for search.
223
			return $posts;
224
		}
225
226
		/**
227
		 * Bypass the main query and return dummy data
228
		 *  WP Core doesn't call the set_found_posts and its filters when filtering
229
		 *  posts_pre_query like we do, so need to do these manually.
230
		 */
231
		$query->found_posts   = 1;
232
		$query->max_num_pages = 1;
233
234
		return array();
235
	}
236
237
	/**
238
	 * Run the aggregations API query for any filtering
239
	 *
240
	 * @since 8.3.0
241
	 *
242
	 * @param WP_Query $query The WP_Query being filtered.
243
	 */
244
	public function action__parse_query( $query ) {
245
		if ( ! empty( $this->search_result ) ) {
246
			return;
247
		}
248
249
		if ( is_admin() ) {
250
			return;
251
		}
252
253
		if ( empty( $this->aggregations ) ) {
254
			return;
255
		}
256
257
		jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-builder' );
258
259
		$builder = new Jetpack_WPES_Query_Builder();
260
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
261
		$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...
262
			array(
263
				'aggregations' => $builder->build_aggregation(),
264
				'size'         => 0,
265
				'from'         => 0,
266
			)
267
		);
268
	}
269
270
	/**
271
	 * Run an instant search on the WordPress.com public API.
272
	 *
273
	 * @since 8.3.0
274
	 *
275
	 * @param array $args Args conforming to the WP.com v1.3/sites/<blog_id>/search endpoint.
276
	 *
277
	 * @return object|WP_Error The response from the public API, or a WP_Error.
278
	 */
279
	public function instant_api( array $args ) {
280
		global $wp_version;
281
		$start_time = microtime( true );
282
283
		// Cache locally to avoid remote request slowing the page.
284
		$transient_name = 'jetpack_instant_search_cache_' . md5( wp_json_encode( $args ) );
285
		$cache          = get_transient( $transient_name );
286
		if ( false !== $cache ) {
287
			return $cache;
288
		}
289
290
		$service_url = add_query_arg(
291
			$args,
292
			sprintf(
293
				'https://public-api.wordpress.com/rest/v1.3/sites/%d/search',
294
				$this->jetpack_blog_id
295
			)
296
		);
297
298
		$request_args = array(
299
			'timeout'    => 10,
300
			'user-agent' => "WordPress/{$wp_version} | Jetpack/" . constant( 'JETPACK__VERSION' ),
301
		);
302
303
		$request  = wp_remote_get( esc_url_raw( $service_url ), $request_args );
304
		$end_time = microtime( true );
305
306
		if ( is_wp_error( $request ) ) {
307
			return $request;
308
		}
309
310
		$response_code = wp_remote_retrieve_response_code( $request );
311
		$response      = json_decode( wp_remote_retrieve_body( $request ), true );
312
313 View Code Duplication
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
314
			/**
315
			 * Fires after a search query request has failed
316
			 *
317
			 * @module search
318
			 *
319
			 * @since  5.6.0
320
			 *
321
			 * @param array Array containing the response code and response from the failed search query
322
			 */
323
			do_action(
324
				'failed_jetpack_search_query',
325
				array(
326
					'response_code' => $response_code,
327
					'json'          => $response,
328
				)
329
			);
330
331
			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...
332
		}
333
334
		$took = is_array( $response ) && ! empty( $response['took'] )
335
			? $response['took']
336
			: null;
337
338
		$query = array(
339
			'args'          => $args,
340
			'response'      => $response,
341
			'response_code' => $response_code,
342
			'elapsed_time'  => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms.
343
			'es_time'       => $took,
344
			'url'           => $service_url,
345
		);
346
347
		/**
348
		 * Fires after a search request has been performed.
349
		 *
350
		 * Includes the following info in the $query parameter:
351
		 *
352
		 * array args Array of Elasticsearch arguments for the search
353
		 * array response Raw API response, JSON decoded
354
		 * int response_code HTTP response code of the request
355
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
356
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
357
		 * string url API url that was queried
358
		 *
359
		 * @module search
360
		 *
361
		 * @since  5.0.0
362
		 * @since  5.8.0 This action now fires on all queries instead of just successful queries.
363
		 *
364
		 * @param array $query Array of information about the query performed
365
		 */
366
		do_action( 'did_jetpack_search_query', $query );
367
368
		// Update local cache.
369
		set_transient( $transient_name, $response, 1 * HOUR_IN_SECONDS );
370
371
		return $response;
372
	}
373
374
	/**
375
	 * Get the raw Aggregation results from the Elasticsearch response.
376
	 *
377
	 * @since  8.4.0
378
	 *
379
	 * @return array Array of Aggregations performed on the search.
380
	 */
381
	public function get_search_aggregations_results() {
382
		if ( empty( $this->search_result ) || is_wp_error( $this->search_result ) || ! isset( $this->search_result['aggregations'] ) ) {
383
			return array();
384
		}
385
386
		return $this->search_result['aggregations'];
387
	}
388
389
	/**
390
	 * Autoconfig search by adding filter widgets
391
	 *
392
	 * @since  8.3.0
393
	 */
394
	public function auto_config_search() {
395
		if ( ! current_user_can( 'edit_theme_options' ) ) {
396
			return;
397
		}
398
399
		global $wp_registered_sidebars;
400
		$sidebars = get_option( 'sidebars_widgets', array() );
401
		$slug     = Jetpack_Search_Helpers::FILTER_WIDGET_BASE;
402
403
		if ( isset( $sidebars['jetpack-instant-search-sidebar'] ) ) {
404
			foreach ( (array) $sidebars['jetpack-instant-search-sidebar'] as $widget_id ) {
405
				if ( 0 === strpos( $widget_id, $slug ) ) {
406
					// Already configured.
407
					return;
408
				}
409
			}
410
		}
411
412
		$has_sidebar           = isset( $wp_registered_sidebars['sidebar-1'] );
413
		$sidebar_id            = false;
414
		$sidebar_searchbox_idx = false;
415
		if ( $has_sidebar ) {
416
			if ( empty( $sidebars['sidebar-1'] ) ) {
417
				// Adding to an empty sidebar is generally a bad idea.
418
				$has_sidebar = false;
419
			}
420
			foreach ( (array) $sidebars['sidebar-1'] as $idx => $widget_id ) {
421
				if ( 0 === strpos( $widget_id, 'search-' ) ) {
422
					$sidebar_searchbox_idx = $idx;
423
				}
424
				if ( 0 === strpos( $widget_id, $slug ) ) {
425
					$sidebar_id = (int) str_replace( Jetpack_Search_Helpers::FILTER_WIDGET_BASE . '-', '', $widget_id );
426
					break;
427
				}
428
			}
429
		}
430
431
		$next_id         = 1;
432
		$widget_opt_name = Jetpack_Search_Helpers::get_widget_option_name();
433
		$widget_options  = get_option( $widget_opt_name, array() );
434
		foreach ( $widget_options as $id => $w ) {
435
			if ( $id >= $next_id ) {
436
				$next_id = $id + 1;
437
			}
438
		}
439
440
		// Copy sidebar settings to overlay.
441
		if ( ( false !== $sidebar_id ) && isset( $widget_options[ $sidebar_id ] ) ) {
442
			$widget_options[ $next_id ] = $widget_options[ $sidebar_id ];
443
			update_option( $widget_opt_name, $widget_options );
444
445
			if ( ! isset( $sidebars['jetpack-instant-search-sidebar'] ) ) {
446
				$sidebars['jetpack-instant-search-sidebar'] = array();
447
			}
448
			array_unshift( $sidebars['jetpack-instant-search-sidebar'], Jetpack_Search_Helpers::build_widget_id( $next_id ) );
449
			update_option( 'sidebars_widgets', $sidebars );
450
451
			return;
452
		}
453
454
		// Configure overlay and sidebar (if it exists).
455
		$preconfig_opts = $this->get_preconfig_widget_options();
456
		if ( ! isset( $sidebars['jetpack-instant-search-sidebar'] ) ) {
457
			$sidebars['jetpack-instant-search-sidebar'] = array();
458
		}
459
		if ( $has_sidebar ) {
460
			$widget_options[ $next_id ] = $preconfig_opts;
461
			if ( false !== $sidebar_searchbox_idx ) {
462
				// Replace Core search box.
463
				$sidebars['sidebar-1'][ $sidebar_searchbox_idx ] = Jetpack_Search_Helpers::build_widget_id( $next_id );
464
			} else {
465
				// Add to top.
466
				array_unshift( $sidebars['sidebar-1'], Jetpack_Search_Helpers::build_widget_id( $next_id ) );
467
			}
468
			$next_id++;
469
		}
470
		$widget_options[ $next_id ] = $preconfig_opts;
471
		array_unshift( $sidebars['jetpack-instant-search-sidebar'], Jetpack_Search_Helpers::build_widget_id( $next_id ) );
472
473
		update_option( $widget_opt_name, $widget_options );
474
		update_option( 'sidebars_widgets', $sidebars );
475
	}
476
477
	/**
478
	 * Autoconfig search by adding filter widgets
479
	 *
480
	 * @since  8.4.0
481
	 *
482
	 * @return array Array of config settings for search widget.
483
	 */
484
	protected function get_preconfig_widget_options() {
485
		$settings = array(
486
			'title'              => '',
487
			'search_box_enabled' => 1,
488
			'user_sort_enabled'  => 0,
489
			'filters'            => array(),
490
		);
491
492
		$post_types = get_post_types(
493
			array(
494
				'public'   => true,
495
				'_builtin' => false,
496
			)
497
		);
498
499
		if ( ! empty( $post_types ) ) {
500
			$settings['filters'][] = array(
501
				array(
502
					'name'  => '',
503
					'type'  => 'post_type',
504
					'count' => 5,
505
				),
506
			);
507
		}
508
509
		$taxonomies = get_taxonomies(
510
			array(
511
				'public'   => true,
512
				'_builtin' => false,
513
			)
514
		);
515
516
		foreach ( $taxonomies as $t ) {
517
			$settings['filters'][] = array(
518
				'name'     => '',
519
				'type'     => 'taxonomy',
520
				'taxonomy' => $t,
521
				'count'    => 5,
522
			);
523
		}
524
525
		$settings['filters'][] = array(
526
			'name'     => '',
527
			'type'     => 'taxonomy',
528
			'taxonomy' => 'category',
529
			'count'    => 5,
530
		);
531
		$settings['filters'][] = array(
532
			'name'     => '',
533
			'type'     => 'taxonomy',
534
			'taxonomy' => 'post_tag',
535
			'count'    => 5,
536
		);
537
		$settings['filters'][] = array(
538
			'name'     => '',
539
			'type'     => 'date_histogram',
540
			'count'    => 5,
541
			'field'    => 'post_date',
542
			'interval' => 'year',
543
		);
544
545
		return $settings;
546
	}
547
548
}
549