get_all_settings();
if ( ! empty( $settings['google_fonts_remove'] ) ) {
add_action( 'wp_enqueue_scripts', array( $this, 'remove_google_fonts' ), 100 );
add_filter( 'style_loader_src', array( $this, 'block_google_fonts_src' ), 100 );
add_action( 'template_redirect', array( $this, 'buffer_remove_head_links' ), 1 );
return;
}
if ( ! empty( $settings['google_fonts_optimize'] ) || ! empty( $settings['google_fonts_local'] ) ) {
add_filter( 'style_loader_src', array( $this, 'add_display_param' ), 20 );
if ( empty( $settings['google_fonts_local'] ) ) {
add_filter( 'wp_resource_hints', array( $this, 'preconnect_resource_hints' ), 10, 2 );
}
if ( ! empty( $settings['google_fonts_preload'] ) && empty( $settings['google_fonts_local'] ) ) {
add_filter( 'wp_preload_resources', array( $this, 'preload_fonts' ) );
}
if ( ! empty( $settings['font_preload_list'] ) ) {
add_filter( 'wp_preload_resources', array( $this, 'preload_custom_fonts' ) );
}
if ( ! empty( $settings['google_fonts_local'] ) ) {
add_filter( 'style_loader_src', array( $this, 'maybe_local_css' ), 25 );
add_action( 'template_redirect', array( $this, 'buffer_local_head_links' ), 1 );
add_filter( 'wp_preload_resources', array( $this, 'preload_local_font_files' ) );
}
}
// Admin tools always registered from Plugin; clear via admin-post.
add_action( 'admin_post_qwsc_clear_local_fonts', array( $this, 'handle_clear_local_fonts' ) );
}
public function remove_google_fonts() {
if ( ! \QWSpeedCache\Plugin::should_optimize() ) {
return;
}
global $wp_styles;
if ( empty( $wp_styles->registered ) ) {
return;
}
foreach ( $wp_styles->registered as $handle => $obj ) {
if ( ! empty( $obj->src ) && self::is_google_fonts_url( $obj->src ) ) {
wp_dequeue_style( $handle );
wp_deregister_style( $handle );
}
}
}
public function block_google_fonts_src( $src ) {
if ( ! \QWSpeedCache\Plugin::should_optimize() ) {
return $src;
}
return self::is_google_fonts_url( $src ) ? false : $src;
}
/** Start a managed buffer for removing hardcoded Google Fonts links. */
public function buffer_remove_head_links() {
$this->start_managed_buffer( 'filter_removed_font_links' );
}
/** @param string $html HTML markup. */
public function filter_removed_font_links( $html ) {
if ( ! is_string( $html ) ) {
return $html;
}
$html = preg_replace( '/]+fonts\.googleapis\.com[^>]*>/i', '', $html );
return preg_replace( '/]+fonts\.gstatic\.com[^>]*>/i', '', $html );
}
/**
* Only flush the buffer we own, never a different plugin's output buffer.
*
* @param string $callback Method on this object.
*/
private function start_managed_buffer( $callback ) {
if ( ! \QWSpeedCache\Plugin::should_optimize() ) {
return;
}
\QWSpeedCache\Support\OutputBuffer::start( array( $this, $callback ) );
}
public function add_display_param( $src ) {
if ( ! \QWSpeedCache\Plugin::should_optimize() ) {
return $src;
}
if ( ! self::is_google_fonts_url( $src ) ) {
return $src;
}
$settings = \QWSpeedCache\Plugin::instance()->get_all_settings();
$display = isset( $settings['font_display'] ) ? sanitize_key( $settings['font_display'] ) : 'swap';
$allowed = array( 'auto', 'block', 'swap', 'fallback', 'optional' );
if ( ! in_array( $display, $allowed, true ) ) {
$display = 'swap';
}
if ( false !== strpos( $src, 'display=' ) ) {
$src = remove_query_arg( 'display', $src );
}
return add_query_arg( 'display', $display, $src );
}
/**
* Add Google Fonts connections through WordPress resource hints.
*
* @param array $urls Existing hints.
* @param string $relation_type Hint relation.
* @return array
*/
public function preconnect_resource_hints( $urls, $relation_type ) {
if ( ! \QWSpeedCache\Plugin::should_optimize() ) {
return $urls;
}
if ( 'preconnect' !== $relation_type ) {
return $urls;
}
$settings = \QWSpeedCache\Plugin::instance()->get_all_settings();
if ( ! empty( $settings['google_fonts_local'] ) ) {
return $urls;
}
$urls[] = 'https://fonts.googleapis.com';
$urls[] = array( 'href' => 'https://fonts.gstatic.com', 'crossorigin' => 'anonymous' );
return $urls;
}
/**
* Register Google Fonts stylesheet preloads with WordPress, rather than
* printing link elements directly into the document head.
*
* @param array $preloads Existing preload resources.
* @return array
*/
public function preload_fonts( $preloads ) {
if ( ! \QWSpeedCache\Plugin::should_optimize() ) {
return $preloads;
}
global $wp_styles;
if ( empty( $wp_styles->registered ) ) {
return $preloads;
}
foreach ( $wp_styles->queue as $handle ) {
if ( empty( $wp_styles->registered[ $handle ]->src ) ) {
continue;
}
$src = $wp_styles->registered[ $handle ]->src;
if ( self::is_google_fonts_url( $src ) && wp_http_validate_url( $src ) ) {
$preloads[] = array( 'href' => esc_url_raw( $src ), 'as' => 'style' );
}
}
return $preloads;
}
/**
* Register configured custom font preloads with WordPress.
*
* @param array $preloads Existing preload resources.
* @return array
*/
public function preload_custom_fonts( $preloads ) {
if ( ! \QWSpeedCache\Plugin::should_optimize() ) {
return $preloads;
}
$settings = \QWSpeedCache\Plugin::instance()->get_all_settings();
$lines = array_filter( array_map( 'trim', explode( "\n", (string) ( $settings['font_preload_list'] ?? '' ) ) ) );
foreach ( $lines as $url ) {
if ( 0 === strpos( $url, '#' ) || ! wp_http_validate_url( $url ) ) {
continue;
}
$type = 'font/woff2';
if ( preg_match( '/\.woff2?(\?|$)/i', $url, $m ) ) {
$type = false !== stripos( $m[0], 'woff2' ) ? 'font/woff2' : 'font/woff';
}
$preloads[] = array(
'href' => esc_url_raw( $url ),
'as' => 'font',
'type' => $type,
'crossorigin' => 'anonymous',
);
}
return $preloads;
}
/**
* Whether URL is Google Fonts CSS or file host.
*
* @param string $url URL.
* @return bool
*/
public static function is_google_fonts_url( $url ) {
if ( ! is_string( $url ) || '' === $url || strlen( $url ) > 2048 ) {
return false;
}
// Only recognize real Google Fonts hosts, never lookalike hostnames or userinfo.
$parts = wp_parse_url( $url );
if ( ! is_array( $parts ) || empty( $parts['host'] ) || empty( $parts['scheme'] ) || isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
return false;
}
return 'https' === strtolower( (string) $parts['scheme'] )
&& in_array( strtolower( (string) $parts['host'] ), array( 'fonts.googleapis.com', 'fonts.gstatic.com' ), true );
}
/**
* Extract font file URLs from Google CSS.
*
* @param string $css CSS body.
* @return string[] Absolute https URLs (unique).
*/
public static function extract_font_file_urls( $css ) {
$out = array();
if ( ! is_string( $css ) || '' === $css ) {
return $out;
}
if ( ! preg_match_all( '/url\(\s*[\'"]?([^\'"\)]+)[\'"]?\s*\)/i', $css, $m ) ) {
return $out;
}
foreach ( $m[1] as $u ) {
$u = trim( $u );
if ( '' === $u || 0 === strpos( $u, 'data:' ) ) {
continue;
}
if ( 0 === strpos( $u, '//' ) ) {
$u = 'https:' . $u;
}
if ( 0 !== strpos( $u, 'https://' ) && 0 !== strpos( $u, 'http://' ) ) {
continue;
}
if ( ! self::is_google_fonts_url( $u ) ) {
continue;
}
$out[] = $u;
}
return array_values( array_unique( $out ) );
}
/**
* Rewrite font URLs in CSS using a map remote => local path or URL.
*
* @param string $css CSS.
* @param array $map Remote URL => local URL.
* @return string
*/
public static function rewrite_font_urls( $css, array $map ) {
if ( empty( $map ) ) {
return $css;
}
// Longest first to avoid partial replacements.
uksort(
$map,
function ( $a, $b ) {
return strlen( $b ) - strlen( $a );
}
);
foreach ( $map as $remote => $local ) {
$css = str_replace( $remote, $local, $css );
// Protocol-relative variants.
if ( 0 === strpos( $remote, 'https:' ) ) {
$css = str_replace( substr( $remote, 6 ), $local, $css );
}
}
return $css;
}
/**
* Cache directory for font CSS and files.
*
* @return string Absolute path with trailing slash.
*/
public static function cache_dir() {
$dir = WP_CONTENT_DIR . '/cache/qw-speed-cache/fonts/';
if ( ! is_dir( $dir ) ) {
wp_mkdir_p( $dir );
}
$files = $dir . 'files/';
if ( ! is_dir( $files ) ) {
wp_mkdir_p( $files );
}
return $dir;
}
/**
* Public URL for a relative file under the fonts cache dir.
*
* @param string $rel Relative path e.g. hash.css or files/x.woff2.
* @return string
*/
public static function cache_url( $rel ) {
return content_url( 'cache/qw-speed-cache/fonts/' . ltrim( $rel, '/' ) );
}
/**
* Safe local filename extension from remote font URL.
*
* @param string $url Remote URL.
* @return string Extension including dot.
*/
public static function font_extension( $url ) {
$path = (string) ( wp_parse_url( $url, PHP_URL_PATH ) );
$ext = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) );
$ok = array( 'woff2', 'woff', 'ttf', 'otf', 'eot' );
if ( ! in_array( $ext, $ok, true ) ) {
return '.woff2';
}
return '.' . $ext;
}
/**
* Serve locally cached Google CSS; download CSS + font files and rewrite URLs.
*
* @param string $src Stylesheet src.
* @return string
*/
public function maybe_local_css( $src ) {
if ( ! \QWSpeedCache\Plugin::should_optimize() ) {
return $src;
}
return self::localize_stylesheet( $src );
}
/**
* Download + cache Google Fonts CSS and font files; return local URL.
*
* @param string $src Stylesheet src.
* @return string
*/
public static function localize_stylesheet( $src ) {
if ( ! self::is_google_fonts_url( $src ) || 'fonts.googleapis.com' !== strtolower( (string) wp_parse_url( $src, PHP_URL_HOST ) ) ) {
return $src;
}
// Normalize with display param already applied by earlier filter.
$hash = md5( $src );
$dir = self::cache_dir();
$css_file = $dir . $hash . '.css';
if ( file_exists( $css_file ) && filesize( $css_file ) > 0 ) {
return self::cache_url( $hash . '.css' );
}
$response = wp_safe_remote_get(
$src,
array(
'timeout' => 10,
'sslverify' => true,
'limit_response_size' => 1048576,
'headers' => array(
// Prefer woff2-capable CSS.
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
),
)
);
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
return $src;
}
$body = (string) wp_remote_retrieve_body( $response );
if ( '' === $body || strlen( $body ) > 1048576 ) {
return $src;
}
$remote_fonts = self::extract_font_file_urls( $body );
$map = array();
$n = 0;
foreach ( $remote_fonts as $font_url ) {
if ( $n >= self::MAX_FONT_FILES ) {
break;
}
$ext = self::font_extension( $font_url );
$font_hash = md5( $font_url );
$rel = 'files/' . $font_hash . $ext;
$local = $dir . $rel;
if ( ! file_exists( $local ) || filesize( $local ) < 1 ) {
$fr = wp_safe_remote_get(
$font_url,
array(
'timeout' => 15,
'sslverify' => true,
'limit_response_size' => 5242880,
)
);
if ( is_wp_error( $fr ) || 200 !== (int) wp_remote_retrieve_response_code( $fr ) ) {
continue;
}
$bin = (string) wp_remote_retrieve_body( $fr );
if ( '' === $bin || strlen( $bin ) > 5242880 ) {
continue;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
@file_put_contents( $local, $bin, LOCK_EX );
}
if ( file_exists( $local ) && filesize( $local ) > 0 ) {
$map[ $font_url ] = self::cache_url( $rel );
$n++;
}
}
$body = self::rewrite_font_urls( $body, $map );
$settings = \QWSpeedCache\Plugin::instance()->get_all_settings();
$display = isset( $settings['font_display'] ) ? $settings['font_display'] : 'swap';
$body = self::ensure_font_display( $body, $display );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
@file_put_contents( $css_file, $body, LOCK_EX );
if ( file_exists( $css_file ) && filesize( $css_file ) > 0 ) {
return self::cache_url( $hash . '.css' );
}
return $src;
}
/**
* Rewrite hardcoded Google Fonts tags in HTML to local CSS when available.
*/
public function buffer_local_head_links() {
$this->start_managed_buffer( 'filter_local_font_links' );
}
/** @param string $html HTML markup. */
public function filter_local_font_links( $html ) {
if ( ! is_string( $html ) || false === stripos( $html, 'fonts.googleapis.com' ) ) {
return $html;
}
return preg_replace_callback(
'/]+)>/i',
static function ( $m ) {
$tag = $m[0];
if ( false === stripos( $tag, 'fonts.googleapis.com' ) ) {
return $tag;
}
if ( ! preg_match( '/href=[\'"]([^\'"]+)[\'"]/i', $tag, $hm ) ) {
return $tag;
}
$local = self::localize_stylesheet( $hm[1] );
if ( $local === $hm[1] || false !== strpos( $local, 'fonts.googleapis.com' ) ) {
return $tag;
}
return str_replace( $hm[1], esc_url( $local ), $tag );
},
$html
);
}
/**
* Preload locally cached .woff2 files (first few) for faster first paint.
*/
/**
* Append locally hosted font files to WordPress's preload resource list.
*
* @param array $preloads Existing resources.
* @return array
*/
public function preload_local_font_files( $preloads ) {
if ( ! \QWSpeedCache\Plugin::should_optimize() ) {
return $preloads;
}
if ( ! is_array( $preloads ) ) {
$preloads = array();
}
$dir = self::cache_dir() . 'files/';
if ( ! is_dir( $dir ) ) {
return $preloads;
}
$files = glob( $dir . '*.woff2' ) ?: array();
$n = 0;
foreach ( $files as $file ) {
if ( $n >= 6 ) {
break;
}
if ( ! is_file( $file ) || ! is_readable( $file ) ) {
continue;
}
$url = self::cache_url( 'files/' . basename( $file ) );
if ( ! wp_http_validate_url( $url ) ) {
continue;
}
$preloads[] = array(
'href' => esc_url_raw( $url ),
'as' => 'font',
'type' => 'font/woff2',
'crossorigin' => 'anonymous',
);
$n++;
}
return $preloads;
}
/**
* Ensure font-display is present on @font-face in local CSS body.
*
* @param string $css CSS.
* @param string $display Value.
* @return string
*/
public static function ensure_font_display( $css, $display = 'swap' ) {
$display = sanitize_key( $display );
$allowed = array( 'auto', 'block', 'swap', 'fallback', 'optional' );
if ( ! in_array( $display, $allowed, true ) ) {
$display = 'swap';
}
// Inject into @font-face blocks missing font-display.
return preg_replace_callback(
'/@font-face\s*\{([^}]*)\}/i',
function ( $m ) use ( $display ) {
$body = $m[1];
if ( false !== stripos( $body, 'font-display' ) ) {
return $m[0];
}
return '@font-face{' . $body . 'font-display:' . $display . ';}';
},
(string) $css
);
}
/**
* Cache status for admin UI.
*
* @return array{css:int,files:int,bytes:int}
*/
public static function cache_status() {
$dir = self::cache_dir();
$css = 0;
$files = 0;
$bytes = 0;
foreach ( glob( $dir . '*.css' ) ?: array() as $f ) {
$css++;
$bytes += (int) filesize( $f );
}
foreach ( glob( $dir . 'files/*' ) ?: array() as $f ) {
if ( is_file( $f ) ) {
$files++;
$bytes += (int) filesize( $f );
}
}
return array(
'css' => $css,
'files' => $files,
'bytes' => $bytes,
);
}
/**
* Delete local Google Fonts cache.
*/
public static function clear_cache() {
$dir = self::cache_dir();
foreach ( glob( $dir . '*.css' ) ?: array() as $f ) {
if ( file_exists( $f ) ) { wp_delete_file( $f ); }
}
foreach ( glob( $dir . 'files/*' ) ?: array() as $f ) {
if ( is_file( $f ) ) {
if ( file_exists( $f ) ) { wp_delete_file( $f ); }
}
}
}
public function handle_clear_local_fonts() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Forbidden', 'qw-optimize' ) );
}
check_admin_referer( 'qwsc_clear_local_fonts' );
self::clear_cache();
wp_safe_redirect( add_query_arg( array( 'page' => 'qw-speed-cache-critical', 'fonts_cleared' => '1' ), admin_url( 'admin.php' ) ) );
exit;
}
}