PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / 2.0.0
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant v2.0.0
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 / Frontend / HtmlProcessor.php

HtmlProcessor.php in EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant 2.0.0, at src/Frontend/HtmlProcessor.php

230 lines 5.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * HTML manipulation backed by WordPress's native HTML API.
4 *
5 * Link discovery/rewriting goes through WP_HTML_Tag_Processor (a real
6 * tokenizer — no catastrophic backtracking). Operations the tokenizer can't do
7 * in core (reading <style> text, deleting nodes) use tightly-scoped helpers.
8 *
9 * @package EasyFonts
10 */
11
12 namespace EasyFonts\Frontend;
13
14 use WP_HTML_Tag_Processor;
15
16 defined( 'ABSPATH' ) || exit;
17
18 /**
19 * Reliable HTML read/rewrite helpers.
20 */
21 class HtmlProcessor {
22
23 /**
24 * Iterate every <link rel="stylesheet">, letting a callback rewrite the href.
25 *
26 * @param string $html HTML.
27 * @param callable $callback fn(string $href, array $attrs): ?string — return new href or null to skip.
28 * @return array{html:string,changed:bool}
29 */
30 public function rewrite_stylesheet_links( string $html, callable $callback ): array {
31 $processor = new WP_HTML_Tag_Processor( $html );
32 $changed = false;
33
34 while ( $processor->next_tag( array( 'tag_name' => 'LINK' ) ) ) {
35 $rel = (string) $processor->get_attribute( 'rel' );
36
37 if ( false === stripos( $rel, 'stylesheet' ) ) {
38 continue;
39 }
40
41 $href = $processor->get_attribute( 'href' );
42
43 if ( ! is_string( $href ) || '' === $href ) {
44 continue;
45 }
46
47 $attrs = array(
48 'id' => $processor->get_attribute( 'id' ),
49 'media' => $processor->get_attribute( 'media' ),
50 );
51
52 $new = $callback( $href, $attrs );
53
54 if ( is_string( $new ) && $new !== $href ) {
55 $processor->set_attribute( 'href', $new );
56 $changed = true;
57 }
58 }
59
60 return array(
61 'html' => $processor->get_updated_html(),
62 'changed' => $changed,
63 );
64 }
65
66 /**
67 * Collect all <link rel="stylesheet"> hrefs (read-only).
68 *
69 * @param string $html HTML.
70 * @return string[]
71 */
72 public function stylesheet_hrefs( string $html ): array {
73 $processor = new WP_HTML_Tag_Processor( $html );
74 $hrefs = array();
75
76 while ( $processor->next_tag( array( 'tag_name' => 'LINK' ) ) ) {
77 $rel = (string) $processor->get_attribute( 'rel' );
78
79 if ( false === stripos( $rel, 'stylesheet' ) ) {
80 continue;
81 }
82
83 $href = $processor->get_attribute( 'href' );
84
85 if ( is_string( $href ) && '' !== $href ) {
86 $hrefs[] = $href;
87 }
88 }
89
90 return $hrefs;
91 }
92
93 /**
94 * Physically remove specific <link> stylesheet tags by exact href.
95 *
96 * The tokenizer can't delete nodes, so we mark them inert with the
97 * tokenizer (reliable matching) and then strip the marked tags by regex.
98 *
99 * @param string $html HTML.
100 * @param string[] $hrefs Exact href values to remove.
101 * @return string
102 */
103 public function remove_stylesheet_links( string $html, array $hrefs ): string {
104 if ( empty( $hrefs ) ) {
105 return $html;
106 }
107
108 $targets = array_fill_keys( $hrefs, true );
109 $processor = new WP_HTML_Tag_Processor( $html );
110 $hit = false;
111
112 while ( $processor->next_tag( array( 'tag_name' => 'LINK' ) ) ) {
113 $rel = (string) $processor->get_attribute( 'rel' );
114
115 if ( false === stripos( $rel, 'stylesheet' ) ) {
116 continue;
117 }
118
119 $href = $processor->get_attribute( 'href' );
120
121 if ( is_string( $href ) && isset( $targets[ $href ] ) ) {
122 $processor->set_attribute( 'data-easyfonts-remove', '1' );
123 $hit = true;
124 }
125 }
126
127 if ( ! $hit ) {
128 return $html;
129 }
130
131 $html = $processor->get_updated_html();
132
133 // Drop any <link …> carrying our marker.
134 $html = preg_replace( '#<link\b[^>]*\bdata-easyfonts-remove=([\'"])1\1[^>]*/?>\s*#i', '', $html );
135
136 return null === $html ? '' : $html;
137 }
138
139 /**
140 * Process the CSS contents of each <style> block.
141 *
142 * @param string $html HTML.
143 * @param callable $callback fn(string $css): ?string — return new CSS or null to leave unchanged.
144 * @return array{html:string,changed:bool}
145 */
146 public function rewrite_style_blocks( string $html, callable $callback ): array {
147 $changed = false;
148
149 $out = preg_replace_callback(
150 '/(<style\b[^>]*>)(.*?)(<\/style>)/is',
151 static function ( $m ) use ( $callback, &$changed ) {
152 $new = $callback( $m[2] );
153
154 if ( is_string( $new ) && $new !== $m[2] ) {
155 $changed = true;
156 return $m[1] . $new . $m[3];
157 }
158
159 return $m[0];
160 },
161 $html
162 );
163
164 return array(
165 'html' => null === $out ? $html : $out,
166 'changed' => $changed,
167 );
168 }
169
170 /**
171 * Remove Google-font resource hints (preconnect / dns-prefetch / preload).
172 *
173 * @param string $html HTML.
174 * @param string[] $domains Domains to strip hints for.
175 * @return array{html:string,changed:bool}
176 */
177 public function strip_font_hints( string $html, array $domains ): array {
178 $processor = new WP_HTML_Tag_Processor( $html );
179 $remove = array();
180
181 // Identify offending hint tags by their href (read pass).
182 while ( $processor->next_tag( array( 'tag_name' => 'LINK' ) ) ) {
183 $rel = strtolower( (string) $processor->get_attribute( 'rel' ) );
184
185 if ( ! in_array( $rel, array( 'preconnect', 'dns-prefetch', 'preload' ), true ) ) {
186 continue;
187 }
188
189 $href = (string) $processor->get_attribute( 'href' );
190
191 foreach ( $domains as $domain ) {
192 if ( false !== strpos( $href, $domain ) ) {
193 // Neutralise: blank the href + rel so the hint is inert.
194 $processor->set_attribute( 'href', '' );
195 $processor->set_attribute( 'rel', 'easyfonts-removed' );
196 $remove[] = true;
197 break;
198 }
199 }
200 }
201
202 $html = $processor->get_updated_html();
203
204 // Physically drop the neutralised tags.
205 $html = preg_replace( '/<link\b[^>]*\brel=([\'"])easyfonts-removed\1[^>]*>\s*/i', '', $html );
206
207 return array(
208 'html' => null === $html ? '' : $html,
209 'changed' => ! empty( $remove ),
210 );
211 }
212
213 /**
214 * Insert a snippet immediately before </head>.
215 *
216 * @param string $html HTML.
217 * @param string $snippet Snippet.
218 * @return string
219 */
220 public function before_head_close( string $html, string $snippet ): string {
221 $pos = stripos( $html, '</head>' );
222
223 if ( false === $pos ) {
224 return $html;
225 }
226
227 return substr_replace( $html, $snippet . "\n</head>", $pos, 7 );
228 }
229 }
230