<?php
/**
 * @package     com_htprotect
 * @copyright   (C) 2026 Pascal Lohmann / website-bereinigung.de
 * @license     GNU General Public License version 2 or later
 *
 * Verdichtet den Gesamt-Schutzstatus zu EINER Ampel-Stufe fuer die
 * Home-Dashboard-Kachel (Quickicon-Plugin). Bewusst getrennt:
 *  - Aktions-Stufe (gelb): wird LIVE + billig berechnet (Konfiguration +
 *    ein Lesen der geschriebenen .htaccess) - so wird die Kachel direkt nach
 *    "Jetzt absichern" wieder gruen, ohne auf einen Hintergrundlauf zu warten.
 *  - Bedrohungs-Stufe (rot): kommt aus einem kleinen, vom Hintergrund-Waechter
 *    (und beim Oeffnen der Komponente) geschriebenen Schnappschuss. So loest
 *    die Kachel KEINEN teuren Scan bei jedem Dashboard-Aufruf aus.
 *
 * Rot ist echten Bedrohungen vorbehalten (verwundbare Erweiterung, aktive
 * Angriffsspuren) - Abwesenheit/Veraltung des Schutzes ist Gelb (Handlung
 * noetig), nie Rot. So bleibt Rot glaubwuerdig.
 */

defined('_JEXEC') or die;

class HtpStatus
{
    /**
     * Dateiname des Status-Schnappschusses im Joomla-Temp-Ordner.
     */
    const SNAPSHOT_FILE = 'htprotect_status.json';

    /**
     * Kachel-Modell fuer das Quickicon (alle Joomla-Versionen). Liest den
     * Bedrohungs-Schnappschuss (rot) und bewertet die Aktions-Stufe (gelb)
     * live. Liefert level/text/link fuer die Anzeige.
     *
     * @param   array  $cfg  Konfiguration
     *
     * @return array ['level' => 'success'|'warning'|'danger', 'reason' => string, 'text' => string, 'link' => string, 'count' => int]
     */
    public static function card($cfg)
    {
        $snap = self::readSnapshot();

        if (is_array($snap) && !empty($snap['threat'])) {
            // Bedrohung (rot) hat Vorrang - aus dem Schnappschuss.
            $reason = isset($snap['reason']) ? (string) $snap['reason'] : 'attack';
            $anchor = isset($snap['anchor']) ? (string) $snap['anchor'] : '';
            $count  = isset($snap['count']) ? (int) $snap['count'] : 0;
        } else {
            // Sonst die live bewertete Aktions-Stufe (gelb/gruen).
            list($reason, $anchor) = self::actionState($cfg);
            $count = 0;
        }

        return array(
            'level'  => self::levelForReason($reason),
            'reason' => $reason,
            'anchor' => $anchor,
            'text'   => self::reasonText($reason, $count),
            'link'   => self::reasonLink($anchor),
            'count'  => $count,
        );
    }

    /**
     * Ampel-Stufe zu einem Grund (die zentrale Severity-Politik, rein testbar):
     *  - rot  (danger)  = echte Bedrohung (verwundbare Erweiterung / Angriffsspur)
     *  - gelb (warning) = Handlung noetig (nicht eingerichtet / veraltet / aus / nginx)
     *  - gruen(success) = alles gut
     *
     * @param   string  $reason  Grund-Schluessel
     *
     * @return string 'danger'|'warning'|'success'
     */
    public static function levelForReason($reason)
    {
        switch ($reason) {
            case 'vuln':
            case 'attack':
            case 'malware':
                return 'danger';

            case 'unwritten':
            case 'shield_off':
            case 'stale':
            case 'nginx':
            case 'iis':
                return 'warning';

            case 'ok':
            default:
                return 'success';
        }
    }

    /**
     * Muss ein veraltetes Regelwerk als Warnung angezeigt werden - oder heilt
     * sich das Schild ohnehin selbst? Ein Handlungshinweis ("Regelwerk veraltet")
     * ist NUR sinnvoll, wenn der Nutzer wirklich eingreifen muss: die automatische
     * Aktualisierung ist AUS oder headless nicht moeglich (kein Self-Test verfuegbar).
     * Ist sie an und moeglich, schreibt sich das Schild beim naechsten Tick von allein
     * neu - die Echtzeit-WAF traegt die aktuellen Signaturen ohnehin schon -, deshalb
     * KEINE (verwirrende) Warnung, z. B. direkt nach dem HTProtect-Auto-Update, solange
     * der .htaccess-Nachzug noch laeuft. Rein, testbar (nur cfg + billiger Probe-Check).
     *
     * @param   array  $cfg  Konfiguration
     *
     * @return boolean true = Warnung anzeigen (Nutzer muss handeln)
     */
    public static function staleNeedsAttention($cfg)
    {
        return empty($cfg['shield_autoheal'])
            || !class_exists('HtpProbe')
            || !HtpProbe::available();
    }

    /**
     * Aktions-Stufe live + billig ermitteln (kein Scan): nginx / nicht
     * geschrieben / veraltet / Schild aus / alles gut.
     *
     * @param   array  $cfg  Konfiguration
     *
     * @return array array($reason, $anchor)
     */
    private static function actionState($cfg)
    {
        // .htaccess EXTERN verwaltet (z. B. Akeeba Admin Tools Pro .htaccess Maker):
        // HTProtect prueft/nagt die .htaccess-Schreib-Zustaende (nginx / nicht
        // geschrieben / veraltet) bewusst NICHT - die Ampel bleibt dadurch gruen.
        if (empty($cfg['htaccess_external'])) {
            // nginx: der Regelblock muss manuell eingebunden werden -> Hinweis.
            // Maszgeblich ist der gecachte .htaccess-Funktionstest (htp_nginx_mode),
            // damit Apache-hinter-nginx (Plesk-Proxy) auch in der Kachel korrekt als
            // Apache gilt - kein HTTP-Test hier (billiger Schnappschuss).
            $nginxMode = function_exists('htp_nginx_mode')
                ? htp_nginx_mode($cfg)
                : (function_exists('htp_is_nginx') && htp_is_nginx());

            if ($nginxMode) {
                return array('nginx', 'htp-a-write');
            }

            // IIS: .htaccess wird nicht gelesen -> die web.config muss manuell platziert werden.
            // Gecachter .htaccess-Funktionstest (htp_iis_mode), damit ein Proxy davor korrekt faellt.
            $iisMode = function_exists('htp_iis_mode')
                ? htp_iis_mode($cfg)
                : (function_exists('htp_is_iis') && htp_is_iis());

            if ($iisMode) {
                return array('iis', 'htp-a-write');
            }

            $st = HtpStorage::status($cfg);

            if (empty($st['exists']) || empty($st['ours'])) {
                return array('unwritten', 'htp-a-write');
            }

            // Veraltet? Inhaltsbasiert (spiegelt exakt HtpApp::overview): Regel-Koerper
            // (ohne Marker/Zeitstempel/Signatur-Block) weicht vom frisch erzeugten ab,
            // oder eine abgeschaltete Signatur ist noch aktiv. Reines Versions-/Feed-
            // Update ohne echte Regelaenderung gilt NICHT als veraltet.
            if (HtpBuilder::shieldOutdated($cfg, HtpStorage::read())
                && self::staleNeedsAttention($cfg)) {
                return array('stale', 'htp-a-write');
            }
        }

        if (empty($cfg['x_shield'])) {
            return array('shield_off', 'htp-a-exploit');
        }

        return array('ok', '');
    }

    /**
     * Bedrohungs-Schnappschuss aus den Anzeige-/Waechterdaten schreiben. Nur die
     * (rote) Bedrohungssicht wird persistiert - die Aktions-Stufe bewertet die
     * Kachel ohnehin live. Aktive Angriffsspuren haben Vorrang vor der Warnliste.
     *
     * @param   array  $cfg   Konfiguration
     * @param   array  $data  Erwartet suspicious_admins, jce_profiles, mal_htaccess, account_changes, seo_finding, vuln (je optional)
     *
     * @return boolean
     */
    public static function writeSnapshot($cfg, $data)
    {
        $threat = self::threatFromData($data);

        $payload = array(
            'threat' => $threat !== null ? 1 : 0,
            'reason' => $threat !== null ? $threat[0] : 'ok',
            'anchor' => $threat !== null ? $threat[1] : '',
            'count'  => $threat !== null ? $threat[2] : 0,
            'ts'     => time(),
        );

        $dir = function_exists('htp_state_dir') ? htp_state_dir() : (function_exists('htp_tmp_path') ? htp_tmp_path() : '');

        if ($dir === '' || !is_dir($dir)) {
            return false;
        }

        $json = json_encode($payload);

        if ($json === false) {
            return false;
        }

        return @file_put_contents(rtrim(str_replace('\\', '/', $dir), '/') . '/' . self::SNAPSHOT_FILE, $json) !== false;
    }

    /**
     * Bedrohung aus den Daten ableiten: aktive Angriffsspuren (gehackte JCE-
     * Profile, boesartige .htaccess, Konten-Anomalien) vor verwundbaren
     * Erweiterungen. Liefert array($reason, $anchor, $count) oder null.
     *
     * @param   array  $data  Anzeige-/Waechterdaten
     *
     * @return array|null array($reason, $anchor, $count) oder null
     */
    public static function threatFromData($data)
    {
        if (!is_array($data)) {
            return null;
        }

        $adm = isset($data['suspicious_admins']) && is_array($data['suspicious_admins']) ? $data['suspicious_admins'] : array();
        $jce = isset($data['jce_profiles']) && is_array($data['jce_profiles']) ? $data['jce_profiles'] : array();
        $mal = isset($data['mal_htaccess']) && is_array($data['mal_htaccess']) ? $data['mal_htaccess'] : array();
        $acc = isset($data['account_changes']) && is_array($data['account_changes']) ? $data['account_changes'] : array();
        // AUFGESCHOBENE (stillgestellte) Warnungen zaehlen hier NICHT rot mit: bevorzugt die
        // defer-bereinigte Aktivliste (vuln_active); nur wenn die fehlt (Alt-Aufrufer) die
        // volle Liste - fail-safe zum Warnen.
        $vul = isset($data['vuln_active']) && is_array($data['vuln_active'])
            ? $data['vuln_active']
            : (isset($data['vuln']) && is_array($data['vuln']) ? $data['vuln'] : array());
        $seo = isset($data['seo_finding']) && is_array($data['seo_finding']) ? $data['seo_finding'] : array();

        // Untergeschobener Super-Admin (SP-Page-Builder-Kampagne) zuerst - das ist
        // eine aktive Persistenz und am dringendsten.
        if (!empty($adm)) {
            return array('attack', 'htp-a-admins', count($adm));
        }

        if (!empty($jce)) {
            return array('attack', 'htp-a-jce', count($jce));
        }

        if (!empty($mal)) {
            return array('attack', 'htp-a-malht', count($mal));
        }

        if (!empty($acc)) {
            return array('attack', 'htp-a-accounts', count($acc));
        }

        // Defacement/SEO-Spam-Injektion (eingeschleuster Loader, Fremd-Redirect):
        // ein definitiver SEO-Befund ist eine aktive Kompromittierung -> attack.
        if (!empty($seo) && !empty($seo['reasons'])) {
            return array('attack', 'htp-a-seowatch', 1);
        }

        // Eingeschleuster Code in einer Kerndatei (configuration.php/Template) -
        // aktive Kompromittierung -> attack; Anker = Malware-Scanner (Site-Scan).
        $cor = isset($data['core_finding']) && is_array($data['core_finding']) ? $data['core_finding'] : array();

        if (!empty($cor) && !empty($cor['reasons'])) {
            return array('attack', 'htp-a-malfiles', 1);
        }

        if (!empty($vul)) {
            return array('vuln', 'htp-a-vuln', count($vul));
        }

        return null;
    }

    /**
     * Schnappschuss lesen (oder null).
     *
     * @return array|null
     */
    public static function readSnapshot()
    {
        $dir = function_exists('htp_state_dir') ? htp_state_dir() : (function_exists('htp_tmp_path') ? htp_tmp_path() : '');

        if ($dir === '') {
            return null;
        }

        $file = rtrim(str_replace('\\', '/', $dir), '/') . '/' . self::SNAPSHOT_FILE;

        if (!is_file($file)) {
            return null;
        }

        $json = json_decode((string) @file_get_contents($file), true);

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

    /**
     * Lokalisierten Status-Text zu einem Grund liefern.
     *
     * @param   string  $reason  Grund-Schluessel
     * @param   integer $count   Anzahl (fuer rote Stufen)
     *
     * @return string
     */
    private static function reasonText($reason, $count)
    {
        switch ($reason) {
            case 'vuln':
                return htp_t('COM_HTPROTECT_QI_VULN', $count);

            case 'attack':
            case 'malware':
                return htp_t('COM_HTPROTECT_QI_ATTACK');

            case 'unwritten':
            case 'shield_off':
                return htp_t('COM_HTPROTECT_QI_SECURE');

            case 'stale':
                return htp_t('COM_HTPROTECT_QI_UPDATE');

            case 'nginx':
                return htp_t('COM_HTPROTECT_QI_NGINX');

            case 'iis':
                return htp_t('COM_HTPROTECT_QI_IIS');

            case 'ok':
            default:
                return htp_t('COM_HTPROTECT_QI_OK');
        }
    }

    /**
     * Deep-Link in die Komponente (optional auf eine Ampel-Sprungmarke).
     *
     * @param   string  $anchor  Anker ohne '#' oder ''
     *
     * @return string
     */
    private static function reasonLink($anchor)
    {
        $url = 'index.php?option=com_htprotect';

        if ($anchor === '') {
            return $url;
        }

        // Die meisten Ampel-Anker leben in der Uebersicht; die SEO-/Defacement-
        // Karte sitzt im Scan-Bereich -> dorthin verlinken, damit der Anker greift.
        if ($anchor === 'htp-a-seowatch' || $anchor === 'htp-a-malfiles') {
            $url .= '&sec=scan';
        }

        return $url . '#' . $anchor;
    }
}
