PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 All 35 releases
← All changes | includes/desktop-files/favicon.php +63 -52 0.9.1 → 1.1.11 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Favicon resolver.
3 + * OpenStation — Favicon resolver.
4 4 *
5 5 * Resolves the favicon for an arbitrary http(s) URL, downloads the
6 6 * bytes server-side, and returns a base64 `data:` URI suitable for
7 7 * stuffing into a `placement.meta.iconUrl` so the tile renderer can
@@ -12,9 +12,10 @@
12 12 *
13 13 * 1. Fetch the page HTML via `wp_safe_remote_get()` — the `_safe_`
14 14 * flavour blocks loopback / private-IP fetches, which prevents
15 15 * this user-supplied-URL endpoint from doubling as an SSRF
16 - * pivot.
16 + * pivot. The download is capped via `limit_response_size` so
17 + * a hostile host can't stream an unbounded body into memory.
17 18 * 2. Parse the response with `DOMDocument` (libxml errors silenced
18 19 * because real-world HTML is gnarly). Walk for the first
19 20 * `<link rel="icon|shortcut icon|apple-touch-icon" href="…">`
20 21 * and resolve the href against the page URL.
@@ -19,9 +20,10 @@
19 20 * `<link rel="icon|shortcut icon|apple-touch-icon" href="…">`
20 21 * and resolve the href against the page URL.
21 22 * 3. Fall back to `<scheme>://<host>/favicon.ico` when no link tag
22 23 * is present.
23 - * 4. Fetch the candidate icon via `wp_safe_remote_get()`. Reject
24 + * 4. Fetch the candidate icon via `wp_safe_remote_get()`, with
25 + * the download truncated at one byte over the size cap. Reject
24 26 * anything that isn't `image/*`, anything bigger than the
25 27 * configured size cap, and anything `getimagesizefromstring()`
26 28 * can't recognize (catches HTML pages whose servers lie about
27 29 * `Content-Type`).
@@ -29,14 +31,13 @@
29 31 *
30 32 * Failure at any step returns `null` — the caller treats this as
31 33 * "no favicon, render the dashicons fallback". Never throws.
32 34 *
33 - * Filter the final return value through `desktop_mode_resolve_favicon`
35 + * Filter the final return value through `openstation_resolve_favicon`
34 36 * so plugins can short-circuit (return `null` to force-skip, return
35 37 * a synthetic data URI to override).
36 38 *
37 - * @package WPDesktopMode
38 - * @since 0.20.0
39 + * @package OpenStation
39 40 */
40 41
41 42 defined( 'ABSPATH' ) || exit;
42 43
@@ -45,28 +46,34 @@
45 46 * under 4 KB. The 256 KB cap exists to keep `placement.meta` blobs
46 47 * sane and to avoid base64-encoding a multi-megabyte payload that
47 48 * a malicious or sloppy host might serve at `/favicon.ico`.
48 49 */
49 -const DESKTOP_MODE_FAVICON_MAX_BYTES = 256 * 1024;
50 +const OPENSTATION_FAVICON_MAX_BYTES = 256 * 1024;
50 51
51 52 /**
53 + * Maximum page-HTML download size, in bytes, for the step-1 page
54 + * fetch. The `<link rel="icon">` tags live in `<head>`, so 1 MB
55 + * is plenty; the cap stops a malicious or sloppy host from
56 + * streaming an unbounded body into memory before the parser runs.
57 + */
58 +const OPENSTATION_FAVICON_MAX_PAGE_BYTES = 1024 * 1024;
59 +
60 +/**
52 61 * Per-request HTTP timeout, in seconds. Two fetches happen worst-
53 62 * case (page + icon) so the user-visible wait caps around 2× this
54 63 * value. Tune downward if QA finds the dialog "Create" button
55 64 * sitting too long.
56 65 */
57 -const DESKTOP_MODE_FAVICON_TIMEOUT = 4;
66 +const OPENSTATION_FAVICON_TIMEOUT = 4;
58 67
59 68 /**
60 69 * Resolve a page URL to a base64 data URI of its favicon.
61 70 *
62 - * @since 0.20.0
63 - *
64 71 * @param string $page_url HTTP(S) URL of the target page.
65 72 * @return string|null Data URI on success; `null` on any failure.
66 73 */
67 -function desktop_mode_resolve_favicon( $page_url ) {
68 - $result = desktop_mode_resolve_favicon_internal( (string) $page_url );
74 +function openstation_resolve_favicon( $page_url ) {
75 + $result = openstation_resolve_favicon_internal( (string) $page_url );
69 76
70 77 /**
71 78 * Filters the favicon data URI before it is returned to the
72 79 * caller. Plugins can override (return a synthetic data URI),
@@ -71,15 +78,13 @@
71 78 * Filters the favicon data URI before it is returned to the
72 79 * caller. Plugins can override (return a synthetic data URI),
73 80 * suppress (return `null`), or pass through.
74 81 *
75 - * @since 0.20.0
76 - *
77 82 * @param string|null $result Base64 data URI, or `null` if
78 83 * the resolver could not produce one.
79 84 * @param string $page_url The page URL that was resolved.
80 85 */
81 - $filtered = apply_filters( 'desktop_mode_resolve_favicon', $result, (string) $page_url );
86 + $filtered = apply_filters( 'openstation_resolve_favicon', $result, (string) $page_url );
82 87
83 88 if ( null === $filtered ) {
84 89 return null;
85 90 }
@@ -86,21 +91,20 @@
86 91 return is_string( $filtered ) ? $filtered : null;
87 92 }
88 93
89 94 /**
90 - * Internal resolver — see {@see desktop_mode_resolve_favicon}.
95 + * Internal resolver — see {@see openstation_resolve_favicon}.
91 96 *
92 97 * Kept separate so the public function is the only place the
93 - * `desktop_mode_resolve_favicon` filter runs (a plugin can't sneak
98 + * `openstation_resolve_favicon` filter runs (a plugin can't sneak
94 99 * its filter past the validation by hooking the internal helper).
95 100 *
96 - * @since 0.20.0
97 101 * @internal
98 102 *
99 103 * @param string $page_url Page URL.
100 104 * @return string|null
101 105 */
102 -function desktop_mode_resolve_favicon_internal( $page_url ) {
106 +function openstation_resolve_favicon_internal( $page_url ) {
103 107 $parts = wp_parse_url( $page_url );
104 108 if ( ! is_array( $parts ) || empty( $parts['host'] ) ) {
105 109 return null;
106 110 }
@@ -108,9 +112,9 @@
108 112 if ( 'http' !== $scheme && 'https' !== $scheme ) {
109 113 return null;
110 114 }
111 115
112 - $page_response = wp_safe_remote_get( $page_url, desktop_mode_favicon_request_args() );
116 + $page_response = wp_safe_remote_get( $page_url, openstation_favicon_request_args( OPENSTATION_FAVICON_MAX_PAGE_BYTES ) );
113 117 $page_body = '';
114 118 if ( ! is_wp_error( $page_response ) && 200 === (int) wp_remote_retrieve_response_code( $page_response ) ) {
115 119 $page_body = (string) wp_remote_retrieve_body( $page_response );
116 120 }
@@ -115,31 +119,42 @@
115 119 $page_body = (string) wp_remote_retrieve_body( $page_response );
116 120 }
117 121
118 122 $candidate_url = '' !== $page_body
119 - ? desktop_mode_favicon_extract_link_href( $page_body, $page_url )
123 + ? openstation_favicon_extract_link_href( $page_body, $page_url )
120 124 : '';
121 125 if ( '' === $candidate_url ) {
122 126 $candidate_url = $scheme . '://' . $parts['host'] . ( isset( $parts['port'] ) ? ':' . $parts['port'] : '' ) . '/favicon.ico';
123 127 }
124 128
125 - return desktop_mode_favicon_fetch_as_data_uri( $candidate_url );
129 + return openstation_favicon_fetch_as_data_uri( $candidate_url );
126 130 }
127 131
128 132 /**
129 133 * Common request args for both the page fetch and the icon fetch.
130 134 *
131 - * @since 0.20.0
135 + * `limit_response_size` makes WP_Http stop reading at the cap, so
136 + * an oversize (or maliciously unbounded) body is truncated during
137 + * the download instead of being buffered whole into memory before
138 + * the size check runs.
139 + *
132 140 * @internal
133 141 *
142 + * @param int $limit_response_size Maximum response body size, in
143 + * bytes, enforced by WP_Http while
144 + * downloading. Default one byte over
145 + * `OPENSTATION_FAVICON_MAX_BYTES`,
146 + * so the post-fetch size check still
147 + * rejects truncated over-cap bodies.
134 148 * @return array
135 149 */
136 -function desktop_mode_favicon_request_args() {
150 +function openstation_favicon_request_args( $limit_response_size = OPENSTATION_FAVICON_MAX_BYTES + 1 ) {
137 151 return array(
138 - 'timeout' => DESKTOP_MODE_FAVICON_TIMEOUT,
139 - 'redirection' => 3,
140 - 'user-agent' => 'WP Desktop Mode favicon resolver/1.0',
141 - 'headers' => array(
152 + 'timeout' => OPENSTATION_FAVICON_TIMEOUT,
153 + 'redirection' => 3,
154 + 'user-agent' => 'WP OpenStation favicon resolver/1.0',
155 + 'limit_response_size' => (int) $limit_response_size,
156 + 'headers' => array(
142 157 'Accept' => 'text/html,application/xhtml+xml,image/*;q=0.9,*/*;q=0.5',
143 158 ),
144 159 );
145 160 }
@@ -149,9 +164,8 @@
149 164 * icon|apple-touch-icon" href="…">` and resolve `href` against
150 165 * `$base_url`. Returns the absolute icon URL, or `''` if none
151 166 * found.
152 167 *
153 - * @since 0.20.0
154 168 * @internal
155 169 *
156 170 * @param string $html Page body.
157 171 * @param string $base_url URL of the page that produced `$html`.
@@ -156,9 +170,9 @@
156 170 * @param string $html Page body.
157 171 * @param string $base_url URL of the page that produced `$html`.
158 172 * @return string
159 173 */
160 -function desktop_mode_favicon_extract_link_href( $html, $base_url ) {
174 +function openstation_favicon_extract_link_href( $html, $base_url ) {
161 175 $dom = new DOMDocument();
162 176 $prev_errors = libxml_use_internal_errors( true );
163 177 // `LIBXML_NOWARNING | LIBXML_NOERROR` suppresses libxml's stderr
164 178 // chatter on malformed HTML; we already silence libxml errors above.
@@ -177,11 +191,11 @@
177 191 // `apple-touch-icon` images are nicer for retina displays but
178 192 // usually larger than the 256 KB cap so we only fall back to
179 193 // them when nothing else exists.
180 194 $buckets = array(
181 - 'icon' => '',
182 - 'shortcut icon' => '',
183 - 'apple-touch-icon' => '',
195 + 'icon' => '',
196 + 'shortcut icon' => '',
197 + 'apple-touch-icon' => '',
184 198 );
185 199
186 200 foreach ( $links as $link ) {
187 201 if ( ! ( $link instanceof DOMElement ) ) {
@@ -208,9 +222,9 @@
208 222 foreach ( $buckets as $href ) {
209 223 if ( '' === $href ) {
210 224 continue;
211 225 }
212 - $absolute = desktop_mode_favicon_absolutize_url( $href, $base_url );
226 + $absolute = openstation_favicon_absolutize_url( $href, $base_url );
213 227 if ( '' !== $absolute ) {
214 228 return $absolute;
215 229 }
216 230 }
@@ -220,9 +234,8 @@
220 234 /**
221 235 * Resolve a possibly-relative `href` against `$base_url`. Returns
222 236 * `''` if the result isn't an http(s) URL.
223 237 *
224 - * @since 0.20.0
225 238 * @internal
226 239 *
227 240 * @param string $href Link href (absolute, scheme-relative, or path).
228 241 * @param string $base_url Page URL.
@@ -227,9 +240,9 @@
227 240 * @param string $href Link href (absolute, scheme-relative, or path).
228 241 * @param string $base_url Page URL.
229 242 * @return string
230 243 */
231 -function desktop_mode_favicon_absolutize_url( $href, $base_url ) {
244 +function openstation_favicon_absolutize_url( $href, $base_url ) {
232 245 $href = trim( $href );
233 246 if ( '' === $href ) {
234 247 return '';
235 248 }
@@ -265,19 +278,18 @@
265 278
266 279 /**
267 280 * Fetch the candidate icon URL and encode it as a data URI.
268 281 *
269 - * @since 0.20.0
270 282 * @internal
271 283 *
272 284 * @param string $icon_url Absolute http(s) URL of the icon.
273 285 * @return string|null
274 286 */
275 -function desktop_mode_favicon_fetch_as_data_uri( $icon_url ) {
287 +function openstation_favicon_fetch_as_data_uri( $icon_url ) {
276 288 if ( '' === $icon_url || ! preg_match( '#^https?://#i', $icon_url ) ) {
277 289 return null;
278 290 }
279 - $response = wp_safe_remote_get( $icon_url, desktop_mode_favicon_request_args() );
291 + $response = wp_safe_remote_get( $icon_url, openstation_favicon_request_args() );
280 292 if ( is_wp_error( $response ) ) {
281 293 return null;
282 294 }
283 295 if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
@@ -289,12 +301,12 @@
289 301 if ( 0 !== strpos( $content_type, 'image/' ) ) {
290 302 return null;
291 303 }
292 304 $body = (string) wp_remote_retrieve_body( $response );
293 - if ( '' === $body || strlen( $body ) > DESKTOP_MODE_FAVICON_MAX_BYTES ) {
305 + if ( '' === $body || strlen( $body ) > OPENSTATION_FAVICON_MAX_BYTES ) {
294 306 return null;
295 307 }
296 - $subtype = desktop_mode_favicon_subtype_from_content_type( $content_type );
308 + $subtype = openstation_favicon_subtype_from_content_type( $content_type );
297 309 if ( null === $subtype ) {
298 310 return null;
299 311 }
300 312 // Catch HTML / text bodies served with a lying `Content-Type:
@@ -314,9 +326,8 @@
314 326 /**
315 327 * Map a `Content-Type` header to a known image subtype, or `null`
316 328 * if the type isn't on the allowlist.
317 329 *
318 - * @since 0.20.0
319 330 * @internal
320 331 *
321 332 * @param string $content_type Lowercased `Content-Type` value
322 333 * (no parameters).
@@ -321,18 +332,18 @@
321 332 * @param string $content_type Lowercased `Content-Type` value
322 333 * (no parameters).
323 334 * @return string|null
324 335 */
325 -function desktop_mode_favicon_subtype_from_content_type( $content_type ) {
336 +function openstation_favicon_subtype_from_content_type( $content_type ) {
326 337 $map = array(
327 - 'image/png' => 'png',
328 - 'image/jpeg' => 'jpeg',
329 - 'image/jpg' => 'jpeg',
330 - 'image/gif' => 'gif',
331 - 'image/webp' => 'webp',
332 - 'image/x-icon' => 'x-icon',
333 - 'image/vnd.microsoft.icon' => 'x-icon',
334 - 'image/ico' => 'x-icon',
335 - 'image/svg+xml' => 'svg+xml',
338 + 'image/png' => 'png',
339 + 'image/jpeg' => 'jpeg',
340 + 'image/jpg' => 'jpeg',
341 + 'image/gif' => 'gif',
342 + 'image/webp' => 'webp',
343 + 'image/x-icon' => 'x-icon',
344 + 'image/vnd.microsoft.icon' => 'x-icon',
345 + 'image/ico' => 'x-icon',
346 + 'image/svg+xml' => 'svg+xml',
336 347 );
337 348 return isset( $map[ $content_type ] ) ? $map[ $content_type ] : null;
338 349 }