Completed
Push — update/search/formatting-and-p... ( cb3630...02e684 )
by Alex
49:40 queued 38:52
created

Jetpack_Search_Helpers   D

Complexity

Total Complexity 112

Size/Duplication

Total Lines 520
Duplicated Lines 6.54 %

Coupling/Cohesion

Components 3
Dependencies 1

Importance

Changes 0
Metric Value
dl 34
loc 520
rs 4.8717
c 0
b 0
f 0
wmc 112
lcom 3
cbo 1

23 Methods

Rating   Name   Duplication   Size   Complexity  
A get_search_url() 0 15 3
A add_query_arg() 0 8 3
A remove_query_arg() 0 4 2
A get_widget_option_name() 0 3 1
A get_widgets_from_option() 0 10 3
A build_widget_id() 0 3 1
A is_active_widget() 0 3 1
C get_filters_from_widgets() 20 74 23
A should_rerun_search_in_customizer_preview() 0 11 4
A array_diff() 0 13 3
A post_types_differ_searchable() 0 10 2
A post_types_differ_query() 0 18 4
C get_widget_tracks_value() 0 80 21
B get_widget_properties_for_tracks() 0 20 5
B get_filter_properties_for_tracks() 0 24 5
A get_active_post_types() 0 9 4
B remove_active_from_post_type_buckets() 0 13 5
D ensure_post_types_on_remove_url() 0 35 9
B filter_post_types() 0 15 5
A site_has_vip_index() 0 17 2
A get_max_posts_per_page() 0 3 2
A get_max_offset() 0 3 2
A add_post_types_to_url() 14 14 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Jetpack_Search_Helpers often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Jetpack_Search_Helpers, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
class Jetpack_Search_Helpers {
4
	const FILTER_WIDGET_BASE = 'jetpack-search-filters';
5
6
	static function get_search_url() {
7
		$query_args = $_GET;
8
9
		// Handle the case where a permastruct is being used, such as /search/{$query}
10
		if ( ! isset( $query_args['s'] ) ) {
11
			$query_args['s'] = get_search_query();
12
		}
13
14
		if ( isset( $query_args['paged'] ) ) {
15
			unset( $query_args['paged'] );
16
		}
17
18
		$query = http_build_query( $query_args );
19
		return home_url( "?{$query}" );
20
	}
21
22
	static function add_query_arg( $key, $value = false, $url = false ) {
23
		$url = empty( $url ) ? self::get_search_url() : $url;
24
		if ( is_array( $key ) ) {
25
			return add_query_arg( $key, $url );
26
		}
27
28
		return add_query_arg( $key, $value, $url );
29
	}
30
31
	static function remove_query_arg( $key, $url = false ) {
32
		$url = empty( $url ) ? self::get_search_url() : $url;
33
		return remove_query_arg( $key, $url );
34
	}
35
36
	static function get_widget_option_name() {
37
		return sprintf( 'widget_%s', self::FILTER_WIDGET_BASE );
38
	}
39
40
	static function get_widgets_from_option() {
41
		$widget_options = get_option( self::get_widget_option_name(), array() );
42
43
		// We don't need this
44
		if ( ! empty( $widget_options ) && isset( $widget_options['_multiwidget'] ) ) {
45
			unset( $widget_options['_multiwidget'] );
46
		}
47
48
		return $widget_options;
49
	}
50
51
	static function build_widget_id( $number ) {
52
		return sprintf( '%s-%d', self::FILTER_WIDGET_BASE, $number );
53
	}
54
55
	static function is_active_widget( $widget_id ) {
56
		return (bool) is_active_widget( false, $widget_id, self::FILTER_WIDGET_BASE );
57
	}
58
59
	static function get_filters_from_widgets() {
60
		$filters = array();
61
62
		$widget_options = self::get_widgets_from_option();
63
		if ( empty( $widget_options ) ) {
64
			return $filters;
65
		}
66
67
		foreach ( (array) $widget_options as $number => $settings ) {
68
			$widget_id = self::build_widget_id( $number );
69
			if ( ! self::is_active_widget( $widget_id ) || empty( $settings['filters'] ) ) {
70
				continue;
71
			}
72
73
			if ( empty( $settings['use_filters'] ) ) {
74
				continue;
75
			}
76
77
			foreach ( (array) $settings['filters'] as $widget_filter ) {
78
				$widget_filter['widget_id'] = $widget_id;
79
				$key = sprintf( '%s_%d', $widget_filter['type'], count( $filters ) );
80
81
				if ( empty( $widget_filter['name'] ) ) {
82
					switch ( $widget_filter['type'] ) {
83
						case 'post_type':
84
							$widget_filter['name'] = _x( 'Post Types', 'label for filtering posts', 'jetpack' );
85
							break;
86
						case 'date_histogram':
87
							switch ( $widget_filter['field'] ) {
88
								case 'post_date':
89 View Code Duplication
								case 'post_date_gmt':
90
									switch ( $widget_filter['interval'] ) {
91
										case 'month':
92
											$widget_filter['name'] = _x( 'Month', 'label for filtering posts', 'jetpack' );
93
											break;
94
										case 'year':
95
											$widget_filter['name'] = _x( 'Year', 'label for filtering posts', 'jetpack' );
96
											break;
97
									}
98
									break;
99
								case 'post_modified':
100 View Code Duplication
								case 'post_modified_gmt':
101
									switch ( $widget_filter['interval'] ) {
102
										case 'month':
103
											$widget_filter['name'] = _x( 'Month Updated', 'label for filtering posts', 'jetpack' );
104
											break;
105
										case 'year':
106
											$widget_filter['name'] = _x( 'Year Updated', 'label for filtering posts', 'jetpack' );
107
											break;
108
									}
109
									break;
110
							}
111
							break;
112
						case 'taxonomy':
113
							$tax = get_taxonomy( $widget_filter['taxonomy'] );
114
							if ( ! $tax ) {
115
								break;
116
							}
117
118
							if ( isset( $tax->label ) ) {
119
								$widget_filter['name'] = $tax->label;
120
							} else if ( isset( $tax->labels ) && isset( $tax->labels->name ) ) {
121
								$widget_filter['name'] = $tax->labels->name;
122
							}
123
							break;
124
					}
125
				}
126
127
				$filters[ $key ] = $widget_filter;
128
			}
129
		}
130
131
		return $filters;
132
	}
133
134
	/**
135
	 * Whether we should rerun a search in the customizer preview or not.
136
	 *
137
	 * @since 5.8.0
138
	 *
139
	 * @return bool
140
	 */
141
	static function should_rerun_search_in_customizer_preview() {
142
		// Only update when in a customizer preview and data is being posted.
143
		// Check for $_POST removes an extra update when the customizer loads.
144
		//
145
		// Note: We use $GLOBALS['wp_customize'] here instead of is_customize_preview() to support unit tests.
146
		if ( ! isset( $GLOBALS['wp_customize'] ) || ! $GLOBALS['wp_customize']->is_preview() || empty( $_POST ) ) {
147
			return false;
148
		}
149
150
		return true;
151
	}
152
153
	/**
154
	 * Since PHP's built-in array_diff() works by comparing the values that are in array 1 to the other arrays,
155
	 * if there are less values in array 1, it's possible to get an empty diff where one might be expected.
156
	 *
157
	 * @since 5.8.0
158
	 *
159
	 * @param array $array_1
160
	 * @param array $array_2
161
	 *
162
	 * @return array
163
	 */
164
	static function array_diff( $array_1, $array_2 ) {
165
		// If the array counts are the same, then the order doesn't matter. If the count of
166
		// $array_1 is higher than $array_2, that's also fine. If the count of $array_2 is higher,
167
		// we need to swap the array order though.
168
		if ( count( $array_1 ) != count( $array_2 ) && count( $array_2 ) > count( $array_1 ) ) {
169
			$temp = $array_1;
170
			$array_1 = $array_2;
171
			$array_2 = $temp;
172
		}
173
174
		// Disregard keys
175
		return array_values( array_diff( $array_1, $array_2 ) );
176
	}
177
178
	/**
179
	 * Given the widget instance, will return true when selected post types differ from searchable post types.
180
	 *
181
	 * @since 5.8.0
182
	 *
183
	 * @param array $instance
184
	 * @return bool
185
	 */
186
	static function post_types_differ_searchable( $instance ) {
187
		if ( empty( $instance['post_types'] ) ) {
188
			return false;
189
		}
190
191
		$searchable_post_types = get_post_types( array( 'exclude_from_search' => false ) );
192
		$diff_of_searchable = self::array_diff( $searchable_post_types, (array) $instance['post_types'] );
193
194
		return ! empty( $diff_of_searchable );
195
	}
196
197
	/**
198
	 * Given the widget instance, will return true when selected post types differ from the post type filters
199
	 * applied to the search.
200
	 *
201
	 * @since 5.8.0
202
	 *
203
	 * @param array $instance
204
	 * @return bool
205
	 */
206
	static function post_types_differ_query( $instance ) {
207
		if ( empty( $instance['post_types'] ) ) {
208
			return false;
209
		}
210
211
		if ( empty( $_GET['post_type'] ) ) {
212
			$post_types_from_query = array();
213
		} else if ( is_array( $_GET['post_type'] ) ) {
214
			$post_types_from_query = $_GET['post_type'];
215
		} else {
216
			$post_types_from_query = (array) explode( ',',  $_GET['post_type'] );
217
		}
218
219
		$post_types_from_query = array_map( 'trim', $post_types_from_query );
220
221
		$diff_query = self::array_diff( (array) $instance['post_types'], $post_types_from_query );
222
		return ! empty( $diff_query );
223
	}
224
225
	static function get_widget_tracks_value( $old_value, $new_value ) {
226
		$old_value = (array) $old_value;
227
		if ( isset( $old_value['_multiwidget'] ) ) {
228
			unset( $old_value['_multiwidget'] );
229
		}
230
231
		$new_value = (array) $new_value;
232
		if ( isset( $new_value['_multiwidget'] ) ) {
233
			unset( $new_value['_multiwidget'] );
234
		}
235
236
		$action = '';
0 ignored issues
show
Unused Code introduced by
$action is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
237
		$old_keys = array_keys( $old_value );
238
		$new_keys = array_keys( $new_value );
239
240
		if ( count( $new_keys ) > count( $old_keys ) ) { // This is the case for a widget being added
241
			$diff = self::array_diff( $new_keys, $old_keys );
242
			$action = 'widget_added';
243
			$widget = empty( $diff ) || ! isset( $new_value[ $diff[0] ] )
244
				? false
245
				: $new_value[ $diff[0] ];
246
		} else if ( count( $old_keys ) > count( $new_keys ) ) { // This is the case for a widget being deleted
247
			$diff = self::array_diff( $old_keys, $new_keys );
248
			$action = 'widget_deleted';
249
			$widget = empty( $diff ) || ! isset( $old_value[ $diff[0] ] )
250
				? false
251
				: $old_value[ $diff[0] ];
252
		} else {
253
			$action = 'widget_updated';
254
			$widget = false;
255
256
			// This is a bit crazy. Since there can be multiple widgets stored in a single option,
257
			// we need to diff the old and new values to figure out which widget was updated.
258
			foreach ( $new_value as $key => $new_instance ) {
259
				if ( ! isset( $old_value[ $key ] ) ) {
260
					continue;
261
				}
262
				$old_instance = $old_value[ $key ];
263
264
				// First, let's test the keys of each instance
265
				$diff = self::array_diff( array_keys( $new_instance ), array_keys( $old_instance ) );
266
				if ( ! empty( $diff ) ) {
267
					$widget = $new_instance;
268
					break;
269
				}
270
271
				// Next, lets's loop over each value and compare it
272
				foreach ( $new_instance as $k => $v ) {
273
					if ( is_scalar( $v ) && (string) $v !== (string) $old_instance[ $k ] ) {
274
						$widget = $new_instance;
275
						break;
276
					}
277
278
					if ( 'filters' == $k ) {
279
						if ( count( $new_instance['filters'] ) != count( $old_instance['filters'] ) ) {
280
							$widget = $new_instance;
281
							break;
282
						}
283
284
						foreach ( $v as $filter_key => $new_filter_value ) {
285
							$diff = self::array_diff( $new_filter_value, $old_instance[ 'filters' ][ $filter_key ] );
286
							if ( ! empty( $diff ) ) {
287
								$widget = $new_instance;
288
								break;
289
							}
290
						}
291
					}
292
				}
293
			}
294
		}
295
296
		if ( empty( $action ) || empty( $widget ) ) {
297
			return false;
298
		}
299
300
		return array(
301
			'action' => $action,
302
			'widget' => self::get_widget_properties_for_tracks( $widget ),
303
		);
304
	}
305
306
	static function get_widget_properties_for_tracks( $widget ) {
307
		$sanitized = array();
308
309
		foreach ( (array) $widget as $key => $value ) {
310
			if ( '_multiwidget' == $key ) {
311
				continue;
312
			}
313
			if ( is_scalar( $value ) ) {
314
				$key = str_replace( '-', '_', sanitize_key( $key ) );
315
				$key = "widget_{$key}";
316
				$sanitized[ $key ] = $value;
317
			}
318
		}
319
320
		$filters_properties = ! empty( $widget['filters'] )
321
			? self::get_filter_properties_for_tracks( $widget['filters'] )
322
			: array();
323
324
		return array_merge( $sanitized, $filters_properties );
325
	}
326
327
	static function get_filter_properties_for_tracks( $filters ) {
328
		if ( empty( $filters ) ) {
329
			return $filters;
330
		}
331
332
		$filters_properties = array(
333
			'widget_filter_count' => count( $filters ),
334
		);
335
336
		foreach ( $filters as $filter ) {
337
			if ( empty( $filter['type'] ) ) {
338
				continue;
339
			}
340
341
			$key = sprintf( 'widget_filter_type_%s', $filter['type'] );
342
			if ( isset( $filters_properties[ $key ] ) ) {
343
				$filters_properties[ $key ]++;
344
			} else {
345
				$filters_properties[ $key ] = 1;
346
			}
347
		}
348
349
		return $filters_properties;
350
	}
351
352
	/**
353
	 * Gets the active post types given a set of filters.
354
	 *
355
	 * @param array $filters The active filters for the current query.
356
	 * @param array $default_post_types The default post types.
357
	 *
358
	 * @return array
359
	 */
360
	public static function get_active_post_types( $filters, $default_post_types ) {
361
		$active_post_types = array();
362
		foreach( $filters as $item ) {
363
			if ( ( 'post_type' == $item['type'] ) && isset( $item['query_vars']['post_type'] ) ) {
364
				$active_post_types[] = $item['query_vars']['post_type'];
365
			}
366
		}
367
		return $active_post_types;
368
	}
369
370
	/**
371
	 * Sets active to false on all post type buckets.
372
	 *
373
	 * @param array $filters The available filters for the current query.
374
	 *
375
	 * @return array $modified The filters for the current query with modified active field.
376
	 */
377
	public static function remove_active_from_post_type_buckets( $filters ) {
378
		$modified = $filters;
379
		foreach ( $filters as $key => $filter ) {
380
			if ( 'post_type' === $filter['type'] && ! empty( $filter['buckets'] ) ) {
381
				foreach ( $filter['buckets'] as $k => $bucket ) {
382
					$bucket['active']                  = false;
383
					$modified[ $key ]['buckets'][ $k ] = $bucket;
384
				}
385
			}
386
		}
387
388
		return $modified;
389
	}
390
391
	/**
392
	 * Given a url and an array of post types, will ensure that the post types are properly applied to the URL as args.
393
	 *
394
	 * @param string $url        The URL to add post types to.
395
	 * @param array  $post_types An array of post types that should be added to the URL.
396
	 *
397
	 * @return string $url The URL with added post types.
398
	 */
399 View Code Duplication
	public static function add_post_types_to_url( $url, $post_types ) {
400
		$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...
401
		if ( empty( $post_types ) ) {
402
			return $url;
403
		}
404
405
		$url = Jetpack_Search_Helpers::add_query_arg(
406
			'post_type',
407
			implode( ',', $post_types ),
0 ignored issues
show
Documentation introduced by
implode(',', $post_types) 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...
408
			$url
409
		);
410
411
		return $url;
412
	}
413
414
	/**
415
	 * Since we provide support for the widget restricting post types by adding the selected post types as
416
	 * active filters, if removing a post type filter would result in there no longer be post_type args in the URL,
417
	 * we need to be sure to add them back.
418
	 *
419
	 * @param array $filters    An array of possible filters for the current query.
420
	 * @param array $post_types The post types to ensure are on the link.
421
	 *
422
	 * @return array $modified The updated array of filters with post typed added to the remove URLs.
423
	 */
424
	public static function ensure_post_types_on_remove_url( $filters, $post_types ) {
425
		$modified = $filters;
426
427
		foreach ( (array) $filters as $filter_key => $filter ) {
428
			if ( 'post_type' !== $filter['type'] || empty( $filter['buckets'] ) ) {
429
				$modified[ $filter_key ] = $filter;
430
				continue;
431
			}
432
433
			foreach ( (array) $filter['buckets'] as $bucket_key => $bucket ) {
434
				if ( empty( $bucket['remove_url'] ) ) {
435
					continue;
436
				}
437
438
				$parsed = wp_parse_url( $bucket['remove_url'] );
439
				if ( ! $parsed ) {
440
					continue;
441
				}
442
443
				$query = array();
444
				if ( ! empty( $parsed['query'] ) ) {
445
					wp_parse_str( $parsed['query'], $query );
446
				}
447
448
				if ( empty( $query['post_type'] ) ) {
449
					$modified[ $filter_key ]['buckets'][ $bucket_key ]['remove_url'] = self::add_post_types_to_url(
450
						$bucket['remove_url'],
451
						$post_types
452
					);
453
				}
454
			}
455
		}
456
457
		return $modified;
458
	}
459
460
	/**
461
	 * Given an array of filters or active buckets, will filter out any that are post types.
462
	 *
463
	 * @param array $filters The array of filters or active buckets.
464
	 * @return array
465
	 */
466
	public static function filter_post_types( $filters ) {
467
		$no_post_types = array();
468
469
		foreach ( (array) $filters as $key => $filter ) {
470
			if ( empty( $filter['type'] ) || 'post_type' !== $filter['type'] ) {
471
				if ( is_int( $key ) ) {
472
					$no_post_types[] = $filter;
473
				} else {
474
					$no_post_types[ $key ] = $filter;
475
				}
476
			}
477
		}
478
479
		return $no_post_types;
480
	}
481
482
	/**
483
	 * Returns a boolen for whether the current site has a VIP index.
484
	 *
485
	 * @return bool
486
	 */
487
	public static function site_has_vip_index() {
488
		$has_vip_index = (
489
			Jetpack_Constants::is_defined( 'JETPACK_SEARCH_VIP_INDEX' ) &&
490
			Jetpack_Constants::get_constant( 'JETPACK_SEARCH_VIP_INDEX' )
491
		);
492
493
		/**
494
		 * Allows developers to filter whether the current site has a VIP index.
495
		 *
496
		 * @module search
497
		 *
498
		 * @since 5.8.0
499
		 *
500
		 * @param bool $has_vip_index Whether the current site has a VIP index.
501
		 */
502
		return apply_filters( 'jetpack_search_has_vip_index', $has_vip_index );
503
	}
504
505
	/**
506
	 * Returns the maximum posts per page for a search query
507
	 *
508
	 * @return int
509
	 */
510
	public static function get_max_posts_per_page() {
511
		return self::site_has_vip_index() ? 1000 : 100;
512
	}
513
514
	/**
515
	 * Returns the maximum offset for a search query
516
	 *
517
	 * @return int
518
	 */
519
	public static function get_max_offset() {
520
		return self::site_has_vip_index() ? 9000 : 1000;
521
	}
522
}
523