Completed
Push — add/double-encode-message ( 8b6530...2d4e84 )
by
unknown
14:26 queued 05:57
created

Jetpack_Search_Helpers::is_active_widget()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Jetpack Search: Jetpack_Search_Helpers class
4
 *
5
 * @package    Jetpack
6
 * @subpackage Jetpack Search
7
 * @since      5.8.0
8
 */
9
10
/**
11
 * Various helper functions for reuse throughout the Jetpack Search code.
12
 *
13
 * @since 5.8.0
14
 */
15
class Jetpack_Search_Helpers {
16
17
	/**
18
	 * The search widget's base ID.
19
	 *
20
	 * @since 5.8.0
21
	 * @var string
22
	 */
23
	const FILTER_WIDGET_BASE = 'jetpack-search-filters';
24
25
	/**
26
	 * Create a URL for the current search that doesn't include the "paged" parameter.
27
	 *
28
	 * @since 5.8.0
29
	 *
30
	 * @return string The search URL.
31
	 */
32
	static function get_search_url() {
33
		$query_args = stripslashes_deep( $_GET );
34
35
		// Handle the case where a permastruct is being used, such as /search/{$query}
36
		if ( ! isset( $query_args['s'] ) ) {
37
			$query_args['s'] = get_search_query();
38
		}
39
40
		if ( isset( $query_args['paged'] ) ) {
41
			unset( $query_args['paged'] );
42
		}
43
44
		$query = http_build_query( $query_args );
45
46
		return home_url( "?{$query}" );
47
	}
48
49
	/**
50
	 * Wraps add_query_arg() with the URL defaulting to the current search URL.
51
	 *
52
	 * @see   add_query_arg()
53
	 *
54
	 * @since 5.8.0
55
	 *
56
	 * @param string|array $key   Either a query variable key, or an associative array of query variables.
57
	 * @param string       $value Optional. A query variable value.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $value not be false|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
58
	 * @param bool|string  $url   Optional. A URL to act upon. Defaults to the current search URL.
59
	 *
60
	 * @return string New URL query string (unescaped).
61
	 */
62
	static function add_query_arg( $key, $value = false, $url = false ) {
63
		$url = empty( $url ) ? self::get_search_url() : $url;
64
		if ( is_array( $key ) ) {
65
			return add_query_arg( $key, $url );
66
		}
67
68
		return add_query_arg( $key, $value, $url );
69
	}
70
71
	/**
72
	 * Wraps remove_query_arg() with the URL defaulting to the current search URL.
73
	 *
74
	 * @see   remove_query_arg()
75
	 *
76
	 * @since 5.8.0
77
	 *
78
	 * @param string|array $key   Query key or keys to remove.
79
	 * @param bool|string  $query Optional. A URL to act upon.  Defaults to the current search URL.
0 ignored issues
show
Bug introduced by
There is no parameter named $query. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
80
	 *
81
	 * @return string New URL query string (unescaped).
82
	 */
83
	static function remove_query_arg( $key, $url = false ) {
84
		$url = empty( $url ) ? self::get_search_url() : $url;
85
86
		return remove_query_arg( $key, $url );
87
	}
88
89
	/**
90
	 * Returns the name of the search widget's option.
91
	 *
92
	 * @since 5.8.0
93
	 *
94
	 * @return string The search widget option name.
95
	 */
96
	static function get_widget_option_name() {
97
		return sprintf( 'widget_%s', self::FILTER_WIDGET_BASE );
98
	}
99
100
	/**
101
	 * Returns the search widget instances from the widget's option.
102
	 *
103
	 * @since 5.8.0
104
	 *
105
	 * @return array The widget options.
106
	 */
107
	static function get_widgets_from_option() {
108
		$widget_options = get_option( self::get_widget_option_name(), array() );
109
110
		// We don't need this
111
		if ( ! empty( $widget_options ) && isset( $widget_options['_multiwidget'] ) ) {
112
			unset( $widget_options['_multiwidget'] );
113
		}
114
115
		return $widget_options;
116
	}
117
118
	/**
119
	 * Returns the widget ID (widget base plus the numeric ID).
120
	 *
121
	 * @param int $number The widget's numeric ID.
122
	 *
123
	 * @return string The widget's numeric ID prefixed with the search widget base.
124
	 */
125
	static function build_widget_id( $number ) {
126
		return sprintf( '%s-%d', self::FILTER_WIDGET_BASE, $number );
127
	}
128
129
	/**
130
	 * Wrapper for is_active_widget() with the other parameters automatically supplied.
131
	 *
132
	 * @see   is_active_widget()
133
	 *
134
	 * @since 5.8.0
135
	 *
136
	 * @param int $widget_id Widget ID.
137
	 *
138
	 * @return bool Whether the widget is active or not.
139
	 */
140
	static function is_active_widget( $widget_id ) {
141
		return (bool) is_active_widget( false, $widget_id, self::FILTER_WIDGET_BASE, true );
142
	}
143
144
	/**
145
	 * Returns an array of the filters from all active search widgets.
146
	 *
147
	 * @since 5.8.0
148
	 *
149
	 * @return array Active filters.
150
	 */
151
	static function get_filters_from_widgets() {
152
		$filters = array();
153
154
		$widget_options = self::get_widgets_from_option();
155
		if ( empty( $widget_options ) ) {
156
			return $filters;
157
		}
158
159
		foreach ( (array) $widget_options as $number => $settings ) {
160
			$widget_id = self::build_widget_id( $number );
161
			if ( ! self::is_active_widget( $widget_id ) || empty( $settings['filters'] ) ) {
162
				continue;
163
			}
164
165
			foreach ( (array) $settings['filters'] as $widget_filter ) {
166
				$widget_filter['widget_id'] = $widget_id;
167
168
				if ( empty( $widget_filter['name'] ) ) {
169
					$widget_filter['name'] = self::generate_widget_filter_name( $widget_filter );
170
				}
171
172
				$key = sprintf( '%s_%d', $widget_filter['type'], count( $filters ) );
173
174
				$filters[ $key ] = $widget_filter;
175
			}
176
		}
177
178
		return $filters;
179
	}
180
181
	/**
182
	 * Get the localized default label for a date filter.
183
	 *
184
	 * @since 5.8.0
185
	 *
186
	 * @param string $type       Date type, either year or month.
187
	 * @param bool   $is_updated Whether the filter was updated or not (adds "Updated" to the end).
188
	 *
189
	 * @return string The filter label.
190
	 */
191
	static function get_date_filter_type_name( $type, $is_updated = false ) {
192
		switch ( $type ) {
193 View Code Duplication
			case 'year':
194
				$string = ( $is_updated )
195
					? esc_html_x( 'Year Updated', 'label for filtering posts', 'jetpack' )
196
					: esc_html_x( 'Year', 'label for filtering posts', 'jetpack' );
197
				break;
198
			case 'month':
199 View Code Duplication
			default:
200
				$string = ( $is_updated )
201
					? esc_html_x( 'Month Updated', 'label for filtering posts', 'jetpack' )
202
					: esc_html_x( 'Month', 'label for filtering posts', 'jetpack' );
203
				break;
204
		}
205
206
		return $string;
207
	}
208
209
	/**
210
	 * Creates a default name for a filter. Used when the filter label is blank.
211
	 *
212
	 * @since 5.8.0
213
	 *
214
	 * @param array $widget_filter The filter to generate the title for.
215
	 *
216
	 * @return string The suggested filter name.
217
	 */
218
	static function generate_widget_filter_name( $widget_filter ) {
219
		$name = '';
220
221
		switch ( $widget_filter['type'] ) {
222
			case 'post_type':
223
				$name = _x( 'Post Types', 'label for filtering posts', 'jetpack' );
224
				break;
225
226
			case 'date_histogram':
227
				$modified_fields = array(
228
					'post_modified',
229
					'post_modified_gmt',
230
				);
231
				switch ( $widget_filter['interval'] ) {
232
					case 'year':
233
						$name = self::get_date_filter_type_name(
234
							'year',
235
							in_array( $widget_filter['field'], $modified_fields )
236
						);
237
						break;
238
					case 'month':
239
					default:
240
						$name = self::get_date_filter_type_name(
241
							'month',
242
							in_array( $widget_filter['field'], $modified_fields )
243
						);
244
						break;
245
				}
246
				break;
247
248
			case 'taxonomy':
249
				$tax = get_taxonomy( $widget_filter['taxonomy'] );
250
				if ( ! $tax ) {
251
					break;
252
				}
253
254
				if ( isset( $tax->label ) ) {
255
					$name = $tax->label;
256
				} elseif ( isset( $tax->labels ) && isset( $tax->labels->name ) ) {
257
					$name = $tax->labels->name;
258
				}
259
				break;
260
		}
261
262
		return $name;
263
	}
264
265
	/**
266
	 * Whether we should rerun a search in the customizer preview or not.
267
	 *
268
	 * @since 5.8.0
269
	 *
270
	 * @return bool
271
	 */
272
	static function should_rerun_search_in_customizer_preview() {
273
		// Only update when in a customizer preview and data is being posted.
274
		// Check for $_POST removes an extra update when the customizer loads.
275
		//
276
		// Note: We use $GLOBALS['wp_customize'] here instead of is_customize_preview() to support unit tests.
277
		if ( ! isset( $GLOBALS['wp_customize'] ) || ! $GLOBALS['wp_customize']->is_preview() || empty( $_POST ) ) {
278
			return false;
279
		}
280
281
		return true;
282
	}
283
284
	/**
285
	 * Since PHP's built-in array_diff() works by comparing the values that are in array 1 to the other arrays,
286
	 * if there are less values in array 1, it's possible to get an empty diff where one might be expected.
287
	 *
288
	 * @since 5.8.0
289
	 *
290
	 * @param array $array_1
291
	 * @param array $array_2
292
	 *
293
	 * @return array
294
	 */
295
	static function array_diff( $array_1, $array_2 ) {
296
		// If the array counts are the same, then the order doesn't matter. If the count of
297
		// $array_1 is higher than $array_2, that's also fine. If the count of $array_2 is higher,
298
		// we need to swap the array order though.
299
		if ( count( $array_1 ) !== count( $array_2 ) && count( $array_2 ) > count( $array_1 ) ) {
300
			$temp    = $array_1;
301
			$array_1 = $array_2;
302
			$array_2 = $temp;
303
		}
304
305
		// Disregard keys
306
		return array_values( array_diff( $array_1, $array_2 ) );
307
	}
308
309
	/**
310
	 * Given the widget instance, will return true when selected post types differ from searchable post types.
311
	 *
312
	 * @since 5.8.0
313
	 *
314
	 * @param array $post_types An array of post types.
315
	 *
316
	 * @return bool
317
	 */
318
	static function post_types_differ_searchable( $post_types ) {
319
		if ( empty( $post_types ) ) {
320
			return false;
321
		}
322
323
		$searchable_post_types = get_post_types( array( 'exclude_from_search' => false ) );
324
		$diff_of_searchable    = self::array_diff( $searchable_post_types, (array) $post_types );
325
326
		return ! empty( $diff_of_searchable );
327
	}
328
329
	/**
330
	 * Given the array of post types, will return true when these differ from the current search query.
331
	 *
332
	 * @since 5.8.0
333
	 *
334
	 * @param array $post_types An array of post types.
335
	 *
336
	 * @return bool
337
	 */
338
	static function post_types_differ_query( $post_types ) {
339
		if ( empty( $post_types ) ) {
340
			return false;
341
		}
342
343
		if ( empty( $_GET['post_type'] ) ) {
344
			$post_types_from_query = array();
345
		} elseif ( is_array( $_GET['post_type'] ) ) {
346
			$post_types_from_query = $_GET['post_type'];
347
		} else {
348
			$post_types_from_query = (array) explode( ',', $_GET['post_type'] );
349
		}
350
351
		$post_types_from_query = array_map( 'trim', $post_types_from_query );
352
353
		$diff_query = self::array_diff( (array) $post_types, $post_types_from_query );
354
355
		return ! empty( $diff_query );
356
	}
357
358
	/**
359
	 * Determine what Tracks value should be used when updating a widget.
360
	 *
361
	 * @since 5.8.0
362
	 *
363
	 * @param mixed $old_value The old option value.
364
	 * @param mixed $new_value The new option value.
365
	 *
366
	 * @return array|false False if the widget wasn't updated, otherwise an array of the Tracks action and widget properties.
367
	 */
368
	static function get_widget_tracks_value( $old_value, $new_value ) {
369
		$old_value = (array) $old_value;
370
		if ( isset( $old_value['_multiwidget'] ) ) {
371
			unset( $old_value['_multiwidget'] );
372
		}
373
374
		$new_value = (array) $new_value;
375
		if ( isset( $new_value['_multiwidget'] ) ) {
376
			unset( $new_value['_multiwidget'] );
377
		}
378
379
		$old_keys = array_keys( $old_value );
380
		$new_keys = array_keys( $new_value );
381
382
		if ( count( $new_keys ) > count( $old_keys ) ) { // This is the case for a widget being added
383
			$diff   = self::array_diff( $new_keys, $old_keys );
384
			$action = 'widget_added';
385
			$widget = empty( $diff ) || ! isset( $new_value[ $diff[0] ] )
386
				? false
387
				: $new_value[ $diff[0] ];
388
		} elseif ( count( $old_keys ) > count( $new_keys ) ) { // This is the case for a widget being deleted
389
			$diff   = self::array_diff( $old_keys, $new_keys );
390
			$action = 'widget_deleted';
391
			$widget = empty( $diff ) || ! isset( $old_value[ $diff[0] ] )
392
				? false
393
				: $old_value[ $diff[0] ];
394
		} else {
395
			$action = 'widget_updated';
396
			$widget = false;
397
398
			// This is a bit crazy. Since there can be multiple widgets stored in a single option,
399
			// we need to diff the old and new values to figure out which widget was updated.
400
			foreach ( $new_value as $key => $new_instance ) {
401
				if ( ! isset( $old_value[ $key ] ) ) {
402
					continue;
403
				}
404
				$old_instance = $old_value[ $key ];
405
406
				// First, let's test the keys of each instance
407
				$diff = self::array_diff( array_keys( $new_instance ), array_keys( $old_instance ) );
408
				if ( ! empty( $diff ) ) {
409
					$widget = $new_instance;
410
					break;
411
				}
412
413
				// Next, lets's loop over each value and compare it
414
				foreach ( $new_instance as $k => $v ) {
415
					if ( is_scalar( $v ) && (string) $v !== (string) $old_instance[ $k ] ) {
416
						$widget = $new_instance;
417
						break;
418
					}
419
420
					if ( 'filters' == $k ) {
421
						if ( count( $new_instance['filters'] ) != count( $old_instance['filters'] ) ) {
422
							$widget = $new_instance;
423
							break;
424
						}
425
426
						foreach ( $v as $filter_key => $new_filter_value ) {
427
							$diff = self::array_diff( $new_filter_value, $old_instance['filters'][ $filter_key ] );
428
							if ( ! empty( $diff ) ) {
429
								$widget = $new_instance;
430
								break;
431
							}
432
						}
433
					}
434
				}
435
			}
436
		}
437
438
		if ( empty( $action ) || empty( $widget ) ) {
439
			return false;
440
		}
441
442
		return array(
443
			'action' => $action,
444
			'widget' => self::get_widget_properties_for_tracks( $widget ),
445
		);
446
	}
447
448
	/**
449
	 * Creates the widget properties for sending to Tracks.
450
	 *
451
	 * @since 5.8.0
452
	 *
453
	 * @param array $widget The widget instance.
454
	 *
455
	 * @return array The widget properties.
456
	 */
457
	static function get_widget_properties_for_tracks( $widget ) {
458
		$sanitized = array();
459
460
		foreach ( (array) $widget as $key => $value ) {
461
			if ( '_multiwidget' == $key ) {
462
				continue;
463
			}
464
465
			if ( is_scalar( $value ) ) {
466
				$key               = str_replace( '-', '_', sanitize_key( $key ) );
467
				$key               = "widget_{$key}";
468
				$sanitized[ $key ] = $value;
469
			}
470
		}
471
472
		$filters_properties = ! empty( $widget['filters'] )
473
			? self::get_filter_properties_for_tracks( $widget['filters'] )
474
			: array();
475
476
		return array_merge( $sanitized, $filters_properties );
477
	}
478
479
	/**
480
	 * Creates the filter properties for sending to Tracks.
481
	 *
482
	 * @since 5.8.0
483
	 *
484
	 * @param array $filters An array of filters.
485
	 *
486
	 * @return array The filter properties.
487
	 */
488
	static function get_filter_properties_for_tracks( $filters ) {
489
		if ( empty( $filters ) ) {
490
			return $filters;
491
		}
492
493
		$filters_properties = array(
494
			'widget_filter_count' => count( $filters ),
495
		);
496
497
		foreach ( $filters as $filter ) {
498
			if ( empty( $filter['type'] ) ) {
499
				continue;
500
			}
501
502
			$key = sprintf( 'widget_filter_type_%s', $filter['type'] );
503
			if ( isset( $filters_properties[ $key ] ) ) {
504
				$filters_properties[ $key ] ++;
505
			} else {
506
				$filters_properties[ $key ] = 1;
507
			}
508
		}
509
510
		return $filters_properties;
511
	}
512
513
	/**
514
	 * Gets the active post types given a set of filters.
515
	 *
516
	 * @since 5.8.0
517
	 *
518
	 * @param array $filters The active filters for the current query.
519
	 *
520
	 * @return array The active post types.
521
	 */
522
	public static function get_active_post_types( $filters ) {
523
		$active_post_types = array();
524
525
		foreach ( $filters as $item ) {
526
			if ( ( 'post_type' == $item['type'] ) && isset( $item['query_vars']['post_type'] ) ) {
527
				$active_post_types[] = $item['query_vars']['post_type'];
528
			}
529
		}
530
531
		return $active_post_types;
532
	}
533
534
	/**
535
	 * Sets active to false on all post type buckets.
536
	 *
537
	 * @since 5.8.0
538
	 *
539
	 * @param array $filters The available filters for the current query.
540
	 *
541
	 * @return array The filters for the current query with modified active field.
542
	 */
543
	public static function remove_active_from_post_type_buckets( $filters ) {
544
		$modified = $filters;
545
		foreach ( $filters as $key => $filter ) {
546
			if ( 'post_type' === $filter['type'] && ! empty( $filter['buckets'] ) ) {
547
				foreach ( $filter['buckets'] as $k => $bucket ) {
548
					$bucket['active']                  = false;
549
					$modified[ $key ]['buckets'][ $k ] = $bucket;
550
				}
551
			}
552
		}
553
554
		return $modified;
555
	}
556
557
	/**
558
	 * Given a url and an array of post types, will ensure that the post types are properly applied to the URL as args.
559
	 *
560
	 * @since 5.8.0
561
	 *
562
	 * @param string $url        The URL to add post types to.
563
	 * @param array  $post_types An array of post types that should be added to the URL.
564
	 *
565
	 * @return string The URL with added post types.
566
	 */
567
	public static function add_post_types_to_url( $url, $post_types ) {
568
		$url = Jetpack_Search_Helpers::remove_query_arg( 'post_type', $url );
0 ignored issues
show
Documentation introduced by
$url is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
569
		if ( empty( $post_types ) ) {
570
			return $url;
571
		}
572
573
		$url = Jetpack_Search_Helpers::add_query_arg(
574
			'post_type',
575
			implode( ',', $post_types ),
576
			$url
577
		);
578
579
		return $url;
580
	}
581
582
	/**
583
	 * Since we provide support for the widget restricting post types by adding the selected post types as
584
	 * active filters, if removing a post type filter would result in there no longer be post_type args in the URL,
585
	 * we need to be sure to add them back.
586
	 *
587
	 * @since 5.8.0
588
	 *
589
	 * @param array $filters    An array of possible filters for the current query.
590
	 * @param array $post_types The post types to ensure are on the link.
591
	 *
592
	 * @return array The updated array of filters with post typed added to the remove URLs.
593
	 */
594
	public static function ensure_post_types_on_remove_url( $filters, $post_types ) {
595
		$modified = $filters;
596
597
		foreach ( (array) $filters as $filter_key => $filter ) {
598
			if ( 'post_type' !== $filter['type'] || empty( $filter['buckets'] ) ) {
599
				$modified[ $filter_key ] = $filter;
600
				continue;
601
			}
602
603
			foreach ( (array) $filter['buckets'] as $bucket_key => $bucket ) {
604
				if ( empty( $bucket['remove_url'] ) ) {
605
					continue;
606
				}
607
608
				$parsed = wp_parse_url( $bucket['remove_url'] );
609
				if ( ! $parsed ) {
610
					continue;
611
				}
612
613
				$query = array();
614
				if ( ! empty( $parsed['query'] ) ) {
615
					wp_parse_str( $parsed['query'], $query );
616
				}
617
618
				if ( empty( $query['post_type'] ) ) {
619
					$modified[ $filter_key ]['buckets'][ $bucket_key ]['remove_url'] = self::add_post_types_to_url(
620
						$bucket['remove_url'],
621
						$post_types
622
					);
623
				}
624
			}
625
		}
626
627
		return $modified;
628
	}
629
630
	/**
631
	 * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
632
	 * developers to disable filters supplied by the search widget. Useful if filters are
633
	 * being defined at the code level.
634
	 *
635
	 * @since 5.8.0
636
	 *
637
	 * @return bool
638
	 */
639
	public static function are_filters_by_widget_disabled() {
640
		/**
641
		 * Allows developers to disable filters being set by widget, in favor of manually
642
		 * setting filters via `Jetpack_Search::set_filters()`.
643
		 *
644
		 * @module search
645
		 *
646
		 * @since  5.7.0
647
		 *
648
		 * @param bool false
649
		 */
650
		return apply_filters( 'jetpack_search_disable_widget_filters', false );
651
	}
652
653
	/**
654
	 * Returns a boolean for whether the current site has a VIP index.
655
	 *
656
	 * @since 5.8.0
657
	 *
658
	 * @return bool
659
	 */
660
	public static function site_has_vip_index() {
661
		$has_vip_index = (
662
			Jetpack_Constants::is_defined( 'JETPACK_SEARCH_VIP_INDEX' ) &&
663
			Jetpack_Constants::get_constant( 'JETPACK_SEARCH_VIP_INDEX' )
664
		);
665
666
		/**
667
		 * Allows developers to filter whether the current site has a VIP index.
668
		 *
669
		 * @module search
670
		 *
671
		 * @since  5.8.0
672
		 *
673
		 * @param bool $has_vip_index Whether the current site has a VIP index.
674
		 */
675
		return apply_filters( 'jetpack_search_has_vip_index', $has_vip_index );
676
	}
677
678
	/**
679
	 * Returns the maximum posts per page for a search query.
680
	 *
681
	 * @since 5.8.0
682
	 *
683
	 * @return int
684
	 */
685
	public static function get_max_posts_per_page() {
686
		return self::site_has_vip_index() ? 1000 : 100;
687
	}
688
689
	/**
690
	 * Returns the maximum offset for a search query.
691
	 *
692
	 * @since 5.8.0
693
	 *
694
	 * @return int
695
	 */
696
	public static function get_max_offset() {
697
		return self::site_has_vip_index() ? 9000 : 1000;
698
	}
699
}
700