Completed
Push — add/engagement-search-results-... ( e442e5...0f247d )
by
unknown
23:53 queued 10:20
created

Jetpack_Instant_Search::get_asset_version()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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