123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697 |
- <?php
- /**
- * Download webfonts locally.
- *
- * @package wptt/font-loader
- * @license https://opensource.org/licenses/MIT
- */
- if ( ! class_exists( 'WPTT_WebFont_Loader' ) ) {
- /**
- * Download webfonts locally.
- */
- class WPTT_WebFont_Loader {
- /**
- * The font-format.
- *
- * Use "woff" or "woff2".
- * This will change the user-agent user to make the request.
- *
- * @access protected
- * @since 1.0.0
- * @var string
- */
- protected $font_format = 'woff2';
- /**
- * The remote URL.
- *
- * @access protected
- * @since 1.1.0
- * @var string
- */
- protected $remote_url;
- /**
- * Base path.
- *
- * @access protected
- * @since 1.1.0
- * @var string
- */
- protected $base_path;
- /**
- * Base URL.
- *
- * @access protected
- * @since 1.1.0
- * @var string
- */
- protected $base_url;
- /**
- * Subfolder name.
- *
- * @access protected
- * @since 1.1.0
- * @var string
- */
- protected $subfolder_name;
- /**
- * The fonts folder.
- *
- * @access protected
- * @since 1.1.0
- * @var string
- */
- protected $fonts_folder;
- /**
- * The local stylesheet's path.
- *
- * @access protected
- * @since 1.1.0
- * @var string
- */
- protected $local_stylesheet_path;
- /**
- * The local stylesheet's URL.
- *
- * @access protected
- * @since 1.1.0
- * @var string
- */
- protected $local_stylesheet_url;
- /**
- * The remote CSS.
- *
- * @access protected
- * @since 1.1.0
- * @var string
- */
- protected $remote_styles;
- /**
- * The final CSS.
- *
- * @access protected
- * @since 1.1.0
- * @var string
- */
- protected $css;
- /**
- * Cleanup routine frequency.
- */
- const CLEANUP_FREQUENCY = 'monthly';
- /**
- * Constructor.
- *
- * Get a new instance of the object for a new URL.
- *
- * @access public
- * @since 1.1.0
- * @param string $url The remote URL.
- */
- public function __construct( $url = '' ) {
- $this->remote_url = $url;
- // Add a cleanup routine.
- $this->schedule_cleanup();
- add_action( 'delete_fonts_folder', array( $this, 'delete_fonts_folder' ) );
- }
- /**
- * Get the local URL which contains the styles.
- *
- * Fallback to the remote URL if we were unable to write the file locally.
- *
- * @access public
- * @since 1.1.0
- * @return string
- */
- public function get_url() {
- // Check if the local stylesheet exists.
- if ( $this->local_file_exists() ) {
- // Attempt to update the stylesheet. Return the local URL on success.
- if ( $this->write_stylesheet() ) {
- return $this->get_local_stylesheet_url();
- }
- }
- // If the local file exists, return its URL, with a fallback to the remote URL.
- return file_exists( $this->get_local_stylesheet_path() )
- ? $this->get_local_stylesheet_url()
- : $this->remote_url;
- }
- /**
- * Get the local stylesheet URL.
- *
- * @access public
- * @since 1.1.0
- * @return string
- */
- public function get_local_stylesheet_url() {
- if ( ! $this->local_stylesheet_url ) {
- $this->local_stylesheet_url = str_replace(
- $this->get_base_path(),
- $this->get_base_url(),
- $this->get_local_stylesheet_path()
- );
- }
- return $this->local_stylesheet_url;
- }
- /**
- * Get styles with fonts downloaded locally.
- *
- * @access public
- * @since 1.0.0
- * @return string
- */
- public function get_styles() {
- // If we already have the local file, return its contents.
- $local_stylesheet_contents = $this->get_local_stylesheet_contents();
- if ( $local_stylesheet_contents ) {
- return $local_stylesheet_contents;
- }
- // Get the remote URL contents.
- $this->remote_styles = $this->get_remote_url_contents();
- // Get an array of locally-hosted files.
- $files = $this->get_local_files_from_css();
- // Convert paths to URLs.
- foreach ( $files as $remote => $local ) {
- $files[ $remote ] = str_replace(
- $this->get_base_path(),
- $this->get_base_url(),
- $local
- );
- }
- $this->css = str_replace(
- array_keys( $files ),
- array_values( $files ),
- $this->remote_styles
- );
- $this->write_stylesheet();
- return $this->css;
- }
- /**
- * Get local stylesheet contents.
- *
- * @access public
- * @since 1.1.0
- * @return string|false Returns the remote URL contents.
- */
- public function get_local_stylesheet_contents() {
- $local_path = $this->get_local_stylesheet_path();
- // Check if the local stylesheet exists.
- if ( $this->local_file_exists() ) {
- // Attempt to update the stylesheet. Return false on fail.
- if ( ! $this->write_stylesheet() ) {
- return false;
- }
- }
- ob_start();
- include $local_path;
- return ob_get_clean();
- }
- /**
- * Get remote file contents.
- *
- * @access public
- * @since 1.0.0
- * @return string Returns the remote URL contents.
- */
- public function get_remote_url_contents() {
- /**
- * The user-agent we want to use.
- *
- * The default user-agent is the only one compatible with woff (not woff2)
- * which also supports unicode ranges.
- */
- $user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8';
- // Switch to a user-agent supporting woff2 if we don't need to support IE.
- if ( 'woff2' === $this->font_format ) {
- $user_agent = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:73.0) Gecko/20100101 Firefox/73.0';
- }
- // Get the response.
- $response = wp_remote_get( $this->remote_url, array( 'user-agent' => $user_agent ) );
- // Early exit if there was an error.
- if ( is_wp_error( $response ) ) {
- return '';
- }
- // Get the CSS from our response.
- $contents = wp_remote_retrieve_body( $response );
- return $contents;
- }
- /**
- * Download files mentioned in our CSS locally.
- *
- * @access public
- * @since 1.0.0
- * @return array Returns an array of remote URLs and their local counterparts.
- */
- public function get_local_files_from_css() {
- $font_files = $this->get_remote_files_from_css();
- $stored = get_site_option( 'downloaded_font_files', array() );
- $change = false; // If in the end this is true, we need to update the cache option.
- if ( ! defined( 'FS_CHMOD_DIR' ) ) {
- define( 'FS_CHMOD_DIR', ( 0755 & ~ umask() ) );
- }
- // If the fonts folder don't exist, create it.
- if ( ! file_exists( $this->get_fonts_folder() ) ) {
- $this->get_filesystem()->mkdir( $this->get_fonts_folder(), FS_CHMOD_DIR );
- }
- foreach ( $font_files as $font_family => $files ) {
- // The folder path for this font-family.
- $folder_path = $this->get_fonts_folder() . '/' . $font_family;
- // If the folder doesn't exist, create it.
- if ( ! file_exists( $folder_path ) ) {
- $this->get_filesystem()->mkdir( $folder_path, FS_CHMOD_DIR );
- }
- foreach ( $files as $url ) {
- // Get the filename.
- $filename = basename( wp_parse_url( $url, PHP_URL_PATH ) );
- $font_path = $folder_path . '/' . $filename;
- // Check if the file already exists.
- if ( file_exists( $font_path ) ) {
- // Skip if already cached.
- if ( isset( $stored[ $url ] ) ) {
- continue;
- }
- // Add file to the cache and change the $changed var to indicate we need to update the option.
- $stored[ $url ] = $font_path;
- $change = true;
- // Since the file exists we don't need to proceed with downloading it.
- continue;
- }
- /**
- * If we got this far, we need to download the file.
- */
- // require file.php if the download_url function doesn't exist.
- if ( ! function_exists( 'download_url' ) ) {
- require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' );
- }
- // Download file to temporary location.
- $tmp_path = download_url( $url );
- // Make sure there were no errors.
- if ( is_wp_error( $tmp_path ) ) {
- continue;
- }
- // Move temp file to final destination.
- $success = $this->get_filesystem()->move( $tmp_path, $font_path, true );
- if ( $success ) {
- $stored[ $url ] = $font_path;
- $change = true;
- }
- }
- }
- // If there were changes, update the option.
- if ( $change ) {
- // Cleanup the option and then save it.
- foreach ( $stored as $url => $path ) {
- if ( ! file_exists( $path ) ) {
- unset( $stored[ $url ] );
- }
- }
- update_site_option( 'downloaded_font_files', $stored );
- }
- return $stored;
- }
- /**
- * Get font files from the CSS.
- *
- * @access public
- * @since 1.0.0
- * @return array Returns an array of font-families and the font-files used.
- */
- public function get_remote_files_from_css() {
- $font_faces = explode( '@font-face', $this->remote_styles );
- $result = array();
- // Loop all our font-face declarations.
- foreach ( $font_faces as $font_face ) {
- // Make sure we only process styles inside this declaration.
- $style = explode( '}', $font_face )[0];
- // Sanity check.
- if ( false === strpos( $style, 'font-family' ) ) {
- continue;
- }
- // Get an array of our font-families.
- preg_match_all( '/font-family.*?\;/', $style, $matched_font_families );
- // Get an array of our font-files.
- preg_match_all( '/url\(.*?\)/i', $style, $matched_font_files );
- // Get the font-family name.
- $font_family = 'unknown';
- if ( isset( $matched_font_families[0] ) && isset( $matched_font_families[0][0] ) ) {
- $font_family = rtrim( ltrim( $matched_font_families[0][0], 'font-family:' ), ';' );
- $font_family = trim( str_replace( array( "'", ';' ), '', $font_family ) );
- $font_family = sanitize_key( strtolower( str_replace( ' ', '-', $font_family ) ) );
- }
- // Make sure the font-family is set in our array.
- if ( ! isset( $result[ $font_family ] ) ) {
- $result[ $font_family ] = array();
- }
- // Get files for this font-family and add them to the array.
- foreach ( $matched_font_files as $match ) {
- // Sanity check.
- if ( ! isset( $match[0] ) ) {
- continue;
- }
- // Add the file URL.
- $font_family_url = rtrim( ltrim( $match[0], 'url(' ), ')' );
- // Make sure to convert relative URLs to absolute.
- $font_family_url = $this->get_absolute_path( $font_family_url );
- $result[ $font_family ][] = $font_family_url;
- }
- // Make sure we have unique items.
- // We're using array_flip here instead of array_unique for improved performance.
- $result[ $font_family ] = array_flip( array_flip( $result[ $font_family ] ) );
- }
- return $result;
- }
- /**
- * Write the CSS to the filesystem.
- *
- * @access protected
- * @since 1.1.0
- * @return string|false Returns the absolute path of the file on success, or false on fail.
- */
- protected function write_stylesheet() {
- $file_path = $this->get_local_stylesheet_path();
- $filesystem = $this->get_filesystem();
- if ( ! defined( 'FS_CHMOD_DIR' ) ) {
- define( 'FS_CHMOD_DIR', ( 0755 & ~ umask() ) );
- }
- // If the folder doesn't exist, create it.
- if ( ! file_exists( $this->get_fonts_folder() ) ) {
- $this->get_filesystem()->mkdir( $this->get_fonts_folder(), FS_CHMOD_DIR );
- }
- // If the file doesn't exist, create it. Return false if it can not be created.
- if ( ! $filesystem->exists( $file_path ) && ! $filesystem->touch( $file_path ) ) {
- return false;
- }
- // If we got this far, we need to write the file.
- // Get the CSS.
- if ( ! $this->css ) {
- $this->get_styles();
- }
- // Put the contents in the file. Return false if that fails.
- if ( ! $filesystem->put_contents( $file_path, $this->css ) ) {
- return false;
- }
- return $file_path;
- }
- /**
- * Get the stylesheet path.
- *
- * @access public
- * @since 1.1.0
- * @return string
- */
- public function get_local_stylesheet_path() {
- if ( ! $this->local_stylesheet_path ) {
- $this->local_stylesheet_path = $this->get_fonts_folder() . '/' . $this->get_local_stylesheet_filename() . '.css';
- }
- return $this->local_stylesheet_path;
- }
- /**
- * Get the local stylesheet filename.
- *
- * This is a hash, generated from the site-URL, the wp-content path and the URL.
- * This way we can avoid issues with sites changing their URL, or the wp-content path etc.
- *
- * @access public
- * @since 1.1.0
- * @return string
- */
- public function get_local_stylesheet_filename() {
- return md5( $this->get_base_url() . $this->get_base_path() . $this->remote_url . $this->font_format );
- }
- /**
- * Set the font-format to be used.
- *
- * @access public
- * @since 1.0.0
- * @param string $format The format to be used. Use "woff" or "woff2".
- * @return void
- */
- public function set_font_format( $format = 'woff2' ) {
- $this->font_format = $format;
- }
- /**
- * Check if the local stylesheet exists.
- *
- * @access public
- * @since 1.1.0
- * @return bool
- */
- public function local_file_exists() {
- return ( ! file_exists( $this->get_local_stylesheet_path() ) );
- }
- /**
- * Get the base path.
- *
- * @access public
- * @since 1.1.0
- * @return string
- */
- public function get_base_path() {
- if ( ! $this->base_path ) {
- $this->base_path = apply_filters( 'wptt_get_local_fonts_base_path', $this->get_filesystem()->wp_content_dir() );
- }
- return $this->base_path;
- }
- /**
- * Get the base URL.
- *
- * @access public
- * @since 1.1.0
- * @return string
- */
- public function get_base_url() {
- if ( ! $this->base_url ) {
- $this->base_url = apply_filters( 'wptt_get_local_fonts_base_url', content_url() );
- }
- return $this->base_url;
- }
- /**
- * Get the subfolder name.
- *
- * @access public
- * @since 1.1.0
- * @return string
- */
- public function get_subfolder_name() {
- if ( ! $this->subfolder_name ) {
- $this->subfolder_name = apply_filters( 'wptt_get_local_fonts_subfolder_name', 'fonts' );
- }
- return $this->subfolder_name;
- }
- /**
- * Get the folder for fonts.
- *
- * @access public
- * @return string
- */
- public function get_fonts_folder() {
- if ( ! $this->fonts_folder ) {
- $this->fonts_folder = $this->get_base_path();
- if ( $this->get_subfolder_name() ) {
- $this->fonts_folder .= '/' . $this->get_subfolder_name();
- }
- }
- return $this->fonts_folder;
- }
- /**
- * Schedule a cleanup.
- *
- * Deletes the fonts files on a regular basis.
- * This way font files will get updated regularly,
- * and we avoid edge cases where unused files remain in the server.
- *
- * @access public
- * @since 1.1.0
- * @return void
- */
- public function schedule_cleanup() {
- if ( ! is_multisite() || ( is_multisite() && is_main_site() ) ) {
- if ( ! wp_next_scheduled( 'delete_fonts_folder' ) && ! wp_installing() ) {
- wp_schedule_event( time(), self::CLEANUP_FREQUENCY, 'delete_fonts_folder' );
- }
- }
- }
- /**
- * Delete the fonts folder.
- *
- * This runs as part of a cleanup routine.
- *
- * @access public
- * @since 1.1.0
- * @return bool
- */
- public function delete_fonts_folder() {
- return $this->get_filesystem()->delete( $this->get_fonts_folder(), true );
- }
- /**
- * Get the filesystem.
- *
- * @access protected
- * @since 1.0.0
- * @return \WP_Filesystem_Base
- */
- protected function get_filesystem() {
- global $wp_filesystem;
- // If the filesystem has not been instantiated yet, do it here.
- if ( ! $wp_filesystem ) {
- if ( ! function_exists( 'WP_Filesystem' ) ) {
- require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' );
- }
- WP_Filesystem();
- }
- return $wp_filesystem;
- }
- /**
- * Get an absolute URL from a relative URL.
- *
- * @access protected
- *
- * @param string $url The URL.
- *
- * @return string
- */
- protected function get_absolute_path( $url ) {
- // If dealing with a root-relative URL.
- if ( 0 === stripos( $url, '/' ) ) {
- $parsed_url = parse_url( $this->remote_url );
- return $parsed_url['scheme'] . '://' . $parsed_url['hostname'] . $url;
- }
- return $url;
- }
- }
- }
- if ( ! function_exists( 'wptt_get_webfont_styles' ) ) {
- /**
- * Get styles for a webfont.
- *
- * This will get the CSS from the remote API,
- * download any fonts it contains,
- * replace references to remote URLs with locally-downloaded assets,
- * and finally return the resulting CSS.
- *
- * @since 1.0.0
- *
- * @param string $url The URL of the remote webfont.
- * @param string $format The font-format. If you need to support IE, change this to "woff".
- *
- * @return string Returns the CSS.
- */
- function wptt_get_webfont_styles( $url, $format = 'woff2' ) {
- $font = new WPTT_WebFont_Loader( $url );
- $font->set_font_format( $format );
- return $font->get_styles();
- }
- }
- if ( ! function_exists( 'wptt_get_webfont_url' ) ) {
- /**
- * Get a stylesheet URL for a webfont.
- *
- * @since 1.1.0
- *
- * @param string $url The URL of the remote webfont.
- * @param string $format The font-format. If you need to support IE, change this to "woff".
- *
- * @return string Returns the CSS.
- */
- function wptt_get_webfont_url( $url, $format = 'woff2' ) {
- $font = new WPTT_WebFont_Loader( $url );
- $font->set_font_format( $format );
- return $font->get_url();
- }
- }
|