PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / 2.0.2
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant v2.0.2
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 / Detect / Providers.php

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

206 lines 4.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Font-provider URL recognition.
4 *
5 * @package EasyFonts
6 */
7
8 namespace EasyFonts\Detect;
9
10 defined( 'ABSPATH' ) || exit;
11
12 /**
13 * Knows which URLs are font-provider stylesheets and which provider they are.
14 */
15 class Providers {
16
17 /**
18 * Hostnames that serve font CSS (incl. GDPR proxies).
19 *
20 * @return string[]
21 */
22 public static function css_hosts(): array {
23 return apply_filters(
24 'easyfonts_css_hosts',
25 array(
26 'fonts.googleapis.com',
27 'fonts.bunny.net',
28 'fonts-api.wp.com',
29 )
30 );
31 }
32
33 /**
34 * Hostnames + domains used for resource hints.
35 *
36 * @return string[]
37 */
38 public static function hint_hosts(): array {
39 return array(
40 'fonts.googleapis.com',
41 'fonts.gstatic.com',
42 'fonts.bunny.net',
43 'fonts-api.wp.com',
44 );
45 }
46
47 /**
48 * Is this a provider stylesheet URL (css / css2 / icon)?
49 *
50 * @param string $url URL.
51 * @return bool
52 */
53 public static function is_css_url( string $url ): bool {
54 $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
55 $path = (string) wp_parse_url( $url, PHP_URL_PATH );
56
57 if ( '' === $host ) {
58 return false;
59 }
60
61 foreach ( self::css_hosts() as $candidate ) {
62 if ( self::host_matches( $host, $candidate )
63 && ( 0 === strpos( $path, '/css' ) || 0 === strpos( $path, '/icon' ) ) ) {
64 return true;
65 }
66 }
67
68 return false;
69 }
70
71 /**
72 * Does $host equal an allowed host or a subdomain of it? Parsed-host match
73 * (not substring), so a crafted URL like https://evil.test/fonts.googleapis.com/css
74 * — whose real host is evil.test — is correctly rejected.
75 *
76 * @param string $host Lowercased host from the URL under test.
77 * @param string $candidate Allowed host.
78 * @return bool
79 */
80 private static function host_matches( string $host, string $candidate ): bool {
81 $candidate = strtolower( trim( $candidate ) );
82
83 if ( '' === $candidate ) {
84 return false;
85 }
86
87 return $host === $candidate || substr( $host, -strlen( '.' . $candidate ) ) === '.' . $candidate;
88 }
89
90 /**
91 * Provider id for a URL.
92 *
93 * @param string $url URL.
94 * @return string
95 */
96 public static function id_for( string $url ): string {
97 if ( false !== strpos( $url, 'bunny.net' ) ) {
98 return 'bunny';
99 }
100
101 if ( false !== strpos( $url, 'wp.com' ) ) {
102 return 'wp';
103 }
104
105 return 'google';
106 }
107
108 /**
109 * Normalize a URL to an absolute, fetchable form.
110 *
111 * Handles the cases that broke detection: protocol-relative `//host/...`
112 * (the main bug — wp_remote_get can't fetch these) and root-relative
113 * `/path`. HTML-decodes `&amp;` etc. so query strings survive.
114 *
115 * @param string $url Raw URL (possibly `//…`, `/…`, or entity-encoded).
116 * @param string $base_url Optional base for resolving truly relative paths.
117 * @return string
118 */
119 public static function normalize_url( string $url, string $base_url = '' ): string {
120 $url = trim( html_entity_decode( $url, ENT_QUOTES, 'UTF-8' ) );
121
122 if ( '' === $url || 0 === strpos( $url, 'data:' ) ) {
123 return $url;
124 }
125
126 // Protocol-relative → add the current scheme.
127 if ( 0 === strpos( $url, '//' ) ) {
128 return ( is_ssl() ? 'https:' : 'http:' ) . $url;
129 }
130
131 // Already absolute.
132 if ( preg_match( '#^https?://#i', $url ) ) {
133 return $url;
134 }
135
136 // Root-relative → site root.
137 if ( 0 === strpos( $url, '/' ) ) {
138 return home_url( $url );
139 }
140
141 // Truly relative → resolve against the base directory.
142 if ( '' !== $base_url ) {
143 return self::resolve_relative( $url, $base_url );
144 }
145
146 return $url;
147 }
148
149 /**
150 * Does this URL point at a known font-provider host (any path)?
151 *
152 * Broader than is_css_url(): also matches gstatic font binaries and any
153 * provider host, so we can recognise remote @font-face src values.
154 *
155 * @param string $url URL.
156 * @return bool
157 */
158 public static function is_provider_url( string $url ): bool {
159 $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
160
161 if ( '' === $host ) {
162 return false;
163 }
164
165 foreach ( self::hint_hosts() as $candidate ) {
166 if ( self::host_matches( $host, $candidate ) ) {
167 return true;
168 }
169 }
170
171 return false;
172 }
173
174 /**
175 * Resolve a relative URL against a base URL's directory.
176 *
177 * @param string $relative Relative path.
178 * @param string $base_url Absolute base URL.
179 * @return string
180 */
181 private static function resolve_relative( string $relative, string $base_url ): string {
182 $parts = wp_parse_url( $base_url );
183
184 if ( empty( $parts['scheme'] ) || empty( $parts['host'] ) ) {
185 return $relative;
186 }
187
188 $origin = $parts['scheme'] . '://' . $parts['host'] . ( isset( $parts['port'] ) ? ':' . $parts['port'] : '' );
189 $dir = isset( $parts['path'] ) ? preg_replace( '#/[^/]*$#', '/', $parts['path'] ) : '/';
190
191 // Collapse ../ segments.
192 $path = $dir . $relative;
193 $segments = array();
194
195 foreach ( explode( '/', $path ) as $segment ) {
196 if ( '..' === $segment ) {
197 array_pop( $segments );
198 } elseif ( '.' !== $segment && '' !== $segment ) {
199 $segments[] = $segment;
200 }
201 }
202
203 return $origin . '/' . implode( '/', $segments );
204 }
205 }
206