includes/dashboard/privacy.php
<?php
declare(strict_types=1);
/*
|--------------------------------------------------------------------------
| Privacy transparency
|--------------------------------------------------------------------------
|
| The privacy modal must reflect what Brivacia actually stores and how it is
| configured. Values shown in the modal are therefore computed from the
| database schema, the current visitor snapshot and the active settings.
|
*/
function privacyYesNo(?bool $value): string
{
if ($value === null) {
return t('ui.unknown');
}
return $value ? t('ui.yes') : t('ui.no');
}
function privacyDbTables(PDO $db): array
{
static $tables = null;
if ($tables !== null) {
return $tables;
}
$rows = fetchAll($db, "
SELECT name
FROM sqlite_master
WHERE type = 'table'
AND name NOT LIKE 'sqlite_%'
");
$tables = array_values(array_filter(array_map(
static fn (array $row): string => (string)($row['name'] ?? ''),
$rows
)));
return $tables;
}
function privacyDbColumns(PDO $db, string $table): array
{
static $columns = [];
if (isset($columns[$table])) {
return $columns[$table];
}
$rows = fetchAll($db, 'PRAGMA table_info(' . $table . ')');
$columns[$table] = array_values(array_filter(array_map(
static fn (array $row): string => strtolower((string)($row['name'] ?? '')),
$rows
)));
return $columns[$table];
}
function privacyDbHasAnyColumn(PDO $db, array $needles): bool
{
$needles = array_map('strtolower', $needles);
foreach (privacyDbTables($db) as $table) {
foreach (privacyDbColumns($db, $table) as $column) {
if (in_array($column, $needles, true)) {
return true;
}
}
}
return false;
}
function privacyDbHasColumnContaining(PDO $db, array $needles): bool
{
$needles = array_map('strtolower', $needles);
foreach (privacyDbTables($db) as $table) {
foreach (privacyDbColumns($db, $table) as $column) {
foreach ($needles as $needle) {
if ($needle !== '' && str_contains($column, $needle)) {
return true;
}
}
}
}
return false;
}
function privacyIpPreview(string $ip, string $geoPrefix): string
{
if ($geoPrefix !== '') {
$visibleParts = explode('.', rtrim($geoPrefix, '.'));
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$parts = explode('.', $ip);
$octets = max(1, min(3, brivacia_setting('privacy.ip_prefix_octets', 2)));
$visibleParts = array_slice($parts, 0, $octets);
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return t('ui.hidden.ipv6');
} else {
return t('ui.unknown');
}
$visibleParts = array_slice($visibleParts, 0, 3);
$maskedParts = array_fill(0, max(0, 4 - count($visibleParts)), 'x');
return implode('.', array_merge($visibleParts, $maskedParts));
}
$visitorDay = date('Y-m-d');
$visitorUa = $_SERVER['HTTP_USER_AGENT'] ?? '';
$visitorIp = $_SERVER['REMOTE_ADDR'] ?? '';
$visitorGeo = geoLookup($visitorIp);
$visitorCountry = (string)($visitorGeo['country'] ?? BRIVACIA_UNKNOWN);
$visitorIpPrefix = (string)($visitorGeo['prefix'] ?? '');
$visitorGeoProvider = (string)($visitorGeo['provider'] ?? '');
$visitorVpn = $visitorGeo['vpn'] ?? null;
$visitorHash = visitorHash($visitorDay, $visitorUa, $visitorIp);
$visitorSeenToday = (int)(one($db, "
SELECT COUNT(*) AS total
FROM seen_daily
WHERE day = ? AND visitor_hash = ?
", [$visitorDay, $visitorHash])['total'] ?? 0);
/*
|--------------------------------------------------------------------------
| Database-derived transparency values
|--------------------------------------------------------------------------
*/
$storesFullIp = privacyDbHasAnyColumn($db, [
'ip',
'remote_addr',
'remote_ip',
'visitor_ip',
'full_ip',
'raw_ip',
]);
$storesBrowserDetails = privacyDbHasAnyColumn($db, [
'user_agent',
'ua',
'raw_user_agent',
'browser',
'browser_details',
]);
$storesPermanentId = privacyDbHasColumnContaining($db, [
'permanent',
'client_id',
'user_id',
'visitor_id',
'profile_id',
]);
$storesVisitorProfile = privacyDbHasColumnContaining($db, [
'profile',
'journey',
'session_id',
'visitor_id',
]);
$storesExactVisitorVisits = privacyDbHasAnyColumn($db, [
'visit_count',
'visits_count',
'visitor_visits',
]);
$visitorIpPreview = privacyIpPreview($visitorIp, $visitorIpPrefix);
$visitorDbSnapshot = [
'visited_today' => privacyYesNo($visitorSeenToday > 0),
'already_visited_today' => privacyYesNo($visitorSeenToday > 0),
'exact_visits_today' => $storesExactVisitorVisits ? t('ui.yes') : t('ui.unknown'),
'ip_preview' => $visitorIpPreview,
'country' => $visitorCountry,
'raw_user_agent_stored' => privacyYesNo($storesBrowserDetails),
'permanent_id_stored' => privacyYesNo($storesPermanentId),
'visitor_profile_stored' => privacyYesNo($storesVisitorProfile),
'full_ip_stored' => privacyYesNo($storesFullIp),
];