| Server IP : 216.238.66.20 / Your IP : 216.73.216.199 Web Server : nginx/1.30.4 System : Linux woropds 5.15.0-187-generic #197-Ubuntu SMP Fri Jul 17 19:17:01 UTC 2026 x86_64 User : root ( 0) PHP Version : 8.2.33 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /var/www/sweetheart.mx/htdocs/wp-content/plugins/wp-slimstat/src/Utils/ |
Upload File : |
<?php
declare(strict_types=1);
namespace SlimStat\Utils;
/**
* Centralized consent utility for tracking eligibility and PII handling.
*
* Implements multi-layered consent: Anonymous mode (no PII by default), Standard mode (CMP-based),
* DNT header support, and CMP integrations (WP Consent API, Real Cookie Banner).
* External plugins can override via 'slimstat_can_track' filter.
*
* @since 5.4.0
*/
class Consent
{
/**
* Retrieve the configured consent integration, falling back to SlimStat's banner when enabled.
*
* @return string
*/
public static function getIntegrationKey(): string
{
$settings = \wp_slimstat::$settings;
$integrationKey = $settings['consent_integration'] ?? '';
// Normalize shorthand: JS accepts both 'slimstat' and 'slimstat_banner',
// but PHP consent checks only handle 'slimstat_banner'. Map the alias so
// canTrack() and piiAllowed() recognize it consistently.
if ('slimstat' === $integrationKey) {
$integrationKey = 'slimstat_banner';
}
// Fallback: if GDPR is enabled but no integration is explicitly set, use slimstat_banner
// This ensures backward compatibility with existing installations
if ('' === $integrationKey) {
$gdprEnabled = ('on' === ($settings['gdpr_enabled'] ?? 'off'));
if ($gdprEnabled) {
// Auto-enable SlimStat banner if GDPR is on but no integration configured
$integrationKey = 'slimstat_banner';
}
}
// Also check use_slimstat_banner setting for backward compatibility
if ('' === $integrationKey && 'on' === ($settings['use_slimstat_banner'] ?? 'off')) {
$integrationKey = 'slimstat_banner';
}
return $integrationKey;
}
/**
* Build the context array for the slimstat_can_track filter.
*
* This context allows filter callbacks to distinguish between normal browser requests
* and programmatic/server-side tracking calls (e.g., from slimtrack_server()).
*
* @since 5.4.4
* @return array Context array with 'programmatic' flag and 'source' identifier
*/
public static function buildFilterContext(): array
{
return [
'programmatic' => \wp_slimstat::$is_programmatic_tracking,
'source' => \wp_slimstat::$is_programmatic_tracking ? 'server' : 'browser',
];
}
/**
* Apply the slimstat_can_track filter with tracking context.
*
* Centralizes the filter call so context is never accidentally omitted.
*
* @since 5.4.4
* @param bool $default Default tracking decision before filter override.
* @return bool Filtered tracking decision.
*/
private static function applyCanTrackFilter(bool $default): bool
{
return (bool) apply_filters('slimstat_can_track', $default, self::buildFilterContext());
}
/**
* Safe wrapper around wp_has_consent() that ensures wp_get_consent_type() is set.
*
* When no CMP has registered a consent type, wp_has_consent() defaults to true
* regardless of the user's actual consent cookie. This helper temporarily sets
* the consent type to 'optin' if unregistered, then cleans up the filter.
*
* @param string $category Consent category (e.g. 'statistics').
* @return bool Whether the user has granted consent for the given category.
*/
public static function wpHasConsentSafe(string $category): bool
{
if (!function_exists('wp_has_consent')) {
return false;
}
$callback = null;
$needsFilter = function_exists('wp_get_consent_type') && ! wp_get_consent_type();
if ($needsFilter) {
$callback = static function () {
return 'optin';
};
add_filter('wp_get_consent_type', $callback, 10, 1);
}
try {
return (bool) \wp_has_consent($category);
} finally {
if ($needsFilter && $callback !== null) {
remove_filter('wp_get_consent_type', $callback, 10);
}
}
}
/**
* Normalize consent data from various CMP formats to a standard structure.
*
* Converts different CMP consent formats (WP Consent API, Real Cookie Banner, etc.)
* into a standardized format for storage and processing.
*
* @param mixed $raw Raw consent data from CMP (array, object, boolean, etc.)
* @return array Normalized consent structure with categories as keys
*/
public static function normalizeConsent($raw): array
{
$normalized = [
'functional' => 'deny',
'statistics' => 'deny',
'statistics_anonymous' => 'deny',
'marketing' => 'deny',
];
if (is_bool($raw)) {
$value = $raw ? 'allow' : 'deny';
$normalized['statistics'] = $value;
return $normalized;
}
if (is_string($raw)) {
if ('accepted' === $raw || 'allow' === $raw || 'grant' === $raw) {
$normalized['statistics'] = 'allow';
} elseif ('denied' === $raw || 'deny' === $raw || 'revoke' === $raw) {
$normalized['statistics'] = 'deny';
}
return $normalized;
}
if (!is_array($raw) && !is_object($raw)) {
return $normalized;
}
$data = (array) $raw;
if (isset($data['allowed']) && is_array($data['allowed'])) {
foreach ($data['allowed'] as $category) {
if (isset($normalized[$category])) {
$normalized[$category] = 'allow';
}
}
return $normalized;
}
if (isset($data['denied']) && is_array($data['denied'])) {
foreach ($data['denied'] as $category) {
if (isset($normalized[$category])) {
$normalized[$category] = 'deny';
}
}
}
$categories = ['functional', 'statistics', 'statistics_anonymous', 'marketing'];
foreach ($categories as $category) {
if (isset($data[$category])) {
$value = $data[$category];
if (is_bool($value)) {
$normalized[$category] = $value ? 'allow' : 'deny';
} elseif (is_string($value)) {
$normalized[$category] = in_array($value, ['allow', 'accepted', 'grant', 'true'], true) ? 'allow' : 'deny';
}
} elseif (isset($data['groups'][$category])) {
$value = $data['groups'][$category];
if (is_bool($value)) {
$normalized[$category] = $value ? 'allow' : 'deny';
} elseif (is_string($value)) {
$normalized[$category] = in_array($value, ['allow', 'accepted', 'grant', 'true'], true) ? 'allow' : 'deny';
}
} elseif (isset($data['decision'])) {
if ('all' === $data['decision']) {
$normalized[$category] = 'allow';
} elseif (is_array($data['decision']) && isset($data['decision'][$category])) {
$value = $data['decision'][$category];
if (is_bool($value)) {
$normalized[$category] = $value ? 'allow' : 'deny';
} elseif (is_string($value)) {
$normalized[$category] = in_array($value, ['allow', 'accepted', 'grant', 'true'], true) ? 'allow' : 'deny';
}
}
}
}
return $normalized;
}
/**
* Determine whether SlimStat is allowed to track the current request.
*
* This is the PRIMARY consent gate. If this returns false, no tracking occurs at all.
*
* Decision tree:
* 1. Check programmatic tracking flag (bypasses CMP consent checks)
* 2. Check DNT header (if enabled in settings)
* 3. Check Anonymous Tracking mode (allows tracking without consent)
* 4. Determine if configuration collects PII (cookies OR full IPs)
* 5. If collects PII: Check CMP consent (for server-side verifiable CMPs or conservative blocking)
* 6. Apply 'slimstat_can_track' filter for external override
* 7. Return final decision
*
* @return bool True if tracking is allowed, false otherwise
*/
public static function canTrack(): bool
{
$settings = \wp_slimstat::$settings;
$default = true;
// DNT is non-negotiable: check before GDPR mode and before any filter.
// do_not_track is a site-owner setting independent of GDPR — it must block
// tracking even when gdpr_enabled=off.
$respectDnt = ('on' === ($settings['do_not_track'] ?? 'off'));
if ($respectDnt) {
$dntHeader = isset($_SERVER['HTTP_DNT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_DNT'])) : '';
if ('1' === $dntHeader) {
return false; // DNT always wins — no filter override
}
}
// Check if GDPR compliance mode is enabled
$gdprEnabled = ('on' === ($settings['gdpr_enabled'] ?? 'off'));
// If GDPR is disabled, allow normal tracking without consent checks
if (!$gdprEnabled) {
return self::applyCanTrackFilter($default);
}
// GDPR is enabled - consent integration is REQUIRED
$integrationKey = self::getIntegrationKey();
$hasConsentIntegration = '' !== $integrationKey;
// Note: getIntegrationKey() now auto-returns 'slimstat_banner' if GDPR is enabled
// So this block should rarely execute unless GDPR is explicitly disabled
if (!$hasConsentIntegration) {
return self::applyCanTrackFilter(false);
}
// GDPR is enabled and consent integration is configured - proceed with consent checks
// Programmatic tracking mode - bypass CMP consent checks
// Used by slimtrack_server() for server-side contexts (cron, CLI, redirect handlers)
// where no browser session exists and CMP consent has no meaningful role.
// DNT already returned false above if the header was set — safe to skip here.
if (\wp_slimstat::$is_programmatic_tracking) {
return self::applyCanTrackFilter($default);
}
// Anonymous Tracking mode - ALWAYS allow tracking (no PII collected by default)
// This mode is GDPR-safe because it hashes IPs, doesn't set cookies, and doesn't store usernames
$isAnonymousTracking = ('on' === ($settings['anonymous_tracking'] ?? 'off'));
if ($isAnonymousTracking) {
// Allow tracking - server will hash IPs and not store PII
// Users can still opt-in later for enhanced features (via consent upgrade)
// Continue to filter below
} else {
// Standard tracking mode - check if configuration collects PII
$setTrackerCookie = ('on' === ($settings['set_tracker_cookie'] ?? 'on'));
$anonymizeIp = ('on' === ($settings['anonymize_ip'] ?? 'off'));
$hashIp = ('on' === ($settings['hash_ip'] ?? 'off'));
// We collect PII if:
// - Cookies are enabled (identifies returning visitors) OR
// - Full IPs are stored (not anonymized AND not hashed)
$collectsPii = ($setTrackerCookie || (!$anonymizeIp && !$hashIp));
// Only check CMP consent if configuration actually collects PII
if ($collectsPii) {
// Check CMP integration for consent
$integrationKey = self::getIntegrationKey();
// SlimStat Banner integration - check consent cookie.
// Only enforce when the banner is explicitly enabled by the admin.
// Sites where the banner is off (e.g. upgrades from 5.3.x that never
// configured GDPR) must not have tracking silently blocked, because
// visitors have no way to grant consent through a banner they never see.
if ('slimstat_banner' === $integrationKey) {
if ('on' === ($settings['use_slimstat_banner'] ?? 'off')) {
$gdpr_service = new \SlimStat\Services\GDPRService($settings);
if (!$gdpr_service->hasConsent()) {
$default = false;
}
}
}
// Real Cookie Banner - cannot reliably read consent server-side
// Allow anonymous tracking (no PII) but block PII collection
// Client-side JS will upgrade to full tracking after consent is verified
// This provides better user experience while maintaining GDPR compliance
if ('real_cookie_banner' === $integrationKey) {
// Allow anonymous tracking, PII will be blocked separately in piiAllowed()
// This ensures basic analytics work while respecting consent for enhanced features
$default = true;
}
// WP Consent API integration - can read consent server-side
if ('wp_consent_api' === $integrationKey && function_exists('wp_has_consent')) {
$wpConsentCategory = (string) ($settings['consent_level_integration'] ?? 'statistics');
try {
if (!self::wpHasConsentSafe($wpConsentCategory)) {
$default = false;
}
} catch (\Throwable $e) {
// Consent API error - be conservative, deny tracking
$default = false;
}
}
}
// If configuration doesn't collect PII: $default remains true (tracking allowed)
}
return self::applyCanTrackFilter($default);
}
/**
* Determine whether PII (Personally Identifiable Information) collection is allowed.
*
* This is the SECONDARY consent gate. Even if tracking is allowed, PII may be restricted.
*
* PII includes:
* - Cookies (tracking cookies for session management)
* - Full IP addresses (not anonymized or hashed)
* - Username and email (for logged-in users)
* - Any other identifiable data
*
* Decision tree:
* 1. Check programmatic tracking flag (bypasses CMP consent checks)
*
* 2. HIGHEST PRIORITY: Anonymous tracking mode
* - If enabled: PII NEVER allowed unless explicit consent given
*
* 3. Check DNT header (if enabled in settings)
* - If DNT=1: PII NEVER allowed (regardless of other settings)
*
* 4. Determine if current configuration collects PII:
* - Cookies enabled? → collects PII
* - Full IPs stored (not anonymized AND not hashed)? → collects PII
*
* 5. If configuration doesn't collect PII:
* - Return true (no PII to protect, operations allowed)
*
* 6. If configuration collects PII:
* - Check CMP consent status (if CMP integration enabled)
* - WP Consent API: read server-side consent
* - Other CMPs: conservative default (no consent)
* - No CMP: allow (legacy behavior, but not GDPR-safe)
*
* @param bool $explicitConsentGiven Optional. Set to true when consent was explicitly granted
* in the current request (e.g., consent upgrade flow).
* Only relevant for anonymous tracking mode.
*
* @return bool True if PII collection is allowed, false otherwise
*/
public static function piiAllowed(bool $explicitConsentGiven = false): bool
{
$settings = \wp_slimstat::$settings;
// Check if GDPR compliance mode is enabled
$gdprEnabled = ('on' === ($settings['gdpr_enabled'] ?? 'off'));
// If GDPR is disabled, allow PII collection without consent checks
if (!$gdprEnabled) {
return true;
}
// GDPR is enabled - consent integration is REQUIRED
$integrationKey = self::getIntegrationKey();
$hasConsentIntegration = '' !== $integrationKey;
// Note: getIntegrationKey() now auto-returns 'slimstat_banner' if GDPR is enabled
// So this block should rarely execute unless GDPR is explicitly disabled
if (!$hasConsentIntegration) {
return false;
}
// GDPR is enabled and consent integration is configured - proceed with consent checks
// Check DNT header first - this supersedes all other checks including programmatic tracking
$respectDnt = ('on' === ($settings['do_not_track'] ?? 'off'));
if ($respectDnt) {
$dntHeader = isset($_SERVER['HTTP_DNT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_DNT'])) : '';
if ('1' === $dntHeader) {
// DNT header present - NEVER allow PII
return false;
}
}
// Programmatic tracking mode - bypass CMP consent checks
// Used by slimtrack_server() for server-side contexts (cron, CLI, redirect handlers)
// where no browser session exists and CMP consent has no meaningful role.
// DNT headers are still respected above.
// Anonymous mode constraints remain intact; only CMP checks are bypassed.
if (\wp_slimstat::$is_programmatic_tracking) {
$isAnonymousTracking = ('on' === ($settings['anonymous_tracking'] ?? 'off'));
if ($isAnonymousTracking && !$explicitConsentGiven) {
return false;
}
return true;
}
// PRIORITY 1: Anonymous tracking mode - strictest setting
// In this mode, PII is BLOCKED by default until explicit consent is granted
$isAnonymousTracking = ('on' === ($settings['anonymous_tracking'] ?? 'off'));
if ($isAnonymousTracking) {
// If explicit consent signal is provided (e.g., from consent upgrade AJAX handler), allow PII
if ($explicitConsentGiven) {
return true;
}
// In anonymous mode, consent is determined by two factors:
// 1. A tracking cookie MUST exist, proving the consent upgrade happened in this browser.
// 2. The CMP must report that consent is active.
// Check for tracking cookie (proof of upgrade in this browser)
$hasTrackingCookie = false;
if (isset($_COOKIE['slimstat_tracking_code'])) {
$cookieValue = \SlimStat\Tracker\Utils::getValueWithoutChecksum(sanitize_text_field(wp_unslash($_COOKIE['slimstat_tracking_code'])));
if (false !== $cookieValue) {
$hasTrackingCookie = true;
}
}
// Check for consent signal from the configured CMP
$hasCmpConsent = false;
if ('slimstat_banner' === $integrationKey) {
$gdpr_service = new \SlimStat\Services\GDPRService($settings);
$cookieName = \SlimStat\Services\GDPRService::CONSENT_COOKIE_NAME;
$cookieValue = isset($_COOKIE[$cookieName]) ? sanitize_text_field(wp_unslash($_COOKIE[$cookieName])) : 'not_set';
if ($gdpr_service->hasConsent()) {
$hasCmpConsent = true;
}
} elseif ('wp_consent_api' === $integrationKey && function_exists('wp_has_consent')) {
$wpConsentCategory = (string) ($settings['consent_level_integration'] ?? 'statistics');
try {
if (self::wpHasConsentSafe($wpConsentCategory)) {
$hasCmpConsent = true;
}
} catch (\Throwable $e) {
}
} elseif ('real_cookie_banner' === $integrationKey) {
// Real Cookie Banner: check consent cookie directly to handle race conditions
// where tracking cookie isn't set yet but consent has been granted.
$wpConsentCategory = (string) ($settings['consent_level_integration'] ?? 'statistics');
$rcbCookies = ['real_cookie_banner', 'rcb_consent', 'rcb_acceptance', 'real_cookie_consent', 'rcb-consent'];
foreach ($_COOKIE as $name => $value) {
$isMatch = false;
foreach ($rcbCookies as $rcbName) {
if (strpos($name, $rcbName) === 0) {
$isMatch = true;
break;
}
}
if ($isMatch) {
// Sanitize cookie value before processing
$sanitized_value = wp_unslash($value);
$rawJson = stripslashes($sanitized_value);
$data = json_decode($rawJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$data = json_decode(stripslashes(urldecode($sanitized_value)), true);
}
if (is_array($data)) {
// Check various structures based on RCB versions
$consentGiven = false;
// Structure 1: { "groups": { "statistics": true } }
if (isset($data['groups'][$wpConsentCategory]) && true === $data['groups'][$wpConsentCategory]) {
$consentGiven = true;
}
// Structure 2: { "decision": { "statistics": true } } OR { "decision": "all" }
elseif (isset($data['decision'])) {
if ('all' === $data['decision']) {
$consentGiven = true;
} elseif (is_array($data['decision']) && isset($data['decision'][$wpConsentCategory]) && true === $data['decision'][$wpConsentCategory]) {
$consentGiven = true;
}
}
// Structure 3: { "statistics": true } (Legacy/Simplified)
elseif (isset($data[$wpConsentCategory]) && true === $data[$wpConsentCategory]) {
$consentGiven = true;
}
if ($consentGiven) {
$hasCmpConsent = true;
// If we have explicit consent from RCB, allow PII even if tracking cookie is missing
// This breaks the deadlock for the first request after consent
$hasTrackingCookie = true;
break;
}
}
}
}
// Legacy fallback: If a SlimStat tracking cookie exists in anonymous mode,
// it implies the browser completed a consent upgrade flow previously.
if (!$hasCmpConsent && $hasTrackingCookie) {
$hasCmpConsent = true;
}
}
// PII is allowed if CMP consent is present (or implied via legacy tracking cookie).
// We do NOT require a tracking cookie to be present if we have explicit CMP consent.
$result = $hasCmpConsent;
return $result;
}
// PRIORITY 2: Determine if current configuration collects PII
$setTrackerCookie = ('on' === ($settings['set_tracker_cookie'] ?? 'on'));
$anonymizeIp = ('on' === ($settings['anonymize_ip'] ?? 'off'));
$hashIp = ('on' === ($settings['hash_ip'] ?? 'off'));
// We collect PII if:
// - Cookies are enabled (identifies returning visitors) OR
// - Full IPs are stored (not anonymized AND not hashed)
$collectsPii = ($setTrackerCookie || (!$anonymizeIp && !$hashIp));
// If configuration doesn't collect PII, then PII operations are allowed
// (because there's no PII to protect in the first place)
if (!$collectsPii) {
return true;
}
// PRIORITY 3: Configuration DOES collect PII - check consent status
$integrationKey = self::getIntegrationKey();
// SlimStat Banner integration - check consent cookie.
// Only enforce when the banner is explicitly enabled by the admin (same
// guard as canTrack()). If the banner is off, PII gating is not enforced
// via SlimStat's banner — preserves pre-5.4.0 behaviour for upgrades.
if ('slimstat_banner' === $integrationKey) {
if ('on' !== ($settings['use_slimstat_banner'] ?? 'off')) {
return true;
}
$gdpr_service = new \SlimStat\Services\GDPRService($settings);
return $gdpr_service->hasConsent();
}
// WP Consent API integration - can read consent server-side
if ('wp_consent_api' === $integrationKey && function_exists('wp_has_consent')) {
$wpConsentCategory = (string) ($settings['consent_level_integration'] ?? 'statistics');
try {
return self::wpHasConsentSafe($wpConsentCategory);
} catch (\Throwable $e) {
// Consent API error - be conservative, deny PII
return false;
}
}
// Real Cookie Banner - check consent cookie directly to handle race conditions
// where tracking cookie isn't set yet but consent has been granted.
if ('real_cookie_banner' === $integrationKey) {
// If explicit consent signal is provided (e.g., from consent upgrade request), allow PII
if ($explicitConsentGiven) {
return true;
}
// Check RCB consent cookies directly
$wpConsentCategory = (string) ($settings['consent_level_integration'] ?? 'statistics');
$rcbCookies = ['real_cookie_banner', 'rcb_consent', 'rcb_acceptance', 'real_cookie_consent', 'rcb-consent'];
foreach ($_COOKIE as $name => $value) {
$isMatch = false;
foreach ($rcbCookies as $rcbName) {
if (strpos($name, $rcbName) === 0) {
$isMatch = true;
break;
}
}
if ($isMatch) {
// Sanitize cookie value before processing
$sanitized_value = wp_unslash($value);
$rawJson = stripslashes($sanitized_value);
$data = json_decode($rawJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$data = json_decode(stripslashes(urldecode($sanitized_value)), true);
}
if (is_array($data)) {
// Check various structures based on RCB versions
$consentGiven = false;
// Structure 1: { "groups": { "statistics": true } }
if (isset($data['groups'][$wpConsentCategory]) && true === $data['groups'][$wpConsentCategory]) {
$consentGiven = true;
}
// Structure 2: { "decision": { "statistics": true } } OR { "decision": "all" }
elseif (isset($data['decision'])) {
if ('all' === $data['decision']) {
$consentGiven = true;
} elseif (is_array($data['decision']) && isset($data['decision'][$wpConsentCategory]) && true === $data['decision'][$wpConsentCategory]) {
$consentGiven = true;
}
}
// Structure 3: { "statistics": true } (Legacy/Simplified)
elseif (isset($data[$wpConsentCategory]) && true === $data[$wpConsentCategory]) {
$consentGiven = true;
}
if ($consentGiven) {
return true;
}
}
}
}
// Legacy fallback: If a SlimStat tracking cookie exists, it implies consent was granted previously
if (isset($_COOKIE['slimstat_tracking_code'])) {
$cookieValue = \SlimStat\Tracker\Utils::getValueWithoutChecksum(sanitize_text_field(wp_unslash($_COOKIE['slimstat_tracking_code'])));
if (false !== $cookieValue) {
return true;
}
}
// Conservative: assume no consent on server-side for PII collection
// Client-side JavaScript will handle consent verification and upgrade
return false;
}
// PRIORITY 4: No CMP integration configured
// When GDPR is enabled and no CMP is configured, be conservative and deny PII
// unless the configuration doesn't collect PII
// Site admins should either:
// - Enable a CMP integration, OR
// - Use anonymous tracking mode, OR
// - Configure cookie-less + anonymized/hashed IP tracking
// - Disable GDPR mode if not subject to GDPR regulations
// If configuration doesn't collect PII, allow (no PII to protect)
if (!$collectsPii) {
return true;
}
// Configuration collects PII but no CMP configured - deny for GDPR compliance
return false;
}
}