<?php
/**
 * @package     com_htprotect
 * @copyright   (C) 2026 Pascal Lohmann / website-bereinigung.de
 * @license     GNU General Public License version 2 or later
 *
 * Signierte Einmal-Links fuer das Auto-Update-FEEDBACK ("lief problemlos" /
 * "Problem gemeldet") aus der Post-Update-Mail. REINE, testbare Logik (Token/
 * Verify/Link/Format) ohne Joomla-Abhaengigkeit; die IO-Anbindung (Engine laden,
 * an radar melden, Seite rendern, jexit) sitzt im Waechter-Plugin.
 *
 * Sicherheitsmodell (wie HtpUnsub, aber mit eigener Domaene): HMAC-SHA256 mit dem
 * site-eigenen Secret bindet das Token an GENAU (Urteil + Erweiterung + Version).
 * Eine eigene Kontext-Konstante (CTX) trennt Feedback- von Abmelde-Tokens - ein
 * Abmelde-Token kann NIE als Feedback gelten und umgekehrt. Ohne das Secret ist es
 * unfaelschbar: ein fremder Besucher kann also nicht im Namen der Seite ein Urteil
 * an radar schicken. Vergleich in konstanter Zeit (hash_equals); keine Adresse/PII
 * in der URL. Reine Funktionen -> voll unit-testbar (test/autoupdatecheck.php).
 *
 * Das Feedback ist REIN MELDEND: der Klick aendert NICHTS an der Seite (kein
 * Rollback, kein Aussetzen) - er schickt nur ein anonymes Zeugen-Signal an radar.
 */

defined('_JEXEC') or die;

class HtpFeedback
{
    /**
     * Domaenen-Kontext des Feedback-Tokens. Trennt es kryptografisch vom Abmelde-
     * Token (HtpUnsub nutzt KEINEN Kontext) und erlaubt spaetere Versionierung.
     */
    const CTX = 'htp-aufb-v1';

    /** Urteil normalisieren: '1' = lief problemlos, '0' = Problem. */
    public static function normResult($result)
    {
        return ((string) $result === '1') ? '1' : '0';
    }

    /**
     * Unfaelschbares Feedback-Token (base64url, ohne Padding) ueber (Urteil,
     * Erweiterungs-Schluessel, Zielversion). Leeres Secret -> ''.
     *
     * @param   string  $result   '1' (ok) | '0' (Problem)
     * @param   string  $key      Erweiterungs-Schluessel type|element|folder
     * @param   string  $version  Zielversion (to)
     * @param   string  $secret   Site-eigenes HMAC-Secret
     *
     * @return string
     */
    public static function token($result, $key, $version, $secret)
    {
        $secret = (string) $secret;

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

        $msg = self::CTX . '|' . self::normResult($result) . '|' . strtolower(trim((string) $key)) . '|' . trim((string) $version);

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

    /**
     * Token gegen die mitgesendeten Felder pruefen (konstante Zeit). Re-berechnet
     * das erwartete Token und vergleicht per hash_equals - so kann ein Angreifer
     * weder Urteil noch Erweiterung/Version manipulieren.
     *
     * @param   string  $result   Urteil aus dem Request
     * @param   string  $key      Erweiterungs-Schluessel aus dem Request
     * @param   string  $version  Version aus dem Request
     * @param   string  $token    Token aus dem Request
     * @param   string  $secret   Site-eigenes Secret
     *
     * @return boolean
     */
    public static function verify($result, $key, $version, $token, $secret)
    {
        $token  = (string) $token;
        $secret = (string) $secret;

        if ($token === '' || $secret === '') {
            return false;
        }

        $expected = self::token($result, $key, $version, $secret);

        return $expected !== '' && hash_equals($expected, $token);
    }

    /**
     * Frontend-Feedback-URL bauen. Alle Felder werden roh-URL-kodiert; die strenge
     * Formpruefung passiert wieder beim Eingang (validKey/validVersion/validToken).
     *
     * @param   string  $siteUrl  Vertrauenswuerdige Basis-URL
     * @param   string  $result   '1' | '0'
     * @param   string  $key      Erweiterungs-Schluessel
     * @param   string  $version  Zielversion
     * @param   string  $token    Token
     * @param   string  $lang     Optionaler Sprach-Hinweis ('de'|'en'|'de-DE' ...) -
     *                            damit die Report-Seite in DERSELBEN Sprache wie die
     *                            Mail erscheint. NICHT signiert (rein kosmetisch:
     *                            manipuliert nur die Anzeigesprache, kein Sicherheitswert).
     *
     * @return string
     */
    public static function link($siteUrl, $result, $key, $version, $token, $lang = '')
    {
        $url = rtrim((string) $siteUrl, '/') . '/index.php'
            . '?htp_fb=' . self::normResult($result)
            . '&htp_fbe=' . rawurlencode(strtolower(trim((string) $key)))
            . '&htp_fbv=' . rawurlencode(trim((string) $version))
            . '&htp_fbt=' . rawurlencode((string) $token);

        $lang = strtolower(trim((string) $lang));

        if ($lang !== '' && preg_match('~^[a-z]{2,3}(-[a-z]{2})?$~', $lang)) {
            $url .= '&htp_fbl=' . rawurlencode($lang);
        }

        return $url;
    }

    /** Erweiterungs-Schluessel streng pruefen (type|element|folder). */
    public static function validKey($key)
    {
        return is_string($key) && preg_match('~^[a-z0-9_.\-]*\|[a-z0-9_.\-]+\|[a-z0-9_.\-]*$~', $key) === 1;
    }

    /** Versionsform streng pruefen (Ziffern/Punkte/kurzes Suffix). */
    public static function validVersion($version)
    {
        return is_string($version) && preg_match('~^[0-9]+(\.[0-9]+){0,3}([a-z0-9.\-]{0,12})?$~i', $version) === 1;
    }

    /** Token-Form streng pruefen (nur diese Zeichen, feste Laenge). */
    public static function validToken($token)
    {
        return is_string($token) && preg_match('~^[A-Za-z0-9_-]{16,64}$~', $token) === 1;
    }

    /** Element-Name aus dem Schluessel ziehen (mittleres Feld). */
    public static function elementOf($key)
    {
        $parts = explode('|', (string) $key);

        return isset($parts[1]) ? (string) $parts[1] : '';
    }

    /** Typ aus dem Schluessel ziehen (erstes Feld). */
    public static function typeOf($key)
    {
        $parts = explode('|', (string) $key);

        return isset($parts[0]) ? (string) $parts[0] : '';
    }

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