diff options
Diffstat (limited to 'wp-includes/fonts')
-rw-r--r-- | wp-includes/fonts/class-wp-font-collection.php | 295 | ||||
-rw-r--r-- | wp-includes/fonts/class-wp-font-face-resolver.php | 7 | ||||
-rw-r--r-- | wp-includes/fonts/class-wp-font-library.php | 142 | ||||
-rw-r--r-- | wp-includes/fonts/class-wp-font-utils.php | 259 |
4 files changed, 697 insertions, 6 deletions
diff --git a/wp-includes/fonts/class-wp-font-collection.php b/wp-includes/fonts/class-wp-font-collection.php new file mode 100644 index 0000000..b915e3e --- /dev/null +++ b/wp-includes/fonts/class-wp-font-collection.php @@ -0,0 +1,295 @@ +<?php +/** + * Font Collection class. + * + * This file contains the Font Collection class definition. + * + * @package WordPress + * @subpackage Fonts + * @since 6.5.0 + */ + +/** + * Font Collection class. + * + * @since 6.5.0 + * + * @see wp_register_font_collection() + */ +final class WP_Font_Collection { + /** + * The unique slug for the font collection. + * + * @since 6.5.0 + * @var string + */ + public $slug; + + /** + * Font collection data. + * + * @since 6.5.0 + * @var array|WP_Error|null + */ + private $data; + + /** + * Font collection JSON file path or URL. + * + * @since 6.5.0 + * @var string|null + */ + private $src; + + /** + * WP_Font_Collection constructor. + * + * @since 6.5.0 + * + * @param string $slug Font collection slug. May only contain alphanumeric characters, dashes, + * and underscores. See sanitize_title(). + * @param array $args Font collection data. See wp_register_font_collection() for information on accepted arguments. + */ + public function __construct( string $slug, array $args ) { + $this->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' + ); + } + + $required_properties = array( 'name', 'font_families' ); + + if ( isset( $args['font_families'] ) && is_string( $args['font_families'] ) ) { + // JSON data is lazy loaded by ::get_data(). + $this->src = $args['font_families']; + unset( $args['font_families'] ); + + $required_properties = array( 'name' ); + } + + $this->data = $this->sanitize_and_validate_data( $args, $required_properties ); + } + + /** + * 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 ( is_wp_error( $this->data ) ) { + return $this->data; + } + + // If the collection uses JSON data, load it and cache the data/error. + if ( isset( $this->src ) ) { + $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 ); + } + + $data = $url ? $this->load_from_url( $url ) : $this->load_from_file( $file ); + + if ( is_wp_error( $data ) ) { + return $data; + } + + $data = array( + 'name' => $this->data['name'], + 'font_families' => $data['font_families'], + ); + + if ( isset( $this->data['description'] ) ) { + $data['description'] = $this->data['description']; + } + + if ( isset( $this->data['categories'] ) ) { + $data['categories'] = $this->data['categories']; + } + + return $data; + } + + /** + * 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, array( 'font_families' ) ); + } + + /** + * 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 ) ) { + return new WP_Error( + 'font_collection_request_error', + sprintf( + // translators: %s: Font collection URL. + __( '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, array( 'font_families' ) ); + 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. + * @param array $required_properties Required properties that must exist in the passed data. + * @return array|WP_Error Sanitized data if valid, otherwise a WP_Error instance. + */ + private function sanitize_and_validate_data( $data, $required_properties = array() ) { + $schema = self::get_sanitization_schema(); + $data = WP_Font_Utils::sanitize_from_schema( $data, $schema ); + + 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' => static function ( $value ) { + return _wp_to_kebab_case( sanitize_title( $value ) ); + }, + 'fontFamily' => 'WP_Font_Utils::sanitize_font_family', + '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/wp-includes/fonts/class-wp-font-face-resolver.php b/wp-includes/fonts/class-wp-font-face-resolver.php index 125ff6a..12245af 100644 --- a/wp-includes/fonts/class-wp-font-face-resolver.php +++ b/wp-includes/fonts/class-wp-font-face-resolver.php @@ -67,12 +67,7 @@ class WP_Font_Face_Resolver { continue; } - // Prepare the fonts array structure for this font-family. - if ( ! array_key_exists( $font_family_name, $fonts ) ) { - $fonts[ $font_family_name ] = array(); - } - - $fonts[ $font_family_name ] = static::convert_font_face_properties( $definition['fontFace'], $font_family_name ); + $fonts[] = static::convert_font_face_properties( $definition['fontFace'], $font_family_name ); } } diff --git a/wp-includes/fonts/class-wp-font-library.php b/wp-includes/fonts/class-wp-font-library.php new file mode 100644 index 0000000..3069ecb --- /dev/null +++ b/wp-includes/fonts/class-wp-font-library.php @@ -0,0 +1,142 @@ +<?php +/** + * Font Library class. + * + * This file contains the Font Library class definition. + * + * @package WordPress + * @subpackage Fonts + * @since 6.5.0 + */ + +/** + * Font Library class. + * + * @since 6.5.0 + */ +class WP_Font_Library { + + /** + * Font collections. + * + * @since 6.5.0 + * @var array + */ + private $collections = array(); + + /** + * Container for the main instance of the class. + * + * @since 6.5.0 + * @var WP_Font_Library|null + */ + private static $instance = null; + + /** + * Register a new font collection. + * + * @since 6.5.0 + * + * @param string $slug Font collection slug. May only contain alphanumeric characters, dashes, + * and underscores. See sanitize_title(). + * @param array $args Font collection data. See wp_register_font_collection() for information on accepted arguments. + * @return WP_Font_Collection|WP_Error A font collection if it was registered successfully, + * or WP_Error object on failure. + */ + public function register_font_collection( string $slug, array $args ) { + $new_collection = new WP_Font_Collection( $slug, $args ); + + if ( $this->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( string $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( string $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|null Font collection object, or null if the font collection doesn't exist. + */ + public function get_font_collection( string $slug ) { + if ( $this->is_collection_registered( $slug ) ) { + return $this->collections[ $slug ]; + } + return null; + } + + /** + * 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/wp-includes/fonts/class-wp-font-utils.php b/wp-includes/fonts/class-wp-font-utils.php new file mode 100644 index 0000000..6e91440 --- /dev/null +++ b/wp-includes/fonts/class-wp-font-utils.php @@ -0,0 +1,259 @@ +<?php +/** + * Font Utils class. + * + * Provides utility functions for working with font families. + * + * @package WordPress + * @subpackage Fonts + * @since 6.5.0 + */ + +/** + * A class of utilities for working with the Font Library. + * + * These utilities may change or be removed in the future and are intended for internal use only. + * + * @since 6.5.0 + * @access private + */ +class WP_Font_Utils { + /** + * Adds surrounding quotes to font family names that contain special characters. + * + * It follows the recommendations from the CSS Fonts Module Level 4. + * @link https://www.w3.org/TR/css-fonts-4/#font-family-prop + * + * @since 6.5.0 + * + * @param string $item A font family name. + * @return string The font family name with surrounding quotes, if necessary. + */ + private static function maybe_add_quotes( $item ) { + // Matches strings that are not exclusively alphabetic characters or hyphens, and do not exactly follow the pattern generic(alphabetic characters or hyphens). + $regex = '/^(?!generic\([a-zA-Z\-]+\)$)(?!^[a-zA-Z\-]+$).+/'; + $item = trim( $item ); + if ( preg_match( $regex, $item ) ) { + $item = trim( $item, "\"'" ); + return '"' . $item . '"'; + } + return $item; + } + + /** + * Sanitizes and formats font family names. + * + * - Applies `sanitize_text_field`. + * - Adds surrounding quotes to names containing any characters that are not alphabetic or dashes. + * + * It follows the recommendations from the CSS Fonts Module Level 4. + * @link https://www.w3.org/TR/css-fonts-4/#font-family-prop + * + * @since 6.5.0 + * @access private + * + * @see sanitize_text_field() + * + * @param string $font_family Font family name(s), comma-separated. + * @return string Sanitized and formatted font family name(s). + */ + public static function sanitize_font_family( $font_family ) { + if ( ! $font_family ) { + return ''; + } + + $output = sanitize_text_field( $font_family ); + $formatted_items = array(); + if ( str_contains( $output, ',' ) ) { + $items = explode( ',', $output ); + foreach ( $items as $item ) { + $formatted_item = self::maybe_add_quotes( $item ); + if ( ! empty( $formatted_item ) ) { + $formatted_items[] = $formatted_item; + } + } + return implode( ', ', $formatted_items ); + } + return self::maybe_add_quotes( $output ); + } + + /** + * Generates a slug from font face properties, e.g. `open sans;normal;400;100%;U+0-10FFFF` + * + * Used for comparison with other font faces in the same family, to prevent duplicates + * that would both match according the CSS font matching spec. Uses only simple case-insensitive + * matching for fontFamily and unicodeRange, so does not handle overlapping font-family lists or + * unicode ranges. + * + * @since 6.5.0 + * @access private + * + * @link https://drafts.csswg.org/css-fonts/#font-style-matching + * + * @param array $settings { + * Font face settings. + * + * @type string $fontFamily Font family name. + * @type string $fontStyle Optional font style, defaults to 'normal'. + * @type string $fontWeight Optional font weight, defaults to 400. + * @type string $fontStretch Optional font stretch, defaults to '100%'. + * @type string $unicodeRange Optional unicode range, defaults to 'U+0-10FFFF'. + * } + * @return string Font face slug. + */ + public static function get_font_face_slug( $settings ) { + $defaults = array( + 'fontFamily' => '', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'fontStretch' => '100%', + 'unicodeRange' => 'U+0-10FFFF', + ); + $settings = wp_parse_args( $settings, $defaults ); + if ( function_exists( 'mb_strtolower' ) ) { + $font_family = mb_strtolower( $settings['fontFamily'] ); + } else { + $font_family = 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( implode( ';', $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 sanitized 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 >= 80112 ? 'font/woff' : 'application/font-woff', + 'woff2' => PHP_VERSION_ID >= 80112 ? 'font/woff2' : 'application/font-woff2', + ); + } +} |