Initial commit: ATT Consent plugin v1.0.0
Google Consent Mode v2 cookie consent plugin with session attribution preservation, custom script management, and gtag.js/GTM support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
304
includes/class-admin.php
Normal file
304
includes/class-admin.php
Normal file
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin settings handler.
|
||||
*
|
||||
* @package ATT_Consent
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class ATT_Consent_Admin {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'admin_menu', array( $this, 'add_menu_page' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
|
||||
add_action( 'admin_post_att_cc_save_settings', array( $this, 'save_settings' ) );
|
||||
add_action( 'wp_ajax_att_cc_save_script', array( $this, 'ajax_save_script' ) );
|
||||
add_action( 'wp_ajax_att_cc_delete_script', array( $this, 'ajax_delete_script' ) );
|
||||
add_action( 'wp_ajax_att_cc_get_script', array( $this, 'ajax_get_script' ) );
|
||||
add_filter( 'plugin_action_links_' . ATT_CC_PLUGIN_BASENAME, array( $this, 'add_settings_link' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add settings link to plugins page.
|
||||
*
|
||||
* @param array $links Existing links.
|
||||
* @return array
|
||||
*/
|
||||
public function add_settings_link( $links ) {
|
||||
$settings_link = sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
esc_url( admin_url( 'admin.php?page=att-consent' ) ),
|
||||
esc_html__( 'Settings', 'att-consent' )
|
||||
);
|
||||
array_unshift( $links, $settings_link );
|
||||
return $links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the admin menu page.
|
||||
*/
|
||||
public function add_menu_page() {
|
||||
add_menu_page(
|
||||
__( 'ATT Consent', 'att-consent' ),
|
||||
__( 'Cookie Consent', 'att-consent' ),
|
||||
'manage_options',
|
||||
'att-consent',
|
||||
array( $this, 'render_settings_page' ),
|
||||
'dashicons-shield',
|
||||
81
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin assets.
|
||||
*
|
||||
* @param string $hook_suffix The current admin page hook.
|
||||
*/
|
||||
public function enqueue_assets( $hook_suffix ) {
|
||||
if ( 'toplevel_page_att-consent' !== $hook_suffix ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style( 'wp-color-picker' );
|
||||
wp_enqueue_style(
|
||||
'att-consent-admin',
|
||||
ATT_CC_PLUGIN_URL . 'admin/css/admin.css',
|
||||
array(),
|
||||
ATT_CC_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script( 'wp-color-picker' );
|
||||
wp_enqueue_script(
|
||||
'att-consent-admin',
|
||||
ATT_CC_PLUGIN_URL . 'admin/js/admin.js',
|
||||
array( 'jquery', 'wp-color-picker' ),
|
||||
ATT_CC_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script( 'att-consent-admin', 'attCCAdmin', array(
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'att_cc_admin' ),
|
||||
'strings' => array(
|
||||
'confirmDelete' => __( 'Are you sure you want to delete this script?', 'att-consent' ),
|
||||
'saved' => __( 'Script saved.', 'att-consent' ),
|
||||
'deleted' => __( 'Script deleted.', 'att-consent' ),
|
||||
'error' => __( 'An error occurred. Please try again.', 'att-consent' ),
|
||||
),
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the settings page.
|
||||
*/
|
||||
public function render_settings_page() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$tabs = array(
|
||||
'general' => __( 'General', 'att-consent' ),
|
||||
'appearance' => __( 'Appearance', 'att-consent' ),
|
||||
'categories' => __( 'Categories', 'att-consent' ),
|
||||
'scripts' => __( 'Custom Scripts', 'att-consent' ),
|
||||
'advanced' => __( 'Advanced', 'att-consent' ),
|
||||
);
|
||||
|
||||
$settings = ATT_Consent::get_settings();
|
||||
?>
|
||||
<div class="wrap att-cc-admin-wrap">
|
||||
<h1><?php esc_html_e( 'ATT Consent Settings', 'att-consent' ); ?></h1>
|
||||
|
||||
<?php
|
||||
if ( isset( $_GET['saved'] ) && '1' === $_GET['saved'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Settings saved.', 'att-consent' ) . '</p></div>';
|
||||
}
|
||||
?>
|
||||
|
||||
<nav class="nav-tab-wrapper">
|
||||
<?php foreach ( $tabs as $tab_key => $tab_label ) : ?>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=att-consent&tab=' . $tab_key ) ); ?>"
|
||||
class="nav-tab <?php echo $active_tab === $tab_key ? 'nav-tab-active' : ''; ?>">
|
||||
<?php echo esc_html( $tab_label ); ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
|
||||
<div class="att-cc-tab-content">
|
||||
<?php
|
||||
$view_file = ATT_CC_PLUGIN_DIR . 'admin/views/settings-' . $active_tab . '.php';
|
||||
if ( file_exists( $view_file ) ) {
|
||||
include $view_file;
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings from any tab.
|
||||
*/
|
||||
public function save_settings() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_die( esc_html__( 'Unauthorized.', 'att-consent' ) );
|
||||
}
|
||||
|
||||
check_admin_referer( 'att_cc_save_settings' );
|
||||
|
||||
$tab = sanitize_key( $_POST['att_cc_tab'] ?? 'general' );
|
||||
$settings = ATT_Consent::get_settings();
|
||||
|
||||
switch ( $tab ) {
|
||||
case 'general':
|
||||
$settings['tracking_mode'] = in_array( $_POST['tracking_mode'] ?? '', array( 'gtag', 'gtm' ), true ) ? $_POST['tracking_mode'] : 'gtag';
|
||||
$settings['ga4_measurement_id'] = sanitize_text_field( $_POST['ga4_measurement_id'] ?? '' );
|
||||
$settings['gtm_container_id'] = sanitize_text_field( $_POST['gtm_container_id'] ?? '' );
|
||||
$settings['consent_mode'] = in_array( $_POST['consent_mode'] ?? '', array( 'advanced', 'basic' ), true ) ? $_POST['consent_mode'] : 'advanced';
|
||||
$settings['banner_position'] = in_array( $_POST['banner_position'] ?? '', array( 'bottom', 'top', 'center' ), true ) ? $_POST['banner_position'] : 'bottom';
|
||||
$settings['consent_expiry'] = absint( $_POST['consent_expiry'] ?? 365 );
|
||||
break;
|
||||
|
||||
case 'appearance':
|
||||
$color_fields = array(
|
||||
'banner_bg_color', 'banner_text_color',
|
||||
'btn_accept_bg', 'btn_accept_text',
|
||||
'btn_reject_bg', 'btn_reject_text',
|
||||
'btn_preferences_bg', 'btn_preferences_text',
|
||||
);
|
||||
foreach ( $color_fields as $field ) {
|
||||
$value = $_POST[ $field ] ?? '';
|
||||
if ( 'transparent' === $value ) {
|
||||
$settings[ $field ] = 'transparent';
|
||||
} else {
|
||||
$settings[ $field ] = sanitize_hex_color( $value ) ?: $settings[ $field ];
|
||||
}
|
||||
}
|
||||
|
||||
$text_fields = array(
|
||||
'banner_heading', 'banner_message',
|
||||
'btn_accept_label', 'btn_reject_label',
|
||||
'btn_preferences_label', 'btn_save_label',
|
||||
);
|
||||
foreach ( $text_fields as $field ) {
|
||||
if ( 'banner_message' === $field ) {
|
||||
$settings[ $field ] = wp_kses_post( $_POST[ $field ] ?? '' );
|
||||
} else {
|
||||
$settings[ $field ] = sanitize_text_field( $_POST[ $field ] ?? '' );
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'categories':
|
||||
$cat_fields = array(
|
||||
'cat_necessary_desc', 'cat_functional_desc',
|
||||
'cat_analytics_desc', 'cat_marketing_desc',
|
||||
);
|
||||
foreach ( $cat_fields as $field ) {
|
||||
$settings[ $field ] = wp_kses_post( $_POST[ $field ] ?? '' );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'advanced':
|
||||
$settings['url_passthrough'] = ! empty( $_POST['url_passthrough'] );
|
||||
$settings['ads_data_redaction'] = ! empty( $_POST['ads_data_redaction'] );
|
||||
$settings['wait_for_update'] = absint( $_POST['wait_for_update'] ?? 500 );
|
||||
if ( $settings['wait_for_update'] < 100 ) {
|
||||
$settings['wait_for_update'] = 100;
|
||||
}
|
||||
if ( $settings['wait_for_update'] > 10000 ) {
|
||||
$settings['wait_for_update'] = 10000;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$settings = apply_filters( 'att_consent_settings_save', $settings, $tab );
|
||||
update_option( 'att_consent_settings', $settings );
|
||||
|
||||
wp_safe_redirect( admin_url( 'admin.php?page=att-consent&tab=' . $tab . '&saved=1' ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Save a custom script.
|
||||
*/
|
||||
public function ajax_save_script() {
|
||||
check_ajax_referer( 'att_cc_admin', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( 'Unauthorized' );
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'id' => absint( $_POST['script_id'] ?? 0 ) ?: null,
|
||||
'name' => $_POST['name'] ?? '',
|
||||
'snippet' => $_POST['snippet'] ?? '',
|
||||
'category' => $_POST['category'] ?? 'analytics',
|
||||
'placement' => $_POST['placement'] ?? 'head',
|
||||
'status' => $_POST['status'] ?? 'active',
|
||||
'priority' => $_POST['priority'] ?? 10,
|
||||
);
|
||||
|
||||
$result = ATT_Consent_Scripts_Manager::save_script( $data );
|
||||
|
||||
if ( false !== $result ) {
|
||||
wp_send_json_success( array(
|
||||
'id' => $result,
|
||||
'scripts' => ATT_Consent_Scripts_Manager::get_all_scripts(),
|
||||
) );
|
||||
} else {
|
||||
wp_send_json_error( 'Failed to save script.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Delete a custom script.
|
||||
*/
|
||||
public function ajax_delete_script() {
|
||||
check_ajax_referer( 'att_cc_admin', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( 'Unauthorized' );
|
||||
}
|
||||
|
||||
$id = absint( $_POST['script_id'] ?? 0 );
|
||||
if ( ! $id ) {
|
||||
wp_send_json_error( 'Invalid ID.' );
|
||||
}
|
||||
|
||||
if ( ATT_Consent_Scripts_Manager::delete_script( $id ) ) {
|
||||
wp_send_json_success( array(
|
||||
'scripts' => ATT_Consent_Scripts_Manager::get_all_scripts(),
|
||||
) );
|
||||
} else {
|
||||
wp_send_json_error( 'Failed to delete script.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Get a single script for editing.
|
||||
*/
|
||||
public function ajax_get_script() {
|
||||
check_ajax_referer( 'att_cc_admin', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( 'Unauthorized' );
|
||||
}
|
||||
|
||||
$id = absint( $_POST['script_id'] ?? 0 );
|
||||
$script = ATT_Consent_Scripts_Manager::get_script( $id );
|
||||
|
||||
if ( $script ) {
|
||||
wp_send_json_success( $script );
|
||||
} else {
|
||||
wp_send_json_error( 'Script not found.' );
|
||||
}
|
||||
}
|
||||
}
|
||||
179
includes/class-att-consent.php
Normal file
179
includes/class-att-consent.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
/**
|
||||
* Main plugin class.
|
||||
*
|
||||
* @package ATT_Consent
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class ATT_Consent {
|
||||
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @var ATT_Consent|null
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Default settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $defaults = array(
|
||||
// General.
|
||||
'tracking_mode' => 'gtag',
|
||||
'ga4_measurement_id' => '',
|
||||
'gtm_container_id' => '',
|
||||
'consent_mode' => 'advanced',
|
||||
'banner_position' => 'bottom',
|
||||
'consent_expiry' => 365,
|
||||
|
||||
// Appearance.
|
||||
'banner_bg_color' => '#1a1a2e',
|
||||
'banner_text_color' => '#ffffff',
|
||||
'btn_accept_bg' => '#4CAF50',
|
||||
'btn_accept_text' => '#ffffff',
|
||||
'btn_reject_bg' => '#555555',
|
||||
'btn_reject_text' => '#ffffff',
|
||||
'btn_preferences_bg' => 'transparent',
|
||||
'btn_preferences_text' => '#ffffff',
|
||||
|
||||
// Text / Labels.
|
||||
'banner_heading' => 'We value your privacy',
|
||||
'banner_message' => 'We use cookies to enhance your browsing experience, serve personalised content, and analyse our traffic. By clicking "Accept All", you consent to our use of cookies.',
|
||||
'btn_accept_label' => 'Accept All',
|
||||
'btn_reject_label' => 'Reject All',
|
||||
'btn_preferences_label' => 'Manage Preferences',
|
||||
'btn_save_label' => 'Save Preferences',
|
||||
|
||||
// Category descriptions.
|
||||
'cat_necessary_desc' => 'These cookies are essential for the website to function and cannot be switched off.',
|
||||
'cat_functional_desc' => 'These cookies enable personalised features and functionality.',
|
||||
'cat_analytics_desc' => 'These cookies help us understand how visitors interact with our website.',
|
||||
'cat_marketing_desc' => 'These cookies are used to deliver advertisements relevant to you.',
|
||||
|
||||
// Advanced.
|
||||
'url_passthrough' => true,
|
||||
'ads_data_redaction' => true,
|
||||
'wait_for_update' => 500,
|
||||
);
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
*
|
||||
* @return ATT_Consent
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
private function __construct() {
|
||||
add_action( 'init', array( $this, 'load_textdomain' ) );
|
||||
$this->load_dependencies();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugin text domain.
|
||||
*/
|
||||
public function load_textdomain() {
|
||||
load_plugin_textdomain( 'att-consent', false, dirname( ATT_CC_PLUGIN_BASENAME ) . '/languages' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Load required files.
|
||||
*/
|
||||
private function load_dependencies() {
|
||||
require_once ATT_CC_PLUGIN_DIR . 'includes/class-scripts-manager.php';
|
||||
require_once ATT_CC_PLUGIN_DIR . 'includes/class-consent-api.php';
|
||||
|
||||
if ( is_admin() ) {
|
||||
require_once ATT_CC_PLUGIN_DIR . 'includes/class-admin.php';
|
||||
new ATT_Consent_Admin();
|
||||
}
|
||||
|
||||
if ( ! is_admin() || wp_doing_ajax() ) {
|
||||
require_once ATT_CC_PLUGIN_DIR . 'includes/class-frontend.php';
|
||||
new ATT_Consent_Frontend();
|
||||
}
|
||||
|
||||
new ATT_Consent_API();
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin activation.
|
||||
*/
|
||||
public static function activate() {
|
||||
self::create_scripts_table();
|
||||
|
||||
// Set defaults if no settings exist yet.
|
||||
if ( false === get_option( 'att_consent_settings' ) ) {
|
||||
update_option( 'att_consent_settings', self::$defaults );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin deactivation.
|
||||
*/
|
||||
public static function deactivate() {
|
||||
// No-op. Cleanup happens in uninstall.php.
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the custom scripts table.
|
||||
*/
|
||||
private static function create_scripts_table() {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'att_cc_scripts';
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE $table_name (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
name varchar(200) NOT NULL DEFAULT '',
|
||||
snippet longtext NOT NULL,
|
||||
category varchar(50) NOT NULL DEFAULT 'analytics',
|
||||
placement varchar(20) NOT NULL DEFAULT 'head',
|
||||
status varchar(20) NOT NULL DEFAULT 'active',
|
||||
priority int(11) NOT NULL DEFAULT 10,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY category (category),
|
||||
KEY status (status)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
dbDelta( $sql );
|
||||
|
||||
update_option( 'att_cc_db_version', ATT_CC_VERSION );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin settings merged with defaults.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_settings() {
|
||||
$settings = get_option( 'att_consent_settings', array() );
|
||||
return wp_parse_args( $settings, self::$defaults );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default settings.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_defaults() {
|
||||
return self::$defaults;
|
||||
}
|
||||
}
|
||||
51
includes/class-consent-api.php
Normal file
51
includes/class-consent-api.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* WP Consent API integration.
|
||||
*
|
||||
* Registers with the WP Consent API plugin (if active) so other plugins
|
||||
* can query consent status via wp_has_consent().
|
||||
*
|
||||
* @package ATT_Consent
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class ATT_Consent_API {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Register as a compliant plugin with WP Consent API.
|
||||
add_filter( 'wp_consent_api_registered_' . ATT_CC_PLUGIN_BASENAME, '__return_true' );
|
||||
|
||||
// Map our categories to WP Consent API categories.
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_consent_api_bridge' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue a small bridge script that syncs ATT Consent with WP Consent API.
|
||||
*/
|
||||
public function enqueue_consent_api_bridge() {
|
||||
// Only load if WP Consent API is active.
|
||||
if ( ! function_exists( 'wp_has_consent' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$script = "
|
||||
document.addEventListener('att_consent_update', function(e) {
|
||||
var c = e.detail;
|
||||
if (typeof wp_set_consent === 'function') {
|
||||
wp_set_consent('functional', c.functional ? 'allow' : 'deny');
|
||||
wp_set_consent('statistics', c.analytics ? 'allow' : 'deny');
|
||||
wp_set_consent('marketing', c.marketing ? 'allow' : 'deny');
|
||||
wp_set_consent('preferences', c.functional ? 'allow' : 'deny');
|
||||
}
|
||||
});
|
||||
";
|
||||
|
||||
wp_add_inline_script( 'att-consent-manager', $script, 'after' );
|
||||
}
|
||||
}
|
||||
292
includes/class-frontend.php
Normal file
292
includes/class-frontend.php
Normal file
@@ -0,0 +1,292 @@
|
||||
<?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 );
|
||||
|
||||
// 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'],
|
||||
);
|
||||
|
||||
// In basic mode, pass the tracking snippet for deferred injection.
|
||||
if ( 'basic' === $s['consent_mode'] ) {
|
||||
$config['tracking_snippet'] = $this->get_tracking_snippet();
|
||||
}
|
||||
|
||||
// Pass custom scripts for conditional injection.
|
||||
$config['scripts'] = ATT_Consent_Scripts_Manager::get_scripts_for_frontend();
|
||||
|
||||
$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;
|
||||
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=ck?JSON.parse(decodeURIComponent(ck[1])):null;
|
||||
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')||'',
|
||||
t:Date.now()
|
||||
};
|
||||
if(a.r||a.us||a.gc){
|
||||
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>window.attCCGtag('config','{$id}');</script>";
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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-label="' . esc_attr__( 'Cookie consent', 'att-consent' ) . '" aria-hidden="false" style="' . $style_vars . '">
|
||||
<div class="att-cc-banner__inner">
|
||||
<div class="att-cc-banner__content">
|
||||
<h2 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-label="' . esc_attr__( 'Cookie preferences', 'att-consent' ) . '" aria-hidden="true" aria-modal="true" style="' . $style_vars . '">
|
||||
<div class="att-cc-modal__overlay"></div>
|
||||
<div class="att-cc-modal__dialog">
|
||||
<h2 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>';
|
||||
|
||||
$html = apply_filters( 'att_consent_banner_html', $banner_html . $modal_html, $s );
|
||||
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above.
|
||||
|
||||
do_action( 'att_consent_after_banner' );
|
||||
}
|
||||
}
|
||||
168
includes/class-scripts-manager.php
Normal file
168
includes/class-scripts-manager.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
/**
|
||||
* Custom scripts CRUD manager.
|
||||
*
|
||||
* @package ATT_Consent
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class ATT_Consent_Scripts_Manager {
|
||||
|
||||
/**
|
||||
* Get the table name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function table_name() {
|
||||
global $wpdb;
|
||||
return $wpdb->prefix . 'att_cc_scripts';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all scripts, optionally filtered.
|
||||
*
|
||||
* @param string|null $category Filter by category.
|
||||
* @param string|null $placement Filter by placement.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_scripts( $category = null, $placement = null ) {
|
||||
global $wpdb;
|
||||
$table = self::table_name();
|
||||
|
||||
$where = array( 'status = %s' );
|
||||
$params = array( 'active' );
|
||||
|
||||
if ( null !== $category ) {
|
||||
$where[] = 'category = %s';
|
||||
$params[] = $category;
|
||||
}
|
||||
|
||||
if ( null !== $placement ) {
|
||||
$where[] = 'placement = %s';
|
||||
$params[] = $placement;
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where );
|
||||
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE {$where_sql} ORDER BY priority ASC, id ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
...$params
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all scripts (including inactive) for the admin list.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_all_scripts() {
|
||||
global $wpdb;
|
||||
$table = self::table_name();
|
||||
|
||||
return $wpdb->get_results(
|
||||
"SELECT * FROM {$table} ORDER BY priority ASC, id ASC", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single script by ID.
|
||||
*
|
||||
* @param int $id Script ID.
|
||||
* @return array|null
|
||||
*/
|
||||
public static function get_script( $id ) {
|
||||
global $wpdb;
|
||||
$table = self::table_name();
|
||||
|
||||
return $wpdb->get_row(
|
||||
$wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save (insert or update) a script.
|
||||
*
|
||||
* @param array $data Script data.
|
||||
* @return int|false The script ID on success, false on failure.
|
||||
*/
|
||||
public static function save_script( $data ) {
|
||||
global $wpdb;
|
||||
$table = self::table_name();
|
||||
|
||||
$allowed_categories = array( 'functional', 'analytics', 'marketing' );
|
||||
$allowed_placements = array( 'head', 'footer' );
|
||||
$allowed_statuses = array( 'active', 'inactive' );
|
||||
|
||||
$fields = array(
|
||||
'name' => sanitize_text_field( $data['name'] ?? '' ),
|
||||
'snippet' => wp_unslash( $data['snippet'] ?? '' ),
|
||||
'category' => in_array( $data['category'] ?? '', $allowed_categories, true ) ? $data['category'] : 'analytics',
|
||||
'placement' => in_array( $data['placement'] ?? '', $allowed_placements, true ) ? $data['placement'] : 'head',
|
||||
'status' => in_array( $data['status'] ?? '', $allowed_statuses, true ) ? $data['status'] : 'active',
|
||||
'priority' => absint( $data['priority'] ?? 10 ),
|
||||
);
|
||||
|
||||
if ( ! empty( $data['id'] ) ) {
|
||||
$fields['updated_at'] = current_time( 'mysql' );
|
||||
$result = $wpdb->update(
|
||||
$table,
|
||||
$fields,
|
||||
array( 'id' => absint( $data['id'] ) ),
|
||||
array( '%s', '%s', '%s', '%s', '%s', '%d', '%s' ),
|
||||
array( '%d' )
|
||||
);
|
||||
return false !== $result ? absint( $data['id'] ) : false;
|
||||
}
|
||||
|
||||
$fields['created_at'] = current_time( 'mysql' );
|
||||
$fields['updated_at'] = current_time( 'mysql' );
|
||||
$result = $wpdb->insert( $table, $fields );
|
||||
|
||||
return false !== $result ? $wpdb->insert_id : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a script by ID.
|
||||
*
|
||||
* @param int $id Script ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete_script( $id ) {
|
||||
global $wpdb;
|
||||
$table = self::table_name();
|
||||
|
||||
return false !== $wpdb->delete( $table, array( 'id' => absint( $id ) ), array( '%d' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scripts grouped by category and placement for frontend output.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_scripts_for_frontend() {
|
||||
$scripts = self::get_scripts();
|
||||
$grouped = array(
|
||||
'functional' => array( 'head' => array(), 'footer' => array() ),
|
||||
'analytics' => array( 'head' => array(), 'footer' => array() ),
|
||||
'marketing' => array( 'head' => array(), 'footer' => array() ),
|
||||
);
|
||||
|
||||
foreach ( $scripts as $script ) {
|
||||
$cat = $script['category'];
|
||||
$placement = $script['placement'];
|
||||
if ( isset( $grouped[ $cat ][ $placement ] ) ) {
|
||||
$grouped[ $cat ][ $placement ][] = $script['snippet'];
|
||||
}
|
||||
}
|
||||
|
||||
return $grouped;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user