wp/wp-includes/fonts/class-wp-font-utils.php
changeset 21 48c4eec2b7e6
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
       
     1 <?php
       
     2 /**
       
     3  * Font Utils class.
       
     4  *
       
     5  * Provides utility functions for working with font families.
       
     6  *
       
     7  * @package    WordPress
       
     8  * @subpackage Fonts
       
     9  * @since      6.5.0
       
    10  */
       
    11 
       
    12 /**
       
    13  * A class of utilities for working with the Font Library.
       
    14  *
       
    15  * These utilities may change or be removed in the future and are intended for internal use only.
       
    16  *
       
    17  * @since 6.5.0
       
    18  * @access private
       
    19  */
       
    20 class WP_Font_Utils {
       
    21 	/**
       
    22 	 * Adds surrounding quotes to font family names that contain special characters.
       
    23 	 *
       
    24 	 * It follows the recommendations from the CSS Fonts Module Level 4.
       
    25 	 * @link https://www.w3.org/TR/css-fonts-4/#font-family-prop
       
    26 	 *
       
    27 	 * @since 6.5.0
       
    28 	 *
       
    29 	 * @param string $item A font family name.
       
    30 	 * @return string The font family name with surrounding quotes, if necessary.
       
    31 	 */
       
    32 	private static function maybe_add_quotes( $item ) {
       
    33 		// Matches strings that are not exclusively alphabetic characters or hyphens, and do not exactly follow the pattern generic(alphabetic characters or hyphens).
       
    34 		$regex = '/^(?!generic\([a-zA-Z\-]+\)$)(?!^[a-zA-Z\-]+$).+/';
       
    35 		$item  = trim( $item );
       
    36 		if ( preg_match( $regex, $item ) ) {
       
    37 			$item = trim( $item, "\"'" );
       
    38 			return '"' . $item . '"';
       
    39 		}
       
    40 		return $item;
       
    41 	}
       
    42 
       
    43 	/**
       
    44 	 * Sanitizes and formats font family names.
       
    45 	 *
       
    46 	 * - Applies `sanitize_text_field`.
       
    47 	 * - Adds surrounding quotes to names containing any characters that are not alphabetic or dashes.
       
    48 	 *
       
    49 	 * It follows the recommendations from the CSS Fonts Module Level 4.
       
    50 	 * @link https://www.w3.org/TR/css-fonts-4/#font-family-prop
       
    51 	 *
       
    52 	 * @since 6.5.0
       
    53 	 * @access private
       
    54 	 *
       
    55 	 * @see sanitize_text_field()
       
    56 	 *
       
    57 	 * @param string $font_family Font family name(s), comma-separated.
       
    58 	 * @return string Sanitized and formatted font family name(s).
       
    59 	 */
       
    60 	public static function sanitize_font_family( $font_family ) {
       
    61 		if ( ! $font_family ) {
       
    62 			return '';
       
    63 		}
       
    64 
       
    65 		$output          = sanitize_text_field( $font_family );
       
    66 		$formatted_items = array();
       
    67 		if ( str_contains( $output, ',' ) ) {
       
    68 			$items = explode( ',', $output );
       
    69 			foreach ( $items as $item ) {
       
    70 				$formatted_item = self::maybe_add_quotes( $item );
       
    71 				if ( ! empty( $formatted_item ) ) {
       
    72 					$formatted_items[] = $formatted_item;
       
    73 				}
       
    74 			}
       
    75 			return implode( ', ', $formatted_items );
       
    76 		}
       
    77 		return self::maybe_add_quotes( $output );
       
    78 	}
       
    79 
       
    80 	/**
       
    81 	 * Generates a slug from font face properties, e.g. `open sans;normal;400;100%;U+0-10FFFF`
       
    82 	 *
       
    83 	 * Used for comparison with other font faces in the same family, to prevent duplicates
       
    84 	 * that would both match according the CSS font matching spec. Uses only simple case-insensitive
       
    85 	 * matching for fontFamily and unicodeRange, so does not handle overlapping font-family lists or
       
    86 	 * unicode ranges.
       
    87 	 *
       
    88 	 * @since 6.5.0
       
    89 	 * @access private
       
    90 	 *
       
    91 	 * @link https://drafts.csswg.org/css-fonts/#font-style-matching
       
    92 	 *
       
    93 	 * @param array $settings {
       
    94 	 *     Font face settings.
       
    95 	 *
       
    96 	 *     @type string $fontFamily   Font family name.
       
    97 	 *     @type string $fontStyle    Optional font style, defaults to 'normal'.
       
    98 	 *     @type string $fontWeight   Optional font weight, defaults to 400.
       
    99 	 *     @type string $fontStretch  Optional font stretch, defaults to '100%'.
       
   100 	 *     @type string $unicodeRange Optional unicode range, defaults to 'U+0-10FFFF'.
       
   101 	 * }
       
   102 	 * @return string Font face slug.
       
   103 	 */
       
   104 	public static function get_font_face_slug( $settings ) {
       
   105 		$defaults = array(
       
   106 			'fontFamily'   => '',
       
   107 			'fontStyle'    => 'normal',
       
   108 			'fontWeight'   => '400',
       
   109 			'fontStretch'  => '100%',
       
   110 			'unicodeRange' => 'U+0-10FFFF',
       
   111 		);
       
   112 		$settings = wp_parse_args( $settings, $defaults );
       
   113 		if ( function_exists( 'mb_strtolower' ) ) {
       
   114 			$font_family = mb_strtolower( $settings['fontFamily'] );
       
   115 		} else {
       
   116 			$font_family = strtolower( $settings['fontFamily'] );
       
   117 		}
       
   118 		$font_style    = strtolower( $settings['fontStyle'] );
       
   119 		$font_weight   = strtolower( $settings['fontWeight'] );
       
   120 		$font_stretch  = strtolower( $settings['fontStretch'] );
       
   121 		$unicode_range = strtoupper( $settings['unicodeRange'] );
       
   122 
       
   123 		// Convert weight keywords to numeric strings.
       
   124 		$font_weight = str_replace( array( 'normal', 'bold' ), array( '400', '700' ), $font_weight );
       
   125 
       
   126 		// Convert stretch keywords to numeric strings.
       
   127 		$font_stretch_map = array(
       
   128 			'ultra-condensed' => '50%',
       
   129 			'extra-condensed' => '62.5%',
       
   130 			'condensed'       => '75%',
       
   131 			'semi-condensed'  => '87.5%',
       
   132 			'normal'          => '100%',
       
   133 			'semi-expanded'   => '112.5%',
       
   134 			'expanded'        => '125%',
       
   135 			'extra-expanded'  => '150%',
       
   136 			'ultra-expanded'  => '200%',
       
   137 		);
       
   138 		$font_stretch     = str_replace( array_keys( $font_stretch_map ), array_values( $font_stretch_map ), $font_stretch );
       
   139 
       
   140 		$slug_elements = array( $font_family, $font_style, $font_weight, $font_stretch, $unicode_range );
       
   141 
       
   142 		$slug_elements = array_map(
       
   143 			function ( $elem ) {
       
   144 				// Remove quotes to normalize font-family names, and ';' to use as a separator.
       
   145 				$elem = trim( str_replace( array( '"', "'", ';' ), '', $elem ) );
       
   146 
       
   147 				// Normalize comma separated lists by removing whitespace in between items,
       
   148 				// but keep whitespace within items (e.g. "Open Sans" and "OpenSans" are different fonts).
       
   149 				// CSS spec for whitespace includes: U+000A LINE FEED, U+0009 CHARACTER TABULATION, or U+0020 SPACE,
       
   150 				// which by default are all matched by \s in PHP.
       
   151 				return preg_replace( '/,\s+/', ',', $elem );
       
   152 			},
       
   153 			$slug_elements
       
   154 		);
       
   155 
       
   156 		return sanitize_text_field( implode( ';', $slug_elements ) );
       
   157 	}
       
   158 
       
   159 	/**
       
   160 	 * Sanitizes a tree of data using a schema.
       
   161 	 *
       
   162 	 * The schema structure should mirror the data tree. Each value provided in the
       
   163 	 * schema should be a callable that will be applied to sanitize the corresponding
       
   164 	 * value in the data tree. Keys that are in the data tree, but not present in the
       
   165 	 * schema, will be removed in the sanitized data. Nested arrays are traversed recursively.
       
   166 	 *
       
   167 	 * @since 6.5.0
       
   168 	 *
       
   169 	 * @access private
       
   170 	 *
       
   171 	 * @param array $tree   The data to sanitize.
       
   172 	 * @param array $schema The schema used for sanitization.
       
   173 	 * @return array The sanitized data.
       
   174 	 */
       
   175 	public static function sanitize_from_schema( $tree, $schema ) {
       
   176 		if ( ! is_array( $tree ) || ! is_array( $schema ) ) {
       
   177 			return array();
       
   178 		}
       
   179 
       
   180 		foreach ( $tree as $key => $value ) {
       
   181 			// Remove keys not in the schema or with null/empty values.
       
   182 			if ( ! array_key_exists( $key, $schema ) ) {
       
   183 				unset( $tree[ $key ] );
       
   184 				continue;
       
   185 			}
       
   186 
       
   187 			$is_value_array  = is_array( $value );
       
   188 			$is_schema_array = is_array( $schema[ $key ] ) && ! is_callable( $schema[ $key ] );
       
   189 
       
   190 			if ( $is_value_array && $is_schema_array ) {
       
   191 				if ( wp_is_numeric_array( $value ) ) {
       
   192 					// If indexed, process each item in the array.
       
   193 					foreach ( $value as $item_key => $item_value ) {
       
   194 						$tree[ $key ][ $item_key ] = isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] )
       
   195 							? self::sanitize_from_schema( $item_value, $schema[ $key ][0] )
       
   196 							: self::apply_sanitizer( $item_value, $schema[ $key ][0] );
       
   197 					}
       
   198 				} else {
       
   199 					// If it is an associative or indexed array, process as a single object.
       
   200 					$tree[ $key ] = self::sanitize_from_schema( $value, $schema[ $key ] );
       
   201 				}
       
   202 			} elseif ( ! $is_value_array && $is_schema_array ) {
       
   203 				// If the value is not an array but the schema is, remove the key.
       
   204 				unset( $tree[ $key ] );
       
   205 			} elseif ( ! $is_schema_array ) {
       
   206 				// If the schema is not an array, apply the sanitizer to the value.
       
   207 				$tree[ $key ] = self::apply_sanitizer( $value, $schema[ $key ] );
       
   208 			}
       
   209 
       
   210 			// Remove keys with null/empty values.
       
   211 			if ( empty( $tree[ $key ] ) ) {
       
   212 				unset( $tree[ $key ] );
       
   213 			}
       
   214 		}
       
   215 
       
   216 		return $tree;
       
   217 	}
       
   218 
       
   219 	/**
       
   220 	 * Applies a sanitizer function to a value.
       
   221 	 *
       
   222 	 * @since 6.5.0
       
   223 	 *
       
   224 	 * @param mixed    $value     The value to sanitize.
       
   225 	 * @param callable $sanitizer The sanitizer function to apply.
       
   226 	 * @return mixed The sanitized value.
       
   227 	 */
       
   228 	private static function apply_sanitizer( $value, $sanitizer ) {
       
   229 		if ( null === $sanitizer ) {
       
   230 			return $value;
       
   231 
       
   232 		}
       
   233 		return call_user_func( $sanitizer, $value );
       
   234 	}
       
   235 
       
   236 	/**
       
   237 	 * Returns the expected mime-type values for font files, depending on PHP version.
       
   238 	 *
       
   239 	 * This is needed because font mime types vary by PHP version, so checking the PHP version
       
   240 	 * is necessary until a list of valid mime-types for each file extension can be provided to
       
   241 	 * the 'upload_mimes' filter.
       
   242 	 *
       
   243 	 * @since 6.5.0
       
   244 	 *
       
   245 	 * @access private
       
   246 	 *
       
   247 	 * @return string[] A collection of mime types keyed by file extension.
       
   248 	 */
       
   249 	public static function get_allowed_font_mime_types() {
       
   250 		$php_7_ttf_mime_type = PHP_VERSION_ID >= 70300 ? 'application/font-sfnt' : 'application/x-font-ttf';
       
   251 
       
   252 		return array(
       
   253 			'otf'   => 'application/vnd.ms-opentype',
       
   254 			'ttf'   => PHP_VERSION_ID >= 70400 ? 'font/sfnt' : $php_7_ttf_mime_type,
       
   255 			'woff'  => PHP_VERSION_ID >= 80112 ? 'font/woff' : 'application/font-woff',
       
   256 			'woff2' => PHP_VERSION_ID >= 80112 ? 'font/woff2' : 'application/font-woff2',
       
   257 		);
       
   258 	}
       
   259 }