$fonts family => weights */ private static function normalize( array $fonts ) { $custom = array_map( 'strtolower', self::custom_families() ); $out = array(); foreach ( $fonts as $family => $weights ) { $family = trim( (string) $family ); if ( '' === $family || preg_match( self::SYSTEM, $family ) || in_array( strtolower( $family ), $custom, true ) || isset( self::BUNDLED[ $family ] ) ) { continue; } $w = array_values( array_unique( array_filter( array_map( 'intval', (array) $weights ) ) ) ); sort( $w ); $out[ $family ] = $w ?: array( 400 ); } ksort( $out ); return $out; } public static function google_url( array $fonts ) { $parts = array(); foreach ( $fonts as $family => $weights ) { $parts[] = 'family=' . str_replace( ' ', '+', $family ) . ':wght@' . implode( ';', $weights ); } return $parts ? 'https://fonts.googleapis.com/css2?' . implode( '&', $parts ) . '&display=swap' : ''; } /** Fonts shipped with the plugin (OFL, latin, variable): the default design system needs no download. */ const BUNDLED = array( 'Bricolage Grotesque' => array( 'bricolage-grotesque.woff2', '200 800' ), 'Figtree' => array( 'figtree.woff2', '300 900' ), ); /** @font-face rules for the bundled families in use. */ private static function bundled_css( array $fonts ) { $css = ''; foreach ( array_intersect_key( self::BUNDLED, $fonts ) as $family => $f ) { $css .= '@font-face{font-family:"' . $family . '";font-style:normal;font-weight:' . $f[1] . ';font-display:swap;src:url(' . esc_url( AKRAM_URL . 'assets/fonts/' . $f[0] ) . ') format("woff2")}'; } return $css; } /** * Google Fonts are self-hosted: the server downloads the files once and visitors load them from this site. * Visitors' browsers only contact Google when the site owner turns on "Load fonts from Google's servers". * Until the download has finished the theme's fallback font is shown. * * @return string|null stylesheet URL to load, if any */ private static function google_stylesheet( array $fonts, $hash ) { $local = self::local_url( $hash ); if ( $local ) { return $local; } if ( Options::get( 'google_cdn', false ) ) { return self::google_url( $fonts ); } self::queue_download( $fonts, $hash ); return null; } public static function enqueue( array $fonts ) { if ( ! Options::get( 'google_fonts', true ) ) { return; } $bundled = self::bundled_css( $fonts ); if ( $bundled && ! wp_style_is( 'akram-fonts-bundled', 'enqueued' ) ) { wp_register_style( 'akram-fonts-bundled', false, array(), AKRAM_VERSION ); wp_enqueue_style( 'akram-fonts-bundled' ); wp_add_inline_style( 'akram-fonts-bundled', $bundled ); } $fonts = self::normalize( $fonts ); if ( ! $fonts ) { return; } $hash = substr( md5( wp_json_encode( $fonts ) ), 0, 12 ); if ( isset( self::$printed[ $hash ] ) ) { return; } self::$printed[ $hash ] = true; $url = self::google_stylesheet( $fonts, $hash ); if ( ! $url ) { return; } wp_enqueue_style( 'akram-fonts-' . $hash, $url, array(), null ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- the URL already carries a content version. if ( 0 === strpos( $url, 'https://fonts.googleapis.com/' ) ) { add_filter( 'wp_resource_hints', static function ( $urls, $type ) { if ( 'preconnect' === $type ) { $urls[] = array( 'href' => 'https://fonts.gstatic.com', 'crossorigin', ); } return $urls; }, 10, 2 ); } } public static function print_late( array $fonts ) { if ( ! Options::get( 'google_fonts', true ) ) { return; } $bundled = self::bundled_css( $fonts ); if ( $bundled ) { Frontend::print_style( 'akram-fonts-bundled', '', $bundled ); } $fonts = self::normalize( $fonts ); if ( ! $fonts ) { return; } $hash = substr( md5( wp_json_encode( $fonts ) ), 0, 12 ); if ( isset( self::$printed[ $hash ] ) ) { return; } self::$printed[ $hash ] = true; $url = self::google_stylesheet( $fonts, $hash ); if ( $url ) { Frontend::print_style( 'akram-fonts-' . $hash, $url ); } } /** * Fetch the font files once. WP-Cron is used when it runs; on sites with DISABLE_WP_CRON (common with * real server cron) the download runs after the response has been sent. A lock keeps it to one try per hour. */ private static function queue_download( array $fonts, $hash ) { if ( get_transient( 'akram_font_try_' . $hash ) ) { return; } set_transient( 'akram_font_try_' . $hash, 1, HOUR_IN_SECONDS ); if ( ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) ) { if ( ! wp_next_scheduled( 'akram_fetch_fonts', array( $fonts ) ) ) { wp_schedule_single_event( time() + 5, 'akram_fetch_fonts', array( $fonts ) ); } return; } add_action( 'shutdown', static function () use ( $fonts ) { if ( function_exists( 'fastcgi_finish_request' ) ) { fastcgi_finish_request(); } self::download( $fonts ); }, PHP_INT_MAX ); } private static function local_url( $hash ) { $d = Document::upload_dir(); return file_exists( $d['dir'] . 'fonts/' . $hash . '.css' ) ? $d['url'] . 'fonts/' . $hash . '.css' : null; } /** Download Google CSS + woff2 files into uploads so visitors never contact Google. */ public static function download( $fonts ) { $fonts = self::normalize( (array) $fonts ); if ( ! $fonts || ! Document::ensure_upload_dir() ) { return; } $hash = substr( md5( wp_json_encode( $fonts ) ), 0, 12 ); $res = wp_remote_get( self::google_url( $fonts ), array( 'timeout' => 15, 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36', ) ); if ( is_wp_error( $res ) || 200 !== wp_remote_retrieve_response_code( $res ) ) { return; } $css = wp_remote_retrieve_body( $res ); $d = Document::upload_dir(); wp_mkdir_p( $d['dir'] . 'fonts/' ); $css = preg_replace_callback( '/url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.woff2)\)/', static function ( $m ) use ( $d ) { $name = substr( md5( $m[1] ), 0, 16 ) . '.woff2'; $path = $d['dir'] . 'fonts/' . $name; // The folder can disappear while files download in the background (e.g. data removed meanwhile): stop quietly. if ( ! file_exists( $path ) && is_dir( $d['dir'] . 'fonts' ) ) { $f = wp_remote_get( $m[1], array( 'timeout' => 15 ) ); if ( is_wp_error( $f ) || 200 !== wp_remote_retrieve_response_code( $f ) ) { return 'url(' . $m[1] . ')'; } clearstatcache( true, dirname( $path ) ); // is_dir() results are cached per request if ( ! is_dir( dirname( $path ) ) || false === file_put_contents( $path, wp_remote_retrieve_body( $f ) ) ) { return 'url(' . $m[1] . ')'; } } return file_exists( $path ) ? 'url(' . $name . ')' : 'url(' . $m[1] . ')'; }, $css ); clearstatcache( true, $d['dir'] . 'fonts' ); if ( is_dir( $d['dir'] . 'fonts' ) ) { file_put_contents( $d['dir'] . 'fonts/' . $hash . '.css', $css ); } } public static function custom_faces() { $fonts = (array) Options::get( 'custom_fonts', array() ); if ( ! $fonts ) { return; } $css = ''; foreach ( $fonts as $font ) { foreach ( (array) $font['faces'] as $face ) { if ( empty( $face['url'] ) ) { continue; } $ext = strtolower( pathinfo( wp_parse_url( $face['url'], PHP_URL_PATH ), PATHINFO_EXTENSION ) ); $fmt = array( 'woff2' => 'woff2', 'woff' => 'woff', 'ttf' => 'truetype', 'otf' => 'opentype', )[ $ext ] ?? 'woff2'; $css .= '@font-face{font-family:"' . esc_attr( $font['family'] ) . '";src:url("' . esc_url( $face['url'] ) . '") format("' . $fmt . '");font-weight:' . (int) $face['weight'] . ';font-style:' . ( 'italic' === $face['style'] ? 'italic' : 'normal' ) . ';font-display:swap}'; } } if ( $css ) { wp_register_style( 'akram-custom-fonts', false, array(), AKRAM_VERSION ); wp_enqueue_style( 'akram-custom-fonts' ); wp_add_inline_style( 'akram-custom-fonts', $css ); } } }