<?php
/**
 * @package     com_htprotect
 * @copyright   (C) 2026 Pascal Lohmann / website-bereinigung.de
 * @license     GNU General Public License version 2 or later
 *
 * Sichere Frontend-Abmeldung von Waechter-/Alarm-Mails. REINE, testbare Logik
 * (Token/Match/Remove/Mask/Link) ohne Joomla-Abhaengigkeit; die IO-Anbindung
 * (notify_email lesen/schreiben, Rendern, jexit) sitzt im Waechter-Plugin.
 *
 * Sicherheitsmodell: HMAC-SHA256 mit einem site-eigenen 256-bit-Secret bindet
 * das Token an GENAU EINE Adresse. Ohne das Secret ist es unfaelschbar - ein
 * Dritter kann also nicht die Alarm-Adresse stilllegen (kein "Blindmachen").
 * Keine Adresse steht je in der URL; Vergleich in konstanter Zeit (hash_equals);
 * keine Enumeration (generische Antwort bei Nicht-Treffer). Reine Funktionen ->
 * voll unit-testbar (test/unsubcheck.php).
 */

defined('_JEXEC') or die;

class HtpUnsub
{
    /** Adresse normalisieren (stabil fuer HMAC + Vergleich). */
    public static function normalize($email)
    {
        return strtolower(trim((string) $email));
    }

    /** Empfaenger-Liste aus dem notify_email-Feld (Komma/Semikolon/Whitespace). */
    public static function normalizeList($raw)
    {
        $out   = array();
        $parts = preg_split('~[\s,;]+~', (string) $raw);

        if (!is_array($parts)) {
            return $out;
        }

        foreach ($parts as $p) {
            $p = trim($p);

            if ($p !== '') {
                $out[] = $p;
            }
        }

        return $out;
    }

    /** Unfaelschbares Abmelde-Token einer Adresse (base64url, ohne Padding). */
    public static function token($email, $secret)
    {
        $secret = (string) $secret;

        if ($secret === '') {
            return '';
        }

        return self::b64url(hash_hmac('sha256', self::normalize($email), $secret, true));
    }

    /**
     * Welche Adresse aus der Liste passt zum Token? '' wenn keine.
     * Bewusst OHNE fruehen Abbruch (jeder Eintrag wird per hash_equals geprueft)
     * - so entsteht kein Timing-Orakel ueber die Listengroesse/Treffer.
     */
    public static function match($token, $emails, $secret)
    {
        $token = (string) $token;

        if ($token === '' || (string) $secret === '' || !is_array($emails)) {
            return '';
        }

        $hit = '';

        foreach ($emails as $email) {
            $expected = self::token($email, $secret);

            if ($expected !== '' && hash_equals($expected, $token)) {
                $hit = (string) $email;
            }
        }

        return $hit;
    }

    /** Liste ohne die (normalisiert) genannte Adresse - entfernt nur diese eine. */
    public static function remove($email, $emails)
    {
        $target = self::normalize($email);
        $out    = array();

        if (!is_array($emails)) {
            return $out;
        }

        foreach ($emails as $e) {
            if (self::normalize($e) !== $target) {
                $out[] = $e;
            }
        }

        return $out;
    }

    /** Adresse fuer die Anzeige maskieren: m***@e***.de (TLD bleibt erkennbar). */
    public static function mask($email)
    {
        $email = (string) $email;
        $at    = strpos($email, '@');

        if ($at === false || $at === 0) {
            return '***';
        }

        $local  = substr($email, 0, $at);
        $domain = substr($email, $at + 1);
        $localM = substr($local, 0, 1) . '***';
        $dot    = strrpos($domain, '.');

        if ($dot === false || $dot === 0) {
            $domainM = substr($domain, 0, 1) . '***';
        } else {
            $domainM = substr($domain, 0, 1) . '***' . substr($domain, $dot);
        }

        return $localM . '@' . $domainM;
    }

    /** Neues 256-bit-Secret (base64url) - CSPRNG, versionsneutral. */
    public static function genSecret()
    {
        // htp_random_bytes (compat) = random_bytes auf PHP 7+, mit 5.6-Fallback.
        // Im reinen Standalone-Test (ohne compat) direkt random_bytes (PHP 7+).
        $bytes = function_exists('htp_random_bytes') ? htp_random_bytes(32) : random_bytes(32);

        return self::b64url($bytes);
    }

    /**
     * Strenge Token-Form fuer den Frontend-Eingang. MUSS vor jeder weiteren
     * Verarbeitung/Reflektion geprueft werden (nur diese Zeichen, feste Laenge).
     */
    public static function validTokenFormat($token)
    {
        return is_string($token) && preg_match('~^[A-Za-z0-9_-]{16,64}$~', $token) === 1;
    }

    /** Frontend-Abmelde-URL fuer eine Adresse. */
    public static function link($siteUrl, $token)
    {
        return rtrim((string) $siteUrl, '/') . '/index.php?htp_unsub=' . rawurlencode((string) $token);
    }

    /** base64url ohne Padding. */
    private static function b64url($bin)
    {
        return rtrim(strtr(base64_encode((string) $bin), '+/', '-_'), '=');
    }
}
