diff --git a/src/wp-includes/class-wp-fonts-provider-google.php b/src/wp-includes/class-wp-fonts-provider-google.php new file mode 100644 index 0000000000000..de642eaa9668f --- /dev/null +++ b/src/wp-includes/class-wp-fonts-provider-google.php @@ -0,0 +1,149 @@ + 'https://fonts.gstatic.com', + 'crossorigin' => true, + ), + array( + 'href' => 'https://fonts.googleapis.com', + 'crossorigin' => false, + ), + ); + + /** + * The provider's root URL. + * + * @access protected + * @since 5.9.0 + * @var string + */ + protected $root_url = 'https://fonts.googleapis.com/css2'; + + /** + * An array of API parameters which will not be added to the @font-face. + * + * @access protected + * @since 5.9.0 + * @var array + */ + protected $api_params = array( + 'subset', + 'text', + 'effect', + ); + + /** + * Build the API URL from the query args. + * + * @access protected + * @since 5.9.0 + * @return string + */ + protected function build_api_url() { + $query_args = array( + 'family' => $this->params['font-family'], + 'display' => $this->params['font-display'], + ); + + if ( 'italic' === $this->params['font-style'] ) { + $query_args['family'] .= ':ital,wght@1,' . $this->params['font-weight']; + } else { + $query_args['family'] .= ':wght@' . $this->params['font-weight']; + } + + if ( ! empty( $this->params['subset'] ) ) { + $query_args['subset'] = implode( ',', (array) $this->params['subset'] ); + } + + if ( ! empty( $this->params['text'] ) ) { + $query_args['text'] = $this->params['text']; + } + + if ( ! empty( $this->params['effect'] ) ) { + $query_args['effect'] = implode( '|', (array) $this->params['effect'] ); + } + + return add_query_arg( $query_args, $this->root_url ); + } + + /** + * Get the CSS for the font. + * + * @access public + * @since 5.9.0 + * @return string + */ + public function get_css() { + $remote_url = $this->build_api_url(); + $transient_name = 'google_fonts_' . md5( $remote_url ); + $css = get_site_transient( $transient_name ); + + // Get remote response and cache the CSS if it hasn't been cached already. + if ( false === $css ) { + // Get the remote URL contents. + $response = wp_remote_get( + $remote_url, + array( + // Use a modern user-agent, to get woff2 files. + 'user-agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:73.0) Gecko/20100101 Firefox/73.0', + ) + ); + + // Early return if the request failed. + if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { + set_site_transient( $transient_name, '', 60 ); + return ''; + } + + // Get the response body. + $css = wp_remote_retrieve_body( $response ); + + // Cache the CSS for a month. + set_site_transient( $transient_name, $css, MONTH_IN_SECONDS ); + } + + // If there are additional props not included in the CSS provided by the API, add them to the final CSS. + $additional_props = array_diff( + array_keys( $this->params ), + array( 'font-family', 'font-style', 'font-weight', 'font-display', 'src', 'unicode-range' ) + ); + foreach ( $additional_props as $prop ) { + $css = str_replace( + '@font-face {', + '@font-face {' . $prop . ':' . $this->params[ $prop ] . ';', + $css + ); + } + + return $css; + } +} diff --git a/src/wp-includes/class-wp-fonts-provider-local.php b/src/wp-includes/class-wp-fonts-provider-local.php new file mode 100644 index 0000000000000..cbe885264de32 --- /dev/null +++ b/src/wp-includes/class-wp-fonts-provider-local.php @@ -0,0 +1,72 @@ +params['font-family'] ) ) { + return ''; + } + + $css = '@font-face{'; + foreach ( $this->params as $key => $value ) { + + // Skip the "preload" parameter. + if ( 'preload' === $key ) { + continue; + } + + // Compile the "src" parameter. + if ( 'src' === $key ) { + $src = "local({$this->params['font-family']})"; + foreach ( $value as $item ) { + $src .= ( 'data' === $item['format'] ) + ? ", url({$item['url']})" + : ", url('{$item['url']}') format('{$item['format']}')"; + } + $value = $src; + } + + // If font-variation-settings is an array, convert it to a string. + if ( 'font-variation-settings' === $key && is_array( $value ) ) { + $variations = array(); + foreach ( $value as $key => $val ) { + $variations[] = "$key $val"; + } + $value = implode( ', ', $variations ); + } + + if ( ! empty( $value ) ) { + $css .= "$key:$value;"; + } + } + $css .= '}'; + + return $css; + } +} diff --git a/src/wp-includes/class-wp-fonts-provider.php b/src/wp-includes/class-wp-fonts-provider.php new file mode 100644 index 0000000000000..c6e6a048164f6 --- /dev/null +++ b/src/wp-includes/class-wp-fonts-provider.php @@ -0,0 +1,257 @@ +id; + } + + /** + * Get the root URL for the provider. + * + * @access public + * @return string + */ + public function get_root_url() { + return $this->root_url; + } + + /** + * Get the array of URLs to preconnect to. + * + * @access public + * @return array + */ + public function get_preconnect_urls() { + return $this->preconnect_urls; + } + + /** + * Set the object's params. + * + * @access public + * @since 5.9.0 + * @param array $params The webfont's parameters. + * @return void + */ + public function set_params( $params ) { + // Default values. + $defaults = array( + 'font-weight' => '400', + 'font-style' => 'normal', + 'font-display' => 'fallback', + 'src' => array(), + ); + + // Merge defaults with passed params. + $params = wp_parse_args( $params, $defaults ); + + // Whitelisted params. + $whitelist = array_merge( $this->valid_font_face_properties, $this->api_params ); + + // Only allow whitelisted properties. + foreach ( $params as $key => $value ) { + if ( ! in_array( $key, $whitelist, true ) ) { + unset( $params[ $key ] ); + } + } + + // Order $src items to optimize for browser support. + if ( ! empty( $params['src'] ) ) { + $params['src'] = (array) $params['src']; + $src = array(); + $src_ordered = array(); + + foreach ( $params['src'] as $url ) { + // Add data URIs first. + if ( 0 === strpos( trim( $url ), 'data:' ) ) { + $src_ordered[] = array( + 'url' => $url, + 'format' => 'data', + ); + continue; + } + $format = pathinfo( $url, PATHINFO_EXTENSION ); + $src[ $format ] = $url; + } + + // Add woff2. + if ( ! empty( $src['woff2'] ) ) { + $src_ordered[] = array( + 'url' => $src['woff2'], + 'format' => 'woff2', + ); + } + + // Add woff. + if ( ! empty( $src['woff'] ) ) { + $src_ordered[] = array( + 'url' => $src['woff'], + 'format' => 'woff', + ); + } + + // Add ttf. + if ( ! empty( $src['ttf'] ) ) { + $src_ordered[] = array( + 'url' => $src['ttf'], + 'format' => 'truetype', + ); + } + + // Add eot. + if ( ! empty( $src['eot'] ) ) { + $src_ordered[] = array( + 'url' => $src['eot'], + 'format' => 'embedded-opentype', + ); + } + + // Add otf. + if ( ! empty( $src['otf'] ) ) { + $src_ordered[] = array( + 'url' => $src['otf'], + 'format' => 'opentype', + ); + } + $params['src'] = $src_ordered; + } + + // Only allow valid font-display values. + if ( + ! empty( $params['font-display'] ) && + ! in_array( $params['font-display'], array( 'auto', 'block', 'swap', 'fallback' ), true ) + ) { + $params['font-display'] = 'fallback'; + } + + // Only allow valid font-style values. + if ( + ! empty( $params['font-style'] ) && + ! in_array( $params['font-style'], array( 'normal', 'italic', 'oblique' ), true ) && + ! preg_match( '/^oblique\s+(\d+)%/', $params['font-style'], $matches ) + ) { + $params['font-style'] = 'normal'; + } + + // Only allow valid font-weight values. + if ( + ! empty( $params['font-weight'] ) && + ! in_array( $params['font-weight'], array( 'normal', 'bold', 'bolder', 'lighter', 'inherit' ), true ) && + ! preg_match( '/^(\d+)$/', $params['font-weight'], $matches ) && + ! preg_match( '/^(\d+)\s+(\d+)$/', $params['font-weight'], $matches ) + ) { + $params['font-weight'] = 'normal'; + } + + $this->params = $params; + } + + /** + * Get the object's params. + * + * @access public + * @since 5.9.0 + * @return array + */ + public function get_params() { + return $this->params; + } + + /** + * Get the CSS for the font. + * + * @access public + * @since 5.9.0 + * @return string + */ + abstract public function get_css(); +} diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index aa507bb0045ea..b30982283cd55 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -3251,6 +3251,7 @@ function wp_get_image_mime( $file ) { * @since 4.2.0 Support was added for GIMP (.xcf) files. * @since 4.9.2 Support was added for Flac (.flac) files. * @since 4.9.6 Support was added for AAC (.aac) files. + * @since 5.9.0 Support was added for webfont (.woff2, .woff, .ttf, .eot, .otf) files. * * @return string[] Array of mime types keyed by the file extension regex corresponding to those types. */ @@ -3315,6 +3316,12 @@ function wp_get_mime_types() { 'wma' => 'audio/x-ms-wma', 'wax' => 'audio/x-ms-wax', 'mka' => 'audio/x-matroska', + // Webfonts formats. + 'woff2' => 'font/woff2', + 'woff' => 'font/woff', + 'ttf' => 'font/ttf', + 'eot' => 'application/vnd.ms-fontobject', + 'otf' => 'application/x-font-opentype', // Misc application formats. 'rtf' => 'application/rtf', 'js' => 'application/javascript', diff --git a/src/wp-includes/functions.wp-webfonts.php b/src/wp-includes/functions.wp-webfonts.php new file mode 100644 index 0000000000000..fd8a888076704 --- /dev/null +++ b/src/wp-includes/functions.wp-webfonts.php @@ -0,0 +1,227 @@ +set_params( $params ); + // Get the CSS. + return $provider->get_css(); +} + +/** + * Add preconnect links to
for enqueued webfonts. + * + * @since 5.9.0 + * + * @param array $params The webfont parameters. + * + * @return void + */ +function _wp_webfont_add_preconnect_links( $params ) { + + $provider = isset( $params['provider'] ) ? $params['provider'] : new WP_Fonts_Provider_Local(); + $provider->set_params( $params ); + + // Store a static var to avoid adding the same preconnect links multiple times. + static $preconnect_urls_added_from_api = array(); + // Add preconnect links. + add_action( + 'wp_head', + function() use ( $provider, &$preconnect_urls_added_from_api ) { + + // Early exit if the provider has already added preconnect links. + if ( in_array( $provider->get_id(), $preconnect_urls_added_from_api ) ) { + return; + } + + // Add the preconnect links. + $preconnect_urls = $provider->get_preconnect_urls(); + foreach ( $preconnect_urls as $preconnection ) { + echo ' $value ) { + if ( 'href' === $key ) { + echo ' href="' . esc_url( $value ) . '"'; + } elseif ( true === $value || false === $value ) { + echo $value ? ' ' . esc_attr( $key ) : ''; + } else { + echo ' ' . esc_attr( $key ) . '="' . esc_attr( $value ) . '"'; + } + } + echo '>' . "\n"; + } + $preconnect_urls_added_from_api[] = $provider->get_id(); + } + ); +} diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 0a6ce2637c523..2a1083f998647 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -34,6 +34,12 @@ /** WordPress Styles Functions */ require ABSPATH . WPINC . '/functions.wp-styles.php'; +/** WordPress Webfonts Functions */ +require ABSPATH . WPINC . '/class-wp-fonts-provider.php'; +require ABSPATH . WPINC . '/class-wp-fonts-provider-local.php'; +require ABSPATH . WPINC . '/class-wp-fonts-provider-google.php'; +require ABSPATH . WPINC . '/functions.wp-webfonts.php'; + /** * Registers TinyMCE scripts. * @@ -2299,7 +2305,7 @@ function wp_enqueue_global_styles() { ! is_admin() ); - $stylesheet = null; + $stylesheet = null; $transient_name = 'global_styles_' . get_stylesheet(); if ( $can_use_cache ) { $cache = get_transient( $transient_name ); diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 14c1f080751a1..373e71958e949 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -218,6 +218,7 @@ public function test_registered_query_params() { ); if ( ! is_multisite() ) { $media_types[] = 'text'; + $media_types[] = 'font'; } $this->assertSameSets( $media_types, $data['endpoints'][0]['args']['media_type']['enum'] ); } diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 63dbf37f05c88..c8ad4200687da 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -2827,7 +2827,8 @@ mockedApiResponse.Schema = { "video", "text", "application", - "audio" + "audio", + "font" ], "required": false },