PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / 2.0.0
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant v2.0.0
2.0.4 2.0.3 2.0.2 2.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2 1.3 2.0.0
easyfonts / src / Fonts / Downloader.php

Downloader.php in EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant 2.0.0, at src/Fonts/Downloader.php

258 lines 6.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Remote fetcher for stylesheets and font binaries.
4 *
5 * @package EasyFonts
6 */
7
8 namespace EasyFonts\Fonts;
9
10 defined( 'ABSPATH' ) || exit;
11
12 /**
13 * Downloads Google/Bunny CSS and font files.
14 */
15 class Downloader {
16
17 /**
18 * Modern desktop UA so the API returns woff2.
19 */
20 const UA_WOFF2 = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0';
21
22 /**
23 * Old UA that makes the API return TTF (used for metric extraction only).
24 */
25 const UA_TTF = 'Mozilla/5.0 (Windows NT 6.1; rv:27.0) Gecko/20100101 Firefox/27.0';
26
27 /**
28 * Content-Type => extension.
29 */
30 const MIME = array(
31 'font/woff2' => 'woff2',
32 'application/font-woff2' => 'woff2',
33 'font/woff' => 'woff',
34 'application/font-woff' => 'woff',
35 'font/ttf' => 'ttf',
36 'application/x-font-ttf' => 'ttf',
37 'font/sfnt' => 'ttf',
38 'application/font-sfnt' => 'ttf',
39 'font/otf' => 'otf',
40 'application/x-font-opentype' => 'otf',
41 'application/vnd.ms-fontobject' => 'eot',
42 );
43
44 /**
45 * Fetch CSS text from a font-provider URL.
46 *
47 * @param string $url Stylesheet URL.
48 * @return string|null
49 */
50 public function fetch_css( string $url ): ?string {
51 $url = \EasyFonts\Detect\Providers::normalize_url( $url );
52
53 $response = wp_remote_get(
54 $url,
55 array(
56 'timeout' => 20,
57 'user-agent' => apply_filters( 'easyfonts_css_user_agent', self::UA_WOFF2 ),
58 )
59 );
60
61 if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
62 return null;
63 }
64
65 $body = wp_remote_retrieve_body( $response );
66
67 return '' === $body ? null : $body;
68 }
69
70 /**
71 * Download a font binary into the cache and return [filename, ext, size].
72 *
73 * @param string $url Font file URL.
74 * @param string $basename Cache basename (without extension).
75 * @param Storage $storage Storage.
76 * @return array{file:string,ext:string,size:int}|null
77 */
78 public function fetch_font( string $url, string $basename, Storage $storage ): ?array {
79 $url = \EasyFonts\Detect\Providers::normalize_url( $url );
80
81 // SSRF guard: only fetch font binaries from recognised provider hosts
82 // (or the site's own origin). A crafted @font-face can otherwise point
83 // src at an internal address; this blocks that without affecting any
84 // legitimate Google/Bunny/wp.com font. Filterable for niche providers.
85 if ( ! self::host_allowed( $url ) ) {
86 return null;
87 }
88
89 $response = wp_remote_get(
90 $url,
91 array(
92 'timeout' => 30,
93 'user-agent' => self::UA_WOFF2,
94 )
95 );
96
97 if ( is_wp_error( $response ) ) {
98 return null;
99 }
100
101 $code = (int) wp_remote_retrieve_response_code( $response );
102
103 if ( $code < 200 || $code >= 300 ) {
104 return null;
105 }
106
107 $body = wp_remote_retrieve_body( $response );
108
109 if ( '' === $body ) {
110 return null;
111 }
112
113 $ct = strtolower( trim( explode( ';', (string) wp_remote_retrieve_header( $response, 'content-type' ) )[0] ) );
114 $ext = self::MIME[ $ct ] ?? $this->ext_from_url( $url );
115
116 $filename = $basename . '.' . $ext;
117
118 if ( ! $storage->write( $filename, $body ) ) {
119 return null;
120 }
121
122 return array(
123 'file' => $filename,
124 'ext' => $ext,
125 'size' => strlen( $body ),
126 );
127 }
128
129 /**
130 * Fetch the raw bytes of a TTF copy of a Google Fonts family for metrics.
131 * Returns the binary, never writes it to disk.
132 *
133 * @param string $css_url Original CSS URL.
134 * @return string|null
135 */
136 public function fetch_ttf_bytes( string $css_url ): ?string {
137 $response = wp_remote_get(
138 $css_url,
139 array(
140 'timeout' => 20,
141 'user-agent' => self::UA_TTF,
142 )
143 );
144
145 if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
146 return null;
147 }
148
149 $css = wp_remote_retrieve_body( $response );
150
151 if ( ! preg_match( '/src\s*:\s*[^;]*?url\(\s*[\'"]?([^\'")]+\.(?:ttf|otf))[\'"]?\s*\)/i', $css, $m ) ) {
152 return null;
153 }
154
155 $font = wp_remote_get( $m[1], array( 'timeout' => 20 ) );
156
157 if ( is_wp_error( $font ) || 200 !== (int) wp_remote_retrieve_response_code( $font ) ) {
158 return null;
159 }
160
161 $bytes = wp_remote_retrieve_body( $font );
162
163 return '' === $bytes ? null : $bytes;
164 }
165
166 /**
167 * Fetch TTF bytes for a specific Google family/weight purely for metric
168 * extraction. Source-independent: we construct a css2 request from the
169 * family name, so it works whether the font came from a <link>, @import, or
170 * an inline @font-face. Returns binary, never written to disk.
171 *
172 * @param string $family Family name (e.g. "Open Sans").
173 * @param string $weight Representative numeric weight.
174 * @return string|null
175 */
176 public function fetch_family_ttf( string $family, string $weight = '400' ): ?string {
177 $weight = (string) absint( $weight );
178
179 if ( '' === $weight || '0' === $weight ) {
180 $weight = '400';
181 }
182
183 $url = 'https://fonts.googleapis.com/css2?family='
184 . rawurlencode( $family ) . ':wght@' . $weight . '&display=swap';
185 $url = str_replace( '%20', '+', $url );
186
187 return $this->fetch_ttf_bytes( $url );
188 }
189
190 /**
191 * Is this URL host allowed for font-binary downloads? Provider font hosts
192 * plus the site's own origin. Filterable via `easyfonts_font_src_hosts`.
193 *
194 * @param string $url Absolute URL.
195 * @return bool
196 */
197 private static function host_allowed( string $url ): bool {
198 $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
199
200 if ( '' === $host ) {
201 return false;
202 }
203
204 $own = strtolower( (string) wp_parse_url( home_url(), PHP_URL_HOST ) );
205
206 /**
207 * Allowed host suffixes for downloading font binaries.
208 *
209 * @param string[] $hosts
210 */
211 $allowed = apply_filters(
212 'easyfonts_font_src_hosts',
213 array(
214 'fonts.gstatic.com',
215 'fonts.googleapis.com',
216 'fonts.bunny.net',
217 'fonts.wp.com',
218 'fonts-api.wp.com',
219 's.w.org',
220 'i0.wp.com',
221 'i1.wp.com',
222 'i2.wp.com',
223 )
224 );
225
226 if ( '' !== $own ) {
227 $allowed[] = $own;
228 }
229
230 foreach ( $allowed as $candidate ) {
231 $candidate = strtolower( trim( (string) $candidate ) );
232
233 if ( '' === $candidate ) {
234 continue;
235 }
236
237 // Exact host or a subdomain of an allowed host.
238 if ( $host === $candidate || substr( $host, -strlen( '.' . $candidate ) ) === '.' . $candidate ) {
239 return true;
240 }
241 }
242
243 return false;
244 }
245
246 /**
247 * Guess an extension from a URL path.
248 *
249 * @param string $url URL.
250 * @return string
251 */
252 private function ext_from_url( string $url ): string {
253 $ext = strtolower( (string) pathinfo( (string) wp_parse_url( $url, PHP_URL_PATH ), PATHINFO_EXTENSION ) );
254
255 return in_array( $ext, array( 'woff2', 'woff', 'ttf', 'otf', 'eot' ), true ) ? $ext : 'woff2';
256 }
257 }
258