Changeset 3707539
- Timestamp:
- 09/22/2026 02:15:39 PM (5 weeks ago)
- Location:
- host-webfonts-local
- Files:
-
- 22 edited
- 1 copied
-
tags/6.3.11 (copied) (copied from host-webfonts-local/trunk )
-
tags/6.3.11/host-webfonts-local.php (modified) (1 diff)
-
tags/6.3.11/readme.txt (modified) (2 diffs)
-
tags/6.3.11/src/Admin/Actions.php (modified) (1 diff)
-
tags/6.3.11/src/Admin/Optimize.php (modified) (4 diffs)
-
tags/6.3.11/src/Compatibility/CachingPlugins.php (modified) (1 diff)
-
tags/6.3.11/src/Download.php (modified) (7 diffs)
-
tags/6.3.11/src/Frontend/Compatibility/Elementor.php (modified) (3 diffs)
-
tags/6.3.11/src/Frontend/Compatibility/WPTT.php (modified) (4 diffs)
-
tags/6.3.11/src/Frontend/Process.php (modified) (4 diffs)
-
tags/6.3.11/src/Helper.php (modified) (2 diffs)
-
tags/6.3.11/src/Optimize.php (modified) (3 diffs)
-
trunk/host-webfonts-local.php (modified) (1 diff)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/src/Admin/Actions.php (modified) (1 diff)
-
trunk/src/Admin/Optimize.php (modified) (4 diffs)
-
trunk/src/Compatibility/CachingPlugins.php (modified) (1 diff)
-
trunk/src/Download.php (modified) (7 diffs)
-
trunk/src/Frontend/Compatibility/Elementor.php (modified) (3 diffs)
-
trunk/src/Frontend/Compatibility/WPTT.php (modified) (4 diffs)
-
trunk/src/Frontend/Process.php (modified) (4 diffs)
-
trunk/src/Helper.php (modified) (2 diffs)
-
trunk/src/Optimize.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
host-webfonts-local/tags/6.3.11/host-webfonts-local.php
r3641590 r3707539 4 4 * Plugin URI: https://daan.dev/wordpress/omgf/ 5 5 * Description: Increase GDPR/DSGVO compliance and leverage browser cache by automatically self-hosting Google Fonts. 6 * Version: 6.3.1 06 * Version: 6.3.11 7 7 * Author: Daan from Daan.dev 8 8 * Author URI: https://daan.dev -
host-webfonts-local/tags/6.3.11/readme.txt
r3641590 r3707539 4 4 Requires at least: 5.9 5 5 Tested up to: 7.0 6 Stable tag: 6.3.1 06 Stable tag: 6.3.11 7 7 Requires PHP: 7.3 8 8 License: GPLv2 or later … … 87 87 88 88 == Changelog == 89 90 = 6.3.11 = 91 * Security: hardened the detection of Google Fonts stylesheets and the handling of downloaded files. Thanks to Wordfence for responsibly disclosing this issue. 92 * Security: Save & Optimize can now only be started by an authorized request. Thanks to Ángel Santana (via WPScan) for responsibly disclosing this issue. 93 * Fixed: stylesheets generated by the WPTT Webfont Loader were removed from the page instead of being optimized because the compatibility fix's validation was inverted. 94 * Fixed: a font family whose name contains a character with a special meaning in a regular expression (e.g., a dot or an asterisk) claimed the @font-face statements — and the downloads — of every 95 other font in the same stylesheet. The name is now escaped before it's matched. 96 * Improved: values which can't be the name of a font are no longer stored as font families, e.g. 'Arial, Helvetica, sans-serif !important', 'var(--tve-font-family,Mulish)', 'inherit' and 'sans-serif.' 97 A stylesheet which isn't a Google Fonts API response (e.g., a theme's stylesheet, which is only processed by OMGF Pro) contains font-family declarations outside its @font-face statements, and each 98 of those was stored as a font family. They can't be downloaded, so they only cluttered the Optimize Fonts screen. Enable Debug Mode to log which values were skipped. 89 99 90 100 = 6.3.10 = -
host-webfonts-local/tags/6.3.11/src/Admin/Actions.php
r3641590 r3707539 248 248 set_transient( 'settings_errors', get_settings_errors(), 30 ); 249 249 250 // Redirect back to the settings page that was submitted. 251 $goback = add_query_arg( 'settings-updated', 'true', wp_get_referer() ); 250 // Redirect back to the settings page that was submitted. The nonce authorizes the Save & Optimize run. 251 $goback = add_query_arg( 252 [ 253 'settings-updated' => 'true', 254 '_wpnonce' => wp_create_nonce( Optimize::NONCE_ACTION ), 255 ], 256 wp_get_referer() 257 ); 252 258 253 259 if ( ! defined( 'DAAN_DOING_TESTS' ) ) { -
host-webfonts-local/tags/6.3.11/src/Admin/Optimize.php
r3494741 r3707539 21 21 22 22 class Optimize { 23 /** 24 * Nonce action which authorizes a Save & Optimize run. It's added to the redirect URL after 25 * the settings are saved, so it's only present on a request that follows a form submit. 26 */ 27 const NONCE_ACTION = 'omgf-optimize'; 28 23 29 /** @var string */ 24 30 private $settings_page = ''; … … 30 36 private $settings_updated = false; 31 37 38 /** @var string */ 39 private $nonce = ''; 40 32 41 /** 33 42 * OMGF\Admin\Optimize constructor. 34 *35 * @codeCoverageIgnore36 43 */ 37 44 public function __construct() { … … 39 46 $this->settings_tab = isset( $_GET['tab'] ) ? sanitize_text_field( $_GET['tab'] ) : Settings::OMGF_SETTINGS_FIELD_OPTIMIZE; 40 47 $this->settings_updated = isset( $_GET['settings-updated'] ); 48 $this->nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( $_GET['_wpnonce'] ) : ''; 41 49 42 50 $this->init(); … … 59 67 if ( ! $this->settings_updated ) { 60 68 return; // @codeCoverageIgnore 69 } 70 71 /** 72 * A run has to be authorized by a capable user and the nonce that's issued when the settings are saved. 73 * 74 * @see Actions::update_settings() 75 */ 76 if ( ! current_user_can( 'manage_options' ) ) { 77 return; 78 } 79 80 if ( wp_verify_nonce( $this->nonce, self::NONCE_ACTION ) < 1 ) { 81 return; 61 82 } 62 83 -
host-webfonts-local/tags/6.3.11/src/Compatibility/CachingPlugins.php
r3494951 r3707539 37 37 * - The `admin_init` action is triggered, 38 38 * - This is OMGF's settings page, 39 * - Settings are updated .39 * - Settings are updated by an administrator. 40 40 * 41 * So, finally, we need to check if this is an administrator.41 * The capability is checked again here, because the action can be triggered from elsewhere. 42 42 * 43 43 * @see \OMGF\Admin\Optimize::init() -
host-webfonts-local/tags/6.3.11/src/Download.php
r3628684 r3707539 76 76 } 77 77 78 $temp_filename = $this->path . '/' . $this->filename . '.tmp'; 78 /** 79 * @since v6.3.11 Use an unguessable name for the temporary file, because $this->path is inside the 80 * uploads directory, i.e. it's publicly accessible. 81 */ 82 $temp_filename = $this->path . '/' . $this->filename . '-' . wp_generate_password( 12, false ) . '.tmp'; 79 83 80 84 $response = wp_safe_remote_get( … … 88 92 89 93 if ( is_wp_error( $response ) ) { 90 if ( file_exists( $temp_filename ) ) { 91 unlink( $temp_filename ); 92 } 94 $this->delete_temp_file( $temp_filename ); 93 95 94 96 Notice::set_notice( … … 106 108 // Handle non-success HTTP status codes. 107 109 if ( $code < 200 || $code >= 300 ) { 108 if ( file_exists( $temp_filename ) ) { 109 unlink( $temp_filename ); 110 } 110 $this->delete_temp_file( $temp_filename ); 111 111 112 112 Notice::set_notice( … … 123 123 124 124 if ( ! $content_type ) { 125 $this->delete_temp_file( $temp_filename ); 126 125 127 Notice::set_notice( 126 128 __( 'OMGF couldn\'t determine the mime-type for the downloaded font file', 'host-webfonts-local' ) . ': ' . $this->filename, … … 138 140 139 141 if ( ! $extension ) { 142 $this->delete_temp_file( $temp_filename ); 143 140 144 OMGF::debug( 141 145 sprintf( … … 169 173 170 174 // Clean up the temp file 171 if ( file_exists( $temp_filename ) ) { 172 unlink( $temp_filename ); 173 } 175 $this->delete_temp_file( $temp_filename ); 174 176 175 177 return ''; … … 179 181 return OMGF_UPLOAD_URL . str_replace( OMGF_UPLOAD_DIR, '', $this->path ) . '/' . $this->filename . '.' . $extension; 180 182 } 183 184 /** 185 * Removes the temporary file the response was streamed to. 186 * 187 * @since v6.3.11 The temporary file lives inside the uploads directory, which is publicly accessible, so 188 * it should never be left behind, no matter why the download was aborted. 189 * 190 * @param string $temp_filename 191 * 192 * @return void 193 */ 194 private function delete_temp_file( $temp_filename ) { 195 if ( file_exists( $temp_filename ) ) { 196 unlink( $temp_filename ); 197 } 198 } 181 199 } -
host-webfonts-local/tags/6.3.11/src/Frontend/Compatibility/Elementor.php
r3503345 r3707539 38 38 public function init() { 39 39 add_filter( 'omgf_frontend_process_fonts_set', [ $this, 'maybe_modify_id' ], 10, 2 ); 40 add_filter( 'omgf_frontend_process_parse_links', [ $this, 'validate_link_element' ], 10, 2);40 add_filter( 'omgf_frontend_process_parse_links', [ $this, 'validate_link_element' ], 10, 3 ); 41 41 add_filter( 'omgf_frontend_process_invalid_request', [ $this, 'validate_request' ], 10, 2 ); 42 42 } … … 68 68 * @see \OMGF\Frontend\Process::process() 69 69 * 70 * @since v6.3.11 Validate the element's attribute values, instead of matching its markup, so only 71 * elements which actually point to the stylesheet are included. 72 * 70 73 * @param $is_valid 71 74 * @param $link 75 * @param $urls The element's attribute values, sanitized by @see \OMGF\Frontend\Process::get_element_urls() 72 76 * 73 77 * @return bool 74 78 */ 75 public function validate_link_element( $is_valid, $link ) { 76 return $is_valid || str_contains( $link, '/uploads/elementor/google-fonts' ); 79 public function validate_link_element( $is_valid, $link, $urls = [] ) { 80 if ( $is_valid ) { 81 return true; 82 } 83 84 foreach ( (array) $urls as $url ) { 85 if ( $this->is_google_fonts_stylesheet( $url ) ) { 86 return true; 87 } 88 } 89 90 return false; 91 } 92 93 /** 94 * Does $url point to a Google Fonts stylesheet Elementor stored locally? 95 * 96 * @since v6.3.11 97 * 98 * @param string $url 99 * 100 * @return bool 101 */ 102 private function is_google_fonts_stylesheet( $url ) { 103 $path = wp_parse_url( html_entity_decode( (string) $url ), PHP_URL_PATH ); 104 105 return is_string( $path ) && str_contains( $path, '/uploads/elementor/google-fonts' ); 77 106 } 78 107 … … 89 118 */ 90 119 public function validate_request( $is_invalid, $url ) { 91 return $is_invalid && ! str_contains( $url, '/uploads/elementor/google-fonts');120 return $is_invalid && ! $this->is_google_fonts_stylesheet( $url ); 92 121 } 93 122 } -
host-webfonts-local/tags/6.3.11/src/Frontend/Compatibility/WPTT.php
r3503345 r3707539 32 32 */ 33 33 private function init() { 34 add_filter( 'omgf_frontend_process_parse_links', [ $this, 'validate_link_element' ], 10, 2);34 add_filter( 'omgf_frontend_process_parse_links', [ $this, 'validate_link_element' ], 10, 3 ); 35 35 add_filter( 'omgf_frontend_process_invalid_request', [ $this, 'validate_request' ], 10, 2 ); 36 36 } … … 42 42 * @see \OMGF\Frontend\Process::process() 43 43 * 44 * @since v6.3.11 Validate the element's attribute values, instead of matching its markup, so only 45 * elements which actually point to the stylesheet are included. 46 * 44 47 * @param $is_valid 45 48 * @param $link 49 * @param $urls The element's attribute values, sanitized by @see \OMGF\Frontend\Process::get_element_urls() 46 50 * 47 51 * @return bool 48 52 */ 49 public function validate_link_element( $is_valid, $link ) { 50 return $is_valid || preg_match( '/wp-content\/fonts\/[a-zA-Z0-9]{32}\.css/', $link ); 53 public function validate_link_element( $is_valid, $link, $urls = [] ) { 54 if ( $is_valid ) { 55 return true; 56 } 57 58 foreach ( (array) $urls as $url ) { 59 if ( $this->is_webfont_loader_stylesheet( $url ) ) { 60 return true; 61 } 62 } 63 64 return false; 65 } 66 67 /** 68 * Does $url point to a stylesheet stored by the WPTT Webfont Loader? 69 * 70 * @since v6.3.11 71 * 72 * @param string $url 73 * 74 * @return bool 75 */ 76 private function is_webfont_loader_stylesheet( $url ) { 77 $path = wp_parse_url( html_entity_decode( (string) $url ), PHP_URL_PATH ); 78 79 return is_string( $path ) && (bool) preg_match( '/wp-content\/fonts\/[a-zA-Z0-9]{32}\.css$/', $path ); 51 80 } 52 81 … … 57 86 * @see \OMGF\Frontend\Process::build_search_replace() 58 87 * 88 * @since v6.3.11 The condition was inverted: a WPTT stylesheet (which has no family parameter) was marked 89 * as invalid and removed from the page, while any other request without a family parameter 90 * was no longer marked as invalid, and requested instead of removed. 91 * 59 92 * @param $is_invalid 60 93 * @param $url … … 63 96 */ 64 97 public function validate_request( $is_invalid, $url ) { 65 return $is_invalid && preg_match( '/wp-content\/fonts\/[a-zA-Z0-9]{32}\.css/',$url );98 return $is_invalid && ! $this->is_webfont_loader_stylesheet( $url ); 66 99 } 67 100 } -
host-webfonts-local/tags/6.3.11/src/Frontend/Process.php
r3641590 r3707539 44 44 45 45 /** 46 * The hosts of the Google Fonts API (and its GDPR compliant alternatives) OMGF processes, along with 47 * the path(s) a stylesheet request to each of them starts with. 48 * 49 * @see self::is_font_api_url() 50 * @since v6.3.11 51 */ 52 const FONT_API_HOSTS = [ 53 'fonts.googleapis.com' => [ '/css' ], 54 'fonts.bunny.net' => [ '/css' ], 55 'fonts-api.wp.com' => [ '/css' ], 56 ]; 57 58 /** 46 59 * Post types that still trigger template_redirect. 47 60 * … … 432 445 * $link itself is left untouched, because it's passed to the filter below and 433 446 * used for search/replace later on. 447 * @since v6.3.11 Detect the API by parsing the element's attribute values, instead of running a 448 * substring match against the serialized element. Only elements which actually 449 * point at the API should be processed, not elements which merely mention it. 434 450 */ 435 $sanitized = $this->sanitize_url( $link ); 436 437 return apply_filters( 438 'omgf_frontend_process_parse_links', 439 str_contains( $sanitized, 'fonts.googleapis.com/css' ) || str_contains( $sanitized, 'fonts.bunny.net/css' ) || str_contains( $sanitized, 'fonts-api.wp.com/css' ), 440 $link 441 ); 451 $urls = $this->get_element_urls( $link ); 452 $found = false; 453 454 foreach ( $urls as $url ) { 455 if ( $this->is_font_api_url( $url ) ) { 456 $found = true; 457 458 break; 459 } 460 } 461 462 /** 463 * @since v6.3.11 The element's (sanitized) attribute values are passed along, so 3rd parties can 464 * validate the URLs themselves, instead of matching the element's markup. 465 */ 466 return apply_filters( 'omgf_frontend_process_parse_links', $found, $link, $urls ); 442 467 } 443 468 ); … … 475 500 private function is_amp() { 476 501 return ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) || ( function_exists( 'ampforwp_is_amp_endpoint' ) && ampforwp_is_amp_endpoint() ); 502 } 503 504 /** 505 * Returns the (sanitized, non-empty) values of all quoted attributes in a HTML element. 506 * 507 * Which attribute holds the stylesheet's URL isn't set in stone: themes and plugins are known to 508 * park it in e.g. a data-href attribute and fill the href attribute in later on. 509 * 510 * @see self::sanitize_url() 511 * @since v6.3.11 512 * 513 * @param string $element A serialized HTML element, e.g. <link href="..." rel="stylesheet" />. 514 * 515 * @return array 516 */ 517 public function get_element_urls( $element ) { 518 preg_match_all( '/[a-zA-Z0-9_:.-]+=([\'"])(?P<value>[^\'"]*)\1/', (string) $element, $attributes ); 519 520 if ( empty( $attributes['value'] ) ) { 521 return []; // @codeCoverageIgnore 522 } 523 524 return array_values( array_filter( array_map( [ $this, 'sanitize_url' ], $attributes['value'] ) ) ); 525 } 526 527 /** 528 * Is $url a request to one of the Google Fonts API (compatible) endpoints OMGF processes? 529 * 530 * The host is matched in full (never as a substring) and the path has to be the endpoint serving 531 * the stylesheets. 532 * 533 * @see self::FONT_API_HOSTS 534 * @since v6.3.11 535 * 536 * @param string $url 537 * 538 * @return bool 539 */ 540 public function is_font_api_url( $url ) { 541 if ( ! is_string( $url ) || $url === '' ) { 542 return false; 543 } 544 545 /** 546 * Validate the URL in the same shape it's requested in later on. 547 * 548 * @see \OMGF\Frontend\Filters::decode_url() 549 */ 550 $url = $this->sanitize_url( html_entity_decode( $url ) ); 551 552 /** 553 * A scheme less URL (e.g. fonts.googleapis.com/css?family=Roboto) isn't parsed into a host by 554 * wp_parse_url(), while a scheme relative URL (//fonts.googleapis.com/css?family=Roboto) is. 555 */ 556 if ( ! preg_match( '~^(https?:)?//~i', $url ) ) { 557 $url = '//' . ltrim( $url, '/' ); 558 } 559 560 $parts = wp_parse_url( $url ); 561 562 if ( empty( $parts['host'] ) ) { 563 return false; // @codeCoverageIgnore 564 } 565 566 /** 567 * @filter omgf_font_api_hosts Allows add-ons to process additional endpoints, e.g. Material Icons. 568 */ 569 $hosts = apply_filters( 'omgf_font_api_hosts', self::FONT_API_HOSTS ); 570 $host = strtolower( rtrim( $parts['host'], '.' ) ); 571 572 if ( ! isset( $hosts[ $host ] ) ) { 573 return false; 574 } 575 576 $path = strtolower( $parts['path'] ?? '' ); 577 578 foreach ( (array) $hosts[ $host ] as $prefix ) { 579 if ( str_starts_with( $path, $prefix ) ) { 580 return true; 581 } 582 } 583 584 return false; 585 } 586 587 /** 588 * Strips characters from a URL which browsers ignore when requesting it. 589 * 590 * HTML allows line breaks (and tabs) inside attribute values, e.g., to keep a long Google Fonts API request 591 * readable in the editor. Browsers remove those characters before requesting the URL, which means the 592 * stylesheet loads just fine, while the same URL would be rejected when used as-is in a HTTP request. 593 * 594 * @see https://url.spec.whatwg.org/#concept-basic-url-parser (tab/newline removal) 595 * @since v6.3.10 596 * 597 * @param string $url 598 * 599 * @return string 600 */ 601 private function sanitize_url( $url ) { 602 return trim( str_replace( [ "\r", "\n", "\t" ], '', $url ) ); 477 603 } 478 604 … … 572 698 573 699 return $handle; 574 }575 576 /**577 * Strips characters from a URL which browsers ignore when requesting it.578 *579 * HTML allows line breaks (and tabs) inside attribute values, e.g., to keep a long Google Fonts API request580 * readable in the editor. Browsers remove those characters before requesting the URL, which means the581 * stylesheet loads just fine, while the same URL would be rejected when used as-is in a HTTP request.582 *583 * @see https://url.spec.whatwg.org/#concept-basic-url-parser (tab/newline removal)584 * @since v6.3.10585 *586 * @param string $url587 *588 * @return string589 */590 private function sanitize_url( $url ) {591 return trim( str_replace( [ "\r", "\n", "\t" ], '', $url ) );592 700 } 593 701 -
host-webfonts-local/tags/6.3.11/src/Helper.php
r3586383 r3707539 20 20 21 21 class Helper { 22 /** 23 * CSS-wide keywords and generic font families. They're valid values for a font-family 24 * declaration, but they're never the name of an actual font. 25 * 26 * @see self::is_valid_font_family() 27 * @since v6.3.11 28 */ 29 const OMGF_INVALID_FONT_FAMILIES = [ 30 'cursive', 31 'default', 32 'emoji', 33 'fangsong', 34 'fantasy', 35 'inherit', 36 'initial', 37 'math', 38 'monospace', 39 'none', 40 'revert', 41 'revert-layer', 42 'sans-serif', 43 'serif', 44 'system-ui', 45 'ui-monospace', 46 'ui-rounded', 47 'ui-sans-serif', 48 'ui-serif', 49 'unset', 50 ]; 51 22 52 /** 23 53 * Property to hold all settings. … … 227 257 228 258 /** 259 * Check if $family is (or could be) the name of an actual font. 260 * 261 * A font-family name is a single name. Any value which is a complete declaration value 262 * (e.g., 'Arial,Helvetica,sans-serif !important' or 'var(--wp--preset--font-family--foo)'), 263 * a CSS-wide keyword (e.g., 'inherit') or a generic family (e.g., 'sans-serif') is never a 264 * font we can download, and should never be stored as one. 265 * 266 * This can't be an allow list of characters, because font family names are free-form and 267 * can contain (non-Latin) letters, digits, spaces, dots and dashes. 268 * 269 * @since v6.3.11 270 * 271 * @param string $family 272 * 273 * @return bool 274 */ 275 public static function is_valid_font_family( $family ) { 276 if ( ! is_string( $family ) ) { 277 return false; 278 } 279 280 $family = trim( $family ); 281 282 if ( $family === '' ) { 283 return false; 284 } 285 286 /** 287 * Any of these characters means this isn't a name, but (a leftover of) a complete 288 * declaration value: a font stack, a CSS function, an !important flag, or CSS syntax. 289 */ 290 if ( preg_match( '/[,!(){};:"\']/', $family ) ) { 291 return false; 292 } 293 294 // A CSS custom property, e.g. --wp--preset--font-family--foo. 295 if ( str_starts_with( $family, '--' ) ) { 296 return false; 297 } 298 299 return ! in_array( strtolower( $family ), self::OMGF_INVALID_FONT_FAMILIES, true ); 300 } 301 302 /** 229 303 * Delete a file or directory from the filesystem. 230 304 * -
host-webfonts-local/tags/6.3.11/src/Optimize.php
r3494741 r3707539 315 315 } 316 316 317 /** 318 * @since v6.3.11 Make sure the response is a stylesheet before it's parsed as one. The Google Fonts 319 * API (and any locally hosted stylesheet) is served as text/css, so anything else 320 * shouldn't be parsed. 321 */ 322 if ( ! $this->is_stylesheet_response( $response ) ) { 323 return ''; // @codeCoverageIgnore 324 } 325 317 326 return wp_remote_retrieve_body( $response ); 327 } 328 329 /** 330 * Does $response contain a stylesheet? 331 * 332 * A response without a Content-Type is accepted, because not every (proxy) server sets one. 333 * 334 * @since v6.3.11 335 * 336 * @param array $response A response as returned by wp_remote_get(). 337 * 338 * @return bool 339 */ 340 private function is_stylesheet_response( $response ) { 341 $content_type = wp_remote_retrieve_header( $response, 'content-type' ); 342 343 if ( is_array( $content_type ) ) { 344 $content_type = reset( $content_type ); // @codeCoverageIgnore 345 } 346 347 if ( ! $content_type ) { 348 return true; 349 } 350 351 // Normalize before lookup, i.e. strip the parameters (e.g. charset) and lowercase it. 352 $content_type = strtolower( trim( explode( ';', (string) $content_type )[0] ) ); 353 354 /** 355 * @filter omgf_optimize_stylesheet_content_types Allows adding Content-Types used by servers which 356 * don't serve stylesheets as text/css. 357 */ 358 $allowed = apply_filters( 359 'omgf_optimize_stylesheet_content_types', 360 [ 'text/css', 'text/x-css', 'application/css', 'application/x-css' ] 361 ); 362 363 if ( in_array( $content_type, $allowed, true ) ) { 364 return true; 365 } 366 367 // @codeCoverageIgnoreStart 368 OMGF::debug( 369 sprintf( 370 __( 'The response for %1$s was ignored, because it isn\'t a stylesheet: %2$s.', 'host-webfonts-local' ), 371 $this->url, 372 $content_type 373 ) 374 ); 375 376 return false; 377 // @codeCoverageIgnoreEnd 318 378 } 319 379 … … 335 395 336 396 $font_families = array_unique( $font_families[1] ); 337 $object = []; 397 398 /** 399 * @since v6.3.11 Only keep values which are (or could be) the name of an actual font. 400 * A stylesheet which isn't a Google Fonts API response contains font-family 401 * declarations outside @font-face statements, e.g. 'Arial,Helvetica,sans-serif 402 * !important' or 'inherit'. Those can't be downloaded, and there's no font to 403 * match them to, but they were stored as font families all the same. They were 404 * also interpolated (unescaped) into a pattern in self::parse_variants(), where 405 * any character with a special meaning in a regular expression is interpreted 406 * as such. 407 */ 408 $valid_families = array_filter( $font_families, [ OMGF::class, 'is_valid_font_family' ] ); 409 $invalid_families = array_diff( $font_families, $valid_families ); 410 411 if ( ! empty( $invalid_families ) ) { 412 OMGF::debug_array( __( 'Skipped invalid font-families', 'host-webfonts-local' ), $invalid_families ); 413 } 414 415 $font_families = $valid_families; 416 417 if ( empty( $font_families ) ) { 418 return []; 419 } 420 421 $object = []; 338 422 339 423 OMGF::debug_array( __( 'Font-families found', 'host-webfonts-local' ), $font_families ); … … 398 482 foreach ( $font_faces[0] as $font_face ) { 399 483 /** 400 * @since v5.3.3 Exact match for font-family attribute, to prevent similar font names from falling through, e.g., Roboto and Roboto Slab. 484 * @since v5.3.3 Exact match for font-family attribute, to prevent similar font names from falling through, e.g., Roboto and Roboto Slab. 485 * @since v6.3.11 Escape the font family, so a name containing a character with a special meaning in a 486 * regular expression (e.g., '.*') is matched literally, instead of matching (and claiming 487 * the variants of) every other @font-face statement in the stylesheet. 401 488 */ 402 if ( ! preg_match( '/font-family:[\s\'"]*?' . $font_family. '[\'"]?;/', $font_face ) ) {489 if ( ! preg_match( '/font-family:[\s\'"]*?' . preg_quote( $font_family, '/' ) . '[\'"]?;/', $font_face ) ) { 403 490 continue; // @codeCoverageIgnore 404 491 } -
host-webfonts-local/trunk/host-webfonts-local.php
r3641590 r3707539 4 4 * Plugin URI: https://daan.dev/wordpress/omgf/ 5 5 * Description: Increase GDPR/DSGVO compliance and leverage browser cache by automatically self-hosting Google Fonts. 6 * Version: 6.3.1 06 * Version: 6.3.11 7 7 * Author: Daan from Daan.dev 8 8 * Author URI: https://daan.dev -
host-webfonts-local/trunk/readme.txt
r3641590 r3707539 4 4 Requires at least: 5.9 5 5 Tested up to: 7.0 6 Stable tag: 6.3.1 06 Stable tag: 6.3.11 7 7 Requires PHP: 7.3 8 8 License: GPLv2 or later … … 87 87 88 88 == Changelog == 89 90 = 6.3.11 = 91 * Security: hardened the detection of Google Fonts stylesheets and the handling of downloaded files. Thanks to Wordfence for responsibly disclosing this issue. 92 * Security: Save & Optimize can now only be started by an authorized request. Thanks to Ángel Santana (via WPScan) for responsibly disclosing this issue. 93 * Fixed: stylesheets generated by the WPTT Webfont Loader were removed from the page instead of being optimized because the compatibility fix's validation was inverted. 94 * Fixed: a font family whose name contains a character with a special meaning in a regular expression (e.g., a dot or an asterisk) claimed the @font-face statements — and the downloads — of every 95 other font in the same stylesheet. The name is now escaped before it's matched. 96 * Improved: values which can't be the name of a font are no longer stored as font families, e.g. 'Arial, Helvetica, sans-serif !important', 'var(--tve-font-family,Mulish)', 'inherit' and 'sans-serif.' 97 A stylesheet which isn't a Google Fonts API response (e.g., a theme's stylesheet, which is only processed by OMGF Pro) contains font-family declarations outside its @font-face statements, and each 98 of those was stored as a font family. They can't be downloaded, so they only cluttered the Optimize Fonts screen. Enable Debug Mode to log which values were skipped. 89 99 90 100 = 6.3.10 = -
host-webfonts-local/trunk/src/Admin/Actions.php
r3641590 r3707539 248 248 set_transient( 'settings_errors', get_settings_errors(), 30 ); 249 249 250 // Redirect back to the settings page that was submitted. 251 $goback = add_query_arg( 'settings-updated', 'true', wp_get_referer() ); 250 // Redirect back to the settings page that was submitted. The nonce authorizes the Save & Optimize run. 251 $goback = add_query_arg( 252 [ 253 'settings-updated' => 'true', 254 '_wpnonce' => wp_create_nonce( Optimize::NONCE_ACTION ), 255 ], 256 wp_get_referer() 257 ); 252 258 253 259 if ( ! defined( 'DAAN_DOING_TESTS' ) ) { -
host-webfonts-local/trunk/src/Admin/Optimize.php
r3494741 r3707539 21 21 22 22 class Optimize { 23 /** 24 * Nonce action which authorizes a Save & Optimize run. It's added to the redirect URL after 25 * the settings are saved, so it's only present on a request that follows a form submit. 26 */ 27 const NONCE_ACTION = 'omgf-optimize'; 28 23 29 /** @var string */ 24 30 private $settings_page = ''; … … 30 36 private $settings_updated = false; 31 37 38 /** @var string */ 39 private $nonce = ''; 40 32 41 /** 33 42 * OMGF\Admin\Optimize constructor. 34 *35 * @codeCoverageIgnore36 43 */ 37 44 public function __construct() { … … 39 46 $this->settings_tab = isset( $_GET['tab'] ) ? sanitize_text_field( $_GET['tab'] ) : Settings::OMGF_SETTINGS_FIELD_OPTIMIZE; 40 47 $this->settings_updated = isset( $_GET['settings-updated'] ); 48 $this->nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( $_GET['_wpnonce'] ) : ''; 41 49 42 50 $this->init(); … … 59 67 if ( ! $this->settings_updated ) { 60 68 return; // @codeCoverageIgnore 69 } 70 71 /** 72 * A run has to be authorized by a capable user and the nonce that's issued when the settings are saved. 73 * 74 * @see Actions::update_settings() 75 */ 76 if ( ! current_user_can( 'manage_options' ) ) { 77 return; 78 } 79 80 if ( wp_verify_nonce( $this->nonce, self::NONCE_ACTION ) < 1 ) { 81 return; 61 82 } 62 83 -
host-webfonts-local/trunk/src/Compatibility/CachingPlugins.php
r3494951 r3707539 37 37 * - The `admin_init` action is triggered, 38 38 * - This is OMGF's settings page, 39 * - Settings are updated .39 * - Settings are updated by an administrator. 40 40 * 41 * So, finally, we need to check if this is an administrator.41 * The capability is checked again here, because the action can be triggered from elsewhere. 42 42 * 43 43 * @see \OMGF\Admin\Optimize::init() -
host-webfonts-local/trunk/src/Download.php
r3628684 r3707539 76 76 } 77 77 78 $temp_filename = $this->path . '/' . $this->filename . '.tmp'; 78 /** 79 * @since v6.3.11 Use an unguessable name for the temporary file, because $this->path is inside the 80 * uploads directory, i.e. it's publicly accessible. 81 */ 82 $temp_filename = $this->path . '/' . $this->filename . '-' . wp_generate_password( 12, false ) . '.tmp'; 79 83 80 84 $response = wp_safe_remote_get( … … 88 92 89 93 if ( is_wp_error( $response ) ) { 90 if ( file_exists( $temp_filename ) ) { 91 unlink( $temp_filename ); 92 } 94 $this->delete_temp_file( $temp_filename ); 93 95 94 96 Notice::set_notice( … … 106 108 // Handle non-success HTTP status codes. 107 109 if ( $code < 200 || $code >= 300 ) { 108 if ( file_exists( $temp_filename ) ) { 109 unlink( $temp_filename ); 110 } 110 $this->delete_temp_file( $temp_filename ); 111 111 112 112 Notice::set_notice( … … 123 123 124 124 if ( ! $content_type ) { 125 $this->delete_temp_file( $temp_filename ); 126 125 127 Notice::set_notice( 126 128 __( 'OMGF couldn\'t determine the mime-type for the downloaded font file', 'host-webfonts-local' ) . ': ' . $this->filename, … … 138 140 139 141 if ( ! $extension ) { 142 $this->delete_temp_file( $temp_filename ); 143 140 144 OMGF::debug( 141 145 sprintf( … … 169 173 170 174 // Clean up the temp file 171 if ( file_exists( $temp_filename ) ) { 172 unlink( $temp_filename ); 173 } 175 $this->delete_temp_file( $temp_filename ); 174 176 175 177 return ''; … … 179 181 return OMGF_UPLOAD_URL . str_replace( OMGF_UPLOAD_DIR, '', $this->path ) . '/' . $this->filename . '.' . $extension; 180 182 } 183 184 /** 185 * Removes the temporary file the response was streamed to. 186 * 187 * @since v6.3.11 The temporary file lives inside the uploads directory, which is publicly accessible, so 188 * it should never be left behind, no matter why the download was aborted. 189 * 190 * @param string $temp_filename 191 * 192 * @return void 193 */ 194 private function delete_temp_file( $temp_filename ) { 195 if ( file_exists( $temp_filename ) ) { 196 unlink( $temp_filename ); 197 } 198 } 181 199 } -
host-webfonts-local/trunk/src/Frontend/Compatibility/Elementor.php
r3503345 r3707539 38 38 public function init() { 39 39 add_filter( 'omgf_frontend_process_fonts_set', [ $this, 'maybe_modify_id' ], 10, 2 ); 40 add_filter( 'omgf_frontend_process_parse_links', [ $this, 'validate_link_element' ], 10, 2);40 add_filter( 'omgf_frontend_process_parse_links', [ $this, 'validate_link_element' ], 10, 3 ); 41 41 add_filter( 'omgf_frontend_process_invalid_request', [ $this, 'validate_request' ], 10, 2 ); 42 42 } … … 68 68 * @see \OMGF\Frontend\Process::process() 69 69 * 70 * @since v6.3.11 Validate the element's attribute values, instead of matching its markup, so only 71 * elements which actually point to the stylesheet are included. 72 * 70 73 * @param $is_valid 71 74 * @param $link 75 * @param $urls The element's attribute values, sanitized by @see \OMGF\Frontend\Process::get_element_urls() 72 76 * 73 77 * @return bool 74 78 */ 75 public function validate_link_element( $is_valid, $link ) { 76 return $is_valid || str_contains( $link, '/uploads/elementor/google-fonts' ); 79 public function validate_link_element( $is_valid, $link, $urls = [] ) { 80 if ( $is_valid ) { 81 return true; 82 } 83 84 foreach ( (array) $urls as $url ) { 85 if ( $this->is_google_fonts_stylesheet( $url ) ) { 86 return true; 87 } 88 } 89 90 return false; 91 } 92 93 /** 94 * Does $url point to a Google Fonts stylesheet Elementor stored locally? 95 * 96 * @since v6.3.11 97 * 98 * @param string $url 99 * 100 * @return bool 101 */ 102 private function is_google_fonts_stylesheet( $url ) { 103 $path = wp_parse_url( html_entity_decode( (string) $url ), PHP_URL_PATH ); 104 105 return is_string( $path ) && str_contains( $path, '/uploads/elementor/google-fonts' ); 77 106 } 78 107 … … 89 118 */ 90 119 public function validate_request( $is_invalid, $url ) { 91 return $is_invalid && ! str_contains( $url, '/uploads/elementor/google-fonts');120 return $is_invalid && ! $this->is_google_fonts_stylesheet( $url ); 92 121 } 93 122 } -
host-webfonts-local/trunk/src/Frontend/Compatibility/WPTT.php
r3503345 r3707539 32 32 */ 33 33 private function init() { 34 add_filter( 'omgf_frontend_process_parse_links', [ $this, 'validate_link_element' ], 10, 2);34 add_filter( 'omgf_frontend_process_parse_links', [ $this, 'validate_link_element' ], 10, 3 ); 35 35 add_filter( 'omgf_frontend_process_invalid_request', [ $this, 'validate_request' ], 10, 2 ); 36 36 } … … 42 42 * @see \OMGF\Frontend\Process::process() 43 43 * 44 * @since v6.3.11 Validate the element's attribute values, instead of matching its markup, so only 45 * elements which actually point to the stylesheet are included. 46 * 44 47 * @param $is_valid 45 48 * @param $link 49 * @param $urls The element's attribute values, sanitized by @see \OMGF\Frontend\Process::get_element_urls() 46 50 * 47 51 * @return bool 48 52 */ 49 public function validate_link_element( $is_valid, $link ) { 50 return $is_valid || preg_match( '/wp-content\/fonts\/[a-zA-Z0-9]{32}\.css/', $link ); 53 public function validate_link_element( $is_valid, $link, $urls = [] ) { 54 if ( $is_valid ) { 55 return true; 56 } 57 58 foreach ( (array) $urls as $url ) { 59 if ( $this->is_webfont_loader_stylesheet( $url ) ) { 60 return true; 61 } 62 } 63 64 return false; 65 } 66 67 /** 68 * Does $url point to a stylesheet stored by the WPTT Webfont Loader? 69 * 70 * @since v6.3.11 71 * 72 * @param string $url 73 * 74 * @return bool 75 */ 76 private function is_webfont_loader_stylesheet( $url ) { 77 $path = wp_parse_url( html_entity_decode( (string) $url ), PHP_URL_PATH ); 78 79 return is_string( $path ) && (bool) preg_match( '/wp-content\/fonts\/[a-zA-Z0-9]{32}\.css$/', $path ); 51 80 } 52 81 … … 57 86 * @see \OMGF\Frontend\Process::build_search_replace() 58 87 * 88 * @since v6.3.11 The condition was inverted: a WPTT stylesheet (which has no family parameter) was marked 89 * as invalid and removed from the page, while any other request without a family parameter 90 * was no longer marked as invalid, and requested instead of removed. 91 * 59 92 * @param $is_invalid 60 93 * @param $url … … 63 96 */ 64 97 public function validate_request( $is_invalid, $url ) { 65 return $is_invalid && preg_match( '/wp-content\/fonts\/[a-zA-Z0-9]{32}\.css/',$url );98 return $is_invalid && ! $this->is_webfont_loader_stylesheet( $url ); 66 99 } 67 100 } -
host-webfonts-local/trunk/src/Frontend/Process.php
r3641590 r3707539 44 44 45 45 /** 46 * The hosts of the Google Fonts API (and its GDPR compliant alternatives) OMGF processes, along with 47 * the path(s) a stylesheet request to each of them starts with. 48 * 49 * @see self::is_font_api_url() 50 * @since v6.3.11 51 */ 52 const FONT_API_HOSTS = [ 53 'fonts.googleapis.com' => [ '/css' ], 54 'fonts.bunny.net' => [ '/css' ], 55 'fonts-api.wp.com' => [ '/css' ], 56 ]; 57 58 /** 46 59 * Post types that still trigger template_redirect. 47 60 * … … 432 445 * $link itself is left untouched, because it's passed to the filter below and 433 446 * used for search/replace later on. 447 * @since v6.3.11 Detect the API by parsing the element's attribute values, instead of running a 448 * substring match against the serialized element. Only elements which actually 449 * point at the API should be processed, not elements which merely mention it. 434 450 */ 435 $sanitized = $this->sanitize_url( $link ); 436 437 return apply_filters( 438 'omgf_frontend_process_parse_links', 439 str_contains( $sanitized, 'fonts.googleapis.com/css' ) || str_contains( $sanitized, 'fonts.bunny.net/css' ) || str_contains( $sanitized, 'fonts-api.wp.com/css' ), 440 $link 441 ); 451 $urls = $this->get_element_urls( $link ); 452 $found = false; 453 454 foreach ( $urls as $url ) { 455 if ( $this->is_font_api_url( $url ) ) { 456 $found = true; 457 458 break; 459 } 460 } 461 462 /** 463 * @since v6.3.11 The element's (sanitized) attribute values are passed along, so 3rd parties can 464 * validate the URLs themselves, instead of matching the element's markup. 465 */ 466 return apply_filters( 'omgf_frontend_process_parse_links', $found, $link, $urls ); 442 467 } 443 468 ); … … 475 500 private function is_amp() { 476 501 return ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) || ( function_exists( 'ampforwp_is_amp_endpoint' ) && ampforwp_is_amp_endpoint() ); 502 } 503 504 /** 505 * Returns the (sanitized, non-empty) values of all quoted attributes in a HTML element. 506 * 507 * Which attribute holds the stylesheet's URL isn't set in stone: themes and plugins are known to 508 * park it in e.g. a data-href attribute and fill the href attribute in later on. 509 * 510 * @see self::sanitize_url() 511 * @since v6.3.11 512 * 513 * @param string $element A serialized HTML element, e.g. <link href="..." rel="stylesheet" />. 514 * 515 * @return array 516 */ 517 public function get_element_urls( $element ) { 518 preg_match_all( '/[a-zA-Z0-9_:.-]+=([\'"])(?P<value>[^\'"]*)\1/', (string) $element, $attributes ); 519 520 if ( empty( $attributes['value'] ) ) { 521 return []; // @codeCoverageIgnore 522 } 523 524 return array_values( array_filter( array_map( [ $this, 'sanitize_url' ], $attributes['value'] ) ) ); 525 } 526 527 /** 528 * Is $url a request to one of the Google Fonts API (compatible) endpoints OMGF processes? 529 * 530 * The host is matched in full (never as a substring) and the path has to be the endpoint serving 531 * the stylesheets. 532 * 533 * @see self::FONT_API_HOSTS 534 * @since v6.3.11 535 * 536 * @param string $url 537 * 538 * @return bool 539 */ 540 public function is_font_api_url( $url ) { 541 if ( ! is_string( $url ) || $url === '' ) { 542 return false; 543 } 544 545 /** 546 * Validate the URL in the same shape it's requested in later on. 547 * 548 * @see \OMGF\Frontend\Filters::decode_url() 549 */ 550 $url = $this->sanitize_url( html_entity_decode( $url ) ); 551 552 /** 553 * A scheme less URL (e.g. fonts.googleapis.com/css?family=Roboto) isn't parsed into a host by 554 * wp_parse_url(), while a scheme relative URL (//fonts.googleapis.com/css?family=Roboto) is. 555 */ 556 if ( ! preg_match( '~^(https?:)?//~i', $url ) ) { 557 $url = '//' . ltrim( $url, '/' ); 558 } 559 560 $parts = wp_parse_url( $url ); 561 562 if ( empty( $parts['host'] ) ) { 563 return false; // @codeCoverageIgnore 564 } 565 566 /** 567 * @filter omgf_font_api_hosts Allows add-ons to process additional endpoints, e.g. Material Icons. 568 */ 569 $hosts = apply_filters( 'omgf_font_api_hosts', self::FONT_API_HOSTS ); 570 $host = strtolower( rtrim( $parts['host'], '.' ) ); 571 572 if ( ! isset( $hosts[ $host ] ) ) { 573 return false; 574 } 575 576 $path = strtolower( $parts['path'] ?? '' ); 577 578 foreach ( (array) $hosts[ $host ] as $prefix ) { 579 if ( str_starts_with( $path, $prefix ) ) { 580 return true; 581 } 582 } 583 584 return false; 585 } 586 587 /** 588 * Strips characters from a URL which browsers ignore when requesting it. 589 * 590 * HTML allows line breaks (and tabs) inside attribute values, e.g., to keep a long Google Fonts API request 591 * readable in the editor. Browsers remove those characters before requesting the URL, which means the 592 * stylesheet loads just fine, while the same URL would be rejected when used as-is in a HTTP request. 593 * 594 * @see https://url.spec.whatwg.org/#concept-basic-url-parser (tab/newline removal) 595 * @since v6.3.10 596 * 597 * @param string $url 598 * 599 * @return string 600 */ 601 private function sanitize_url( $url ) { 602 return trim( str_replace( [ "\r", "\n", "\t" ], '', $url ) ); 477 603 } 478 604 … … 572 698 573 699 return $handle; 574 }575 576 /**577 * Strips characters from a URL which browsers ignore when requesting it.578 *579 * HTML allows line breaks (and tabs) inside attribute values, e.g., to keep a long Google Fonts API request580 * readable in the editor. Browsers remove those characters before requesting the URL, which means the581 * stylesheet loads just fine, while the same URL would be rejected when used as-is in a HTTP request.582 *583 * @see https://url.spec.whatwg.org/#concept-basic-url-parser (tab/newline removal)584 * @since v6.3.10585 *586 * @param string $url587 *588 * @return string589 */590 private function sanitize_url( $url ) {591 return trim( str_replace( [ "\r", "\n", "\t" ], '', $url ) );592 700 } 593 701 -
host-webfonts-local/trunk/src/Helper.php
r3586383 r3707539 20 20 21 21 class Helper { 22 /** 23 * CSS-wide keywords and generic font families. They're valid values for a font-family 24 * declaration, but they're never the name of an actual font. 25 * 26 * @see self::is_valid_font_family() 27 * @since v6.3.11 28 */ 29 const OMGF_INVALID_FONT_FAMILIES = [ 30 'cursive', 31 'default', 32 'emoji', 33 'fangsong', 34 'fantasy', 35 'inherit', 36 'initial', 37 'math', 38 'monospace', 39 'none', 40 'revert', 41 'revert-layer', 42 'sans-serif', 43 'serif', 44 'system-ui', 45 'ui-monospace', 46 'ui-rounded', 47 'ui-sans-serif', 48 'ui-serif', 49 'unset', 50 ]; 51 22 52 /** 23 53 * Property to hold all settings. … … 227 257 228 258 /** 259 * Check if $family is (or could be) the name of an actual font. 260 * 261 * A font-family name is a single name. Any value which is a complete declaration value 262 * (e.g., 'Arial,Helvetica,sans-serif !important' or 'var(--wp--preset--font-family--foo)'), 263 * a CSS-wide keyword (e.g., 'inherit') or a generic family (e.g., 'sans-serif') is never a 264 * font we can download, and should never be stored as one. 265 * 266 * This can't be an allow list of characters, because font family names are free-form and 267 * can contain (non-Latin) letters, digits, spaces, dots and dashes. 268 * 269 * @since v6.3.11 270 * 271 * @param string $family 272 * 273 * @return bool 274 */ 275 public static function is_valid_font_family( $family ) { 276 if ( ! is_string( $family ) ) { 277 return false; 278 } 279 280 $family = trim( $family ); 281 282 if ( $family === '' ) { 283 return false; 284 } 285 286 /** 287 * Any of these characters means this isn't a name, but (a leftover of) a complete 288 * declaration value: a font stack, a CSS function, an !important flag, or CSS syntax. 289 */ 290 if ( preg_match( '/[,!(){};:"\']/', $family ) ) { 291 return false; 292 } 293 294 // A CSS custom property, e.g. --wp--preset--font-family--foo. 295 if ( str_starts_with( $family, '--' ) ) { 296 return false; 297 } 298 299 return ! in_array( strtolower( $family ), self::OMGF_INVALID_FONT_FAMILIES, true ); 300 } 301 302 /** 229 303 * Delete a file or directory from the filesystem. 230 304 * -
host-webfonts-local/trunk/src/Optimize.php
r3494741 r3707539 315 315 } 316 316 317 /** 318 * @since v6.3.11 Make sure the response is a stylesheet before it's parsed as one. The Google Fonts 319 * API (and any locally hosted stylesheet) is served as text/css, so anything else 320 * shouldn't be parsed. 321 */ 322 if ( ! $this->is_stylesheet_response( $response ) ) { 323 return ''; // @codeCoverageIgnore 324 } 325 317 326 return wp_remote_retrieve_body( $response ); 327 } 328 329 /** 330 * Does $response contain a stylesheet? 331 * 332 * A response without a Content-Type is accepted, because not every (proxy) server sets one. 333 * 334 * @since v6.3.11 335 * 336 * @param array $response A response as returned by wp_remote_get(). 337 * 338 * @return bool 339 */ 340 private function is_stylesheet_response( $response ) { 341 $content_type = wp_remote_retrieve_header( $response, 'content-type' ); 342 343 if ( is_array( $content_type ) ) { 344 $content_type = reset( $content_type ); // @codeCoverageIgnore 345 } 346 347 if ( ! $content_type ) { 348 return true; 349 } 350 351 // Normalize before lookup, i.e. strip the parameters (e.g. charset) and lowercase it. 352 $content_type = strtolower( trim( explode( ';', (string) $content_type )[0] ) ); 353 354 /** 355 * @filter omgf_optimize_stylesheet_content_types Allows adding Content-Types used by servers which 356 * don't serve stylesheets as text/css. 357 */ 358 $allowed = apply_filters( 359 'omgf_optimize_stylesheet_content_types', 360 [ 'text/css', 'text/x-css', 'application/css', 'application/x-css' ] 361 ); 362 363 if ( in_array( $content_type, $allowed, true ) ) { 364 return true; 365 } 366 367 // @codeCoverageIgnoreStart 368 OMGF::debug( 369 sprintf( 370 __( 'The response for %1$s was ignored, because it isn\'t a stylesheet: %2$s.', 'host-webfonts-local' ), 371 $this->url, 372 $content_type 373 ) 374 ); 375 376 return false; 377 // @codeCoverageIgnoreEnd 318 378 } 319 379 … … 335 395 336 396 $font_families = array_unique( $font_families[1] ); 337 $object = []; 397 398 /** 399 * @since v6.3.11 Only keep values which are (or could be) the name of an actual font. 400 * A stylesheet which isn't a Google Fonts API response contains font-family 401 * declarations outside @font-face statements, e.g. 'Arial,Helvetica,sans-serif 402 * !important' or 'inherit'. Those can't be downloaded, and there's no font to 403 * match them to, but they were stored as font families all the same. They were 404 * also interpolated (unescaped) into a pattern in self::parse_variants(), where 405 * any character with a special meaning in a regular expression is interpreted 406 * as such. 407 */ 408 $valid_families = array_filter( $font_families, [ OMGF::class, 'is_valid_font_family' ] ); 409 $invalid_families = array_diff( $font_families, $valid_families ); 410 411 if ( ! empty( $invalid_families ) ) { 412 OMGF::debug_array( __( 'Skipped invalid font-families', 'host-webfonts-local' ), $invalid_families ); 413 } 414 415 $font_families = $valid_families; 416 417 if ( empty( $font_families ) ) { 418 return []; 419 } 420 421 $object = []; 338 422 339 423 OMGF::debug_array( __( 'Font-families found', 'host-webfonts-local' ), $font_families ); … … 398 482 foreach ( $font_faces[0] as $font_face ) { 399 483 /** 400 * @since v5.3.3 Exact match for font-family attribute, to prevent similar font names from falling through, e.g., Roboto and Roboto Slab. 484 * @since v5.3.3 Exact match for font-family attribute, to prevent similar font names from falling through, e.g., Roboto and Roboto Slab. 485 * @since v6.3.11 Escape the font family, so a name containing a character with a special meaning in a 486 * regular expression (e.g., '.*') is matched literally, instead of matching (and claiming 487 * the variants of) every other @font-face statement in the stylesheet. 401 488 */ 402 if ( ! preg_match( '/font-family:[\s\'"]*?' . $font_family. '[\'"]?;/', $font_face ) ) {489 if ( ! preg_match( '/font-family:[\s\'"]*?' . preg_quote( $font_family, '/' ) . '[\'"]?;/', $font_face ) ) { 403 490 continue; // @codeCoverageIgnore 404 491 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)