Completed
Push — develop ( 1cdc08...e2f7b0 )
by Aristeides
05:56 queued 03:25
created

Kirki_Fonts_Google::get_instance()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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