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 -489 1.3 → 2.0.4 View file →
@@ -1,520 +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.3.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.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' );
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 - add_action( 'update_option_easyfonts_options', [ $this, 'auto_clear_cache' ], 10, 0 );
39 - register_uninstall_hook( __FILE__, [ __CLASS__, 'uninstall' ] );
40 - }
64 + $relative = substr( $class, $len );
65 + $path = EASYFONTS_DIR . 'src/' . str_replace( '\\', '/', $relative ) . '.php';
41 66
42 - public function load_textdomain() {
43 - load_plugin_textdomain( 'easyfonts', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
44 - }
67 + if ( is_readable( $path ) ) {
68 + require_once $path;
69 + }
70 + }
71 +);
45 72
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 - }
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;
54 80
55 - public function maybe_start_buffering() {
56 - if ( is_admin() ) {
57 - return;
58 - }
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 + }
59 88
60 - if ( $this->should_process() ) {
61 - ob_start( [ $this, 'combined_callback' ] );
62 - }
89 + return $paths;
90 +}
63 91
64 - add_filter( 'wordpress_prepare_output', [ $this, 'after_smart_slider' ], 11 );
65 - add_filter( 'groovy_menu_final_output', [ $this, 'after_smart_slider' ], 11 );
66 - }
92 +/**
93 + * Boot the plugin on plugins_loaded.
94 + */
95 +add_action(
96 + 'plugins_loaded',
97 + static function () {
98 + $paths = easyfonts_paths();
67 99
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 - }
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 + }
72 106
73 - public function combined_callback( $buffer ) {
74 - $options = get_option( 'easyfonts_options', [] );
107 + \EasyFonts\Plugin::instance();
108 + }
109 +);
75 110
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 - }
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 +}
97 122
98 - return $buffer;
99 - }
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';
100 137
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 - }
138 + $install = static function () {
139 + ( new \EasyFonts\Database\Migrator() )->install();
140 + add_option( 'easyfonts_activated_at', time() );
141 + };
108 142
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 - }
143 + if ( is_multisite() && $network_wide ) {
144 + $site_ids = get_sites( array( 'fields' => 'ids', 'number' => 0 ) );
125 145
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 - }
146 + foreach ( $site_ids as $site_id ) {
147 + switch_to_blog( (int) $site_id );
148 + $install();
149 + restore_current_blog();
150 + }
132 151
133 - return wp_remote_retrieve_body( $response );
134 - }
152 + return;
153 + }
135 154
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 - }
155 + $install();
156 + }
157 +);
165 158
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 - }
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 + }
174 171
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 - }
172 + if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
173 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
174 + }
219 175
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 - }
176 + if ( ! is_plugin_active_for_network( EASYFONTS_BASENAME ) ) {
177 + return;
178 + }
253 179
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 - }
180 + $blog_id = is_object( $site ) && isset( $site->blog_id ) ? (int) $site->blog_id : (int) $site;
270 181
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 - }
182 + if ( $blog_id <= 0 ) {
183 + return;
184 + }
281 185
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 - }
186 + require_once EASYFONTS_DIR . 'src/Database/Migrator.php';
321 187
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 - }
188 + switch_to_blog( $blog_id );
189 + ( new \EasyFonts\Database\Migrator() )->install();
190 + restore_current_blog();
191 + },
192 + 10,
193 + 1
194 +);
352 195
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();
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 +);