diff --git a/lib/compat/wordpress-5.9/class-wp-theme-json-resolver-gutenberg.php b/lib/compat/wordpress-5.9/class-wp-theme-json-resolver-gutenberg.php
index 4d6acd25122fc0..447edfeb037350 100644
--- a/lib/compat/wordpress-5.9/class-wp-theme-json-resolver-gutenberg.php
+++ b/lib/compat/wordpress-5.9/class-wp-theme-json-resolver-gutenberg.php
@@ -144,7 +144,11 @@ public static function get_theme_data( $deprecated = array() ) {
if ( null === self::$theme ) {
$theme_json_data = self::read_json_file( self::get_file_path_from_theme( 'theme.json' ) );
$theme_json_data = self::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) );
- self::$theme = new WP_Theme_JSON_Gutenberg( $theme_json_data );
+
+ // Add webfonts data.
+ $theme_json_data = gutenberg_add_registered_webfonts_to_theme_json( $theme_json_data );
+
+ self::$theme = new WP_Theme_JSON_Gutenberg( $theme_json_data );
if ( wp_get_theme()->parent() ) {
// Get parent theme.json.
diff --git a/lib/global-styles.php b/lib/global-styles.php
index 2a2e1925ee1e11..be822b4bd73394 100644
--- a/lib/global-styles.php
+++ b/lib/global-styles.php
@@ -288,6 +288,133 @@ function gutenberg_global_styles_include_support_for_wp_variables( $allow_css, $
return ! ! preg_match( '/^var\(--wp-[a-zA-Z0-9\-]+\)$/', trim( $parts[1] ) );
}
+/**
+ * Register webfonts defined in theme.json.
+ */
+function gutenberg_register_webfonts_from_theme_json() {
+ // Get settings from theme.json.
+ $theme_settings = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data()->get_settings();
+
+ // Bail out early if there are no settings for webfonts.
+ if ( empty( $theme_settings['typography'] ) || empty( $theme_settings['typography']['fontFamilies'] ) ) {
+ return;
+ }
+
+ $webfonts = array();
+
+ // Look for fontFamilies.
+ foreach ( $theme_settings['typography']['fontFamilies'] as $font_families ) {
+ foreach ( $font_families as $font_family ) {
+
+ // Skip if fontFace is not defined.
+ if ( empty( $font_family['fontFace'] ) ) {
+ continue;
+ }
+
+ $font_family['fontFace'] = (array) $font_family['fontFace'];
+
+ foreach ( $font_family['fontFace'] as $font_face ) {
+ // Check if webfonts have a "src" param, and if they do account for the use of "file:./".
+ if ( ! empty( $font_face['src'] ) ) {
+ $font_face['src'] = (array) $font_face['src'];
+
+ foreach ( $font_face['src'] as $src_key => $url ) {
+ // Tweak the URL to be relative to the theme root.
+ if ( 0 !== strpos( $url, 'file:./' ) ) {
+ continue;
+ }
+ $font_face['src'][ $src_key ] = get_theme_file_uri( str_replace( 'file:./', '', $url ) );
+ }
+ }
+
+ // Convert keys to kebab-case.
+ foreach ( $font_face as $property => $value ) {
+ $kebab_case = _wp_to_kebab_case( $property );
+ $font_face[ $kebab_case ] = $value;
+ if ( $kebab_case !== $property ) {
+ unset( $font_face[ $property ] );
+ }
+ }
+
+ $webfonts[] = $font_face;
+ }
+ }
+ }
+ wp_register_webfonts( $webfonts );
+}
+
+/**
+ * Add missing fonts data to the global styles.
+ *
+ * @param array $data The global styles.
+ *
+ * @return array The global styles with missing fonts data.
+ */
+function gutenberg_add_registered_webfonts_to_theme_json( $data ) {
+ $font_families_registered = wp_webfonts()->webfonts()->get_all_registered();
+ $font_families_from_theme = array();
+ if ( ! empty( $data['settings'] ) && ! empty( $data['settings']['typography'] ) && ! empty( $data['settings']['typography']['fontFamilies'] ) ) {
+ $font_families_from_theme = $data['settings']['typography']['fontFamilies'];
+ }
+
+ /**
+ * Helper to get an array of the font-families.
+ *
+ * @param array $families_data The font-families data.
+ *
+ * @return array The font-families array.
+ */
+ $get_families = function( $families_data ) {
+ $families = array();
+ foreach ( $families_data as $family ) {
+ if ( isset( $family['font-family'] ) ) {
+ $families[] = $family['font-family'];
+ } elseif ( isset( $family['fontFamily'] ) ) {
+ $families[] = $family['fontFamily'];
+ }
+ }
+
+ // Micro-optimization: Use array_flip( array_flip( $array ) )
+ // instead of array_unique( $array ) because it's faster.
+ // The result is the same.
+ return array_flip( array_flip( $families ) );
+ };
+
+ // Diff the arrays to find the missing fonts.
+ $to_add = array_diff(
+ $get_families( $font_families_registered ),
+ $get_families( $font_families_from_theme )
+ );
+
+ // Bail out early if there are no missing fonts.
+ if ( empty( $to_add ) ) {
+ return $data;
+ }
+
+ // Make sure the path to settings.typography.fontFamilies.theme exists
+ // before adding missing fonts.
+ if ( empty( $data['settings'] ) ) {
+ $data['settings'] = array();
+ }
+ if ( empty( $data['settings']['typography'] ) ) {
+ $data['settings']['typography'] = array();
+ }
+ if ( empty( $data['settings']['typography']['fontFamilies'] ) ) {
+ $data['settings']['typography']['fontFamilies'] = array();
+ }
+
+ // Add missing fonts.
+ foreach ( $to_add as $family ) {
+ $data['settings']['typography']['fontFamilies'][] = array(
+ 'fontFamily' => false !== strpos( $family, ' ' ) ? "'{$family}'" : $family,
+ 'name' => $family,
+ 'slug' => sanitize_title( $family ),
+ );
+ }
+
+ return $data;
+}
+
// The else clause can be removed when plugin support requires WordPress 5.8.0+.
if ( function_exists( 'get_block_editor_settings' ) ) {
add_filter( 'block_editor_settings_all', 'gutenberg_experimental_global_styles_settings', PHP_INT_MAX );
@@ -297,6 +424,7 @@ function gutenberg_global_styles_include_support_for_wp_variables( $allow_css, $
add_action( 'init', 'gutenberg_experimental_global_styles_register_user_cpt' );
add_action( 'wp_enqueue_scripts', 'gutenberg_experimental_global_styles_enqueue_assets' );
+add_action( 'wp_loaded', 'gutenberg_register_webfonts_from_theme_json' );
// kses actions&filters.
add_action( 'init', 'gutenberg_global_styles_kses_init' );
diff --git a/lib/load.php b/lib/load.php
index 1262c56f0dd304..ee180080f874e8 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -146,3 +146,31 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/block-supports/spacing.php';
require __DIR__ . '/block-supports/dimensions.php';
require __DIR__ . '/block-supports/duotone.php';
+
+// Webfonts API.
+if ( ! function_exists( 'wp_webfonts' ) ) {
+
+ /** WordPress Webfonts Classes & Functions */
+ require_once __DIR__ . '/webfonts-api/class-wp-webfonts-schema-validator.php';
+ require_once __DIR__ . '/webfonts-api/class-wp-webfonts-registry.php';
+ require_once __DIR__ . '/webfonts-api/class-wp-webfonts-provider-registry.php';
+ require_once __DIR__ . '/webfonts-api/class-wp-webfonts-controller.php';
+ require_once __DIR__ . '/webfonts.php';
+
+ /**
+ * Add webfonts mime types.
+ */
+ add_filter(
+ 'mime_types',
+ function( $mime_types ) {
+ // Webfonts formats.
+ $mime_types['woff2'] = 'font/woff2';
+ $mime_types['woff'] = 'font/woff';
+ $mime_types['ttf'] = 'font/ttf';
+ $mime_types['eot'] = 'application/vnd.ms-fontobject';
+ $mime_types['otf'] = 'application/x-font-opentype';
+
+ return $mime_types;
+ }
+ );
+}
diff --git a/lib/webfonts-api/class-wp-webfonts-controller.php b/lib/webfonts-api/class-wp-webfonts-controller.php
new file mode 100644
index 00000000000000..bd0038e9bcfdfa
--- /dev/null
+++ b/lib/webfonts-api/class-wp-webfonts-controller.php
@@ -0,0 +1,276 @@
+`
+ * (e.g. `'wp_enqueue_scripts'`), or print the resource ``
+ * (`'wp_resource_hints'` ). Then it interacts with the components
+ * in this API to process the event.
+ *
+ * @since 5.9.0
+ */
+class WP_Webfonts_Controller {
+
+ /**
+ * Instance of the webfont's registry.
+ *
+ * @since 5.9.0
+ *
+ * @var WP_Webfonts_Registry
+ */
+ private $webfonts_registry;
+
+ /**
+ * Instance of the providers' registry.
+ *
+ * @since 5.9.0
+ *
+ * @var WP_Webfonts_Provider_Registry
+ */
+ private $providers_registry;
+
+ /**
+ * Stylesheet handle.
+ *
+ * @since 5.9.0
+ *
+ * @var string
+ */
+ private $stylesheet_handle = '';
+
+ /**
+ * Create the controller.
+ *
+ * @since 5.9.0
+ *
+ * @param WP_Webfonts_Registry $webfonts_registry Instance of the webfonts' registry.
+ * @param WP_Webfonts_Provider_Registry $providers_registry Instance of the providers' registry.
+ */
+ public function __construct(
+ WP_Webfonts_Registry $webfonts_registry,
+ WP_Webfonts_Provider_Registry $providers_registry
+ ) {
+ $this->webfonts_registry = $webfonts_registry;
+ $this->providers_registry = $providers_registry;
+ }
+
+ /**
+ * Initializes the controller.
+ *
+ * @since 5.9.0
+ */
+ public function init() {
+ $this->providers_registry->init();
+
+ // Register callback to generate and enqueue styles.
+ if ( did_action( 'wp_enqueue_scripts' ) ) {
+ $this->stylesheet_handle = 'webfonts-footer';
+ $hook = 'wp_print_footer_scripts';
+ } else {
+ $this->stylesheet_handle = 'webfonts';
+ $hook = 'wp_enqueue_scripts';
+ }
+ add_action( $hook, array( $this, 'generate_and_enqueue_styles' ) );
+
+ // Enqueue webfonts in the block editor.
+ add_action( 'admin_init', array( $this, 'generate_and_enqueue_editor_styles' ) );
+
+ // Add resources hints.
+ add_filter( 'wp_resource_hints', array( $this, 'get_resource_hints' ), 10, 2 );
+ }
+
+ /**
+ * Gets the instance of the webfonts' registry.
+ *
+ * The Webfonts Registry handles the registration
+ * and in-memory storage of webfonts.
+ *
+ * @since 5.9.0
+ *
+ * @return WP_Webfonts_Registry
+ */
+ public function webfonts() {
+ return $this->webfonts_registry;
+ }
+
+ /**
+ * Gets the instance of the providers' registry.
+ *
+ * @see WP_Webfonts_Provider_Registry for more information
+ * on the available methods for use.
+ *
+ * @since 5.9.0
+ *
+ * @return WP_Webfonts_Provider_Registry
+ */
+ public function providers() {
+ return $this->providers_registry;
+ }
+
+ /**
+ * Generate and enqueue webfonts styles.
+ *
+ * @since 5.9.0
+ */
+ public function generate_and_enqueue_styles() {
+ // Generate the styles.
+ $styles = $this->generate_styles();
+
+ // Bail out if there are no styles to enqueue.
+ if ( '' === $styles ) {
+ return;
+ }
+
+ // Enqueue the stylesheet.
+ wp_register_style( $this->stylesheet_handle, '' );
+ wp_enqueue_style( $this->stylesheet_handle );
+
+ // Add the styles to the stylesheet.
+ wp_add_inline_style( $this->stylesheet_handle, $styles );
+ }
+
+ /**
+ * Generate and enqueue editor styles.
+ *
+ * @since 5.9.0
+ */
+ public function generate_and_enqueue_editor_styles() {
+ // Generate the styles.
+ $styles = $this->generate_styles();
+
+ // Bail out if there are no styles to enqueue.
+ if ( '' === $styles ) {
+ return;
+ }
+
+ wp_add_inline_style( 'wp-block-library', $styles );
+ }
+
+ /**
+ * Generate styles for webfonts.
+ *
+ * By default (due to privacy concerns), this API will not do remote requests to
+ * external webfont services nor generate `@font-face` styles for these remote
+ * providers. The filter `'has_remote_webfonts_request_permission'` is provided
+ * to grant permission to do the remote request.
+ *
+ * @since 5.9.0
+ *
+ * @return string $styles Generated styles.
+ */
+ private function generate_styles() {
+ $styles = '';
+ $providers = $this->providers_registry->get_all_registered();
+
+ /*
+ * Loop through each of the providers to get the CSS for their respective webfonts
+ * to incrementally generate the collective styles for all of them.
+ */
+ foreach ( $providers as $provider_id => $provider ) {
+ $registered_webfonts = $this->webfonts_registry->get_by_provider( $provider_id );
+
+ // If there are no registered webfonts for this provider, skip it.
+ if ( empty( $registered_webfonts ) ) {
+ continue;
+ }
+
+ /*
+ * Skip fetching from a remote fonts service if the user has not
+ * consented to the remote request.
+ */
+ if (
+ 'local' !== $provider_id &&
+ /**
+ * Allows permission to be set for doing remote requests
+ * to a webfont service provider.
+ *
+ * By default, the Webfonts API will not make remote requests
+ * due to privacy concerns.
+ *
+ * @since 5.9.0
+ *
+ * @param bool $has_permission Permission to do the remote request.
+ * Default false.
+ * @param string $provider_id Provider's ID, e.g. 'local', to identify
+ * the remote webfonts service provider.
+ */
+ true !== apply_filters( 'has_remote_webfonts_request_permission', false, $provider_id )
+ ) {
+ continue;
+ }
+
+ /*
+ * Process the webfonts by first passing them to the provider via `set_webfonts()`
+ * and then getting the CSS from the provider.
+ */
+ $provider->set_webfonts( $registered_webfonts );
+ $styles .= $provider->get_css();
+ }
+
+ return $styles;
+ }
+
+ /**
+ * Gets the resource hints.
+ *
+ * Callback hooked to the filter `'wp_resource_hints'`. Generation
+ * and rendering of the resource `` is handled where that filter
+ * fires. This method adds the resource link attributes to pass back
+ * to that handler.
+ *
+ * @since 5.9.0
+ *
+ * @param array $urls {
+ * Array of resources and their attributes, or URLs to print for resource hints.
+ *
+ * @type array|string ...$0 {
+ * Array of resource attributes, or a URL string.
+ *
+ * @type string $href URL to include in resource hints. Required.
+ * @type string $as How the browser should treat the resource
+ * (`script`, `style`, `image`, `document`, etc).
+ * @type string $crossorigin Indicates the CORS policy of the specified resource.
+ * @type float $pr Expected probability that the resource hint will be used.
+ * @type string $type Type of the resource (`text/html`, `text/css`, etc).
+ * }
+ * }
+ * @param string $relation_type The relation type the URLs are printed for,
+ * e.g. 'preconnect' or 'prerender'.
+ * @return array URLs to print for resource hints.
+ */
+ public function get_resource_hints( $urls, $relation_type ) {
+ foreach ( $this->providers_registry->get_all_registered() as $provider ) {
+ foreach ( $provider->get_resource_hints() as $relation => $relation_hints ) {
+ if ( $relation !== $relation_type ) {
+ continue;
+ }
+ // Append this provider's resource hints to the end of the given `$urls` array.
+ array_push( $urls, ...$relation_hints );
+ }
+ }
+
+ return $urls;
+ }
+}
diff --git a/lib/webfonts-api/class-wp-webfonts-provider-registry.php b/lib/webfonts-api/class-wp-webfonts-provider-registry.php
new file mode 100644
index 00000000000000..462f1f590e5572
--- /dev/null
+++ b/lib/webfonts-api/class-wp-webfonts-provider-registry.php
@@ -0,0 +1,132 @@
+ @type WP_Webfonts_Provider Provider instance.
+ *
+ * @since 5.9.0
+ *
+ * @var WP_Webfonts_Provider[]
+ */
+ private $registered = array();
+
+ /**
+ * Gets all registered providers.
+ *
+ * Return an array of providers, each keyed by their unique
+ * ID (i.e. the `$id` property in the provider's object) with
+ * an instance of the provider (object):
+ * ID => provider instance
+ *
+ * @since 5.9.0
+ *
+ * @return WP_Webfonts_Provider[] All registered providers,
+ * each keyed by their unique ID.
+ */
+ public function get_all_registered() {
+ return $this->registered;
+ }
+
+ /**
+ * Initializes the registry.
+ *
+ * @since 5.9.0
+ */
+ public function init() {
+ $this->register_core_providers();
+ }
+
+ /**
+ * Registers the core providers.
+ *
+ * Loads each bundled provider's file into memory and
+ * then registers it for use with the API.
+ *
+ * @since 5.9.0
+ */
+ private function register_core_providers() {
+ // Load the abstract class into memory.
+ require_once __DIR__ . '/providers/class-wp-webfonts-provider.php';
+
+ // Register the Local Provider.
+ require_once __DIR__ . '/providers/class-wp-webfonts-local-provider.php';
+ $this->register( WP_Webfonts_Local_Provider::class );
+ }
+
+ /**
+ * Registers a webfont provider.
+ *
+ * The provider will be registered by its unique ID
+ * (via `WP_Webfonts_Provider::get_id()`) and instance of
+ * the provider (object):
+ * ID => provider instance
+ *
+ * Once registered, provider is ready for use within the API.
+ *
+ * @since 5.9.0
+ *
+ * @param string $classname The provider's class name.
+ * The class should be a child of `WP_Webfonts_Provider`.
+ * See {@see WP_Webfonts_Provider}.
+ *
+ * @return bool True when registered. False when provider does not exist.
+ */
+ public function register( $classname ) {
+ /*
+ * Bail out if the class does not exist in memory (its file
+ * has to be loaded into memory before registration) or the
+ * `class` itself is not a child that extends `WP_Webfonts_Provider`
+ * (the parent class of a provider).
+ */
+ if (
+ ! class_exists( $classname ) ||
+ ! is_subclass_of( $classname, 'WP_Webfonts_Provider' )
+ ) {
+ return false;
+ }
+
+ /*
+ * Create an instance of the provider.
+ * This API uses one instance of each provider.
+ */
+ $provider = new $classname;
+ $id = $provider->get_id();
+
+ // Store the provider's instance by its unique provider ID.
+ if ( ! isset( $this->registered[ $id ] ) ) {
+ $this->registered[ $id ] = $provider;
+ }
+
+ return true;
+ }
+}
diff --git a/lib/webfonts-api/class-wp-webfonts-registry.php b/lib/webfonts-api/class-wp-webfonts-registry.php
new file mode 100644
index 00000000000000..d197431f76b1b5
--- /dev/null
+++ b/lib/webfonts-api/class-wp-webfonts-registry.php
@@ -0,0 +1,245 @@
+ @type array Webfont.
+ *
+ * @since 5.9.0
+ *
+ * @var array[]
+ */
+ private $registered = array();
+
+ /**
+ * Registration keys per provider.
+ *
+ * Provides a O(1) lookup when querying by provider.
+ *
+ * @since 5.9.0
+ *
+ * @var array[]
+ */
+ private $registry_by_provider = array();
+
+ /**
+ * Schema validator.
+ *
+ * @since 5.9.0
+ *
+ * @var WP_Webfonts_Schema_Validator
+ */
+ private $validator;
+
+ /**
+ * Creates the registry.
+ *
+ * @since 5.9.0
+ *
+ * @param WP_Webfonts_Schema_Validator $validator Instance of the validator.
+ */
+ public function __construct( WP_Webfonts_Schema_Validator $validator ) {
+ $this->validator = $validator;
+ }
+
+ /**
+ * Gets all registered webfonts.
+ *
+ * @since 5.9.0
+ *
+ * @return array[] Registered webfonts each keyed by font-family.font-style.font-weight.
+ */
+ public function get_all_registered() {
+ return $this->registered;
+ }
+
+ /**
+ * Gets the registered webfonts for the given provider.
+ *
+ * @since 5.9.0
+ *
+ * @param string $provider_id Provider ID to fetch.
+ * @return array[] Registered webfonts.
+ */
+ public function get_by_provider( $provider_id ) {
+ if ( ! isset( $this->registry_by_provider[ $provider_id ] ) ) {
+ return array();
+ }
+
+ $webfonts = array();
+ foreach ( $this->registry_by_provider[ $provider_id ] as $registration_key ) {
+ // Skip if not registered.
+ if ( ! isset( $this->registered[ $registration_key ] ) ) {
+ continue;
+ }
+
+ $webfonts[ $registration_key ] = $this->registered[ $registration_key ];
+ }
+
+ return $webfonts;
+ }
+
+ /**
+ * Registers the given webfont if its schema is valid.
+ *
+ * @since 5.9.0
+ *
+ * @param array $webfont {
+ * Webfont definition.
+ *
+ * @type string $provider The provider ID (e.g. 'local').
+ * @type string $font_family The @font-face font-family property.
+ * @type string $font_weight The @font-face font-weight property.
+ * The font-weight can be a single value, or a range.
+ * If a single value, then the font-weight can either be
+ * a numeric value (400, 700, etc), or a word value
+ * (normal, bold, etc).
+ * If a range, then the font-weight can be a numeric range
+ * using 2 values, separated by a space ('100 700').
+ * @type string $font_style The @font-face font-style property.
+ * The font-style can be a valid CSS value (normal, italic etc).
+ * @type string $font_display The @font-face font-display property.
+ * Accepted values: 'auto', 'block', 'fallback', 'swap'.
+ * @type array|string $src The @font-face src property.
+ * The src can be a single URL, or an array of URLs.
+ * @type string $font_stretch The @font-face font-stretch property.
+ * @type string $font_variant The @font-face font-variant property.
+ * @type string $font_feature_settings The @font-face font-feature-settings property.
+ * @type string $font_variation_settings The @font-face font-variation-settings property.
+ * @type string $line_gap_override The @font-face line-gap-override property.
+ * @type string $size_adjust The @font-face size-adjust property.
+ * @type string $unicode_range The @font-face unicode-range property.
+ * @type string $ascend_override The @font-face ascend-override property.
+ * @type string $descend_override The @font-face descend-override property.
+ * }
+ * @return string Registration key.
+ */
+ public function register( array $webfont ) {
+ $webfont = $this->convert_to_kebab_case( $webfont );
+
+ // Validate schema.
+ if ( ! $this->validator->is_valid_schema( $webfont ) ) {
+ return '';
+ }
+
+ $webfont = $this->validator->set_valid_properties( $webfont );
+
+ // Add to registry.
+ $registration_key = $this->generate_registration_key( $webfont );
+ if ( isset( $this->registered[ $registration_key ] ) ) {
+ return $registration_key;
+ }
+
+ $this->registered[ $registration_key ] = $webfont;
+ $this->store_for_query_by( $webfont, $registration_key );
+
+ return $registration_key;
+ }
+
+ /**
+ * Convert snake_case keys into kebab-case.
+ *
+ * @since 5.9.0
+ *
+ * @param array $webfont Webfont definition.
+ * @return array Webfont with kebab-case properties (keys).
+ */
+ private function convert_to_kebab_case( array $webfont ) {
+ $kebab_case = array();
+ foreach ( $webfont as $key => $value ) {
+ $converted_key = str_replace( '_', '-', $key );
+ $kebab_case[ $converted_key ] = $value;
+ }
+
+ return $kebab_case;
+ }
+
+ /**
+ * Store the webfont for query by request.
+ *
+ * This container provides a performant way to quickly query webfonts by
+ * provider. The registration keys are stored for O(1) lookup.
+ *
+ * @since 5.9.0
+ *
+ * @param array $webfont Webfont definition.
+ * @param string $registration_key Webfont's registration key.
+ */
+ private function store_for_query_by( array $webfont, $registration_key ) {
+ $provider = $webfont['provider'];
+
+ // Initialize the array if it does not exist.
+ if ( ! isset( $this->registry_by_provider[ $provider ] ) ) {
+ $this->registry_by_provider[ $provider ] = array();
+ }
+
+ $this->registry_by_provider[ $provider ][] = $registration_key;
+ }
+
+ /**
+ * Generates the registration key.
+ *
+ * Format: font-family.font-style.font-weight
+ * For example: `'open-sans.normal.400'`.
+ *
+ * @since 5.9.0
+ *
+ * @param array $webfont Webfont definition.
+ * @return string Registration key.
+ */
+ private function generate_registration_key( array $webfont ) {
+ return sprintf(
+ '%s.%s.%s',
+ $this->convert_font_family_into_key( $webfont['font-family'] ),
+ trim( $webfont['font-style'] ),
+ trim( $webfont['font-weight'] )
+ );
+ }
+
+ /**
+ * Converts the given font family into a key.
+ *
+ * For example: 'Open Sans' becomes 'open-sans'.
+ *
+ * @since 5.9.0
+ *
+ * @param string $font_family Font family to convert into a key.
+ * @return string Font-family as a key.
+ */
+ private function convert_font_family_into_key( $font_family ) {
+ return sanitize_title( $font_family );
+ }
+}
diff --git a/lib/webfonts-api/class-wp-webfonts-schema-validator.php b/lib/webfonts-api/class-wp-webfonts-schema-validator.php
new file mode 100644
index 00000000000000..85ec5966a6a4dc
--- /dev/null
+++ b/lib/webfonts-api/class-wp-webfonts-schema-validator.php
@@ -0,0 +1,335 @@
+ 'local',
+ 'font-family' => '',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ );
+
+ /**
+ * Webfont being validated.
+ *
+ * Set as a property for performance.
+ *
+ * @var array
+ */
+ private $webfont = array();
+
+ /**
+ * Checks if the given webfont schema is valid.
+ *
+ * @since 5.9.0
+ *
+ * @param array $webfont Webfont to validate.
+ * @return bool True when valid. False when invalid.
+ */
+ public function is_valid_schema( array $webfont ) {
+ $is_valid = (
+ $this->is_valid_provider( $webfont ) &&
+ $this->is_valid_font_family( $webfont )
+ );
+
+ if ( ! $is_valid ) {
+ return false;
+ }
+
+ if ( 'local' === $webfont['provider'] || array_key_exists( 'src', $webfont ) ) {
+ $is_valid = $this->is_src_valid( $webfont );
+ }
+
+ return $is_valid;
+ }
+
+ /**
+ * Checks if the provider is valid.
+ *
+ * @since 5.9.0
+ *
+ * @param array $webfont Webfont to validate.
+ * @return bool True if valid. False if invalid.
+ */
+ private function is_valid_provider( array $webfont ) {
+ if (
+ empty( $webfont['provider'] ) ||
+ ! is_string( $webfont['provider'] )
+ ) {
+ trigger_error( __( 'Webfont provider must be a non-empty string.', 'gutenberg' ) );
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if the font family is valid.
+ *
+ * @since 5.9.0
+ *
+ * @param array $webfont Webfont to validate.
+ * @return bool True when valid. False when invalid.
+ */
+ private function is_valid_font_family( array $webfont ) {
+ if (
+ empty( $webfont['font-family'] ) ||
+ ! is_string( $webfont['font-family'] )
+ ) {
+ trigger_error( __( 'Webfont font family must be a non-empty string.', 'gutenberg' ) );
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if the "src" value is valid.
+ *
+ * @since 5.9.0
+ *
+ * @param array $webfont Webfont to validate.
+ * @return bool True if valid. False if invalid.
+ */
+ private function is_src_valid( array $webfont ) {
+ if (
+ empty( $webfont['src'] ) ||
+ (
+ ! is_string( $webfont['src'] ) && ! is_array( $webfont['src'] )
+ )
+ ) {
+ trigger_error( __( 'Webfont src must be a non-empty string or an array of strings.', 'gutenberg' ) );
+
+ return false;
+ }
+
+ foreach ( (array) $webfont['src'] as $src ) {
+ if ( empty( $src ) || ! is_string( $src ) ) {
+ trigger_error( __( 'Each webfont src must be a non-empty string.', 'gutenberg' ) );
+
+ return false;
+ }
+
+ if ( ! $this->is_src_value_valid( $src ) ) {
+ trigger_error( __( 'Webfont src must be a valid URL or a data URI.', 'gutenberg' ) );
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if the given `src` value is valid.
+ *
+ * @since 5.9.0
+ *
+ * @param string $src Source to validate.
+ * @return bool True when valid. False when invalid.
+ */
+ private function is_src_value_valid( $src ) {
+ if (
+ // Validate data URLs.
+ preg_match( '/^data:.+;base64/', $src ) ||
+ // Validate URLs.
+ filter_var( $src, FILTER_VALIDATE_URL ) ||
+ // Check if it's a URL starting with "//" (omitted protocol).
+ 0 === strpos( $src, '//' )
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Sets valid properties.
+ *
+ * @since 5.9.0
+ *
+ * @param array $webfont Webfont definition.
+ * @return array Updated webfont.
+ */
+ public function set_valid_properties( array $webfont ) {
+ $this->webfont = array_merge( $this->basic_schema, $webfont );
+
+ $this->set_valid_font_face_property();
+ $this->set_valid_font_style();
+ $this->set_valid_font_weight();
+ $this->set_valid_font_display();
+
+ $webfont = $this->webfont;
+ $this->webfont = array(); // Reset property.
+
+ return $webfont;
+ }
+
+ /**
+ * Checks if the CSS property is valid for @font-face.
+ *
+ * @since 5.9.0
+ */
+ private function set_valid_font_face_property() {
+ foreach ( array_keys( $this->webfont ) as $property ) {
+ /*
+ * Skip valid configuration parameters
+ * (these are configuring the webfont but are not @font-face properties).
+ */
+ if ( 'provider' === $property || 'provider-params' === $property ) {
+ continue;
+ }
+
+ if ( ! in_array( $property, $this->font_face_properties, true ) ) {
+ unset( $this->webfont[ $property ] );
+ }
+ }
+ }
+
+ /**
+ * Sets a default font-style if invalid.
+ *
+ * @since 5.9.0
+ */
+ private function set_valid_font_style() {
+ // If empty or not a string, trigger an error and then set the default value.
+ if (
+ empty( $this->webfont['font-style'] ) ||
+ ! is_string( $this->webfont['font-style'] )
+ ) {
+ trigger_error( __( 'Webfont font style must be a non-empty string.', 'gutenberg' ) );
+
+ } elseif ( // Bail out if the font-style is a valid value.
+ in_array( $this->webfont['font-style'], self::VALID_FONT_STYLE, true ) ||
+ preg_match( '/^oblique\s+(\d+)%/', $this->webfont['font-style'] )
+ ) {
+ return;
+ }
+
+ $this->webfont['font-style'] = 'normal';
+ }
+
+ /**
+ * Sets a default font-weight if invalid.
+ *
+ * @since 5.9.0
+ */
+ private function set_valid_font_weight() {
+ // If empty or not a string, trigger an error and then set the default value.
+ if (
+ empty( $this->webfont['font-weight'] ) ||
+ ! is_string( $this->webfont['font-weight'] )
+ ) {
+ trigger_error( __( 'Webfont font weight must be a non-empty string.', 'gutenberg' ) );
+
+ } elseif ( // Bail out if the font-weight is a valid value.
+ // Check if value is a single font-weight, formatted as a number.
+ in_array( $this->webfont['font-weight'], self::VALID_FONT_WEIGHT, true ) ||
+ // Check if value is a single font-weight, formatted as a number.
+ preg_match( '/^(\d+)$/', $this->webfont['font-weight'], $matches ) ||
+ // Check if value is a range of font-weights, formatted as a number range.
+ preg_match( '/^(\d+)\s+(\d+)$/', $this->webfont['font-weight'], $matches )
+ ) {
+ return;
+ }
+
+ // Not valid. Set the default value.
+ $this->webfont['font-weight'] = '400';
+ }
+
+ /**
+ * Sets a default font-display if invalid.
+ *
+ * @since 5.9.0
+ */
+ private function set_valid_font_display() {
+ if (
+ empty( $this->webfont['font-display'] ) ||
+ ! in_array( $this->webfont['font-display'], self::VALID_FONT_DISPLAY, true )
+ ) {
+ $this->webfont['font-display'] = 'fallback';
+ }
+ }
+}
diff --git a/lib/webfonts-api/providers/class-wp-webfonts-local-provider.php b/lib/webfonts-api/providers/class-wp-webfonts-local-provider.php
new file mode 100644
index 00000000000000..6030cbffbe3265
--- /dev/null
+++ b/lib/webfonts-api/providers/class-wp-webfonts-local-provider.php
@@ -0,0 +1,263 @@
+
+ * array(
+ * 'source-serif-pro.normal.200 900' => array(
+ * 'provider' => 'local',
+ * 'font_family' => 'Source Serif Pro',
+ * 'font_weight' => '200 900',
+ * 'font_style' => 'normal',
+ * 'src' => 'https://example.com/wp-content/themes/twentytwentytwo/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2' ),
+ * ),
+ * 'source-serif-pro.italic.400 900' => array(
+ * 'provider' => 'local',
+ * 'font_family' => 'Source Serif Pro',
+ * 'font_weight' => '200 900',
+ * 'font_style' => 'italic',
+ * 'src' => 'https://example.com/wp-content/themes/twentytwentytwo/assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2' ),
+ * ),
+ * )
+ *
+ *
+ * the following `@font-face` styles are generated and returned:
+ *
+ *
+ * @font-face{
+ * font-family:"Source Serif Pro";
+ * font-style:normal;
+ * font-weight:200 900;
+ * font-stretch:normal;
+ * src:local("Source Serif Pro"), url('/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2') format('woff2');
+ * }
+ * @font-face{
+ * font-family:"Source Serif Pro";
+ * font-style:italic;
+ * font-weight:200 900;
+ * font-stretch:normal;
+ * src:local("Source Serif Pro"), url('/assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2') format('woff2');
+ * }
+ *
+ *
+ * @since 5.9.0
+ *
+ * @return string The `@font-face` CSS.
+ */
+ public function get_css() {
+ $css = '';
+
+ foreach ( $this->webfonts as $webfont ) {
+ // Order the webfont's `src` items to optimize for browser support.
+ $webfont = $this->order_src( $webfont );
+
+ // Build the @font-face CSS for this webfont.
+ $css .= '@font-face{' . $this->build_font_face_css( $webfont ) . '}';
+ }
+
+ return $css;
+ }
+
+ /**
+ * Order `src` items to optimize for browser support.
+ *
+ * @since 5.9.0
+ *
+ * @param array $webfont Webfont to process.
+ * @return array
+ */
+ private function order_src( array $webfont ) {
+ if ( ! is_array( $webfont['src'] ) ) {
+ $webfont['src'] = (array) $webfont['src'];
+ }
+
+ $src = array();
+ $src_ordered = array();
+
+ foreach ( $webfont['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',
+ );
+ }
+ $webfont['src'] = $src_ordered;
+
+ return $webfont;
+ }
+
+ /**
+ * Builds the font-family's CSS.
+ *
+ * @since 5.9.0
+ *
+ * @param array $webfont Webfont to process.
+ * @return string This font-family's CSS.
+ */
+ private function build_font_face_css( array $webfont ) {
+ $css = '';
+
+ // Wrap font-family in quotes if it contains spaces.
+ if (
+ false !== strpos( $webfont['font-family'], ' ' ) &&
+ false === strpos( $webfont['font-family'], '"' ) &&
+ false === strpos( $webfont['font-family'], "'" )
+ ) {
+ $webfont['font-family'] = '"' . $webfont['font-family'] . '"';
+ }
+
+ foreach ( $webfont as $key => $value ) {
+
+ // Skip "provider".
+ if ( 'provider' === $key ) {
+ continue;
+ }
+
+ // Compile the "src" parameter.
+ if ( 'src' === $key ) {
+ $value = $this->compile_src( $webfont['font-family'], $value );
+ }
+
+ // If font-variation-settings is an array, convert it to a string.
+ if ( 'font-variation-settings' === $key && is_array( $value ) ) {
+ $value = $this->compile_variations( $value );
+ }
+
+ if ( ! empty( $value ) ) {
+ $css .= "$key:$value;";
+ }
+ }
+
+ return $css;
+ }
+
+ /**
+ * Compiles the `src` into valid CSS.
+ *
+ * @since 5.9.0
+ *
+ * @param string $font_family Font family.
+ * @param array $value Value to process.
+ * @return string The CSS.
+ */
+ private function compile_src( $font_family, array $value ) {
+ $src = "local($font_family)";
+
+ foreach ( $value as $item ) {
+
+ if ( 0 === strpos( $item['url'], get_site_url() ) ) {
+ $item['url'] = wp_make_link_relative( $item['url'] );
+ }
+
+ $src .= ( 'data' === $item['format'] )
+ ? ", url({$item['url']})"
+ : ", url('{$item['url']}') format('{$item['format']}')";
+ }
+ return $src;
+ }
+
+ /**
+ * Compiles the font variation settings.
+ *
+ * @since 5.9.0
+ *
+ * @param array $font_variation_settings Array of font variation settings.
+ * @return string The CSS.
+ */
+ private function compile_variations( array $font_variation_settings ) {
+ $variations = '';
+
+ foreach ( $font_variation_settings as $key => $value ) {
+ $variations .= "$key $value";
+ }
+
+ return $variations;
+ }
+}
diff --git a/lib/webfonts-api/providers/class-wp-webfonts-provider.php b/lib/webfonts-api/providers/class-wp-webfonts-provider.php
new file mode 100644
index 00000000000000..f6a66b4d73812c
--- /dev/null
+++ b/lib/webfonts-api/providers/class-wp-webfonts-provider.php
@@ -0,0 +1,135 @@
+` in the `
+ * wp_register_webfonts(
+ * array(
+ * array(
+ * 'provider' => 'local',
+ * 'font_family' => 'Source Serif Pro',
+ * 'font_weight' => '200 900',
+ * 'font_style' => 'normal',
+ * 'src' => get_theme_file_uri( 'assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2' ),
+ * ),
+ * array(
+ * 'provider' => 'local',
+ * 'font_family' => 'Source Serif Pro',
+ * 'font_weight' => '200 900',
+ * 'font_style' => 'italic',
+ * 'src' => get_theme_file_uri( 'assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2' ),
+ * ),
+ * )
+ * );
+ *
+ *
+ * When requesting from the remote Google Fonts API service provider:
+ *
+ * wp_register_webfonts(
+ * array(
+ * array(
+ * 'provider' => 'google',
+ * 'font_family' => 'Source Serif Pro',
+ * 'font_weight' => '200 900',
+ * 'font_style' => 'normal',
+ * ),
+ * array(
+ * 'provider' => 'google',
+ * 'font_family' => 'Source Serif Pro',
+ * 'font_weight' => '200 900',
+ * 'font_style' => 'italic',
+ * ),
+ * )
+ * );
+ *
+ *
+ * @since 5.9.0
+ *
+ * @param array $webfonts Webfonts to be registered.
+ * This contains an array of webfonts to be registered.
+ * Each webfont is an array.
+ * See {@see WP_Webfonts_Registry::register()} for a list of
+ * supported arguments for each webfont.
+ */
+function wp_register_webfonts( array $webfonts = array() ) {
+ foreach ( $webfonts as $webfont ) {
+ wp_webfonts()->webfonts()->register( $webfont );
+ }
+}
+
+/**
+ * Registers a single webfont.
+ *
+ * Example of how to register Source Serif Pro font with font-weight range of 200-900:
+ *
+ * If the font file is contained within the theme:
+ * ```
+ * wp_register_webfont(
+ * array(
+ * 'provider' => 'local',
+ * 'font_family' => 'Source Serif Pro',
+ * 'font_weight' => '200 900',
+ * 'font_style' => 'normal',
+ * 'src' => get_theme_file_uri( 'assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2' ),
+ * )
+ * );
+ * ```
+ *
+ * When requesting from the remote Google Fonts API service provider:
+ * ```
+ * wp_register_webfonts(
+ * array(
+ * 'provider' => 'google',
+ * 'font_family' => 'Source Serif Pro',
+ * 'font_weight' => '200 900',
+ * 'font_style' => 'normal',
+ * )
+ * );
+ * ```
+ *
+ * @since 5.9.0
+ *
+ * @param array $webfont Webfont to be registered.
+ * See {@see WP_Webfonts_Registry::register()} for a list of supported arguments.
+ * @return string Registration key.
+ */
+function wp_register_webfont( array $webfont ) {
+ return wp_webfonts()->webfonts()->register( $webfont );
+}
+
+/**
+ * Registers a custom font service provider.
+ *
+ * A webfont provider contains the business logic for how to
+ * interact with a remote font service and how to generate
+ * the `@font-face` styles for that remote service.
+ *
+ * See the `WP_Webfonts_Google_Provider` for inspiration.
+ *
+ * How to register a custom font service provider:
+ * 1. Load its class file into memory before registration.
+ * 2. Pass the class' name to this function.
+ *
+ * For example, for a class named `My_Custom_Font_Service_Provider`:
+ * ```
+ * wp_register_webfont_provider( My_Custom_Font_Service_Provider::class );
+ * ```
+ *
+ * @since 5.9.0
+ *
+ * @param string $classname The provider's class name.
+ * The class should be a child of `WP_Webfonts_Provider`.
+ * See {@see WP_Webfonts_Provider}.
+ *
+ * @return bool True when registered. False when provider does not exist.
+ */
+function wp_register_webfont_provider( $classname ) {
+ return wp_webfonts()->providers()->register( $classname );
+}
+
+/**
+ * Gets all registered providers.
+ *
+ * Return an array of providers, each keyed by their unique
+ * ID (i.e. the `$id` property in the provider's object) with
+ * an instance of the provider (object):
+ * ID => provider instance
+ *
+ * Each provider contains the business logic for how to
+ * process its specific font service (i.e. local or remote)
+ * and how to generate the `@font-face` styles for its service.
+ *
+ * @since 5.9.0
+ *
+ * @return WP_Webfonts_Provider[] All registered providers,
+ * each keyed by their unique ID.
+ */
+function wp_get_webfont_providers() {
+ return wp_webfonts()->providers()->get_all_registered();
+}
diff --git a/phpunit/webfonts-api/class-wp-webfonts-controller-test.php b/phpunit/webfonts-api/class-wp-webfonts-controller-test.php
new file mode 100644
index 00000000000000..2f9dad2b9db463
--- /dev/null
+++ b/phpunit/webfonts-api/class-wp-webfonts-controller-test.php
@@ -0,0 +1,211 @@
+webfont_registry_mock = $this->getMockBuilder( 'WP_Webfonts_Registry' )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->provider_registry_mock = $this->getMockBuilder( 'WP_Webfonts_Provider_Registry' )
+ ->getMock();
+ $this->controller = new WP_Webfonts_Controller(
+ $this->webfont_registry_mock,
+ $this->provider_registry_mock
+ );
+ }
+
+ /**
+ * @covers WP_Webfonts_Controller::init
+ *
+ * @dataProvider data_init
+ *
+ * @param string $hook Expected hook name.
+ * @param bool $did_action Whether the action fired or not.
+ */
+ public function test_init( $hook, $did_action ) {
+ $this->provider_registry_mock
+ ->expects( $this->once() )
+ ->method( 'init' );
+
+ if ( $did_action ) {
+ do_action( 'wp_enqueue_scripts' );
+ }
+
+ $this->controller->init();
+
+ $this->assertSame(
+ 10,
+ has_action( $hook, array( $this->controller, 'generate_and_enqueue_styles' ) )
+ );
+ $this->assertSame(
+ 10,
+ has_action( 'admin_init', array( $this->controller, 'generate_and_enqueue_editor_styles' ) )
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_init() {
+ return array(
+ 'did_action fired' => array(
+ 'hook' => 'wp_print_footer_scripts',
+ 'did_action' => true,
+ ),
+ 'did_action did not fire' => array(
+ 'hook' => 'wp_enqueue_scripts',
+ 'did_action' => false,
+ ),
+ );
+ }
+
+ /**
+ * By default, the Webfonts API should not request webfonts from
+ * a remote provider. Test the permissions logic works as expected.
+ *
+ * @covers WP_Webfonts_Controller::generate_and_enqueue_styles
+ *
+ * @dataProvider data_generate_and_enqueue_editor_styles
+ *
+ * @param string $stylesheet_handle Handle for the registered stylesheet.
+ */
+ public function test_generate_and_enqueue_styles_default( $stylesheet_handle ) {
+ /*
+ * Set the stylesheet_handle property.
+ * This is set in WP_Webfonts_Controller::init(); however, init is not part
+ * of this test (as it has its own test).
+ */
+ $property = new ReflectionProperty( $this->controller, 'stylesheet_handle' );
+ $property->setAccessible( true );
+ $property->setValue( $this->controller, $stylesheet_handle );
+
+ // Set up the provider mock.
+ $provider = $this->getMockBuilder( 'WP_Webfonts_Local_Provider' )->getMock();
+ $providers = array(
+ 'local' => $provider,
+ );
+ $this->provider_registry_mock
+ ->expects( $this->once() )
+ ->method( 'get_all_registered' )
+ ->willReturn( $providers );
+ // The Local Fonts provider should never be called.
+ $provider
+ ->expects( $this->never() )
+ ->method( 'set_webfonts' );
+
+ // Fire the method being tested.
+ $this->controller->generate_and_enqueue_styles();
+ $this->expectOutputString( '' );
+ wp_print_styles( $stylesheet_handle );
+ }
+
+ /**
+ * @covers WP_Webfonts_Controller::generate_and_enqueue_styles
+ * @covers WP_Webfonts_Controller::generate_and_enqueue_editor_styles
+ *
+ * @dataProvider data_generate_and_enqueue_editor_styles
+ *
+ * @param string $stylesheet_handle Handle for the registered stylesheet.
+ */
+ public function test_generate_and_enqueue_styles_with_permission( $stylesheet_handle ) {
+ add_filter( 'has_remote_webfonts_request_permission', '__return_true' );
+
+ /*
+ * Set the stylesheet_handle property.
+ * This is set in WP_Webfonts_Controller::init(); however, init is not part
+ * of this test (as it has its own test).
+ */
+ $property = new ReflectionProperty( $this->controller, 'stylesheet_handle' );
+ $property->setAccessible( true );
+ $property->setValue( $this->controller, $stylesheet_handle );
+
+ // Set up the provider mock.
+ $provider = new My_Custom_Webfonts_Provider_Mock();
+ $providers = array(
+ 'my-custom-provider' => $provider,
+ );
+ $this->provider_registry_mock
+ ->expects( $this->once() )
+ ->method( 'get_all_registered' )
+ ->willReturn( $providers );
+
+ // Set up the webfonts registry mock.
+ $webfonts = array(
+ 'source-serif-pro.normal.200 900' => array(
+ 'provider' => 'my-custom-provider',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ ),
+ 'source-serif-pro.italic.200 900' => array(
+ 'provider' => 'my-custom-provider',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'italic',
+ 'font-weight' => '200 900',
+ ),
+ );
+ $this->webfont_registry_mock
+ ->expects( $this->once() )
+ ->method( 'get_by_provider' )
+ ->with( $this->equalTo( 'my-custom-provider' ) )
+ ->willReturn( $webfonts );
+
+ // Fire the method being tested.
+ $this->controller->generate_and_enqueue_styles();
+
+ /*
+ * As this method adds an inline style, the test needs to print it.
+ * Print the webfont styles and test the output matches expectation.
+ */
+ $expected = "\n";
+ $this->expectOutputString( $expected );
+ wp_print_styles( $stylesheet_handle );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_generate_and_enqueue_editor_styles() {
+ return array(
+ 'for wp_enqueue_scripts' => array( 'webfonts' ),
+ 'for wp_print_footer_scripts' => array( 'webfonts-footer' ),
+ );
+ }
+
+ /**
+ * @covers WP_Webfonts_Controller::webfonts
+ */
+ public function test_webfonts() {
+ $this->assertSame( $this->webfont_registry_mock, $this->controller->webfonts() );
+ }
+
+ /**
+ * @covers WP_Webfonts_Controller::providers
+ */
+ public function test_providers() {
+ $this->assertSame( $this->provider_registry_mock, $this->controller->providers() );
+ }
+}
diff --git a/phpunit/webfonts-api/class-wp-webfonts-provider-registry-test.php b/phpunit/webfonts-api/class-wp-webfonts-provider-registry-test.php
new file mode 100644
index 00000000000000..7db9d4a1a4df81
--- /dev/null
+++ b/phpunit/webfonts-api/class-wp-webfonts-provider-registry-test.php
@@ -0,0 +1,82 @@
+assertSame( array(), $registry->get_all_registered() );
+ }
+
+ /**
+ * @covers WP_Webfonts_Provider_Registry::register
+ * @covers WP_Webfonts_Provider_Registry::get_all_registered
+ */
+ public function test_register_with_invalid_class() {
+ $registry = new WP_Webfonts_Provider_Registry();
+ $registry->register( 'DoesNotExist' );
+
+ $this->assertSame( array(), $registry->get_all_registered() );
+ }
+
+ /**
+ * @covers WP_Webfonts_Provider_Registry::register
+ * @covers WP_Webfonts_Provider_Registry::get_all_registered
+ */
+ public function test_register_with_valid_class() {
+ $registry = new WP_Webfonts_Provider_Registry();
+ $registry->register( My_Custom_Webfonts_Provider_Mock::class );
+
+ $providers = $registry->get_all_registered();
+
+ $this->assertIsArray( $providers );
+ $this->assertCount( 1, $providers );
+ $this->assertArrayHasKey( 'my-custom-provider', $providers );
+ $this->assertInstanceOf( 'My_Custom_Webfonts_Provider_Mock', $providers['my-custom-provider'] );
+ }
+
+ /**
+ * @covers WP_Webfonts_Provider_Registry::init
+ * @covers WP_Webfonts_Provider_Registry::get_all_registered
+ */
+ public function test_init() {
+ $registry = new WP_Webfonts_Provider_Registry();
+ // Register the core providers.
+ $registry->init();
+
+ $providers = $registry->get_all_registered();
+
+ $expected = array( 'local' );
+ $this->assertSame( $expected, array_keys( $providers ) );
+ $this->assertInstanceOf( 'WP_Webfonts_Local_Provider', $providers['local'] );
+ }
+
+ /**
+ * @covers WP_Webfonts_Provider_Registry::register
+ * @covers WP_Webfonts_Provider_Registry::get_all_registered
+ */
+ public function test_register_with_core_providers() {
+ $registry = new WP_Webfonts_Provider_Registry();
+ // Register the core providers.
+ $registry->init();
+ // Register a custom provider.
+ $registry->register( My_Custom_Webfonts_Provider_Mock::class );
+
+ $providers = $registry->get_all_registered();
+
+ $expected = array( 'local', 'my-custom-provider' );
+ $this->assertSame( $expected, array_keys( $providers ) );
+ }
+}
diff --git a/phpunit/webfonts-api/class-wp-webfonts-registry-test.php b/phpunit/webfonts-api/class-wp-webfonts-registry-test.php
new file mode 100644
index 00000000000000..4e489fb753cf12
--- /dev/null
+++ b/phpunit/webfonts-api/class-wp-webfonts-registry-test.php
@@ -0,0 +1,341 @@
+validator_mock = $this->getMockBuilder( 'WP_Webfonts_Schema_Validator' )->getMock();
+
+ $this->registry = new WP_Webfonts_Registry( $this->validator_mock );
+ }
+
+ /**
+ * @covers WP_Webfonts_Registry::get_all_registered
+ */
+ public function test_get_all_registered() {
+ $expected = array(
+ 'open-sans.normal.400' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Open Sans',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ ),
+ 'roboto.normal.900' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Robot',
+ 'font-style' => 'normal',
+ 'font-weight' => '900',
+ 'font-display' => 'fallback',
+ ),
+ );
+
+ /*
+ * Set the registry property.
+ * This is set in WP_Webfonts_Registry::register(), which not part of this test.
+ */
+ $property = new ReflectionProperty( $this->registry, 'registered' );
+ $property->setAccessible( true );
+ $property->setValue( $this->registry, $expected );
+
+ $this->assertSame( $expected, $this->registry->get_all_registered() );
+ }
+
+ /**
+ * @covers WP_Webfonts_Registry::register
+ *
+ * @dataProvider data_register_with_invalid_schema
+ *
+ * @param array $webfont Webfonts input.
+ */
+ public function test_register_with_invalid_schema( array $webfont ) {
+ $this->validator_mock
+ ->expects( $this->once() )
+ ->method( 'is_valid_schema' )
+ ->willReturn( false );
+ $this->validator_mock
+ ->expects( $this->never() )
+ ->method( 'set_valid_properties' );
+
+ $this->assertSame( '', $this->registry->register( $webfont ) );
+ }
+
+ /**
+ * Data Provider.
+ *
+ * @return array
+ */
+ public function data_register_with_invalid_schema() {
+ return array(
+ 'empty array - no schema' => array(
+ array(),
+ ),
+ 'provider: not defined' => array(
+ array(
+ 'font_family' => 'Some Font',
+ 'font_style' => 'normal',
+ 'font_weight' => '400',
+ ),
+ ),
+ 'provider: empty string' => array(
+ array(
+ 'provider' => '',
+ 'font_family' => 'Some Font',
+ 'font_style' => 'normal',
+ 'font_weight' => '400',
+ ),
+ ),
+ 'provider: not a string' => array(
+ array(
+ 'provider' => null,
+ 'font_family' => 'Some Font',
+ 'font_style' => 'normal',
+ 'font_weight' => '400',
+ ),
+ ),
+ 'font family: not defined' => array(
+ array(
+ 'provider' => 'local',
+ 'font_style' => 'normal',
+ 'font_weight' => '400',
+ ),
+ ),
+ 'font-family: not defined' => array(
+ array(
+ 'provider' => 'some-provider',
+ 'font_style' => 'normal',
+ 'font_weight' => '400',
+ ),
+ ),
+ 'font-family: empty string' => array(
+ array(
+ 'provider' => 'some-provider',
+ 'font_family' => '',
+ 'font_style' => 'normal',
+ 'font_weight' => '400',
+ ),
+ ),
+ 'font-family: not a string' => array(
+ array(
+ 'provider' => 'some-provider',
+ 'font_family' => null,
+ 'font_style' => 'normal',
+ 'font_weight' => '400',
+ ),
+ ),
+ );
+ }
+
+ /**
+ * @covers WP_Webfonts_Registry::register
+ *
+ * @dataProvider data_register_with_valid_schema
+ *
+ * @param array $webfont Webfont input.
+ * @param array $validated_webfont Webfont after being processed by the validator.
+ * @param string $expected Expected return value.
+ */
+ public function test_register_with_valid_schema( array $webfont, array $validated_webfont, $expected ) {
+ $this->validator_mock
+ ->expects( $this->once() )
+ ->method( 'is_valid_schema' )
+ ->willReturn( true );
+ $this->validator_mock
+ ->expects( $this->once() )
+ ->method( 'set_valid_properties' )
+ ->willReturn( $validated_webfont );
+
+ $this->assertSame( $expected, $this->registry->register( $webfont ) );
+ }
+
+ /**
+ * Data Provider.
+ *
+ * @return array
+ */
+ public function data_register_with_valid_schema() {
+ return array(
+ 'valid schema without font-display' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font_family' => 'Roboto',
+ 'font_style' => 'normal',
+ 'font_weight' => 'normal',
+ ),
+ 'validated_webfont' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Roboto',
+ 'font-style' => 'normal',
+ 'font-weight' => 'normal',
+ 'font-display' => 'fallback',
+ ),
+ 'expected' => 'roboto.normal.normal',
+ ),
+ 'valid schema with src' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font_family' => 'Source Serif Pro',
+ 'font_style' => 'normal',
+ 'font_weight' => '200 900',
+ 'font_stretch' => 'normal',
+ 'src' => 'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2',
+ ),
+ 'validated_webfont' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'font-display' => 'fallback',
+ 'font-stretch' => 'normal',
+ 'src' => 'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2',
+ ),
+ 'expected' => 'source-serif-pro.normal.200 900',
+ ),
+ );
+ }
+
+ /**
+ * @covers WP_Webfonts_Registry::get_by_provider
+ */
+ public function test_get_by_provider_when_does_not_exist() {
+ /*
+ * Set the `registry_by_provider` property.
+ * This is set in WP_Webfonts_Registry::register(), which not part of this test.
+ */
+ $property = new ReflectionProperty( $this->registry, 'registry_by_provider' );
+ $property->setAccessible( true );
+ $property->setValue( $this->registry, array( 'local' ) );
+
+ $this->assertSame( array(), $this->registry->get_by_provider( 'my-custom-provider' ) );
+ }
+
+ /**
+ * As there are many moving parts to getting by provider, this test is an integration
+ * test that does not mock.
+ *
+ * @covers WP_Webfonts_Registry::get_by_provider
+ * @covers WP_Webfonts_Registry::register
+ *
+ * @dataProvider data_get_by_provider_integrated
+ *
+ * @param array $webfonts Given webfont to register.
+ * @param string $provider_id Provider ID to query.
+ * @param array $expected Expected return value.
+ */
+ public function test_get_by_provider_integrated( array $webfonts, $provider_id, array $expected ) {
+ $registry = new WP_Webfonts_Registry( new WP_Webfonts_Schema_Validator() );
+
+ foreach ( $webfonts as $webfont ) {
+ $registry->register( $webfont );
+ }
+
+ $this->assertSame( $expected, $registry->get_by_provider( $provider_id ) );
+ }
+
+ /**
+ * Data Provider.
+ *
+ * @return array
+ */
+ public function data_get_by_provider_integrated() {
+ return array(
+ 'no webfonts for requested provider' => array(
+ 'webfonts' => array(
+ array(
+ 'provider' => 'local',
+ 'font_family' => 'Lato',
+ 'font_style' => 'italic',
+ 'font_weight' => '400',
+ ),
+ ),
+ 'provider_id' => 'local',
+ 'expected' => array(),
+ ),
+ 'with one provider' => array(
+ 'webfonts' => array(
+ array(
+ 'provider' => 'local',
+ 'font_family' => 'Lato',
+ 'font_style' => 'italic',
+ 'font_weight' => '400',
+ 'src' => 'https://example.com/lato-400i.woff2',
+ ),
+ array(
+ 'provider' => 'local',
+ 'font_family' => 'Roboto',
+ 'font_style' => 'normal',
+ 'font_weight' => '900',
+ 'src' => 'https://example.com/lato-900.woff2',
+ ),
+ ),
+ 'provider_id' => 'local',
+ 'expected' => array(
+ 'lato.italic.400' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Lato',
+ 'font-style' => 'italic',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ 'src' => 'https://example.com/lato-400i.woff2',
+ ),
+ 'roboto.normal.900' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Roboto',
+ 'font-style' => 'normal',
+ 'font-weight' => '900',
+ 'font-display' => 'fallback',
+ 'src' => 'https://example.com/lato-900.woff2',
+ ),
+ ),
+ ),
+ 'with multiple providers' => array(
+ 'webfonts' => array(
+ array(
+ 'provider' => 'example',
+ 'font_family' => 'Open Sans',
+ 'font_style' => 'normal',
+ 'font_weight' => '400',
+ ),
+ array(
+ 'provider' => 'local',
+ 'font_family' => 'Source Serif Pro',
+ 'font_style' => 'normal',
+ 'font_weight' => '200 900',
+ 'font_stretch' => 'normal',
+ 'src' => 'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2',
+ ),
+ array(
+ 'provider' => 'example',
+ 'font_family' => 'Roboto',
+ 'font_style' => 'normal',
+ 'font_weight' => '900',
+ ),
+ ),
+ 'provider_id' => 'local',
+ 'expected' => array(
+ 'source-serif-pro.normal.200 900' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'font-display' => 'fallback',
+ 'font-stretch' => 'normal',
+ 'src' => 'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2',
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/phpunit/webfonts-api/class-wp-webfonts-schema-validator-test.php b/phpunit/webfonts-api/class-wp-webfonts-schema-validator-test.php
new file mode 100644
index 00000000000000..7b7d23dc564ba2
--- /dev/null
+++ b/phpunit/webfonts-api/class-wp-webfonts-schema-validator-test.php
@@ -0,0 +1,463 @@
+assertTrue( self::$validator->is_valid_schema( $webfont ) );
+ }
+
+ /**
+ * Data Provider.
+ *
+ * @return array
+ */
+ public function data_is_valid_schema_with_valid() {
+ return array(
+ 'basic schema' => array(
+ array(
+ 'provider' => 'local',
+ 'font-family' => 'Open Sans',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'src' => 'https://example.com/open-sans-400.woff2',
+ ),
+ ),
+ );
+ }
+
+ /**
+ * @covers WP_Webfonts_Schema_Validator::is_valid_schema
+ *
+ * @dataProvider data_is_valid_schema_with_invalid
+ *
+ * @param array $webfont Webfont input.
+ */
+ public function test_is_valid_schema_with_invalid( array $webfont ) {
+ $this->assertFalse( self::$validator->is_valid_schema( $webfont ) );
+ }
+
+ /**
+ * Data Provider.
+ *
+ * @return array
+ */
+ public function data_is_valid_schema_with_invalid() {
+ return array(
+ 'empty array - no schema' => array(
+ 'webfont' => array(),
+ 'expected_message' => 'Webfont provider must be a non-empty string.',
+ ),
+ 'provider: not defined' => array(
+ 'webfont' => array(
+ 'font-family' => 'Some Font',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ ),
+ 'expected_message' => 'Webfont provider must be a non-empty string.',
+ ),
+ 'provider: empty string' => array(
+ 'webfont' => array(
+ 'provider' => '',
+ 'font-family' => 'Some Font',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ ),
+ 'expected_message' => 'Webfont provider must be a non-empty string.',
+ ),
+ 'provider: not a string' => array(
+ 'webfont' => array(
+ 'provider' => null,
+ 'font-family' => 'Some Font',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ ),
+ 'expected_message' => 'Webfont provider must be a non-empty string.',
+ ),
+ 'font-family: not defined' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ ),
+ 'expected_message' => 'Webfont font family must be a non-empty string.',
+ ),
+ 'font-family: empty string' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font-family' => '',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ ),
+ 'expected_message' => 'Webfont font family must be a non-empty string.',
+ ),
+ 'font-family: not a string' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font-family' => null,
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ ),
+ 'expected_message' => 'Webfont font family must be a non-empty string.',
+ ),
+ 'src: not defined' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ ),
+ 'expected_message' => 'Webfont src must be a non-empty string or an array of strings.',
+ ),
+ 'src: type is invalid' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'src' => null,
+ ),
+ 'expected_message' => 'Webfont src must be a non-empty string or an array of strings.',
+ ),
+ 'src: individual src is not a string' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'src' => array( null ),
+ ),
+ 'expected_message' => 'Each webfont src must be a non-empty string.',
+ ),
+ 'src: invalid url' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'src' => '/assets/fonts/font.woff2',
+ ),
+ 'expected_message' => 'Webfont src must be a valid URL or a data URI.',
+ ),
+ 'src: invalid data uri' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'src' => 'data:text/plain',
+ ),
+ 'expected_message' => 'Webfont src must be a valid URL or a data URI.',
+ ),
+ );
+ }
+
+ /**
+ * @covers WP_Webfonts_Schema_Validator::set_valid_properties
+ *
+ * @dataProvider data_set_valid_properties_with_valid_input
+ *
+ * @param array $webfont Webfont input.
+ * @param array $expected Expected updated webfont.
+ */
+ public function test_set_valid_properties_with_valid_input( array $webfont, array $expected ) {
+ $this->assertSame( $expected, self::$validator->set_valid_properties( $webfont ) );
+ }
+
+ /**
+ * Data Provider.
+ *
+ * @return array
+ */
+ public function data_set_valid_properties_with_valid_input() {
+ return array(
+ 'basic schema' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Open Sans',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ ),
+ 'expected' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Open Sans',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ ),
+ ),
+ 'basic schema in opposite order' => array(
+ 'webfont' => array(
+ 'font-weight' => '400',
+ 'font-style' => 'normal',
+ 'font-family' => 'Open Sans',
+ 'provider' => 'local',
+ ),
+ 'expected' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Open Sans',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ ),
+ ),
+ 'src: with protocol' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'src' => 'http://example.org/assets/fonts/SourceSerif4Variable-Roman.ttf.woff2',
+ ),
+ 'expected' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'font-display' => 'fallback',
+ 'src' => 'http://example.org/assets/fonts/SourceSerif4Variable-Roman.ttf.woff2',
+ ),
+ ),
+ 'src: without protocol' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'src' => '//example.org/assets/fonts/SourceSerif4Variable-Roman.ttf.woff2',
+ ),
+ 'expected' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'font-display' => 'fallback',
+ 'src' => '//example.org/assets/fonts/SourceSerif4Variable-Roman.ttf.woff2',
+ ),
+ ),
+ 'src: data:' => array(
+ 'webfont' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'src' => 'data:font/opentype; base64, SGVsbG8sIFdvcmxkIQ==',
+ ),
+ 'expected' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'font-display' => 'fallback',
+ 'src' => 'data:font/opentype; base64, SGVsbG8sIFdvcmxkIQ==',
+ ),
+ ),
+ 'with font-stretch' => array(
+ 'webfont' => array(
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'font-stretch' => 'normal',
+ 'src' => 'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2',
+ 'provider' => 'local',
+ ),
+ 'expected' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'font-display' => 'fallback',
+ 'font-stretch' => 'normal',
+ 'src' => 'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2',
+ ),
+ ),
+ );
+ }
+
+ /**
+ * @covers WP_Webfonts_Schema_Validator::set_valid_properties
+ *
+ * @dataProvider data_set_valid_properties_with_invalid_input
+ *
+ * @param array $webfont Webfont input.
+ * @param array $expected Expected updated webfont.
+ */
+ public function test_set_valid_properties_with_invalid_input( array $webfont, array $expected ) {
+ $this->assertSame( $expected, self::$validator->set_valid_properties( $webfont ) );
+ }
+
+ /**
+ * Data Provider.
+ *
+ * @return array
+ */
+ public function data_set_valid_properties_with_invalid_input() {
+ return array(
+ 'empty array - no schema' => array(
+ 'webfont' => array(),
+ 'expected' => array(
+ 'provider' => 'local',
+ 'font-family' => '',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ ),
+ ),
+ 'with invalid @font-face property' => array(
+ 'webfont' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'invalid' => 'should remove it',
+ ),
+ 'expected' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ ),
+ ),
+ 'font-style: invalid value' => array(
+ 'webfont' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-style' => 'invalid',
+ ),
+ 'expected' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ ),
+ ),
+ 'font-weight: invalid value' => array(
+ 'webfont' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-weight' => 'invalid',
+ ),
+ 'expected' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ ),
+ ),
+ 'font-display: invalid value' => array(
+ 'webfont' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-display' => 'invalid',
+ ),
+ 'expected' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ ),
+ ),
+ );
+ }
+
+ /**
+ * @covers WP_Webfonts_Schema_Validator::set_valid_properties
+ *
+ * @dataProvider data_set_valid_properties_with_invalid_and_error
+ *
+ * @param array $webfont Webfont input.
+ * @param array $expected Expected updated webfont.
+ */
+ public function test_set_valid_properties_with_invalid_and_error( array $webfont, array $expected ) {
+ $this->assertSame( $expected, self::$validator->set_valid_properties( $webfont ) );
+ }
+
+ /**
+ * Data Provider.
+ *
+ * @return array
+ */
+ public function data_set_valid_properties_with_invalid_and_error() {
+ return array(
+ 'font-style: empty value' => array(
+ 'webfont' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-style' => '',
+ ),
+ 'expected' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ ),
+ 'expected_message' => 'Webfont font style must be a non-empty string.',
+ ),
+ 'font-style: not a string' => array(
+ 'webfont' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-style' => null,
+ ),
+ 'expected' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ ),
+ 'expected_message' => 'Webfont font style must be a non-empty string.',
+ ),
+ 'font-weight: empty value' => array(
+ 'webfont' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-weight' => '',
+ ),
+ 'expected' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ ),
+ 'expected_message' => 'Webfont font weight must be a non-empty string.',
+ ),
+ 'font-weight: not a string' => array(
+ 'webfont' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-weight' => true,
+ ),
+ 'expected' => array(
+ 'provider' => 'some-provider',
+ 'font-family' => 'Some Font',
+ 'font-style' => 'normal',
+ 'font-weight' => '400',
+ 'font-display' => 'fallback',
+ ),
+ 'expected_message' => 'Webfont font weight must be a non-empty string.',
+ ),
+ );
+ }
+}
diff --git a/phpunit/webfonts-api/mocks/class-my-custom-webfonts-provider-mock.php b/phpunit/webfonts-api/mocks/class-my-custom-webfonts-provider-mock.php
new file mode 100644
index 00000000000000..ac38f822a3ad03
--- /dev/null
+++ b/phpunit/webfonts-api/mocks/class-my-custom-webfonts-provider-mock.php
@@ -0,0 +1,35 @@
+ array(
+ array(
+ 'href' => 'https://fonts.my-custom-api.com',
+ 'crossorigin' => 'anonymous',
+ ),
+ ),
+ );
+
+ public function get_css() {
+ return "
+ @font-face{
+ font-family: 'Source Serif Pro';
+ font-weight: 200 900;
+ font-style: normal;
+ font-stretch: normal;
+ src: url('https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2') format('woff2');
+ }
+ @font-face{
+ font-family: 'Source Serif Pro';
+ font-weight: 200 900;
+ font-style: italic;
+ font-stretch: normal;
+ src: url('https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2') format('woff2');
+ }
+ ";
+ }
+}
diff --git a/phpunit/webfonts-api/providers/class-wp-webfonts-local-provider-test.php b/phpunit/webfonts-api/providers/class-wp-webfonts-local-provider-test.php
new file mode 100644
index 00000000000000..0db1ea11c3f941
--- /dev/null
+++ b/phpunit/webfonts-api/providers/class-wp-webfonts-local-provider-test.php
@@ -0,0 +1,154 @@
+provider = new WP_Webfonts_Local_Provider();
+
+ $this->set_up_theme();
+ }
+
+ /**
+ * Local `src` paths to need to be relative to the theme. This method sets up the
+ * `wp-content/themes/` directory to ensure consistency when running tests.
+ */
+ private function set_up_theme() {
+ $this->theme_root = realpath( DIR_TESTDATA . '/themedir1' );
+ $this->orig_theme_dir = $GLOBALS['wp_theme_directories'];
+ $GLOBALS['wp_theme_directories'] = array( $this->theme_root );
+
+ $theme_root_callback = function () {
+ return $this->theme_root;
+ };
+ add_filter( 'theme_root', $theme_root_callback );
+ add_filter( 'stylesheet_root', $theme_root_callback );
+ add_filter( 'template_root', $theme_root_callback );
+
+ // Clear caches.
+ wp_clean_themes_cache();
+ unset( $GLOBALS['wp_themes'] );
+ }
+
+ function tear_down() {
+ // Restore the original theme directory setup.
+ $GLOBALS['wp_theme_directories'] = $this->orig_theme_dir;
+ wp_clean_themes_cache();
+ unset( $GLOBALS['wp_themes'] );
+
+ parent::tear_down();
+ }
+
+ /**
+ * @covers WP_Webfonts_Local_Provider::set_webfonts
+ */
+ public function test_set_webfonts() {
+ $webfonts = array(
+ 'source-serif-pro.normal.200 900' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'normal',
+ 'font-weight' => '200 900',
+ 'font-stretch' => 'normal',
+ 'src' => 'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2',
+ ),
+ 'source-serif-pro.italic.200 900' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Source Serif Pro',
+ 'font-style' => 'italic',
+ 'font-weight' => '200 900',
+ 'font-stretch' => 'normal',
+ 'src' => 'https://example.com/assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2',
+ ),
+ );
+
+ $this->provider->set_webfonts( $webfonts );
+
+ $property = $this->get_webfonts_property();
+ $this->assertSame( $webfonts, $property->getValue( $this->provider ) );
+ }
+
+ /**
+ * @covers WP_Webfonts_Local_Provider::get_css
+ *
+ * @dataProvider data_get_css
+ *
+ * @param array $webfonts Prepared webfonts (to store in WP_Webfonts_Local_Provider::$webfonts property).
+ * @param string $expected Expected CSS.
+ */
+ public function test_get_css( array $webfonts, $expected ) {
+ $property = $this->get_webfonts_property();
+ $property->setValue( $this->provider, $webfonts );
+
+ $this->assertSame( $expected, $this->provider->get_css() );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_get_css() {
+ return array(
+ 'truetype format' => array(
+ 'webfonts' => array(
+ 'open-sans.italic.bold' => array(
+ 'provider' => 'local',
+ 'font-family' => 'Open Sans',
+ 'font-style' => 'italic',
+ 'font-weight' => 'bold',
+ 'src' => 'http://example.org/assets/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf',
+ ),
+ ),
+ 'expected' => <<