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:
2026-02-16 05:45:42 +00:00
commit 6bf9f553de
19 changed files with 2726 additions and 0 deletions

304
includes/class-admin.php Normal file
View 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.' );
}
}
}

View 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;
}
}

View 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
View 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' );
}
}

View 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;
}
}