Completed
Push — develop ( 97838c...1cdc08 )
by Aristeides
03:14
created

Kirki_Fonts_Google::create_link()   C

Complexity

Conditions 10
Paths 43

Size

Total Lines 49
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 24
nc 43
nop 0
dl 0
loc 49
rs 5.5471
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Processes typography-related fields
4
 * and generates the google-font link.
5
 *
6
 * @package     Kirki
7
 * @category    Core
8
 * @author      Aristeides Stathopoulos
9
 * @copyright   Copyright (c) 2016, Aristeides Stathopoulos
10
 * @license     http://opensource.org/licenses/https://opensource.org/licenses/MIT
11
 * @since       1.0
12
 */
13
14
/**
15
 * Manages the way Google Fonts are enqueued.
16
 */
17
final class Kirki_Fonts_Google {
18
19
	/**
20
	 * The Kirki_Fonts_Google instance.
21
	 * We use the singleton pattern here to avoid loading the google-font array multiple times.
22
	 * This is mostly a performance tweak.
23
	 *
24
	 * @access private
25
	 * @var null|object
26
	 */
27
	private static $instance = null;
28
29
	/**
30
	 * If set to true, forces loading ALL variants.
31
	 *
32
	 * @static
33
	 * @access public
34
	 * @var bool
35
	 */
36
	public static $force_load_all_variants = false;
37
38
	/**
39
	 * If set to true, forces loading ALL subsets.
40
	 *
41
	 * @static
42
	 * @access public
43
	 * @var bool
44
	 */
45
	public static $force_load_all_subsets = false;
46
47
	/**
48
	 * The array of fonts
49
	 *
50
	 * @access private
51
	 * @var array
52
	 */
53
	private $fonts = array();
54
55
	/**
56
	 * An array of all google fonts.
57
	 *
58
	 * @access private
59
	 * @var array
60
	 */
61
	private $google_fonts = array();
62
63
	/**
64
	 * The array of subsets
65
	 *
66
	 * @access private
67
	 * @var array
68
	 */
69
	private $subsets = array();
70
71
	/**
72
	 * The google link
73
	 *
74
	 * @access private
75
	 * @var string
76
	 */
77
	private $link = '';
78
79
	/**
80
	 * Which method to use when loading googlefonts.
81
	 * Available options: link, js, embed.
82
	 *
83
	 * @static
84
	 * @access private
85
	 * @since 3.0.0
86
	 * @var string
87
	 */
88
	private static $method = array(
89
		'global' => 'embed',
90
	);
91
92
	/**
93
	 * Whether we should fallback to the link method or not.
94
	 *
95
	 * @access private
96
	 * @since 3.0.0
97
	 * @var bool
98
	 */
99
	private $fallback_to_link = false;
100
101
	/**
102
	 * The class constructor.
103
	 */
104
	private function __construct() {
105
106
		$config = apply_filters( 'kirki/config', array() );
107
108
		// Get the $fallback_to_link value from transient.
109
		$fallback_to_link = get_transient( 'kirki_googlefonts_fallback_to_link' );
110
		if ( 'yes' === $fallback_to_link ) {
111
			$this->fallback_to_link = true;
112
		}
113
114
		// Use links when in the customizer.
115
		global $wp_customize;
116
		if ( $wp_customize ) {
117
			$this->fallback_to_link = true;
118
		}
119
120
		// If we have set $config['disable_google_fonts'] to true then do not proceed any further.
121
		if ( isset( $config['disable_google_fonts'] ) && true === $config['disable_google_fonts'] ) {
122
			return;
123
		}
124
125
		// Populate the array of google fonts.
126
		$this->google_fonts = Kirki_Fonts::get_google_fonts();
127
128
		// Process the request.
129
		$this->process_request();
130
131
	}
132
133
	/**
134
	 * Processes the request according to the method we're using.
135
	 *
136
	 * @access protected
137
	 * @since 3.0.0
138
	 */
139
	protected function process_request() {
140
141
		// Figure out which method to use for all.
142
		$method = 'link';
143
		foreach ( self::$method as $config_id => $method ) {
0 ignored issues
show
Bug introduced by
The expression self::$method of type string is not traversable.
Loading history...
144
			if ( 'embed' === $method && true !== $this->fallback_to_link ) {
145
				$method = 'embed';
146
			}
147
		}
148
		foreach ( self::$method as $config_id => $config_method ) {
0 ignored issues
show
Bug introduced by
The expression self::$method of type string is not traversable.
Loading history...
149
150
			switch ( $method ) {
151
152
				case 'embed':
153
					add_filter( 'kirki/' . $config_id . '/dynamic_css', array( $this, 'embed_css' ) );
154
155
					if ( true === $this->fallback_to_link ) {
156
						// Fallback to enqueue method.
157
						add_action( 'wp_enqueue_scripts', array( $this, 'enqueue' ), 105 );
158
					}
159
					break;
160
				case 'js':
161
					// TODO: Build a JS method.
162
					break;
163
				case 'link':
164
					// Enqueue link.
165
					add_action( 'wp_enqueue_scripts', array( $this, 'enqueue' ), 105 );
166
					break;
167
			}
168
		}
169
	}
170
171
	/**
172
	 * Get the one, true instance of this class.
173
	 * Prevents performance issues since this is only loaded once.
174
	 *
175
	 * @return object Kirki_Fonts_Google
176
	 */
177
	public static function get_instance() {
178
		if ( null === self::$instance ) {
179
			self::$instance = new Kirki_Fonts_Google();
180
		}
181
		return self::$instance;
182
	}
183
184
	/**
185
	 * Calls all the other necessary methods to populate and create the link.
186
	 */
187
	public function enqueue() {
188
189
		// Go through our fields and populate $this->fonts.
190
		$this->loop_fields();
191
192
		$this->fonts = apply_filters( 'kirki/enqueue_google_fonts', $this->fonts );
193
194
		// Goes through $this->fonts and adds or removes things as needed.
195
		$this->process_fonts();
196
197
		// Go through $this->fonts and populate $this->link.
198
		$this->create_link();
199
200
		// If $this->link is not empty then enqueue it.
201
		if ( '' !== $this->link ) {
202
			wp_enqueue_style( 'kirki_google_fonts', $this->link, array(), null );
203
		}
204
	}
205
206
	/**
207
	 * Goes through all our fields and then populates the $this->fonts property.
208
	 */
209
	private function loop_fields() {
210
		foreach ( Kirki::$fields as $field ) {
211
			$this->generate_google_font( $field );
212
		}
213
	}
214
215
	/**
216
	 * Processes the arguments of a field
217
	 * determines if it's a typography field
218
	 * and if it is, then takes appropriate actions.
219
	 *
220
	 * @param array $args The field arguments.
221
	 */
222
	private function generate_google_font( $args ) {
223
224
		// Process typography fields.
225
		if ( isset( $args['type'] ) && 'kirki-typography' === $args['type'] ) {
226
227
			// Get the value.
228
			$value = Kirki_Values::get_sanitized_field_value( $args );
229
230
			// If we don't have a font-family then we can skip this.
231
			if ( ! isset( $value['font-family'] ) ) {
232
				return;
233
			}
234
235
			// Add support for older formats of the typography control.
236
			// We used to have font-weight instead of variant.
237
			if ( isset( $value['font-weight'] ) && ( ! isset( $value['variant'] ) || empty( $value['variant'] ) ) ) {
238
				$value['variant'] = $value['font-weight'];
239
			}
240
241
			// Set a default value for variants.
242
			if ( ! isset( $value['variant'] ) ) {
243
				$value['variant'] = 'regular';
244
			}
245
			if ( isset( $value['subsets'] ) ) {
246
247
				// Add the subset directly to the array of subsets in the Kirki_GoogleFonts_Manager object.
248
				// Subsets must be applied to ALL fonts if possible.
249
				if ( ! is_array( $value['subsets'] ) ) {
250
					$this->subsets[] = $value['subsets'];
251
				} else {
252
					foreach ( $value['subsets'] as $subset ) {
253
						$this->subsets[] = $subset;
254
					}
255
				}
256
			}
257
258
			// Add the requested google-font.
259
			if ( ! isset( $this->fonts[ $value['font-family'] ] ) ) {
260
				$this->fonts[ $value['font-family'] ] = array();
261
			}
262
			if ( ! in_array( $value['variant'], $this->fonts[ $value['font-family'] ], true ) ) {
263
				$this->fonts[ $value['font-family'] ][] = $value['variant'];
264
			}
265
		} else {
266
267
			// Process non-typography fields.
268
			if ( isset( $args['output'] ) && is_array( $args['output'] ) ) {
269
				foreach ( $args['output'] as $output ) {
270
271
					// If we don't have a typography-related output argument we can skip this.
272
					if ( ! isset( $output['property'] ) || ! in_array( $output['property'], array( 'font-family', 'font-weight', 'font-subset', 'subset', 'subsets' ), true ) ) {
273
						continue;
274
					}
275
276
					// Get the value.
277
					$value = Kirki_Values::get_sanitized_field_value( $args );
278
279
					if ( 'font-family' === $output['property'] ) {
280
						if ( ! array_key_exists( $value, $this->fonts ) ) {
281
							$this->fonts[ $value ] = array();
282
						}
283
					} elseif ( 'font-weight' === $output['property'] ) {
284
						foreach ( $this->fonts as $font => $variants ) {
285
							if ( ! in_array( $value, $variants, true ) ) {
286
								$this->fonts[ $font ][] = $value;
287
							}
288
						}
289
					} elseif ( 'font-subset' === $output['property'] || 'subset' === $output['property'] || 'subsets' === $output['property'] ) {
290
						if ( ! is_array( $value ) ) {
291
							if ( ! in_array( $value, $this->subsets, true ) ) {
292
								$this->subsets[] = $value;
293
							}
294
						} else {
295
							foreach ( $value as $subset ) {
296
								if ( ! in_array( $subset, $this->subsets, true ) ) {
297
									$this->subsets[] = $subset;
298
								}
299
							}
300
						}
301
					}
302
				}
303
			} // End if().
304
		} // End if().
305
	}
306
307
	/**
308
	 * Determines the vbalidity of the selected font as well as its properties.
309
	 * This is vital to make sure that the google-font script that we'll generate later
310
	 * does not contain any invalid options.
311
	 */
312
	private function process_fonts() {
313
314
		// Early exit if font-family is empty.
315
		if ( empty( $this->fonts ) ) {
316
			return;
317
		}
318
319
		$valid_subsets = array();
320
		foreach ( $this->fonts as $font => $variants ) {
321
322
			// Determine if this is indeed a google font or not.
323
			// If it's not, then just remove it from the array.
324
			if ( ! array_key_exists( $font, $this->google_fonts ) ) {
325
				unset( $this->fonts[ $font ] );
326
				continue;
327
			}
328
329
			// Get all valid font variants for this font.
330
			$font_variants = array();
331
			if ( isset( $this->google_fonts[ $font ]['variants'] ) ) {
332
				$font_variants = $this->google_fonts[ $font ]['variants'];
333
			}
334
			foreach ( $variants as $variant ) {
335
336
				// If this is not a valid variant for this font-family
337
				// then unset it and move on to the next one.
338
				if ( ! in_array( $variant, $font_variants, true ) ) {
339
					$variant_key = array_search( $variant, $this->fonts[ $font ] );
340
					unset( $this->fonts[ $font ][ $variant_key ] );
341
					continue;
342
				}
343
			}
344
345
			// Check if the selected subsets exist, even in one of the selected fonts.
346
			// If they don't, then they have to be removed otherwise the link will fail.
347
			if ( isset( $this->google_fonts[ $font ]['subsets'] ) ) {
348
				foreach ( $this->subsets as $subset ) {
349
					if ( in_array( $subset, $this->google_fonts[ $font ]['subsets'], true ) ) {
350
						$valid_subsets[] = $subset;
351
					}
352
				}
353
			}
354
		}
355
		$this->subsets = $valid_subsets;
356
	}
357
358
	/**
359
	 * Creates the google-fonts link.
360
	 */
361
	private function create_link() {
362
363
		// If we don't have any fonts then we can exit.
364
		if ( empty( $this->fonts ) ) {
365
			return;
366
		}
367
368
		// Add a fallback to Roboto.
369
		$font = 'Roboto';
370
371
		// Get font-family + subsets.
372
		$link_fonts = array();
373
		foreach ( $this->fonts as $font => $variants ) {
374
375
			// Are we force-loading all variants?
376
			if ( true === self::$force_load_all_variants ) {
377
				if ( isset( $this->google_fonts[ $font ]['variants'] ) ) {
378
					$variants = $this->google_fonts[ $font ]['variants'];
379
				}
380
			}
381
			$variants = implode( ',', $variants );
382
383
			$link_font = str_replace( ' ', '+', $font );
384
			if ( ! empty( $variants ) ) {
385
				$link_font .= ':' . $variants;
386
			}
387
			$link_fonts[] = $link_font;
388
		}
389
390
		// Are we force-loading all subsets?
391
		if ( true === self::$force_load_all_subsets ) {
392
393
			if ( isset( $this->google_fonts[ $font ]['subsets'] ) ) {
394
				foreach ( $this->google_fonts[ $font ]['subsets'] as $subset ) {
395
					$this->subsets[] = $subset;
396
				}
397
			}
398
		}
399
400
		if ( ! empty( $this->subsets ) ) {
401
			$this->subsets = array_unique( $this->subsets );
402
		}
403
404
		$this->link = add_query_arg( array(
405
			'family' => str_replace( '%2B', '+', urlencode( implode( '|', $link_fonts ) ) ),
406
			'subset' => urlencode( implode( ',', $this->subsets ) ),
407
		), 'https://fonts.googleapis.com/css' );
408
409
	}
410
411
	/**
412
	 * Sets the method to use for loading the fonts.
413
	 *
414
	 * @static
415
	 * @access public
416
	 * @since 3.0.0
417
	 * @param string $config_id The config ID. Will be used in a filter later.
418
	 * @param string $method    The method to use.
419
	 */
420
	public static function set_method( $config_id = 'global', $method = 'link' ) {
421
422
		$valid_methods = array(
423
			'link',
424
			'js',
425
			'embed',
426
		);
427
		// Early exit if the defined method is invalid.
428
		if ( ! in_array( $method, $valid_methods ) ) {
429
			$method = 'embed';
430
		}
431
		self::$method[ $config_id ] = $method;
432
	}
433
434
	/**
435
	 * Get the contents of a remote google-fonts link.
436
	 * Responses get cached for 1 day.
437
	 *
438
	 * @access protected
439
	 * @since 3.0.0
440
	 * @param string $url The link we want to get.
441
	 * @return string|false Returns false if there's an error.
442
	 */
443
	protected function get_url_contents( $url = '' ) {
444
445
		// If $url is not set, use $this->link.
446
		$url = ( '' === $url ) ? $this->link : $url;
447
448
		// Sanitize the URL.
449
		$url = esc_url_raw( $url );
450
451
		// The transient name.
452
		$transient_name = 'kirki_googlefonts_contents_' . md5( $url );
453
454
		// Get the transient value.
455
		$html = get_transient( $transient_name );
456
457
		// Check for transient, if none, grab remote HTML file.
458
		if ( false === $html ) {
459
460
			// Get remote HTML file.
461
			$response = wp_remote_get( $url );
0 ignored issues
show
introduced by
wp_remote_get is highly discouraged, please use vip_safe_wp_remote_get() instead.
Loading history...
462
463
			// Check for error.
464
			if ( is_wp_error( $response ) ) {
465
				set_transient( 'kirki_googlefonts_fallback_to_link', 'yes', HOUR_IN_SECONDS );
466
				return false;
467
			}
468
469
			// Parse remote HTML file.
470
			$data = wp_remote_retrieve_body( $response );
471
472
			// Check for error.
473
			if ( is_wp_error( $data ) ) {
474
				set_transient( 'kirki_googlefonts_fallback_to_link', 'yes', HOUR_IN_SECONDS );
475
				return false;
476
			}
477
478
			// If empty, return false.
479
			if ( ! $data ) {
480
				set_transient( 'kirki_googlefonts_fallback_to_link', 'yes', HOUR_IN_SECONDS );
481
				return false;
482
			}
483
484
			// Store remote HTML file in transient, expire after 24 hours.
485
			set_transient( $transient_name, $data, DAY_IN_SECONDS );
486
			set_transient( 'kirki_googlefonts_fallback_to_link', 'no', DAY_IN_SECONDS );
487
		}
488
489
		return $html;
490
491
	}
492
493
	/**
494
	 * Embeds the CSS from googlefonts API inside the Kirki output CSS.
495
	 *
496
	 * @access public
497
	 * @since 3.0.0
498
	 * @param string $css The original CSS.
499
	 * @return string     The modified CSS.
500
	 */
501
	public function embed_css( $css ) {
502
503
		// Go through our fields and populate $this->fonts.
504
		$this->loop_fields();
505
506
		$this->fonts = apply_filters( 'kirki/enqueue_google_fonts', $this->fonts );
507
508
		// Goes through $this->fonts and adds or removes things as needed.
509
		$this->process_fonts();
510
511
		// Go through $this->fonts and populate $this->link.
512
		$this->create_link();
513
514
		// If $this->link is not empty then enqueue it.
515
		if ( '' !== $this->link ) {
516
			return $this->get_url_contents( $this->link ) . "\n" . $css;
517
		}
518
		return $css;
519
	}
520
}
521