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),
];