Completed
Push — develop ( ccf59a...c5ee8a )
by Aristeides
09:51
created

Kirki_Fonts_Google::webfont_loader()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 27
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 11
nc 2
nop 0
dl 0
loc 27
rs 8.8571
c 0
b 0
f 0
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) 2017, 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
			$method = apply_filters( "kirki/{$config_id}/googlefonts_load_method", 'link' );
145
			if ( 'embed' === $method && true !== $this->fallback_to_link ) {
146
				$method = 'embed';
147
			}
148
		}
149
		// Force using the JS method while in the customizer.
150
		// This will help us work-out the live-previews for typography fields.
151
		if ( is_customize_preview() ) {
152
			$method = 'async';
153
		}
154
		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...
155
156
			switch ( $method ) {
157
158
				case 'embed':
159
					add_filter( "kirki/{$config_id}/dynamic_css", array( $this, 'embed_css' ) );
160
161
					if ( true === $this->fallback_to_link ) {
162
						// Fallback to enqueue method.
163
						add_action( 'wp_enqueue_scripts', array( $this, 'enqueue' ), 105 );
164
					}
165
					break;
166
				case 'async':
167
					add_action( 'wp_head', array( $this, 'webfont_loader' ) );
168
					break;
169
				case 'link':
170
					// Enqueue link.
171
					add_action( 'wp_enqueue_scripts', array( $this, 'enqueue' ), 105 );
172
					break;
173
			}
174
		}
175
	}
176
177
	/**
178
	 * Get the one, true instance of this class.
179
	 * Prevents performance issues since this is only loaded once.
180
	 *
181
	 * @return object Kirki_Fonts_Google
182
	 */
183
	public static function get_instance() {
184
		if ( null === self::$instance ) {
185
			self::$instance = new Kirki_Fonts_Google();
186
		}
187
		return self::$instance;
188
	}
189
190
	/**
191
	 * Calls all the other necessary methods to populate and create the link.
192
	 */
193
	public function enqueue() {
194
195
		// Go through our fields and populate $this->fonts.
196
		$this->loop_fields();
197
198
		$this->fonts = apply_filters( 'kirki/enqueue_google_fonts', $this->fonts );
199
200
		// Goes through $this->fonts and adds or removes things as needed.
201
		$this->process_fonts();
202
203
		// Go through $this->fonts and populate $this->link.
204
		$this->create_link();
205
206
		// If $this->link is not empty then enqueue it.
207
		if ( '' !== $this->link ) {
208
			wp_enqueue_style( 'kirki_google_fonts', $this->link, array(), null );
209
		}
210
	}
211
212
	/**
213
	 * Goes through all our fields and then populates the $this->fonts property.
214
	 */
215
	private function loop_fields() {
216
		foreach ( Kirki::$fields as $field ) {
217
			$this->generate_google_font( $field );
218
		}
219
	}
220
221
	/**
222
	 * Processes the arguments of a field
223
	 * determines if it's a typography field
224
	 * and if it is, then takes appropriate actions.
225
	 *
226
	 * @param array $args The field arguments.
227
	 */
228
	private function generate_google_font( $args ) {
229
230
		// Process typography fields.
231
		if ( isset( $args['type'] ) && 'kirki-typography' === $args['type'] ) {
232
233
			// Get the value.
234
			$value = Kirki_Values::get_sanitized_field_value( $args );
235
236
			// If we don't have a font-family then we can skip this.
237
			if ( ! isset( $value['font-family'] ) ) {
238
				return;
239
			}
240
241
			// Add support for older formats of the typography control.
242
			// We used to have font-weight instead of variant.
243
			if ( isset( $value['font-weight'] ) && ( ! isset( $value['variant'] ) || empty( $value['variant'] ) ) ) {
244
				$value['variant'] = $value['font-weight'];
245
			}
246
247
			// Set a default value for variants.
248
			if ( ! isset( $value['variant'] ) ) {
249
				$value['variant'] = 'regular';
250
			}
251
			if ( isset( $value['subsets'] ) ) {
252
253
				// Add the subset directly to the array of subsets in the Kirki_GoogleFonts_Manager object.
254
				// Subsets must be applied to ALL fonts if possible.
255
				if ( ! is_array( $value['subsets'] ) ) {
256
					$this->subsets[] = $value['subsets'];
257
				} else {
258
					foreach ( $value['subsets'] as $subset ) {
259
						$this->subsets[] = $subset;
260
					}
261
				}
262
			}
263
264
			// Add the requested google-font.
265
			if ( ! isset( $this->fonts[ $value['font-family'] ] ) ) {
266
				$this->fonts[ $value['font-family'] ] = array();
267
			}
268
			if ( ! in_array( $value['variant'], $this->fonts[ $value['font-family'] ], true ) ) {
269
				$this->fonts[ $value['font-family'] ][] = $value['variant'];
270
			}
271
		} else {
272
273
			// Process non-typography fields.
274
			if ( isset( $args['output'] ) && is_array( $args['output'] ) ) {
275
				foreach ( $args['output'] as $output ) {
276
277
					// If we don't have a typography-related output argument we can skip this.
278
					if ( ! isset( $output['property'] ) || ! in_array( $output['property'], array( 'font-family', 'font-weight', 'font-subset', 'subset', 'subsets' ), true ) ) {
279
						continue;
280
					}
281
282
					// Get the value.
283
					$value = Kirki_Values::get_sanitized_field_value( $args );
284
285
					if ( 'font-family' === $output['property'] ) {
286
						if ( ! array_key_exists( $value, $this->fonts ) ) {
287
							$this->fonts[ $value ] = array();
288
						}
289
					} elseif ( 'font-weight' === $output['property'] ) {
290
						foreach ( $this->fonts as $font => $variants ) {
291
							if ( ! in_array( $value, $variants, true ) ) {
292
								$this->fonts[ $font ][] = $value;
293
							}
294
						}
295
					} elseif ( 'font-subset' === $output['property'] || 'subset' === $output['property'] || 'subsets' === $output['property'] ) {
296
						if ( ! is_array( $value ) ) {
297
							if ( ! in_array( $value, $this->subsets, true ) ) {
298
								$this->subsets[] = $value;
299
							}
300
						} else {
301
							foreach ( $value as $subset ) {
302
								if ( ! in_array( $subset, $this->subsets, true ) ) {
303
									$this->subsets[] = $subset;
304
								}
305
							}
306
						}
307
					}
308
				}
309
			} // End if().
310
		} // End if().
311
	}
312
313
	/**
314
	 * Determines the vbalidity of the selected font as well as its properties.
315
	 * This is vital to make sure that the google-font script that we'll generate later
316
	 * does not contain any invalid options.
317
	 */
318
	private function process_fonts() {
319
320
		// Early exit if font-family is empty.
321
		if ( empty( $this->fonts ) ) {
322
			return;
323
		}
324
325
		$valid_subsets = array();
326
		foreach ( $this->fonts as $font => $variants ) {
327
328
			// Determine if this is indeed a google font or not.
329
			// If it's not, then just remove it from the array.
330
			if ( ! array_key_exists( $font, $this->google_fonts ) ) {
331
				unset( $this->fonts[ $font ] );
332
				continue;
333
			}
334
335
			// Get all valid font variants for this font.
336
			$font_variants = array();
337
			if ( isset( $this->google_fonts[ $font ]['variants'] ) ) {
338
				$font_variants = $this->google_fonts[ $font ]['variants'];
339
			}
340
			foreach ( $variants as $variant ) {
341
342
				// If this is not a valid variant for this font-family
343
				// then unset it and move on to the next one.
344
				if ( ! in_array( $variant, $font_variants, true ) ) {
345
					$variant_key = array_search( $variant, $this->fonts[ $font ] );
346
					unset( $this->fonts[ $font ][ $variant_key ] );
347
					continue;
348
				}
349
			}
350
351
			// Check if the selected subsets exist, even in one of the selected fonts.
352
			// If they don't, then they have to be removed otherwise the link will fail.
353
			if ( isset( $this->google_fonts[ $font ]['subsets'] ) ) {
354
				foreach ( $this->subsets as $subset ) {
355
					if ( in_array( $subset, $this->google_fonts[ $font ]['subsets'], true ) ) {
356
						$valid_subsets[] = $subset;
357
					}
358
				}
359
			}
360
		}
361
		$this->subsets = $valid_subsets;
362
	}
363
364
	/**
365
	 * Creates the google-fonts link.
366
	 */
367
	private function create_link() {
368
369
		// If we don't have any fonts then we can exit.
370
		if ( empty( $this->fonts ) ) {
371
			return;
372
		}
373
374
		// Add a fallback to Roboto.
375
		$font = 'Roboto';
376
377
		// Get font-family + subsets.
378
		$link_fonts = array();
379
		foreach ( $this->fonts as $font => $variants ) {
380
381
			// Are we force-loading all variants?
382
			if ( true === self::$force_load_all_variants ) {
383
				if ( isset( $this->google_fonts[ $font ]['variants'] ) ) {
384
					$variants = $this->google_fonts[ $font ]['variants'];
385
				}
386
			}
387
			$variants = implode( ',', $variants );
388
389
			$link_font = str_replace( ' ', '+', $font );
390
			if ( ! empty( $variants ) ) {
391
				$link_font .= ':' . $variants;
392
			}
393
			$link_fonts[] = $link_font;
394
		}
395
396
		// Are we force-loading all subsets?
397
		if ( true === self::$force_load_all_subsets ) {
398
399
			if ( isset( $this->google_fonts[ $font ]['subsets'] ) ) {
400
				foreach ( $this->google_fonts[ $font ]['subsets'] as $subset ) {
401
					$this->subsets[] = $subset;
402
				}
403
			}
404
		}
405
406
		if ( ! empty( $this->subsets ) ) {
407
			$this->subsets = array_unique( $this->subsets );
408
		}
409
410
		$this->link = add_query_arg( array(
411
			'family' => str_replace( '%2B', '+', urlencode( implode( '|', $link_fonts ) ) ),
412
			'subset' => urlencode( implode( ',', $this->subsets ) ),
413
		), 'https://fonts.googleapis.com/css' );
414
415
	}
416
417
	/**
418
	 * Get the contents of a remote google-fonts link.
419
	 * Responses get cached for 1 day.
420
	 *
421
	 * @access protected
422
	 * @since 3.0.0
423
	 * @param string $url The link we want to get.
424
	 * @return string|false Returns false if there's an error.
425
	 */
426
	protected function get_url_contents( $url = '' ) {
427
428
		// If $url is not set, use $this->link.
429
		$url = ( '' === $url ) ? $this->link : $url;
430
431
		// Sanitize the URL.
432
		$url = esc_url_raw( $url );
433
434
		// The transient name.
435
		$transient_name = 'kirki_googlefonts_contents_' . md5( $url );
436
437
		// Get the transient value.
438
		$html = get_transient( $transient_name );
439
440
		// Check for transient, if none, grab remote HTML file.
441
		if ( false === $html ) {
442
443
			// Get remote HTML file.
444
			$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...
445
446
			// Check for error.
447
			if ( is_wp_error( $response ) ) {
448
				set_transient( 'kirki_googlefonts_fallback_to_link', 'yes', HOUR_IN_SECONDS );
449
				return false;
450
			}
451
452
			// Parse remote HTML file.
453
			$data = wp_remote_retrieve_body( $response );
454
455
			// Check for error.
456
			if ( is_wp_error( $data ) ) {
457
				set_transient( 'kirki_googlefonts_fallback_to_link', 'yes', HOUR_IN_SECONDS );
458
				return false;
459
			}
460
461
			// If empty, return false.
462
			if ( ! $data ) {
463
				set_transient( 'kirki_googlefonts_fallback_to_link', 'yes', HOUR_IN_SECONDS );
464
				return false;
465
			}
466
467
			// Store remote HTML file in transient, expire after 24 hours.
468
			set_transient( $transient_name, $data, DAY_IN_SECONDS );
469
			set_transient( 'kirki_googlefonts_fallback_to_link', 'no', DAY_IN_SECONDS );
470
		}
471
472
		return $html;
473
474
	}
475
476
	/**
477
	 * Embeds the CSS from googlefonts API inside the Kirki output CSS.
478
	 *
479
	 * @access public
480
	 * @since 3.0.0
481
	 * @param string $css The original CSS.
482
	 * @return string     The modified CSS.
483
	 */
484
	public function embed_css( $css ) {
485
486
		// Go through our fields and populate $this->fonts.
487
		$this->loop_fields();
488
489
		$this->fonts = apply_filters( 'kirki/enqueue_google_fonts', $this->fonts );
490
491
		// Goes through $this->fonts and adds or removes things as needed.
492
		$this->process_fonts();
493
494
		// Go through $this->fonts and populate $this->link.
495
		$this->create_link();
496
497
		// If $this->link is not empty then enqueue it.
498
		if ( '' !== $this->link ) {
499
			return $this->get_url_contents( $this->link ) . "\n" . $css;
500
		}
501
		return $css;
502
	}
503
504
	/**
505
	 * Webfont Loader for Google Fonts.
506
	 *
507
	 * @access public
508
	 * @since 3.0.0
509
	 */
510
	public function webfont_loader() {
511
512
		// Go through our fields and populate $this->fonts.
513
		$this->loop_fields();
514
515
		$this->fonts = apply_filters( 'kirki/enqueue_google_fonts', $this->fonts );
516
517
		// Goes through $this->fonts and adds or removes things as needed.
518
		$this->process_fonts();
519
520
		$fonts_to_load = array();
521
		foreach ( $this->fonts as $font => $weights ) {
522
			$fonts_to_load[] = esc_attr( $font ) . ':' . esc_attr( join( ',', $weights ) );
523
		}
524
525
		?>
526
		<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js"></script>
0 ignored issues
show
introduced by
Scripts must be registered/enqueued via wp_enqueue_script
Loading history...
527
		<script id="kirki-webfont-loader">
528
			window.kirkiWebontsLoaderFonts = '<?php echo esc_attr( join( '\', \'', $fonts_to_load ) ); ?>';
529
			WebFont.load({
530
				google: {
531
					families: [ window.kirkiWebontsLoaderFonts ]
532
				}
533
			});
534
		</script>
535
		<?php
536
	}
537
}
538