Passed
Push — develop ( d4781b...de7504 )
by Aristeides
03:52
created

Kirki_Fonts_Google_Local::init()   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 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
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( $this->files );
0 ignored issues
show
Bug introduced by
$this->files of type array is incompatible with the type string expected by parameter $str of md5(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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