Completed
Push — add/jetpack-search-indexing-st... ( 5cba09...3d8dee )
by
unknown
06:35
created

Jetpack_Instant_Search::init_hooks()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16

Duplication

Lines 16
Ratio 100 %

Importance

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