includes/settings.php

<?php
declare(strict_types=1);

/*
|--------------------------------------------------------------------------
| Brivacia Settings
|--------------------------------------------------------------------------
|
| Defaults to /data/settings.json and /data/brivacia.key.
| settings.json is the source of truth for user configuration.
|
*/

function brivacia_default_settings(): array
{
    return [
        'installed' => false,
        'version' => '1.0.0',
        'sites' => [
            'main' => '',
        ],
        'dashboard' => [
            'auto_refresh' => 1,
            'instance_name' => 'Brivacia',
            'light_theme' => false,
            'show_external_icon_in_top_pages' => true,
        ],
        'admin' => [
            'ignore_cookie_years' => 5,
        ],
        'privacy' => [
            'country_provider' => 'none',
            'ip_prefix_octets' => 2,
        ],
        'referrers' => [
            'auto_referrers' => true,
            'auto_referrer_icons' => true,
            'max_icon_bytes' => 102400,
            'max_icon_size' => 96,
        ],
        'trends' => [
            'visitors' => true,
            'visits' => true,
            'pageviews' => true,
        ],
    ];
}

function brivacia_root_path(string $path = ''): string
{
    $root = rtrim(__DIR__ . '/..', '/\\');
    $path = trim($path, '/\\');

    return $path === '' ? $root : $root . '/' . $path;
}

function brivacia_default_data_dir(): string
{
    return brivacia_root_path('data');
}

function brivacia_settings_path(): string
{
    return brivacia_default_data_dir() . '/settings.json';
}

function brivacia_key_path(): string
{
    return brivacia_default_data_dir() . '/brivacia.key';
}

function brivacia_read_json_file(string $file): ?array
{
    if (!is_file($file)) {
        return null;
    }

    $json = file_get_contents($file);

    if ($json === false || trim($json) === '') {
        return null;
    }

    $data = json_decode($json, true);

    return is_array($data) ? $data : null;
}

function brivacia_array_merge_recursive_distinct(array $base, array $override): array
{
    foreach ($override as $key => $value) {
        if (
            isset($base[$key]) &&
            is_array($base[$key]) &&
            is_array($value) &&
            array_is_list($base[$key]) === false &&
            array_is_list($value) === false
        ) {
            $base[$key] = brivacia_array_merge_recursive_distinct($base[$key], $value);
            continue;
        }

        $base[$key] = $value;
    }

    return $base;
}

function brivacia_settings(): array
{
    static $settings = null;

    if ($settings !== null) {
        return $settings;
    }

    $loaded = brivacia_read_json_file(brivacia_settings_path()) ?? [];

    $settings = brivacia_array_merge_recursive_distinct(
        brivacia_default_settings(),
        $loaded
    );

    if (isset($loaded['sites']) && is_array($loaded['sites'])) {
        $settings['sites'] = $loaded['sites'];
    }

    return $settings;
}

function brivacia_setting(string $key, mixed $default = null): mixed
{
    $value = brivacia_settings();

    foreach (explode('.', $key) as $part) {
        if (!is_array($value) || !array_key_exists($part, $value)) {
            return $default;
        }

        $value = $value[$part];
    }

    return $value;
}


function brivacia_sites(): array
{
    $sites = brivacia_setting('sites', []);

    return is_array($sites) ? $sites : [];
}

function brivacia_public_settings(): array
{
    $settings = brivacia_settings();

    unset($settings['secret'], $settings['key']);

    return $settings;
}

function brivacia_secret_key(): string
{
    static $key = null;

    if ($key !== null) {
        return $key;
    }

    $file = brivacia_key_path();

    if (!is_file($file)) {
        return $key = '';
    }

    return $key = trim((string)file_get_contents($file));
}

function brivacia_is_installed(): bool
{
    $settings = brivacia_settings();
    $key = brivacia_secret_key();

    return
        $key !== ''
        && $key !== 'CHANGE_ME_BEFORE_INSTALLATION'
        && (bool)($settings['installed'] ?? false);
}

function brivacia_write_file_atomic(string $file, string $content): void
{
    $dir = dirname($file);

    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }

    $tmp = $file . '.tmp.' . bin2hex(random_bytes(6));

    if (file_put_contents($tmp, $content, LOCK_EX) === false) {
        throw new RuntimeException('Unable to write temporary file: ' . $tmp);
    }

    if (!rename($tmp, $file)) {
        @unlink($tmp);
        throw new RuntimeException('Unable to write file: ' . $file);
    }
}

function brivacia_save_settings(array $settings): void
{
    $settings = brivacia_sanitize_settings($settings, brivacia_settings());
    $dataDir = brivacia_default_data_dir();

    if (!is_dir($dataDir)) {
        mkdir($dataDir, 0755, true);
    }

    $keyFile = brivacia_key_path();

    if (!is_file($keyFile)) {
        brivacia_write_file_atomic($keyFile, bin2hex(random_bytes(32)) . "\n");
    }

    $json = json_encode(
        $settings,
        JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
    );

    if ($json === false) {
        throw new RuntimeException('Unable to encode settings JSON.');
    }

    brivacia_write_file_atomic(brivacia_settings_path(), $json . "\n");
}

function brivacia_sanitize_bool(mixed $value): bool
{
    if (is_bool($value)) {
        return $value;
    }

    if (is_string($value)) {
        return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true);
    }

    return (bool)$value;
}

function brivacia_sanitize_int(mixed $value, int $min, int $max, int $default): int
{
    if (!is_numeric($value)) {
        return $default;
    }

    return max($min, min($max, (int)$value));
}

function brivacia_sanitize_site_code(string $code): string
{
    $code = strtolower(trim($code));
    $code = preg_replace('/[^a-z0-9_-]/', '-', $code) ?? '';
    $code = trim($code, '-_');

    return $code !== '' ? $code : 'main';
}

function brivacia_sanitize_domain(string $domain): string
{
    $domain = strtolower(trim($domain));
    $domain = preg_replace('#^https?://#', '', $domain) ?? $domain;
    $domain = explode('/', $domain)[0] ?? $domain;
    $domain = preg_replace('/[^a-z0-9.-]/', '', $domain) ?? '';

    return trim($domain, '.');
}

function brivacia_sanitize_sites(mixed $sites): array
{
    $clean = [];

    if (is_array($sites)) {
        foreach ($sites as $code => $domain) {
            if (is_array($domain)) {
                $code = (string)($domain['code'] ?? $code);
                $domain = (string)($domain['domain'] ?? '');
            }

            $code = brivacia_sanitize_site_code((string)$code);
            $domain = brivacia_sanitize_domain((string)$domain);

            if ($domain !== '') {
                $clean[$code] = $domain;
            }
        }
    }

    return $clean !== [] ? $clean : ['main' => ''];
}

function brivacia_sanitize_settings(array $input, ?array $base = null): array
{
    $base ??= brivacia_default_settings();
    $merged = brivacia_array_merge_recursive_distinct($base, $input);

    $provider = (string)($merged['privacy']['country_provider'] ?? 'none');

    if (!in_array($provider, ['none', 'blurloc', 'cloudflare'], true)) {
        $provider = 'none';
    }

    return [
        'installed' => brivacia_sanitize_bool($merged['installed'] ?? false),
        'version' => preg_match('/^\d+\.\d+\.\d+$/', (string)($merged['version'] ?? '1.0.0'))
            ? (string)$merged['version']
            : '1.0.0',
        'sites' => brivacia_sanitize_sites($input['sites'] ?? $merged['sites'] ?? []),
        'dashboard' => [
            'auto_refresh' => brivacia_sanitize_int($merged['dashboard']['auto_refresh'] ?? 1, 0, 1440, 1),
            'instance_name' => trim((string)($merged['dashboard']['instance_name'] ?? 'Brivacia')) ?: 'Brivacia',
            'light_theme' => brivacia_sanitize_bool($merged['dashboard']['light_theme'] ?? false),
            'show_external_icon_in_top_pages' => brivacia_sanitize_bool($merged['dashboard']['show_external_icon_in_top_pages'] ?? true),
        ],
        'admin' => [
            'ignore_cookie_years' => brivacia_sanitize_int($merged['admin']['ignore_cookie_years'] ?? 5, 1, 20, 5),
        ],
        'privacy' => [
            'country_provider' => $provider,
            'ip_prefix_octets' => brivacia_sanitize_int($merged['privacy']['ip_prefix_octets'] ?? 2, 1, 3, 2),
        ],
        'referrers' => [
            'auto_referrers' => brivacia_sanitize_bool($merged['referrers']['auto_referrers'] ?? true),
            'auto_referrer_icons' => brivacia_sanitize_bool($merged['referrers']['auto_referrer_icons'] ?? true),
            'max_icon_bytes' => brivacia_sanitize_int($merged['referrers']['max_icon_bytes'] ?? 102400, 1024, 1048576, 102400),
            'max_icon_size' => brivacia_sanitize_int($merged['referrers']['max_icon_size'] ?? 96, 16, 512, 96),
        ],
        'trends' => [
            'visitors' => brivacia_sanitize_bool($merged['trends']['visitors'] ?? true),
            'visits' => brivacia_sanitize_bool($merged['trends']['visits'] ?? true),
            'pageviews' => brivacia_sanitize_bool($merged['trends']['pageviews'] ?? true),
        ],
    ];
}

function brivacia_install(array $input): array
{
    $settings = brivacia_sanitize_settings($input, brivacia_default_settings());
    $settings['installed'] = true;

    brivacia_save_settings($settings);

    return $settings;
}