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:
82
CLAUDE.md
Normal file
82
CLAUDE.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# ATT Consent - Developer Context
|
||||
|
||||
## What This Is
|
||||
|
||||
A WordPress plugin implementing Google Consent Mode v2 with session attribution preservation. It replaces paid CMv2 plugins (e.g., CookieYes) with a self-contained, distributable alternative.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Flow
|
||||
|
||||
1. **Inline `<head>` script (priority 1)** - Sets consent defaults and captures attribution to sessionStorage. Must run before any Google tags.
|
||||
2. **Tracking script (priority 2)** - Loads gtag.js or GTM. In Advanced mode, loads immediately. In Basic mode, deferred until consent.
|
||||
3. **Footer** - Banner HTML + enqueued `banner.js` and `banner.css`.
|
||||
|
||||
### Key Files
|
||||
|
||||
- `includes/class-frontend.php` - Orchestrates the output order. The `output_consent_defaults()` method generates the critical inline script.
|
||||
- `public/js/consent-manager.js` - Core logic: `AttConsent.update()` replays attribution, sends consent signals, injects scripts, sets cookie. This is the public API.
|
||||
- `public/js/banner.js` - UI only. Calls `AttConsent.update()` on button clicks.
|
||||
- `includes/class-admin.php` - Admin settings with 5 tabs. Handles save via `admin_post`, scripts CRUD via AJAX.
|
||||
- `includes/class-scripts-manager.php` - CRUD for custom JS snippets stored in `{prefix}att_cc_scripts` table.
|
||||
|
||||
### Settings
|
||||
|
||||
Single serialized option: `att_consent_settings`. Access via `ATT_Consent::get_settings()` which merges with defaults.
|
||||
|
||||
### Custom Scripts Table
|
||||
|
||||
```sql
|
||||
{prefix}att_cc_scripts (id, name, snippet, category, placement, status, priority, created_at, updated_at)
|
||||
```
|
||||
|
||||
Scripts are passed to frontend JS via the inline config object, NOT rendered in PHP (they'd execute before consent). Injected into DOM by `consent-manager.js` after consent.
|
||||
|
||||
### Consent Cookie
|
||||
|
||||
Name: `att_cc_consent`. Value: JSON `{"functional":bool,"analytics":bool,"marketing":bool}`. First-party, SameSite=Lax.
|
||||
|
||||
### Attribution Storage
|
||||
|
||||
sessionStorage key `att_cc_attr` stores referrer, UTMs, GCLID/DCLID on first page of session. Key `att_cc_consent_given` tracks whether attribution has been replayed. Attribution is replayed via `gtag('set')` BEFORE `gtag('consent','update')` so GA4 session_start gets correct source.
|
||||
|
||||
## Category-to-Signal Mapping
|
||||
|
||||
| Category | Google Signals |
|
||||
|----------|---------------|
|
||||
| Necessary | `security_storage` (always granted) |
|
||||
| Functional | `functionality_storage`, `personalization_storage` |
|
||||
| Analytics | `analytics_storage` |
|
||||
| Marketing | `ad_storage`, `ad_user_data`, `ad_personalization` |
|
||||
|
||||
## WordPress Hooks
|
||||
|
||||
**Filters:** `att_consent_settings_save`, `att_consent_banner_html`
|
||||
**Actions:** `att_consent_before_banner`, `att_consent_after_banner`
|
||||
**JS Event:** `att_consent_update` (CustomEvent with `detail: {functional, analytics, marketing}`)
|
||||
|
||||
## Coding Standards
|
||||
|
||||
- WordPress PHPCS (WordPress-Extra)
|
||||
- All admin inputs sanitized: `sanitize_text_field`, `sanitize_hex_color`, `absint`, `wp_kses_post`
|
||||
- Nonce verification on all forms and AJAX
|
||||
- Capability check: `manage_options` for all admin operations
|
||||
- Script snippets stored raw (admin-only input, intentionally executable)
|
||||
- Frontend JS is vanilla - no jQuery dependency
|
||||
- Text domain: `att-consent`
|
||||
|
||||
## Testing
|
||||
|
||||
Activate plugin, enter a GA4 Measurement ID in General settings, visit frontend. Verify:
|
||||
- `window.dataLayer` contains consent default with `wait_for_update`
|
||||
- Clicking Accept All fires `gtag('consent','update')` with all granted
|
||||
- Cookie `att_cc_consent` is set
|
||||
- Return visit: no banner shown, consent applied from cookie
|
||||
- Attribution test: visit with `?utm_source=test`, navigate without consenting, consent on page 2, check GA4 DebugView for correct source
|
||||
|
||||
## Environment
|
||||
|
||||
- WordPress 6.8.3, Astra child theme
|
||||
- Local Sites development environment
|
||||
- WooCommerce + LearnPress site
|
||||
- Previously used CookieYes (`cookie-law-info` plugin)
|
||||
127
admin/css/admin.css
Normal file
127
admin/css/admin.css
Normal file
@@ -0,0 +1,127 @@
|
||||
/* ATT Consent Admin Styles */
|
||||
|
||||
.att-cc-admin-wrap .nav-tab-wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.att-cc-tab-content {
|
||||
background: #fff;
|
||||
border: 1px solid #ccd0d4;
|
||||
border-top: none;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Scripts table */
|
||||
#att-cc-scripts-table .column-name { width: 25%; }
|
||||
#att-cc-scripts-table .column-category { width: 15%; }
|
||||
#att-cc-scripts-table .column-placement { width: 12%; }
|
||||
#att-cc-scripts-table .column-priority { width: 10%; }
|
||||
#att-cc-scripts-table .column-status { width: 12%; }
|
||||
#att-cc-scripts-table .column-actions { width: 20%; }
|
||||
|
||||
.att-cc-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.att-cc-badge--functional { background: #2196F3; }
|
||||
.att-cc-badge--analytics { background: #FF9800; }
|
||||
.att-cc-badge--marketing { background: #9C27B0; }
|
||||
|
||||
.att-cc-status {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.att-cc-status--active {
|
||||
background: #e7f5e7;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.att-cc-status--inactive {
|
||||
background: #f5e7e7;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
/* Admin modal */
|
||||
.att-cc-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.att-cc-modal__overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.att-cc-modal__content {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
max-width: 700px;
|
||||
width: 90%;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.att-cc-modal__content h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.att-cc-modal__actions {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #ddd;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Code textarea */
|
||||
textarea.code {
|
||||
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
/* Color picker spacing */
|
||||
.att-cc-admin-wrap .wp-picker-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Feedback messages */
|
||||
.att-cc-feedback {
|
||||
padding: 8px 12px;
|
||||
margin: 10px 0;
|
||||
border-radius: 3px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.att-cc-feedback--success {
|
||||
background: #e7f5e7;
|
||||
border: 1px solid #2e7d32;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.att-cc-feedback--error {
|
||||
background: #f5e7e7;
|
||||
border: 1px solid #c62828;
|
||||
color: #c62828;
|
||||
}
|
||||
166
admin/js/admin.js
Normal file
166
admin/js/admin.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/* global jQuery, attCCAdmin */
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
$(function() {
|
||||
// Initialize color pickers.
|
||||
$('.att-cc-color-picker').wpColorPicker();
|
||||
|
||||
// Toggle gtag/GTM fields based on tracking mode.
|
||||
var $trackingMode = $('#tracking_mode');
|
||||
function toggleTrackingFields() {
|
||||
var mode = $trackingMode.val();
|
||||
$('.att-cc-gtag-row').toggle(mode === 'gtag');
|
||||
$('.att-cc-gtm-row').toggle(mode === 'gtm');
|
||||
}
|
||||
if ($trackingMode.length) {
|
||||
toggleTrackingFields();
|
||||
$trackingMode.on('change', toggleTrackingFields);
|
||||
}
|
||||
|
||||
// --- Custom Scripts CRUD ---
|
||||
|
||||
var $modal = $('#att-cc-script-modal');
|
||||
var $form = $('#att-cc-script-form');
|
||||
var $title = $('#att-cc-script-modal-title');
|
||||
var $scriptId = $('#att-cc-script-id');
|
||||
|
||||
// Open modal for new script.
|
||||
$('#att-cc-add-script').on('click', function() {
|
||||
resetForm();
|
||||
$title.text('Add Script');
|
||||
$modal.show();
|
||||
});
|
||||
|
||||
// Close modal.
|
||||
$('.att-cc-close-modal, .att-cc-modal__overlay').on('click', function() {
|
||||
$modal.hide();
|
||||
});
|
||||
|
||||
// Escape key closes modal.
|
||||
$(document).on('keydown', function(e) {
|
||||
if (e.key === 'Escape' && $modal.is(':visible')) {
|
||||
$modal.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Edit script.
|
||||
$(document).on('click', '.att-cc-edit-script', function() {
|
||||
var id = $(this).data('id');
|
||||
$.post(attCCAdmin.ajaxUrl, {
|
||||
action: 'att_cc_get_script',
|
||||
nonce: attCCAdmin.nonce,
|
||||
script_id: id,
|
||||
}, function(response) {
|
||||
if (response.success) {
|
||||
var s = response.data;
|
||||
$scriptId.val(s.id);
|
||||
$('#att-cc-script-name').val(s.name);
|
||||
$('#att-cc-script-category').val(s.category);
|
||||
$('#att-cc-script-placement').val(s.placement);
|
||||
$('#att-cc-script-priority').val(s.priority);
|
||||
$('#att-cc-script-status').val(s.status);
|
||||
$('#att-cc-script-snippet').val(s.snippet);
|
||||
$title.text('Edit Script');
|
||||
$modal.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete script.
|
||||
$(document).on('click', '.att-cc-delete-script', function() {
|
||||
if (!confirm(attCCAdmin.strings.confirmDelete)) {
|
||||
return;
|
||||
}
|
||||
var id = $(this).data('id');
|
||||
$.post(attCCAdmin.ajaxUrl, {
|
||||
action: 'att_cc_delete_script',
|
||||
nonce: attCCAdmin.nonce,
|
||||
script_id: id,
|
||||
}, function(response) {
|
||||
if (response.success) {
|
||||
refreshScriptsTable(response.data.scripts);
|
||||
} else {
|
||||
alert(attCCAdmin.strings.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Save script (form submit).
|
||||
$form.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var data = {
|
||||
action: 'att_cc_save_script',
|
||||
nonce: attCCAdmin.nonce,
|
||||
script_id: $scriptId.val(),
|
||||
name: $('#att-cc-script-name').val(),
|
||||
snippet: $('#att-cc-script-snippet').val(),
|
||||
category: $('#att-cc-script-category').val(),
|
||||
placement: $('#att-cc-script-placement').val(),
|
||||
status: $('#att-cc-script-status').val(),
|
||||
priority: $('#att-cc-script-priority').val(),
|
||||
};
|
||||
|
||||
$.post(attCCAdmin.ajaxUrl, data, function(response) {
|
||||
if (response.success) {
|
||||
$modal.hide();
|
||||
refreshScriptsTable(response.data.scripts);
|
||||
} else {
|
||||
alert(attCCAdmin.strings.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
$scriptId.val('');
|
||||
$('#att-cc-script-name').val('');
|
||||
$('#att-cc-script-category').val('analytics');
|
||||
$('#att-cc-script-placement').val('head');
|
||||
$('#att-cc-script-priority').val('10');
|
||||
$('#att-cc-script-status').val('active');
|
||||
$('#att-cc-script-snippet').val('');
|
||||
}
|
||||
|
||||
function refreshScriptsTable(scripts) {
|
||||
var $tbody = $('#att-cc-scripts-list');
|
||||
$tbody.empty();
|
||||
|
||||
if (!scripts || scripts.length === 0) {
|
||||
$tbody.append('<tr class="att-cc-no-scripts"><td colspan="6">No custom scripts added yet.</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
var categoryColors = {
|
||||
functional: 'functional',
|
||||
analytics: 'analytics',
|
||||
marketing: 'marketing',
|
||||
};
|
||||
|
||||
scripts.forEach(function(s) {
|
||||
var row = '<tr data-id="' + s.id + '">' +
|
||||
'<td class="column-name">' + escapeHtml(s.name) + '</td>' +
|
||||
'<td class="column-category"><span class="att-cc-badge att-cc-badge--' + s.category + '">' + capitalize(s.category) + '</span></td>' +
|
||||
'<td class="column-placement">' + capitalize(s.placement) + '</td>' +
|
||||
'<td class="column-priority">' + s.priority + '</td>' +
|
||||
'<td class="column-status"><span class="att-cc-status att-cc-status--' + s.status + '">' + capitalize(s.status) + '</span></td>' +
|
||||
'<td class="column-actions">' +
|
||||
'<button type="button" class="button button-small att-cc-edit-script" data-id="' + s.id + '">Edit</button> ' +
|
||||
'<button type="button" class="button button-small button-link-delete att-cc-delete-script" data-id="' + s.id + '">Delete</button>' +
|
||||
'</td>' +
|
||||
'</tr>';
|
||||
$tbody.append(row);
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(text));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
});
|
||||
})(jQuery);
|
||||
89
admin/views/settings-advanced.php
Normal file
89
admin/views/settings-advanced.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* Advanced settings tab.
|
||||
*
|
||||
* @package ATT_Consent
|
||||
* @var array $settings Current settings.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
|
||||
<?php wp_nonce_field( 'att_cc_save_settings' ); ?>
|
||||
<input type="hidden" name="action" value="att_cc_save_settings">
|
||||
<input type="hidden" name="att_cc_tab" value="advanced">
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'URL Passthrough', 'att-consent' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="url_passthrough" value="1" <?php checked( $settings['url_passthrough'] ); ?>>
|
||||
<?php esc_html_e( 'Enable URL passthrough', 'att-consent' ); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Passes ad click information (gclid, dclid) through URL parameters when navigating between pages, even before consent is granted. Recommended for better conversion attribution.', 'att-consent' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Ads Data Redaction', 'att-consent' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="ads_data_redaction" value="1" <?php checked( $settings['ads_data_redaction'] ); ?>>
|
||||
<?php esc_html_e( 'Enable ads data redaction', 'att-consent' ); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'When ad_storage is denied, redacts ad click identifiers sent in network requests. Provides additional privacy protection.', 'att-consent' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wait_for_update"><?php esc_html_e( 'Wait for Update (ms)', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="wait_for_update" id="wait_for_update"
|
||||
value="<?php echo esc_attr( $settings['wait_for_update'] ); ?>"
|
||||
class="small-text" min="100" max="10000" step="100">
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'How long Google tags wait for a consent update before firing with default (denied) state. 500ms is the recommended default. Only applies on first visit.', 'att-consent' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2><?php esc_html_e( 'Google Consent Mode v2 Signal Reference', 'att-consent' ); ?></h2>
|
||||
<table class="widefat fixed" style="max-width: 700px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Consent Category', 'att-consent' ); ?></th>
|
||||
<th><?php esc_html_e( 'Google Consent Signals', 'att-consent' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong><?php esc_html_e( 'Necessary', 'att-consent' ); ?></strong></td>
|
||||
<td><code>security_storage</code> <?php esc_html_e( '(always granted)', 'att-consent' ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong><?php esc_html_e( 'Functional', 'att-consent' ); ?></strong></td>
|
||||
<td><code>functionality_storage</code>, <code>personalization_storage</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong><?php esc_html_e( 'Analytics', 'att-consent' ); ?></strong></td>
|
||||
<td><code>analytics_storage</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong><?php esc_html_e( 'Marketing', 'att-consent' ); ?></strong></td>
|
||||
<td><code>ad_storage</code>, <code>ad_user_data</code>, <code>ad_personalization</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php submit_button(); ?>
|
||||
</form>
|
||||
118
admin/views/settings-appearance.php
Normal file
118
admin/views/settings-appearance.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/**
|
||||
* Appearance settings tab.
|
||||
*
|
||||
* @package ATT_Consent
|
||||
* @var array $settings Current settings.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
|
||||
<?php wp_nonce_field( 'att_cc_save_settings' ); ?>
|
||||
<input type="hidden" name="action" value="att_cc_save_settings">
|
||||
<input type="hidden" name="att_cc_tab" value="appearance">
|
||||
|
||||
<h2><?php esc_html_e( 'Banner Text', 'att-consent' ); ?></h2>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="banner_heading"><?php esc_html_e( 'Heading', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="banner_heading" id="banner_heading"
|
||||
value="<?php echo esc_attr( $settings['banner_heading'] ); ?>"
|
||||
class="large-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="banner_message"><?php esc_html_e( 'Message', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea name="banner_message" id="banner_message" rows="4"
|
||||
class="large-text"><?php echo esc_textarea( $settings['banner_message'] ); ?></textarea>
|
||||
<p class="description"><?php esc_html_e( 'Basic HTML is allowed (links, bold, italic).', 'att-consent' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="btn_accept_label"><?php esc_html_e( 'Accept Button Label', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="btn_accept_label" id="btn_accept_label"
|
||||
value="<?php echo esc_attr( $settings['btn_accept_label'] ); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="btn_reject_label"><?php esc_html_e( 'Reject Button Label', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="btn_reject_label" id="btn_reject_label"
|
||||
value="<?php echo esc_attr( $settings['btn_reject_label'] ); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="btn_preferences_label"><?php esc_html_e( 'Preferences Button Label', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="btn_preferences_label" id="btn_preferences_label"
|
||||
value="<?php echo esc_attr( $settings['btn_preferences_label'] ); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="btn_save_label"><?php esc_html_e( 'Save Preferences Label', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="btn_save_label" id="btn_save_label"
|
||||
value="<?php echo esc_attr( $settings['btn_save_label'] ); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2><?php esc_html_e( 'Colours', 'att-consent' ); ?></h2>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Banner Background', 'att-consent' ); ?></th>
|
||||
<td><input type="text" name="banner_bg_color" value="<?php echo esc_attr( $settings['banner_bg_color'] ); ?>" class="att-cc-color-picker"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Banner Text', 'att-consent' ); ?></th>
|
||||
<td><input type="text" name="banner_text_color" value="<?php echo esc_attr( $settings['banner_text_color'] ); ?>" class="att-cc-color-picker"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Accept Button Background', 'att-consent' ); ?></th>
|
||||
<td><input type="text" name="btn_accept_bg" value="<?php echo esc_attr( $settings['btn_accept_bg'] ); ?>" class="att-cc-color-picker"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Accept Button Text', 'att-consent' ); ?></th>
|
||||
<td><input type="text" name="btn_accept_text" value="<?php echo esc_attr( $settings['btn_accept_text'] ); ?>" class="att-cc-color-picker"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Reject Button Background', 'att-consent' ); ?></th>
|
||||
<td><input type="text" name="btn_reject_bg" value="<?php echo esc_attr( $settings['btn_reject_bg'] ); ?>" class="att-cc-color-picker"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Reject Button Text', 'att-consent' ); ?></th>
|
||||
<td><input type="text" name="btn_reject_text" value="<?php echo esc_attr( $settings['btn_reject_text'] ); ?>" class="att-cc-color-picker"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Preferences Button Background', 'att-consent' ); ?></th>
|
||||
<td>
|
||||
<input type="text" name="btn_preferences_bg" value="<?php echo esc_attr( $settings['btn_preferences_bg'] ); ?>" class="att-cc-color-picker">
|
||||
<p class="description"><?php esc_html_e( 'Use "transparent" for an outline-style button.', 'att-consent' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Preferences Button Text', 'att-consent' ); ?></th>
|
||||
<td><input type="text" name="btn_preferences_text" value="<?php echo esc_attr( $settings['btn_preferences_text'] ); ?>" class="att-cc-color-picker"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button(); ?>
|
||||
</form>
|
||||
64
admin/views/settings-categories.php
Normal file
64
admin/views/settings-categories.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/**
|
||||
* Categories settings tab.
|
||||
*
|
||||
* @package ATT_Consent
|
||||
* @var array $settings Current settings.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
|
||||
<?php wp_nonce_field( 'att_cc_save_settings' ); ?>
|
||||
<input type="hidden" name="action" value="att_cc_save_settings">
|
||||
<input type="hidden" name="att_cc_tab" value="categories">
|
||||
|
||||
<p><?php esc_html_e( 'Customise the descriptions shown to visitors for each cookie category in the preferences modal.', 'att-consent' ); ?></p>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="cat_necessary_desc"><?php esc_html_e( 'Necessary', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea name="cat_necessary_desc" id="cat_necessary_desc" rows="3"
|
||||
class="large-text"><?php echo esc_textarea( $settings['cat_necessary_desc'] ); ?></textarea>
|
||||
<p class="description"><?php esc_html_e( 'Always enabled. Maps to: security_storage.', 'att-consent' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="cat_functional_desc"><?php esc_html_e( 'Functional', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea name="cat_functional_desc" id="cat_functional_desc" rows="3"
|
||||
class="large-text"><?php echo esc_textarea( $settings['cat_functional_desc'] ); ?></textarea>
|
||||
<p class="description"><?php esc_html_e( 'Maps to: functionality_storage, personalization_storage.', 'att-consent' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="cat_analytics_desc"><?php esc_html_e( 'Analytics', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea name="cat_analytics_desc" id="cat_analytics_desc" rows="3"
|
||||
class="large-text"><?php echo esc_textarea( $settings['cat_analytics_desc'] ); ?></textarea>
|
||||
<p class="description"><?php esc_html_e( 'Maps to: analytics_storage.', 'att-consent' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="cat_marketing_desc"><?php esc_html_e( 'Marketing', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea name="cat_marketing_desc" id="cat_marketing_desc" rows="3"
|
||||
class="large-text"><?php echo esc_textarea( $settings['cat_marketing_desc'] ); ?></textarea>
|
||||
<p class="description"><?php esc_html_e( 'Maps to: ad_storage, ad_user_data, ad_personalization.', 'att-consent' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button(); ?>
|
||||
</form>
|
||||
120
admin/views/settings-general.php
Normal file
120
admin/views/settings-general.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
/**
|
||||
* General settings tab.
|
||||
*
|
||||
* @package ATT_Consent
|
||||
* @var array $settings Current settings.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
|
||||
<?php wp_nonce_field( 'att_cc_save_settings' ); ?>
|
||||
<input type="hidden" name="action" value="att_cc_save_settings">
|
||||
<input type="hidden" name="att_cc_tab" value="general">
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="tracking_mode"><?php esc_html_e( 'Tracking Mode', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="tracking_mode" id="tracking_mode">
|
||||
<option value="gtag" <?php selected( $settings['tracking_mode'], 'gtag' ); ?>>
|
||||
<?php esc_html_e( 'Google Analytics (gtag.js)', 'att-consent' ); ?>
|
||||
</option>
|
||||
<option value="gtm" <?php selected( $settings['tracking_mode'], 'gtm' ); ?>>
|
||||
<?php esc_html_e( 'Google Tag Manager', 'att-consent' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Choose whether to inject gtag.js directly or use a GTM container.', 'att-consent' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="att-cc-gtag-row">
|
||||
<th scope="row">
|
||||
<label for="ga4_measurement_id"><?php esc_html_e( 'GA4 Measurement ID', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="ga4_measurement_id" id="ga4_measurement_id"
|
||||
value="<?php echo esc_attr( $settings['ga4_measurement_id'] ); ?>"
|
||||
class="regular-text" placeholder="G-XXXXXXXXXX">
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Your GA4 Measurement ID (starts with G-).', 'att-consent' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="att-cc-gtm-row">
|
||||
<th scope="row">
|
||||
<label for="gtm_container_id"><?php esc_html_e( 'GTM Container ID', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="gtm_container_id" id="gtm_container_id"
|
||||
value="<?php echo esc_attr( $settings['gtm_container_id'] ); ?>"
|
||||
class="regular-text" placeholder="GTM-XXXXXXX">
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Your Google Tag Manager Container ID (starts with GTM-).', 'att-consent' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="consent_mode"><?php esc_html_e( 'Consent Mode', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="consent_mode" id="consent_mode">
|
||||
<option value="advanced" <?php selected( $settings['consent_mode'], 'advanced' ); ?>>
|
||||
<?php esc_html_e( 'Advanced (recommended)', 'att-consent' ); ?>
|
||||
</option>
|
||||
<option value="basic" <?php selected( $settings['consent_mode'], 'basic' ); ?>>
|
||||
<?php esc_html_e( 'Basic', 'att-consent' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Advanced: Tags load before consent, sending cookieless pings for better modelling. Basic: No tags load until consent is given.', 'att-consent' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="banner_position"><?php esc_html_e( 'Banner Position', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="banner_position" id="banner_position">
|
||||
<option value="bottom" <?php selected( $settings['banner_position'], 'bottom' ); ?>>
|
||||
<?php esc_html_e( 'Bottom Bar', 'att-consent' ); ?>
|
||||
</option>
|
||||
<option value="top" <?php selected( $settings['banner_position'], 'top' ); ?>>
|
||||
<?php esc_html_e( 'Top Bar', 'att-consent' ); ?>
|
||||
</option>
|
||||
<option value="center" <?php selected( $settings['banner_position'], 'center' ); ?>>
|
||||
<?php esc_html_e( 'Centre Modal', 'att-consent' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="consent_expiry"><?php esc_html_e( 'Consent Expiry (days)', 'att-consent' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="consent_expiry" id="consent_expiry"
|
||||
value="<?php echo esc_attr( $settings['consent_expiry'] ); ?>"
|
||||
class="small-text" min="1" max="730">
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'How long the consent cookie lasts before the user is asked again.', 'att-consent' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button(); ?>
|
||||
</form>
|
||||
133
admin/views/settings-scripts.php
Normal file
133
admin/views/settings-scripts.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
/**
|
||||
* Custom Scripts tab.
|
||||
*
|
||||
* @package ATT_Consent
|
||||
* @var array $settings Current settings.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$scripts = ATT_Consent_Scripts_Manager::get_all_scripts();
|
||||
?>
|
||||
<div class="att-cc-scripts-wrap">
|
||||
<h2>
|
||||
<?php esc_html_e( 'Custom Scripts', 'att-consent' ); ?>
|
||||
<button type="button" class="page-title-action" id="att-cc-add-script">
|
||||
<?php esc_html_e( 'Add New Script', 'att-consent' ); ?>
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<p><?php esc_html_e( 'Add custom JavaScript snippets that will only execute after the visitor consents to the relevant category. This is useful for conversion pixels, chat widgets, and other third-party scripts.', 'att-consent' ); ?></p>
|
||||
|
||||
<!-- Scripts list -->
|
||||
<table class="wp-list-table widefat fixed striped" id="att-cc-scripts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="column-name"><?php esc_html_e( 'Name', 'att-consent' ); ?></th>
|
||||
<th class="column-category"><?php esc_html_e( 'Category', 'att-consent' ); ?></th>
|
||||
<th class="column-placement"><?php esc_html_e( 'Placement', 'att-consent' ); ?></th>
|
||||
<th class="column-priority"><?php esc_html_e( 'Priority', 'att-consent' ); ?></th>
|
||||
<th class="column-status"><?php esc_html_e( 'Status', 'att-consent' ); ?></th>
|
||||
<th class="column-actions"><?php esc_html_e( 'Actions', 'att-consent' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="att-cc-scripts-list">
|
||||
<?php if ( empty( $scripts ) ) : ?>
|
||||
<tr class="att-cc-no-scripts">
|
||||
<td colspan="6"><?php esc_html_e( 'No custom scripts added yet.', 'att-consent' ); ?></td>
|
||||
</tr>
|
||||
<?php else : ?>
|
||||
<?php foreach ( $scripts as $script ) : ?>
|
||||
<tr data-id="<?php echo esc_attr( $script['id'] ); ?>">
|
||||
<td class="column-name"><?php echo esc_html( $script['name'] ); ?></td>
|
||||
<td class="column-category"><span class="att-cc-badge att-cc-badge--<?php echo esc_attr( $script['category'] ); ?>"><?php echo esc_html( ucfirst( $script['category'] ) ); ?></span></td>
|
||||
<td class="column-placement"><?php echo esc_html( ucfirst( $script['placement'] ) ); ?></td>
|
||||
<td class="column-priority"><?php echo esc_html( $script['priority'] ); ?></td>
|
||||
<td class="column-status">
|
||||
<span class="att-cc-status att-cc-status--<?php echo esc_attr( $script['status'] ); ?>">
|
||||
<?php echo esc_html( ucfirst( $script['status'] ) ); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="column-actions">
|
||||
<button type="button" class="button button-small att-cc-edit-script" data-id="<?php echo esc_attr( $script['id'] ); ?>">
|
||||
<?php esc_html_e( 'Edit', 'att-consent' ); ?>
|
||||
</button>
|
||||
<button type="button" class="button button-small button-link-delete att-cc-delete-script" data-id="<?php echo esc_attr( $script['id'] ); ?>">
|
||||
<?php esc_html_e( 'Delete', 'att-consent' ); ?>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Add/Edit modal -->
|
||||
<div id="att-cc-script-modal" class="att-cc-modal" style="display:none;">
|
||||
<div class="att-cc-modal__overlay"></div>
|
||||
<div class="att-cc-modal__content">
|
||||
<h2 id="att-cc-script-modal-title"><?php esc_html_e( 'Add Script', 'att-consent' ); ?></h2>
|
||||
<form id="att-cc-script-form">
|
||||
<input type="hidden" name="script_id" id="att-cc-script-id" value="">
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label for="att-cc-script-name"><?php esc_html_e( 'Name', 'att-consent' ); ?></label></th>
|
||||
<td><input type="text" id="att-cc-script-name" name="name" class="regular-text" required></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="att-cc-script-category"><?php esc_html_e( 'Category', 'att-consent' ); ?></label></th>
|
||||
<td>
|
||||
<select id="att-cc-script-category" name="category">
|
||||
<option value="functional"><?php esc_html_e( 'Functional', 'att-consent' ); ?></option>
|
||||
<option value="analytics" selected><?php esc_html_e( 'Analytics', 'att-consent' ); ?></option>
|
||||
<option value="marketing"><?php esc_html_e( 'Marketing', 'att-consent' ); ?></option>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e( 'The script will only execute after the visitor consents to this category.', 'att-consent' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="att-cc-script-placement"><?php esc_html_e( 'Placement', 'att-consent' ); ?></label></th>
|
||||
<td>
|
||||
<select id="att-cc-script-placement" name="placement">
|
||||
<option value="head"><?php esc_html_e( 'Head', 'att-consent' ); ?></option>
|
||||
<option value="footer"><?php esc_html_e( 'Footer', 'att-consent' ); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="att-cc-script-priority"><?php esc_html_e( 'Priority', 'att-consent' ); ?></label></th>
|
||||
<td>
|
||||
<input type="number" id="att-cc-script-priority" name="priority" value="10" min="1" max="100" class="small-text">
|
||||
<p class="description"><?php esc_html_e( 'Lower numbers execute first.', 'att-consent' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="att-cc-script-status"><?php esc_html_e( 'Status', 'att-consent' ); ?></label></th>
|
||||
<td>
|
||||
<select id="att-cc-script-status" name="status">
|
||||
<option value="active"><?php esc_html_e( 'Active', 'att-consent' ); ?></option>
|
||||
<option value="inactive"><?php esc_html_e( 'Inactive', 'att-consent' ); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="att-cc-script-snippet"><?php esc_html_e( 'Script Code', 'att-consent' ); ?></label></th>
|
||||
<td>
|
||||
<textarea id="att-cc-script-snippet" name="snippet" rows="10" class="large-text code" placeholder="<script> // Your code here </script>"></textarea>
|
||||
<p class="description"><?php esc_html_e( 'Include the full script tag(s). This code will be injected into the page after consent is given for the selected category.', 'att-consent' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="att-cc-modal__actions">
|
||||
<button type="submit" class="button button-primary"><?php esc_html_e( 'Save Script', 'att-consent' ); ?></button>
|
||||
<button type="button" class="button att-cc-close-modal"><?php esc_html_e( 'Cancel', 'att-consent' ); ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
31
att-consent.php
Normal file
31
att-consent.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: ATT Consent
|
||||
* Plugin URI: https://github.com/att-consent/att-consent
|
||||
* Description: Google Consent Mode v2 cookie consent with session attribution preservation, custom script management, and full gtag.js/GTM support.
|
||||
* Version: 1.0.0
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 7.4
|
||||
* Author: ATT Consent
|
||||
* Author URI: https://github.com/att-consent
|
||||
* License: GPL-2.0-or-later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Text Domain: att-consent
|
||||
* Domain Path: /languages
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'ATT_CC_VERSION', '1.0.0' );
|
||||
define( 'ATT_CC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'ATT_CC_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'ATT_CC_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||
|
||||
require_once ATT_CC_PLUGIN_DIR . 'includes/class-att-consent.php';
|
||||
|
||||
register_activation_hook( __FILE__, array( 'ATT_Consent', 'activate' ) );
|
||||
register_deactivation_hook( __FILE__, array( 'ATT_Consent', 'deactivate' ) );
|
||||
|
||||
ATT_Consent::get_instance();
|
||||
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;
|
||||
}
|
||||
}
|
||||
272
public/css/banner.css
Normal file
272
public/css/banner.css
Normal file
@@ -0,0 +1,272 @@
|
||||
/* ATT Consent - Banner & Modal Styles */
|
||||
|
||||
/* Reset box-sizing for our elements */
|
||||
#att-cc-banner,
|
||||
#att-cc-banner *,
|
||||
#att-cc-modal,
|
||||
#att-cc-modal * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* --- Banner --- */
|
||||
|
||||
#att-cc-banner {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999999;
|
||||
background: var(--att-cc-bg, #1a1a2e);
|
||||
color: var(--att-cc-text, #ffffff);
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
#att-cc-banner.att-cc-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Position variants */
|
||||
#att-cc-banner.att-cc-position-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#att-cc-banner.att-cc-position-top {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#att-cc-banner.att-cc-position-center {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 600px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.att-cc-banner__inner {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.att-cc-position-center .att-cc-banner__inner {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.att-cc-banner__content {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.att-cc-banner__heading {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 6px 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.att-cc-banner__message {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.att-cc-banner__message a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.att-cc-banner__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.att-cc-position-center .att-cc-banner__actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.att-cc-position-center .att-cc-btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* --- Buttons --- */
|
||||
|
||||
.att-cc-btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: opacity 0.2s, box-shadow 0.2s;
|
||||
line-height: 1.4;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.att-cc-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.att-cc-btn:focus-visible {
|
||||
outline: 2px solid #fff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.att-cc-btn--accept {
|
||||
background: var(--att-cc-accept-bg, #4CAF50);
|
||||
color: var(--att-cc-accept-text, #ffffff);
|
||||
}
|
||||
|
||||
.att-cc-btn--reject {
|
||||
background: var(--att-cc-reject-bg, #555555);
|
||||
color: var(--att-cc-reject-text, #ffffff);
|
||||
}
|
||||
|
||||
.att-cc-btn--preferences {
|
||||
background: var(--att-cc-pref-bg, transparent);
|
||||
color: var(--att-cc-pref-text, #ffffff);
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
|
||||
/* --- Preferences Modal --- */
|
||||
|
||||
#att-cc-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000000;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#att-cc-modal.att-cc-modal--open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.att-cc-modal__overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.att-cc-modal__dialog {
|
||||
position: relative;
|
||||
background: var(--att-cc-bg, #1a1a2e);
|
||||
color: var(--att-cc-text, #ffffff);
|
||||
border-radius: 12px;
|
||||
padding: 28px;
|
||||
max-width: 540px;
|
||||
width: 90%;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.att-cc-modal__heading {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 20px 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.att-cc-modal__category {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 14px 0;
|
||||
}
|
||||
|
||||
.att-cc-modal__category:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.att-cc-modal__cat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.att-cc-modal__cat-header label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.att-cc-modal__cat-header input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--att-cc-accept-bg, #4CAF50);
|
||||
}
|
||||
|
||||
.att-cc-always-on {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
opacity: 0.6;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.att-cc-modal__cat-desc {
|
||||
margin: 6px 0 0 26px;
|
||||
font-size: 12px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.att-cc-modal__actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.att-cc-modal__actions .att-cc-btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Prevent body scroll when modal is open */
|
||||
body.att-cc-modal-active {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* --- Responsive --- */
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.att-cc-banner__inner {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.att-cc-banner__actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.att-cc-btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.att-cc-modal__dialog {
|
||||
width: 95%;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
174
public/js/banner.js
Normal file
174
public/js/banner.js
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* ATT Consent Banner UI
|
||||
*
|
||||
* Handles banner display, preferences modal, focus trapping, and user interactions.
|
||||
* Depends on AttConsent (consent-manager.js) for consent state management.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var banner = document.getElementById('att-cc-banner');
|
||||
var modal = document.getElementById('att-cc-modal');
|
||||
|
||||
if (!banner || !modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If consent already exists, hide the banner immediately.
|
||||
if (AttConsent.hasConsent()) {
|
||||
hideBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Banner buttons ---
|
||||
|
||||
var acceptBtn = banner.querySelector('[data-att-cc="accept-all"]');
|
||||
var rejectBtn = banner.querySelector('[data-att-cc="reject-all"]');
|
||||
var prefsBtn = banner.querySelector('[data-att-cc="preferences"]');
|
||||
|
||||
if (acceptBtn) {
|
||||
acceptBtn.addEventListener('click', function() {
|
||||
AttConsent.update({ functional: true, analytics: true, marketing: true });
|
||||
hideBanner();
|
||||
});
|
||||
}
|
||||
|
||||
if (rejectBtn) {
|
||||
rejectBtn.addEventListener('click', function() {
|
||||
AttConsent.update({ functional: false, analytics: false, marketing: false });
|
||||
hideBanner();
|
||||
});
|
||||
}
|
||||
|
||||
if (prefsBtn) {
|
||||
prefsBtn.addEventListener('click', function() {
|
||||
openModal();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Modal buttons ---
|
||||
|
||||
var saveBtn = modal.querySelector('[data-att-cc="save-preferences"]');
|
||||
var closeBtn = modal.querySelector('[data-att-cc="close-modal"]');
|
||||
var overlay = modal.querySelector('.att-cc-modal__overlay');
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', function() {
|
||||
var categories = {
|
||||
functional: document.getElementById('att-cc-functional').checked,
|
||||
analytics: document.getElementById('att-cc-analytics').checked,
|
||||
marketing: document.getElementById('att-cc-marketing').checked
|
||||
};
|
||||
AttConsent.update(categories);
|
||||
closeModal();
|
||||
hideBanner();
|
||||
});
|
||||
}
|
||||
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', function() {
|
||||
closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', function() {
|
||||
closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Helper functions ---
|
||||
|
||||
function hideBanner() {
|
||||
banner.setAttribute('aria-hidden', 'true');
|
||||
banner.classList.add('att-cc-hidden');
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
// Pre-fill checkboxes with existing consent if available.
|
||||
var existing = AttConsent.getConsent();
|
||||
if (existing) {
|
||||
setCheckbox('att-cc-functional', existing.functional);
|
||||
setCheckbox('att-cc-analytics', existing.analytics);
|
||||
setCheckbox('att-cc-marketing', existing.marketing);
|
||||
}
|
||||
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
modal.classList.add('att-cc-modal--open');
|
||||
document.body.classList.add('att-cc-modal-active');
|
||||
trapFocus(modal);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
modal.classList.remove('att-cc-modal--open');
|
||||
document.body.classList.remove('att-cc-modal-active');
|
||||
|
||||
// Return focus to the preferences button.
|
||||
if (prefsBtn) {
|
||||
prefsBtn.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function setCheckbox(id, value) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.checked = !!value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trap keyboard focus within an element.
|
||||
*
|
||||
* @param {HTMLElement} element The container to trap focus in.
|
||||
*/
|
||||
function trapFocus(element) {
|
||||
var focusable = element.querySelectorAll(
|
||||
'button, [href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
if (focusable.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var first = focusable[0];
|
||||
var last = focusable[focusable.length - 1];
|
||||
|
||||
first.focus();
|
||||
|
||||
function handleKeydown(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
element.removeEventListener('keydown', handleKeydown);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key !== 'Tab') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === first) {
|
||||
last.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === last) {
|
||||
first.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
element.addEventListener('keydown', handleKeydown);
|
||||
}
|
||||
|
||||
// Listen for programmatic consent revocation to re-show banner.
|
||||
document.addEventListener('att_consent_update', function(e) {
|
||||
var cats = e.detail;
|
||||
if (!cats.functional && !cats.analytics && !cats.marketing) {
|
||||
// All denied - consent was effectively revoked, but don't re-show
|
||||
// banner immediately (user just clicked reject).
|
||||
}
|
||||
});
|
||||
})();
|
||||
259
public/js/consent-manager.js
Normal file
259
public/js/consent-manager.js
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* ATT Consent Manager
|
||||
*
|
||||
* Handles Google Consent Mode v2 updates, session attribution replay,
|
||||
* custom script injection, and consent state persistence.
|
||||
*
|
||||
* Phase 1 (consent defaults + attribution capture) runs inline in <head>.
|
||||
* This file handles Phase 2 (consent updates) and Phase 3 (basic mode tag loading).
|
||||
*/
|
||||
var AttConsent = (function() {
|
||||
'use strict';
|
||||
|
||||
var gtag = window.attCCGtag;
|
||||
var config = window.attCCConfig || {};
|
||||
|
||||
/**
|
||||
* Update consent state.
|
||||
*
|
||||
* @param {Object} categories - { functional: bool, analytics: bool, marketing: bool }
|
||||
*/
|
||||
function update(categories) {
|
||||
// Step 1: Replay stored attribution BEFORE consent update.
|
||||
// This ensures GA4 session_start gets correct source/medium.
|
||||
if (!sessionStorage.getItem('att_cc_consent_given')) {
|
||||
replayAttribution();
|
||||
sessionStorage.setItem('att_cc_consent_given', '1');
|
||||
}
|
||||
|
||||
// Step 2: Send consent update to Google.
|
||||
gtag('consent', 'update', {
|
||||
ad_storage: categories.marketing ? 'granted' : 'denied',
|
||||
analytics_storage: categories.analytics ? 'granted' : 'denied',
|
||||
ad_user_data: categories.marketing ? 'granted' : 'denied',
|
||||
ad_personalization: categories.marketing ? 'granted' : 'denied',
|
||||
functionality_storage: categories.functional ? 'granted' : 'denied',
|
||||
personalization_storage: categories.functional ? 'granted' : 'denied'
|
||||
});
|
||||
|
||||
// Step 3: Store consent in first-party cookie.
|
||||
setConsentCookie(categories);
|
||||
|
||||
// Step 4: In basic mode, inject tracking script if analytics or marketing consented.
|
||||
if (config.consent_mode === 'basic' && (categories.analytics || categories.marketing)) {
|
||||
injectTrackingScript();
|
||||
}
|
||||
|
||||
// Step 5: Execute custom scripts for consented categories.
|
||||
executeConsentedScripts(categories);
|
||||
|
||||
// Step 6: Fire custom event for theme/plugin integration.
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent('att_consent_update', {
|
||||
detail: categories
|
||||
}));
|
||||
} catch (e) {
|
||||
// IE11 fallback (if ever needed).
|
||||
var evt = document.createEvent('CustomEvent');
|
||||
evt.initCustomEvent('att_consent_update', true, true, categories);
|
||||
document.dispatchEvent(evt);
|
||||
}
|
||||
|
||||
// Step 7: WP Consent API integration.
|
||||
if (typeof wp_set_consent === 'function') {
|
||||
wp_set_consent('functional', categories.functional ? 'allow' : 'deny');
|
||||
wp_set_consent('statistics', categories.analytics ? 'allow' : 'deny');
|
||||
wp_set_consent('marketing', categories.marketing ? 'allow' : 'deny');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replay stored attribution data via gtag('set').
|
||||
* Called BEFORE the consent update so GA4 picks up the original
|
||||
* campaign parameters on the session_start event.
|
||||
*/
|
||||
function replayAttribution() {
|
||||
var stored = sessionStorage.getItem('att_cc_attr');
|
||||
if (!stored) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var a = JSON.parse(stored);
|
||||
var params = {};
|
||||
|
||||
if (a.us) { params.campaign_source = a.us; }
|
||||
if (a.um) { params.campaign_medium = a.um; }
|
||||
if (a.uc) { params.campaign_name = a.uc; }
|
||||
if (a.ut) { params.campaign_term = a.ut; }
|
||||
if (a.uo) { params.campaign_content = a.uo; }
|
||||
|
||||
if (Object.keys(params).length > 0) {
|
||||
gtag('set', params);
|
||||
}
|
||||
} catch (e) {
|
||||
// Silent failure - don't block consent.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store consent choices in a first-party cookie.
|
||||
*
|
||||
* @param {Object} categories Consent categories.
|
||||
*/
|
||||
function setConsentCookie(categories) {
|
||||
var days = config.consent_expiry || 365;
|
||||
var expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||||
var value = encodeURIComponent(JSON.stringify(categories));
|
||||
var cookie = 'att_cc_consent=' + value +
|
||||
'; expires=' + expires +
|
||||
'; path=/; SameSite=Lax';
|
||||
|
||||
if (location.protocol === 'https:') {
|
||||
cookie += '; Secure';
|
||||
}
|
||||
|
||||
document.cookie = cookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject the tracking script in basic mode (deferred until consent).
|
||||
*/
|
||||
function injectTrackingScript() {
|
||||
if (!config.tracking_snippet || window.attCCTrackingLoaded) {
|
||||
return;
|
||||
}
|
||||
window.attCCTrackingLoaded = true;
|
||||
injectHTML(config.tracking_snippet, document.head);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute custom scripts for the consented categories.
|
||||
*
|
||||
* @param {Object} categories Consent categories.
|
||||
*/
|
||||
function executeConsentedScripts(categories) {
|
||||
var scripts = config.scripts || {};
|
||||
|
||||
Object.keys(categories).forEach(function(cat) {
|
||||
if (!categories[cat] || !scripts[cat]) {
|
||||
return;
|
||||
}
|
||||
|
||||
['head', 'footer'].forEach(function(placement) {
|
||||
if (!scripts[cat][placement] || !scripts[cat][placement].length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var container = placement === 'head' ? document.head : document.body;
|
||||
|
||||
scripts[cat][placement].forEach(function(snippet) {
|
||||
injectHTML(snippet, container);
|
||||
});
|
||||
|
||||
// Clear to prevent double execution.
|
||||
scripts[cat][placement] = [];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject an HTML string (potentially containing <script> tags) into a container.
|
||||
* Script tags inserted via innerHTML don't execute, so we create fresh elements.
|
||||
*
|
||||
* @param {string} html The HTML/script string to inject.
|
||||
* @param {HTMLElement} container Target container element.
|
||||
*/
|
||||
function injectHTML(html, container) {
|
||||
var temp = document.createElement('div');
|
||||
temp.innerHTML = html;
|
||||
|
||||
var nodes = Array.prototype.slice.call(temp.childNodes);
|
||||
nodes.forEach(function(node) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
// Skip whitespace-only text nodes.
|
||||
if (node.textContent.trim()) {
|
||||
container.appendChild(node.cloneNode(true));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.nodeName === 'SCRIPT') {
|
||||
var script = document.createElement('script');
|
||||
|
||||
// Copy all attributes.
|
||||
Array.prototype.slice.call(node.attributes).forEach(function(attr) {
|
||||
script.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
|
||||
if (!node.src) {
|
||||
script.textContent = node.textContent;
|
||||
}
|
||||
|
||||
container.appendChild(script);
|
||||
} else {
|
||||
container.appendChild(node.cloneNode(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current consent state from the cookie.
|
||||
*
|
||||
* @return {Object|null} The consent categories or null if not set.
|
||||
*/
|
||||
function getConsent() {
|
||||
var match = document.cookie.match(/(?:^|; )att_cc_consent=([^;]*)/);
|
||||
if (match) {
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(match[1]));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the user has already given consent.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
function hasConsent() {
|
||||
return !!getConsent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke consent (clear cookie, reset to denied).
|
||||
*/
|
||||
function revoke() {
|
||||
document.cookie = 'att_cc_consent=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax';
|
||||
sessionStorage.removeItem('att_cc_consent_given');
|
||||
window.attCCHasConsent = false;
|
||||
|
||||
gtag('consent', 'update', {
|
||||
ad_storage: 'denied',
|
||||
analytics_storage: 'denied',
|
||||
ad_user_data: 'denied',
|
||||
ad_personalization: 'denied',
|
||||
functionality_storage: 'denied',
|
||||
personalization_storage: 'denied'
|
||||
});
|
||||
}
|
||||
|
||||
// If consent already exists (return visitor), execute scripts for consented categories.
|
||||
if (window.attCCHasConsent) {
|
||||
var existing = getConsent();
|
||||
if (existing) {
|
||||
executeConsentedScripts(existing);
|
||||
}
|
||||
}
|
||||
|
||||
// Public API.
|
||||
return {
|
||||
update: update,
|
||||
getConsent: getConsent,
|
||||
hasConsent: hasConsent,
|
||||
revoke: revoke
|
||||
};
|
||||
})();
|
||||
80
readme.txt
Normal file
80
readme.txt
Normal file
@@ -0,0 +1,80 @@
|
||||
=== ATT Consent ===
|
||||
Contributors: attconsent
|
||||
Tags: cookie consent, gdpr, consent mode, google analytics, gtm
|
||||
Requires at least: 6.0
|
||||
Tested up to: 6.8
|
||||
Stable tag: 1.0.0
|
||||
Requires PHP: 7.4
|
||||
License: GPLv2 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
Google Consent Mode v2 cookie consent with session attribution preservation, custom script management, and full gtag.js/GTM support.
|
||||
|
||||
== Description ==
|
||||
|
||||
ATT Consent is a lightweight, self-contained cookie consent plugin that fully implements Google Consent Mode v2. It manages your cookie banner, sends the correct consent signals to Google, and preserves session attribution data even when users delay their consent decision.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
* **Google Consent Mode v2** - Full implementation of all consent signals (ad_storage, analytics_storage, ad_user_data, ad_personalization, functionality_storage, personalization_storage)
|
||||
* **Session Attribution Preservation** - Captures UTM parameters, GCLID, and referrer data on first page load. When consent is granted (even pages later), replays this data so GA4 session source/medium is correct
|
||||
* **Basic & Advanced Consent Mode** - Choose between Advanced (tags load before consent, cookieless pings) or Basic (no tags until consent)
|
||||
* **gtag.js & GTM Support** - Works with direct Google Analytics integration or Google Tag Manager
|
||||
* **Custom Script Management** - Add JavaScript snippets via the admin that only execute after consent for their assigned category
|
||||
* **4 Consent Categories** - Necessary (always on), Functional, Analytics, Marketing
|
||||
* **Customisable Banner** - Colours, text, position (top bar, bottom bar, centre modal)
|
||||
* **Accessible** - Keyboard navigation, focus trapping, ARIA attributes, screen reader friendly
|
||||
* **WP Consent API Compatible** - Integrates with the WordPress Consent API for plugin interoperability
|
||||
* **No External Dependencies** - Vanilla JavaScript frontend, no jQuery requirement
|
||||
* **Caching Compatible** - Works correctly with page caching plugins
|
||||
* **Translation Ready** - All strings are translatable
|
||||
|
||||
**How Attribution Preservation Works:**
|
||||
|
||||
1. On the first page of a session, UTM parameters, GCLID/DCLID, and the referrer are captured into sessionStorage (before any consent)
|
||||
2. When the user grants consent (even on a subsequent page), this stored attribution data is replayed via `gtag('set')` before the consent update fires
|
||||
3. This ensures GA4's `session_start` event records the correct traffic source, not "(direct) / (none)"
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. Upload the `att-consent` folder to `/wp-content/plugins/`
|
||||
2. Activate the plugin through the 'Plugins' menu in WordPress
|
||||
3. Go to Cookie Consent > General and enter your GA4 Measurement ID or GTM Container ID
|
||||
4. Customise the banner text and appearance under the Appearance tab
|
||||
5. Optionally add custom scripts under the Custom Scripts tab
|
||||
|
||||
== Frequently Asked Questions ==
|
||||
|
||||
= What is the difference between Basic and Advanced consent mode? =
|
||||
|
||||
**Advanced mode** (recommended): Google tags load immediately and send cookieless pings even before consent. This gives Google data for behavioural modelling while still respecting consent. Most sites should use this.
|
||||
|
||||
**Basic mode**: No Google tags load until consent is granted. Maximum privacy but you lose behavioural modelling data.
|
||||
|
||||
= Does this work with page caching? =
|
||||
|
||||
Yes. The banner HTML is always rendered on the page. JavaScript checks for the consent cookie and hides the banner if consent has already been given. This means cached pages display correctly.
|
||||
|
||||
= Can I use this with Google Tag Manager? =
|
||||
|
||||
Yes. Set the tracking mode to GTM in the General settings and enter your container ID. The plugin will inject the GTM container and manage consent signals via the dataLayer.
|
||||
|
||||
= How do custom scripts work? =
|
||||
|
||||
You can add JavaScript snippets in the Custom Scripts tab. Each snippet is assigned to a consent category (Functional, Analytics, or Marketing) and a placement (Head or Footer). The script only executes after the visitor consents to that category.
|
||||
|
||||
= Does this replace my existing consent plugin? =
|
||||
|
||||
Yes. Deactivate and remove your existing consent plugin (e.g., CookieYes, Complianz) before activating ATT Consent to avoid conflicts.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 1.0.0 =
|
||||
* Initial release
|
||||
* Google Consent Mode v2 with all signals
|
||||
* Session attribution preservation
|
||||
* Basic and Advanced consent modes
|
||||
* gtag.js and GTM support
|
||||
* Custom script management
|
||||
* Customisable banner and preferences modal
|
||||
* WP Consent API integration
|
||||
17
uninstall.php
Normal file
17
uninstall.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* Uninstall handler.
|
||||
*
|
||||
* @package ATT_Consent
|
||||
*/
|
||||
|
||||
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
delete_option( 'att_consent_settings' );
|
||||
delete_option( 'att_cc_db_version' );
|
||||
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'att_cc_scripts';
|
||||
$wpdb->query( "DROP TABLE IF EXISTS {$table_name}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
Reference in New Issue
Block a user