PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / 2.0.4
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant v2.0.4
2.0.4 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
← All changes | easyfonts.php +173 -300 1.2 → 2.0.4 View file →
@@ -1,331 +1,204 @@
1 1 <?php
2 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.2.0
7 - * Author: Uzair
8 - * Author URI: https://easywpstuff.com
9 - * License: GPL2
10 - * Text Domain: easyfonts
3 + * Plugin Name: Easy Fonts
4 + * Plugin URI: https://fluxpress.io
5 + * Description: Detect, self-host, preload, and trim Google Fonts automatically — reliably, with zero-CLS metric-matched fallbacks. Built on WordPress's native HTML API.
6 + * Version: 2.0.4
7 + * Author: Uzair
8 + * Author URI: https://fluxpress.io
9 + * License: GPL-2.0-or-later
10 + * License URI: https://www.gnu.org/licenses/gpl-2.0.html
11 + * Text Domain: easyfonts
12 + * Domain Path: /languages
13 + * Requires at least: 6.4
14 + * Requires PHP: 7.4
15 + *
16 + * @package EasyFonts
11 17 */
12 18
13 -if ( ! defined( 'WPINC' ) ) {
14 - die;
19 +defined( 'ABSPATH' ) || exit;
20 +
21 +define( 'EASYFONTS_VERSION', '2.0.4' );
22 +define( 'EASYFONTS_FILE', __FILE__ );
23 +define( 'EASYFONTS_DIR', plugin_dir_path( __FILE__ ) );
24 +define( 'EASYFONTS_URL', plugin_dir_url( __FILE__ ) );
25 +define( 'EASYFONTS_BASENAME', plugin_basename( __FILE__ ) );
26 +
27 +/**
28 + * PHP 7.4 polyfills. The plugin advertises "Requires PHP: 7.4", but parts of the
29 + * codebase use string helpers added in PHP 8.0. These guarded shims keep it
30 + * genuinely 7.4-compatible without changing any call sites.
31 + */
32 +if ( ! function_exists( 'str_starts_with' ) ) {
33 + function str_starts_with( $haystack, $needle ): bool {
34 + return '' === $needle || 0 === strncmp( (string) $haystack, (string) $needle, strlen( (string) $needle ) );
35 + }
15 36 }
16 -
17 -define( 'EASYFONTS_VERSION', '1.2.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' );
37 +if ( ! function_exists( 'str_ends_with' ) ) {
38 + function str_ends_with( $haystack, $needle ): bool {
39 + if ( '' === $needle ) {
40 + return true;
41 + }
42 + $len = strlen( (string) $needle );
43 + return $len <= strlen( (string) $haystack ) && 0 === substr_compare( (string) $haystack, (string) $needle, -$len );
44 + }
24 45 }
25 -define( 'EASYFONTS_UPLOAD_URL', $upload_baseurl . '/easyfonts' );
46 +if ( ! function_exists( 'str_contains' ) ) {
47 + function str_contains( $haystack, $needle ): bool {
48 + return '' === $needle || false !== strpos( (string) $haystack, (string) $needle );
49 + }
50 +}
26 51
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';
52 +/**
53 + * PSR-4 autoloader for the EasyFonts\ namespace.
54 + */
55 +spl_autoload_register(
56 + static function ( $class ) {
57 + $prefix = 'EasyFonts\\';
58 + $len = strlen( $prefix );
30 59
31 -class EasyFonts {
60 + if ( 0 !== strncmp( $prefix, $class, $len ) ) {
61 + return;
62 + }
32 63
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 - register_uninstall_hook( __FILE__, [ __CLASS__, 'uninstall' ] );
39 - }
64 + $relative = substr( $class, $len );
65 + $path = EASYFONTS_DIR . 'src/' . str_replace( '\\', '/', $relative ) . '.php';
40 66
41 - public function load_textdomain() {
42 - load_plugin_textdomain( 'easyfonts', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
43 - }
67 + if ( is_readable( $path ) ) {
68 + require_once $path;
69 + }
70 + }
71 +);
44 72
45 - /**
46 - * Get base URL considering SSL.
47 - *
48 - * @return string
49 - */
50 - private static function get_base_url() {
51 - return is_ssl() ? set_url_scheme( wp_upload_dir()['baseurl'], 'https' ) : wp_upload_dir()['baseurl'];
52 - }
73 +/**
74 + * Resolve the upload directory + URL once. Scheme-correct, multisite-safe.
75 + *
76 + * @return array{path:string,url:string}
77 + */
78 +function easyfonts_paths(): array {
79 + static $paths = null;
53 80
54 - public function maybe_start_buffering() {
55 - if ( is_admin() ) {
56 - return;
57 - }
81 + if ( null === $paths ) {
82 + $upload = wp_upload_dir();
83 + $paths = array(
84 + 'path' => trailingslashit( $upload['basedir'] ) . 'easyfonts',
85 + 'url' => set_url_scheme( trailingslashit( $upload['baseurl'] ) . 'easyfonts' ),
86 + );
87 + }
58 88
59 - if ( $this->should_process() ) {
60 - ob_start( [ $this, 'combined_callback' ] );
61 - }
89 + return $paths;
90 +}
62 91
63 - add_filter( 'wordpress_prepare_output', [ $this, 'after_smart_slider' ], 11 );
64 - add_filter( 'groovy_menu_final_output', [ $this, 'after_smart_slider' ], 11 );
65 - }
92 +/**
93 + * Boot the plugin on plugins_loaded.
94 + */
95 +add_action(
96 + 'plugins_loaded',
97 + static function () {
98 + $paths = easyfonts_paths();
66 99
67 - private function should_process() {
68 - $options = get_option( 'easyfonts_options', [] );
69 - return ! empty( $options['host_link'] ) || ! empty( $options['host_import'] ) || ! empty( $options['process_fontface'] ) || ! empty( $options['remove_hints'] ) || ! empty( $options['remove_scripts'] );
70 - }
100 + if ( ! defined( 'EASYFONTS_CACHE_DIR' ) ) {
101 + define( 'EASYFONTS_CACHE_DIR', $paths['path'] );
102 + }
103 + if ( ! defined( 'EASYFONTS_CACHE_URL' ) ) {
104 + define( 'EASYFONTS_CACHE_URL', $paths['url'] );
105 + }
71 106
72 - public function combined_callback( $buffer ) {
73 - $options = get_option( 'easyfonts_options', [] );
107 + \EasyFonts\Plugin::instance();
108 + }
109 +);
74 110
75 - if ( ! empty( $options['host_link'] ) ) {
76 - $buffer = $this->process_content_link_tag( $buffer );
77 - }
78 - if ( ! empty( $options['host_import'] ) ) {
79 - $buffer = $this->process_content_import( $buffer );
80 - }
81 - if ( ! empty( $options['process_fontface'] ) ) {
82 - $buffer = $this->download_gstatic_fonts( $buffer );
83 - }
84 - if ( ! empty( $options['remove_hints'] ) ) {
85 - $buffer = $this->remove_resource_hints( $buffer );
86 - }
87 - if ( ! empty( $options['remove_scripts'] ) ) {
88 - $buffer = $this->remove_font_scripts( $buffer );
89 - }
111 +/**
112 + * WP-CLI registration (loaded outside the normal request lifecycle).
113 + */
114 +if ( defined( 'WP_CLI' ) && WP_CLI ) {
115 + add_action(
116 + 'cli_init',
117 + static function () {
118 + \WP_CLI::add_command( 'easyfonts', \EasyFonts\Cli\Command::class );
119 + }
120 + );
121 +}
90 122
91 - return $buffer;
92 - }
123 +/**
124 + * Activation — create tables, seed defaults, mark for rewrite.
125 + *
126 + * On multisite the activation hook fires once even for a network-wide enable,
127 + * so when network-activated we install on every existing site. New sites
128 + * created later are handled by the wp_initialize_site hook below, and any site
129 + * that somehow slips through self-heals on its first load (Plugin::maybe_migrate).
130 + *
131 + * @param bool $network_wide Whether the plugin is being network-activated.
132 + */
133 +register_activation_hook(
134 + __FILE__,
135 + static function ( $network_wide = false ) {
136 + require_once EASYFONTS_DIR . 'src/Database/Migrator.php';
93 137
94 - public function after_smart_slider( $buffer ) {
95 - $options = get_option( 'easyfonts_options', [] );
96 - if ( ! empty( $options['host_link'] ) ) {
97 - $buffer = $this->process_content_link_tag( $buffer );
98 - }
99 - return $buffer;
100 - }
138 + $install = static function () {
139 + ( new \EasyFonts\Database\Migrator() )->install();
140 + add_option( 'easyfonts_activated_at', time() );
141 + };
101 142
102 - /**
103 - * Fetch remote file content.
104 - *
105 - * @param string $url URL to fetch.
106 - * @return string|false Content or false on error.
107 - */
108 - private function get_remote_file( $url ) {
109 - $response = wp_remote_get( $url, [
110 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
111 - ] );
112 - if ( is_wp_error( $response ) ) {
113 - error_log( __( 'EasyFonts remote fetch error: ', 'easyfonts' ) . $response->get_error_message() );
114 - return false;
115 - }
116 - return wp_remote_retrieve_body( $response );
117 - }
143 + if ( is_multisite() && $network_wide ) {
144 + $site_ids = get_sites( array( 'fields' => 'ids', 'number' => 0 ) );
118 145
119 - /**
120 - * Process @font-face in CSS and download fonts.
121 - *
122 - * @param string $css CSS content.
123 - * @return string Processed CSS.
124 - */
125 - private function process_font_face_declarations( $css ) {
126 - if ( preg_match_all( '/@font-face\s*\{([^}]+)\}/', $css, $matches ) ) {
127 - foreach ( $matches[1] as $match ) {
128 - if ( preg_match_all( '/src\s*:\s*([^;]+);/', $match, $srcs ) ) {
129 - foreach ( $srcs[1] as $src ) {
130 - if ( preg_match( '/url\(([^)]+)\)/', $src, $url_match ) ) {
131 - $font_url = trim( $url_match[1], "'\"" );
132 - $font_filename = substr( hash( 'sha256', $font_url ), 0, 10 ) . '.' . pathinfo( $font_url, PATHINFO_EXTENSION );
133 - $local_path = EASYFONTS_UPLOAD_DIR . '/' . $font_filename;
134 - if ( ! file_exists( $local_path ) ) {
135 - $font_data = $this->get_remote_file( $font_url );
136 - if ( $font_data !== false ) {
137 - file_put_contents( $local_path, $font_data );
138 - }
139 - }
140 - $css = str_replace( $src, "url('" . EASYFONTS_UPLOAD_URL . "/" . $font_filename . "')", $css );
141 - }
142 - }
143 - }
144 - }
145 - }
146 - return $css;
147 - }
146 + foreach ( $site_ids as $site_id ) {
147 + switch_to_blog( (int) $site_id );
148 + $install();
149 + restore_current_blog();
150 + }
148 151
149 - private function process_html_with_dom( $content, $callback ) {
150 - $html = hgfl_str_get_html( $content, false, true, 'UTF-8', false, PHP_EOL, ' ' );
151 - if ( empty( $html ) ) {
152 - return $content;
153 - }
154 - $callback( $html );
155 - return $html->save();
156 - }
152 + return;
153 + }
157 154
158 - private function process_content_link_tag( $content ) {
159 - return $this->process_html_with_dom( $content, function( $html ) {
160 - if ( ! wp_mkdir_p( EASYFONTS_UPLOAD_DIR ) ) {
161 - error_log( __( 'EasyFonts could not create directory: ', 'easyfonts' ) . EASYFONTS_UPLOAD_DIR );
162 - return;
163 - }
164 - $apply_bunny = apply_filters( 'easyfonts_bunnyfonts', true );
165 - $providers = [ 'fonts.googleapis.com' ];
166 - if ( $apply_bunny ) {
167 - $providers[] = 'fonts.bunny.net';
168 - }
169 - foreach ( $html->find( 'link[rel=stylesheet]' ) as $link ) {
170 - $href = $link->href;
171 - $matched = false;
172 - foreach ( $providers as $provider ) {
173 - if ( strpos( $href, $provider ) !== false ) {
174 - $matched = true;
175 - break;
176 - }
177 - }
178 - if ( ! $matched ) {
179 - continue;
180 - }
181 - if ( strpos( $href, '//' ) === 0 ) {
182 - $href = 'https:' . $href;
183 - }
184 - $decoded_url = rawurldecode( htmlspecialchars_decode( $href ) );
185 - $filename = substr( hash( 'sha256', $decoded_url ), 0, 10 ) . '.css';
186 - $local_path = EASYFONTS_UPLOAD_DIR . '/' . $filename;
187 - if ( ! file_exists( $local_path ) ) {
188 - $css = $this->get_remote_file( $decoded_url );
189 - if ( $css === false ) {
190 - continue;
191 - }
192 - if ( ! preg_match_all( '/@font-face\s*\{([^}]+)\}/', $css, $matches ) ) {
193 - continue;
194 - }
195 - $css = $this->process_font_face_declarations( $css );
196 - file_put_contents( $local_path, $css );
197 - }
198 - $link->href = EASYFONTS_UPLOAD_URL . '/' . $filename;
199 - }
200 - } );
201 - }
155 + $install();
156 + }
157 +);
202 158
203 - private function process_content_import( $content ) {
204 - return $this->process_html_with_dom( $content, function( $html ) {
205 - if ( ! wp_mkdir_p( EASYFONTS_UPLOAD_DIR ) ) {
206 - error_log( __( 'EasyFonts could not create directory: ', 'easyfonts' ) . EASYFONTS_UPLOAD_DIR );
207 - return;
208 - }
209 - foreach ( $html->find( 'style' ) as $style ) {
210 - if ( preg_match_all( '/@import\s+(url\()?\s*([^\)]+)\s*(\))?/', $style->innertext, $matches ) ) {
211 - foreach ( $matches[2] as $match ) {
212 - if ( strpos( $match, 'fonts.googleapis.com' ) === false ) {
213 - continue;
214 - }
215 - $url = trim( $match, "'\"" );
216 - if ( strpos( $url, '//' ) === 0 ) {
217 - $url = 'https:' . $url;
218 - }
219 - $decoded_url = rawurldecode( htmlspecialchars_decode( $url ) );
220 - $filename = substr( hash( 'sha256', $decoded_url ), 0, 10 ) . '.css';
221 - $local_path = EASYFONTS_UPLOAD_DIR . '/' . $filename;
222 - if ( ! file_exists( $local_path ) ) {
223 - $css = $this->get_remote_file( $decoded_url );
224 - if ( $css === false ) {
225 - continue;
226 - }
227 - $css = $this->process_font_face_declarations( $css );
228 - file_put_contents( $local_path, $css );
229 - }
230 - $style->innertext = str_replace( $match, EASYFONTS_UPLOAD_URL . '/' . $filename, $style->innertext );
231 - }
232 - }
233 - }
234 - } );
235 - }
159 +/**
160 + * Multisite: install tables when a brand-new subsite is created, if the plugin
161 + * is network-active. (WP 5.1+ passes a WP_Site; older signatures are tolerated.)
162 + *
163 + * @param mixed $site New site (WP_Site) or blog id.
164 + */
165 +add_action(
166 + 'wp_initialize_site',
167 + static function ( $site ) {
168 + if ( ! is_multisite() ) {
169 + return;
170 + }
236 171
237 - private function remove_resource_hints( $content ) {
238 - return $this->process_html_with_dom( $content, function( $html ) {
239 - $links = $html->find( 'link[rel=preload], link[rel=preconnect], link[rel=dns-prefetch]' );
240 - foreach ( $links as $link ) {
241 - if ( preg_match( '/(https:\/\/|\/\/)(fonts\.googleapis\.com|fonts\.gstatic\.com)/', $link->href ) ) {
242 - $link->outertext = '';
243 - }
244 - }
245 - $styles = $html->find( 'style' );
246 - foreach ( $styles as $style ) {
247 - if ( strpos( $style->innertext, 'fonts.googleapis.com' ) !== false || strpos( $style->innertext, 'fonts.gstatic.com' ) !== false ) {
248 - $style->innertext = preg_replace( '#/\*(?:[^*]*(?:\*(?!/))*)*\*/#', '', $style->innertext );
249 - }
250 - }
251 - } );
252 - }
172 + if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
173 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
174 + }
253 175
254 - private function remove_font_scripts( $content ) {
255 - return $this->process_html_with_dom( $content, function( $html ) {
256 - $scripts = $html->find( 'script' );
257 - foreach ( $scripts as $script ) {
258 - if ( strpos( $script->innertext, 'WebFontConfig' ) !== false || strpos( $script->innertext, 'webfont.js' ) !== false ) {
259 - $script->outertext = '';
260 - }
261 - }
262 - } );
263 - }
176 + if ( ! is_plugin_active_for_network( EASYFONTS_BASENAME ) ) {
177 + return;
178 + }
264 179
265 - private function download_gstatic_fonts( $content ) {
266 - if ( ! wp_mkdir_p( EASYFONTS_UPLOAD_DIR ) ) {
267 - error_log( __( 'EasyFonts could not create directory: ', 'easyfonts' ) . EASYFONTS_UPLOAD_DIR );
268 - return $content;
269 - }
270 - $content = preg_replace( '/(url\s?\(["\']?)\/\/(fonts\.gstatic\.com[^"\']+)/', '$1https://$2', $content );
271 - $html = hgfl_str_get_html( $content, false, true, 'UTF-8', false, PHP_EOL, ' ' );
272 - if ( empty( $html ) ) {
273 - return $content;
274 - }
275 - foreach ( $html->find( 'style' ) as $style ) {
276 - if ( preg_match_all( '/url\(([^)]+)\)/', $style->innertext, $matches ) ) {
277 - foreach ( $matches[1] as $match ) {
278 - $url = trim( $match, "'\"" );
279 - if ( strpos( $url, '//' ) === 0 ) {
280 - $url = 'https:' . $url;
281 - }
282 - if ( strpos( $url, 'fonts.gstatic.com' ) === false ) {
283 - continue;
284 - }
285 - $path_parts = pathinfo( $url );
286 - $extension = $path_parts['extension'] ?? '';
287 - if ( empty( $extension ) ) {
288 - $extension = strpos( $url, '/l/font' ) !== false ? 'svg' : 'woff2';
289 - }
290 - $filename = substr( hash( 'sha256', $url ), 0, 10 ) . '.' . $extension;
291 - $local_path = EASYFONTS_UPLOAD_DIR . '/' . $filename;
292 - if ( ! file_exists( $local_path ) ) {
293 - $font_data = $this->get_remote_file( $url );
294 - if ( $font_data !== false ) {
295 - file_put_contents( $local_path, $font_data );
296 - }
297 - }
298 - $content = str_replace( $url, EASYFONTS_UPLOAD_URL . '/' . $filename, $content );
299 - }
300 - }
301 - }
302 - return $content;
303 - }
180 + $blog_id = is_object( $site ) && isset( $site->blog_id ) ? (int) $site->blog_id : (int) $site;
304 181
305 - public function enqueue_options_styles() {
306 - if ( 'settings_page_easyfonts' === get_current_screen()->id ) {
307 - wp_enqueue_style( 'easyfonts-options-styles', EASYFONTS_PLUGIN_URL . 'assets/style.css', [], EASYFONTS_VERSION );
308 - }
309 - }
182 + if ( $blog_id <= 0 ) {
183 + return;
184 + }
310 185
311 - public function add_settings_link( $links ) {
312 - $settings_link = '<a href="options-general.php?page=easyfonts">' . __( 'Settings', 'easyfonts' ) . '</a>';
313 - array_unshift( $links, $settings_link );
314 - return $links;
315 - }
186 + require_once EASYFONTS_DIR . 'src/Database/Migrator.php';
316 187
317 - public static function uninstall() {
318 - delete_option( 'easyfonts_options' );
319 - $dir = EASYFONTS_UPLOAD_DIR;
320 - if ( is_dir( $dir ) ) {
321 - $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
322 - foreach ( $files as $fileinfo ) {
323 - $todo = ( $fileinfo->isDir() ? 'rmdir' : 'unlink' );
324 - $todo( $fileinfo->getRealPath() );
325 - }
326 - rmdir( $dir );
327 - }
328 - }
329 -}
188 + switch_to_blog( $blog_id );
189 + ( new \EasyFonts\Database\Migrator() )->install();
190 + restore_current_blog();
191 + },
192 + 10,
193 + 1
194 +);
330 195
331 -new EasyFonts();
196 +/**
197 + * Deactivation — clear scheduled events.
198 + */
199 +register_deactivation_hook(
200 + __FILE__,
201 + static function () {
202 + wp_clear_scheduled_hook( 'easyfonts_cleanup' );
203 + }
204 +);