|
1 <?php |
|
2 /** |
|
3 * Font Collection class. |
|
4 * |
|
5 * This file contains the Font Collection class definition. |
|
6 * |
|
7 * @package WordPress |
|
8 * @subpackage Fonts |
|
9 * @since 6.5.0 |
|
10 */ |
|
11 |
|
12 /** |
|
13 * Font Collection class. |
|
14 * |
|
15 * @since 6.5.0 |
|
16 * |
|
17 * @see wp_register_font_collection() |
|
18 */ |
|
19 final class WP_Font_Collection { |
|
20 /** |
|
21 * The unique slug for the font collection. |
|
22 * |
|
23 * @since 6.5.0 |
|
24 * @var string |
|
25 */ |
|
26 public $slug; |
|
27 |
|
28 /** |
|
29 * Font collection data. |
|
30 * |
|
31 * @since 6.5.0 |
|
32 * @var array|WP_Error|null |
|
33 */ |
|
34 private $data; |
|
35 |
|
36 /** |
|
37 * Font collection JSON file path or URL. |
|
38 * |
|
39 * @since 6.5.0 |
|
40 * @var string|null |
|
41 */ |
|
42 private $src; |
|
43 |
|
44 /** |
|
45 * WP_Font_Collection constructor. |
|
46 * |
|
47 * @since 6.5.0 |
|
48 * |
|
49 * @param string $slug Font collection slug. May only contain alphanumeric characters, dashes, |
|
50 * and underscores. See sanitize_title(). |
|
51 * @param array $args Font collection data. See wp_register_font_collection() for information on accepted arguments. |
|
52 */ |
|
53 public function __construct( string $slug, array $args ) { |
|
54 $this->slug = sanitize_title( $slug ); |
|
55 if ( $this->slug !== $slug ) { |
|
56 _doing_it_wrong( |
|
57 __METHOD__, |
|
58 /* translators: %s: Font collection slug. */ |
|
59 sprintf( __( 'Font collection slug "%s" is not valid. Slugs must use only alphanumeric characters, dashes, and underscores.' ), $slug ), |
|
60 '6.5.0' |
|
61 ); |
|
62 } |
|
63 |
|
64 $required_properties = array( 'name', 'font_families' ); |
|
65 |
|
66 if ( isset( $args['font_families'] ) && is_string( $args['font_families'] ) ) { |
|
67 // JSON data is lazy loaded by ::get_data(). |
|
68 $this->src = $args['font_families']; |
|
69 unset( $args['font_families'] ); |
|
70 |
|
71 $required_properties = array( 'name' ); |
|
72 } |
|
73 |
|
74 $this->data = $this->sanitize_and_validate_data( $args, $required_properties ); |
|
75 } |
|
76 |
|
77 /** |
|
78 * Retrieves the font collection data. |
|
79 * |
|
80 * @since 6.5.0 |
|
81 * |
|
82 * @return array|WP_Error An array containing the font collection data, or a WP_Error on failure. |
|
83 */ |
|
84 public function get_data() { |
|
85 if ( is_wp_error( $this->data ) ) { |
|
86 return $this->data; |
|
87 } |
|
88 |
|
89 // If the collection uses JSON data, load it and cache the data/error. |
|
90 if ( isset( $this->src ) ) { |
|
91 $this->data = $this->load_from_json( $this->src ); |
|
92 } |
|
93 |
|
94 if ( is_wp_error( $this->data ) ) { |
|
95 return $this->data; |
|
96 } |
|
97 |
|
98 // Set defaults for optional properties. |
|
99 $defaults = array( |
|
100 'description' => '', |
|
101 'categories' => array(), |
|
102 ); |
|
103 |
|
104 return wp_parse_args( $this->data, $defaults ); |
|
105 } |
|
106 |
|
107 /** |
|
108 * Loads font collection data from a JSON file or URL. |
|
109 * |
|
110 * @since 6.5.0 |
|
111 * |
|
112 * @param string $file_or_url File path or URL to a JSON file containing the font collection data. |
|
113 * @return array|WP_Error An array containing the font collection data on success, |
|
114 * else an instance of WP_Error on failure. |
|
115 */ |
|
116 private function load_from_json( $file_or_url ) { |
|
117 $url = wp_http_validate_url( $file_or_url ); |
|
118 $file = file_exists( $file_or_url ) ? wp_normalize_path( realpath( $file_or_url ) ) : false; |
|
119 |
|
120 if ( ! $url && ! $file ) { |
|
121 // translators: %s: File path or URL to font collection JSON file. |
|
122 $message = __( 'Font collection JSON file is invalid or does not exist.' ); |
|
123 _doing_it_wrong( __METHOD__, $message, '6.5.0' ); |
|
124 return new WP_Error( 'font_collection_json_missing', $message ); |
|
125 } |
|
126 |
|
127 $data = $url ? $this->load_from_url( $url ) : $this->load_from_file( $file ); |
|
128 |
|
129 if ( is_wp_error( $data ) ) { |
|
130 return $data; |
|
131 } |
|
132 |
|
133 $data = array( |
|
134 'name' => $this->data['name'], |
|
135 'font_families' => $data['font_families'], |
|
136 ); |
|
137 |
|
138 if ( isset( $this->data['description'] ) ) { |
|
139 $data['description'] = $this->data['description']; |
|
140 } |
|
141 |
|
142 if ( isset( $this->data['categories'] ) ) { |
|
143 $data['categories'] = $this->data['categories']; |
|
144 } |
|
145 |
|
146 return $data; |
|
147 } |
|
148 |
|
149 /** |
|
150 * Loads the font collection data from a JSON file path. |
|
151 * |
|
152 * @since 6.5.0 |
|
153 * |
|
154 * @param string $file File path to a JSON file containing the font collection data. |
|
155 * @return array|WP_Error An array containing the font collection data on success, |
|
156 * else an instance of WP_Error on failure. |
|
157 */ |
|
158 private function load_from_file( $file ) { |
|
159 $data = wp_json_file_decode( $file, array( 'associative' => true ) ); |
|
160 if ( empty( $data ) ) { |
|
161 return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection JSON file contents.' ) ); |
|
162 } |
|
163 |
|
164 return $this->sanitize_and_validate_data( $data, array( 'font_families' ) ); |
|
165 } |
|
166 |
|
167 /** |
|
168 * Loads the font collection data from a JSON file URL. |
|
169 * |
|
170 * @since 6.5.0 |
|
171 * |
|
172 * @param string $url URL to a JSON file containing the font collection data. |
|
173 * @return array|WP_Error An array containing the font collection data on success, |
|
174 * else an instance of WP_Error on failure. |
|
175 */ |
|
176 private function load_from_url( $url ) { |
|
177 // Limit key to 167 characters to avoid failure in the case of a long URL. |
|
178 $transient_key = substr( 'wp_font_collection_url_' . $url, 0, 167 ); |
|
179 $data = get_site_transient( $transient_key ); |
|
180 |
|
181 if ( false === $data ) { |
|
182 $response = wp_safe_remote_get( $url ); |
|
183 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { |
|
184 return new WP_Error( |
|
185 'font_collection_request_error', |
|
186 sprintf( |
|
187 // translators: %s: Font collection URL. |
|
188 __( 'Error fetching the font collection data from "%s".' ), |
|
189 $url |
|
190 ) |
|
191 ); |
|
192 } |
|
193 |
|
194 $data = json_decode( wp_remote_retrieve_body( $response ), true ); |
|
195 if ( empty( $data ) ) { |
|
196 return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection data from the HTTP response JSON.' ) ); |
|
197 } |
|
198 |
|
199 // Make sure the data is valid before storing it in a transient. |
|
200 $data = $this->sanitize_and_validate_data( $data, array( 'font_families' ) ); |
|
201 if ( is_wp_error( $data ) ) { |
|
202 return $data; |
|
203 } |
|
204 |
|
205 set_site_transient( $transient_key, $data, DAY_IN_SECONDS ); |
|
206 } |
|
207 |
|
208 return $data; |
|
209 } |
|
210 |
|
211 /** |
|
212 * Sanitizes and validates the font collection data. |
|
213 * |
|
214 * @since 6.5.0 |
|
215 * |
|
216 * @param array $data Font collection data to sanitize and validate. |
|
217 * @param array $required_properties Required properties that must exist in the passed data. |
|
218 * @return array|WP_Error Sanitized data if valid, otherwise a WP_Error instance. |
|
219 */ |
|
220 private function sanitize_and_validate_data( $data, $required_properties = array() ) { |
|
221 $schema = self::get_sanitization_schema(); |
|
222 $data = WP_Font_Utils::sanitize_from_schema( $data, $schema ); |
|
223 |
|
224 foreach ( $required_properties as $property ) { |
|
225 if ( empty( $data[ $property ] ) ) { |
|
226 $message = sprintf( |
|
227 // translators: 1: Font collection slug, 2: Missing property name, e.g. "font_families". |
|
228 __( 'Font collection "%1$s" has missing or empty property: "%2$s".' ), |
|
229 $this->slug, |
|
230 $property |
|
231 ); |
|
232 _doing_it_wrong( __METHOD__, $message, '6.5.0' ); |
|
233 return new WP_Error( 'font_collection_missing_property', $message ); |
|
234 } |
|
235 } |
|
236 |
|
237 return $data; |
|
238 } |
|
239 |
|
240 /** |
|
241 * Retrieves the font collection sanitization schema. |
|
242 * |
|
243 * @since 6.5.0 |
|
244 * |
|
245 * @return array Font collection sanitization schema. |
|
246 */ |
|
247 private static function get_sanitization_schema() { |
|
248 return array( |
|
249 'name' => 'sanitize_text_field', |
|
250 'description' => 'sanitize_text_field', |
|
251 'font_families' => array( |
|
252 array( |
|
253 'font_family_settings' => array( |
|
254 'name' => 'sanitize_text_field', |
|
255 'slug' => static function ( $value ) { |
|
256 return _wp_to_kebab_case( sanitize_title( $value ) ); |
|
257 }, |
|
258 'fontFamily' => 'WP_Font_Utils::sanitize_font_family', |
|
259 'preview' => 'sanitize_url', |
|
260 'fontFace' => array( |
|
261 array( |
|
262 'fontFamily' => 'sanitize_text_field', |
|
263 'fontStyle' => 'sanitize_text_field', |
|
264 'fontWeight' => 'sanitize_text_field', |
|
265 'src' => static function ( $value ) { |
|
266 return is_array( $value ) |
|
267 ? array_map( 'sanitize_text_field', $value ) |
|
268 : sanitize_text_field( $value ); |
|
269 }, |
|
270 'preview' => 'sanitize_url', |
|
271 'fontDisplay' => 'sanitize_text_field', |
|
272 'fontStretch' => 'sanitize_text_field', |
|
273 'ascentOverride' => 'sanitize_text_field', |
|
274 'descentOverride' => 'sanitize_text_field', |
|
275 'fontVariant' => 'sanitize_text_field', |
|
276 'fontFeatureSettings' => 'sanitize_text_field', |
|
277 'fontVariationSettings' => 'sanitize_text_field', |
|
278 'lineGapOverride' => 'sanitize_text_field', |
|
279 'sizeAdjust' => 'sanitize_text_field', |
|
280 'unicodeRange' => 'sanitize_text_field', |
|
281 ), |
|
282 ), |
|
283 ), |
|
284 'categories' => array( 'sanitize_title' ), |
|
285 ), |
|
286 ), |
|
287 'categories' => array( |
|
288 array( |
|
289 'name' => 'sanitize_text_field', |
|
290 'slug' => 'sanitize_title', |
|
291 ), |
|
292 ), |
|
293 ); |
|
294 } |
|
295 } |