PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-search/src/class-helper.php +1321 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,1321 @@
1 +<?php
2 +/**
3 + * Helper class providing various static utility functions for use in Search.
4 + *
5 + * @package automattic/jetpack-search
6 + */
7 +
8 +namespace Automattic\Jetpack\Search;
9 +
10 +use Automattic\Jetpack\Status;
11 +use GP_Locales;
12 +use Jetpack; // TODO: Remove this once migrated.
13 +
14 +if ( ! defined( 'ABSPATH' ) ) {
15 + exit( 0 );
16 +}
17 +
18 +/**
19 + * Various helper functions for reuse throughout the Jetpack Search code.
20 + */
21 +class Helper {
22 +
23 + /**
24 + * The search widget's base ID.
25 + *
26 + * @since 5.8.0
27 + * @var string
28 + */
29 + const FILTER_WIDGET_BASE = 'jetpack-search-filters';
30 +
31 + /**
32 + * Create a URL for the current search that doesn't include the "paged" parameter.
33 + *
34 + * @since 5.8.0
35 + *
36 + * @return string The search URL.
37 + */
38 + public static function get_search_url() {
39 + // WordPress search doesn't use nonces.
40 + $query_args = stripslashes_deep( $_GET ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
41 +
42 + // Handle the case where a permastruct is being used, such as /search/{$query}.
43 + if ( ! isset( $query_args['s'] ) ) {
44 + $query_args['s'] = get_search_query();
45 + }
46 +
47 + if ( isset( $query_args['paged'] ) ) {
48 + unset( $query_args['paged'] );
49 + }
50 +
51 + $query = http_build_query( $query_args );
52 +
53 + return home_url( "?{$query}" );
54 + }
55 +
56 + /**
57 + * Wraps add_query_arg() with the URL defaulting to the current search URL.
58 + *
59 + * @see add_query_arg()
60 + *
61 + * @since 5.8.0
62 + *
63 + * @param string|array $key Either a query variable key, or an associative array of query variables.
64 + * @param string $value Optional. A query variable value.
65 + * @param bool|string $url Optional. A URL to act upon. Defaults to the current search URL.
66 + *
67 + * @return string New URL query string (unescaped).
68 + */
69 + public static function add_query_arg( $key, $value = false, $url = false ) {
70 + $url = empty( $url ) ? self::get_search_url() : $url;
71 + if ( is_array( $key ) ) {
72 + return add_query_arg( $key, $url );
73 + }
74 +
75 + return add_query_arg( $key, $value, $url );
76 + }
77 +
78 + /**
79 + * Wraps remove_query_arg() with the URL defaulting to the current search URL.
80 + *
81 + * @see remove_query_arg()
82 + *
83 + * @since 5.8.0
84 + *
85 + * @param string|array $key Query key or keys to remove.
86 + * @param bool|string $url Optional. A URL to act upon. Defaults to the current search URL.
87 + *
88 + * @return string New URL query string (unescaped).
89 + */
90 + public static function remove_query_arg( $key, $url = false ) {
91 + $url = empty( $url ) ? self::get_search_url() : $url;
92 +
93 + return remove_query_arg( $key, $url );
94 + }
95 +
96 + /**
97 + * Returns the name of the search widget's option.
98 + *
99 + * @since 5.8.0
100 + *
101 + * @return string The search widget option name.
102 + */
103 + public static function get_widget_option_name() {
104 + return sprintf( 'widget_%s', self::FILTER_WIDGET_BASE );
105 + }
106 +
107 + /**
108 + * Returns the search widget instances from the widget's option.
109 + *
110 + * @since 5.8.0
111 + *
112 + * @return array The widget options.
113 + */
114 + public static function get_widgets_from_option() {
115 + $widget_options = get_option( self::get_widget_option_name(), array() );
116 +
117 + // We don't need this.
118 + if ( ! empty( $widget_options ) && isset( $widget_options['_multiwidget'] ) ) {
119 + unset( $widget_options['_multiwidget'] );
120 + }
121 +
122 + return $widget_options;
123 + }
124 +
125 + /**
126 + * Returns the widget ID (widget base plus the numeric ID).
127 + *
128 + * @param int $number The widget's numeric ID.
129 + *
130 + * @return string The widget's numeric ID prefixed with the search widget base.
131 + */
132 + public static function build_widget_id( $number ) {
133 + return sprintf( '%s-%d', self::FILTER_WIDGET_BASE, $number );
134 + }
135 +
136 + /**
137 + * Wrapper for is_active_widget() with the other parameters automatically supplied.
138 + *
139 + * @see is_active_widget()
140 + *
141 + * @since 5.8.0
142 + *
143 + * @param int $widget_id Widget ID.
144 + *
145 + * @return bool Whether the widget is active or not.
146 + */
147 + public static function is_active_widget( $widget_id ) {
148 + return (bool) is_active_widget( false, $widget_id, self::FILTER_WIDGET_BASE, true );
149 + }
150 +
151 + /**
152 + * Returns an array of the filters from all active search widgets.
153 + *
154 + * @since 5.8.0
155 + *
156 + * @param array|null $allowed_widget_ids array of allowed widget IDs.
157 + *
158 + * @return array Active filters.
159 + */
160 + public static function get_filters_from_widgets( $allowed_widget_ids = null ) {
161 + $filters = array();
162 +
163 + $widget_options = self::get_widgets_from_option();
164 + if ( empty( $widget_options ) ) {
165 + return $filters;
166 + }
167 +
168 + foreach ( (array) $widget_options as $number => $settings ) {
169 + $widget_id = self::build_widget_id( $number );
170 + if ( ! self::is_active_widget( $widget_id ) || empty( $settings['filters'] ) ) {
171 + continue;
172 + }
173 + if ( isset( $allowed_widget_ids ) && ! in_array( $widget_id, $allowed_widget_ids, true ) ) {
174 + continue;
175 + }
176 +
177 + foreach ( (array) $settings['filters'] as $widget_filter ) {
178 + $widget_filter['widget_id'] = $widget_id;
179 +
180 + if ( empty( $widget_filter['name'] ) ) {
181 + $widget_filter['name'] = self::generate_widget_filter_name( $widget_filter );
182 + }
183 +
184 + $type = $widget_filter['type'] ?? '';
185 +
186 + // If this is a product_attribute filter with no specific attribute, expand it to all global attributes.
187 + if ( 'product_attribute' === $type && empty( $widget_filter['attribute'] ) ) {
188 + $filters = self::expand_product_attribute_filters( $widget_filter, $filters );
189 + } else {
190 + $key = sprintf( '%s_%d', $type, count( $filters ) );
191 + $filters[ $key ] = $widget_filter;
192 + }
193 + }
194 + }
195 +
196 + return $filters;
197 + }
198 +
199 + /**
200 + * Expands a product_attribute filter into individual filters for each attribute.
201 + *
202 + * @since 5.8.0
203 + *
204 + * @param array $widget_filter The filter configuration.
205 + * @param array $filters The existing filters array.
206 + * @return array The filters array with expanded product attribute filters.
207 + */
208 + private static function expand_product_attribute_filters( $widget_filter, $filters ) {
209 + if ( ! function_exists( 'wc_get_attribute_taxonomies' ) || ! function_exists( 'wc_attribute_taxonomy_name' ) ) {
210 + return $filters;
211 + }
212 +
213 + $product_attributes = wc_get_attribute_taxonomies();
214 + $included_attributes = isset( $widget_filter['included_attributes'] ) ? (array) $widget_filter['included_attributes'] : array();
215 +
216 + // If no attributes are explicitly included, show all attributes (backward compatibility).
217 + // Also optimize by treating "all selected" the same as "none selected" to avoid O(n²) in_array() checks.
218 + $show_all = empty( $included_attributes ) || count( $included_attributes ) === count( $product_attributes );
219 +
220 + foreach ( $product_attributes as $attribute ) {
221 + $attribute_name = wc_attribute_taxonomy_name( $attribute->attribute_name );
222 +
223 + if ( ! $show_all && ! in_array( $attribute_name, $included_attributes, true ) ) {
224 + continue;
225 + }
226 +
227 + $key = sprintf( 'product_attribute_%d', count( $filters ) );
228 + $expanded_filter = $widget_filter;
229 + $expanded_filter['attribute'] = $attribute_name;
230 + $expanded_filter['name'] = $attribute->attribute_label;
231 + unset( $expanded_filter['included_attributes'] );
232 + $filters[ $key ] = $expanded_filter;
233 + }
234 +
235 + return $filters;
236 + }
237 +
238 + /**
239 + * Get the localized default label for a date filter.
240 + *
241 + * @since 5.8.0
242 + *
243 + * @param string $type Date type, either year or month.
244 + * @param bool $is_updated Whether the filter was updated or not (adds "Updated" to the end).
245 + *
246 + * @return string The filter label.
247 + */
248 + public static function get_date_filter_type_name( $type, $is_updated = false ) {
249 + switch ( $type ) {
250 + case 'year':
251 + $string = ( $is_updated )
252 + ? esc_html_x( 'Year Updated', 'label for filtering posts', 'jetpack-search-pkg' )
253 + : esc_html_x( 'Year', 'label for filtering posts', 'jetpack-search-pkg' );
254 + break;
255 + case 'month':
256 + default:
257 + $string = ( $is_updated )
258 + ? esc_html_x( 'Month Updated', 'label for filtering posts', 'jetpack-search-pkg' )
259 + : esc_html_x( 'Month', 'label for filtering posts', 'jetpack-search-pkg' );
260 + break;
261 + }
262 +
263 + return $string;
264 + }
265 +
266 + /**
267 + * Creates a default name for a filter. Used when the filter label is blank.
268 + *
269 + * @since 5.8.0
270 + *
271 + * @param array $widget_filter The filter to generate the title for.
272 + *
273 + * @return string The suggested filter name.
274 + */
275 + public static function generate_widget_filter_name( $widget_filter ) {
276 + $name = '';
277 +
278 + if ( ! isset( $widget_filter['type'] ) ) {
279 + return $name;
280 + }
281 +
282 + switch ( $widget_filter['type'] ) {
283 + case 'post_type':
284 + $name = _x( 'Post Types', 'label for filtering posts', 'jetpack-search-pkg' );
285 + break;
286 +
287 + case 'author':
288 + $name = _x( 'Authors', 'label for filtering posts', 'jetpack-search-pkg' );
289 + break;
290 +
291 + case 'blog_id':
292 + $name = _x( 'Blogs', 'label for filtering posts', 'jetpack-search-pkg' );
293 + break;
294 +
295 + case 'date_histogram':
296 + $modified_fields = array(
297 + 'post_modified',
298 + 'post_modified_gmt',
299 + );
300 + switch ( $widget_filter['interval'] ) {
301 + case 'year':
302 + $name = self::get_date_filter_type_name(
303 + 'year',
304 + in_array( $widget_filter['field'], $modified_fields, true )
305 + );
306 + break;
307 + case 'month':
308 + default:
309 + $name = self::get_date_filter_type_name(
310 + 'month',
311 + in_array( $widget_filter['field'], $modified_fields, true )
312 + );
313 + break;
314 + }
315 + break;
316 +
317 + case 'taxonomy':
318 + $tax = get_taxonomy( $widget_filter['taxonomy'] );
319 + if ( ! $tax ) {
320 + break;
321 + }
322 +
323 + if ( isset( $tax->label ) ) {
324 + $name = $tax->label;
325 + } elseif ( isset( $tax->labels ) && isset( $tax->labels->name ) ) {
326 + $name = $tax->labels->name;
327 + }
328 + break;
329 +
330 + case 'product_attribute':
331 + $name = _x( 'Product Attributes', 'label for filtering posts', 'jetpack-search-pkg' );
332 + break;
333 +
334 + }
335 +
336 + return $name;
337 + }
338 +
339 + /**
340 + * Whether we should rerun a search in the customizer preview or not.
341 + *
342 + * @since 5.8.0
343 + *
344 + * @return bool
345 + */
346 + public static function should_rerun_search_in_customizer_preview() {
347 + // Only update when in a customizer preview and data is being posted.
348 + // Check for $_POST removes an extra update when the customizer loads.
349 + //
350 + // Note: We use $GLOBALS['wp_customize'] here instead of is_customize_preview() to support unit tests.
351 + return isset( $GLOBALS['wp_customize'] ) && $GLOBALS['wp_customize']->is_preview() && ! empty( $_POST ); // phpcs:ignore
352 + }
353 +
354 + /**
355 + * Since PHP's built-in array_diff() works by comparing the values that are in array 1 to the other arrays,
356 + * if there are less values in array 1, it's possible to get an empty diff where one might be expected.
357 + *
358 + * @since 5.8.0
359 + *
360 + * @param array $array_1 The first array.
361 + * @param array $array_2 The second array.
362 + *
363 + * @return array
364 + */
365 + public static function array_diff( $array_1, $array_2 ) {
366 + // If the array counts are the same, then the order doesn't matter. If the count of
367 + // $array_1 is higher than $array_2, that's also fine. If the count of $array_2 is higher,
368 + // we need to swap the array order though.
369 + if ( count( $array_1 ) !== count( $array_2 ) && count( $array_2 ) > count( $array_1 ) ) {
370 + $temp = $array_1;
371 + $array_1 = $array_2;
372 + $array_2 = $temp;
373 + }
374 +
375 + // Disregard keys.
376 + return array_values( array_diff( $array_1, $array_2 ) );
377 + }
378 +
379 + /**
380 + * Given the widget instance, will return true when selected post types differ from searchable post types.
381 + *
382 + * @since 5.8.0
383 + *
384 + * @param array $post_types An array of post types.
385 + *
386 + * @return bool
387 + */
388 + public static function post_types_differ_searchable( $post_types ) {
389 + if ( empty( $post_types ) ) {
390 + return false;
391 + }
392 +
393 + $searchable_post_types = get_post_types( array( 'exclude_from_search' => false ) );
394 + $diff_of_searchable = self::array_diff( $searchable_post_types, (array) $post_types );
395 +
396 + return ! empty( $diff_of_searchable );
397 + }
398 +
399 + /**
400 + * Given the array of post types, will return true when these differ from the current search query.
401 + *
402 + * @since 5.8.0
403 + *
404 + * @param array $post_types An array of post types.
405 + *
406 + * @return bool
407 + */
408 + public static function post_types_differ_query( $post_types ) {
409 + if ( empty( $post_types ) ) {
410 + return false;
411 + }
412 +
413 + // phpcs:disable WordPress.Security.NonceVerification.Recommended -- WordPress search doesn't use nonces.
414 + // phpcs:disable WordPress.Security.ValidatedSanitizedInput -- Sanitization happens at the end.
415 + if ( empty( $_GET['post_type'] ) ) {
416 + $post_types_from_query = array();
417 + } elseif ( is_array( $_GET['post_type'] ) ) {
418 + $post_types_from_query = $_GET['post_type'];
419 + } else {
420 + $post_types_from_query = explode( ',', $_GET['post_type'] );
421 + }
422 + // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
423 +
424 + $post_types_from_query = array_map( 'sanitize_key', $post_types_from_query );
425 +
426 + $diff_query = self::array_diff( (array) $post_types, $post_types_from_query );
427 +
428 + return ! empty( $diff_query );
429 + }
430 +
431 + /**
432 + * Determine what Tracks value should be used when updating a widget.
433 + *
434 + * @since 5.8.0
435 + *
436 + * @param mixed $old_value The old option value.
437 + * @param mixed $new_value The new option value.
438 + *
439 + * @return array|false False if the widget wasn't updated, otherwise an array of the Tracks action and widget properties.
440 + */
441 + public static function get_widget_tracks_value( $old_value, $new_value ) {
442 + $old_value = (array) $old_value;
443 + if ( isset( $old_value['_multiwidget'] ) ) {
444 + unset( $old_value['_multiwidget'] );
445 + }
446 +
447 + $new_value = (array) $new_value;
448 + if ( isset( $new_value['_multiwidget'] ) ) {
449 + unset( $new_value['_multiwidget'] );
450 + }
451 +
452 + $old_keys = array_keys( $old_value );
453 + $new_keys = array_keys( $new_value );
454 +
455 + if ( count( $new_keys ) > count( $old_keys ) ) { // This is the case for a widget being added.
456 + $diff = self::array_diff( $new_keys, $old_keys );
457 + $action = 'widget_added';
458 + $widget = empty( $diff ) || ! isset( $new_value[ $diff[0] ] )
459 + ? false
460 + : $new_value[ $diff[0] ];
461 + } elseif ( count( $old_keys ) > count( $new_keys ) ) { // This is the case for a widget being deleted.
462 + $diff = self::array_diff( $old_keys, $new_keys );
463 + $action = 'widget_deleted';
464 + $widget = empty( $diff ) || ! isset( $old_value[ $diff[0] ] )
465 + ? false
466 + : $old_value[ $diff[0] ];
467 + } else {
468 + $action = 'widget_updated';
469 + $widget = false;
470 +
471 + // This is a bit crazy. Since there can be multiple widgets stored in a single option,
472 + // we need to diff the old and new values to figure out which widget was updated.
473 + foreach ( $new_value as $key => $new_instance ) {
474 + if ( ! isset( $old_value[ $key ] ) ) {
475 + continue;
476 + }
477 + $old_instance = $old_value[ $key ];
478 +
479 + // First, let's test the keys of each instance.
480 + $diff = self::array_diff( array_keys( $new_instance ), array_keys( $old_instance ) );
481 + if ( ! empty( $diff ) ) {
482 + $widget = $new_instance;
483 + break;
484 + }
485 +
486 + // Next, lets's loop over each value and compare it.
487 + foreach ( $new_instance as $k => $v ) {
488 + if ( is_scalar( $v ) && (string) $v !== (string) $old_instance[ $k ] ) {
489 + $widget = $new_instance;
490 + break;
491 + }
492 +
493 + if ( 'filters' === $k ) {
494 + if ( ! is_countable( $new_instance['filters'] ) || ! is_countable( $old_instance['filters'] ) ) {
495 + continue;
496 + }
497 +
498 + if ( count( $new_instance['filters'] ) !== count( $old_instance['filters'] ) ) {
499 + $widget = $new_instance;
500 + break;
501 + }
502 +
503 + foreach ( $v as $filter_key => $new_filter_value ) {
504 + $diff = self::array_diff( $new_filter_value, $old_instance['filters'][ $filter_key ] );
505 + if ( ! empty( $diff ) ) {
506 + $widget = $new_instance;
507 + break;
508 + }
509 + }
510 + }
511 + }
512 + }
513 + }
514 +
515 + if ( empty( $widget ) ) {
516 + return false;
517 + }
518 +
519 + return array(
520 + 'action' => $action,
521 + 'widget' => self::get_widget_properties_for_tracks( $widget ),
522 + );
523 + }
524 +
525 + /**
526 + * Creates the widget properties for sending to Tracks.
527 + *
528 + * @since 5.8.0
529 + *
530 + * @param array $widget The widget instance.
531 + *
532 + * @return array The widget properties.
533 + */
534 + public static function get_widget_properties_for_tracks( $widget ) {
535 + $sanitized = array();
536 +
537 + foreach ( (array) $widget as $key => $value ) {
538 + if ( '_multiwidget' === $key ) {
539 + continue;
540 + }
541 +
542 + if ( is_scalar( $value ) ) {
543 + $key = str_replace( '-', '_', sanitize_key( $key ) );
544 + $key = "widget_{$key}";
545 + $sanitized[ $key ] = $value;
546 + }
547 + }
548 +
549 + $filters_properties = ! empty( $widget['filters'] )
550 + ? self::get_filter_properties_for_tracks( $widget['filters'] )
551 + : array();
552 +
553 + return array_merge( $sanitized, $filters_properties );
554 + }
555 +
556 + /**
557 + * Creates the filter properties for sending to Tracks.
558 + *
559 + * @since 5.8.0
560 + *
561 + * @param array $filters An array of filters.
562 + *
563 + * @return array The filter properties.
564 + */
565 + public static function get_filter_properties_for_tracks( $filters ) {
566 + if ( empty( $filters ) ) {
567 + return $filters;
568 + }
569 +
570 + $filters_properties = array(
571 + 'widget_filter_count' => count( $filters ),
572 + );
573 +
574 + foreach ( $filters as $filter ) {
575 + if ( empty( $filter['type'] ) ) {
576 + continue;
577 + }
578 +
579 + $key = sprintf( 'widget_filter_type_%s', $filter['type'] );
580 + if ( isset( $filters_properties[ $key ] ) ) {
581 + ++$filters_properties[ $key ];
582 + } else {
583 + $filters_properties[ $key ] = 1;
584 + }
585 + }
586 +
587 + return $filters_properties;
588 + }
589 +
590 + /**
591 + * Gets the active post types given a set of filters.
592 + *
593 + * @since 5.8.0
594 + *
595 + * @param array $filters The active filters for the current query.
596 + *
597 + * @return array The active post types.
598 + */
599 + public static function get_active_post_types( $filters ) {
600 + $active_post_types = array();
601 +
602 + foreach ( $filters as $item ) {
603 + if ( ( 'post_type' === $item['type'] ) && isset( $item['query_vars']['post_type'] ) ) {
604 + $active_post_types[] = $item['query_vars']['post_type'];
605 + }
606 + }
607 +
608 + return $active_post_types;
609 + }
610 +
611 + /**
612 + * Sets active to false on all post type buckets.
613 + *
614 + * @since 5.8.0
615 + *
616 + * @param array $filters The available filters for the current query.
617 + *
618 + * @return array The filters for the current query with modified active field.
619 + */
620 + public static function remove_active_from_post_type_buckets( $filters ) {
621 + $modified = $filters;
622 + foreach ( $filters as $key => $filter ) {
623 + if ( 'post_type' === $filter['type'] && ! empty( $filter['buckets'] ) ) {
624 + foreach ( $filter['buckets'] as $k => $bucket ) {
625 + $bucket['active'] = false;
626 + $modified[ $key ]['buckets'][ $k ] = $bucket;
627 + }
628 + }
629 + }
630 +
631 + return $modified;
632 + }
633 +
634 + /**
635 + * Given a url and an array of post types, will ensure that the post types are properly applied to the URL as args.
636 + *
637 + * @since 5.8.0
638 + *
639 + * @param string $url The URL to add post types to.
640 + * @param array $post_types An array of post types that should be added to the URL.
641 + *
642 + * @return string The URL with added post types.
643 + */
644 + public static function add_post_types_to_url( $url, $post_types ) {
645 + $url = self::remove_query_arg( 'post_type', $url );
646 + if ( empty( $post_types ) ) {
647 + return $url;
648 + }
649 +
650 + $url = self::add_query_arg(
651 + 'post_type',
652 + implode( ',', $post_types ),
653 + $url
654 + );
655 +
656 + return $url;
657 + }
658 +
659 + /**
660 + * Since we provide support for the widget restricting post types by adding the selected post types as
661 + * active filters, if removing a post type filter would result in there no longer be post_type args in the URL,
662 + * we need to be sure to add them back.
663 + *
664 + * @since 5.8.0
665 + *
666 + * @param array $filters An array of possible filters for the current query.
667 + * @param array $post_types The post types to ensure are on the link.
668 + *
669 + * @return array The updated array of filters with post typed added to the remove URLs.
670 + */
671 + public static function ensure_post_types_on_remove_url( $filters, $post_types ) {
672 + $modified = $filters;
673 +
674 + foreach ( (array) $filters as $filter_key => $filter ) {
675 + if ( 'post_type' !== $filter['type'] || empty( $filter['buckets'] ) ) {
676 + $modified[ $filter_key ] = $filter;
677 + continue;
678 + }
679 +
680 + foreach ( (array) $filter['buckets'] as $bucket_key => $bucket ) {
681 + if ( empty( $bucket['remove_url'] ) ) {
682 + continue;
683 + }
684 +
685 + $parsed = wp_parse_url( $bucket['remove_url'] );
686 + if ( ! $parsed ) {
687 + continue;
688 + }
689 +
690 + $query = array();
691 + if ( ! empty( $parsed['query'] ) ) {
692 + wp_parse_str( $parsed['query'], $query );
693 + }
694 +
695 + if ( empty( $query['post_type'] ) ) {
696 + $modified[ $filter_key ]['buckets'][ $bucket_key ]['remove_url'] = self::add_post_types_to_url(
697 + $bucket['remove_url'],
698 + $post_types
699 + );
700 + }
701 + }
702 + }
703 +
704 + return $modified;
705 + }
706 +
707 + /**
708 + * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
709 + * developers to disable filters supplied by the search widget. Useful if filters are
710 + * being defined at the code level.
711 + *
712 + * @since 5.8.0
713 + *
714 + * @return bool
715 + */
716 + public static function are_filters_by_widget_disabled() {
717 + /**
718 + * Allows developers to disable filters being set by widget, in favor of manually
719 + * setting filters via `Classic_Search::set_filters()`.
720 + *
721 + * @module search
722 + *
723 + * @since 5.7.0
724 + *
725 + * @param bool false
726 + */
727 + return apply_filters( 'jetpack_search_disable_widget_filters', false );
728 + }
729 +
730 + /**
731 + * Returns the maximum posts per page for a search query.
732 + *
733 + * @since 5.8.0
734 + *
735 + * @return int
736 + */
737 + public static function get_max_posts_per_page() {
738 + return Options::site_has_vip_index() ? 1000 : 100;
739 + }
740 +
741 + /**
742 + * Resolves the effective results-per-page value for the Search 3.0 blocks.
743 + *
744 + * `$override` is the author-set `resultsPerPage` block attribute; `0` (or
745 + * any non-positive value) means "use the site's `posts_per_page` Reading
746 + * setting." A non-positive Reading setting (e.g. `-1`, "show all") falls
747 + * back to 10 rather than clamping down to a single result. Either way the
748 + * result is capped at `get_max_posts_per_page()`.
749 + *
750 + * @param int $override Author override, or 0 to use the site default.
751 + * @return int Clamped, always-positive results-per-page value.
752 + */
753 + public static function resolve_results_per_page( $override = 0 ) {
754 + $value = $override > 0 ? (int) $override : (int) get_option( 'posts_per_page' );
755 + if ( $value <= 0 ) {
756 + $value = 10;
757 + }
758 + return min( $value, self::get_max_posts_per_page() );
759 + }
760 +
761 + /**
762 + * Returns the maximum offset for a search query.
763 + *
764 + * @since 5.8.0
765 + *
766 + * @return int
767 + */
768 + public static function get_max_offset() {
769 + return Options::site_has_vip_index() ? 9000 : 1000;
770 + }
771 +
772 + /**
773 + * Returns the maximum offset for a search query.
774 + *
775 + * @since 8.4.0
776 + * @param string $locale A potentially valid locale string.
777 + *
778 + * @return bool
779 + */
780 + public static function is_valid_locale( $locale ) {
781 + if ( ! class_exists( 'GP_Locales' ) ) {
782 + // Assume locale to be valid if we can't check with GlotPress.
783 + return true;
784 + }
785 + return false !== GP_Locales::by_field( 'wp_locale', $locale );
786 + }
787 +
788 + /**
789 + * Get the version number to use when loading the file. Allows us to bypass cache when developing.
790 + *
791 + * @since 8.6.0
792 + * @param string $file Path of the file we are looking for.
793 + * @return string $script_version Version number.
794 + */
795 + public static function get_asset_version( $file ) {
796 + return Package::is_development_version() && file_exists( Package::get_installed_path() . $file )
797 + ? filemtime( Package::get_installed_path() . $file )
798 + : Package::VERSION;
799 + }
800 +
801 + /**
802 + * Generates a customizer settings ID for a given post type.
803 + *
804 + * @since 8.8.0
805 + * @param object $post_type Post type object returned from get_post_types.
806 + * @return string $customizer_id Customizer setting ID.
807 + */
808 + public static function generate_post_type_customizer_id( $post_type ) {
809 + return Options::OPTION_PREFIX . 'disable_post_type_' . $post_type->name;
810 + }
811 +
812 + /**
813 + * Generates an array of post types associated with their customizer IDs.
814 + *
815 + * @since 8.8.0
816 + * @return array $ids Post type => post type customizer ID object.
817 + */
818 + public static function generate_post_type_customizer_ids() {
819 + return array_map(
820 + array( 'self', 'generate_post_type_customizer_id' ),
821 + get_post_types( array( 'exclude_from_search' => false ), 'objects' )
822 + );
823 + }
824 +
825 + /**
826 + * Sanitizes a checkbox value for writing to the database.
827 + *
828 + * @since 8.9.0
829 + *
830 + * @param mixed $value from the customizer form.
831 + * @return string either '0' or '1'.
832 + */
833 + public static function sanitize_checkbox_value( $value ) {
834 + return true === $value ? '1' : '0';
835 + }
836 +
837 + /**
838 + * Sanitizes a checkbox value for rendering the Customizer.
839 + *
840 + * @since 8.9.0
841 + *
842 + * @param mixed $value from the database.
843 + * @return boolean
844 + */
845 + public static function sanitize_checkbox_value_for_js( $value ) {
846 + return '1' === $value;
847 + }
848 +
849 + /**
850 + * Passes all options to the JS app.
851 + */
852 + public static function generate_initial_javascript_state() {
853 + $widget_options = self::get_widgets_from_option();
854 + if ( is_array( $widget_options ) ) {
855 + $widget_options = end( $widget_options );
856 + }
857 +
858 + $overlay_widget_ids = is_active_sidebar( Instant_Search::INSTANT_SEARCH_SIDEBAR ) ?
859 + wp_get_sidebars_widgets()[ Instant_Search::INSTANT_SEARCH_SIDEBAR ] : array();
860 + $filters = self::get_filters_from_widgets();
861 + $widgets = array();
862 + $widgets_outside_overlay = array();
863 + foreach ( $filters as $key => &$filter ) {
864 + $filter['filter_id'] = $key;
865 +
866 + if ( in_array( $filter['widget_id'], $overlay_widget_ids, true ) ) {
867 + if ( ! isset( $widgets[ $filter['widget_id'] ] ) ) {
868 + $widgets[ $filter['widget_id'] ]['filters'] = array();
869 + $widgets[ $filter['widget_id'] ]['widget_id'] = $filter['widget_id'];
870 + }
871 + $widgets[ $filter['widget_id'] ]['filters'][] = $filter;
872 + } else {
873 + if ( ! isset( $widgets_outside_overlay[ $filter['widget_id'] ] ) ) {
874 + $widgets_outside_overlay[ $filter['widget_id'] ]['filters'] = array();
875 + $widgets_outside_overlay[ $filter['widget_id'] ]['widget_id'] = $filter['widget_id'];
876 + }
877 + $widgets_outside_overlay[ $filter['widget_id'] ]['filters'][] = $filter;
878 + }
879 + }
880 + unset( $filter );
881 +
882 + $has_non_search_widgets = false;
883 + foreach ( $overlay_widget_ids as $overlay_widget_id ) {
884 + if ( strpos( $overlay_widget_id, self::FILTER_WIDGET_BASE ) === false ) {
885 + $has_non_search_widgets = true;
886 + break;
887 + }
888 + }
889 +
890 + $post_type_objs = get_post_types( array( 'exclude_from_search' => false ), 'objects' );
891 + $post_type_labels = array();
892 + foreach ( $post_type_objs as $key => $obj ) {
893 + $post_type_labels[ $key ] = array(
894 + 'singular_name' => $obj->labels->singular_name,
895 + 'name' => $obj->labels->name,
896 + );
897 + }
898 +
899 + $prefix = Options::OPTION_PREFIX;
900 + $posts_per_page = (int) get_option( 'posts_per_page' );
901 + if ( ( $posts_per_page > 20 ) || ( $posts_per_page <= 0 ) ) {
902 + $posts_per_page = 20;
903 + }
904 +
905 + $excluded_post_types = get_option( $prefix . 'excluded_post_types' ) ? explode( ',', get_option( $prefix . 'excluded_post_types', '' ) ) : array();
906 + $post_types = array_values(
907 + get_post_types(
908 + array(
909 + 'exclude_from_search' => false,
910 + 'public' => true,
911 + )
912 + )
913 + );
914 + $unexcluded_post_types = array_diff( $post_types, $excluded_post_types );
915 + // NOTE: If all post types are being excluded, ignore the option value.
916 + if ( array() === $unexcluded_post_types ) {
917 + $excluded_post_types = array();
918 + }
919 +
920 + $is_wpcom = static::is_wpcom();
921 + $is_private_site = ( new Status() )->is_private_site();
922 + $is_jetpack_photon_enabled = method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'photon' );
923 +
924 + $options = array(
925 + 'overlayOptions' => array(
926 + 'colorTheme' => get_option( $prefix . 'color_theme', 'light' ),
927 + 'enableInfScroll' => get_option( $prefix . 'inf_scroll', '1' ) === '1',
928 + 'enableFilteringOpensOverlay' => get_option( $prefix . 'filtering_opens_overlay', '1' ) === '1',
929 + 'enablePostDate' => get_option( $prefix . 'show_post_date', '1' ) === '1',
930 + 'enableProductPrice' => get_option( $prefix . 'show_product_price', '1' ) === '1',
931 + 'enableSort' => get_option( $prefix . 'enable_sort', '1' ) === '1',
932 + 'highlightColor' => get_option( $prefix . 'highlight_color', '#FFC' ),
933 + 'overlayTrigger' => get_option( $prefix . 'overlay_trigger', Options::DEFAULT_OVERLAY_TRIGGER ),
934 + 'resultFormat' => get_option( $prefix . 'result_format', Options::RESULT_FORMAT_MINIMAL ),
935 + 'showPoweredBy' => ( new Plan() )->is_free_plan() || ( get_option( $prefix . 'show_powered_by', '1' ) === '1' ),
936 +
937 + // These options require kicking off a new search.
938 + 'defaultSort' => get_option( $prefix . 'default_sort', 'relevance' ),
939 + 'excludedPostTypes' => $excluded_post_types,
940 +
941 + // Fallback image options
942 + 'fallbackImageUrl' => get_option( $prefix . 'fallback_image_url', '' ),
943 + 'enableFallbackImage' => get_option( $prefix . 'enable_fallback_image', '0' ) === '1',
944 + ),
945 +
946 + // core config.
947 + 'homeUrl' => home_url(),
948 + 'locale' => str_replace( '_', '-', self::is_valid_locale( get_locale() ) ? get_locale() : 'en_US' ),
949 + 'postsPerPage' => $posts_per_page,
950 + 'siteId' => self::get_wpcom_site_id(),
951 + 'searchSuggestionsEnabled' => (bool) get_option( 'jetpack_search_suggestions_enabled', false ),
952 + 'postTypes' => $post_type_labels,
953 + 'webpackPublicPath' => plugins_url( '/build/instant-search/', __DIR__ ),
954 + 'isPhotonEnabled' => ( $is_wpcom || $is_jetpack_photon_enabled ) && ! $is_private_site,
955 + 'isFreePlan' => ( new Plan() )->is_free_plan(),
956 +
957 + // config values related to private site support.
958 + 'apiRoot' => esc_url_raw( rest_url() ),
959 + 'apiNonce' => wp_create_nonce( 'wp_rest' ),
960 + 'isPrivateSite' => $is_private_site,
961 + 'isWpcom' => $is_wpcom,
962 +
963 + // widget info.
964 + 'hasOverlayWidgets' => is_countable( $overlay_widget_ids ) && count( $overlay_widget_ids ) > 0,
965 + 'widgets' => array_values( $widgets ),
966 + 'widgetsOutsideOverlay' => array_values( $widgets_outside_overlay ),
967 + 'hasNonSearchWidgets' => $has_non_search_widgets,
968 + /**
969 + * Whether to prevent tracking cookie reset. More information `pbmxuV-39H-p2`.
970 + *
971 + * @module search
972 + *
973 + * @since 0.41.0
974 + *
975 + * @param bool Prevent cookie reset for automattic sites as default value.
976 + */
977 + 'preventTrackingCookiesReset' => apply_filters( 'jetpack_instant_search_prevent_tracking_cookies_reset', function_exists( 'is_automattic' ) && is_automattic() ),
978 +
979 + /**
980 + * Whether to disable Tracks and TrainTracks analytics.
981 + *
982 + * This can be enabled via URL parameter (?disable_tracking=1) for testing,
983 + * or via the filter for permanent configuration. Useful for debugging issues
984 + * where tracking may interfere with search functionality, such as Safari's
985 + * advanced tracking protection.
986 + *
987 + * @module search
988 + *
989 + * @since 0.56.0
990 + *
991 + * @param bool $disable_tracking Whether to disable tracking. Default false.
992 + */
993 + 'disableTracking' => self::is_tracking_disabled() || apply_filters( 'jetpack_instant_search_disable_tracking', false ),
994 + 'aiAnswersEnabled' => AI_Answers::is_enabled(),
995 + 'aiMasterEnabled' => AI_Answers::is_master_enabled(),
996 + );
997 +
998 + /**
999 + * Customize Instant Search Options.
1000 + *
1001 + * @module search
1002 + *
1003 + * @since 7.7.0
1004 + *
1005 + * @param array $options Array of parameters used in Instant Search queries.
1006 + */
1007 + return apply_filters( 'jetpack_instant_search_options', $options );
1008 + }
1009 +
1010 + /**
1011 + * Default highlight fields when `highlightFields` is unset in
1012 + * `jetpack_instant_search_options`. Matches instant search's JS default.
1013 + *
1014 + * @since 7.4.0
1015 + */
1016 + const DEFAULT_INSTANT_SEARCH_HIGHLIGHT_FIELDS = array( 'title', 'content', 'comments' );
1017 +
1018 + /**
1019 + * Fields added to API `fields` when searching additional blogs.
1020 + *
1021 + * @since 7.4.0
1022 + */
1023 + const MULTISITE_SEARCH_FIELD_NAMES = array( 'author', 'blog_name', 'blog_icon_url', 'blog_id' );
1024 +
1025 + /**
1026 + * Read Instant Search query-customization options from a filtered options array.
1027 + *
1028 + * @since 7.4.0
1029 + *
1030 + * @param array $options Raw `jetpack_instant_search_options` value.
1031 + * @return array Query options with keys:
1032 + * `highlightPhraseOnly`, `highlightFilterStopwords`, `highlightFields`,
1033 + * `additionalBlogIds`, `adminQueryFilter`, and `customResults`.
1034 + */
1035 + public static function parse_instant_search_query_options( array $options ): array {
1036 + $highlight_phrase_only = ! empty( $options['highlightPhraseOnly'] );
1037 +
1038 + // The v1.3 API rejects requests combining `highlight_filter_stopwords` with
1039 + // `highlight_phrase_only` (phrase-only wins server-side) — drop the stopword
1040 + // list so requests stay valid when a filter sets both.
1041 + $stopwords = array();
1042 + if ( ! $highlight_phrase_only && ! empty( $options['highlightFilterStopwords'] ) && is_array( $options['highlightFilterStopwords'] ) ) {
1043 + $stopwords = array_values(
1044 + array_filter(
1045 + $options['highlightFilterStopwords'],
1046 + static function ( $word ) {
1047 + return is_string( $word ) && '' !== $word;
1048 + }
1049 + )
1050 + );
1051 + }
1052 +
1053 + $highlight_fields = null;
1054 + if ( ! empty( $options['highlightFields'] ) && is_array( $options['highlightFields'] ) ) {
1055 + $highlight_fields = array_values(
1056 + array_filter(
1057 + $options['highlightFields'],
1058 + static function ( $field ) {
1059 + return is_string( $field ) && '' !== $field;
1060 + }
1061 + )
1062 + );
1063 + if ( array() === $highlight_fields ) {
1064 + $highlight_fields = null;
1065 + }
1066 + }
1067 +
1068 + $additional_blog_ids = array();
1069 + if ( ! empty( $options['additionalBlogIds'] ) && is_array( $options['additionalBlogIds'] ) ) {
1070 + $additional_blog_ids = array_values( $options['additionalBlogIds'] );
1071 + }
1072 +
1073 + $admin_query_filter = null;
1074 + if ( ! empty( $options['adminQueryFilter'] ) && is_array( $options['adminQueryFilter'] ) ) {
1075 + $admin_query_filter = $options['adminQueryFilter'];
1076 + }
1077 +
1078 + $custom_results = array();
1079 + if ( ! empty( $options['customResults'] ) && is_array( $options['customResults'] ) ) {
1080 + foreach ( $options['customResults'] as $rule ) {
1081 + if ( ! is_array( $rule ) ) {
1082 + continue;
1083 + }
1084 + $pattern = isset( $rule['pattern'] ) && is_string( $rule['pattern'] ) ? $rule['pattern'] : '';
1085 + $ids = isset( $rule['ids'] ) && is_array( $rule['ids'] ) ? array_values( $rule['ids'] ) : array();
1086 + if ( '' === $pattern || array() === $ids ) {
1087 + continue;
1088 + }
1089 + $custom_results[] = array(
1090 + 'pattern' => $pattern,
1091 + 'ids' => $ids,
1092 + );
1093 + }
1094 + }
1095 +
1096 + return array(
1097 + 'highlightPhraseOnly' => $highlight_phrase_only,
1098 + 'highlightFilterStopwords' => $stopwords,
1099 + 'highlightFields' => $highlight_fields,
1100 + 'additionalBlogIds' => $additional_blog_ids,
1101 + 'adminQueryFilter' => $admin_query_filter,
1102 + 'customResults' => $custom_results,
1103 + );
1104 + }
1105 +
1106 + /**
1107 + * Read Instant Search query-customization options via `jetpack_instant_search_options`.
1108 + *
1109 + * Passing an empty array into the filter matches `Filter_Static::read_raw_entries()`
1110 + * — callbacks only add keys.
1111 + *
1112 + * @since 7.4.0
1113 + *
1114 + * @return array Query options with keys:
1115 + * `highlightPhraseOnly`, `highlightFilterStopwords`, `highlightFields`,
1116 + * `additionalBlogIds`, `adminQueryFilter`, and `customResults`.
1117 + */
1118 + public static function get_instant_search_query_options(): array {
1119 + $options = apply_filters( 'jetpack_instant_search_options', array() );
1120 + if ( ! is_array( $options ) ) {
1121 + $options = array();
1122 + }
1123 +
1124 + return self::parse_instant_search_query_options( $options );
1125 + }
1126 +
1127 + /**
1128 + * Merge Instant Search query-customization options into v1.3 API args.
1129 + *
1130 + * Shared by Inline Search (Theme) and any server-side v1.3 callers.
1131 + *
1132 + * @since 7.4.0
1133 + *
1134 + * @param array $api_query_args API query arguments.
1135 + * @param array|null $options Filtered `jetpack_instant_search_options` value. Defaults to `generate_initial_javascript_state()`.
1136 + * @return array
1137 + */
1138 + public static function apply_instant_search_query_options_to_api_args( array $api_query_args, ?array $options = null ): array {
1139 + if ( null === $options ) {
1140 + $options = self::generate_initial_javascript_state();
1141 + }
1142 + if ( ! is_array( $options ) ) {
1143 + return $api_query_args;
1144 + }
1145 +
1146 + $query_options = self::parse_instant_search_query_options( $options );
1147 +
1148 + if ( null !== $query_options['adminQueryFilter'] ) {
1149 + $api_query_args['filter'] = array(
1150 + 'bool' => array(
1151 + 'filter' => $api_query_args['filter'] ?? array(),
1152 + 'must' => $query_options['adminQueryFilter'],
1153 + ),
1154 + );
1155 + }
1156 +
1157 + if ( $query_options['highlightPhraseOnly'] ) {
1158 + $api_query_args['highlight_phrase_only'] = true;
1159 + }
1160 +
1161 + if ( ! empty( $query_options['highlightFilterStopwords'] ) ) {
1162 + $api_query_args['highlight_filter_stopwords'] = $query_options['highlightFilterStopwords'];
1163 + }
1164 +
1165 + if ( null !== $query_options['highlightFields'] ) {
1166 + $api_query_args['highlight_fields'] = $query_options['highlightFields'];
1167 + $api_query_args['highlight'] = array(
1168 + 'fields' => $query_options['highlightFields'],
1169 + );
1170 + }
1171 +
1172 + if ( ! empty( $query_options['additionalBlogIds'] ) ) {
1173 + $api_query_args['additional_blog_ids'] = $query_options['additionalBlogIds'];
1174 + $fields = isset( $api_query_args['fields'] ) && is_array( $api_query_args['fields'] )
1175 + ? $api_query_args['fields']
1176 + : array();
1177 + $api_query_args['fields'] = array_values(
1178 + array_unique( array_merge( $fields, self::MULTISITE_SEARCH_FIELD_NAMES ) )
1179 + );
1180 + }
1181 +
1182 + $matched_custom_results = self::resolve_instant_search_custom_results(
1183 + (string) ( $api_query_args['query'] ?? '' ),
1184 + $query_options['customResults']
1185 + );
1186 + if ( null !== $matched_custom_results ) {
1187 + $api_query_args['custom_results'] = $matched_custom_results;
1188 + }
1189 +
1190 + return $api_query_args;
1191 + }
1192 +
1193 + /**
1194 + * Resolve `customResults` post IDs for a search query.
1195 + *
1196 + * Mirrors instant search's exact / `regex:` pattern matching.
1197 + *
1198 + * @since 7.4.0
1199 + *
1200 + * @param string $query Current search query.
1201 + * @param array $custom_results Normalized custom-results rules.
1202 + * @return array<int, int|string>|null
1203 + */
1204 + public static function resolve_instant_search_custom_results( string $query, array $custom_results ): ?array {
1205 + if ( array() === $custom_results ) {
1206 + return null;
1207 + }
1208 +
1209 + foreach ( $custom_results as $rule ) {
1210 + $pattern = $rule['pattern'];
1211 + $ids = $rule['ids'];
1212 + if ( 0 === strpos( $pattern, 'regex:' ) ) {
1213 + // Escape the delimiter so patterns containing `/` (e.g. `regex:docs/.*`) stay valid PCRE.
1214 + $regex = '/^' . str_replace( '/', '\/', substr( $pattern, strlen( 'regex:' ) ) ) . '$/';
1215 + if ( @preg_match( $regex, $query ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Invalid user regex should be skipped, not fatal.
1216 + return $ids;
1217 + }
1218 + } elseif ( $query === $pattern ) {
1219 + return $ids;
1220 + }
1221 + }
1222 +
1223 + return null;
1224 + }
1225 +
1226 + /**
1227 + * Returns true if the site is a WordPress.com simple site, i.e. the code runs on WPCOM.
1228 + */
1229 + public static function is_wpcom() {
1230 + return defined( 'IS_WPCOM' ) && constant( 'IS_WPCOM' );
1231 + }
1232 +
1233 + /**
1234 + * Prints the Instant Search sidebar.
1235 + */
1236 + public static function print_instant_search_sidebar() {
1237 + ?>
1238 + <div class="jetpack-instant-search__widget-area" style="display: none">
1239 + <?php if ( is_active_sidebar( Instant_Search::INSTANT_SEARCH_SIDEBAR ) ) { ?>
1240 + <?php dynamic_sidebar( Instant_Search::INSTANT_SEARCH_SIDEBAR ); ?>
1241 + <?php } ?>
1242 + </div>
1243 + <?php
1244 + }
1245 +
1246 + /**
1247 + * Gets all of the active plugins via site options.
1248 + * Forked from Jetpack::get_active_plugins from the Jetpack plugin.
1249 + *
1250 + * @return string[]
1251 + */
1252 + public static function get_active_plugins() {
1253 + // active_plugins plugins as values.
1254 + $active_plugins = (array) get_option( 'active_plugins', array() );
1255 +
1256 + // active_sitewide_plugins stores plugins as keys.
1257 + if ( is_multisite() ) {
1258 + $network_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
1259 + if ( $network_plugins ) {
1260 + $active_plugins = array_merge( $active_plugins, $network_plugins );
1261 + }
1262 + }
1263 +
1264 + sort( $active_plugins );
1265 + return array_unique( $active_plugins );
1266 + }
1267 +
1268 + /**
1269 + * Get the current site's WordPress.com ID.
1270 + *
1271 + * @return int Blog ID.
1272 + */
1273 + public static function get_wpcom_site_id() {
1274 + // Returns local blog ID for a multi-site network.
1275 + if ( defined( 'IS_WPCOM' ) && constant( 'IS_WPCOM' ) ) {
1276 + return \get_current_blog_id();
1277 + }
1278 +
1279 + // Returns cache site ID.
1280 + return \Jetpack_Options::get_option( 'id' );
1281 + }
1282 +
1283 + /**
1284 + * Returns true if the free_plan is set to not empty in URL, which is used for testing purpose.
1285 + */
1286 + public static function is_forced_free_plan() {
1287 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1288 + return isset( $_GET['free_plan'] ) && $_GET['free_plan'];
1289 + }
1290 +
1291 + /**
1292 + * Returns true if the new_pricing_202210 is set to not empty in URL for testing purpose.
1293 + */
1294 + public static function is_forced_new_pricing_202208() {
1295 + $referrer = wp_get_referer();
1296 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1297 + return ( isset( $_GET['new_pricing_202208'] ) && $_GET['new_pricing_202208'] ) || $referrer && strpos( $referrer, 'new_pricing_202208=1' ) !== false;
1298 + }
1299 +
1300 + /**
1301 + * Returns true if tracking should be disabled via URL parameter, which is used for testing purposes.
1302 + *
1303 + * @since 0.56.0
1304 + *
1305 + * @return bool
1306 + */
1307 + public static function is_tracking_disabled() {
1308 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1309 + return isset( $_GET['disable_tracking'] ) && $_GET['disable_tracking'];
1310 + }
1311 +
1312 + /**
1313 + * Enqueue the WordPress.com Tracks library that drains `window._tkq` and
1314 + * sends the queued events. Shared by Instant Search and the Search blocks so
1315 + * the handle, src, and cache-busting version live in one place. Callers own
1316 + * the decision of whether to load it (e.g. the `is_tracking_disabled()` gate).
1317 + */
1318 + public static function enqueue_tracks_script() {
1319 + wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
1320 + }
1321 +}