Issues (377)

webfonts/class-kirki-fonts-google-local.php (1 issue)

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    https://opensource.org/licenses/MIT
12
 * @since       3.0.28
13
 */
14
15
// Do not allow directly accessing this file.
16
if ( ! defined( 'ABSPATH' ) ) {
17
	exit( 'Direct script access denied.' );
18
}
19
20
/**
21
 * The Kirki_Fonts object.
22
 *
23
 * @since 3.0.28
24
 */
25
final class Kirki_Fonts_Google_Local {
26
27
	/**
28
	 * The name of the font-family
29
	 *
30
	 * @access private
31
	 * @since 3.0.28
32
	 * @var string
33
	 */
34
	private $family;
35
36
	/**
37
	 * The system path where font-files are stored.
38
	 *
39
	 * @access private
40
	 * @since 3.0.28
41
	 * @var string
42
	 */
43
	private $folder_path;
44
45
	/**
46
	 * The URL where files for this font can be found.
47
	 *
48
	 * @access private
49
	 * @since 3.0.28
50
	 * @var string
51
	 */
52
	private $folder_url;
53
54
	/**
55
	 * The font-family array from the google-fonts API.
56
	 *
57
	 * @access private
58
	 * @since 3.0.28
59
	 * @var array
60
	 */
61
	private $font;
62
63
	/**
64
	 * An array of instances for this object.
65
	 *
66
	 * @static
67
	 * @access private
68
	 * @since 3.0.28
69
	 * @var array
70
	 */
71
	private static $instances = array();
72
73
	/**
74
	 * Create an instance of this object for a specific font-family.
75
	 *
76
	 * @static
77
	 * @access public
78
	 * @since 3.0.28
79
	 * @param string $family The font-family name.
80
	 * @return Kirki_Fonts_Google_Local
81
	 */
82
	public static function init( $family ) {
83
		$key = sanitize_key( $family );
84
		if ( ! isset( self::$instances[ $key ] ) ) {
85
			self::$instances[ $key ] = new self( $family );
86
		}
87
		return self::$instances[ $key ];
88
	}
89
90
	/**
91
	 * Constructor.
92
	 *
93
	 * @access private
94
	 * @since 3.0.28
95
	 * @param string $family The font-family name.
96
	 */
97
	private function __construct( $family ) {
98
		$this->family      = $family;
99
		$key               = sanitize_key( $this->family );
100
		$this->folder_path = $this->get_root_path() . "/$key";
101
		$this->folder_url  = $this->get_root_url() . "/$key";
102
		$this->files       = $this->get_font_family();
103
	}
104
105
	/**
106
	 * Gets the @font-face CSS.
107
	 *
108
	 * @access public
109
	 * @since 3.0.28
110
	 * @param array $variants The variants we want to get.
111
	 * @return string
112
	 */
113
	public function get_css( $variants = array() ) {
114
		if ( ! $this->files ) {
115
			return;
116
		}
117
		$key    = md5( wp_json_encode( $this->files ) );
118
		$cached = get_transient( $key );
119
		if ( $cached ) {
120
			return $cached;
121
		}
122
		$css = '';
123
124
		// If $variants is empty then use all variants available.
125
		if ( empty( $variants ) ) {
126
			$variants = array_keys( $this->files );
127
		}
128
129
		// Download files.
130
		$this->download_font_family( $variants );
131
132
		// Create the @font-face CSS.
133
		foreach ( $variants as $variant ) {
134
			$css .= $this->get_variant_fontface_css( $variant );
135
		}
136
		set_transient( $key, $css, DAY_IN_SECONDS );
137
		return $css;
138
	}
139
140
	/**
141
	 * Get the @font-face CSS for a specific variant.
142
	 *
143
	 * @access public
144
	 * @since 3.0.28
145
	 * @param string $variant The variant.
146
	 * @return string
147
	 */
148
	public function get_variant_fontface_css( $variant ) {
149
		$font_face = "@font-face{font-family:'{$this->family}';";
150
151
		// Get the font-style.
152
		$font_style = ( false !== strpos( $variant, 'italic' ) ) ? 'italic' : 'normal';
153
		$font_face .= "font-style:{$font_style};";
154
155
		// Get the font-weight.
156
		$font_weight = '400';
0 ignored issues
show
The assignment to $font_weight is dead and can be removed.
Loading history...
157
		$font_weight = str_replace( 'italic', '', $variant );
158
		$font_weight = ( ! $font_weight || 'regular' === $font_weight ) ? '400' : $font_weight;
159
		$font_face  .= "font-weight:{$font_weight};";
160
161
		// Get the font-names.
162
		$font_name_0 = $this->get_local_font_name( $variant, false );
163
		$font_name_1 = $this->get_local_font_name( $variant, true );
164
		$font_face  .= "src:local('{$font_name_0}'),";
165
		if ( $font_name_0 !== $font_name_1 ) {
166
			$font_face .= "local('{$font_name_1}'),";
167
		}
168
169
		// Get the font-url.
170
		$font_url = $this->get_variant_local_url( $variant );
171
		$paths    = $this->get_font_files_paths();
172
		if ( ! file_exists( $paths[ $variant ] ) ) {
173
			$font_url = $this->files[ $variant ];
174
		}
175
176
		// Get the font-format.
177
		$font_format = ( strpos( $font_url, '.woff2' ) ) ? 'woff2' : 'truetype';
178
		$font_format = ( strpos( $font_url, '.woff' ) && ! strpos( $font_url, '.woff2' ) ) ? 'woff' : $font_format;
179
		$font_face  .= "url({$font_url}) format('{$font_format}');}";
180
181
		return $font_face;
182
	}
183
184
	/**
185
	 * Gets the local URL for a variant.
186
	 *
187
	 * @access public
188
	 * @since 3.0.28
189
	 * @param string $variant The variant.
190
	 * @return string         The URL.
191
	 */
192
	public function get_variant_local_url( $variant ) {
193
		$local_urls = $this->get_font_files_urls_local();
194
195
		if ( empty( $local_urls ) ) {
196
			return;
197
		}
198
199
		// Return the specific variant if we can find it.
200
		if ( isset( $local_urls[ $variant ] ) ) {
201
			return $local_urls[ $variant ];
202
		}
203
204
		// Return regular if the one we want could not be found.
205
		if ( isset( $local_urls['regular'] ) ) {
206
			return $local_urls['regular'];
207
		}
208
209
		// Return the first available if all else failed.
210
		$vals = array_values( $local_urls );
211
		return $vals[0];
212
	}
213
214
	/**
215
	 * Get the name of the font-family.
216
	 * This is used by @font-face in case the user already has the font downloaded locally.
217
	 *
218
	 * @access public
219
	 * @since 3.0.28
220
	 * @param string $variant The variant.
221
	 * @param bool   $compact Whether we want the compact formatting or not.
222
	 * @return string
223
	 */
224
	public function get_local_font_name( $variant, $compact = false ) {
225
		$variant_names = array(
226
			'100'       => 'Thin',
227
			'100i'      => 'Thin Italic',
228
			'100italic' => 'Thin Italic',
229
			'200'       => 'Extra-Light',
230
			'200i'      => 'Extra-Light Italic',
231
			'200italic' => 'Extra-Light Italic',
232
			'300'       => 'Light',
233
			'300i'      => 'Light Italic',
234
			'300italic' => 'Light Italic',
235
			'400'       => 'Regular',
236
			'regular'   => 'Regular',
237
			'400i'      => 'Regular Italic',
238
			'italic'    => 'Italic',
239
			'400italic' => 'Regular Italic',
240
			'500'       => 'Medium',
241
			'500i'      => 'Medium Italic',
242
			'500italic' => 'Medium Italic',
243
			'600'       => 'Semi-Bold',
244
			'600i'      => 'Semi-Bold Italic',
245
			'600italic' => 'Semi-Bold Italic',
246
			'700'       => 'Bold',
247
			'700i'      => 'Bold Italic',
248
			'700italic' => 'Bold Italic',
249
			'800'       => 'Extra-Bold',
250
			'800i'      => 'Extra-Bold Italic',
251
			'800italic' => 'Extra-Bold Italic',
252
			'900'       => 'Black',
253
			'900i'      => 'Black Italic',
254
			'900italic' => 'Black Italic',
255
		);
256
257
		$variant = (string) $variant;
258
		if ( $compact ) {
259
			if ( isset( $variant_names[ $variant ] ) ) {
260
				return str_replace( array( ' ', '-' ), '', $this->family ) . '-' . str_replace( array( ' ', '-' ), '', $variant_names[ $variant ] );
261
			}
262
			return str_replace( array( ' ', '-' ), '', $this->family );
263
		}
264
265
		if ( isset( $variant_names[ $variant ] ) ) {
266
			return $this->family . ' ' . $variant_names[ $variant ];
267
		}
268
		return $this->family;
269
	}
270
271
	/**
272
	 * Get an array of font-files.
273
	 * Only contains the filenames.
274
	 *
275
	 * @access public
276
	 * @since 3.0.28
277
	 * @return array
278
	 */
279
	public function get_font_files() {
280
		$files = array();
281
		foreach ( $this->files as $key => $url ) {
282
			$files[ $key ] = $this->get_filename_from_url( $url );
283
		}
284
		return $files;
285
	}
286
287
	/**
288
	 * Get an array of local file URLs.
289
	 *
290
	 * @access public
291
	 * @since 3.0.28
292
	 * @return array
293
	 */
294
	public function get_font_files_urls_local() {
295
		$urls  = array();
296
		$files = $this->get_font_files();
297
		foreach ( $files as $key => $file ) {
298
			$urls[ $key ] = $this->folder_url . '/' . $file;
299
		}
300
		return $urls;
301
	}
302
303
	/**
304
	 * Get an array of local file paths.
305
	 *
306
	 * @access public
307
	 * @since 3.0.28
308
	 * @return array
309
	 */
310
	public function get_font_files_paths() {
311
		$paths = array();
312
		$files = $this->get_font_files();
313
		foreach ( $files as $key => $file ) {
314
			$paths[ $key ] = $this->folder_path . '/' . $file;
315
		}
316
		return $paths;
317
	}
318
319
	/**
320
	 * Downloads a font-file and saves it locally.
321
	 *
322
	 * @access private
323
	 * @since 3.0.28
324
	 * @param string $url The URL of the file we want to get.
325
	 * @return bool
326
	 */
327
	private function download_font_file( $url ) {
328
		$contents = $this->get_remote_url_contents( $url );
329
		$path     = $this->folder_path . '/' . $this->get_filename_from_url( $url );
330
331
		// If the folder doesn't exist, create it.
332
		if ( ! file_exists( $this->folder_path ) ) {
333
			wp_mkdir_p( $this->folder_path );
334
		}
335
		// If the file exists no reason to do anything.
336
		if ( file_exists( $path ) ) {
337
			return true;
338
		}
339
340
		// Write file.
341
		return Kirki_Helper::init_filesystem()->put_contents( $path, $contents, FS_CHMOD_FILE );
342
	}
343
344
	/**
345
	 * Get a font-family from the array of google-fonts.
346
	 *
347
	 * @access public
348
	 * @since 3.0.28
349
	 * @return array
350
	 */
351
	public function get_font_family() {
352
353
		// Get the fonts array.
354
		$fonts = $this->get_fonts();
355
		if ( isset( $fonts[ $this->family ] ) ) {
356
			return $fonts[ $this->family ];
357
		}
358
		return array();
359
	}
360
361
	/**
362
	 * Gets the filename by breaking-down the URL parts.
363
	 *
364
	 * @access private
365
	 * @since 3.0.28
366
	 * @param string $url The URL.
367
	 * @return string     The filename.
368
	 */
369
	private function get_filename_from_url( $url ) {
370
		$url_parts   = explode( '/', $url );
371
		$parts_count = count( $url_parts );
372
		if ( 1 < $parts_count ) {
373
			return $url_parts[ count( $url_parts ) - 1 ];
374
		}
375
		return $url;
376
	}
377
378
	/**
379
	 * Get the font defined in the google-fonts API.
380
	 *
381
	 * @access private
382
	 * @since 3.0.28
383
	 * @return array
384
	 */
385
	private function get_fonts() {
386
		ob_start();
387
		include wp_normalize_path( dirname( __FILE__ ) . '/webfont-files.json' );
388
		$json = ob_get_clean();
389
		return json_decode( $json, true );
390
	}
391
392
	/**
393
	 * Gets the root fonts folder path.
394
	 * Other paths are built based on this.
395
	 *
396
	 * @since 1.5
397
	 * @access public
398
	 * @return string
399
	 */
400
	public function get_root_path() {
401
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
			wp_mkdir_p( $path );
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
431
		// Take care of domain mapping.
432
		// When using domain mapping we have to make sure that the URL to the file
433
		// does not include the original domain but instead the mapped domain.
434
		if ( defined( 'DOMAIN_MAPPING' ) && DOMAIN_MAPPING ) {
435
			if ( function_exists( 'domain_mapping_siteurl' ) && function_exists( 'get_original_url' ) ) {
436
				$mapped_domain   = domain_mapping_siteurl( false );
437
				$original_domain = get_original_url( 'siteurl' );
438
				$url             = str_replace( $original_domain, $mapped_domain, $url );
439
			}
440
		}
441
		$url = str_replace( array( 'https://', 'http://' ), '//', $url );
442
		return apply_filters( 'kirki_googlefonts_root_url', untrailingslashit( esc_url_raw( $url ) ) . '/webfonts' );
443
	}
444
445
	/**
446
	 * Download font-family files.
447
	 *
448
	 * @access public
449
	 * @since 3.0.28
450
	 * @param array $variants An array of variants to download. Leave empty to download all.
451
	 * @return void
452
	 */
453
	public function download_font_family( $variants = array() ) {
454
		if ( empty( $variants ) ) {
455
			$variants = array_keys( $this->files );
456
		}
457
		foreach ( $this->files as $variant => $file ) {
458
			if ( in_array( $variant, $variants ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
459
				$this->download_font_file( $file );
460
			}
461
		}
462
	}
463
464
	/**
465
	 * Gets the remote URL contents.
466
	 *
467
	 * @access private
468
	 * @since 3.0.28
469
	 * @param string $url The URL we want to get.
470
	 * @return string     The contents of the remote URL.
471
	 */
472
	public function get_remote_url_contents( $url ) {
473
		$response = wp_remote_get( $url );
474
		if ( is_wp_error( $response ) ) {
475
			return array();
476
		}
477
		$html = wp_remote_retrieve_body( $response );
478
		if ( is_wp_error( $html ) ) {
479
			return;
480
		}
481
		return $html;
482
	}
483
}
484