o0: ߛv$ε"gFPhF ̞yZ1n+A8w_63F?`+؍0ʛi 6E"fz|]yr gqŮ:ǵނPTNv[_ *.HcW gn Z?זҹ/F&[jtIh<5|ĀE/~ N?\6QV׵e"4{| F>WU6H}kd Ad p U3$Zt%Fž)C rui $ѻ"m}fh1,D5iΟECCm\c~[$!L騺if,2)&[\?36fi7l$JO_n;$ײp ql[ak(MF3r%^SiSXD&H̀##77Ð~']y;Ln9Le$xDMe>+n=!/w3X8P RdC]*h?S;s_yh;2IpSk#lwjׯ9s1-$NT1i`ypHFw:{O<킥UnU0_^GPRh^ڬ\Suij?5~WB7OR&kN|iUߦX]7}f,Sc˂7>__iǫ#tRwu/V{V |A}kNbH fdy,S# $b|PY}$Ŷ)[H~# h!lξ8 cZBi`[D9p$kW rUZamWC,x'NoIĴ~vq֣A̪4Q8ݍI÷%_tfoh4mb_`6CH"z*& BU ½]=vBut44HAzbkJ,sʼn,8j6Y \SmrR&}HFYxr1t}e \4ZޞvsJ!m'5ۤ EԀYXOZfg8e&v. D8C8~pB>q]_6^n`T:gr) e ݗ$U {=s GPLꅖV@R94DIƽѸhVl=bИGd)UG-pj0sf V]%PqE][!btCY8Vc(_ h~@B e1Doᘏ| IAiTڄo2'Nf٬w&n|AY]Qm+nB(EdO[Iv̂הUD@b囩,:-}HfxKAS_TleIz+fҀtOL5 wsBݣyC'l0X5LrIVļK1~x`j)*0f[R+pYjhmt53Z 'U_Nl_EUŗ:3UYZpYqU_9_@*b@Y*D ]<YcZ[@:/c`4p75+9a/!T-cP6üQU&$R# 5nԱO׼L[D^m̴D6ɾ(M` _&ú~p6'JtW,;gY|F核0DnTtt2D[IS]uðn2N9Aӭ/U57 SF>ߦ@5)"=M]G7^+a)z;&Fylʣn~…K.P<&ӰZEiD@Ŝ5紊} Cq1ALRƺqg gG+`F/ &~(nRIJ3 Q':hG)E&_hD_cE;!ҎZYn!V8|ߑ5Aʱ<}9mh%O;ƞF8Q+NIrc7;#t*( ,svqFm.LNoȣS 7)|QD tt}OX<= ~!ES@+]<2АeydT ͨ89iy&oi:UŃ F}){!( lGYS"I$GP.,PzԽNV77igyRatВ^O+DX9dg1F%"4BKU)`x}ygU1A9ٯ"cO6/,*3/ktG~ϹV#4"Z>e* h!M;O;2P5CQ.0"HpW &c/"OB]'RG=&v4UkW46Ρ]kaضYr( ú J+$ͧ`|Fe$Qzj܅.F@H8*dskj8)6[z jN]Jf?~bH7a"b2۸9_ZUҠ+xI/Oϟ29D 7TkыIM{7&b.ϕ'{QZ!edЕѺEN]x.q`&0 ڬ8hu(K?U%)N'D?$z>PΙ™,u(DrB ksvg@D`:J@27CWTr^[߻)D8;C 7)V~2_"NYghλzF|5sWeMpr LJjoH~̼́y ;ceAPh̆f]V-qr$us G6S*aI ln 4/>T9j2t5W"N 4rm|&`w%L1 | |T% _#9F/SͰ,A(3WRAEe%c $font_faces ) { foreach ( $font_faces as $font_face ) { $preloads_stylesheet = $preloaded_fonts[ $stylesheet_handle ] ?? []; if ( ! in_array( $font_face->id, array_keys( $preloads_stylesheet ) ) ) { continue; // @codeCoverageIgnore } $font_id = $font_face->id; $preload_variants = array_filter( (array) $font_face->variants, function ( $variant ) use ( $preloads_stylesheet, $font_id ) { return in_array( $variant->id, $preloads_stylesheet[ $font_id ] ); } ); /** * @since v5.3.0 Store all preloaded URLs temporarily, to make sure no duplicate files (Variable Fonts) are preloaded. */ $preloaded = []; foreach ( $preload_variants as $variant ) { $url = rawurldecode( $variant->woff2 ); /** * @since v5.5.4 Since we're forcing relative URLs since v5.5.0, let's make sure $url is a relative URL to ensure * backwards compatibility. */ $url = str_replace( [ 'http:', 'https:' ], '', $url ); /** * @since v5.0.1 An extra check, because people tend to forget to flush their caches when changing fonts, etc. */ $file_path = str_replace( OMGF_UPLOAD_URL, OMGF_UPLOAD_DIR, apply_filters( 'omgf_frontend_process_url', $url ) ); if ( ! defined( 'DAAN_DOING_TESTS' ) && ! file_exists( $file_path ) || in_array( $url, $preloaded ) ) { continue; // @codeCoverageIgnore } $preloaded[] = $url; echo wp_kses( "\n", self::PRELOAD_ALLOWED_HTML ); $i ++; } } } } /** * Start output buffer. * * @action template_redirect * @return bool|string valid HTML. * * @codeCoverageIgnore */ public function maybe_buffer_output() { /** * Always run, if the omgf_optimize parameter (added by Save & Optimize) is set. */ if ( isset( $_GET[ 'omgf_optimize' ] ) ) { do_action( 'omgf_frontend_process_before_ob_start' ); return ob_start( [ $this, 'return_buffer' ] ); } /** * Make sure Page Builder previews don't get optimized content. */ foreach ( $this->page_builders as $page_builder ) { if ( array_key_exists( $page_builder, $_GET ) ) { return false; } } /** * Post edit actions */ if ( array_key_exists( 'action', $_GET ) ) { if ( in_array( $_GET[ 'action' ], $this->edit_actions, true ) ) { return false; } } /** * Honor PageSpeed=off parameter as used by mod_pagespeed, in use by some pagebuilders, * * @see https://www.modpagespeed.com/doc/experiment#ModPagespeed */ if ( array_key_exists( 'PageSpeed', $_GET ) && 'off' === $_GET[ 'PageSpeed' ] ) { return false; } /** * Customizer previews shouldn't get optimized content. */ if ( function_exists( 'is_customize_preview' ) && is_customize_preview() ) { return false; } do_action( 'omgf_frontend_process_before_ob_start' ); /** * Let's GO! */ ob_start( [ $this, 'return_buffer' ] ); } /** * Returns the buffer for filtering, so page cache doesn't break. * * @since v5.0.0 Tested with: * - Asset Cleanup Pro * - Works * - Cache Enabler v1.8.7 * - Default Settings * - Kinsta Cache (Same as Cache Enabler?) * - Works on Daan.dev * - LiteSpeed Cache * - Don't know (Gal Baras tested it: https://wordpress.org/support/topic/completely-broke-wp-rocket-plugin/#post-15377538) * - W3 Total Cache v2.2.1: * - Page Cache: Disk (basic) * - Database/Object Cache: Off * - JS/CSS minify/combine: On * - WP Fastest Cache v0.9.5 * - JS/CSS minify/combine: On * - Page Cache: On * - WP Rocket v3.8.8: * - Page Cache: Enabled * - JS/CSS minify/combine: Enabled * - WP Super Cache v1.7.4 * - Page Cache: Enabled * Not tested (yet): * TODO: [OMGF-41] - Swift Performance * @return string Valid HTML * * @codeCoverageIgnore */ public function return_buffer( $html ) { if ( ! $html ) { return $html; } return apply_filters( 'omgf_buffer_output', $html ); } /** * We're downloading the fonts, so preconnecting to Google is a waste of time. Literally. * * @since v5.0.5 Use a regular expression to match all resource hints. * * @param string $html Valid HTML. * * @return string Valid HTML. */ public function remove_resource_hints( $html ) { /** * @since v5.1.5 Use a lookaround that matches all link elements, because otherwise * matches grow past their supposed boundaries. */ preg_match_all( '/(?=\)/s', $html, $resource_hints ); if ( empty( $resource_hints[ 0 ] ) ) { return $html; // @codeCoverageIgnore } /** * @since v5.1.5 Filter out any resource hints with a href pointing to Google Fonts' APIs. * @since v5.2.1 Use preg_match() to exactly match an element's attribute, since 3rd party * plugins (e.g. Asset Cleanup) also tend to include their own custom attributes, * e.g. data-wpacu-to-be-preloaded, which would also match in strpos('preload', $match). */ $search = array_filter( $resource_hints[ 0 ], function ( $resource_hint ) { preg_match( '/href=[\'"](https?:)?\/\/(.*?)[\'"\/]/', $resource_hint, $url ); preg_match( '/rel=[\'"](.*?)[ \'"]/', $resource_hint, $attr ); if ( empty( $url[ 2 ] ) || empty( $attr[ 1 ] ) ) { return false; // @codeCoverageIgnore } $url = $url[ 2 ]; $attr = $attr[ 1 ]; return ! empty( preg_grep( "/$url/", self::RESOURCE_HINTS_URLS ) ) && in_array( $attr, self::RESOURCE_HINTS_ATTR ); } ); return str_replace( $search, '', $html ); } /** * This method uses Regular Expressions to parse the HTML. It's tested to be at least * twice as fast compared to using Xpath. * Test results (in seconds, with XDebug enabled) * Uncached: 17.81094789505 * 18.687641859055 * 18.301512002945 * Cached: 0.00046515464782715 * 0.00037288665771484 * 0.00053095817565918 * Using Xpath proved to be untestable, because it varied anywhere between 38 seconds and, well, timeouts. * * @param string $html Valid HTML. * * @return string Valid HTML, filtered by @filter omgf_processed_html. */ public function parse( $html ) { if ( $this->is_amp() ) { return apply_filters( 'omgf_processed_html', $html, $this ); // @codeCoverageIgnore } /** * @since v5.3.5 Use a generic regex and filter them separately. */ preg_match_all( '//s', $html, $links ); if ( empty( $links[ 0 ] ) ) { return apply_filters( 'omgf_processed_html', $html, $this ); // @codeCoverageIgnore } /** * @since v5.4.0 This approach is global on purpose. By just matching elements containing the fonts.googleapis.com/css string, * e.g. preload elements are also properly processed. * @since v5.4.0 Added compatibility for BunnyCDN's "GDPR compliant" Google Fonts API. * @since v5.4.1 Make sure hitting the domain, not a subfolder generated by some plugins. * @since v5.5.0 Added compatibility for WP.com's "GDPR compliant" Google Fonts API. */ $links = array_filter( $links[ 0 ], function ( $link ) { return str_contains( $link, 'fonts.googleapis.com/css' ) || str_contains( $link, 'fonts.bunny.net/css' ) || str_contains( $link, 'fonts-api.wp.com/css' ); } ); $google_fonts = $this->build_fonts_set( $links ); $search_replace = $this->build_search_replace( $google_fonts ); if ( empty( $search_replace[ 'search' ] ) || empty( $search_replace[ 'replace' ] ) ) { return apply_filters( 'omgf_processed_html', $html, $this ); } /** * Use string position of $search to make sure only that instance of the string is replaced. * This is to prevent duplicate replaces. * * @since v5.3.7 */ foreach ( $search_replace[ 'search' ] as $key => $search ) { $position = strpos( $html, $search ); if ( $position !== false && isset( $search_replace[ 'replace' ][ $key ] ) ) { $html = substr_replace( $html, $search_replace[ 'replace' ][ $key ], $position, strlen( $search ) ); } } $found_iframes = OMGF::get_option( Settings::OMGF_FOUND_IFRAMES, [] ); $count_iframes = count( $found_iframes ); foreach ( TaskManager::IFRAMES_LOADING_FONTS as $script_id => $script ) { if ( str_contains( $html, $script ) && ! in_array( $script_id, $found_iframes ) ) { $found_iframes[] = $script_id; } } if ( $count_iframes !== count( $found_iframes ) ) { OMGF::update_option( Settings::OMGF_FOUND_IFRAMES, $found_iframes ); } return apply_filters( 'omgf_processed_html', $html, $this ); } /** * @since v5.0.5 Check if current page is AMP page. * @return bool */ private function is_amp() { return ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) || ( function_exists( 'ampforwp_is_amp_endpoint' ) && ampforwp_is_amp_endpoint() ); } /** * Builds a processable array of Google Fonts' ID and (external) URL. * * @param array $links * @param string $handle If an ID attribute is not defined, this will be used instead. * * @return array [ 0 => [ 'id' => (string), 'href' => (string) ] ] */ public function build_fonts_set( $links, $handle = 'omgf-stylesheet' ) { $google_fonts = []; foreach ( $links as $key => $link ) { preg_match( '/id=[\'"](?P.*?)[\'"]/', $link, $id ); /** * @var string $id Fallback to empty string if no id attribute exists. */ $id = $this->strip_css_tag( $id[ 'id' ] ?? '' ); preg_match( '/href=[\'"](?P.*?)[\'"]/', $link, $href ); /** * No valid href attribute provide in link element. */ if ( ! isset( $href[ 'href' ] ) ) { continue; // @codeCoverageIgnore } /** * Mesmerize Theme compatibility */ if ( $href[ 'href' ] === '#' ) { preg_match( '/data-href=[\'"](?P.*?)[\'"]/', $link, $href ); } /** * If no valid id attribute was found then this means that this stylesheet wasn't enqueued * using proper WordPress conventions. We generate our own using the length of the href attribute * to serve as a UID. This prevents clashes with other non-properly enqueued stylesheets on other pages. * * @since v5.1.4 */ if ( ! $id ) { $id = "$handle-" . strlen( $href[ 'href' ] ); } /** * Compatibility fix for Divi Builder * * @since v5.1.3 Because Divi Builder uses the same handle for Google Fonts on each page, * even when these contain Google Fonts, let's append a (kind of) unique * identifier to the string, to make sure we can make a difference between * different Google Fonts configurations. * @since v5.2.0 Allow Divi/Elementor compatibility fixes to be disabled, for those who have too * many different Google Fonts stylesheets configured throughout their pages and * blame OMGF for the fact that it detects all those different stylesheets. :-/ */ if ( OMGF::get_option( Settings::OMGF_ADV_SETTING_COMPATIBILITY ) && str_contains( $id, 'et-builder-googlefonts' ) ) { $google_fonts[ $key ][ 'id' ] = $id . '-' . strlen( $href[ 'href' ] ); } elseif ( OMGF::get_option( Settings::OMGF_ADV_SETTING_COMPATIBILITY ) && $id === 'google-fonts-1' ) { /** * Compatibility fix for Elementor * * @since v5.1.4 Because Elementor uses the same (annoyingly generic) handle for Google Fonts * stylesheets on each page, even when these contain different Google Fonts than * other pages, let's append a (kind of) unique identifier to the string, to make * sure we can make a difference between different Google Fonts configurations. */ $google_fonts[ $key ][ 'id' ] = str_replace( '-1', '-' . strlen( $href[ 'href' ] ), $id ); } elseif ( str_contains( $id, 'sp-wpcp-google-fonts' ) ) { /** * Compatibility fix for Category Slider Pro for WooCommerce by ShapedPlugin * * @since v5.3.7 This plugin finds it necessary to provide each Google Fonts stylesheet with a * unique identifier on each pageload, to make sure its never cached. The worst idea ever. * On top of that, it throws OMGF off the rails entirely, eventually crashing the site. */ $google_fonts[ $key ][ 'id' ] = 'sp-wpcp-google-fonts'; } elseif ( str_contains( $id, 'sp-lc-google-fonts' ) ) { /** * Compatibility fix for Logo Carousel Pro by ShapedPlugin * * @since v5.3.8 Same reason as above. */ $google_fonts[ $key ][ 'id' ] = 'sp-lc-google-fonts'; } elseif ( str_contains( $id, 'custom_fonts_' ) ) { /** * Compatibility fix for Fruitful theme by Fruitful Code. * * @since v5.9.1 Same reason as above. */ $google_fonts[ $key ][ 'id' ] = 'custom_fonts'; } elseif ( apply_filters( 'omgf_frontend_process_convert_pro_compatibility', str_contains( $id, 'cp-google-fonts' ) ) ) { /** * Compatibility fix for Convert Pro by Brainstorm Force * * @since v5.5.4 Same reason as above, although it kind of makes sense in this case (since Convert Pro allows * to create pop-ups and people tend to get creative. I just hope the ID isn't random.) * @filter omgf_frontend_process_convert_pro_compatibility Allows people to disable this feature, in case the different * stylesheets are actually needed. */ $google_fonts[ $key ][ 'id' ] = 'cp-google-fonts'; } else { $google_fonts[ $key ][ 'id' ] = $id; } $google_fonts[ $key ][ 'link' ] = $link; /** * This is used for search/replace later on. This shouldn't be tampered with. */ $google_fonts[ $key ][ 'href' ] = $href[ 'href' ]; } return $google_fonts; } /** * Strip "-css" from the end of the stylesheet id, which WordPress adds to properly enqueued stylesheets. * * @since v5.0.1 This eases the migration from v4.6.0. * * @param mixed $handle * * @return mixed */ private function strip_css_tag( $handle ) { if ( ! str_ends_with( $handle, '-css' ) ) { return $handle; // @codeCoverageIgnore } $pos = strrpos( $handle, '-css' ); if ( $pos !== false ) { $handle = substr_replace( $handle, '', $pos, strlen( $handle ) ); } return $handle; } /** * Build a Search/Replace array for all found Google Fonts. * * @param mixed $google_fonts A processable set generated by $this->build_fonts_set(). * * @return array * @throws SodiumException * @throws SodiumException * @throws TypeError * @throws TypeError * @throws TypeError */ public function build_search_replace( $google_fonts ) { $search = []; $replace = []; foreach ( $google_fonts as $key => $stack ) { /** * Handles should be all lowercase to prevent duplication issues on some filesystems. */ $handle = strtolower( $stack[ 'id' ] ); $original_handle = $handle; /** * If stylesheet with $handle is completely marked for unload, just remove the element * to prevent it from loading. */ if ( OMGF::unloaded_stylesheets() && in_array( $handle, OMGF::unloaded_stylesheets() ) ) { $search[ $key ] = $stack[ 'link' ]; $replace[ $key ] = ''; continue; } $cache_key = OMGF::get_cache_key( $stack[ 'id' ] ); /** * $cache_key is used for caching. $handle contains the original handle. */ if ( ( OMGF::unloaded_fonts() && $cache_key ) || apply_filters( 'omgf_frontend_update_cache_key', false ) ) { $handle = $cache_key; } /** * Regular requests (in the frontend) will end here if the file exists. */ if ( ! isset( $_GET[ 'omgf_optimize' ] ) && file_exists( OMGF_UPLOAD_DIR . "/$handle/$handle.css" ) ) { $search[ $key ] = $stack[ 'href' ]; $replace[ $key ] = OMGF_UPLOAD_URL . "/$handle/$handle.css?ver=" . $this->timestamp; continue; } /** * @since v5.3.7 decode URL and special HTML chars, to make sure all params are properly processed later on. */ $href = urldecode( htmlspecialchars_decode( $stack[ 'href' ] ) ); $query = wp_parse_url( $href, PHP_URL_QUERY ); parse_str( $query, $query ); /** * If required parameters aren't set, this request is most likely invalid. Let's just remove it. */ if ( apply_filters( 'omgf_frontend_process_invalid_request', ! isset( $query[ 'family' ] ), $href ) ) { $search[ $key ] = $stack[ 'link' ]; $replace[ $key ] = ''; continue; } $optimize = new Optimize( $stack[ 'href' ], $handle, $original_handle ); /** * @var string $cached_url Absolute URL or empty string. */ $cached_url = $optimize->process(); $search[ $key ] = $stack[ 'href' ]; $replace[ $key ] = $cached_url ? $cached_url . '?ver=' . $this->timestamp : ''; } return [ 'search' => $search, 'replace' => $replace, ]; } /** * Adds a little success message to the HTML, to create a more logic user flow when manually optimizing pages. * * @param string $html Valid HTML * * @return string */ public function add_success_message( $html ) { if ( ! isset( $_GET[ 'omgf_optimize' ] ) || wp_doing_ajax() ) { return $html; } $parts = preg_split( '/()/', $html, - 1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE ); if ( empty( $parts[ 0 ] ) || empty( $parts[ 1 ] ) || empty( $parts[ 2 ] ) ) { return $html; } $message_div = '
%s
'; return $parts[ 0 ] . $parts[ 1 ] . sprintf( $message_div, __( 'Cache refreshed successful!', 'host-webfonts-local' ) ) . $parts[ 2 ]; } /** * Because all great themes come packed with extra Cumulative Layout Shifting. * * @since v5.4.3 Added compatibility for Highlight Pro; a Mesmerize based theme and Mesmerize, * the non-premium theme. * * @param string $tag * * @return string */ public function remove_mesmerize_filter( $tag ) { if ( ( wp_get_theme()->template === 'mesmerize-pro' || wp_get_theme()->template === 'highlight-pro' || wp_get_theme()->template === 'mesmerize' ) && str_contains( $tag, 'fonts.googleapis.com' ) ) { return str_replace( 'href="" data-href', 'href', $tag ); } return $tag; } }