PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.14.0
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.14.0
2.14.0 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 All 81 releases
ablocks / addons / cookie-consent / embeds.php

embeds.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder 2.14.0, at addons/cookie-consent/embeds.php

295 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace ABlocksCookieConsent;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * The half of the page that is not a script.
10 *
11 * Gating every `<script>` on a site and then leaving a YouTube embed in the
12 * page is not a partial job, it is a broken one: the iframe sets its cookies
13 * the moment the document parses, before the banner has painted, and it does
14 * it without running a line of the site's own JavaScript. The same is true of
15 * a tracking pixel, which is an `<img>` and therefore also invisible to
16 * everything the script layers do.
17 *
18 * Two element types, handled differently because what they cost is different.
19 * An iframe is content the visitor came for, so blocking it has to leave
20 * something in its place and a way to get it back. A pixel is not content at
21 * all: it is removed and nothing is said, because there is nothing to say.
22 *
23 * The list below is an allowlist and that is the whole safety argument. A
24 * consent plugin that guesses at iframes will eventually blank a payment form
25 * or a captcha, and a checkout that does not load is a worse failure than an
26 * ungated embed. Nothing matches unless it is named here.
27 */
28 class Embeds {
29
30 /**
31 * Embeds worth blocking, and what to call them when they are.
32 *
33 * `label` is shown to the visitor in the placeholder, so it is the name
34 * they would recognise rather than the domain it happens to load from.
35 */
36 public static function default_rules() {
37 return [
38 [
39 'id' => 'youtube',
40 'label' => 'YouTube',
41 'category' => 'marketing',
42 // `youtube-nocookie` is included deliberately. It withholds
43 // cookies until playback, not for ever, so it still needs
44 // consent — the name is a description of the first second.
45 'src' => '(youtube\.com|youtube-nocookie\.com)/embed/',
46 'enabled' => true,
47 ],
48 [
49 'id' => 'vimeo',
50 'label' => 'Vimeo',
51 'category' => 'marketing',
52 'src' => 'player\.vimeo\.com/video/',
53 'enabled' => true,
54 ],
55 [
56 'id' => 'google-maps',
57 'label' => 'Google Maps',
58 'category' => 'functional',
59 'src' => '(google\.[a-z.]+/maps/embed|maps\.google\.[a-z.]+/maps)',
60 'enabled' => true,
61 ],
62 [
63 'id' => 'soundcloud',
64 'label' => 'SoundCloud',
65 'category' => 'marketing',
66 'src' => 'w\.soundcloud\.com/player',
67 'enabled' => true,
68 ],
69 [
70 'id' => 'spotify',
71 'label' => 'Spotify',
72 'category' => 'marketing',
73 'src' => 'open\.spotify\.com/embed',
74 'enabled' => true,
75 ],
76 [
77 'id' => 'x-twitter',
78 'label' => 'X (Twitter)',
79 'category' => 'marketing',
80 'src' => '(platform\.twitter\.com|syndication\.twitter\.com)',
81 'enabled' => true,
82 ],
83 [
84 'id' => 'facebook-embed',
85 'label' => 'Facebook',
86 'category' => 'marketing',
87 'src' => 'facebook\.com/plugins/',
88 'enabled' => true,
89 ],
90 [
91 'id' => 'instagram-embed',
92 'label' => 'Instagram',
93 'category' => 'marketing',
94 'src' => 'instagram\.com/(p|reel|tv)/[^/]+/embed',
95 'enabled' => true,
96 ],
97 [
98 'id' => 'tiktok-embed',
99 'label' => 'TikTok',
100 'category' => 'marketing',
101 'src' => 'tiktok\.com/(embed|player)',
102 'enabled' => true,
103 ],
104 [
105 // The `<noscript>` half of the GTM snippet: a 0×0 iframe that
106 // exists to fire without JavaScript. `beacon` because there is
107 // nothing to offer to load — a card standing where a hidden
108 // pixel used to be would announce a loss the visitor never had.
109 'id' => 'gtm-noscript',
110 'label' => 'Google Tag Manager',
111 'category' => 'analytics',
112 'src' => 'googletagmanager\.com/ns\.html',
113 'enabled' => true,
114 'beacon' => true,
115 ],
116 ];
117 }
118
119 /**
120 * Beacons: an `<img>` whose only purpose is the request it makes.
121 *
122 * These get no placeholder. A 1×1 image is not content the visitor is
123 * missing, and drawing a consent card where one used to be would be
124 * inventing a loss to apologise for.
125 *
126 * Note these are the `<noscript>` halves of the same snippets the Tags
127 * screen refuses to print — the difference being that here they are
128 * already in the page and can be taken out.
129 */
130 public static function default_pixel_rules() {
131 return [
132 [
133 'id' => 'meta-pixel-img',
134 'label' => 'Meta Pixel',
135 'category' => 'marketing',
136 'src' => 'facebook\.com/tr',
137 'enabled' => true,
138 ],
139 [
140 'id' => 'google-ads-img',
141 'label' => 'Google Ads',
142 'category' => 'marketing',
143 'src' => '(googleads\.g\.doubleclick\.net|google\.[a-z.]+/ads/ga-audiences|google\.[a-z.]+/pagead)',
144 'enabled' => true,
145 ],
146 [
147 'id' => 'linkedin-img',
148 'label' => 'LinkedIn',
149 'category' => 'marketing',
150 'src' => 'px\.ads\.linkedin\.com',
151 'enabled' => true,
152 ],
153 [
154 'id' => 'x-ads-img',
155 'label' => 'X (Twitter)',
156 'category' => 'marketing',
157 'src' => 't\.co/i/adsct',
158 'enabled' => true,
159 ],
160 ];
161 }
162
163 /**
164 * The rules actually in force, with the site's own enable/category edits
165 * folded over the shipped patterns.
166 *
167 * Patterns are not editable and no rule can be added: unlike the script
168 * rules, a wrong match here blanks something the visitor can see.
169 *
170 * @param string $kind 'embed' or 'pixel'.
171 * @return array
172 */
173 public static function active_rules( $kind = 'embed' ) {
174 $shipped = 'pixel' === $kind ? self::default_pixel_rules() : self::default_rules();
175 $saved = Helper::get( 'pixel' === $kind ? 'pixel_rules' : 'embed_rules', [] );
176 $saved = is_array( $saved ) ? $saved : [];
177
178 $edits = [];
179 foreach ( $saved as $rule ) {
180 if ( ! empty( $rule['id'] ) ) {
181 $edits[ $rule['id'] ] = $rule;
182 }
183 }
184
185 $active = [];
186 foreach ( $shipped as $rule ) {
187 $edit = isset( $edits[ $rule['id'] ] ) ? $edits[ $rule['id'] ] : [];
188
189 if ( isset( $edit['enabled'] ) && ! $edit['enabled'] ) {
190 continue;
191 }
192 if ( ! empty( $edit['category'] ) ) {
193 $rule['category'] = (string) $edit['category'];
194 }
195
196 // A rule pointing at a category the site has removed would gate
197 // against a choice the visitor is never offered, and the embed
198 // would never come back.
199 if ( ! Helper::category_is_active( $rule['category'] ) ) {
200 continue;
201 }
202
203 $active[] = $rule;
204 }
205
206 return apply_filters( 'ablocks/cookie_consent/embed_rules', $active, $kind );
207 }
208
209 /**
210 * Which rule, if any, claims this URL.
211 *
212 * @param string $src Element source.
213 * @param string $kind 'embed' or 'pixel'.
214 * @return array|null The matching rule.
215 */
216 public static function match( $src, $kind = 'embed' ) {
217 $src = (string) $src;
218 if ( '' === trim( $src ) ) {
219 return null;
220 }
221
222 foreach ( self::active_rules( $kind ) as $rule ) {
223 // Silenced for the same reason the script matcher silences: a
224 // pattern can be pointed at a different category from the admin
225 // screen, and a bad edit must not warn on every page view.
226 $hit = @preg_match( '#' . str_replace( '#', '\#', $rule['src'] ) . '#i', $src ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
227 if ( 1 === $hit ) {
228 return $rule;
229 }
230 }
231
232 return null;
233 }
234
235 /**
236 * The card that stands where the embed was.
237 *
238 * The original element is carried across in an attribute rather than left
239 * in the document with its `src` moved aside. Two reasons, and both are
240 * about being sure: markup inside an attribute is text and cannot make a
241 * request under any circumstances, and putting it back is an exact
242 * restoration rather than a reconstruction of what the author wrote.
243 *
244 * The card replaces the element rather than wrapping it, because a wrapper
245 * is a new node in the middle of whatever the theme's responsive-embed CSS
246 * was selecting, and blanking a video is a smaller failure than moving
247 * every video on the site half a column to the left.
248 *
249 * @param string $original The whole original element.
250 * @param array $rule The rule that matched.
251 * @return string
252 */
253 public static function placeholder( $original, $rule ) {
254 $label = isset( $rule['label'] ) ? $rule['label'] : __( 'external', 'ablocks' );
255
256 return sprintf(
257 '<div class="ablocks-consent-embed" data-ablocks-consent="%1$s" data-ablocks-embed="%2$s" data-ablocks-embed-html="%3$s">' .
258 '<div class="ablocks-consent-embed__inner">' .
259 '<p class="ablocks-consent-embed__title">%4$s</p>' .
260 '<p class="ablocks-consent-embed__text">%5$s</p>' .
261 '<span class="ablocks-consent-embed__actions">' .
262 '<button type="button" class="ablocks-consent-embed__btn" data-ablocks-consent-action="load-embed">%6$s</button>' .
263 '<button type="button" class="ablocks-consent-embed__link" data-ablocks-consent-action="allow-embeds">%7$s</button>' .
264 '</span>' .
265 '</div>' .
266 '</div>',
267 esc_attr( $rule['category'] ),
268 esc_attr( $label ),
269 esc_attr( $original ),
270 esc_html(
271 sprintf(
272 /* translators: %s: name of the embed provider, e.g. YouTube. */
273 __( '%s content is blocked', 'ablocks' ),
274 $label
275 )
276 ),
277 esc_html(
278 sprintf(
279 /* translators: %s: name of the embed provider, e.g. YouTube. */
280 __( 'Loading this would let %s set cookies on your device. Nothing has been sent to them yet.', 'ablocks' ),
281 $label
282 )
283 ),
284 esc_html__( 'Load this once', 'ablocks' ),
285 esc_html(
286 sprintf(
287 /* translators: %s: name of the embed provider, e.g. YouTube. */
288 __( 'Always allow %s', 'ablocks' ),
289 $label
290 )
291 )
292 );
293 }
294 }
295