|
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 } |