| 1 |
<?php |
| 2 |
namespace ABlocksCookieConsent; |
| 3 |
|
| 4 |
if ( ! defined( 'ABSPATH' ) ) { |
| 5 |
exit; |
| 6 |
} |
| 7 |
|
| 8 |
/** |
| 9 |
* Google Consent Mode v2. |
| 10 |
* |
| 11 |
* Google's tags read consent signals rather than being blocked outright, and |
| 12 |
* they read them *at load*. A `consent update` issued after gtag.js has already |
| 13 |
* initialised does not retroactively change the default state, so this block |
| 14 |
* has to be the first script in the document — before any enqueued script, |
| 15 |
* before any `wp_head` snippet a theme prints, before GTM. |
| 16 |
* |
| 17 |
* It is exempt from gating by construction: it is the thing that makes the |
| 18 |
* gating legible to Google, and blocking it would defeat the purpose. |
| 19 |
* |
| 20 |
* Note this is complementary to, not a replacement for, blocking the tags. |
| 21 |
* Consent Mode changes what Google's tags *do*; the gating layers stop them |
| 22 |
* loading at all. A site can run either or both, and running both is what most |
| 23 |
* European guidance expects. |
| 24 |
*/ |
| 25 |
class ConsentMode { |
| 26 |
|
| 27 |
/** |
| 28 |
* Category → the Consent Mode signals it controls. |
| 29 |
* |
| 30 |
* @var array |
| 31 |
*/ |
| 32 |
private static $signal_map = [ |
| 33 |
'analytics' => [ 'analytics_storage' ], |
| 34 |
'marketing' => [ 'ad_storage', 'ad_user_data', 'ad_personalization' ], |
| 35 |
'functional' => [ 'functionality_storage', 'personalization_storage' ], |
| 36 |
]; |
| 37 |
|
| 38 |
public static function init() { |
| 39 |
// The settings are read inside the callback rather than here. Reading |
| 40 |
// them at plugin-load time would evaluate the translated defaults |
| 41 |
// before `init`, which WordPress rightly complains about. |
| 42 |
// |
| 43 |
// Negative priority so this beats anything hooked at 0 or 1, including |
| 44 |
// core's own head output. Being first is the whole requirement. |
| 45 |
add_action( 'wp_head', [ new self(), 'print_defaults' ], -9999 ); |
| 46 |
} |
| 47 |
|
| 48 |
/** |
| 49 |
* All signals, denied, except the one that is never optional. |
| 50 |
* |
| 51 |
* @return array |
| 52 |
*/ |
| 53 |
public static function denied_defaults() { |
| 54 |
$defaults = []; |
| 55 |
foreach ( self::$signal_map as $signals ) { |
| 56 |
foreach ( $signals as $signal ) { |
| 57 |
$defaults[ $signal ] = 'denied'; |
| 58 |
} |
| 59 |
} |
| 60 |
$defaults['security_storage'] = 'granted'; |
| 61 |
return $defaults; |
| 62 |
} |
| 63 |
|
| 64 |
/** |
| 65 |
* @return array Category => signals, for the client to build its updates. |
| 66 |
*/ |
| 67 |
public static function signal_map() { |
| 68 |
return apply_filters( 'ablocks/cookie_consent/consent_mode_signals', self::$signal_map ); |
| 69 |
} |
| 70 |
|
| 71 |
public function print_defaults() { |
| 72 |
if ( ! Helper::get( 'enabled', true ) || ! Helper::get( 'consent_mode', true ) ) { |
| 73 |
return; |
| 74 |
} |
| 75 |
if ( ! Helper::should_render_banner() ) { |
| 76 |
return; |
| 77 |
} |
| 78 |
|
| 79 |
$defaults = self::denied_defaults(); |
| 80 |
$wait = (int) Helper::get( 'consent_mode_wait', 500 ); |
| 81 |
if ( $wait > 0 ) { |
| 82 |
$defaults['wait_for_update'] = $wait; |
| 83 |
} |
| 84 |
|
| 85 |
// Both names, because this runs before the main script and has to find |
| 86 |
// the decision even on the first page view after a rename. |
| 87 |
$cookies = array_values( |
| 88 |
array_filter( |
| 89 |
[ |
| 90 |
Helper::get( 'cookie_name', 'ablocks_consent' ), |
| 91 |
Helper::get( 'cookie_name_previous', '' ), |
| 92 |
] |
| 93 |
) |
| 94 |
); |
| 95 |
$map = self::signal_map(); |
| 96 |
$ads = (bool) Helper::get( 'consent_mode_ads', true ); |
| 97 |
?> |
| 98 |
<script id="ablocks-consent-mode" data-ablocks-consent-skip="1"> |
| 99 |
/* aBlocks Consent Mode v2 — must stay the first script in the document. */ |
| 100 |
window.dataLayer = window.dataLayer || []; |
| 101 |
function gtag(){ dataLayer.push( arguments ); } |
| 102 |
<?php if ( $ads ) : ?> |
| 103 |
<?php |
| 104 |
// The two settings Google pairs with denied defaults, and the reason a |
| 105 |
// refusal need not cost the site its conversion reporting. |
| 106 |
// |
| 107 |
// `ads_data_redaction` strips ad identifiers from the requests Google Ads |
| 108 |
// still makes while `ad_storage` is denied. `url_passthrough` carries the |
| 109 |
// click id in the URL between pages instead of a cookie, so a visitor who |
| 110 |
// refused is still attributed to the ad they arrived from. |
| 111 |
// |
| 112 |
// Both are `set` calls, not consent signals: they have to be in place |
| 113 |
// before the first tag reads them, which is why they sit here rather than |
| 114 |
// in the update the banner sends. |
| 115 |
?> |
| 116 |
gtag( 'set', 'ads_data_redaction', true ); |
| 117 |
gtag( 'set', 'url_passthrough', true ); |
| 118 |
<?php endif; ?> |
| 119 |
gtag( 'consent', 'default', <?php echo wp_json_encode( $defaults ); ?> ); |
| 120 |
( function () { |
| 121 |
var names = <?php echo wp_json_encode( $cookies ); ?>; |
| 122 |
var map = <?php echo wp_json_encode( $map ); ?>; |
| 123 |
var raw = null; |
| 124 |
document.cookie.split( ';' ).forEach( function ( part ) { |
| 125 |
var pair = part.split( '=' ); |
| 126 |
var key = pair.shift().trim(); |
| 127 |
if ( null === raw && names.indexOf( key ) > -1 ) { raw = pair.join( '=' ); } |
| 128 |
} ); |
| 129 |
if ( ! raw ) { return; } |
| 130 |
var saved; |
| 131 |
try { saved = JSON.parse( decodeURIComponent( raw ) ); } catch ( e ) { return; } |
| 132 |
<?php // Number() rather than a bare strict compare: a cookie written by an older build may hold the version as a string. ?> |
| 133 |
if ( ! saved || Number( saved.v ) !== <?php echo (int) Helper::get( 'policy_version', 1 ); ?> ) { return; } |
| 134 |
var granted = saved.c || []; |
| 135 |
var update = {}; |
| 136 |
Object.keys( map ).forEach( function ( category ) { |
| 137 |
var state = granted.indexOf( category ) > -1 ? 'granted' : 'denied'; |
| 138 |
map[ category ].forEach( function ( signal ) { update[ signal ] = state; } ); |
| 139 |
} ); |
| 140 |
gtag( 'consent', 'update', update ); |
| 141 |
<?php if ( $ads ) : ?> |
| 142 |
<?php // Redaction is only wanted while ads are refused; leaving it on after a grant would quietly degrade the reporting the visitor agreed to. ?> |
| 143 |
if ( 'granted' === update.ad_storage ) { gtag( 'set', 'ads_data_redaction', false ); } |
| 144 |
<?php endif; ?> |
| 145 |
<?php // Recorded so the main script does not send the same state a second time on every page view of a visitor who already decided. ?> |
| 146 |
window.ABlocksConsentSignals = update; |
| 147 |
} )(); |
| 148 |
</script> |
| 149 |
<?php |
| 150 |
} |
| 151 |
} |
| 152 |
|