diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php
index 7ebc3a1d3bfe1..19a9285ed2fec 100644
--- a/src/wp-includes/default-filters.php
+++ b/src/wp-includes/default-filters.php
@@ -748,5 +748,7 @@
// Font management.
add_action( 'wp_head', 'wp_print_font_faces', 50 );
+add_action( 'deleted_post', '_wp_after_delete_font_family', 10, 2 );
+add_action( 'before_delete_post', '_wp_before_delete_font_face', 10, 2 );
unset( $filter, $action );
diff --git a/src/wp-includes/fonts.php b/src/wp-includes/fonts.php
index 87503c275f390..dae5283d65ce5 100644
--- a/src/wp-includes/fonts.php
+++ b/src/wp-includes/fonts.php
@@ -51,3 +51,140 @@ function wp_print_font_faces( $fonts = array() ) {
$wp_font_face = new WP_Font_Face();
$wp_font_face->generate_and_print( $fonts );
}
+
+/**
+ * Registers a new Font Collection in the Font Library.
+ *
+ * @since 6.5.0
+ *
+ * @param string $slug Font collection slug. May only contain alphanumeric characters, dashes,
+ * and underscores. See sanitize_title().
+ * @param array|string $data_or_file {
+ * Font collection data array or a path/URL to a JSON file containing the font collection.
+ *
+ * @link https://schemas.wp.org/trunk/font-collection.json
+ *
+ * @type string $name Required. Name of the font collection shown in the Font Library.
+ * @type string $description Optional. A short descriptive summary of the font collection. Default empty.
+ * @type array $font_families Required. Array of font family definitions that are in the collection.
+ * @type array $categories Optional. Array of categories, each with a name and slug, that are used by the
+ * fonts in the collection. Default empty.
+ * }
+ * @return WP_Font_Collection|WP_Error A font collection if it was registered
+ * successfully, or WP_Error object on failure.
+ */
+function wp_register_font_collection( $slug, $data_or_file ) {
+ return WP_Font_Library::get_instance()->register_font_collection( $slug, $data_or_file );
+}
+
+/**
+ * Unregisters a font collection from the Font Library.
+ *
+ * @since 6.5.0
+ *
+ * @param string $slug Font collection slug.
+ * @return bool True if the font collection was unregistered successfully, else false.
+ */
+function wp_unregister_font_collection( $slug ) {
+ return WP_Font_Library::get_instance()->unregister_font_collection( $slug );
+}
+
+/**
+ * Returns an array containing the current fonts upload directory's path and URL.
+ *
+ * @since 6.5.0
+ *
+ * @param array $defaults {
+ * Array of information about the upload directory.
+ *
+ * @type string $path Base directory and subdirectory or full path to the fonts upload directory.
+ * @type string $url Base URL and subdirectory or absolute URL to the fonts upload directory.
+ * @type string $subdir Subdirectory
+ * @type string $basedir Path without subdir.
+ * @type string $baseurl URL path without subdir.
+ * @type string|false $error False or error message.
+ * }
+ * @return array $defaults {
+ * Array of information about the upload directory.
+ *
+ * @type string $path Base directory and subdirectory or full path to the fonts upload directory.
+ * @type string $url Base URL and subdirectory or absolute URL to the fonts upload directory.
+ * @type string $subdir Subdirectory
+ * @type string $basedir Path without subdir.
+ * @type string $baseurl URL path without subdir.
+ * @type string|false $error False or error message.
+ * }
+ */
+function wp_get_font_dir( $defaults = array() ) {
+ $site_path = '';
+ if ( is_multisite() && ! ( is_main_network() && is_main_site() ) ) {
+ $site_path = '/sites/' . get_current_blog_id();
+ }
+
+ // Sets the defaults.
+ $defaults['path'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path;
+ $defaults['url'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path;
+ $defaults['subdir'] = '';
+ $defaults['basedir'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path;
+ $defaults['baseurl'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path;
+ $defaults['error'] = false;
+
+ /**
+ * Filters the fonts directory data.
+ *
+ * This filter allows developers to modify the fonts directory data.
+ *
+ * @since 6.5.0
+ *
+ * @param array $defaults The original fonts directory data.
+ */
+ return apply_filters( 'font_dir', $defaults );
+}
+
+/**
+ * Deletes child font faces when a font family is deleted.
+ *
+ * @access private
+ * @since 6.5.0
+ *
+ * @param int $post_id Post ID.
+ * @param WP_Post $post Post object.
+ */
+function _wp_after_delete_font_family( $post_id, $post ) {
+ if ( 'wp_font_family' !== $post->post_type ) {
+ return;
+ }
+
+ $font_faces = get_children(
+ array(
+ 'post_parent' => $post_id,
+ 'post_type' => 'wp_font_face',
+ )
+ );
+
+ foreach ( $font_faces as $font_face ) {
+ wp_delete_post( $font_face->ID, true );
+ }
+}
+
+/**
+ * Deletes associated font files when a font face is deleted.
+ *
+ * @access private
+ * @since 6.5.0
+ *
+ * @param int $post_id Post ID.
+ * @param WP_Post $post Post object.
+ */
+function _wp_before_delete_font_face( $post_id, $post ) {
+ if ( 'wp_font_face' !== $post->post_type ) {
+ return;
+ }
+
+ $font_files = get_post_meta( $post_id, '_wp_font_face_file', false );
+ $font_dir = wp_get_font_dir()['path'];
+
+ foreach ( $font_files as $font_file ) {
+ wp_delete_file( $font_dir . '/' . $font_file );
+ }
+}
diff --git a/src/wp-includes/fonts/class-wp-font-collection.php b/src/wp-includes/fonts/class-wp-font-collection.php
new file mode 100644
index 0000000000000..240bba35e94f8
--- /dev/null
+++ b/src/wp-includes/fonts/class-wp-font-collection.php
@@ -0,0 +1,260 @@
+slug = sanitize_title( $slug );
+ if ( $this->slug !== $slug ) {
+ _doing_it_wrong(
+ __METHOD__,
+ /* translators: %s: Font collection slug. */
+ sprintf( __( 'Font collection slug "%s" is not valid. Slugs must use only alphanumeric characters, dashes, and underscores.' ), $slug ),
+ '6.5.0'
+ );
+ }
+
+ if ( is_array( $data_or_file ) ) {
+ $this->data = $this->sanitize_and_validate_data( $data_or_file );
+ } else {
+ // JSON data is lazy loaded by ::get_data().
+ $this->src = $data_or_file;
+ }
+ }
+
+ /**
+ * Retrieves the font collection data.
+ *
+ * @since 6.5.0
+ *
+ * @return array|WP_Error An array containing the font collection data, or a WP_Error on failure.
+ */
+ public function get_data() {
+ // If the collection uses JSON data, load it and cache the data/error.
+ if ( $this->src && empty( $this->data ) ) {
+ $this->data = $this->load_from_json( $this->src );
+ }
+
+ if ( is_wp_error( $this->data ) ) {
+ return $this->data;
+ }
+
+ // Set defaults for optional properties.
+ $defaults = array(
+ 'description' => '',
+ 'categories' => array(),
+ );
+
+ return wp_parse_args( $this->data, $defaults );
+ }
+
+ /**
+ * Loads font collection data from a JSON file or URL.
+ *
+ * @since 6.5.0
+ *
+ * @param string $file_or_url File path or URL to a JSON file containing the font collection data.
+ * @return array|WP_Error An array containing the font collection data on success,
+ * else an instance of WP_Error on failure.
+ */
+ private function load_from_json( $file_or_url ) {
+ $url = wp_http_validate_url( $file_or_url );
+ $file = file_exists( $file_or_url ) ? wp_normalize_path( realpath( $file_or_url ) ) : false;
+
+ if ( ! $url && ! $file ) {
+ // translators: %s: File path or URL to font collection JSON file.
+ $message = __( 'Font collection JSON file is invalid or does not exist.' );
+ _doing_it_wrong( __METHOD__, $message, '6.5.0' );
+ return new WP_Error( 'font_collection_json_missing', $message );
+ }
+
+ return $url ? $this->load_from_url( $url ) : $this->load_from_file( $file );
+ }
+
+ /**
+ * Loads the font collection data from a JSON file path.
+ *
+ * @since 6.5.0
+ *
+ * @param string $file File path to a JSON file containing the font collection data.
+ * @return array|WP_Error An array containing the font collection data on success,
+ * else an instance of WP_Error on failure.
+ */
+ private function load_from_file( $file ) {
+ $data = wp_json_file_decode( $file, array( 'associative' => true ) );
+ if ( empty( $data ) ) {
+ return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection JSON file contents.' ) );
+ }
+
+ return $this->sanitize_and_validate_data( $data );
+ }
+
+ /**
+ * Loads the font collection data from a JSON file URL.
+ *
+ * @since 6.5.0
+ *
+ * @param string $url URL to a JSON file containing the font collection data.
+ * @return array|WP_Error An array containing the font collection data on success,
+ * else an instance of WP_Error on failure.
+ */
+ private function load_from_url( $url ) {
+ // Limit key to 167 characters to avoid failure in the case of a long URL.
+ $transient_key = substr( 'wp_font_collection_url_' . $url, 0, 167 );
+ $data = get_site_transient( $transient_key );
+
+ if ( false === $data ) {
+ $response = wp_safe_remote_get( $url );
+ if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
+ // translators: %s: Font collection URL.
+ return new WP_Error( 'font_collection_request_error', sprintf( __( 'Error fetching the font collection data from "%s".' ), $url ) );
+ }
+
+ $data = json_decode( wp_remote_retrieve_body( $response ), true );
+ if ( empty( $data ) ) {
+ return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection data from the HTTP response JSON.' ) );
+ }
+
+ // Make sure the data is valid before storing it in a transient.
+ $data = $this->sanitize_and_validate_data( $data );
+ if ( is_wp_error( $data ) ) {
+ return $data;
+ }
+
+ set_site_transient( $transient_key, $data, DAY_IN_SECONDS );
+ }
+
+ return $data;
+ }
+
+ /**
+ * Sanitizes and validates the font collection data.
+ *
+ * @since 6.5.0
+ *
+ * @param array $data Font collection data to sanitize and validate.
+ * @return array|WP_Error Sanitized data if valid, otherwise a WP_Error instance.
+ */
+ private function sanitize_and_validate_data( $data ) {
+ $schema = self::get_sanitization_schema();
+ $data = WP_Font_Utils::sanitize_from_schema( $data, $schema );
+
+ $required_properties = array( 'name', 'font_families' );
+ foreach ( $required_properties as $property ) {
+ if ( empty( $data[ $property ] ) ) {
+ $message = sprintf(
+ // translators: 1: Font collection slug, 2: Missing property name, e.g. "font_families".
+ __( 'Font collection "%1$s" has missing or empty property: "%2$s".' ),
+ $this->slug,
+ $property
+ );
+ _doing_it_wrong( __METHOD__, $message, '6.5.0' );
+ return new WP_Error( 'font_collection_missing_property', $message );
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Retrieves the font collection sanitization schema.
+ *
+ * @since 6.5.0
+ *
+ * @return array Font collection sanitization schema.
+ */
+ private static function get_sanitization_schema() {
+ return array(
+ 'name' => 'sanitize_text_field',
+ 'description' => 'sanitize_text_field',
+ 'font_families' => array(
+ array(
+ 'font_family_settings' => array(
+ 'name' => 'sanitize_text_field',
+ 'slug' => 'sanitize_title',
+ 'fontFamily' => 'sanitize_text_field',
+ 'preview' => 'sanitize_url',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'sanitize_text_field',
+ 'fontStyle' => 'sanitize_text_field',
+ 'fontWeight' => 'sanitize_text_field',
+ 'src' => static function ( $value ) {
+ return is_array( $value )
+ ? array_map( 'sanitize_text_field', $value )
+ : sanitize_text_field( $value );
+ },
+ 'preview' => 'sanitize_url',
+ 'fontDisplay' => 'sanitize_text_field',
+ 'fontStretch' => 'sanitize_text_field',
+ 'ascentOverride' => 'sanitize_text_field',
+ 'descentOverride' => 'sanitize_text_field',
+ 'fontVariant' => 'sanitize_text_field',
+ 'fontFeatureSettings' => 'sanitize_text_field',
+ 'fontVariationSettings' => 'sanitize_text_field',
+ 'lineGapOverride' => 'sanitize_text_field',
+ 'sizeAdjust' => 'sanitize_text_field',
+ 'unicodeRange' => 'sanitize_text_field',
+ ),
+ ),
+ ),
+ 'categories' => array( 'sanitize_title' ),
+ ),
+ ),
+ 'categories' => array(
+ array(
+ 'name' => 'sanitize_text_field',
+ 'slug' => 'sanitize_title',
+ ),
+ ),
+ );
+ }
+}
diff --git a/src/wp-includes/fonts/class-wp-font-library.php b/src/wp-includes/fonts/class-wp-font-library.php
new file mode 100644
index 0000000000000..a0c07eeffcd8f
--- /dev/null
+++ b/src/wp-includes/fonts/class-wp-font-library.php
@@ -0,0 +1,144 @@
+is_collection_registered( $new_collection->slug ) ) {
+ $error_message = sprintf(
+ /* translators: %s: Font collection slug. */
+ __( 'Font collection with slug: "%s" is already registered.' ),
+ $new_collection->slug
+ );
+ _doing_it_wrong(
+ __METHOD__,
+ $error_message,
+ '6.5.0'
+ );
+ return new WP_Error( 'font_collection_registration_error', $error_message );
+ }
+ $this->collections[ $new_collection->slug ] = $new_collection;
+ return $new_collection;
+ }
+
+ /**
+ * Unregisters a previously registered font collection.
+ *
+ * @since 6.5.0
+ *
+ * @param string $slug Font collection slug.
+ * @return bool True if the font collection was unregistered successfully and false otherwise.
+ */
+ public function unregister_font_collection( $slug ) {
+ if ( ! $this->is_collection_registered( $slug ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ /* translators: %s: Font collection slug. */
+ sprintf( __( 'Font collection "%s" not found.' ), $slug ),
+ '6.5.0'
+ );
+ return false;
+ }
+ unset( $this->collections[ $slug ] );
+ return true;
+ }
+
+ /**
+ * Checks if a font collection is registered.
+ *
+ * @since 6.5.0
+ *
+ * @param string $slug Font collection slug.
+ * @return bool True if the font collection is registered and false otherwise.
+ */
+ private function is_collection_registered( $slug ) {
+ return array_key_exists( $slug, $this->collections );
+ }
+
+ /**
+ * Gets all the font collections available.
+ *
+ * @since 6.5.0
+ *
+ * @return array List of font collections.
+ */
+ public function get_font_collections() {
+ return $this->collections;
+ }
+
+ /**
+ * Gets a font collection.
+ *
+ * @since 6.5.0
+ *
+ * @param string $slug Font collection slug.
+ * @return WP_Font_Collection|WP_Error Font collection object,
+ * or WP_Error object if the font collection doesn't exist.
+ */
+ public function get_font_collection( $slug ) {
+ if ( $this->is_collection_registered( $slug ) ) {
+ return $this->collections[ $slug ];
+ }
+ return new WP_Error( 'font_collection_not_found', __( 'Font collection not found.' ) );
+ }
+
+ /**
+ * Utility method to retrieve the main instance of the class.
+ *
+ * The instance will be created if it does not exist yet.
+ *
+ * @since 6.5.0
+ *
+ * @return WP_Font_Library The main instance.
+ */
+ public static function get_instance() {
+ if ( null === self::$instance ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+}
diff --git a/src/wp-includes/fonts/class-wp-font-utils.php b/src/wp-includes/fonts/class-wp-font-utils.php
new file mode 100644
index 0000000000000..8efa7978a8256
--- /dev/null
+++ b/src/wp-includes/fonts/class-wp-font-utils.php
@@ -0,0 +1,238 @@
+ '',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'fontStretch' => '100%',
+ 'unicodeRange' => 'U+0-10FFFF',
+ );
+ $settings = wp_parse_args( $settings, $defaults );
+
+ $font_family = mb_strtolower( $settings['fontFamily'] );
+ $font_style = strtolower( $settings['fontStyle'] );
+ $font_weight = strtolower( $settings['fontWeight'] );
+ $font_stretch = strtolower( $settings['fontStretch'] );
+ $unicode_range = strtoupper( $settings['unicodeRange'] );
+
+ // Convert weight keywords to numeric strings.
+ $font_weight = str_replace( array( 'normal', 'bold' ), array( '400', '700' ), $font_weight );
+
+ // Convert stretch keywords to numeric strings.
+ $font_stretch_map = array(
+ 'ultra-condensed' => '50%',
+ 'extra-condensed' => '62.5%',
+ 'condensed' => '75%',
+ 'semi-condensed' => '87.5%',
+ 'normal' => '100%',
+ 'semi-expanded' => '112.5%',
+ 'expanded' => '125%',
+ 'extra-expanded' => '150%',
+ 'ultra-expanded' => '200%',
+ );
+ $font_stretch = str_replace( array_keys( $font_stretch_map ), array_values( $font_stretch_map ), $font_stretch );
+
+ $slug_elements = array( $font_family, $font_style, $font_weight, $font_stretch, $unicode_range );
+
+ $slug_elements = array_map(
+ function ( $elem ) {
+ // Remove quotes to normalize font-family names, and ';' to use as a separator.
+ $elem = trim( str_replace( array( '"', "'", ';' ), '', $elem ) );
+
+ // Normalize comma separated lists by removing whitespace in between items,
+ // but keep whitespace within items (e.g. "Open Sans" and "OpenSans" are different fonts).
+ // CSS spec for whitespace includes: U+000A LINE FEED, U+0009 CHARACTER TABULATION, or U+0020 SPACE,
+ // which by default are all matched by \s in PHP.
+ return preg_replace( '/,\s+/', ',', $elem );
+ },
+ $slug_elements
+ );
+
+ return sanitize_text_field( join( ';', $slug_elements ) );
+ }
+
+ /**
+ * Sanitizes a tree of data using a schema.
+ *
+ * The schema structure should mirror the data tree. Each value provided in the
+ * schema should be a callable that will be applied to sanitize the corresponding
+ * value in the data tree. Keys that are in the data tree, but not present in the
+ * schema, will be removed in the santized data. Nested arrays are traversed recursively.
+ *
+ * @since 6.5.0
+ *
+ * @access private
+ *
+ * @param array $tree The data to sanitize.
+ * @param array $schema The schema used for sanitization.
+ * @return array The sanitized data.
+ */
+ public static function sanitize_from_schema( $tree, $schema ) {
+ if ( ! is_array( $tree ) || ! is_array( $schema ) ) {
+ return array();
+ }
+
+ foreach ( $tree as $key => $value ) {
+ // Remove keys not in the schema or with null/empty values.
+ if ( ! array_key_exists( $key, $schema ) ) {
+ unset( $tree[ $key ] );
+ continue;
+ }
+
+ $is_value_array = is_array( $value );
+ $is_schema_array = is_array( $schema[ $key ] ) && ! is_callable( $schema[ $key ] );
+
+ if ( $is_value_array && $is_schema_array ) {
+ if ( wp_is_numeric_array( $value ) ) {
+ // If indexed, process each item in the array.
+ foreach ( $value as $item_key => $item_value ) {
+ $tree[ $key ][ $item_key ] = isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] )
+ ? self::sanitize_from_schema( $item_value, $schema[ $key ][0] )
+ : self::apply_sanitizer( $item_value, $schema[ $key ][0] );
+ }
+ } else {
+ // If it is an associative or indexed array, process as a single object.
+ $tree[ $key ] = self::sanitize_from_schema( $value, $schema[ $key ] );
+ }
+ } elseif ( ! $is_value_array && $is_schema_array ) {
+ // If the value is not an array but the schema is, remove the key.
+ unset( $tree[ $key ] );
+ } elseif ( ! $is_schema_array ) {
+ // If the schema is not an array, apply the sanitizer to the value.
+ $tree[ $key ] = self::apply_sanitizer( $value, $schema[ $key ] );
+ }
+
+ // Remove keys with null/empty values.
+ if ( empty( $tree[ $key ] ) ) {
+ unset( $tree[ $key ] );
+ }
+ }
+
+ return $tree;
+ }
+
+ /**
+ * Applies a sanitizer function to a value.
+ *
+ * @since 6.5.0
+ *
+ * @param mixed $value The value to sanitize.
+ * @param mixed $sanitizer The sanitizer function to apply.
+ * @return mixed The sanitized value.
+ */
+ private static function apply_sanitizer( $value, $sanitizer ) {
+ if ( null === $sanitizer ) {
+ return $value;
+
+ }
+ return call_user_func( $sanitizer, $value );
+ }
+
+ /**
+ * Returns the expected mime-type values for font files, depending on PHP version.
+ *
+ * This is needed because font mime types vary by PHP version, so checking the PHP version
+ * is necessary until a list of valid mime-types for each file extension can be provided to
+ * the 'upload_mimes' filter.
+ *
+ * @since 6.5.0
+ *
+ * @access private
+ *
+ * @return array A collection of mime types keyed by file extension.
+ */
+ public static function get_allowed_font_mime_types() {
+ $php_7_ttf_mime_type = PHP_VERSION_ID >= 70300 ? 'application/font-sfnt' : 'application/x-font-ttf';
+
+ return array(
+ 'otf' => 'application/vnd.ms-opentype',
+ 'ttf' => PHP_VERSION_ID >= 70400 ? 'font/sfnt' : $php_7_ttf_mime_type,
+ 'woff' => PHP_VERSION_ID >= 80100 ? 'font/woff' : 'application/font-woff',
+ 'woff2' => PHP_VERSION_ID >= 80100 ? 'font/woff2' : 'application/font-woff2',
+ );
+ }
+}
diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php
index 001ccaf7d0465..eb0d8c1926190 100644
--- a/src/wp-includes/post.php
+++ b/src/wp-includes/post.php
@@ -564,6 +564,64 @@ function create_initial_post_types() {
)
);
+ register_post_type(
+ 'wp_font_family',
+ array(
+ 'labels' => array(
+ 'name' => __( 'Font Families' ),
+ 'singular_name' => __( 'Font Family' ),
+ ),
+ 'public' => false,
+ '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
+ 'hierarchical' => false,
+ 'capabilities' => array(
+ 'read' => 'edit_theme_options',
+ 'read_private_posts' => 'edit_theme_options',
+ 'create_posts' => 'edit_theme_options',
+ 'publish_posts' => 'edit_theme_options',
+ 'edit_posts' => 'edit_theme_options',
+ 'edit_others_posts' => 'edit_theme_options',
+ 'edit_published_posts' => 'edit_theme_options',
+ 'delete_posts' => 'edit_theme_options',
+ 'delete_others_posts' => 'edit_theme_options',
+ 'delete_published_posts' => 'edit_theme_options',
+ ),
+ 'map_meta_cap' => true,
+ 'query_var' => false,
+ 'show_in_rest' => false,
+ 'rewrite' => false,
+ )
+ );
+
+ register_post_type(
+ 'wp_font_face',
+ array(
+ 'labels' => array(
+ 'name' => __( 'Font Faces' ),
+ 'singular_name' => __( 'Font Face' ),
+ ),
+ 'public' => false,
+ '_builtin' => true, /* internal use only. don't use this when registering your own post type. */
+ 'hierarchical' => false,
+ 'capabilities' => array(
+ 'read' => 'edit_theme_options',
+ 'read_private_posts' => 'edit_theme_options',
+ 'create_posts' => 'edit_theme_options',
+ 'publish_posts' => 'edit_theme_options',
+ 'edit_posts' => 'edit_theme_options',
+ 'edit_others_posts' => 'edit_theme_options',
+ 'edit_published_posts' => 'edit_theme_options',
+ 'delete_posts' => 'edit_theme_options',
+ 'delete_others_posts' => 'edit_theme_options',
+ 'delete_published_posts' => 'edit_theme_options',
+ ),
+ 'map_meta_cap' => true,
+ 'query_var' => false,
+ 'show_in_rest' => false,
+ 'rewrite' => false,
+ )
+ );
+
register_post_status(
'publish',
array(
diff --git a/src/wp-settings.php b/src/wp-settings.php
index a80c661d52a65..22683b37d1f5d 100644
--- a/src/wp-settings.php
+++ b/src/wp-settings.php
@@ -374,7 +374,10 @@
require ABSPATH . WPINC . '/style-engine/class-wp-style-engine-css-rules-store.php';
require ABSPATH . WPINC . '/style-engine/class-wp-style-engine-processor.php';
require ABSPATH . WPINC . '/fonts/class-wp-font-face-resolver.php';
+require ABSPATH . WPINC . '/fonts/class-wp-font-collection.php';
require ABSPATH . WPINC . '/fonts/class-wp-font-face.php';
+require ABSPATH . WPINC . '/fonts/class-wp-font-library.php';
+require ABSPATH . WPINC . '/fonts/class-wp-font-utils.php';
require ABSPATH . WPINC . '/fonts.php';
require ABSPATH . WPINC . '/class-wp-script-modules.php';
require ABSPATH . WPINC . '/script-modules.php';
diff --git a/tests/phpunit/data/fonts/OpenSans-Regular.otf b/tests/phpunit/data/fonts/OpenSans-Regular.otf
new file mode 100644
index 0000000000000..8db0f64c67ddd
Binary files /dev/null and b/tests/phpunit/data/fonts/OpenSans-Regular.otf differ
diff --git a/tests/phpunit/data/fonts/OpenSans-Regular.ttf b/tests/phpunit/data/fonts/OpenSans-Regular.ttf
new file mode 100644
index 0000000000000..ae716936e9e4c
Binary files /dev/null and b/tests/phpunit/data/fonts/OpenSans-Regular.ttf differ
diff --git a/tests/phpunit/data/fonts/OpenSans-Regular.woff b/tests/phpunit/data/fonts/OpenSans-Regular.woff
new file mode 100644
index 0000000000000..bd0f824b207d6
Binary files /dev/null and b/tests/phpunit/data/fonts/OpenSans-Regular.woff differ
diff --git a/tests/phpunit/data/fonts/OpenSans-Regular.woff2 b/tests/phpunit/data/fonts/OpenSans-Regular.woff2
new file mode 100644
index 0000000000000..f778f9c8455f2
Binary files /dev/null and b/tests/phpunit/data/fonts/OpenSans-Regular.woff2 differ
diff --git a/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php b/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php
new file mode 100644
index 0000000000000..083c12202aa34
--- /dev/null
+++ b/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php
@@ -0,0 +1,87 @@
+post->create(
+ array(
+ 'post_type' => 'wp_font_family',
+ )
+ );
+ $font_face_id = self::factory()->post->create(
+ array(
+ 'post_type' => 'wp_font_face',
+ 'post_parent' => $font_family_id,
+ )
+ );
+ $other_font_family_id = self::factory()->post->create(
+ array(
+ 'post_type' => 'wp_font_family',
+ )
+ );
+ $other_font_face_id = self::factory()->post->create(
+ array(
+ 'post_type' => 'wp_font_face',
+ 'post_parent' => $other_font_family_id,
+ )
+ );
+
+ wp_delete_post( $font_family_id, true );
+
+ $this->assertNull( get_post( $font_face_id ), 'Font face post should also have been deleted.' );
+ $this->assertNotNull( get_post( $other_font_face_id ), 'The other post should exist.' );
+ }
+
+ public function test_deleting_font_faces_deletes_associated_font_files() {
+ list( $font_face_id, $font_path ) = $this->create_font_face_with_file( 'OpenSans-Regular.woff2' );
+ list( , $other_font_path ) = $this->create_font_face_with_file( 'OpenSans-Regular.ttf' );
+
+ wp_delete_post( $font_face_id, true );
+
+ $this->assertFalse( file_exists( $font_path ), 'The font file should have been deleted when the post was deleted.' );
+ $this->assertTrue( file_exists( $other_font_path ), 'The other font file should exist.' );
+ }
+
+ protected function create_font_face_with_file( $filename ) {
+ $font_face_id = self::factory()->post->create(
+ array(
+ 'post_type' => 'wp_font_face',
+ )
+ );
+
+ $font_file = $this->upload_font_file( $filename );
+
+ // Make sure the font file uploaded successfully.
+ $this->assertFalse( $font_file['error'] );
+
+ $font_path = $font_file['file'];
+ $font_filename = basename( $font_path );
+ add_post_meta( $font_face_id, '_wp_font_face_file', $font_filename );
+
+ return array( $font_face_id, $font_path );
+ }
+
+ protected function upload_font_file( $font_filename ) {
+ $font_file_path = DIR_TESTDATA . '/fonts/' . $font_filename;
+
+ add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
+ add_filter( 'upload_dir', 'wp_get_font_dir' );
+ $font_file = wp_upload_bits(
+ $font_filename,
+ null,
+ file_get_contents( $font_file_path )
+ );
+ remove_filter( 'upload_dir', 'wp_get_font_dir' );
+ remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
+
+ return $font_file;
+ }
+}
diff --git a/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php b/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php
new file mode 100644
index 0000000000000..a0693ce341456
--- /dev/null
+++ b/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php
@@ -0,0 +1,26 @@
+setExpectedIncorrectUsage( 'WP_Font_Collection::__construct' );
+ $mock_collection_data = array(
+ 'name' => 'Test Collection',
+ 'font_families' => array( 'mock ' ),
+ );
+
+ $collection = new WP_Font_Collection( 'slug with spaces', $mock_collection_data );
+
+ $this->assertSame( 'slug-with-spaces', $collection->slug, 'Slug is not sanitized.' );
+ }
+}
diff --git a/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php b/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php
new file mode 100644
index 0000000000000..8a0af0c97d967
--- /dev/null
+++ b/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php
@@ -0,0 +1,358 @@
+get_data();
+
+ $this->assertSame( $slug, $collection->slug, 'The slug should match.' );
+ $this->assertSame( $expected_data, $data, 'The collection data should match.' );
+ }
+
+ /**
+ * @dataProvider data_create_font_collection
+ *
+ * @param string $slug Font collection slug.
+ * @param array $config Font collection config.
+ * @param array $expected_data Expected collection data.
+ */
+ public function test_should_get_data_from_json_file( $slug, $config, $expected_data ) {
+ $mock_file = wp_tempnam( 'my-collection-data-' );
+ file_put_contents( $mock_file, wp_json_encode( $config ) );
+
+ $collection = new WP_Font_Collection( $slug, $mock_file );
+ $data = $collection->get_data();
+
+ $this->assertSame( $slug, $collection->slug, 'The slug should match.' );
+ $this->assertSame( $expected_data, $data, 'The collection data should match.' );
+ }
+
+ /**
+ * @dataProvider data_create_font_collection
+ *
+ * @param string $slug Font collection slug.
+ * @param array $config Font collection config.
+ * @param array $expected_data Expected collection data.
+ */
+ public function test_should_get_data_from_json_url( $slug, $config, $expected_data ) {
+ add_filter( 'pre_http_request', array( $this, 'mock_request' ), 10, 3 );
+
+ self::$mock_collection_data = $config;
+ $collection = new WP_Font_Collection( $slug, 'https://example.com/fonts/mock-font-collection.json' );
+ $data = $collection->get_data();
+
+ remove_filter( 'pre_http_request', array( $this, 'mock_request' ) );
+
+ $this->assertSame( $slug, $collection->slug, 'The slug should match.' );
+ $this->assertSame( $expected_data, $data, 'The collection data should match.' );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_create_font_collection() {
+ return array(
+
+ 'font collection with required data' => array(
+ 'slug' => 'my-collection',
+ 'config' => array(
+ 'name' => 'My Collection',
+ 'font_families' => array( array() ),
+ ),
+ 'expected_data' => array(
+ 'description' => '',
+ 'categories' => array(),
+ 'name' => 'My Collection',
+ 'font_families' => array( array() ),
+ ),
+ ),
+
+ 'font collection with all data' => array(
+ 'slug' => 'my-collection',
+ 'config' => array(
+ 'name' => 'My Collection',
+ 'description' => 'My collection description',
+ 'font_families' => array( array() ),
+ 'categories' => array(),
+ ),
+ 'expected_data' => array(
+ 'description' => 'My collection description',
+ 'categories' => array(),
+ 'name' => 'My Collection',
+ 'font_families' => array( array() ),
+ ),
+ ),
+
+ 'font collection with risky data' => array(
+ 'slug' => 'my-collection',
+ 'config' => array(
+ 'name' => 'My Collection',
+ 'description' => 'My collection description',
+ 'font_families' => array(
+ array(
+ 'font_family_settings' => array(
+ 'fontFamily' => 'Open Sans, sans-serif',
+ 'slug' => 'open-sans',
+ 'name' => 'Open Sans',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Open Sans',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'src' => 'https://example.com/src-as-string.ttf?a=',
+ ),
+ array(
+ 'fontFamily' => 'Open Sans',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'src' => array(
+ 'https://example.com/src-as-array.woff2?a=',
+ 'https://example.com/src-as-array.ttf',
+ ),
+ ),
+ ),
+ 'unwanted_property' => 'potentially evil value',
+ ),
+ 'categories' => array( 'sans-serif' ),
+ ),
+ ),
+ 'categories' => array(
+ array(
+ 'name' => 'Mock col',
+ 'slug' => 'mock-col',
+ 'unwanted_property' => 'potentially evil value',
+ ),
+ ),
+ 'unwanted_property' => 'potentially evil value',
+ ),
+ 'expected_data' => array(
+ 'description' => 'My collection description',
+ 'categories' => array(
+ array(
+ 'name' => 'Mock col',
+ 'slug' => 'mock-colalertxss',
+ ),
+ ),
+ 'name' => 'My Collection',
+ 'font_families' => array(
+ array(
+ 'font_family_settings' => array(
+ 'fontFamily' => 'Open Sans, sans-serif',
+ 'slug' => 'open-sans',
+ 'name' => 'Open Sans',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Open Sans',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'src' => 'https://example.com/src-as-string.ttf?a=',
+ ),
+ array(
+ 'fontFamily' => 'Open Sans',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'src' => array(
+ 'https://example.com/src-as-array.woff2?a=',
+ 'https://example.com/src-as-array.ttf',
+ ),
+ ),
+ ),
+ ),
+ 'categories' => array( 'sans-serifalertxss' ),
+ ),
+ ),
+ ),
+ ),
+
+ );
+ }
+
+ /**
+ * @dataProvider data_should_error_when_missing_properties
+ *
+ * @param array $config Font collection config.
+ */
+ public function test_should_error_when_missing_properties( $config ) {
+ $this->setExpectedIncorrectUsage( 'WP_Font_Collection::sanitize_and_validate_data' );
+
+ $collection = new WP_Font_Collection( 'my-collection', $config );
+ $data = $collection->get_data();
+
+ $this->assertWPError( $data, 'Error is not returned when property is missing or invalid.' );
+ $this->assertSame(
+ $data->get_error_code(),
+ 'font_collection_missing_property',
+ 'Incorrect error code when property is missing or invalid.'
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_should_error_when_missing_properties() {
+ return array(
+ 'missing name' => array(
+ 'config' => array(
+ 'font_families' => array( 'mock' ),
+ ),
+ ),
+ 'empty name' => array(
+ 'config' => array(
+ 'name' => '',
+ 'font_families' => array( 'mock' ),
+ ),
+ ),
+ 'missing font_families' => array(
+ 'config' => array(
+ 'name' => 'My Collection',
+ ),
+ ),
+ 'empty font_families' => array(
+ 'config' => array(
+ 'name' => 'My Collection',
+ 'font_families' => array(),
+ ),
+ ),
+ );
+ }
+
+ public function test_should_error_with_invalid_json_file_path() {
+ $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' );
+
+ $collection = new WP_Font_Collection( 'my-collection', 'non-existing.json' );
+ $data = $collection->get_data();
+
+ $this->assertWPError( $data, 'Error is not returned when invalid file path is provided.' );
+ $this->assertSame(
+ $data->get_error_code(),
+ 'font_collection_json_missing',
+ 'Incorrect error code when invalid file path is provided.'
+ );
+ }
+
+ public function test_should_error_with_invalid_json_from_file() {
+ $mock_file = wp_tempnam( 'my-collection-data-' );
+ file_put_contents( $mock_file, 'invalid-json' );
+
+ $collection = new WP_Font_Collection( 'my-collection', $mock_file );
+
+ // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Testing error response returned by `load_from_json`, not the underlying error from `wp_json_file_decode`.
+ $data = @$collection->get_data();
+
+ $this->assertWPError( $data, 'Error is not returned with invalid json file contents.' );
+ $this->assertSame(
+ $data->get_error_code(),
+ 'font_collection_decode_error',
+ 'Incorrect error code with invalid json file contents.'
+ );
+ }
+
+ public function test_should_error_with_invalid_url() {
+ $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' );
+
+ $collection = new WP_Font_Collection( 'my-collection', 'not-a-url' );
+ $data = $collection->get_data();
+
+ $this->assertWPError( $data, 'Error is not returned when invalid url is provided.' );
+ $this->assertSame(
+ $data->get_error_code(),
+ 'font_collection_json_missing',
+ 'Incorrect error code when invalid url is provided.'
+ );
+ }
+
+ public function test_should_error_with_unsuccessful_response_status() {
+ add_filter( 'pre_http_request', array( $this, 'mock_request_unsuccessful_response' ), 10, 3 );
+
+ $collection = new WP_Font_Collection( 'my-collection', 'https://example.com/fonts/missing-collection.json' );
+ $data = $collection->get_data();
+
+ remove_filter( 'pre_http_request', array( $this, 'mock_request_unsuccessful_response' ) );
+
+ $this->assertWPError( $data, 'Error is not returned when response is unsuccessful.' );
+ $this->assertSame(
+ $data->get_error_code(),
+ 'font_collection_request_error',
+ 'Incorrect error code when response is unsuccussful.'
+ );
+ }
+
+ public function test_should_error_with_invalid_json_from_url() {
+ add_filter( 'pre_http_request', array( $this, 'mock_request_invalid_json' ), 10, 3 );
+
+ $collection = new WP_Font_Collection( 'my-collection', 'https://example.com/fonts/invalid-collection.json' );
+ $data = $collection->get_data();
+
+ remove_filter( 'pre_http_request', array( $this, 'mock_request_invalid_json' ) );
+
+ $this->assertWPError( $data, 'Error is not returned when response is invalid json.' );
+ $this->assertSame(
+ $data->get_error_code(),
+ 'font_collection_decode_error',
+ 'Incorrect error code when response is invalid json.'
+ );
+ }
+
+ public function mock_request( $preempt, $args, $url ) {
+ if ( 'https://example.com/fonts/mock-font-collection.json' !== $url ) {
+ return false;
+ }
+
+ return array(
+ 'body' => wp_json_encode( self::$mock_collection_data ),
+ 'response' => array(
+ 'code' => 200,
+ ),
+ );
+ }
+
+ public function mock_request_unsuccessful_response( $preempt, $args, $url ) {
+ if ( 'https://example.com/fonts/missing-collection.json' !== $url ) {
+ return false;
+ }
+
+ return array(
+ 'body' => '',
+ 'response' => array(
+ 'code' => 404,
+ ),
+ );
+ }
+
+ public function mock_request_invalid_json( $preempt, $args, $url ) {
+ if ( 'https://example.com/fonts/invalid-collection.json' !== $url ) {
+ return false;
+ }
+
+ return array(
+ 'body' => 'invalid',
+ 'response' => array(
+ 'code' => 200,
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php
new file mode 100644
index 0000000000000..135329e5add73
--- /dev/null
+++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php
@@ -0,0 +1,25 @@
+get_font_collections();
+ foreach ( $collections as $slug => $collection ) {
+ WP_Font_Library::get_instance()->unregister_font_collection( $slug );
+ }
+ }
+
+ public function set_up() {
+ parent::set_up();
+ $this->reset_font_collections();
+ }
+
+ public function tear_down() {
+ parent::tear_down();
+ $this->reset_font_collections();
+ }
+}
diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php
new file mode 100644
index 0000000000000..675efe81aec59
--- /dev/null
+++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php
@@ -0,0 +1,30 @@
+ 'Test Collection',
+ 'font_families' => array( 'mock' ),
+ );
+
+ wp_register_font_collection( 'my-font-collection', $mock_collection_data );
+ $font_collection = WP_Font_Library::get_instance()->get_font_collection( 'my-font-collection' );
+ $this->assertInstanceOf( 'WP_Font_Collection', $font_collection );
+ }
+
+ public function test_should_get_no_font_collection_if_the_slug_is_not_registered() {
+ $font_collection = WP_Font_Library::get_instance()->get_font_collection( 'not-registered-font-collection' );
+ $this->assertWPError( $font_collection );
+ }
+}
diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php
new file mode 100644
index 0000000000000..f5ca6389b8ff5
--- /dev/null
+++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php
@@ -0,0 +1,34 @@
+get_font_collections();
+ $this->assertEmpty( $font_collections, 'Should return an empty array.' );
+ }
+
+ public function test_should_get_mock_font_collection() {
+ $my_font_collection_config = array(
+ 'name' => 'My Font Collection',
+ 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
+ 'font_families' => array( 'mock' ),
+ );
+
+ WP_Font_Library::get_instance()->register_font_collection( 'my-font-collection', $my_font_collection_config );
+
+ $font_collections = WP_Font_Library::get_instance()->get_font_collections();
+ $this->assertNotEmpty( $font_collections, 'Should return an array of font collections.' );
+ $this->assertCount( 1, $font_collections, 'Should return an array with one font collection.' );
+ $this->assertArrayHasKey( 'my-font-collection', $font_collections, 'The array should have the key of the registered font collection id.' );
+ $this->assertInstanceOf( 'WP_Font_Collection', $font_collections['my-font-collection'], 'The value of the array $font_collections[id] should be an instance of WP_Font_Collection class.' );
+ }
+}
diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php
new file mode 100644
index 0000000000000..d3b0f126e2e7e
--- /dev/null
+++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php
@@ -0,0 +1,40 @@
+ 'My Collection',
+ 'font_families' => array( 'mock' ),
+ );
+
+ $collection = WP_Font_Library::get_instance()->register_font_collection( 'my-collection', $config );
+ $this->assertInstanceOf( 'WP_Font_Collection', $collection );
+ }
+
+ public function test_should_return_error_if_slug_is_repeated() {
+ $mock_collection_data = array(
+ 'name' => 'Test Collection',
+ 'font_families' => array( 'mock' ),
+ );
+
+ // Register first collection.
+ $collection1 = WP_Font_Library::get_instance()->register_font_collection( 'my-collection-1', $mock_collection_data );
+ $this->assertInstanceOf( 'WP_Font_Collection', $collection1, 'A collection should be registered.' );
+
+ // Expects a _doing_it_wrong notice.
+ $this->setExpectedIncorrectUsage( 'WP_Font_Library::register_font_collection' );
+
+ // Try to register a second collection with same slug.
+ WP_Font_Library::get_instance()->register_font_collection( 'my-collection-1', $mock_collection_data );
+ }
+}
diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php
new file mode 100644
index 0000000000000..ddb0fa91c1d60
--- /dev/null
+++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php
@@ -0,0 +1,46 @@
+ 'Test Collection',
+ 'font_families' => array( 'mock' ),
+ );
+
+ // Registers two mock font collections.
+ WP_Font_Library::get_instance()->register_font_collection( 'mock-font-collection-1', $mock_collection_data );
+ WP_Font_Library::get_instance()->register_font_collection( 'mock-font-collection-2', $mock_collection_data );
+
+ // Unregister mock font collection.
+ WP_Font_Library::get_instance()->unregister_font_collection( 'mock-font-collection-1' );
+ $collections = WP_Font_Library::get_instance()->get_font_collections();
+ $this->assertArrayNotHasKey( 'mock-font-collection-1', $collections, 'Font collection was not unregistered.' );
+ $this->assertArrayHasKey( 'mock-font-collection-2', $collections, 'Font collection was unregistered by mistake.' );
+
+ // Unregisters remaining mock font collection.
+ WP_Font_Library::get_instance()->unregister_font_collection( 'mock-font-collection-2' );
+ $collections = WP_Font_Library::get_instance()->get_font_collections();
+ $this->assertArrayNotHasKey( 'mock-font-collection-2', $collections, 'Mock font collection was not unregistered.' );
+
+ // Checks that all font collections were unregistered.
+ $this->assertEmpty( $collections, 'Font collections were not unregistered.' );
+ }
+
+ public function unregister_non_existing_collection() {
+ // Unregisters non-existing font collection.
+ WP_Font_Library::get_instance()->unregister_font_collection( 'non-existing-collection' );
+ $collections = WP_Font_Library::get_instance()->get_font_collections();
+ $this->assertEmpty( $collections, 'No collections should be registered.' );
+ }
+}
diff --git a/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php b/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php
new file mode 100644
index 0000000000000..de0b02e63185e
--- /dev/null
+++ b/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php
@@ -0,0 +1,92 @@
+assertSame( $expected_slug, $slug );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_get_font_face_slug_normalizes_values() {
+ return array(
+ 'Sets defaults' => array(
+ 'settings' => array(
+ 'fontFamily' => 'Open Sans',
+ ),
+ 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
+ ),
+ 'Converts normal weight to 400' => array(
+ 'settings' => array(
+ 'fontFamily' => 'Open Sans',
+ 'fontWeight' => 'normal',
+ ),
+ 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
+ ),
+ 'Converts bold weight to 700' => array(
+ 'settings' => array(
+ 'fontFamily' => 'Open Sans',
+ 'fontWeight' => 'bold',
+ ),
+ 'expected_slug' => 'open sans;normal;700;100%;U+0-10FFFF',
+ ),
+ 'Converts normal font-stretch to 100%' => array(
+ 'settings' => array(
+ 'fontFamily' => 'Open Sans',
+ 'fontStretch' => 'normal',
+ ),
+ 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
+ ),
+ 'Removes double quotes from fontFamilies' => array(
+ 'settings' => array(
+ 'fontFamily' => '"Open Sans"',
+ ),
+ 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
+ ),
+ 'Removes single quotes from fontFamilies' => array(
+ 'settings' => array(
+ 'fontFamily' => "'Open Sans'",
+ ),
+ 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF',
+ ),
+ 'Removes spaces between comma separated font families' => array(
+ 'settings' => array(
+ 'fontFamily' => 'Open Sans, serif',
+ ),
+ 'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF',
+ ),
+ 'Removes tabs between comma separated font families' => array(
+ 'settings' => array(
+ 'fontFamily' => "Open Sans,\tserif",
+ ),
+ 'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF',
+ ),
+ 'Removes new lines between comma separated font families' => array(
+ 'settings' => array(
+ 'fontFamily' => "Open Sans,\nserif",
+ ),
+ 'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF',
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php b/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php
new file mode 100644
index 0000000000000..71511331c65dc
--- /dev/null
+++ b/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php
@@ -0,0 +1,63 @@
+assertSame(
+ $expected,
+ WP_Font_Utils::sanitize_font_family(
+ $font_family
+ )
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_should_sanitize_font_family() {
+ return array(
+ 'data_families_with_spaces_and_numbers' => array(
+ 'font_family' => 'Rock 3D , Open Sans,serif',
+ 'expected' => '"Rock 3D", "Open Sans", serif',
+ ),
+ 'data_single_font_family' => array(
+ 'font_family' => 'Rock 3D',
+ 'expected' => '"Rock 3D"',
+ ),
+ 'data_no_spaces' => array(
+ 'font_family' => 'Rock3D',
+ 'expected' => 'Rock3D',
+ ),
+ 'data_many_spaces_and_existing_quotes' => array(
+ 'font_family' => 'Rock 3D serif, serif,sans-serif, "Open Sans"',
+ 'expected' => '"Rock 3D serif", serif, sans-serif, "Open Sans"',
+ ),
+ 'data_empty_family' => array(
+ 'font_family' => ' ',
+ 'expected' => '',
+ ),
+ 'data_font_family_with_whitespace_tags_new_lines' => array(
+ 'font_family' => " Rock 3D\n ",
+ 'expected' => '"Rock 3D"',
+ ),
+ );
+ }
+}
diff --git a/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php b/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php
new file mode 100644
index 0000000000000..88983fe15a14e
--- /dev/null
+++ b/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php
@@ -0,0 +1,310 @@
+assertSame( $result, $expected );
+ }
+
+ public function data_sanitize_from_schema() {
+ return array(
+ 'One level associative array' => array(
+ 'data' => array(
+ 'slug' => 'open - sans',
+ 'fontFamily' => 'Open Sans, sans-serif',
+ 'src' => 'https://wordpress.org/example.json',
+ ),
+ 'schema' => array(
+ 'slug' => 'sanitize_title',
+ 'fontFamily' => 'sanitize_text_field',
+ 'src' => 'sanitize_url',
+ ),
+ 'expected' => array(
+ 'slug' => 'open-sansalertxss',
+ 'fontFamily' => 'Open Sans, sans-serif',
+ 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script',
+ ),
+ ),
+
+ 'Nested associative arrays' => array(
+ 'data' => array(
+ 'slug' => 'open - sans',
+ 'fontFamily' => 'Open Sans, sans-serif',
+ 'src' => 'https://wordpress.org/example.json',
+ 'nested' => array(
+ 'key1' => 'value1',
+ 'key2' => 'value2',
+ 'nested2' => array(
+ 'key3' => 'value3',
+ 'key4' => 'value4',
+ ),
+ ),
+ ),
+ 'schema' => array(
+ 'slug' => 'sanitize_title',
+ 'fontFamily' => 'sanitize_text_field',
+ 'src' => 'sanitize_url',
+ 'nested' => array(
+ 'key1' => 'sanitize_text_field',
+ 'key2' => 'sanitize_text_field',
+ 'nested2' => array(
+ 'key3' => 'sanitize_text_field',
+ 'key4' => 'sanitize_text_field',
+ ),
+ ),
+ ),
+ 'expected' => array(
+ 'slug' => 'open-sansalertxss',
+ 'fontFamily' => 'Open Sans, sans-serif',
+ 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script',
+ 'nested' => array(
+ 'key1' => 'value1',
+ 'key2' => 'value2',
+ 'nested2' => array(
+ 'key3' => 'value3',
+ 'key4' => 'value4',
+ ),
+ ),
+ ),
+ ),
+
+ 'Indexed arrays' => array(
+ 'data' => array(
+ 'slug' => 'oPeN SaNs',
+ 'enum' => array(
+ 'value1',
+ 'value2',
+ 'value3',
+ ),
+ ),
+ 'schema' => array(
+ 'slug' => 'sanitize_title',
+ 'enum' => array( 'sanitize_text_field' ),
+ ),
+ 'expected' => array(
+ 'slug' => 'open-sans',
+ 'enum' => array( 'value1', 'value2', 'value3' ),
+ ),
+ ),
+
+ 'Nested indexed arrays' => array(
+ 'data' => array(
+ 'slug' => 'OPEN-SANS',
+ 'name' => 'Open Sans',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Open Sans, sans-serif',
+ 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script',
+ ),
+ array(
+ 'fontFamily' => 'Open Sans, sans-serif',
+ 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script',
+ ),
+ ),
+ ),
+ 'schema' => array(
+ 'slug' => 'sanitize_title',
+ 'name' => 'sanitize_text_field',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'sanitize_text_field',
+ 'src' => 'sanitize_url',
+ ),
+ ),
+ ),
+ 'expected' => array(
+ 'slug' => 'open-sans',
+ 'name' => 'Open Sans',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Open Sans, sans-serif',
+ 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script',
+ ),
+ array(
+ 'fontFamily' => 'Open Sans, sans-serif',
+ 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script',
+ ),
+ ),
+ ),
+ ),
+
+ 'Custom sanitization function' => array(
+ 'data' => array(
+ 'key1' => 'abc123edf456ghi789',
+ 'key2' => 'value2',
+ ),
+ 'schema' => array(
+ 'key1' => function ( $value ) {
+ // Remove the six first character.
+ return substr( $value, 6 );
+ },
+ 'key2' => function ( $value ) {
+ // Capitalize the value.
+ return strtoupper( $value );
+ },
+ ),
+ 'expected' => array(
+ 'key1' => 'edf456ghi789',
+ 'key2' => 'VALUE2',
+ ),
+ ),
+
+ 'Null as schema value' => array(
+ 'data' => array(
+ 'key1' => 'value1',
+ 'key2' => 'value2',
+ 'nested' => array(
+ 'key3' => 'value3',
+ 'key4' => 'value4',
+ ),
+ ),
+ 'schema' => array(
+ 'key1' => null,
+ 'key2' => 'sanitize_text_field',
+ 'nested' => null,
+ ),
+ 'expected' => array(
+ 'key1' => 'value1',
+ 'key2' => 'value2',
+ 'nested' => array(
+ 'key3' => 'value3',
+ 'key4' => 'value4',
+ ),
+ ),
+ ),
+
+ 'Keys to remove' => array(
+ 'data' => array(
+ 'key1' => 'value1',
+ 'key2' => 'value2',
+ 'unwanted1' => 'value',
+ 'unwanted2' => 'value',
+ 'nestedAssociative' => array(
+ 'key5' => 'value5',
+ 'unwanted3' => 'value',
+ ),
+ 'nestedIndexed' => array(
+ array(
+ 'key6' => 'value7',
+ 'unwanted4' => 'value',
+ ),
+ array(
+ 'key6' => 'value7',
+ 'unwanted5' => 'value',
+ ),
+ ),
+
+ ),
+ 'schema' => array(
+ 'key1' => 'sanitize_text_field',
+ 'key2' => 'sanitize_text_field',
+ 'nestedAssociative' => array(
+ 'key5' => 'sanitize_text_field',
+ ),
+ 'nestedIndexed' => array(
+ array(
+ 'key6' => 'sanitize_text_field',
+ ),
+ ),
+ ),
+ 'expected' => array(
+ 'key1' => 'value1',
+ 'key2' => 'value2',
+ 'nestedAssociative' => array(
+ 'key5' => 'value5',
+ ),
+ 'nestedIndexed' => array(
+ array(
+ 'key6' => 'value7',
+ ),
+ array(
+ 'key6' => 'value7',
+ ),
+ ),
+ ),
+ ),
+
+ 'With empty structure' => array(
+ 'data' => array(
+ 'slug' => 'open-sans',
+ 'nested' => array(
+ 'key1' => 'value',
+ 'nested2' => array(
+ 'key2' => 'value',
+ 'nested3' => array(
+ 'nested4' => array(),
+ ),
+ ),
+ ),
+ ),
+ 'schema' => array(
+ 'slug' => 'sanitize_title',
+ 'nested' => array(
+ 'key1' => 'sanitize_text_field',
+ 'nested2' => array(
+ 'key2' => 'sanitize_text_field',
+ 'nested3' => array(
+ 'key3' => 'sanitize_text_field',
+ 'nested4' => array(
+ 'key4' => 'sanitize_text_field',
+ ),
+ ),
+ ),
+ ),
+ ),
+ 'expected' => array(
+ 'slug' => 'open-sans',
+ 'nested' => array(
+ 'key1' => 'value',
+ 'nested2' => array(
+ 'key2' => 'value',
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ public function test_sanitize_from_schema_with_invalid_data() {
+ $data = 'invalid data';
+ $schema = array(
+ 'key1' => 'sanitize_text_field',
+ 'key2' => 'sanitize_text_field',
+ );
+
+ $result = WP_Font_Utils::sanitize_from_schema( $data, $schema );
+
+ $this->assertSame( $result, array() );
+ }
+
+
+ public function test_sanitize_from_schema_with_invalid_schema() {
+ $data = array(
+ 'key1' => 'value1',
+ 'key2' => 'value2',
+ );
+ $schema = 'invalid schema';
+
+ $result = WP_Font_Utils::sanitize_from_schema( $data, $schema );
+
+ $this->assertSame( $result, array() );
+ }
+}
diff --git a/tests/phpunit/tests/fonts/font-library/wpFontsDir.php b/tests/phpunit/tests/fonts/font-library/wpFontsDir.php
new file mode 100644
index 0000000000000..a8f79888315bd
--- /dev/null
+++ b/tests/phpunit/tests/fonts/font-library/wpFontsDir.php
@@ -0,0 +1,72 @@
+ path_join( WP_CONTENT_DIR, 'fonts' ),
+ 'url' => content_url( 'fonts' ),
+ 'subdir' => '',
+ 'basedir' => path_join( WP_CONTENT_DIR, 'fonts' ),
+ 'baseurl' => content_url( 'fonts' ),
+ 'error' => false,
+ );
+ }
+
+ public function test_fonts_dir() {
+ $font_dir = wp_get_font_dir();
+
+ $this->assertSame( $font_dir, static::$dir_defaults );
+ }
+
+ public function test_fonts_dir_with_filter() {
+ // Define a callback function to pass to the filter.
+ function set_new_values( $defaults ) {
+ $defaults['path'] = '/custom-path/fonts/my-custom-subdir';
+ $defaults['url'] = 'http://example.com/custom-path/fonts/my-custom-subdir';
+ $defaults['subdir'] = 'my-custom-subdir';
+ $defaults['basedir'] = '/custom-path/fonts';
+ $defaults['baseurl'] = 'http://example.com/custom-path/fonts';
+ $defaults['error'] = false;
+ return $defaults;
+ }
+
+ // Add the filter.
+ add_filter( 'font_dir', 'set_new_values' );
+
+ // Gets the fonts dir.
+ $font_dir = wp_get_font_dir();
+
+ $expected = array(
+ 'path' => '/custom-path/fonts/my-custom-subdir',
+ 'url' => 'http://example.com/custom-path/fonts/my-custom-subdir',
+ 'subdir' => 'my-custom-subdir',
+ 'basedir' => '/custom-path/fonts',
+ 'baseurl' => 'http://example.com/custom-path/fonts',
+ 'error' => false,
+ );
+
+ // Remove the filter.
+ remove_filter( 'font_dir', 'set_new_values' );
+
+ $this->assertSame( $expected, $font_dir, 'The wp_get_font_dir() method should return the expected values.' );
+
+ // Gets the fonts dir.
+ $font_dir = wp_get_font_dir();
+
+ $this->assertSame( static::$dir_defaults, $font_dir, 'The wp_get_font_dir() method should return the default values.' );
+ }
+}