Move linker config into gtag('config') call with accept_incoming and
decorate_forms for reliable cross-domain linking. Capture _gl parameter
in sessionStorage to preserve attribution across navigation. Add
referrer-based attribution replay fallback so GA4 reports source/medium
instead of (not set) when UTMs are absent. Add "Always" script category
that executes without waiting for consent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
329 lines
11 KiB
PHP
329 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Frontend output handler.
|
|
*
|
|
* @package ATT_Consent
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
class ATT_Consent_Frontend {
|
|
|
|
/**
|
|
* Plugin settings.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $settings;
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct() {
|
|
$this->settings = ATT_Consent::get_settings();
|
|
|
|
// Priority 1: consent defaults + attribution capture (must be first in <head>).
|
|
add_action( 'wp_head', array( $this, 'output_consent_defaults' ), 1 );
|
|
|
|
// Priority 2: gtag.js or GTM (Advanced mode only).
|
|
add_action( 'wp_head', array( $this, 'output_tracking_script' ), 2 );
|
|
|
|
// Custom scripts as inert type="text/plain" tags.
|
|
add_action( 'wp_head', array( $this, 'output_custom_scripts_head' ), 99 );
|
|
add_action( 'wp_footer', array( $this, 'output_custom_scripts_footer' ), 99 );
|
|
|
|
// Footer: banner HTML.
|
|
add_action( 'wp_footer', array( $this, 'output_banner_html' ), 5 );
|
|
|
|
// Enqueue banner assets.
|
|
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
|
|
}
|
|
|
|
/**
|
|
* Output the inline consent defaults script.
|
|
* This MUST run before any gtag/GTM scripts.
|
|
*/
|
|
public function output_consent_defaults() {
|
|
$s = $this->settings;
|
|
|
|
$config = array(
|
|
'consent_mode' => $s['consent_mode'],
|
|
'consent_expiry' => (int) $s['consent_expiry'],
|
|
'wait_for_update' => (int) $s['wait_for_update'],
|
|
'url_passthrough' => (bool) $s['url_passthrough'],
|
|
'ads_data_redaction' => (bool) $s['ads_data_redaction'],
|
|
'tracking_mode' => $s['tracking_mode'],
|
|
);
|
|
|
|
// Cross-domain tracking domains.
|
|
if ( ! empty( $s['cross_domain_domains'] ) ) {
|
|
$domains = array_filter( array_map( 'trim', explode( "\n", $s['cross_domain_domains'] ) ) );
|
|
if ( ! empty( $domains ) ) {
|
|
$config['linker_domains'] = array_values( $domains );
|
|
}
|
|
}
|
|
|
|
// In basic mode, pass the tracking snippet for deferred injection.
|
|
if ( 'basic' === $s['consent_mode'] ) {
|
|
$config['tracking_snippet'] = $this->get_tracking_snippet();
|
|
}
|
|
|
|
$config_json = wp_json_encode( $config );
|
|
|
|
?>
|
|
<script>
|
|
window.attCCConfig=<?php echo $config_json; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON encoded config. ?>;
|
|
(function(){
|
|
'use strict';
|
|
var c=window.attCCConfig;
|
|
window.dataLayer=window.dataLayer||[];
|
|
function g(){window.dataLayer.push(arguments);}
|
|
window.attCCGtag=g;
|
|
<?php if ( ! empty( $s['expose_gtag_global'] ) ) : ?>
|
|
window.gtag=g;
|
|
<?php endif; ?>
|
|
g('js',new Date());
|
|
if(c.ads_data_redaction){g('set','ads_data_redaction',true);}
|
|
if(c.url_passthrough){g('set','url_passthrough',true);}
|
|
var ck=document.cookie.match(/(?:^|; )att_cc_consent=([^;]*)/);
|
|
var st=null;
|
|
if(ck){try{st=JSON.parse(decodeURIComponent(ck[1]));}catch(e){}}
|
|
|
|
if(st){
|
|
g('consent','default',{
|
|
ad_storage:st.marketing?'granted':'denied',
|
|
analytics_storage:st.analytics?'granted':'denied',
|
|
ad_user_data:st.marketing?'granted':'denied',
|
|
ad_personalization:st.marketing?'granted':'denied',
|
|
functionality_storage:st.functional?'granted':'denied',
|
|
personalization_storage:st.functional?'granted':'denied',
|
|
security_storage:'granted'
|
|
});
|
|
window.attCCHasConsent=true;
|
|
}else{
|
|
g('consent','default',{
|
|
ad_storage:'denied',
|
|
analytics_storage:'denied',
|
|
ad_user_data:'denied',
|
|
ad_personalization:'denied',
|
|
functionality_storage:'denied',
|
|
personalization_storage:'denied',
|
|
security_storage:'granted',
|
|
wait_for_update:c.wait_for_update||500
|
|
});
|
|
window.attCCHasConsent=false;
|
|
}
|
|
if(!sessionStorage.getItem('att_cc_attr')){
|
|
var p=new URLSearchParams(window.location.search);
|
|
var a={
|
|
r:document.referrer||'',
|
|
lp:window.location.href,
|
|
us:p.get('utm_source')||'',
|
|
um:p.get('utm_medium')||'',
|
|
uc:p.get('utm_campaign')||'',
|
|
ut:p.get('utm_term')||'',
|
|
uo:p.get('utm_content')||'',
|
|
gc:p.get('gclid')||'',
|
|
dc:p.get('dclid')||'',
|
|
gl:p.get('_gl')||'',
|
|
t:Date.now()
|
|
};
|
|
if(a.r||a.us||a.gc||a.gl){
|
|
sessionStorage.setItem('att_cc_attr',JSON.stringify(a));
|
|
}
|
|
}
|
|
})();
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Output the tracking script (gtag.js or GTM).
|
|
* Only outputs in Advanced mode. Basic mode defers to JS.
|
|
*/
|
|
public function output_tracking_script() {
|
|
$s = $this->settings;
|
|
|
|
// In basic mode, tracking is deferred until consent.
|
|
if ( 'basic' === $s['consent_mode'] ) {
|
|
return;
|
|
}
|
|
|
|
echo $this->get_tracking_snippet(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- contains script tags.
|
|
}
|
|
|
|
/**
|
|
* Get the tracking script snippet HTML.
|
|
*
|
|
* @return string
|
|
*/
|
|
private function get_tracking_snippet() {
|
|
$s = $this->settings;
|
|
|
|
if ( 'gtm' === $s['tracking_mode'] && ! empty( $s['gtm_container_id'] ) ) {
|
|
$id = esc_attr( $s['gtm_container_id'] );
|
|
return "<!-- Google Tag Manager -->
|
|
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
|
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
|
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
|
})(window,document,'script','dataLayer','{$id}');</script>
|
|
<!-- End Google Tag Manager -->";
|
|
}
|
|
|
|
if ( 'gtag' === $s['tracking_mode'] && ! empty( $s['ga4_measurement_id'] ) ) {
|
|
$id = esc_attr( $s['ga4_measurement_id'] );
|
|
return "<script async src=\"https://www.googletagmanager.com/gtag/js?id={$id}\"></script>
|
|
<script>(function(){var o={};var c=window.attCCConfig||{};if(c.linker_domains&&c.linker_domains.length){o.linker={domains:c.linker_domains,accept_incoming:true,decorate_forms:true};}window.attCCGtag('config','{$id}',o);})();</script>";
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Output custom scripts in <head> as inert type="text/plain" tags.
|
|
*/
|
|
public function output_custom_scripts_head() {
|
|
echo ATT_Consent_Scripts_Manager::render_scripts_as_html( 'head' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- contains script/template tags.
|
|
}
|
|
|
|
/**
|
|
* Output custom scripts in footer as inert type="text/plain" tags.
|
|
*/
|
|
public function output_custom_scripts_footer() {
|
|
echo ATT_Consent_Scripts_Manager::render_scripts_as_html( 'footer' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- contains script/template tags.
|
|
}
|
|
|
|
/**
|
|
* Enqueue frontend assets.
|
|
*/
|
|
public function enqueue_assets() {
|
|
wp_enqueue_style(
|
|
'att-consent-banner',
|
|
ATT_CC_PLUGIN_URL . 'public/css/banner.css',
|
|
array(),
|
|
ATT_CC_VERSION
|
|
);
|
|
|
|
wp_enqueue_script(
|
|
'att-consent-manager',
|
|
ATT_CC_PLUGIN_URL . 'public/js/consent-manager.js',
|
|
array(),
|
|
ATT_CC_VERSION,
|
|
true
|
|
);
|
|
|
|
wp_enqueue_script(
|
|
'att-consent-banner',
|
|
ATT_CC_PLUGIN_URL . 'public/js/banner.js',
|
|
array( 'att-consent-manager' ),
|
|
ATT_CC_VERSION,
|
|
true
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Output the banner and preferences modal HTML.
|
|
*/
|
|
public function output_banner_html() {
|
|
$s = $this->settings;
|
|
|
|
do_action( 'att_consent_before_banner' );
|
|
|
|
$position_class = 'att-cc-position-' . esc_attr( $s['banner_position'] );
|
|
|
|
$style_vars = sprintf(
|
|
'--att-cc-bg:%s;--att-cc-text:%s;--att-cc-accept-bg:%s;--att-cc-accept-text:%s;--att-cc-reject-bg:%s;--att-cc-reject-text:%s;--att-cc-pref-bg:%s;--att-cc-pref-text:%s;',
|
|
esc_attr( $s['banner_bg_color'] ),
|
|
esc_attr( $s['banner_text_color'] ),
|
|
esc_attr( $s['btn_accept_bg'] ),
|
|
esc_attr( $s['btn_accept_text'] ),
|
|
esc_attr( $s['btn_reject_bg'] ),
|
|
esc_attr( $s['btn_reject_text'] ),
|
|
esc_attr( $s['btn_preferences_bg'] ),
|
|
esc_attr( $s['btn_preferences_text'] )
|
|
);
|
|
|
|
$banner_html = '<div id="att-cc-banner" class="' . esc_attr( $position_class ) . '" role="dialog" aria-labelledby="att-cc-banner-heading" aria-hidden="false" tabindex="-1" style="' . $style_vars . '">
|
|
<div class="att-cc-banner__inner">
|
|
<div class="att-cc-banner__content">
|
|
<h2 id="att-cc-banner-heading" class="att-cc-banner__heading">' . esc_html( $s['banner_heading'] ) . '</h2>
|
|
<p class="att-cc-banner__message">' . wp_kses_post( $s['banner_message'] ) . '</p>
|
|
</div>
|
|
<div class="att-cc-banner__actions">
|
|
<button type="button" data-att-cc="accept-all" class="att-cc-btn att-cc-btn--accept">' . esc_html( $s['btn_accept_label'] ) . '</button>
|
|
<button type="button" data-att-cc="reject-all" class="att-cc-btn att-cc-btn--reject">' . esc_html( $s['btn_reject_label'] ) . '</button>
|
|
<button type="button" data-att-cc="preferences" class="att-cc-btn att-cc-btn--preferences">' . esc_html( $s['btn_preferences_label'] ) . '</button>
|
|
</div>
|
|
</div>
|
|
</div>';
|
|
|
|
$modal_html = '<div id="att-cc-modal" role="dialog" aria-labelledby="att-cc-modal-heading" aria-hidden="true" aria-modal="true" style="' . $style_vars . '">
|
|
<div class="att-cc-modal__overlay"></div>
|
|
<div class="att-cc-modal__dialog">
|
|
<h2 id="att-cc-modal-heading" class="att-cc-modal__heading">' . esc_html__( 'Manage Cookie Preferences', 'att-consent' ) . '</h2>
|
|
|
|
<div class="att-cc-modal__category">
|
|
<div class="att-cc-modal__cat-header">
|
|
<label>
|
|
<input type="checkbox" checked disabled>
|
|
<strong>' . esc_html__( 'Necessary', 'att-consent' ) . '</strong>
|
|
<span class="att-cc-always-on">' . esc_html__( 'Always active', 'att-consent' ) . '</span>
|
|
</label>
|
|
</div>
|
|
<p class="att-cc-modal__cat-desc">' . wp_kses_post( $s['cat_necessary_desc'] ) . '</p>
|
|
</div>
|
|
|
|
<div class="att-cc-modal__category">
|
|
<div class="att-cc-modal__cat-header">
|
|
<label>
|
|
<input type="checkbox" id="att-cc-functional">
|
|
<strong>' . esc_html__( 'Functional', 'att-consent' ) . '</strong>
|
|
</label>
|
|
</div>
|
|
<p class="att-cc-modal__cat-desc">' . wp_kses_post( $s['cat_functional_desc'] ) . '</p>
|
|
</div>
|
|
|
|
<div class="att-cc-modal__category">
|
|
<div class="att-cc-modal__cat-header">
|
|
<label>
|
|
<input type="checkbox" id="att-cc-analytics">
|
|
<strong>' . esc_html__( 'Analytics', 'att-consent' ) . '</strong>
|
|
</label>
|
|
</div>
|
|
<p class="att-cc-modal__cat-desc">' . wp_kses_post( $s['cat_analytics_desc'] ) . '</p>
|
|
</div>
|
|
|
|
<div class="att-cc-modal__category">
|
|
<div class="att-cc-modal__cat-header">
|
|
<label>
|
|
<input type="checkbox" id="att-cc-marketing">
|
|
<strong>' . esc_html__( 'Marketing', 'att-consent' ) . '</strong>
|
|
</label>
|
|
</div>
|
|
<p class="att-cc-modal__cat-desc">' . wp_kses_post( $s['cat_marketing_desc'] ) . '</p>
|
|
</div>
|
|
|
|
<div class="att-cc-modal__actions">
|
|
<button type="button" data-att-cc="save-preferences" class="att-cc-btn att-cc-btn--accept">' . esc_html( $s['btn_save_label'] ) . '</button>
|
|
<button type="button" data-att-cc="close-modal" class="att-cc-btn att-cc-btn--reject">' . esc_html__( 'Cancel', 'att-consent' ) . '</button>
|
|
</div>
|
|
</div>
|
|
</div>';
|
|
|
|
$widget_html = '';
|
|
if ( 'none' !== $s['floating_widget'] ) {
|
|
$widget_position = 'right' === $s['floating_widget'] ? 'att-cc-widget--right' : 'att-cc-widget--bottom-right';
|
|
$widget_label = ! empty( $s['floating_widget_label'] ) ? $s['floating_widget_label'] : 'Cookie Settings';
|
|
$widget_html = '<button type="button" id="att-cc-widget" class="att-cc-widget ' . esc_attr( $widget_position ) . '" aria-label="' . esc_attr( $widget_label ) . '" style="display:none;' . $style_vars . '"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8zm-1-13a1.5 1.5 0 1 1-1.5-1.5A1.5 1.5 0 0 1 11 7zm4 2a1 1 0 1 1-1-1 1 1 0 0 1 1 1zm-7 3a1 1 0 1 1-1-1 1 1 0 0 1 1 1zm3 4a1.5 1.5 0 1 1-1.5-1.5A1.5 1.5 0 0 1 11 16zm5-1a1 1 0 1 1-1-1 1 1 0 0 1 1 1z"/></svg> ' . esc_html( $widget_label ) . '</button>';
|
|
}
|
|
|
|
$html = apply_filters( 'att_consent_banner_html', $banner_html . $modal_html . $widget_html, $s );
|
|
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above.
|
|
|
|
do_action( 'att_consent_after_banner' );
|
|
}
|
|
}
|