Completed
Push — instant-search-master ( 5ddf14...5408e0 )
by
unknown
06:55
created

Jetpack_Search_Helpers::is_valid_locale()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 3
nop 1
dl 0
loc 11
rs 9.9
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
use Automattic\Jetpack\Constants;
11
12
require_once dirname( __FILE__ ) . '/class.jetpack-search-options.php';
13
14
/**
15
 * Various helper functions for reuse throughout the Jetpack Search code.
16
 *
17
 * @since 5.8.0
18
 */
19
class Jetpack_Search_Helpers {
20
21
	/**
22
	 * The search widget's base ID.
23
	 *
24
	 * @since 5.8.0
25
	 * @var string
26
	 */
27
	const FILTER_WIDGET_BASE = 'jetpack-search-filters';
28
29
	/**
30
	 * Create a URL for the current search that doesn't include the "paged" parameter.
31
	 *
32
	 * @since 5.8.0
33
	 *
34
	 * @return string The search URL.
35
	 */
36
	static function get_search_url() {
37
		$query_args = stripslashes_deep( $_GET );
38
39
		// Handle the case where a permastruct is being used, such as /search/{$query}
40
		if ( ! isset( $query_args['s'] ) ) {
41
			$query_args['s'] = get_search_query();
42
		}
43
44
		if ( isset( $query_args['paged'] ) ) {
45
			unset( $query_args['paged'] );
46
		}
47
48
		$query = http_build_query( $query_args );
49
50
		return home_url( "?{$query}" );
51
	}
52
53
	/**
54
	 * Wraps add_query_arg() with the URL defaulting to the current search URL.
55
	 *
56
	 * @see   add_query_arg()
57
	 *
58
	 * @since 5.8.0
59
	 *
60
	 * @param string|array $key   Either a query variable key, or an associative array of query variables.
61
	 * @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...
62
	 * @param bool|string  $url   Optional. A URL to act upon. Defaults to the current search URL.
63
	 *
64
	 * @return string New URL query string (unescaped).
65
	 */
66
	static function add_query_arg( $key, $value = false, $url = false ) {
67
		$url = empty( $url ) ? self::get_search_url() : $url;
68
		if ( is_array( $key ) ) {
69
			return add_query_arg( $key, $url );
70
		}
71
72
		return add_query_arg( $key, $value, $url );
73
	}
74
75
	/**
76
	 * Wraps remove_query_arg() with the URL defaulting to the current search URL.
77
	 *
78
	 * @see   remove_query_arg()
79
	 *
80
	 * @since 5.8.0
81
	 *
82
	 * @param string|array $key   Query key or keys to remove.
83
	 * @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...
84
	 *
85
	 * @return string New URL query string (unescaped).
86
	 */
87
	static function remove_query_arg( $key, $url = false ) {
88
		$url = empty( $url ) ? self::get_search_url() : $url;
89
90
		return remove_query_arg( $key, $url );
91
	}
92
93
	/**
94
	 * Returns the name of the search widget's option.
95
	 *
96
	 * @since 5.8.0
97
	 *
98
	 * @return string The search widget option name.
99
	 */
100
	static function get_widget_option_name() {
101
		return sprintf( 'widget_%s', self::FILTER_WIDGET_BASE );
102
	}
103
104
	/**
105
	 * Returns the search widget instances from the widget's option.
106
	 *
107
	 * @since 5.8.0
108
	 *
109
	 * @return array The widget options.
110
	 */
111
	static function get_widgets_from_option() {
112
		$widget_options = get_option( self::get_widget_option_name(), array() );
113
114
		// We don't need this
115
		if ( ! empty( $widget_options ) && isset( $widget_options['_multiwidget'] ) ) {
116
			unset( $widget_options['_multiwidget'] );
117
		}
118
119
		return $widget_options;
120
	}
121
122
	/**
123
	 * Returns the widget ID (widget base plus the numeric ID).
124
	 *
125
	 * @param int $number The widget's numeric ID.
126
	 *
127
	 * @return string The widget's numeric ID prefixed with the search widget base.
128
	 */
129
	static function build_widget_id( $number ) {
130
		return sprintf( '%s-%d', self::FILTER_WIDGET_BASE, $number );
131
	}
132
133
	/**
134
	 * Wrapper for is_active_widget() with the other parameters automatically supplied.
135
	 *
136
	 * @see   is_active_widget()
137
	 *
138
	 * @since 5.8.0
139
	 *
140
	 * @param int $widget_id Widget ID.
141
	 *
142
	 * @return bool Whether the widget is active or not.
143
	 */
144
	static function is_active_widget( $widget_id ) {
145
		return (bool) is_active_widget( false, $widget_id, self::FILTER_WIDGET_BASE, true );
146
	}
147
148
	/**
149
	 * Returns an array of the filters from all active search widgets.
150
	 *
151
	 * @since 5.8.0
152
	 *
153
	 * @param array|null $whitelisted_widget_ids array of whitelisted widget IDs.
154
	 *
155
	 * @return array Active filters.
156
	 */
157
	public static function get_filters_from_widgets( $whitelisted_widget_ids = null ) {
158
		$filters = array();
159
160
		$widget_options = self::get_widgets_from_option();
161
		if ( empty( $widget_options ) ) {
162
			return $filters;
163
		}
164
165
		foreach ( (array) $widget_options as $number => $settings ) {
166
			$widget_id = self::build_widget_id( $number );
167
			if ( ! self::is_active_widget( $widget_id ) || empty( $settings['filters'] ) ) {
168
				continue;
169
			}
170
			if ( isset( $whitelisted_widget_ids ) && ! in_array( $widget_id, $whitelisted_widget_ids, true ) ) {
171
				continue;
172
			}
173
174
			foreach ( (array) $settings['filters'] as $widget_filter ) {
175
				$widget_filter['widget_id'] = $widget_id;
176
177
				if ( empty( $widget_filter['name'] ) ) {
178
					$widget_filter['name'] = self::generate_widget_filter_name( $widget_filter );
179
				}
180
181
				$key = sprintf( '%s_%d', $widget_filter['type'], count( $filters ) );
182
183
				$filters[ $key ] = $widget_filter;
184
			}
185
		}
186
187
		return $filters;
188
	}
189
190
	/**
191
	 * Get the localized default label for a date filter.
192
	 *
193
	 * @since 5.8.0
194
	 *
195
	 * @param string $type       Date type, either year or month.
196
	 * @param bool   $is_updated Whether the filter was updated or not (adds "Updated" to the end).
197
	 *
198
	 * @return string The filter label.
199
	 */
200
	static function get_date_filter_type_name( $type, $is_updated = false ) {
201
		switch ( $type ) {
202 View Code Duplication
			case 'year':
203
				$string = ( $is_updated )
204
					? esc_html_x( 'Year Updated', 'label for filtering posts', 'jetpack' )
205
					: esc_html_x( 'Year', 'label for filtering posts', 'jetpack' );
206
				break;
207
			case 'month':
208 View Code Duplication
			default:
209
				$string = ( $is_updated )
210
					? esc_html_x( 'Month Updated', 'label for filtering posts', 'jetpack' )
211
					: esc_html_x( 'Month', 'label for filtering posts', 'jetpack' );
212
				break;
213
		}
214
215
		return $string;
216
	}
217
218
	/**
219
	 * Creates a default name for a filter. Used when the filter label is blank.
220
	 *
221
	 * @since 5.8.0
222
	 *
223
	 * @param array $widget_filter The filter to generate the title for.
224
	 *
225
	 * @return string The suggested filter name.
226
	 */
227
	static function generate_widget_filter_name( $widget_filter ) {
228
		$name = '';
229
230
		switch ( $widget_filter['type'] ) {
231
			case 'post_type':
232
				$name = _x( 'Post Types', 'label for filtering posts', 'jetpack' );
233
				break;
234
235
			case 'date_histogram':
236
				$modified_fields = array(
237
					'post_modified',
238
					'post_modified_gmt',
239
				);
240
				switch ( $widget_filter['interval'] ) {
241
					case 'year':
242
						$name = self::get_date_filter_type_name(
243
							'year',
244
							in_array( $widget_filter['field'], $modified_fields )
245
						);
246
						break;
247
					case 'month':
248
					default:
249
						$name = self::get_date_filter_type_name(
250
							'month',
251
							in_array( $widget_filter['field'], $modified_fields )
252
						);
253
						break;
254
				}
255
				break;
256
257
			case 'taxonomy':
258
				$tax = get_taxonomy( $widget_filter['taxonomy'] );
259
				if ( ! $tax ) {
260
					break;
261
				}
262
263
				if ( isset( $tax->label ) ) {
264
					$name = $tax->label;
265
				} elseif ( isset( $tax->labels ) && isset( $tax->labels->name ) ) {
266
					$name = $tax->labels->name;
267
				}
268
				break;
269
		}
270
271
		return $name;
272
	}
273
274
	/**
275
	 * Whether we should rerun a search in the customizer preview or not.
276
	 *
277
	 * @since 5.8.0
278
	 *
279
	 * @return bool
280
	 */
281
	static function should_rerun_search_in_customizer_preview() {
282
		// Only update when in a customizer preview and data is being posted.
283
		// Check for $_POST removes an extra update when the customizer loads.
284
		//
285
		// Note: We use $GLOBALS['wp_customize'] here instead of is_customize_preview() to support unit tests.
286
		if ( ! isset( $GLOBALS['wp_customize'] ) || ! $GLOBALS['wp_customize']->is_preview() || empty( $_POST ) ) {
287
			return false;
288
		}
289
290
		return true;
291
	}
292
293
	/**
294
	 * Since PHP's built-in array_diff() works by comparing the values that are in array 1 to the other arrays,
295
	 * if there are less values in array 1, it's possible to get an empty diff where one might be expected.
296
	 *
297
	 * @since 5.8.0
298
	 *
299
	 * @param array $array_1
300
	 * @param array $array_2
301
	 *
302
	 * @return array
303
	 */
304
	static function array_diff( $array_1, $array_2 ) {
305
		// If the array counts are the same, then the order doesn't matter. If the count of
306
		// $array_1 is higher than $array_2, that's also fine. If the count of $array_2 is higher,
307
		// we need to swap the array order though.
308
		if ( count( $array_1 ) !== count( $array_2 ) && count( $array_2 ) > count( $array_1 ) ) {
309
			$temp    = $array_1;
310
			$array_1 = $array_2;
311
			$array_2 = $temp;
312
		}
313
314
		// Disregard keys
315
		return array_values( array_diff( $array_1, $array_2 ) );
316
	}
317
318
	/**
319
	 * Given the widget instance, will return true when selected post types differ from searchable post types.
320
	 *
321
	 * @since 5.8.0
322
	 *
323
	 * @param array $post_types An array of post types.
324
	 *
325
	 * @return bool
326
	 */
327
	static function post_types_differ_searchable( $post_types ) {
328
		if ( empty( $post_types ) ) {
329
			return false;
330
		}
331
332
		$searchable_post_types = get_post_types( array( 'exclude_from_search' => false ) );
333
		$diff_of_searchable    = self::array_diff( $searchable_post_types, (array) $post_types );
334
335
		return ! empty( $diff_of_searchable );
336
	}
337
338
	/**
339
	 * Given the array of post types, will return true when these differ from the current search query.
340
	 *
341
	 * @since 5.8.0
342
	 *
343
	 * @param array $post_types An array of post types.
344
	 *
345
	 * @return bool
346
	 */
347
	static function post_types_differ_query( $post_types ) {
348
		if ( empty( $post_types ) ) {
349
			return false;
350
		}
351
352
		if ( empty( $_GET['post_type'] ) ) {
353
			$post_types_from_query = array();
354
		} elseif ( is_array( $_GET['post_type'] ) ) {
355
			$post_types_from_query = $_GET['post_type'];
356
		} else {
357
			$post_types_from_query = (array) explode( ',', $_GET['post_type'] );
358
		}
359
360
		$post_types_from_query = array_map( 'trim', $post_types_from_query );
361
362
		$diff_query = self::array_diff( (array) $post_types, $post_types_from_query );
363
364
		return ! empty( $diff_query );
365
	}
366
367
	/**
368
	 * Determine what Tracks value should be used when updating a widget.
369
	 *
370
	 * @since 5.8.0
371
	 *
372
	 * @param mixed $old_value The old option value.
373
	 * @param mixed $new_value The new option value.
374
	 *
375
	 * @return array|false False if the widget wasn't updated, otherwise an array of the Tracks action and widget properties.
376
	 */
377
	static function get_widget_tracks_value( $old_value, $new_value ) {
378
		$old_value = (array) $old_value;
379
		if ( isset( $old_value['_multiwidget'] ) ) {
380
			unset( $old_value['_multiwidget'] );
381
		}
382
383
		$new_value = (array) $new_value;
384
		if ( isset( $new_value['_multiwidget'] ) ) {
385
			unset( $new_value['_multiwidget'] );
386
		}
387
388
		$old_keys = array_keys( $old_value );
389
		$new_keys = array_keys( $new_value );
390
391
		if ( count( $new_keys ) > count( $old_keys ) ) { // This is the case for a widget being added
392
			$diff   = self::array_diff( $new_keys, $old_keys );
393
			$action = 'widget_added';
394
			$widget = empty( $diff ) || ! isset( $new_value[ $diff[0] ] )
395
				? false
396
				: $new_value[ $diff[0] ];
397
		} elseif ( count( $old_keys ) > count( $new_keys ) ) { // This is the case for a widget being deleted
398
			$diff   = self::array_diff( $old_keys, $new_keys );
399
			$action = 'widget_deleted';
400
			$widget = empty( $diff ) || ! isset( $old_value[ $diff[0] ] )
401
				? false
402
				: $old_value[ $diff[0] ];
403
		} else {
404
			$action = 'widget_updated';
405
			$widget = false;
406
407
			// This is a bit crazy. Since there can be multiple widgets stored in a single option,
408
			// we need to diff the old and new values to figure out which widget was updated.
409
			foreach ( $new_value as $key => $new_instance ) {
410
				if ( ! isset( $old_value[ $key ] ) ) {
411
					continue;
412
				}
413
				$old_instance = $old_value[ $key ];
414
415
				// First, let's test the keys of each instance
416
				$diff = self::array_diff( array_keys( $new_instance ), array_keys( $old_instance ) );
417
				if ( ! empty( $diff ) ) {
418
					$widget = $new_instance;
419
					break;
420
				}
421
422
				// Next, lets's loop over each value and compare it
423
				foreach ( $new_instance as $k => $v ) {
424
					if ( is_scalar( $v ) && (string) $v !== (string) $old_instance[ $k ] ) {
425
						$widget = $new_instance;
426
						break;
427
					}
428
429
					if ( 'filters' == $k ) {
430
						if ( count( $new_instance['filters'] ) != count( $old_instance['filters'] ) ) {
431
							$widget = $new_instance;
432
							break;
433
						}
434
435
						foreach ( $v as $filter_key => $new_filter_value ) {
436
							$diff = self::array_diff( $new_filter_value, $old_instance['filters'][ $filter_key ] );
437
							if ( ! empty( $diff ) ) {
438
								$widget = $new_instance;
439
								break;
440
							}
441
						}
442
					}
443
				}
444
			}
445
		}
446
447
		if ( empty( $action ) || empty( $widget ) ) {
448
			return false;
449
		}
450
451
		return array(
452
			'action' => $action,
453
			'widget' => self::get_widget_properties_for_tracks( $widget ),
454
		);
455
	}
456
457
	/**
458
	 * Creates the widget properties for sending to Tracks.
459
	 *
460
	 * @since 5.8.0
461
	 *
462
	 * @param array $widget The widget instance.
463
	 *
464
	 * @return array The widget properties.
465
	 */
466
	static function get_widget_properties_for_tracks( $widget ) {
467
		$sanitized = array();
468
469
		foreach ( (array) $widget as $key => $value ) {
470
			if ( '_multiwidget' == $key ) {
471
				continue;
472
			}
473
474
			if ( is_scalar( $value ) ) {
475
				$key               = str_replace( '-', '_', sanitize_key( $key ) );
476
				$key               = "widget_{$key}";
477
				$sanitized[ $key ] = $value;
478
			}
479
		}
480
481
		$filters_properties = ! empty( $widget['filters'] )
482
			? self::get_filter_properties_for_tracks( $widget['filters'] )
483
			: array();
484
485
		return array_merge( $sanitized, $filters_properties );
486
	}
487
488
	/**
489
	 * Creates the filter properties for sending to Tracks.
490
	 *
491
	 * @since 5.8.0
492
	 *
493
	 * @param array $filters An array of filters.
494
	 *
495
	 * @return array The filter properties.
496
	 */
497
	static function get_filter_properties_for_tracks( $filters ) {
498
		if ( empty( $filters ) ) {
499
			return $filters;
500
		}
501
502
		$filters_properties = array(
503
			'widget_filter_count' => count( $filters ),
504
		);
505
506
		foreach ( $filters as $filter ) {
507
			if ( empty( $filter['type'] ) ) {
508
				continue;
509
			}
510
511
			$key = sprintf( 'widget_filter_type_%s', $filter['type'] );
512
			if ( isset( $filters_properties[ $key ] ) ) {
513
				$filters_properties[ $key ] ++;
514
			} else {
515
				$filters_properties[ $key ] = 1;
516
			}
517
		}
518
519
		return $filters_properties;
520
	}
521
522
	/**
523
	 * Gets the active post types given a set of filters.
524
	 *
525
	 * @since 5.8.0
526
	 *
527
	 * @param array $filters The active filters for the current query.
528
	 *
529
	 * @return array The active post types.
530
	 */
531
	public static function get_active_post_types( $filters ) {
532
		$active_post_types = array();
533
534
		foreach ( $filters as $item ) {
535
			if ( ( 'post_type' == $item['type'] ) && isset( $item['query_vars']['post_type'] ) ) {
536
				$active_post_types[] = $item['query_vars']['post_type'];
537
			}
538
		}
539
540
		return $active_post_types;
541
	}
542
543
	/**
544
	 * Sets active to false on all post type buckets.
545
	 *
546
	 * @since 5.8.0
547
	 *
548
	 * @param array $filters The available filters for the current query.
549
	 *
550
	 * @return array The filters for the current query with modified active field.
551
	 */
552
	public static function remove_active_from_post_type_buckets( $filters ) {
553
		$modified = $filters;
554
		foreach ( $filters as $key => $filter ) {
555
			if ( 'post_type' === $filter['type'] && ! empty( $filter['buckets'] ) ) {
556
				foreach ( $filter['buckets'] as $k => $bucket ) {
557
					$bucket['active']                  = false;
558
					$modified[ $key ]['buckets'][ $k ] = $bucket;
559
				}
560
			}
561
		}
562
563
		return $modified;
564
	}
565
566
	/**
567
	 * Given a url and an array of post types, will ensure that the post types are properly applied to the URL as args.
568
	 *
569
	 * @since 5.8.0
570
	 *
571
	 * @param string $url        The URL to add post types to.
572
	 * @param array  $post_types An array of post types that should be added to the URL.
573
	 *
574
	 * @return string The URL with added post types.
575
	 */
576
	public static function add_post_types_to_url( $url, $post_types ) {
577
		$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...
578
		if ( empty( $post_types ) ) {
579
			return $url;
580
		}
581
582
		$url = Jetpack_Search_Helpers::add_query_arg(
583
			'post_type',
584
			implode( ',', $post_types ),
585
			$url
586
		);
587
588
		return $url;
589
	}
590
591
	/**
592
	 * Since we provide support for the widget restricting post types by adding the selected post types as
593
	 * active filters, if removing a post type filter would result in there no longer be post_type args in the URL,
594
	 * we need to be sure to add them back.
595
	 *
596
	 * @since 5.8.0
597
	 *
598
	 * @param array $filters    An array of possible filters for the current query.
599
	 * @param array $post_types The post types to ensure are on the link.
600
	 *
601
	 * @return array The updated array of filters with post typed added to the remove URLs.
602
	 */
603
	public static function ensure_post_types_on_remove_url( $filters, $post_types ) {
604
		$modified = $filters;
605
606
		foreach ( (array) $filters as $filter_key => $filter ) {
607
			if ( 'post_type' !== $filter['type'] || empty( $filter['buckets'] ) ) {
608
				$modified[ $filter_key ] = $filter;
609
				continue;
610
			}
611
612
			foreach ( (array) $filter['buckets'] as $bucket_key => $bucket ) {
613
				if ( empty( $bucket['remove_url'] ) ) {
614
					continue;
615
				}
616
617
				$parsed = wp_parse_url( $bucket['remove_url'] );
618
				if ( ! $parsed ) {
619
					continue;
620
				}
621
622
				$query = array();
623
				if ( ! empty( $parsed['query'] ) ) {
624
					wp_parse_str( $parsed['query'], $query );
625
				}
626
627
				if ( empty( $query['post_type'] ) ) {
628
					$modified[ $filter_key ]['buckets'][ $bucket_key ]['remove_url'] = self::add_post_types_to_url(
629
						$bucket['remove_url'],
630
						$post_types
631
					);
632
				}
633
			}
634
		}
635
636
		return $modified;
637
	}
638
639
	/**
640
	 * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
641
	 * developers to disable filters supplied by the search widget. Useful if filters are
642
	 * being defined at the code level.
643
	 *
644
	 * @since 5.8.0
645
	 *
646
	 * @return bool
647
	 */
648
	public static function are_filters_by_widget_disabled() {
649
		/**
650
		 * Allows developers to disable filters being set by widget, in favor of manually
651
		 * setting filters via `Jetpack_Search::set_filters()`.
652
		 *
653
		 * @module search
654
		 *
655
		 * @since  5.7.0
656
		 *
657
		 * @param bool false
658
		 */
659
		return apply_filters( 'jetpack_search_disable_widget_filters', false );
660
	}
661
662
	/**
663
	 * Returns the maximum posts per page for a search query.
664
	 *
665
	 * @since 5.8.0
666
	 *
667
	 * @return int
668
	 */
669
	public static function get_max_posts_per_page() {
670
		return Jetpack_Search_Options::site_has_vip_index() ? 1000 : 100;
671
	}
672
673
	/**
674
	 * Returns the maximum offset for a search query.
675
	 *
676
	 * @since 5.8.0
677
	 *
678
	 * @return int
679
	 */
680
	public static function get_max_offset() {
681
		return Jetpack_Search_Options::site_has_vip_index() ? 9000 : 1000;
682
	}
683
684
	/**
685
	 * Returns the maximum offset for a search query.
686
	 *
687
	 * @since 8.4.0
688
	 * @param string $locale    A potentially valid locale string.
689
	 *
690
	 * @return bool
691
	 */
692
	public static function is_valid_locale( $locale ) {
693
		if ( ! class_exists( 'GP_Locales' ) ) {
694
			if ( defined( 'JETPACK__GLOTPRESS_LOCALES_PATH' ) && file_exists( JETPACK__GLOTPRESS_LOCALES_PATH ) ) {
695
				require JETPACK__GLOTPRESS_LOCALES_PATH;
696
			} else {
697
				// Assume locale to be valid if we can't check with GlotPress.
698
				return true;
699
			}
700
		}
701
		return false !== GP_Locales::by_field( 'wp_locale', $locale );
702
	}
703
}
704