PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.2-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.2-a.1
16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-search / src / class-helper.php
jetpack / jetpack_vendor / automattic / jetpack-search / src Last commit date
classic-search 4 days ago customberg 8 months ago customizer 3 months ago dashboard 2 months ago initializers 2 months ago inline-search 4 days ago instant-search 2 months ago search-blocks 4 days ago widgets 2 months ago wpes 9 months ago class-ai-answers.php 3 months ago class-cli.php 3 months ago class-helper.php 4 days ago class-module-control.php 2 months ago class-options.php 3 months ago class-package.php 4 days ago class-plan.php 9 months ago class-rest-controller.php 2 months ago class-settings.php 3 months ago class-stats.php 9 months ago class-template-tags.php 9 months ago
class-helper.php
1321 lines
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 );
996
997 /**
998 * Customize Instant Search Options.
999 *
1000 * @module search
1001 *
1002 * @since 7.7.0
1003 *
1004 * @param array $options Array of parameters used in Instant Search queries.
1005 */
1006 return apply_filters( 'jetpack_instant_search_options', $options );
1007 }
1008
1009 /**
1010 * Default highlight fields when `highlightFields` is unset in
1011 * `jetpack_instant_search_options`. Matches instant search's JS default.
1012 *
1013 * @since 7.4.0
1014 */
1015 const DEFAULT_INSTANT_SEARCH_HIGHLIGHT_FIELDS = array( 'title', 'content', 'comments' );
1016
1017 /**
1018 * Fields added to API `fields` when searching additional blogs.
1019 *
1020 * @since 7.4.0
1021 */
1022 const MULTISITE_SEARCH_FIELD_NAMES = array( 'author', 'blog_name', 'blog_icon_url', 'blog_id' );
1023
1024 /**
1025 * Read Instant Search query-customization options from a filtered options array.
1026 *
1027 * @since 7.4.0
1028 *
1029 * @param array $options Raw `jetpack_instant_search_options` value.
1030 * @return array Query options with keys:
1031 * `highlightPhraseOnly`, `highlightFilterStopwords`, `highlightFields`,
1032 * `additionalBlogIds`, `adminQueryFilter`, and `customResults`.
1033 */
1034 public static function parse_instant_search_query_options( array $options ): array {
1035 $highlight_phrase_only = ! empty( $options['highlightPhraseOnly'] );
1036
1037 // The v1.3 API rejects requests combining `highlight_filter_stopwords` with
1038 // `highlight_phrase_only` (phrase-only wins server-side) — drop the stopword
1039 // list so requests stay valid when a filter sets both.
1040 $stopwords = array();
1041 if ( ! $highlight_phrase_only && ! empty( $options['highlightFilterStopwords'] ) && is_array( $options['highlightFilterStopwords'] ) ) {
1042 $stopwords = array_values(
1043 array_filter(
1044 $options['highlightFilterStopwords'],
1045 static function ( $word ) {
1046 return is_string( $word ) && '' !== $word;
1047 }
1048 )
1049 );
1050 }
1051
1052 $highlight_fields = null;
1053 if ( ! empty( $options['highlightFields'] ) && is_array( $options['highlightFields'] ) ) {
1054 $highlight_fields = array_values(
1055 array_filter(
1056 $options['highlightFields'],
1057 static function ( $field ) {
1058 return is_string( $field ) && '' !== $field;
1059 }
1060 )
1061 );
1062 if ( array() === $highlight_fields ) {
1063 $highlight_fields = null;
1064 }
1065 }
1066
1067 $additional_blog_ids = array();
1068 if ( ! empty( $options['additionalBlogIds'] ) && is_array( $options['additionalBlogIds'] ) ) {
1069 $additional_blog_ids = array_values( $options['additionalBlogIds'] );
1070 }
1071
1072 $admin_query_filter = null;
1073 if ( ! empty( $options['adminQueryFilter'] ) && is_array( $options['adminQueryFilter'] ) ) {
1074 $admin_query_filter = $options['adminQueryFilter'];
1075 }
1076
1077 $custom_results = array();
1078 if ( ! empty( $options['customResults'] ) && is_array( $options['customResults'] ) ) {
1079 foreach ( $options['customResults'] as $rule ) {
1080 if ( ! is_array( $rule ) ) {
1081 continue;
1082 }
1083 $pattern = isset( $rule['pattern'] ) && is_string( $rule['pattern'] ) ? $rule['pattern'] : '';
1084 $ids = isset( $rule['ids'] ) && is_array( $rule['ids'] ) ? array_values( $rule['ids'] ) : array();
1085 if ( '' === $pattern || array() === $ids ) {
1086 continue;
1087 }
1088 $custom_results[] = array(
1089 'pattern' => $pattern,
1090 'ids' => $ids,
1091 );
1092 }
1093 }
1094
1095 return array(
1096 'highlightPhraseOnly' => $highlight_phrase_only,
1097 'highlightFilterStopwords' => $stopwords,
1098 'highlightFields' => $highlight_fields,
1099 'additionalBlogIds' => $additional_blog_ids,
1100 'adminQueryFilter' => $admin_query_filter,
1101 'customResults' => $custom_results,
1102 );
1103 }
1104
1105 /**
1106 * Read Instant Search query-customization options via `jetpack_instant_search_options`.
1107 *
1108 * Passing an empty array into the filter matches `Filter_Static::read_raw_entries()`
1109 * — callbacks only add keys.
1110 *
1111 * @since 7.4.0
1112 *
1113 * @return array Query options with keys:
1114 * `highlightPhraseOnly`, `highlightFilterStopwords`, `highlightFields`,
1115 * `additionalBlogIds`, `adminQueryFilter`, and `customResults`.
1116 */
1117 public static function get_instant_search_query_options(): array {
1118 $options = apply_filters( 'jetpack_instant_search_options', array() );
1119 if ( ! is_array( $options ) ) {
1120 $options = array();
1121 }
1122
1123 return self::parse_instant_search_query_options( $options );
1124 }
1125
1126 /**
1127 * Merge Instant Search query-customization options into v1.3 API args.
1128 *
1129 * Shared by Inline Search (Theme) and any server-side v1.3 callers.
1130 *
1131 * @since 7.4.0
1132 *
1133 * @param array $api_query_args API query arguments.
1134 * @param array|null $options Filtered `jetpack_instant_search_options` value. Defaults to `generate_initial_javascript_state()`.
1135 * @return array
1136 */
1137 public static function apply_instant_search_query_options_to_api_args( array $api_query_args, ?array $options = null ): array {
1138 if ( null === $options ) {
1139 $options = self::generate_initial_javascript_state();
1140 }
1141 if ( ! is_array( $options ) ) {
1142 return $api_query_args;
1143 }
1144
1145 $query_options = self::parse_instant_search_query_options( $options );
1146
1147 if ( null !== $query_options['adminQueryFilter'] ) {
1148 $api_query_args['filter'] = array(
1149 'bool' => array(
1150 'filter' => $api_query_args['filter'] ?? array(),
1151 'must' => $query_options['adminQueryFilter'],
1152 ),
1153 );
1154 }
1155
1156 if ( $query_options['highlightPhraseOnly'] ) {
1157 $api_query_args['highlight_phrase_only'] = true;
1158 }
1159
1160 if ( ! empty( $query_options['highlightFilterStopwords'] ) ) {
1161 $api_query_args['highlight_filter_stopwords'] = $query_options['highlightFilterStopwords'];
1162 }
1163
1164 if ( null !== $query_options['highlightFields'] ) {
1165 $api_query_args['highlight_fields'] = $query_options['highlightFields'];
1166 $api_query_args['highlight'] = array(
1167 'fields' => $query_options['highlightFields'],
1168 );
1169 }
1170
1171 if ( ! empty( $query_options['additionalBlogIds'] ) ) {
1172 $api_query_args['additional_blog_ids'] = $query_options['additionalBlogIds'];
1173 $fields = isset( $api_query_args['fields'] ) && is_array( $api_query_args['fields'] )
1174 ? $api_query_args['fields']
1175 : array();
1176 $api_query_args['fields'] = array_values(
1177 array_unique( array_merge( $fields, self::MULTISITE_SEARCH_FIELD_NAMES ) )
1178 );
1179 }
1180
1181 $matched_custom_results = self::resolve_instant_search_custom_results(
1182 (string) ( $api_query_args['query'] ?? '' ),
1183 $query_options['customResults']
1184 );
1185 if ( null !== $matched_custom_results ) {
1186 $api_query_args['custom_results'] = $matched_custom_results;
1187 }
1188
1189 return $api_query_args;
1190 }
1191
1192 /**
1193 * Resolve `customResults` post IDs for a search query.
1194 *
1195 * Mirrors instant search's exact / `regex:` pattern matching.
1196 *
1197 * @since 7.4.0
1198 *
1199 * @param string $query Current search query.
1200 * @param array $custom_results Normalized custom-results rules.
1201 * @return array<int, int|string>|null
1202 */
1203 public static function resolve_instant_search_custom_results( string $query, array $custom_results ): ?array {
1204 if ( array() === $custom_results ) {
1205 return null;
1206 }
1207
1208 foreach ( $custom_results as $rule ) {
1209 $pattern = $rule['pattern'];
1210 $ids = $rule['ids'];
1211 if ( 0 === strpos( $pattern, 'regex:' ) ) {
1212 // Escape the delimiter so patterns containing `/` (e.g. `regex:docs/.*`) stay valid PCRE.
1213 $regex = '/^' . str_replace( '/', '\/', substr( $pattern, strlen( 'regex:' ) ) ) . '$/';
1214 if ( @preg_match( $regex, $query ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Invalid user regex should be skipped, not fatal.
1215 return $ids;
1216 }
1217 } elseif ( $query === $pattern ) {
1218 return $ids;
1219 }
1220 }
1221
1222 return null;
1223 }
1224
1225 /**
1226 * Returns true if the site is a WordPress.com simple site, i.e. the code runs on WPCOM.
1227 */
1228 public static function is_wpcom() {
1229 return defined( 'IS_WPCOM' ) && constant( 'IS_WPCOM' );
1230 }
1231
1232 /**
1233 * Prints the Instant Search sidebar.
1234 */
1235 public static function print_instant_search_sidebar() {
1236 ?>
1237 <div class="jetpack-instant-search__widget-area" style="display: none">
1238 <?php if ( is_active_sidebar( Instant_Search::INSTANT_SEARCH_SIDEBAR ) ) { ?>
1239 <?php dynamic_sidebar( Instant_Search::INSTANT_SEARCH_SIDEBAR ); ?>
1240 <?php } ?>
1241 </div>
1242 <?php
1243 }
1244
1245 /**
1246 * Gets all of the active plugins via site options.
1247 * Forked from Jetpack::get_active_plugins from the Jetpack plugin.
1248 *
1249 * @return string[]
1250 */
1251 public static function get_active_plugins() {
1252 // active_plugins plugins as values.
1253 $active_plugins = (array) get_option( 'active_plugins', array() );
1254
1255 // active_sitewide_plugins stores plugins as keys.
1256 if ( is_multisite() ) {
1257 $network_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
1258 if ( $network_plugins ) {
1259 $active_plugins = array_merge( $active_plugins, $network_plugins );
1260 }
1261 }
1262
1263 sort( $active_plugins );
1264 return array_unique( $active_plugins );
1265 }
1266
1267 /**
1268 * Get the current site's WordPress.com ID.
1269 *
1270 * @return int Blog ID.
1271 */
1272 public static function get_wpcom_site_id() {
1273 // Returns local blog ID for a multi-site network.
1274 if ( defined( 'IS_WPCOM' ) && constant( 'IS_WPCOM' ) ) {
1275 return \get_current_blog_id();
1276 }
1277
1278 // Returns cache site ID.
1279 return \Jetpack_Options::get_option( 'id' );
1280 }
1281
1282 /**
1283 * Returns true if the free_plan is set to not empty in URL, which is used for testing purpose.
1284 */
1285 public static function is_forced_free_plan() {
1286 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1287 return isset( $_GET['free_plan'] ) && $_GET['free_plan'];
1288 }
1289
1290 /**
1291 * Returns true if the new_pricing_202210 is set to not empty in URL for testing purpose.
1292 */
1293 public static function is_forced_new_pricing_202208() {
1294 $referrer = wp_get_referer();
1295 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1296 return ( isset( $_GET['new_pricing_202208'] ) && $_GET['new_pricing_202208'] ) || $referrer && strpos( $referrer, 'new_pricing_202208=1' ) !== false;
1297 }
1298
1299 /**
1300 * Returns true if tracking should be disabled via URL parameter, which is used for testing purposes.
1301 *
1302 * @since 0.56.0
1303 *
1304 * @return bool
1305 */
1306 public static function is_tracking_disabled() {
1307 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1308 return isset( $_GET['disable_tracking'] ) && $_GET['disable_tracking'];
1309 }
1310
1311 /**
1312 * Enqueue the WordPress.com Tracks library that drains `window._tkq` and
1313 * sends the queued events. Shared by Instant Search and the Search blocks so
1314 * the handle, src, and cache-busting version live in one place. Callers own
1315 * the decision of whether to load it (e.g. the `is_tracking_disabled()` gate).
1316 */
1317 public static function enqueue_tracks_script() {
1318 wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
1319 }
1320 }
1321