Completed
Push — develop ( fff6df...34fdaa )
by Aristeides
241:46 queued 207:20
created

Kirki_Fonts_Google::get_url_contents()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 49
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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