PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.12.0
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.12.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 1.1.2 All 80 releases
ablocks / addons / cookie-consent / frontend.php

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

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