PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / 1.3
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant v1.3
2.0.3 2.0.2 2.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2 1.3 2.0.0
easyfonts / easyfonts.php

easyfonts.php in EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant 1.3, at easyfonts.php

520 lines 21.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: EasyFonts
4 * Plugin URI: https://easywpstuff.com
5 * Description: Automatically Host existing google fonts locally on your server
6 * Version: 1.3.0
7 * Author: Uzair
8 * Author URI: https://easywpstuff.com
9 * License: GPL2
10 * Text Domain: easyfonts
11 */
12
13 if ( ! defined( 'WPINC' ) ) {
14 die;
15 }
16
17 define( 'EASYFONTS_VERSION', '1.3.0' );
18 define( 'EASYFONTS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
19 define( 'EASYFONTS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
20 define( 'EASYFONTS_UPLOAD_DIR', wp_upload_dir()['basedir'] . '/easyfonts' );
21 $upload_baseurl = wp_upload_dir()['baseurl'];
22 if ( is_ssl() ) {
23 $upload_baseurl = set_url_scheme( $upload_baseurl, 'https' );
24 }
25 define( 'EASYFONTS_UPLOAD_URL', $upload_baseurl . '/easyfonts' );
26
27 require_once EASYFONTS_PLUGIN_DIR . '/lib/simple_html_dom.php';
28 include_once EASYFONTS_PLUGIN_DIR . '/inc/options.php';
29 include_once EASYFONTS_PLUGIN_DIR . '/inc/notices.php';
30
31 class EasyFonts {
32
33 public function __construct() {
34 add_action( 'plugins_loaded', [ $this, 'load_textdomain' ] );
35 add_action( 'template_redirect', [ $this, 'maybe_start_buffering' ] );
36 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_options_styles' ] );
37 add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), [ $this, 'add_settings_link' ] );
38 add_action( 'update_option_easyfonts_options', [ $this, 'auto_clear_cache' ], 10, 0 );
39 register_uninstall_hook( __FILE__, [ __CLASS__, 'uninstall' ] );
40 }
41
42 public function load_textdomain() {
43 load_plugin_textdomain( 'easyfonts', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
44 }
45
46 /**
47 * Get base URL considering SSL.
48 *
49 * @return string
50 */
51 private static function get_base_url() {
52 return is_ssl() ? set_url_scheme( wp_upload_dir()['baseurl'], 'https' ) : wp_upload_dir()['baseurl'];
53 }
54
55 public function maybe_start_buffering() {
56 if ( is_admin() ) {
57 return;
58 }
59
60 if ( $this->should_process() ) {
61 ob_start( [ $this, 'combined_callback' ] );
62 }
63
64 add_filter( 'wordpress_prepare_output', [ $this, 'after_smart_slider' ], 11 );
65 add_filter( 'groovy_menu_final_output', [ $this, 'after_smart_slider' ], 11 );
66 }
67
68 private function should_process() {
69 $options = get_option( 'easyfonts_options', [] );
70 return ! empty( $options['host_link'] ) || ! empty( $options['host_import'] ) || ! empty( $options['process_fontface'] ) || ! empty( $options['remove_hints'] ) || ! empty( $options['remove_scripts'] ) || ! empty( $options['combine_fonts'] ) || ( ! empty( $options['font_display'] ) && $options['font_display'] !== 'none' );
71 }
72
73 public function combined_callback( $buffer ) {
74 $options = get_option( 'easyfonts_options', [] );
75
76 if ( ! empty( $options['host_link'] ) ) {
77 $buffer = $this->process_content_link_tag( $buffer );
78 }
79 if ( ! empty( $options['host_import'] ) ) {
80 $buffer = $this->process_content_import( $buffer );
81 }
82 if ( ! empty( $options['process_fontface'] ) ) {
83 $buffer = $this->download_gstatic_fonts( $buffer );
84 }
85 if ( ! empty( $options['remove_hints'] ) ) {
86 $buffer = $this->remove_resource_hints( $buffer );
87 }
88 if ( ! empty( $options['remove_scripts'] ) ) {
89 $buffer = $this->remove_font_scripts( $buffer );
90 }
91 if ( ! empty( $options['font_display'] ) && $options['font_display'] !== 'none' ) {
92 $buffer = $this->apply_font_display( $buffer, $options['font_display'] );
93 }
94 if ( ! empty( $options['combine_fonts'] ) ) {
95 $buffer = $this->combine_font_styles( $buffer );
96 }
97
98 return $buffer;
99 }
100
101 public function after_smart_slider( $buffer ) {
102 $options = get_option( 'easyfonts_options', [] );
103 if ( ! empty( $options['host_link'] ) ) {
104 $buffer = $this->process_content_link_tag( $buffer );
105 }
106 return $buffer;
107 }
108
109 /**
110 * Fetch remote file content.
111 *
112 * @param string $url URL to fetch.
113 * @return string|false Content or false on error.
114 */
115 private function get_remote_file( $url ) {
116 $response = wp_remote_get( $url, [
117 // Updated to a modern Chrome User-Agent so Google returns Variable Fonts
118 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
119 ] );
120
121 if ( is_wp_error( $response ) ) {
122 error_log( __( 'EasyFonts remote fetch error: ', 'easyfonts' ) . $response->get_error_message() );
123 return false;
124 }
125
126 // Ensure the remote server actually returned the file successfully
127 $response_code = wp_remote_retrieve_response_code( $response );
128 if ( $response_code !== 200 ) {
129 error_log( __( 'EasyFonts remote fetch error: HTTP ' . $response_code . ' for URL ' . $url, 'easyfonts' ) );
130 return false;
131 }
132
133 return wp_remote_retrieve_body( $response );
134 }
135
136 /**
137 * Process @font-face in CSS and download fonts.
138 *
139 * @param string $css CSS content.
140 * @return string Processed CSS.
141 */
142 private function process_font_face_declarations( $css ) {
143 if ( preg_match_all( '/@font-face\s*\{([^}]+)\}/', $css, $matches ) ) {
144 foreach ( $matches[1] as $match ) {
145 if ( preg_match_all( '/src\s*:\s*([^;]+);/', $match, $srcs ) ) {
146 foreach ( $srcs[1] as $src ) {
147 if ( preg_match( '/url\(([^)]+)\)/', $src, $url_match ) ) {
148 $font_url = trim( $url_match[1], "'\"" );
149 $font_filename = substr( hash( 'sha256', $font_url ), 0, 10 ) . '.' . pathinfo( $font_url, PATHINFO_EXTENSION );
150 $local_path = EASYFONTS_UPLOAD_DIR . '/' . $font_filename;
151 if ( ! file_exists( $local_path ) ) {
152 $font_data = $this->get_remote_file( $font_url );
153 if ( $font_data !== false ) {
154 file_put_contents( $local_path, $font_data );
155 }
156 }
157 $css = str_replace( $src, "url('" . EASYFONTS_UPLOAD_URL . "/" . $font_filename . "')", $css );
158 }
159 }
160 }
161 }
162 }
163 return $css;
164 }
165
166 private function process_html_with_dom( $content, $callback ) {
167 $html = hgfl_str_get_html( $content, false, true, 'UTF-8', false, PHP_EOL, ' ' );
168 if ( empty( $html ) ) {
169 return $content;
170 }
171 $callback( $html );
172 return $html->save();
173 }
174
175 private function process_content_link_tag( $content ) {
176 return $this->process_html_with_dom( $content, function( $html ) {
177 if ( ! wp_mkdir_p( EASYFONTS_UPLOAD_DIR ) ) {
178 error_log( __( 'EasyFonts could not create directory: ', 'easyfonts' ) . EASYFONTS_UPLOAD_DIR );
179 return;
180 }
181 $apply_bunny = apply_filters( 'easyfonts_bunnyfonts', true );
182 $providers = [ 'fonts.googleapis.com' ];
183 if ( $apply_bunny ) {
184 $providers[] = 'fonts.bunny.net';
185 }
186 foreach ( $html->find( 'link[rel=stylesheet]' ) as $link ) {
187 $href = $link->href;
188 $matched = false;
189 foreach ( $providers as $provider ) {
190 if ( strpos( $href, $provider ) !== false ) {
191 $matched = true;
192 break;
193 }
194 }
195 if ( ! $matched ) {
196 continue;
197 }
198 if ( strpos( $href, '//' ) === 0 ) {
199 $href = 'https:' . $href;
200 }
201 $decoded_url = rawurldecode( htmlspecialchars_decode( $href ) );
202 $filename = substr( hash( 'sha256', $decoded_url ), 0, 10 ) . '.css';
203 $local_path = EASYFONTS_UPLOAD_DIR . '/' . $filename;
204 if ( ! file_exists( $local_path ) ) {
205 $css = $this->get_remote_file( $decoded_url );
206 if ( $css === false ) {
207 continue;
208 }
209 if ( ! preg_match_all( '/@font-face\s*\{([^}]+)\}/', $css, $matches ) ) {
210 continue;
211 }
212 $css = $this->process_font_face_declarations( $css );
213 file_put_contents( $local_path, $css );
214 }
215 $link->href = EASYFONTS_UPLOAD_URL . '/' . $filename;
216 }
217 } );
218 }
219
220 private function process_content_import( $content ) {
221 return $this->process_html_with_dom( $content, function( $html ) {
222 if ( ! wp_mkdir_p( EASYFONTS_UPLOAD_DIR ) ) {
223 error_log( __( 'EasyFonts could not create directory: ', 'easyfonts' ) . EASYFONTS_UPLOAD_DIR );
224 return;
225 }
226 foreach ( $html->find( 'style' ) as $style ) {
227 if ( preg_match_all( '/@import\s+(url\()?\s*([^\)]+)\s*(\))?/', $style->innertext, $matches ) ) {
228 foreach ( $matches[2] as $match ) {
229 if ( strpos( $match, 'fonts.googleapis.com' ) === false ) {
230 continue;
231 }
232 $url = trim( $match, "'\"" );
233 if ( strpos( $url, '//' ) === 0 ) {
234 $url = 'https:' . $url;
235 }
236 $decoded_url = rawurldecode( htmlspecialchars_decode( $url ) );
237 $filename = substr( hash( 'sha256', $decoded_url ), 0, 10 ) . '.css';
238 $local_path = EASYFONTS_UPLOAD_DIR . '/' . $filename;
239 if ( ! file_exists( $local_path ) ) {
240 $css = $this->get_remote_file( $decoded_url );
241 if ( $css === false ) {
242 continue;
243 }
244 $css = $this->process_font_face_declarations( $css );
245 file_put_contents( $local_path, $css );
246 }
247 $style->innertext = str_replace( $match, EASYFONTS_UPLOAD_URL . '/' . $filename, $style->innertext );
248 }
249 }
250 }
251 } );
252 }
253
254 private function remove_resource_hints( $content ) {
255 return $this->process_html_with_dom( $content, function( $html ) {
256 $links = $html->find( 'link[rel=preload], link[rel=preconnect], link[rel=dns-prefetch]' );
257 foreach ( $links as $link ) {
258 if ( preg_match( '/(https:\/\/|\/\/)(fonts\.googleapis\.com|fonts\.gstatic\.com)/', $link->href ) ) {
259 $link->outertext = '';
260 }
261 }
262 $styles = $html->find( 'style' );
263 foreach ( $styles as $style ) {
264 if ( strpos( $style->innertext, 'fonts.googleapis.com' ) !== false || strpos( $style->innertext, 'fonts.gstatic.com' ) !== false ) {
265 $style->innertext = preg_replace( '#/\*(?:[^*]*(?:\*(?!/))*)*\*/#', '', $style->innertext );
266 }
267 }
268 } );
269 }
270
271 private function remove_font_scripts( $content ) {
272 return $this->process_html_with_dom( $content, function( $html ) {
273 $scripts = $html->find( 'script' );
274 foreach ( $scripts as $script ) {
275 if ( strpos( $script->innertext, 'WebFontConfig' ) !== false || strpos( $script->innertext, 'webfont.js' ) !== false ) {
276 $script->outertext = '';
277 }
278 }
279 } );
280 }
281
282 private function download_gstatic_fonts( $content ) {
283 if ( ! wp_mkdir_p( EASYFONTS_UPLOAD_DIR ) ) {
284 error_log( __( 'EasyFonts could not create directory: ', 'easyfonts' ) . EASYFONTS_UPLOAD_DIR );
285 return $content;
286 }
287 $content = preg_replace( '/(url\s?\(["\']?)\/\/(fonts\.gstatic\.com[^"\']+)/', '$1https://$2', $content );
288 $html = hgfl_str_get_html( $content, false, true, 'UTF-8', false, PHP_EOL, ' ' );
289 if ( empty( $html ) ) {
290 return $content;
291 }
292 foreach ( $html->find( 'style' ) as $style ) {
293 if ( preg_match_all( '/url\(([^)]+)\)/', $style->innertext, $matches ) ) {
294 foreach ( $matches[1] as $match ) {
295 $url = trim( $match, "'\"" );
296 if ( strpos( $url, '//' ) === 0 ) {
297 $url = 'https:' . $url;
298 }
299 if ( strpos( $url, 'fonts.gstatic.com' ) === false ) {
300 continue;
301 }
302 $path_parts = pathinfo( $url );
303 $extension = $path_parts['extension'] ?? '';
304 if ( empty( $extension ) ) {
305 $extension = strpos( $url, '/l/font' ) !== false ? 'svg' : 'woff2';
306 }
307 $filename = substr( hash( 'sha256', $url ), 0, 10 ) . '.' . $extension;
308 $local_path = EASYFONTS_UPLOAD_DIR . '/' . $filename;
309 if ( ! file_exists( $local_path ) ) {
310 $font_data = $this->get_remote_file( $url );
311 if ( $font_data !== false ) {
312 file_put_contents( $local_path, $font_data );
313 }
314 }
315 $content = str_replace( $url, EASYFONTS_UPLOAD_URL . '/' . $filename, $content );
316 }
317 }
318 }
319 return $content;
320 }
321
322 /**
323 * Apply font-display property to all @font-face declarations in buffer and local CSS files.
324 *
325 * @param string $buffer HTML buffer.
326 * @param string $value font-display value.
327 * @return string
328 */
329 private function apply_font_display( $buffer, $value ) {
330 $allowed = [ 'auto', 'block', 'swap', 'fallback', 'optional' ];
331 if ( ! in_array( $value, $allowed, true ) ) {
332 return $buffer;
333 }
334 $buffer = $this->inject_font_display_in_css( $buffer, $value );
335 // Also patch local CSS files in upload dir
336 if ( is_dir( EASYFONTS_UPLOAD_DIR ) ) {
337 $dir = new DirectoryIterator( EASYFONTS_UPLOAD_DIR );
338 foreach ( $dir as $file ) {
339 if ( ! $file->isFile() || $file->getExtension() !== 'css' ) {
340 continue;
341 }
342 $path = $file->getRealPath();
343 $css = file_get_contents( $path );
344 $patched = $this->inject_font_display_in_css( $css, $value );
345 if ( $patched !== $css ) {
346 file_put_contents( $path, $patched );
347 }
348 }
349 }
350 return $buffer;
351 }
352
353 /**
354 * Replace or inject font-display in all @font-face blocks within CSS string.
355 */
356 private function inject_font_display_in_css( $css, $value ) {
357 return preg_replace_callback(
358 '/@font-face\s*\{([^}]+)\}/',
359 function ( $m ) use ( $value ) {
360 $body = $m[1];
361 if ( preg_match( '/font-display\s*:\s*[^;]+;/', $body ) ) {
362 $body = preg_replace( '/font-display\s*:\s*[^;]+;/', 'font-display: ' . $value . ';', $body );
363 } else {
364 $body = rtrim( $body ) . "\n font-display: " . $value . ";\n";
365 }
366 return '@font-face {' . $body . '}';
367 },
368 $css
369 );
370 }
371
372 /**
373 * Combine all locally hosted font CSS into one file, remove originals, inject after <body>.
374 *
375 * Collects @font-face from:
376 * - <link> tags pointing to easyfonts CSS files
377 * - @import rules inside <style> pointing to easyfonts CSS files
378 * - Inline @font-face blocks inside <style> that reference easyfonts URLs
379 *
380 * Deduplicates, writes combined.css, removes originals, injects one <link> after <body>.
381 *
382 * @param string $buffer HTML buffer.
383 * @return string
384 */
385 /**
386 * Combine page-specific font CSS into one file, remove originals, inject after <body>.
387 *
388 * @param string $buffer HTML buffer.
389 * @return string
390 */
391 private function combine_font_styles( $buffer ) {
392 if ( ! is_dir( EASYFONTS_UPLOAD_DIR ) ) {
393 return $buffer;
394 }
395
396 $upload_url = preg_quote( EASYFONTS_UPLOAD_URL, '/' );
397 $upload_url_plain = EASYFONTS_UPLOAD_URL;
398 $all_blocks = [];
399 $seen_hashes = [];
400
401 $collect_block = function ( $block ) use ( &$all_blocks, &$seen_hashes ) {
402 $key = '';
403 if ( preg_match( '/src\s*:\s*url\([\'"]?([^\)\'\"]+)[\'"]?\)/i', $block, $url_m ) ) {
404 $key .= trim( $url_m[1] );
405 }
406 if ( preg_match( '/font-family\s*:\s*[\'"]?([^;\'"]+)/i', $block, $fam_m ) ) {
407 $key .= '|' . preg_replace( '/\s+/', '', trim( $fam_m[1] ) );
408 }
409 if ( preg_match( '/font-weight\s*:\s*([^;]+)/i', $block, $w_m ) ) {
410 $key .= '|' . trim( $w_m[1] );
411 }
412 if ( preg_match( '/font-style\s*:\s*([^;]+)/i', $block, $s_m ) ) {
413 $key .= '|' . trim( $s_m[1] );
414 }
415 $hash = md5( $key );
416 if ( ! isset( $seen_hashes[ $hash ] ) ) {
417 $seen_hashes[ $hash ] = true;
418 $all_blocks[] = $block;
419 }
420 };
421
422 // 1. Collect from <link> tags injected into the buffer
423 if ( preg_match_all( '/<link[^>]+href=["\'](' . $upload_url . '\/([a-f0-9]{10}\.css))["\'][^>]*>/i', $buffer, $link_matches ) ) {
424 foreach ( $link_matches[2] as $filename ) {
425 $local_path = EASYFONTS_UPLOAD_DIR . '/' . $filename;
426 if ( file_exists( $local_path ) ) {
427 $css = file_get_contents( $local_path );
428 if ( preg_match_all( '/@font-face\s*\{[^}]+\}/iu', $css, $matches ) ) {
429 foreach ( $matches[0] as $block ) {
430 $collect_block( $block );
431 }
432 }
433 }
434 }
435 }
436
437 // 2. Globally extract and remove inline @font-face blocks (Bypasses <style> PCRE limits!)
438 $buffer = preg_replace_callback(
439 '/@font-face\s*\{[^}]+\}/iu',
440 function ( $m ) use ( $upload_url_plain, $collect_block ) {
441 if ( strpos( $m[0], $upload_url_plain ) !== false ) {
442 $collect_block( $m[0] );
443 return ''; // Deletes the block from the HTML buffer
444 }
445 return $m[0];
446 },
447 $buffer
448 );
449
450 // 3. Remove @import rules pointing to easyfonts globally
451 $buffer = preg_replace( '/@import\s+(?:url\()?["\']?' . $upload_url . '\/[a-f0-9]{10}\.css["\']?\)?(?:[^;]*);?\s*/i', '', $buffer );
452
453 if ( empty( $all_blocks ) ) {
454 return $buffer;
455 }
456
457 // 4. Write page-specific combined file
458 $combined_content = implode( "\n\n", $all_blocks );
459 $combined_hash = substr( hash( 'sha256', $combined_content ), 0, 10 );
460 $combined_name = $combined_hash . '_combined.css';
461 $combined_path = EASYFONTS_UPLOAD_DIR . '/' . $combined_name;
462 $combined_url = EASYFONTS_UPLOAD_URL . '/' . $combined_name;
463
464 if ( ! file_exists( $combined_path ) ) {
465 file_put_contents( $combined_path, $combined_content );
466 }
467
468 // 5. Clean up old <link> tags and empty <style> tags left behind
469 $buffer = preg_replace( '/<link[^>]+href=["\']' . $upload_url . '\/[a-f0-9]{10}\.css["\'][^>]*\/?>\s*/i', '', $buffer );
470 $buffer = preg_replace( '/<style[^>]*>\s*<\/style>/i', '', $buffer );
471
472 // 6. Inject the single page-specific <link>
473 $link_tag = '<link rel="stylesheet" href="' . esc_url( $combined_url ) . '" type="text/css" media="all" />' . "\n";
474 $buffer = preg_replace( '/(<body[^>]*>)/i', '$1' . "\n" . $link_tag, $buffer, 1 );
475
476 return $buffer;
477 }
478
479 public function enqueue_options_styles() {
480 if ( 'settings_page_easyfonts' === get_current_screen()->id ) {
481 wp_enqueue_style( 'easyfonts-options-styles', EASYFONTS_PLUGIN_URL . 'assets/style.css', [], EASYFONTS_VERSION );
482 }
483 }
484
485 public function add_settings_link( $links ) {
486 $settings_link = '<a href="options-general.php?page=easyfonts">' . __( 'Settings', 'easyfonts' ) . '</a>';
487 array_unshift( $links, $settings_link );
488 return $links;
489 }
490
491 /**
492 * Auto-clear font cache when settings are saved.
493 */
494 public function auto_clear_cache() {
495 $dir = EASYFONTS_UPLOAD_DIR;
496 if ( is_dir( $dir ) ) {
497 $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
498 foreach ( $files as $fileinfo ) {
499 $todo = ( $fileinfo->isDir() ? 'rmdir' : 'unlink' );
500 $todo( $fileinfo->getRealPath() );
501 }
502 rmdir( $dir );
503 }
504 }
505
506 public static function uninstall() {
507 delete_option( 'easyfonts_options' );
508 $dir = EASYFONTS_UPLOAD_DIR;
509 if ( is_dir( $dir ) ) {
510 $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
511 foreach ( $files as $fileinfo ) {
512 $todo = ( $fileinfo->isDir() ? 'rmdir' : 'unlink' );
513 $todo( $fileinfo->getRealPath() );
514 }
515 rmdir( $dir );
516 }
517 }
518 }
519
520 new EasyFonts();