PluginProbe
Gutenify – Visual Site Builder Blocks & Starter Templates / trunk
Gutenify – Visual Site Builder Blocks & Starter Templates vtrunk
1.7.0 1.6.6 1.6.5 1.6.4 trunk 0.0.1 0.0.2 0.0.3 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 All 72 releases
gutenify / core / inc / lib / wptt-webfont-loader.php

wptt-webfont-loader.php in Gutenify – Visual Site Builder Blocks & Starter Templates trunk, at core/inc/lib/wptt-webfont-loader.php

731 lines 17.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Download webfonts locally.
4 *
5 * @package wptt/font-loader
6 * @link https://github.com/WPTT/webfont-loader
7 * @license https://opensource.org/licenses/MIT
8 */
9
10 if ( ! class_exists( 'WPTT_WebFont_Loader' ) ) {
11 /**
12 * Download webfonts locally.
13 */
14 class WPTT_WebFont_Loader {
15
16 /**
17 * The font-format.
18 *
19 * Use "woff" or "woff2".
20 * This will change the user-agent user to make the request.
21 *
22 * @access protected
23 * @since 1.0.0
24 * @var string
25 */
26 protected $font_format = 'woff2';
27
28 /**
29 * The remote URL.
30 *
31 * @access protected
32 * @since 1.1.0
33 * @var string
34 */
35 protected $remote_url;
36
37 /**
38 * Base path.
39 *
40 * @access protected
41 * @since 1.1.0
42 * @var string
43 */
44 protected $base_path;
45
46 /**
47 * Base URL.
48 *
49 * @access protected
50 * @since 1.1.0
51 * @var string
52 */
53 protected $base_url;
54
55 /**
56 * Subfolder name.
57 *
58 * @access protected
59 * @since 1.1.0
60 * @var string
61 */
62 protected $subfolder_name;
63
64 /**
65 * The fonts folder.
66 *
67 * @access protected
68 * @since 1.1.0
69 * @var string
70 */
71 protected $fonts_folder;
72
73 /**
74 * The local stylesheet's path.
75 *
76 * @access protected
77 * @since 1.1.0
78 * @var string
79 */
80 protected $local_stylesheet_path;
81
82 /**
83 * The local stylesheet's URL.
84 *
85 * @access protected
86 * @since 1.1.0
87 * @var string
88 */
89 protected $local_stylesheet_url;
90
91 /**
92 * The remote CSS.
93 *
94 * @access protected
95 * @since 1.1.0
96 * @var string
97 */
98 protected $remote_styles;
99
100 /**
101 * The final CSS.
102 *
103 * @access protected
104 * @since 1.1.0
105 * @var null|string
106 */
107 protected $css = null;
108
109 /**
110 * Cleanup routine frequency.
111 */
112 const CLEANUP_FREQUENCY = 'monthly';
113
114 /**
115 * Constructor.
116 *
117 * Get a new instance of the object for a new URL.
118 *
119 * @access public
120 * @since 1.1.0
121 * @param string $url The remote URL.
122 */
123 public function __construct( $url = '' ) {
124 $this->remote_url = $url;
125
126 // Add a cleanup routine.
127 $this->schedule_cleanup();
128 add_action( 'delete_fonts_folder', array( $this, 'delete_fonts_folder' ) );
129 }
130
131 /**
132 * Get the local URL which contains the styles.
133 *
134 * Fallback to the remote URL if we were unable to write the file locally.
135 *
136 * @access public
137 * @since 1.1.0
138 * @return string
139 */
140 public function get_url() {
141
142 // If remote URL is empty just return itself.
143 if ( empty( $this->remote_url ) ) {
144 return $this->remote_url;
145 }
146
147 // Check if the local stylesheet exists.
148 if ( $this->local_file_exists() ) {
149
150 // Attempt to update the stylesheet. Return the local URL on success.
151 if ( $this->write_stylesheet() ) {
152 return $this->get_local_stylesheet_url();
153 }
154 }
155
156 // If the local file exists, return its URL, with a fallback to the remote URL.
157 return file_exists( $this->get_local_stylesheet_path() )
158 ? $this->get_local_stylesheet_url()
159 : $this->remote_url;
160 }
161
162 /**
163 * Get the local stylesheet URL.
164 *
165 * @access public
166 * @since 1.1.0
167 * @return string
168 */
169 public function get_local_stylesheet_url() {
170 if ( ! $this->local_stylesheet_url ) {
171 $this->local_stylesheet_url = str_replace(
172 $this->get_base_path(),
173 $this->get_base_url(),
174 $this->get_local_stylesheet_path()
175 );
176 }
177 return $this->local_stylesheet_url;
178 }
179
180 /**
181 * Get styles with fonts downloaded locally.
182 *
183 * @access public
184 * @since 1.0.0
185 * @return string
186 */
187 public function get_styles() {
188
189 // If remote URL is empty, set empty string.
190 if ( empty( $this->remote_url ) ) {
191 $this->css = '';
192 }
193
194 // If the CSS is set already, return it.
195 if ( is_string( $this->css ) ) {
196 return $this->css;
197 }
198
199 // If we already have the local file, return its contents.
200 $this->css = $this->get_local_stylesheet_contents();
201 if ( ! empty( $this->css ) ) {
202 return $this->css;
203 }
204
205 // Get the remote URL contents.
206 $this->remote_styles = $this->get_remote_url_contents();
207
208 // Get an array of locally-hosted files.
209 $files = $this->get_local_files_from_css();
210
211 // Convert paths to URLs.
212 foreach ( $files as $remote => $local ) {
213 $files[ $remote ] = str_replace(
214 $this->get_base_path(),
215 $this->get_base_url(),
216 $local
217 );
218 }
219
220 $this->css = str_replace(
221 array_keys( $files ),
222 array_values( $files ),
223 $this->remote_styles
224 );
225
226 $this->write_stylesheet();
227
228 return $this->css;
229 }
230
231 /**
232 * Get local stylesheet contents.
233 *
234 * @access public
235 * @since 1.1.0
236 * @return string|false Returns the remote URL contents.
237 */
238 public function get_local_stylesheet_contents() {
239
240 // Check if the local stylesheet exists.
241 if ( $this->local_file_exists() ) {
242
243 // Attempt to update the stylesheet. Return false on fail.
244 if ( ! $this->write_stylesheet() ) {
245 return false;
246 }
247 }
248
249 ob_start();
250 include $this->get_local_stylesheet_path();
251 return ob_get_clean();
252 }
253
254 /**
255 * Get remote file contents.
256 *
257 * @access public
258 * @since 1.0.0
259 * @return string Returns the remote URL contents.
260 */
261 public function get_remote_url_contents() {
262
263 /**
264 * The user-agent we want to use.
265 *
266 * The default user-agent is the only one compatible with woff (not woff2)
267 * which also supports unicode ranges.
268 */
269 $user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8';
270
271 // Switch to a user-agent supporting woff2 if we don't need to support IE.
272 if ( 'woff2' === $this->font_format ) {
273 $user_agent = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:73.0) Gecko/20100101 Firefox/73.0';
274 }
275
276 // Get the response.
277 $response = wp_remote_get( $this->remote_url, array( 'user-agent' => $user_agent ) );
278
279 // Early exit if there was an error.
280 if ( is_wp_error( $response ) ) {
281 return '';
282 }
283
284 // Get the CSS from our response.
285 $contents = wp_remote_retrieve_body( $response );
286
287 return $contents;
288 }
289
290 /**
291 * Download files mentioned in our CSS locally.
292 *
293 * @access public
294 * @since 1.0.0
295 * @return array Returns an array of remote URLs and their local counterparts.
296 */
297 public function get_local_files_from_css() {
298 $font_files = $this->get_remote_files_from_css();
299 $stored = get_site_option( 'downloaded_font_files', array() );
300 $change = false; // If in the end this is true, we need to update the cache option.
301
302 if ( ! defined( 'FS_CHMOD_DIR' ) ) {
303 define( 'FS_CHMOD_DIR', ( 0755 & ~ umask() ) );
304 }
305
306 // If the fonts folder don't exist, create it.
307 if ( ! file_exists( $this->get_fonts_folder() ) ) {
308 $this->get_filesystem()->mkdir( $this->get_fonts_folder(), FS_CHMOD_DIR );
309 }
310
311 foreach ( $font_files as $font_family => $files ) {
312
313 // The folder path for this font-family.
314 $folder_path = $this->get_fonts_folder() . '/' . $font_family;
315
316 // If the folder doesn't exist, create it.
317 if ( ! file_exists( $folder_path ) ) {
318 $this->get_filesystem()->mkdir( $folder_path, FS_CHMOD_DIR );
319 }
320
321 foreach ( $files as $url ) {
322
323 // Get the filename.
324 $filename = basename( wp_parse_url( $url, PHP_URL_PATH ) );
325 $font_path = $folder_path . '/' . $filename;
326 /**
327 * In Typekit, the filename will always be the same. We also need to check for query vars in their URLs.
328 * They provide this font variation description that we can use https://github.com/typekit/fvd
329 */
330 $queries = parse_url( $url, PHP_URL_QUERY );
331 if ( ! empty( $queries ) ) {
332 $query_args = array();
333 parse_str( $queries, $query_args );
334 if ( array_key_exists( 'fvd', $query_args ) ) {
335 $font_path .= $query_args['fvd'];
336 }
337 }
338
339 // Check if the file already exists.
340 if ( file_exists( $font_path ) ) {
341
342 // Skip if already cached.
343 if ( isset( $stored[ $url ] ) ) {
344 continue;
345 }
346
347 // Add file to the cache and change the $changed var to indicate we need to update the option.
348 $stored[ $url ] = $font_path;
349 $change = true;
350
351 // Since the file exists we don't need to proceed with downloading it.
352 continue;
353 }
354
355 /**
356 * If we got this far, we need to download the file.
357 */
358
359 // require file.php if the download_url function doesn't exist.
360 if ( ! function_exists( 'download_url' ) ) {
361 require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' );
362 }
363
364 // Download file to temporary location.
365 $tmp_path = download_url( $url );
366
367 // Make sure there were no errors.
368 if ( is_wp_error( $tmp_path ) ) {
369 continue;
370 }
371
372 // Move temp file to final destination.
373 $success = $this->get_filesystem()->move( $tmp_path, $font_path, true );
374 if ( $success ) {
375 $stored[ $url ] = $font_path;
376 $change = true;
377 }
378 }
379 }
380
381 // If there were changes, update the option.
382 if ( $change ) {
383
384 // Cleanup the option and then save it.
385 foreach ( $stored as $url => $path ) {
386 if ( ! file_exists( $path ) ) {
387 unset( $stored[ $url ] );
388 }
389 }
390 update_site_option( 'downloaded_font_files', $stored );
391 }
392
393 return $stored;
394 }
395
396 /**
397 * Get font files from the CSS.
398 *
399 * @access public
400 * @since 1.0.0
401 * @return array Returns an array of font-families and the font-files used.
402 */
403 public function get_remote_files_from_css() {
404
405 $font_faces = explode( '@font-face', $this->remote_styles );
406
407 $result = array();
408
409 // Loop all our font-face declarations.
410 foreach ( $font_faces as $font_face ) {
411
412 // Make sure we only process styles inside this declaration.
413 $style = explode( '}', $font_face )[0];
414
415 // Sanity check.
416 if ( false === strpos( $style, 'font-family' ) ) {
417 continue;
418 }
419
420 // Get an array of our font-families.
421 preg_match_all( '/font-family.*?\;/', $style, $matched_font_families );
422
423 // Get an array of our font-files.
424 preg_match_all( '/url\(.*?\)/i', $style, $matched_font_files );
425
426 // Get the font-family name.
427 $font_family = 'unknown';
428 if ( isset( $matched_font_families[0] ) && isset( $matched_font_families[0][0] ) ) {
429 $font_family = rtrim( ltrim( $matched_font_families[0][0], 'font-family:' ), ';' );
430 $font_family = trim( str_replace( array( "'", ';' ), '', $font_family ) );
431 $font_family = sanitize_key( strtolower( str_replace( ' ', '-', $font_family ) ) );
432 }
433
434 // Make sure the font-family is set in our array.
435 if ( ! isset( $result[ $font_family ] ) ) {
436 $result[ $font_family ] = array();
437 }
438
439 // Get files for this font-family and add them to the array.
440 foreach ( $matched_font_files as $match ) {
441
442 // Sanity check.
443 if ( ! isset( $match[0] ) ) {
444 continue;
445 }
446
447 // Add the file URL.
448 $font_family_url = rtrim( ltrim( $match[0], 'url(' ), ')' );
449 $font_family_url = str_replace( '"', '', $font_family_url );
450
451 // Make sure to convert relative URLs to absolute.
452 $font_family_url = $this->get_absolute_path( $font_family_url );
453
454 $result[ $font_family ][] = $font_family_url;
455 }
456
457 // Make sure we have unique items.
458 // We're using array_flip here instead of array_unique for improved performance.
459 $result[ $font_family ] = array_flip( array_flip( $result[ $font_family ] ) );
460 }
461
462 return $result;
463 }
464
465 /**
466 * Write the CSS to the filesystem.
467 *
468 * @access protected
469 * @since 1.1.0
470 * @return string|false Returns the absolute path of the file on success, or false on fail.
471 */
472 protected function write_stylesheet() {
473 $file_path = $this->get_local_stylesheet_path();
474 $filesystem = $this->get_filesystem();
475
476 if ( ! defined( 'FS_CHMOD_DIR' ) ) {
477 define( 'FS_CHMOD_DIR', ( 0755 & ~ umask() ) );
478 }
479
480 // If the folder doesn't exist, create it.
481 if ( ! file_exists( $this->get_fonts_folder() ) ) {
482 $filesystem->mkdir( $this->get_fonts_folder(), FS_CHMOD_DIR );
483 }
484
485 // If the file doesn't exist, create it. Return false if it can not be created.
486 if ( ! $filesystem->exists( $file_path ) && ! $filesystem->touch( $file_path ) ) {
487 return false;
488 }
489
490 // If we got this far, we need to write the file.
491 // Get the CSS.
492 if ( null === $this->css ) {
493 $this->get_styles();
494 }
495
496 // Put the contents in the file. Return false if that fails.
497 if ( ! $filesystem->put_contents( $file_path, $this->css ) ) {
498 return false;
499 }
500
501 return $file_path;
502 }
503
504 /**
505 * Get the stylesheet path.
506 *
507 * @access public
508 * @since 1.1.0
509 * @return string
510 */
511 public function get_local_stylesheet_path() {
512 if ( ! $this->local_stylesheet_path ) {
513 $this->local_stylesheet_path = $this->get_fonts_folder() . '/' . $this->get_local_stylesheet_filename() . '.css';
514 }
515 return $this->local_stylesheet_path;
516 }
517
518 /**
519 * Get the local stylesheet filename.
520 *
521 * This is a hash, generated from the site-URL, the wp-content path and the URL.
522 * This way we can avoid issues with sites changing their URL, or the wp-content path etc.
523 *
524 * @access public
525 * @since 1.1.0
526 * @return string
527 */
528 public function get_local_stylesheet_filename() {
529 return md5( $this->get_base_url() . $this->get_base_path() . $this->remote_url . $this->font_format );
530 }
531
532 /**
533 * Set the font-format to be used.
534 *
535 * @access public
536 * @since 1.0.0
537 * @param string $format The format to be used. Use "woff" or "woff2".
538 * @return void
539 */
540 public function set_font_format( $format = 'woff2' ) {
541 $this->font_format = $format;
542 }
543
544 /**
545 * Check if the local stylesheet exists.
546 *
547 * The name of this method is wrong. Should be "no_local_file_exists()"
548 * as it returns true if the file does NOT exist.
549 * Keeping the original name not to break 3rd party scripts.
550 *
551 * @access public
552 * @since 1.1.0
553 * @return bool
554 */
555 public function local_file_exists() {
556 return ( ! file_exists( $this->get_local_stylesheet_path() ) );
557 }
558
559 /**
560 * Get the base path.
561 *
562 * @access public
563 * @since 1.1.0
564 * @return string
565 */
566 public function get_base_path() {
567 if ( ! $this->base_path ) {
568 $this->base_path = apply_filters( 'wptt_get_local_fonts_base_path', $this->get_filesystem()->wp_content_dir() );
569 }
570 return $this->base_path;
571 }
572
573 /**
574 * Get the base URL.
575 *
576 * @access public
577 * @since 1.1.0
578 * @return string
579 */
580 public function get_base_url() {
581 if ( ! $this->base_url ) {
582 $this->base_url = apply_filters( 'wptt_get_local_fonts_base_url', content_url() );
583 }
584 return $this->base_url;
585 }
586
587 /**
588 * Get the subfolder name.
589 *
590 * @access public
591 * @since 1.1.0
592 * @return string
593 */
594 public function get_subfolder_name() {
595 if ( ! $this->subfolder_name ) {
596 $this->subfolder_name = apply_filters( 'wptt_get_local_fonts_subfolder_name', 'fonts' );
597 }
598 return $this->subfolder_name;
599 }
600
601 /**
602 * Get the folder for fonts.
603 *
604 * @access public
605 * @return string
606 */
607 public function get_fonts_folder() {
608 if ( ! $this->fonts_folder ) {
609 $this->fonts_folder = $this->get_base_path();
610 if ( $this->get_subfolder_name() ) {
611 $this->fonts_folder .= '/' . $this->get_subfolder_name();
612 }
613 }
614 return $this->fonts_folder;
615 }
616
617 /**
618 * Schedule a cleanup.
619 *
620 * Deletes the fonts files on a regular basis.
621 * This way font files will get updated regularly,
622 * and we avoid edge cases where unused files remain in the server.
623 *
624 * @access public
625 * @since 1.1.0
626 * @return void
627 */
628 public function schedule_cleanup() {
629 if ( ! is_multisite() || ( is_multisite() && is_main_site() ) ) {
630 if ( ! wp_next_scheduled( 'delete_fonts_folder' ) && ! wp_installing() ) {
631 wp_schedule_event( time(), self::CLEANUP_FREQUENCY, 'delete_fonts_folder' );
632 }
633 }
634 }
635
636 /**
637 * Delete the fonts folder.
638 *
639 * This runs as part of a cleanup routine.
640 *
641 * @access public
642 * @since 1.1.0
643 * @return bool
644 */
645 public function delete_fonts_folder() {
646 return $this->get_filesystem()->delete( $this->get_fonts_folder(), true );
647 }
648
649 /**
650 * Get the filesystem.
651 *
652 * @access protected
653 * @since 1.0.0
654 * @return \WP_Filesystem_Base
655 */
656 protected function get_filesystem() {
657 global $wp_filesystem;
658
659 // If the filesystem has not been instantiated yet, do it here.
660 if ( ! $wp_filesystem ) {
661 if ( ! function_exists( 'WP_Filesystem' ) ) {
662 require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' );
663 }
664 WP_Filesystem();
665 }
666 return $wp_filesystem;
667 }
668
669 /**
670 * Get an absolute URL from a relative URL.
671 *
672 * @access protected
673 *
674 * @param string $url The URL.
675 *
676 * @return string
677 */
678 protected function get_absolute_path( $url ) {
679
680 // If dealing with a root-relative URL.
681 if ( 0 === stripos( $url, '/' ) ) {
682 $parsed_url = parse_url( $this->remote_url );
683 return $parsed_url['scheme'] . '://' . $parsed_url['hostname'] . $url;
684 }
685
686 return $url;
687 }
688 }
689 }
690
691 if ( ! function_exists( 'wptt_get_webfont_styles' ) ) {
692 /**
693 * Get styles for a webfont.
694 *
695 * This will get the CSS from the remote API,
696 * download any fonts it contains,
697 * replace references to remote URLs with locally-downloaded assets,
698 * and finally return the resulting CSS.
699 *
700 * @since 1.0.0
701 *
702 * @param string $url The URL of the remote webfont.
703 * @param string $format The font-format. If you need to support IE, change this to "woff".
704 *
705 * @return string Returns the CSS.
706 */
707 function wptt_get_webfont_styles( $url, $format = 'woff2' ) {
708 $font = new WPTT_WebFont_Loader( $url );
709 $font->set_font_format( $format );
710 return $font->get_styles();
711 }
712 }
713
714 if ( ! function_exists( 'wptt_get_webfont_url' ) ) {
715 /**
716 * Get a stylesheet URL for a webfont.
717 *
718 * @since 1.1.0
719 *
720 * @param string $url The URL of the remote webfont.
721 * @param string $format The font-format. If you need to support IE, change this to "woff".
722 *
723 * @return string Returns the CSS.
724 */
725 function wptt_get_webfont_url( $url, $format = 'woff2' ) {
726 $font = new WPTT_WebFont_Loader( $url );
727 $font->set_font_format( $format );
728 return $font->get_url();
729 }
730 }
731