Completed
Push — develop ( 0d1318...19da19 )
by Aristeides
03:58
created

get_variant_fontface_css()   F

Complexity

Conditions 9
Paths 256

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 9
eloc 20
c 1
b 0
f 1
nc 256
nop 1
dl 0
loc 34
rs 3
1
<?php
2
/**
3
 * Handles downloading a font from the google-fonts API locally.
4
 * Solves privacy concerns with Google's CDN
5
 * and their sometimes less-than-transparent policies.
6
 *
7
 * @package     Kirki
8
 * @category    Core
9
 * @author      Aristeides Stathopoulos
10
 * @copyright   Copyright (c) 2017, Aristeides Stathopoulos
11
 * @license     http://opensource.org/licenses/https://opensource.org/licenses/MIT
12
 * @since       3.0.28
13
 */
14
15
16
// Do not allow directly accessing this file.
17
if ( ! defined( 'ABSPATH' ) ) {
18
	exit( 'Direct script access denied.' );
19
}
20
21
/**
22
 * The Kirki_Fonts object.
23
 *
24
 * @since 3.0.28
25
 */
26
final class Kirki_Fonts_Google_Local {
27
28
	/**
29
	 * The name of the font-family
30
	 *
31
	 * @access private
32
	 * @since 3.0.28
33
	 * @var string
34
	 */
35
	private $family;
36
37
	/**
38
	 * The system path where font-files are stored.
39
	 *
40
	 * @access private
41
	 * @since 3.0.28
42
	 * @var string
43
	 */
44
	private $folder_path;
45
46
	/**
47
	 * The URL where files for this font can be found.
48
	 *
49
	 * @access private
50
	 * @since 3.0.28
51
	 * @var string
52
	 */
53
	private $folder_url;
54
55
	/**
56
	 * The font-family array from the google-fonts API.
57
	 *
58
	 * @access private
59
	 * @since 3.0.28
60
	 * @var array
61
	 */
62
	private $font;
0 ignored issues
show
introduced by
The private property $font is not used, and could be removed.
Loading history...
63
64
	/**
65
	 * An array of instances for this object.
66
	 *
67
	 * @static
68
	 * @access private
69
	 * @since 3.0.28
70
	 * @var array
71
	 */
72
	private static $instances = array();
73
74
	/**
75
	 * Create an instance of this object for a specific font-family.
76
	 *
77
	 * @static
78
	 * @access public
79
	 * @since 3.0.28
80
	 * @param string $family The font-family name.
81
	 * @return Kirki_Fonts_Google_Local
82
	 */
83
	public static function init( $family ) {
84
		$key = sanitize_key( $family );
85
		if ( ! isset( self::$instances[ $key ] ) ) {
86
			self::$instances[ $key ] = new self( $family );
87
		}
88
		return self::$instances[ $key ];
89
	}
90
91
	/**
92
	 * Constructor.
93
	 *
94
	 * @access private
95
	 * @since 3.0.28
96
	 * @param string $family The font-family name.
97
	 */
98
	private function __construct( $family ) {
99
		$this->family      = $family;
100
		$key               = sanitize_key( $this->family );
101
		$this->folder_path = $this->get_root_path() . "/$key";
102
		$this->folder_url  = $this->get_root_url() . "/$key";
103
		$this->files        = $this->get_font_family();
0 ignored issues
show
Bug Best Practice introduced by
The property files does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
104
	}
105
106
	/**
107
	 * Gets the @font-face CSS.
108
	 *
109
	 * @access public
110
	 * @since 3.0.28
111
	 * @param array $variants The variants we want to get.
112
	 * @return string
113
	 */
114
	public function get_css( $variants = array() ) {
115
		if ( ! $this->files ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->files of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
116
			return;
117
		}
118
		$key = md5( json_encode( $this->files ) );
119
		$cached = get_transient( $key );
120
		if ( $cached ) {
121
			return $cached;
122
		}
123
		$css = '';
124
125
		// If $variants is empty then use all variants available.
126
		if ( empty( $variants ) ) {
127
			$variants = array_keys( $this->files );
128
		}
129
130
		// Download files.
131
		$this->download_font_family( $variants );
132
133
		// Create the @font-face CSS.
134
		foreach ( $variants as $variant ) {
135
			$css .= $this->get_variant_fontface_css( $variant );
136
		}
137
		set_transient( $key, $css, DAY_IN_SECONDS );
0 ignored issues
show
Bug introduced by
The constant DAY_IN_SECONDS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
138
		return $css;
139
	}
140
141
	/**
142
	 * Get the @font-face CSS for a specific variant.
143
	 *
144
	 * @access public
145
	 * @since 3.0.28
146
	 * @param string $variant The variant.
147
	 * @return string
148
	 */
149
	public function get_variant_fontface_css( $variant ) {
150
		$font_face = "@font-face{font-family:'{$this->family}';";
151
152
		// Get the font-style.
153
		$font_style = ( false !== strpos( $variant, 'italic' ) ) ? 'italic' : 'normal';
154
		$font_face .= "font-style:{$font_style};";
155
156
		// Get the font-weight.
157
		$font_weight = '400';
0 ignored issues
show
Unused Code introduced by
The assignment to $font_weight is dead and can be removed.
Loading history...
158
		$font_weight = str_replace( 'italic', '', $variant );
159
		$font_weight = ( ! $font_weight || 'regular' === $font_weight ) ? '400' : $font_weight;
160
		$font_face  .= "font-weight:{$font_weight};";
161
162
		// Get the font-names.
163
		$font_name_0 = $this->get_local_font_name( $variant, false );
164
		$font_name_1 = $this->get_local_font_name( $variant, true );
165
		$font_face  .= "src:local('{$font_name_0}'),";
166
		if ( $font_name_0 !== $font_name_1 ) {
167
			$font_face .= "local('{$font_name_1}'),";
168
		}
169
170
		// Get the font-url.
171
		$font_url = $this->get_variant_local_url( $variant );
172
		$paths    = $this->get_font_files_paths();
173
		if ( ! file_exists( $paths[ $variant ] ) ) {
174
			$font_url = $this->files[ $variant ];
175
		}
176
177
		// Get the font-format.
178
		$font_format = ( strpos( $font_url, '.woff2' ) ) ? 'woff2' : 'truetype';
179
		$font_format = ( strpos( $font_url, '.woff' ) && ! strpos( $font_url, '.woff2' ) ) ? 'woff' : $font_format;
180
		$font_face  .= "url({$font_url}) format('{$font_format}');}";
181
182
		return $font_face;
183
	}
184
185
	/**
186
	 * Gets the local URL for a variant.
187
	 *
188
	 * @access public
189
	 * @since 3.0.28
190
	 * @param string $variant The variant.
191
	 * @return string         The URL.
192
	 */
193
	public function get_variant_local_url( $variant ) {
194
		$local_urls = $this->get_font_files_urls_local();
195
196
		if ( empty( $local_urls ) ) {
197
			return;
198
		}
199
200
		// Return the specific variant if we can find it.
201
		if ( isset( $local_urls[ $variant ] ) ) {
202
			return $local_urls[ $variant ];
203
		}
204
205
		// Return regular if the one we want could not be found.
206
		if ( isset( $local_urls['regular'] ) ) {
207
			return $local_urls['regular'];
208
		}
209
210
		// Return the first available if all else failed.
211
		$vals = array_values( $local_urls );
212
		return $vals[0];
213
	}
214
215
	/**
216
	 * Get the name of the font-family.
217
	 * This is used by @font-face in case the user already has the font downloaded locally.
218
	 *
219
	 * @access public
220
	 * @since 3.0.28
221
	 * @param string $variant The variant.
222
	 * @param bool   $compact Whether we want the compact formatting or not.
223
	 * @return string
224
	 */
225
	public function get_local_font_name( $variant, $compact = false ) {
226
		$variant_names = array(
227
			'100'       => 'Thin',
228
			'100i'      => 'Thin Italic',
229
			'100italic' => 'Thin Italic',
230
			'200'       => 'Extra-Light',
231
			'200i'      => 'Extra-Light Italic',
232
			'200italic' => 'Extra-Light Italic',
233
			'300'       => 'Light',
234
			'300i'      => 'Light Italic',
235
			'300italic' => 'Light Italic',
236
			'400'       => 'Regular',
237
			'regular'   => 'Regular',
238
			'400i'      => 'Regular Italic',
239
			'italic'    => 'Italic',
240
			'400italic' => 'Regular Italic',
241
			'500'       => 'Medium',
242
			'500i'      => 'Medium Italic',
243
			'500italic' => 'Medium Italic',
244
			'600'       => 'Semi-Bold',
245
			'600i'      => 'Semi-Bold Italic',
246
			'600italic' => 'Semi-Bold Italic',
247
			'700'       => 'Bold',
248
			'700i'      => 'Bold Italic',
249
			'700italic' => 'Bold Italic',
250
			'800'       => 'Extra-Bold',
251
			'800i'      => 'Extra-Bold Italic',
252
			'800italic' => 'Extra-Bold Italic',
253
			'900'       => 'Black',
254
			'900i'      => 'Black Italic',
255
			'900italic' => 'Black Italic',
256
		);
257
258
		$variant = (string) $variant;
259
		if ( $compact ) {
260
			if ( isset( $variant_names[ $variant ] ) ) {
261
				return str_replace( array( ' ', '-' ), '', $this->family ) . '-' . str_replace( array( ' ', '-' ), '', $variant_names[ $variant ] );
262
			}
263
			return str_replace( array( ' ', '-' ), '', $this->family );
264
		}
265
266
		if ( isset( $variant_names[ $variant ] ) ) {
267
			return $this->family . ' ' . $variant_names[ $variant ];
268
		}
269
		return $this->family;
270
	}
271
272
	/**
273
	 * Get an array of font-files.
274
	 * Only contains the filenames.
275
	 *
276
	 * @access public
277
	 * @since 3.0.28
278
	 * @return array
279
	 */
280
	public function get_font_files() {
281
		$files = array();
282
		foreach ( $this->files as $key => $url ) {
283
			$files[ $key ] = $this->get_filename_from_url( $url );
284
		}
285
		return $files;
286
	}
287
288
	/**
289
	 * Get an array of local file URLs.
290
	 *
291
	 * @access public
292
	 * @since 3.0.28
293
	 * @return array
294
	 */
295
	public function get_font_files_urls_local() {
296
		$urls  = array();
297
		$files = $this->get_font_files();
298
		foreach ( $files as $key => $file ) {
299
			$urls[ $key ] = $this->folder_url . '/' . $file;
300
		}
301
		return $urls;
302
	}
303
304
	/**
305
	 * Get an array of local file paths.
306
	 *
307
	 * @access public
308
	 * @since 3.0.28
309
	 * @return array
310
	 */
311
	public function get_font_files_paths() {
312
		$paths = array();
313
		$files = $this->get_font_files();
314
		foreach ( $files as $key => $file ) {
315
			$paths[ $key ] = $this->folder_path . '/' . $file;
316
		}
317
		return $paths;
318
	}
319
320
	/**
321
	 * Downloads a font-file and saves it locally.
322
	 *
323
	 * @access private
324
	 * @since 3.0.28
325
	 * @param string $url The URL of the file we want to get.
326
	 * @return bool
327
	 */
328
	private function download_font_file( $url ) {
329
		$contents = $this->get_remote_url_contents( $url );
330
		$path     = $this->folder_path . '/' . $this->get_filename_from_url( $url );
331
332
		// If the folder doesn't exist, create it.
333
		if ( ! file_exists( $this->folder_path ) ) {
334
			Kirki_Helper::init_filesystem()->mkdir( $this->folder_path, FS_CHMOD_FILE );
0 ignored issues
show
Bug introduced by
The constant FS_CHMOD_FILE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
335
		}
336
		// If the file exists no reason to do anything.
337
		if ( file_exists( $path ) ) {
338
			return true;
339
		}
340
341
		// Write file.
342
		return Kirki_Helper::init_filesystem()->put_contents( $path, $contents, FS_CHMOD_FILE );
343
	}
344
345
	/**
346
	 * Get a font-family from the array of google-fonts.
347
	 *
348
	 * @access public
349
	 * @since 3.0.28
350
	 * @return array
351
	 */
352
	public function get_font_family() {
353
354
		// Get the fonts array.
355
		$fonts = $this->get_fonts();
356
		if ( isset( $fonts[ $this->family ] ) ) {
357
			return $fonts[ $this->family ];
358
		}
359
		return array();
360
	}
361
362
	/**
363
	 * Gets the filename by breaking-down the URL parts.
364
	 *
365
	 * @access private
366
	 * @since 3.0.28
367
	 * @param string $url The URL.
368
	 * @return string     The filename.
369
	 */
370
	private function get_filename_from_url( $url ) {
371
		$url_parts   = explode( '/', $url );
372
		$parts_count = count( $url_parts );
373
		if ( 1 < $parts_count ) {
374
			return $url_parts[ count( $url_parts ) - 1 ];
375
		}
376
		return $url;
377
	}
378
379
	/**
380
	 * Get the font defined in the google-fonts API.
381
	 *
382
	 * @access private
383
	 * @since 3.0.28
384
	 * @return array
385
	 */
386
	private function get_fonts() {
387
		ob_start();
388
		include wp_normalize_path( dirname( __FILE__ ) . '/webfont-files.json' );
389
		$json = ob_get_clean();
390
		return json_decode( $json, true );
391
	}
392
393
	/**
394
	 * Gets the root fonts folder path.
395
	 * Other paths are built based on this.
396
	 *
397
	 * @since 1.5
398
	 * @access public
399
	 * @return string
400
	 */
401
	public function get_root_path() {
402
		// Get the upload directory for this site.
403
		$upload_dir = wp_upload_dir();
404
		$path       = untrailingslashit( wp_normalize_path( $upload_dir['basedir'] ) ) . '/webfonts';
405
406
		// If the folder doesn't exist, create it.
407
		if ( ! file_exists( $path ) ) {
408
			Kirki_Helper::init_filesystem()->mkdir( $path, FS_CHMOD_FILE );
0 ignored issues
show
Bug introduced by
The constant FS_CHMOD_FILE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
409
		}
410
411
		// Return the path.
412
		return apply_filters( 'kirki_googlefonts_root_path', $path );
413
	}
414
415
	/**
416
	 * Gets the root folder url.
417
	 * Other urls are built based on this.
418
	 *
419
	 * @since 1.5
420
	 * @access public
421
	 * @return string
422
	 */
423
	public function get_root_url() {
424
425
		// Get the upload directory for this site.
426
		$upload_dir = wp_upload_dir();
427
428
		// The URL.
429
		$url = trailingslashit( $upload_dir['baseurl'] );
430
		// Take care of domain mapping.
431
		// When using domain mapping we have to make sure that the URL to the file
432
		// does not include the original domain but instead the mapped domain.
433
		if ( defined( 'DOMAIN_MAPPING' ) && DOMAIN_MAPPING ) {
0 ignored issues
show
Bug introduced by
The constant DOMAIN_MAPPING was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
434
			if ( function_exists( 'domain_mapping_siteurl' ) && function_exists( 'get_original_url' ) ) {
435
				$mapped_domain   = domain_mapping_siteurl( false );
436
				$original_domain = get_original_url( 'siteurl' );
437
				$url = str_replace( $original_domain, $mapped_domain, $url );
438
			}
439
		}
440
		return apply_filters( 'kirki_googlefonts_root_url', untrailingslashit( esc_url_raw( $url ) ) . '/webfonts' );
441
	}
442
443
	/**
444
	 * Download font-family files.
445
	 *
446
	 * @access public
447
	 * @since 3.0.28
448
	 * @param array $variants An array of variants to download. Leave empty to download all.
449
	 * @return void
450
	 */
451
	public function download_font_family( $variants = array() ) {
452
		if ( empty( $variants ) ) {
453
			$variants = array_keys( $this->files );
454
		}
455
		foreach ( $this->files as $variant => $file ) {
456
			if ( in_array( $variant, $variants ) ) {
457
				$this->download_font_file( $file );
458
			}
459
		}
460
	}
461
462
	/**
463
	 * Gets the remote URL contents.
464
	 *
465
	 * @access private
466
	 * @since 3.0.28
467
	 * @param string $url The URL we want to get.
468
	 * @return string     The contents of the remote URL.
469
	 */
470
	public function get_remote_url_contents( $url ) {
471
		$response = wp_remote_get( $url );
472
		if ( is_wp_error( $response ) ) {
473
			return array();
474
		}
475
		$html = wp_remote_retrieve_body( $response );
476
		if ( is_wp_error( $html ) ) {
477
			return;
478
		}
479
		return $html;
480
	}
481
}
482