PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.13.1
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.13.1
2.13.0 2.13.1 2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 All 80 releases
← All changes | addons/cookie-consent/buffer.php +202 -11 2.12.02.13.1 View file →
@@ -44,11 +44,17 @@
44 44 'module',
45 45 ];
46 46
47 47 public static function init() {
48 - if ( ! Helper::is_gating_active() || ! Helper::get( 'buffer_gating', true ) ) {
48 + if ( ! Helper::is_gating_active() ) {
49 49 return;
50 50 }
51 + // Two independent reasons to buffer. A site can hold scripts back and
52 + // leave its videos alone, or the reverse, so neither switch may speak
53 + // for the other.
54 + if ( ! Helper::get( 'buffer_gating', true ) && ! Helper::get( 'embed_gating', true ) ) {
55 + return;
56 + }
51 57
52 58 $self = new self();
53 59 // Started as early as a theme's output can be, so this buffer is the
54 60 // outermost one and its callback therefore runs on the finished
@@ -71,9 +77,11 @@
71 77 if ( ! is_string( $html ) || '' === $html ) {
72 78 return $html;
73 79 }
74 80 // Cheap bail-outs first: this callback runs on every page.
75 - if ( false === stripos( $html, '<script' ) ) {
81 + if ( false === stripos( $html, '<script' )
82 + && false === stripos( $html, '<iframe' )
83 + && false === stripos( $html, '<img' ) ) {
76 84 return $html;
77 85 }
78 86 if ( ! $this->is_html_response() ) {
79 87 return $html;
@@ -80,19 +88,202 @@
80 88 }
81 89
82 90 $dry_run = (bool) Helper::get( 'dry_run', false );
83 91
84 - $filtered = preg_replace_callback(
85 - '#<script\b([^>]*)>(.*?)</script\s*>#is',
86 - function ( $match ) use ( $dry_run ) {
87 - return $this->process( $match, $dry_run );
88 - },
89 - $html
92 + if ( Helper::get( 'buffer_gating', true ) ) {
93 + $filtered = preg_replace_callback(
94 + '#<script\b([^>]*)>(.*?)</script\s*>#is',
95 + function ( $match ) use ( $dry_run ) {
96 + return $this->process( $match, $dry_run );
97 + },
98 + $html
99 + );
100 +
101 + // A backtrack limit or a catastrophic pattern returns null.
102 + // Serving the original page ungated is bad; serving an empty page
103 + // is worse.
104 + $html = null === $filtered ? $html : $filtered;
105 + }
106 +
107 + if ( Helper::get( 'embed_gating', true ) ) {
108 + $html = $this->gate_elements( $html, $dry_run );
109 + }
110 +
111 + return $html;
112 + }
113 +
114 + /**
115 + * The iframe and pixel pass.
116 + *
117 + * Runs on everything *between* the script elements rather than on the whole
118 + * document. An inline script is perfectly entitled to contain the text
119 + * `<img src="…facebook.com/tr…">` — in a template string, in a JSON blob,
120 + * in an example — and rewriting it there would not gate a request, it would
121 + * corrupt the script. Splitting on script blocks and skipping the captured
122 + * halves costs one more pass and removes the whole class of problem.
123 + *
124 + * @param string $html The document.
125 + * @param bool $dry_run Report instead of rewrite.
126 + * @return string
127 + */
128 + private function gate_elements( $html, $dry_run ) {
129 + $has_iframe = false !== stripos( $html, '<iframe' );
130 + $has_img = false !== stripos( $html, '<img' );
131 +
132 + if ( ! $has_iframe && ! $has_img ) {
133 + return $html;
134 + }
135 +
136 + $parts = preg_split(
137 + '#(<script\b[^>]*>.*?</script\s*>)#is',
138 + $html,
139 + -1,
140 + PREG_SPLIT_DELIM_CAPTURE
90 141 );
91 142
92 - // A backtrack limit or a catastrophic pattern returns null. Serving the
93 - // original page ungated is bad; serving an empty page is worse.
94 - return null === $filtered ? $html : $filtered;
143 + if ( ! is_array( $parts ) ) {
144 + return $html;
145 + }
146 +
147 + foreach ( $parts as $index => $part ) {
148 + // Odd indices are the captured script blocks themselves.
149 + if ( 1 === $index % 2 || '' === $part ) {
150 + continue;
151 + }
152 +
153 + if ( $has_iframe ) {
154 + $done = preg_replace_callback(
155 + '#<iframe\b([^>]*)>(.*?)</iframe\s*>#is',
156 + function ( $match ) use ( $dry_run ) {
157 + return $this->process_embed( $match, $dry_run );
158 + },
159 + $part
160 + );
161 + $part = null === $done ? $part : $done;
162 + }
163 +
164 + if ( $has_img ) {
165 + $done = preg_replace_callback(
166 + '#<img\b([^>]*?)/?>#is',
167 + function ( $match ) use ( $dry_run ) {
168 + return $this->process_pixel( $match, $dry_run );
169 + },
170 + $part
171 + );
172 + $part = null === $done ? $part : $done;
173 + }
174 +
175 + $parts[ $index ] = $part;
176 + }
177 +
178 + return implode( '', $parts );
179 + }
180 +
181 + /**
182 + * One iframe: leave it, or stand a consent card where it was.
183 + *
184 + * @param array $match Regex match: 0 whole, 1 attributes, 2 contents.
185 + * @param bool $dry_run Report instead of rewrite.
186 + * @return string
187 + */
188 + private function process_embed( $match, $dry_run ) {
189 + $whole = $match[0];
190 + $attrs = $match[1];
191 +
192 + if ( false !== stripos( $attrs, 'data-ablocks-consent' ) ) {
193 + return $whole;
194 + }
195 +
196 + $src = $this->attribute( $attrs, 'src' );
197 + if ( '' === (string) $src ) {
198 + return $whole;
199 + }
200 +
201 + $rule = Embeds::match( $src, 'embed' );
202 +
203 + if ( ! $rule ) {
204 + // An unrecognised third-party iframe is the same kind of gap as an
205 + // unrecognised third-party script, and worth the same report.
206 + if ( Gating::is_third_party( $src ) ) {
207 + Report::add( $src, '', 'iframe' );
208 + }
209 + return $whole;
210 + }
211 +
212 + if ( $dry_run ) {
213 + Report::add( $src, $rule['category'], 'iframe' );
214 + return $whole;
215 + }
216 +
217 + // An invisible beacon is stripped the way a pixel is. It occupies no
218 + // space, so there is no gap to explain and nothing to offer to load.
219 + if ( ! empty( $rule['beacon'] ) ) {
220 + return $this->strip_source( $whole, 'iframe', $rule['category'] );
221 + }
222 +
223 + return Embeds::placeholder( $whole, $rule );
224 + }
225 +
226 + /**
227 + * One image: leave it, or take its source away.
228 + *
229 + * No placeholder and no announcement. A tracking pixel is not content the
230 + * visitor is missing, and drawing a consent card where a 1×1 beacon used to
231 + * be would invent a loss to apologise for.
232 + *
233 + * @param array $match Regex match: 0 whole, 1 attributes.
234 + * @param bool $dry_run Report instead of rewrite.
235 + * @return string
236 + */
237 + private function process_pixel( $match, $dry_run ) {
238 + $whole = $match[0];
239 + $attrs = $match[1];
240 +
241 + if ( false !== stripos( $attrs, 'data-ablocks-consent' ) ) {
242 + return $whole;
243 + }
244 +
245 + $src = $this->attribute( $attrs, 'src' );
246 + if ( '' === (string) $src ) {
247 + return $whole;
248 + }
249 +
250 + $rule = Embeds::match( $src, 'pixel' );
251 + if ( ! $rule ) {
252 + return $whole;
253 + }
254 +
255 + if ( $dry_run ) {
256 + Report::add( $src, $rule['category'], 'pixel' );
257 + return $whole;
258 + }
259 +
260 + return $this->strip_source( $whole, 'img', $rule['category'] );
261 + }
262 +
263 + /**
264 + * Move an element's `src` aside and label it with its category.
265 + *
266 + * Renaming the attribute is the whole mechanism: a browser does not fetch
267 + * `data-ablocks-src`, and the element stays exactly where the author put
268 + * it, keeping whatever size and styling it had.
269 + *
270 + * @param string $tag The whole element.
271 + * @param string $name Tag name, `img` or `iframe`.
272 + * @param string $category Category slug.
273 + * @return string
274 + */
275 + private function strip_source( $tag, $name, $category ) {
276 + $rewritten = preg_replace( '/\ssrc=/i', ' data-ablocks-src=', $tag, 1 );
277 +
278 + // `\b`, not `\s`: the same trap that made `ScriptGate` silently skip a
279 + // `<script>` whose only attribute was its type.
280 + return preg_replace(
281 + '/^<' . $name . '\b/i',
282 + sprintf( '<%s data-ablocks-consent="%s" ', $name, esc_attr( $category ) ),
283 + $rewritten,
284 + 1
285 + );
95 286 }
96 287
97 288 /**
98 289 * Decide what to do with one script element.