PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / 2.0.4
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant v2.0.4
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
← All changes | src/Frontend/HtmlProcessor.php +96 -18 2.0.0 → 2.0.4 View file →
@@ -20,8 +20,19 @@
20 20 */
21 21 class HtmlProcessor {
22 22
23 23 /**
24 + * Is WordPress's HTML API available? (WP 6.2+.) When it isn't, every method
25 + * here fails open — returning the HTML unchanged — so a missing core class can
26 + * never fatal inside the output buffer and white-screen the site.
27 + *
28 + * @return bool
29 + */
30 + private function api_ready(): bool {
31 + return class_exists( '\\WP_HTML_Tag_Processor' );
32 + }
33 +
34 + /**
24 35 * Iterate every <link rel="stylesheet">, letting a callback rewrite the href.
25 36 *
26 37 * @param string $html HTML.
27 38 * @param callable $callback fn(string $href, array $attrs): ?string — return new href or null to skip.
@@ -27,8 +38,12 @@
27 38 * @param callable $callback fn(string $href, array $attrs): ?string — return new href or null to skip.
28 39 * @return array{html:string,changed:bool}
29 40 */
30 41 public function rewrite_stylesheet_links( string $html, callable $callback ): array {
42 + if ( ! $this->api_ready() ) {
43 + return array( 'html' => $html, 'changed' => false );
44 + }
45 +
31 46 $processor = new WP_HTML_Tag_Processor( $html );
32 47 $changed = false;
33 48
34 49 while ( $processor->next_tag( array( 'tag_name' => 'LINK' ) ) ) {
@@ -69,8 +84,12 @@
69 84 * @param string $html HTML.
70 85 * @return string[]
71 86 */
72 87 public function stylesheet_hrefs( string $html ): array {
88 + if ( ! $this->api_ready() ) {
89 + return array();
90 + }
91 +
73 92 $processor = new WP_HTML_Tag_Processor( $html );
74 93 $hrefs = array();
75 94
76 95 while ( $processor->next_tag( array( 'tag_name' => 'LINK' ) ) ) {
@@ -104,8 +123,12 @@
104 123 if ( empty( $hrefs ) ) {
105 124 return $html;
106 125 }
107 126
127 + if ( ! $this->api_ready() ) {
128 + return $html;
129 + }
130 +
108 131 $targets = array_fill_keys( $hrefs, true );
109 132 $processor = new WP_HTML_Tag_Processor( $html );
110 133 $hit = false;
111 134
@@ -127,14 +150,15 @@
127 150 if ( ! $hit ) {
128 151 return $html;
129 152 }
130 153
131 - $html = $processor->get_updated_html();
154 + $updated = $processor->get_updated_html();
132 155
133 - // Drop any <link …> carrying our marker.
134 - $html = preg_replace( '#<link\b[^>]*\bdata-easyfonts-remove=([\'"])1\1[^>]*/?>\s*#i', '', $html );
156 + // Drop any <link …> carrying our marker. If PCRE bails (e.g. backtrack
157 + // limit), keep the tokenized HTML rather than blanking the document.
158 + $cleaned = preg_replace( '#<link\b[^>]*\bdata-easyfonts-remove=([\'"])1\1[^>]*/?>\s*#i', '', $updated );
135 159
136 - return null === $html ? '' : $html;
160 + return null === $cleaned ? $updated : $cleaned;
137 161 }
138 162
139 163 /**
140 164 * Process the CSS contents of each <style> block.
@@ -145,25 +169,69 @@
145 169 */
146 170 public function rewrite_style_blocks( string $html, callable $callback ): array {
147 171 $changed = false;
148 172
149 - $out = preg_replace_callback(
150 - '/(<style\b[^>]*>)(.*?)(<\/style>)/is',
151 - static function ( $m ) use ( $callback, &$changed ) {
152 - $new = $callback( $m[2] );
173 + // Linear <style>…</style> scanner — NOT a regex. A regex such as
174 + // `(<style\b[^>]*>)(.*?)(</style>)/s` catastrophically backtracks (and on
175 + // PCRE2+JIT can crash the process) over the megabyte-sized inline CSS that
176 + // page builders emit, which silently blanked the page. This strpos walk is
177 + // O(n) and cannot backtrack or overflow.
178 + $max = (int) apply_filters( 'easyfonts_max_style_block', 2 * 1024 * 1024 );
179 + $out = '';
180 + $offset = 0;
181 + $len = strlen( $html );
153 182
154 - if ( is_string( $new ) && $new !== $m[2] ) {
183 + while ( $offset < $len ) {
184 + $open = stripos( $html, '<style', $offset );
185 +
186 + if ( false === $open ) {
187 + $out .= substr( $html, $offset );
188 + break;
189 + }
190 +
191 + // Make sure "<style" is a real tag start (next char ends the name).
192 + $boundary = $html[ $open + 6 ] ?? '';
193 +
194 + if ( '' !== $boundary && '>' !== $boundary && '/' !== $boundary && ! ctype_space( $boundary ) ) {
195 + $out .= substr( $html, $offset, ( $open + 6 ) - $offset );
196 + $offset = $open + 6;
197 + continue;
198 + }
199 +
200 + $gt = strpos( $html, '>', $open );
201 +
202 + if ( false === $gt ) {
203 + $out .= substr( $html, $offset );
204 + break;
205 + }
206 +
207 + $close = stripos( $html, '</style>', $gt + 1 );
208 +
209 + if ( false === $close ) {
210 + $out .= substr( $html, $offset );
211 + break;
212 + }
213 +
214 + // Copy everything up to this <style>, then the opener verbatim.
215 + $out .= substr( $html, $offset, $open - $offset );
216 + $tag_open = substr( $html, $open, ( $gt - $open ) + 1 );
217 + $content = substr( $html, $gt + 1, $close - ( $gt + 1 ) );
218 +
219 + if ( strlen( $content ) <= $max ) {
220 + $new = $callback( $content );
221 +
222 + if ( is_string( $new ) && $new !== $content ) {
155 223 $changed = true;
156 - return $m[1] . $new . $m[3];
224 + $content = $new;
157 225 }
226 + }
158 227
159 - return $m[0];
160 - },
161 - $html
162 - );
228 + $out .= $tag_open . $content . '</style>';
229 + $offset = $close + 8;
230 + }
163 231
164 232 return array(
165 - 'html' => null === $out ? $html : $out,
233 + 'html' => $out,
166 234 'changed' => $changed,
167 235 );
168 236 }
169 237
@@ -174,8 +242,13 @@
174 242 * @param string[] $domains Domains to strip hints for.
175 243 * @return array{html:string,changed:bool}
176 244 */
177 245 public function strip_font_hints( string $html, array $domains ): array {
246 + if ( ! $this->api_ready() ) {
247 + return array( 'html' => $html, 'changed' => false );
248 + }
249 +
250 + $input = $html;
178 251 $processor = new WP_HTML_Tag_Processor( $html );
179 252 $remove = array();
180 253
181 254 // Identify offending hint tags by their href (read pass).
@@ -200,13 +273,18 @@
200 273 }
201 274
202 275 $html = $processor->get_updated_html();
203 276
204 - // Physically drop the neutralised tags.
205 - $html = preg_replace( '/<link\b[^>]*\brel=([\'"])easyfonts-removed\1[^>]*>\s*/i', '', $html );
277 + // Physically drop the neutralised tags. If PCRE bails, fall back to the
278 + // input HTML rather than returning '' (which would blank the page).
279 + $cleaned = preg_replace( '/<link\b[^>]*\brel=([\'"])easyfonts-removed\1[^>]*>\s*/i', '', $html );
206 280
281 + if ( null === $cleaned ) {
282 + return array( 'html' => $input, 'changed' => false );
283 + }
284 +
207 285 return array(
208 - 'html' => null === $html ? '' : $html,
286 + 'html' => $cleaned,
209 287 'changed' => ! empty( $remove ),
210 288 );
211 289 }
212 290