Navigate privacy laws with feline precision — detect every boundary, respect every territory! GDPR compliance and privacy protection for WordPress. - Cookie consent management - Privacy boundary detection - GDPR-compliant analytics gating - Cross-plugin consent coordination (integrates with TigerStyle Heat) - Visitor preference tracking - Configurable cookie categories Includes build.sh and .distignore for WordPress-installable release ZIPs.
419 lines
13 KiB
PHP
419 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* TigerStyle Whiskers Audit Trail
|
|
*
|
|
* Track every whisker twitch and boundary crossing - complete privacy compliance logging!
|
|
*/
|
|
|
|
// Prevent direct access
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
class TigerStyleWhiskers_AuditTrail {
|
|
|
|
/**
|
|
* Single instance
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Log retention period (days)
|
|
*/
|
|
private $retention_period = 2555; // 7 years for GDPR compliance
|
|
|
|
/**
|
|
* Get instance
|
|
*/
|
|
public static function instance() {
|
|
if (is_null(self::$instance)) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
private function __construct() {
|
|
$this->init_audit_trail();
|
|
}
|
|
|
|
/**
|
|
* Initialize audit trail
|
|
*/
|
|
private function init_audit_trail() {
|
|
// Hook into privacy events
|
|
add_action('tigerstyle_whiskers_consent_granted', array($this, 'log_consent_granted'), 10, 2);
|
|
add_action('tigerstyle_whiskers_consent_withdrawn', array($this, 'log_consent_withdrawn'), 10, 2);
|
|
add_action('tigerstyle_whiskers_data_request', array($this, 'log_data_request'), 10, 3);
|
|
add_action('tigerstyle_whiskers_data_processed', array($this, 'log_data_processed'), 10, 2);
|
|
add_action('tigerstyle_whiskers_data_deleted', array($this, 'log_data_deleted'), 10, 2);
|
|
|
|
// Schedule cleanup
|
|
add_action('tigerstyle_whiskers_cleanup', array($this, 'cleanup_old_logs'));
|
|
|
|
// Admin hooks
|
|
if (is_admin()) {
|
|
add_action('wp_ajax_whiskers_export_audit_log', array($this, 'ajax_export_audit_log'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log consent granted event
|
|
*/
|
|
public function log_consent_granted($consent_data, $user_info) {
|
|
$this->log_event('consent_granted', array(
|
|
'consent_categories' => $consent_data,
|
|
'user_id' => $user_info['user_id'] ?? null,
|
|
'session_id' => $user_info['session_id'] ?? '',
|
|
'ip_address' => $user_info['ip_address'] ?? '',
|
|
'user_agent' => $user_info['user_agent'] ?? '',
|
|
'country' => $user_info['country'] ?? '',
|
|
'timestamp' => current_time('mysql')
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Log consent withdrawn event
|
|
*/
|
|
public function log_consent_withdrawn($consent_categories, $user_info) {
|
|
$this->log_event('consent_withdrawn', array(
|
|
'consent_categories' => $consent_categories,
|
|
'user_id' => $user_info['user_id'] ?? null,
|
|
'session_id' => $user_info['session_id'] ?? '',
|
|
'ip_address' => $user_info['ip_address'] ?? '',
|
|
'user_agent' => $user_info['user_agent'] ?? '',
|
|
'country' => $user_info['country'] ?? '',
|
|
'timestamp' => current_time('mysql')
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Log data request event
|
|
*/
|
|
public function log_data_request($request_type, $email, $verification_token) {
|
|
$this->log_event('data_request', array(
|
|
'request_type' => $request_type,
|
|
'email' => wp_hash($email), // Store hashed for privacy
|
|
'verification_token' => $verification_token,
|
|
'ip_address' => $this->get_client_ip(),
|
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
|
'timestamp' => current_time('mysql')
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Log data processed event
|
|
*/
|
|
public function log_data_processed($process_type, $details) {
|
|
$this->log_event('data_processed', array(
|
|
'process_type' => $process_type,
|
|
'details' => $details,
|
|
'user_id' => get_current_user_id() ?: null,
|
|
'timestamp' => current_time('mysql')
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Log data deleted event
|
|
*/
|
|
public function log_data_deleted($deletion_type, $details) {
|
|
$this->log_event('data_deleted', array(
|
|
'deletion_type' => $deletion_type,
|
|
'details' => $details,
|
|
'user_id' => get_current_user_id() ?: null,
|
|
'timestamp' => current_time('mysql')
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Generic event logging
|
|
*/
|
|
public function log_event($event_type, $data) {
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . 'whiskers_audit_log';
|
|
|
|
$wpdb->insert(
|
|
$table_name,
|
|
array(
|
|
'event_type' => $event_type,
|
|
'event_data' => wp_json_encode($data),
|
|
'ip_address' => $this->get_client_ip(),
|
|
'user_agent' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 500),
|
|
'user_id' => get_current_user_id() ?: null,
|
|
'created_at' => current_time('mysql')
|
|
),
|
|
array('%s', '%s', '%s', '%s', '%d', '%s')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get client IP address
|
|
*/
|
|
private function get_client_ip() {
|
|
$ip_keys = array('HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR');
|
|
|
|
foreach ($ip_keys as $key) {
|
|
if (!empty($_SERVER[$key])) {
|
|
$ip = $_SERVER[$key];
|
|
if (strpos($ip, ',') !== false) {
|
|
$ip = trim(explode(',', $ip)[0]);
|
|
}
|
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
|
return $ip;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
|
}
|
|
|
|
/**
|
|
* Get audit log entries
|
|
*/
|
|
public function get_audit_log($filters = array()) {
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . 'whiskers_audit_log';
|
|
$where_clauses = array('1=1');
|
|
$where_values = array();
|
|
|
|
// Apply filters
|
|
if (!empty($filters['event_type'])) {
|
|
$where_clauses[] = 'event_type = %s';
|
|
$where_values[] = $filters['event_type'];
|
|
}
|
|
|
|
if (!empty($filters['user_id'])) {
|
|
$where_clauses[] = 'user_id = %d';
|
|
$where_values[] = $filters['user_id'];
|
|
}
|
|
|
|
if (!empty($filters['date_from'])) {
|
|
$where_clauses[] = 'created_at >= %s';
|
|
$where_values[] = $filters['date_from'];
|
|
}
|
|
|
|
if (!empty($filters['date_to'])) {
|
|
$where_clauses[] = 'created_at <= %s';
|
|
$where_values[] = $filters['date_to'];
|
|
}
|
|
|
|
$limit = isset($filters['limit']) ? intval($filters['limit']) : 100;
|
|
$offset = isset($filters['offset']) ? intval($filters['offset']) : 0;
|
|
|
|
$where_sql = implode(' AND ', $where_clauses);
|
|
|
|
if (!empty($where_values)) {
|
|
$query = $wpdb->prepare(
|
|
"SELECT * FROM {$table_name} WHERE {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
|
array_merge($where_values, array($limit, $offset))
|
|
);
|
|
} else {
|
|
$query = $wpdb->prepare(
|
|
"SELECT * FROM {$table_name} WHERE {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
|
$limit,
|
|
$offset
|
|
);
|
|
}
|
|
|
|
return $wpdb->get_results($query);
|
|
}
|
|
|
|
/**
|
|
* Get audit log statistics
|
|
*/
|
|
public function get_audit_statistics($period = '30 days') {
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . 'whiskers_audit_log';
|
|
$date_from = date('Y-m-d H:i:s', strtotime("-{$period}"));
|
|
|
|
$stats = array();
|
|
|
|
// Event type distribution
|
|
$event_types = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT event_type, COUNT(*) as count
|
|
FROM {$table_name}
|
|
WHERE created_at >= %s
|
|
GROUP BY event_type
|
|
ORDER BY count DESC",
|
|
$date_from
|
|
));
|
|
|
|
$stats['event_types'] = $event_types;
|
|
|
|
// Daily activity
|
|
$daily_activity = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT DATE(created_at) as date, COUNT(*) as count
|
|
FROM {$table_name}
|
|
WHERE created_at >= %s
|
|
GROUP BY DATE(created_at)
|
|
ORDER BY date DESC",
|
|
$date_from
|
|
));
|
|
|
|
$stats['daily_activity'] = $daily_activity;
|
|
|
|
// Total events
|
|
$total_events = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM {$table_name} WHERE created_at >= %s",
|
|
$date_from
|
|
));
|
|
|
|
$stats['total_events'] = $total_events;
|
|
|
|
return $stats;
|
|
}
|
|
|
|
/**
|
|
* Export audit log
|
|
*/
|
|
public function export_audit_log($format = 'csv', $filters = array()) {
|
|
$logs = $this->get_audit_log($filters);
|
|
|
|
switch ($format) {
|
|
case 'json':
|
|
return $this->export_as_json($logs);
|
|
case 'xml':
|
|
return $this->export_as_xml($logs);
|
|
default:
|
|
return $this->export_as_csv($logs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export as CSV
|
|
*/
|
|
private function export_as_csv($logs) {
|
|
$csv_data = "Date,Event Type,User ID,IP Address,User Agent,Event Data\n";
|
|
|
|
foreach ($logs as $log) {
|
|
$csv_data .= sprintf(
|
|
"%s,%s,%s,%s,%s,%s\n",
|
|
$log->created_at,
|
|
$log->event_type,
|
|
$log->user_id ?: 'N/A',
|
|
$log->ip_address,
|
|
'"' . str_replace('"', '""', $log->user_agent) . '"',
|
|
'"' . str_replace('"', '""', $log->event_data) . '"'
|
|
);
|
|
}
|
|
|
|
return $csv_data;
|
|
}
|
|
|
|
/**
|
|
* Export as JSON
|
|
*/
|
|
private function export_as_json($logs) {
|
|
return wp_json_encode($logs, JSON_PRETTY_PRINT);
|
|
}
|
|
|
|
/**
|
|
* Export as XML
|
|
*/
|
|
private function export_as_xml($logs) {
|
|
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<audit_log>\n";
|
|
|
|
foreach ($logs as $log) {
|
|
$xml .= " <entry>\n";
|
|
$xml .= " <date>" . htmlspecialchars($log->created_at) . "</date>\n";
|
|
$xml .= " <event_type>" . htmlspecialchars($log->event_type) . "</event_type>\n";
|
|
$xml .= " <user_id>" . htmlspecialchars($log->user_id ?: 'N/A') . "</user_id>\n";
|
|
$xml .= " <ip_address>" . htmlspecialchars($log->ip_address) . "</ip_address>\n";
|
|
$xml .= " <user_agent>" . htmlspecialchars($log->user_agent) . "</user_agent>\n";
|
|
$xml .= " <event_data>" . htmlspecialchars($log->event_data) . "</event_data>\n";
|
|
$xml .= " </entry>\n";
|
|
}
|
|
|
|
$xml .= "</audit_log>";
|
|
return $xml;
|
|
}
|
|
|
|
/**
|
|
* Cleanup old log entries
|
|
*/
|
|
public function cleanup_old_logs() {
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . 'whiskers_audit_log';
|
|
$cutoff_date = date('Y-m-d H:i:s', strtotime("-{$this->retention_period} days"));
|
|
|
|
$deleted = $wpdb->query($wpdb->prepare(
|
|
"DELETE FROM {$table_name} WHERE created_at < %s",
|
|
$cutoff_date
|
|
));
|
|
|
|
if ($deleted > 0) {
|
|
$this->log_event('system_cleanup', array(
|
|
'deleted_entries' => $deleted,
|
|
'cutoff_date' => $cutoff_date
|
|
));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX: Export audit log
|
|
*/
|
|
public function ajax_export_audit_log() {
|
|
check_ajax_referer('whiskers_admin_nonce', 'nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(__('Insufficient permissions', 'tigerstyle-whiskers'));
|
|
}
|
|
|
|
$format = sanitize_text_field($_POST['format'] ?? 'csv');
|
|
$filters = array();
|
|
|
|
if (!empty($_POST['event_type'])) {
|
|
$filters['event_type'] = sanitize_text_field($_POST['event_type']);
|
|
}
|
|
|
|
if (!empty($_POST['date_from'])) {
|
|
$filters['date_from'] = sanitize_text_field($_POST['date_from']);
|
|
}
|
|
|
|
if (!empty($_POST['date_to'])) {
|
|
$filters['date_to'] = sanitize_text_field($_POST['date_to']);
|
|
}
|
|
|
|
$export_data = $this->export_audit_log($format, $filters);
|
|
|
|
wp_send_json_success(array(
|
|
'data' => $export_data,
|
|
'format' => $format,
|
|
'filename' => 'whiskers_audit_log_' . date('Y-m-d_H-i-s') . '.' . $format
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Create audit log table
|
|
*/
|
|
public static function create_table() {
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . 'whiskers_audit_log';
|
|
$charset_collate = $wpdb->get_charset_collate();
|
|
|
|
$sql = "CREATE TABLE $table_name (
|
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
event_type varchar(50) NOT NULL,
|
|
event_data longtext NOT NULL,
|
|
ip_address varchar(45) NOT NULL,
|
|
user_agent text NOT NULL,
|
|
user_id bigint(20) unsigned DEFAULT NULL,
|
|
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id),
|
|
KEY event_type (event_type),
|
|
KEY user_id (user_id),
|
|
KEY created_at (created_at),
|
|
KEY ip_address (ip_address)
|
|
) $charset_collate;";
|
|
|
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
dbDelta($sql);
|
|
}
|
|
} |