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 / frontend.php

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

533 lines 19.7 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 banner, and the script that drives it.
10 *
11 * The markup ships on every page whether or not the visitor has already
12 * decided, and it ships hidden. That is not laziness — it is the only shape
13 * that survives a page cache. A cache stores one document per URL; if PHP
14 * decided whether to include the banner, the first visitor through the cache
15 * would decide for everyone behind them.
16 *
17 * So the server renders one document and the client decides what to do with it.
18 */
19 class Frontend {
20
21 public static function init() {
22 $self = new self();
23 add_action( 'wp_enqueue_scripts', [ $self, 'enqueue' ] );
24 add_action( 'wp_footer', [ $self, 'render' ], 20 );
25 add_shortcode( 'ablocks_cookie_preferences', [ $self, 'preferences_shortcode' ] );
26 }
27
28 public function enqueue() {
29 if ( ! Helper::should_render_banner() ) {
30 return;
31 }
32
33 $asset_file = ABLOCKS_ASSETS_PATH . 'build/cookie-consent.asset.php';
34 $asset = file_exists( $asset_file ) ? require $asset_file : [
35 'dependencies' => [],
36 'version' => ABLOCKS_VERSION,
37 ];
38
39 wp_enqueue_style(
40 'ablocks-cookie-consent',
41 ABLOCKS_ASSETS_URL . 'build/cookie-consent.css',
42 [],
43 $asset['version']
44 );
45
46 wp_enqueue_script(
47 'ablocks-cookie-consent',
48 ABLOCKS_ASSETS_URL . 'build/cookie-consent.js',
49 [],
50 $asset['version'],
51 true
52 );
53
54 // `wp_localize_script` casts every scalar to a string, which would turn
55 // the policy version into "1" and break the strict comparison the
56 // Consent Mode reader in <head> makes against the integer. An inline
57 // JSON assignment keeps the types.
58 wp_add_inline_script(
59 'ablocks-cookie-consent',
60 'window.ABlocksConsentConfig = ' . wp_json_encode( $this->script_config() ) . ';',
61 'before'
62 );
63 }
64
65 /**
66 * Everything the client needs to decide what to show and what to release.
67 *
68 * @return array
69 */
70 private function script_config() {
71 $categories = [];
72 foreach ( Helper::active_categories() as $category ) {
73 $categories[] = [
74 'slug' => $category['slug'],
75 'locked' => ! empty( $category['locked'] ),
76 ];
77 }
78
79 return [
80 'cookie' => Helper::get( 'cookie_name', 'ablocks_consent' ),
81 'cookiePrev' => Helper::get( 'cookie_name_previous', '' ),
82 'days' => (int) Helper::get( 'cookie_days', 365 ),
83 'version' => (int) Helper::get( 'policy_version', 1 ),
84 'reconsentDays' => (int) Helper::get( 'reconsent_days', 0 ),
85 'mode' => Helper::get( 'mode', 'optin' ),
86 'categories' => $categories,
87 'consentMode' => (bool) Helper::get( 'consent_mode', true ),
88 'consentModeAds' => (bool) Helper::get( 'consent_mode_ads', true ),
89 'signals' => ConsentMode::signal_map(),
90 'record' => (bool) Helper::get( 'record_enabled', true ),
91 'endpoint' => rest_url( ABLOCKS_REST_NAMESPACE . '/consent' ),
92 'delay' => (int) Helper::banner( 'delay', 0 ),
93 'reopen' => (bool) Helper::banner( 'reopen', true ),
94 'canClose' => (bool) Helper::banner( 'show_banner_close', false ),
95 'closeAction' => Helper::banner( 'close_behaviour', 'dismiss' ),
96 'dismissDays' => (int) Helper::banner( 'dismiss_days', 0 ),
97 ];
98 }
99
100 /**
101 * The CSS custom properties the stylesheet reads.
102 *
103 * Emitted as variables rather than as rules so the whole banner can be
104 * restyled from the settings screen without the stylesheet knowing anything
105 * about a particular site's palette, and so a theme can override any one of
106 * them without fighting specificity.
107 *
108 * @return string
109 */
110 private function style_vars() {
111 $banner = Helper::get( 'banner', [] );
112 $vars = [
113 '--ablocks-cc-bg' => $banner['bg'],
114 '--ablocks-cc-text' => $banner['text'],
115 '--ablocks-cc-muted' => $banner['muted'],
116 '--ablocks-cc-border' => $banner['border'],
117 '--ablocks-cc-accent' => $banner['accent'],
118 '--ablocks-cc-accent-text' => $banner['accent_text'],
119 '--ablocks-cc-secondary-bg' => $banner['secondary_bg'],
120 '--ablocks-cc-secondary-text' => $banner['secondary_text'],
121 '--ablocks-cc-save-bg' => $banner['save_bg'],
122 '--ablocks-cc-save-text' => $banner['save_text'],
123 '--ablocks-cc-locked-bg' => $banner['locked_bg'],
124 '--ablocks-cc-locked-text' => $banner['locked_text'],
125 '--ablocks-cc-radius' => (int) $banner['radius'] . 'px',
126 '--ablocks-cc-max-width' => (int) $banner['max_width'] . 'px',
127 '--ablocks-cc-prefs-max-width' => (int) $banner['prefs_max_width'] . 'px',
128 ];
129
130 $out = '';
131 foreach ( $vars as $name => $value ) {
132 $out .= sprintf( '%s:%s;', $name, esc_attr( $value ) );
133 }
134 return $out;
135 }
136
137 public function render() {
138 if ( ! Helper::should_render_banner() ) {
139 return;
140 }
141
142 $banner = Helper::get( 'banner', [] );
143 $modal = 'modal' === $banner['prefs_layout'];
144 $classes = [
145 'ablocks-consent',
146 'ablocks-consent--' . sanitize_html_class( $banner['layout'] ),
147 'ablocks-consent--' . sanitize_html_class( $banner['position'] ),
148 ];
149 if ( ! empty( $banner['shadow'] ) ) {
150 $classes[] = 'has-shadow';
151 }
152 ?>
153 <div
154 id="ablocks-consent"
155 class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>"
156 style="<?php echo esc_attr( $this->style_vars() ); ?>"
157 role="dialog"
158 aria-modal="<?php echo ! empty( $banner['overlay'] ) ? 'true' : 'false'; ?>"
159 aria-labelledby="ablocks-consent-title"
160 aria-describedby="ablocks-consent-message"
161 data-ablocks-consent-root="1"
162 data-ablocks-consent-prefs="<?php echo $modal ? 'modal' : 'inline'; ?>"
163 hidden
164 >
165 <?php if ( ! empty( $banner['overlay'] ) ) : ?>
166 <div class="ablocks-consent__overlay"></div>
167 <?php endif; ?>
168
169 <div class="ablocks-consent__panel<?php echo ! empty( $banner['show_banner_close'] ) ? ' has-close' : ''; ?>">
170 <?php
171 /*
172 * Closing is not an answer, and it is not treated as one: the
173 * client either stores nothing at all or stores a refusal,
174 * depending on `close_behaviour`. Either way no category is
175 * released. It exists so that a visitor who will not accept —
176 * on a banner whose owner has turned Reject off — has something
177 * to do other than leave.
178 */
179 if ( ! empty( $banner['show_banner_close'] ) ) :
180 ?>
181 <button
182 type="button"
183 class="ablocks-consent__close ablocks-consent__close--banner"
184 data-ablocks-consent-action="close-banner"
185 aria-label="<?php esc_attr_e( 'Close without accepting', 'ablocks' ); ?>"
186 >
187 <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
188 <path d="M6 6l12 12M18 6L6 18" />
189 </svg>
190 </button>
191 <?php endif; ?>
192
193 <div class="ablocks-consent__main">
194 <div class="ablocks-consent__body">
195 <?php if ( ! empty( $banner['title'] ) ) : ?>
196 <p class="ablocks-consent__title" id="ablocks-consent-title">
197 <?php echo esc_html( $banner['title'] ); ?>
198 </p>
199 <?php endif; ?>
200 <div class="ablocks-consent__message" id="ablocks-consent-message">
201 <?php echo wp_kses_post( wpautop( $banner['message'] ) ); ?>
202 <?php $this->render_policy_links( $banner ); ?>
203 </div>
204 </div>
205
206 <?php // Reject sits before Accept and carries the same weight of styling: a refusal that is harder to reach than a grant is not a free choice. ?>
207 <div class="ablocks-consent__actions">
208 <?php if ( ! empty( $banner['show_settings'] ) ) : ?>
209 <button type="button" class="ablocks-consent__btn ablocks-consent__btn--<?php echo esc_attr( 'solid' === $banner['settings_style'] ? 'secondary' : $banner['settings_style'] ); ?>" data-ablocks-consent-action="settings">
210 <?php echo esc_html( $banner['settings_label'] ); ?>
211 </button>
212 <?php endif; ?>
213 <?php if ( ! empty( $banner['show_reject'] ) ) : ?>
214 <button type="button" class="ablocks-consent__btn ablocks-consent__btn--secondary" data-ablocks-consent-action="reject">
215 <?php echo esc_html( $banner['reject_label'] ); ?>
216 </button>
217 <?php endif; ?>
218 <button type="button" class="ablocks-consent__btn ablocks-consent__btn--primary" data-ablocks-consent-action="accept">
219 <?php echo esc_html( $banner['accept_label'] ); ?>
220 </button>
221 </div>
222 </div>
223
224 <?php
225 // Inline preferences live in the banner's own panel and replace
226 // its contents. Modal preferences are printed once, below, as a
227 // dialog of their own — see render_prefs().
228 if ( ! $modal ) :
229 ?>
230 <div class="ablocks-consent__prefs" hidden>
231 <?php $this->render_prefs( $banner ); ?>
232 </div>
233 <?php endif; ?>
234 </div>
235 </div>
236
237 <?php if ( $modal ) : ?>
238 <?php
239 /*
240 * A separate root rather than a second panel inside the banner.
241 *
242 * The banner is positioned by the site owner — a bar across the
243 * bottom, a box in a corner — and the category list has to be
244 * readable wherever that is. Nesting it would inherit that
245 * position and that width; lifting it out lets a 380px corner
246 * notice open into a centred dialog, and lets the notice stay
247 * visible behind it, which is what a visitor expects from
248 * something called "Customize".
249 *
250 * It borrows the banner's own classes so every rule in the
251 * stylesheet applies unchanged; only the width and the stacking
252 * order are its own.
253 */
254 $modal_classes = [
255 'ablocks-consent',
256 'ablocks-consent-modal',
257 'ablocks-consent--center',
258 ];
259 if ( ! empty( $banner['shadow'] ) ) {
260 $modal_classes[] = 'has-shadow';
261 }
262 ?>
263 <div
264 id="ablocks-consent-prefs"
265 class="<?php echo esc_attr( implode( ' ', $modal_classes ) ); ?>"
266 style="<?php echo esc_attr( $this->style_vars() ); ?>"
267 role="dialog"
268 aria-modal="true"
269 aria-labelledby="ablocks-consent-prefs-title"
270 hidden
271 >
272 <?php // Clicking away closes the dialog and returns to the banner. It records nothing: leaving a dialog is not an answer. ?>
273 <div class="ablocks-consent__overlay" data-ablocks-consent-action="close-prefs"></div>
274 <div class="ablocks-consent__panel">
275 <div class="ablocks-consent__prefs">
276 <?php $this->render_prefs( $banner ); ?>
277 </div>
278 </div>
279 </div>
280 <?php endif; ?>
281
282 <?php if ( ! empty( $banner['reopen'] ) ) : ?>
283 <?php // Withdrawal has to be as easy as granting, so the way back in is always on screen once a decision exists. ?>
284 <button
285 type="button"
286 class="ablocks-consent-reopen ablocks-consent-reopen--<?php echo esc_attr( sanitize_html_class( $banner['reopen_position'] ) ); ?>"
287 style="<?php echo esc_attr( $this->style_vars() ); ?>"
288 data-ablocks-consent-action="open"
289 hidden
290 >
291 <span class="ablocks-consent-reopen__icon" aria-hidden="true">
292 <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
293 <path d="M21.5 12a9.5 9.5 0 1 1-6.2-8.9 3.2 3.2 0 0 0 3.4 4.3 3.2 3.2 0 0 0 2.6 3.9c.1.2.2.5.2.7Z" />
294 <circle cx="9" cy="10" r="1" fill="currentColor" stroke="none" />
295 <circle cx="14.5" cy="15" r="1" fill="currentColor" stroke="none" />
296 <circle cx="8.5" cy="15.5" r="1" fill="currentColor" stroke="none" />
297 </svg>
298 </span>
299 <span class="ablocks-consent-reopen__label"><?php echo esc_html( $banner['reopen_label'] ); ?></span>
300 </button>
301 <?php endif; ?>
302 <?php
303 }
304
305 /**
306 * The privacy and cookie policy links.
307 *
308 * Two of them because they are two documents: a cookie policy lists what is
309 * stored and for how long, a privacy policy says what happens to it. A
310 * banner that links only to the second is missing the one a visitor
311 * deciding about cookies actually wants.
312 *
313 * @param array $banner Banner settings.
314 */
315 private function render_policy_links( $banner ) {
316 $links = [
317 [ $banner['policy_url'], $banner['policy_label'] ],
318 [ $banner['cookie_policy_url'], $banner['cookie_policy_label'] ],
319 ];
320
321 $out = '';
322 foreach ( $links as $link ) {
323 list( $url, $label ) = $link;
324 if ( empty( $url ) || '' === trim( (string) $label ) ) {
325 continue;
326 }
327 $out .= sprintf(
328 '<a class="ablocks-consent__policy" href="%s">%s</a>',
329 esc_url( $url ),
330 esc_html( $label )
331 );
332 }
333
334 if ( '' === $out ) {
335 return;
336 }
337
338 // Wrapped so two links get something between them. Adjacent inline
339 // anchors with no separator run together into one word.
340 echo '<span class="ablocks-consent__policies">' . $out . '</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built from esc_url/esc_html above.
341 }
342
343 /**
344 * The preferences body: heading, categories, and the buttons under them.
345 *
346 * Written once and called from either placement, so the inline panel and
347 * the modal cannot drift apart. Only its container differs between the two.
348 *
349 * @param array $banner Banner settings.
350 */
351 private function render_prefs( $banner ) {
352 $accordion = ! empty( $banner['prefs_accordion'] );
353 $open_first = $accordion && ! empty( $banner['prefs_open_first'] );
354 $show_table = ! empty( $banner['show_cookie_table'] );
355 $checkbox = 'checkbox' === $banner['switch_style'];
356 $badge = 'badge' === $banner['locked_style'];
357 $index = 0;
358 ?>
359 <div class="ablocks-consent__prefs-head">
360 <p class="ablocks-consent__prefs-title" id="ablocks-consent-prefs-title">
361 <?php echo esc_html( $banner['prefs_title'] ); ?>
362 </p>
363 <?php if ( ! empty( $banner['show_close'] ) ) : ?>
364 <?php // Closes the preferences only. The banner stays, and no decision is written — there is nothing here that should be dismissible into a "yes". ?>
365 <button
366 type="button"
367 class="ablocks-consent__close"
368 data-ablocks-consent-action="close-prefs"
369 aria-label="<?php esc_attr_e( 'Close', 'ablocks' ); ?>"
370 >
371 <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
372 <path d="M6 6l12 12M18 6L6 18" />
373 </svg>
374 </button>
375 <?php endif; ?>
376 </div>
377
378 <div class="ablocks-consent__prefs-intro">
379 <?php echo wp_kses_post( wpautop( $banner['prefs_intro'] ) ); ?>
380 <?php $this->render_policy_links( $banner ); ?>
381 </div>
382
383 <ul class="ablocks-consent__cats">
384 <?php
385 foreach ( Helper::active_categories() as $category ) :
386 $index++;
387 $panel_id = 'ablocks-consent-cat-' . sanitize_html_class( $category['slug'] );
388 $is_open = ! $accordion || ( 1 === $index && $open_first );
389 $cookies = $show_table && ! empty( $category['cookies'] ) ? (array) $category['cookies'] : [];
390 $has_body = ! empty( $category['description'] ) || $cookies;
391 ?>
392 <li class="ablocks-consent__cat<?php echo $accordion && $has_body ? ' is-collapsible' : ''; ?>">
393 <div class="ablocks-consent__cat-head">
394 <?php
395 /*
396 * A button with aria-expanded rather than
397 * <details>/<summary>: the disclosure sits in the same
398 * row as the category's own switch, and a summary with
399 * an interactive control inside it is both invalid and
400 * a known screen-reader trap.
401 */
402 if ( $accordion && $has_body ) :
403 ?>
404 <button
405 type="button"
406 class="ablocks-consent__cat-toggle"
407 data-ablocks-consent-action="toggle-category"
408 aria-expanded="<?php echo $is_open ? 'true' : 'false'; ?>"
409 aria-controls="<?php echo esc_attr( $panel_id ); ?>"
410 >
411 <span class="ablocks-consent__cat-arrow" aria-hidden="true">
412 <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
413 <path d="M9 6l6 6-6 6" />
414 </svg>
415 </span>
416 <span class="ablocks-consent__cat-label"><?php echo esc_html( $category['label'] ); ?></span>
417 </button>
418 <?php else : ?>
419 <span class="ablocks-consent__cat-label"><?php echo esc_html( $category['label'] ); ?></span>
420 <?php endif; ?>
421
422 <?php if ( ! empty( $category['locked'] ) ) : ?>
423 <span class="ablocks-consent__cat-locked<?php echo $badge ? ' is-badge' : ''; ?>">
424 <?php echo esc_html( $banner['locked_label'] ); ?>
425 </span>
426 <?php else : ?>
427 <label class="ablocks-consent__switch<?php echo $checkbox ? ' ablocks-consent__switch--checkbox' : ''; ?>">
428 <input
429 type="checkbox"
430 data-ablocks-consent-category="<?php echo esc_attr( $category['slug'] ); ?>"
431 />
432 <span class="<?php echo $checkbox ? 'ablocks-consent__switch-box' : 'ablocks-consent__switch-track'; ?>" aria-hidden="true"></span>
433 <span class="screen-reader-text"><?php echo esc_html( $category['label'] ); ?></span>
434 </label>
435 <?php endif; ?>
436 </div>
437
438 <?php if ( $has_body ) : ?>
439 <div
440 class="ablocks-consent__cat-panel"
441 id="<?php echo esc_attr( $panel_id ); ?>"
442 <?php echo $is_open ? '' : 'hidden'; ?>
443 >
444 <?php if ( ! empty( $category['description'] ) ) : ?>
445 <p class="ablocks-consent__cat-desc"><?php echo esc_html( $category['description'] ); ?></p>
446 <?php endif; ?>
447 <?php if ( $cookies ) : ?>
448 <?php $this->render_cookie_table( $cookies ); ?>
449 <?php endif; ?>
450 </div>
451 <?php endif; ?>
452 </li>
453 <?php endforeach; ?>
454 </ul>
455
456 <div class="ablocks-consent__prefs-actions">
457 <?php if ( ! empty( $banner['show_prefs_reject'] ) ) : ?>
458 <button type="button" class="ablocks-consent__btn ablocks-consent__btn--secondary" data-ablocks-consent-action="reject">
459 <?php echo esc_html( $banner['reject_label'] ); ?>
460 </button>
461 <?php endif; ?>
462 <button type="button" class="ablocks-consent__btn ablocks-consent__btn--save" data-ablocks-consent-action="save">
463 <?php echo esc_html( $banner['save_label'] ); ?>
464 </button>
465 <?php if ( ! empty( $banner['show_prefs_accept'] ) ) : ?>
466 <button type="button" class="ablocks-consent__btn ablocks-consent__btn--primary" data-ablocks-consent-action="accept">
467 <?php echo esc_html( $banner['prefs_accept_label'] ); ?>
468 </button>
469 <?php endif; ?>
470 </div>
471 <?php
472 }
473
474 /**
475 * One category's cookie disclosure.
476 *
477 * A real table, because it is tabular and because a visitor reading it with
478 * a screen reader needs the column a cell belongs to announced with it.
479 *
480 * @param array $cookies Cookie rows.
481 */
482 private function render_cookie_table( $cookies ) {
483 ?>
484 <div class="ablocks-consent__cookies-scroll">
485 <table class="ablocks-consent__cookies">
486 <thead>
487 <tr>
488 <th scope="col"><?php esc_html_e( 'Cookie', 'ablocks' ); ?></th>
489 <th scope="col"><?php esc_html_e( 'Provider', 'ablocks' ); ?></th>
490 <th scope="col"><?php esc_html_e( 'Expires', 'ablocks' ); ?></th>
491 <th scope="col"><?php esc_html_e( 'Purpose', 'ablocks' ); ?></th>
492 </tr>
493 </thead>
494 <tbody>
495 <?php foreach ( $cookies as $cookie ) : ?>
496 <tr>
497 <td><code><?php echo esc_html( isset( $cookie['name'] ) ? $cookie['name'] : '' ); ?></code></td>
498 <td><?php echo esc_html( isset( $cookie['provider'] ) ? $cookie['provider'] : '' ); ?></td>
499 <td><?php echo esc_html( isset( $cookie['duration'] ) ? $cookie['duration'] : '' ); ?></td>
500 <td><?php echo esc_html( isset( $cookie['purpose'] ) ? $cookie['purpose'] : '' ); ?></td>
501 </tr>
502 <?php endforeach; ?>
503 </tbody>
504 </table>
505 </div>
506 <?php
507 }
508
509 /**
510 * `[ablocks_cookie_preferences]` — a link that reopens the preferences,
511 * for a privacy policy page.
512 *
513 * @param array $atts Shortcode attributes.
514 * @return string
515 */
516 public function preferences_shortcode( $atts ) {
517 $atts = shortcode_atts(
518 [
519 'label' => Helper::banner( 'settings_label', __( 'Cookie preferences', 'ablocks' ) ),
520 'class' => '',
521 ],
522 $atts,
523 'ablocks_cookie_preferences'
524 );
525
526 return sprintf(
527 '<button type="button" class="ablocks-consent-link %s" data-ablocks-consent-action="open">%s</button>',
528 esc_attr( $atts['class'] ),
529 esc_html( $atts['label'] )
530 );
531 }
532 }
533