<?php
/**
 * @package     HTProtect
 * @subpackage  Auto-Update (Joomla-3-Legacy-Pfad)
 *
 * AUSSCHLIESSLICH JOOMLA 3. Kapselt alles, was der Erweiterungs-Auto-Update-/Self-Update-
 * Pfad auf Joomla 3 ANDERS machen muss als ab Joomla 5.4. Ziel: der 5.4+-Pfad in
 * autoupdate.php bleibt bit-identisch; der komplette J3-Sonderweg liegt hier an EINER Stelle.
 *
 * WARUM ueberhaupt ein Sonderweg?
 *   1) TRIGGER: J3 hat keinen com_scheduler (das gibt es erst ab J4.1) und damit kein echtes
 *      Task-Scheduler-Plugin. Ab J5.4 stoesst das garantierte Cron-Plugin (plg_task_htpautoupdate)
 *      die State-Machine an. Auf J3 uebernimmt das der bereits vorhandene, gedrosselte
 *      Waechter-Herzschlag (PlgSystemHtprotectwatch::htpRun, alle 15 Min, lock-gesichert,
 *      im Hintergrund via fastcgi_finish_request) -> HtpAutoUpdateLegacy::heartbeat().
 *      Sobald ein Job laeuft, uebernimmt der OHNEHIN Joomla-neutrale com_ajax-Self-Retrigger
 *      (htpAutoUpdateTick, ~15 s je Phase) den zuegigen Durchlauf - der lief auf J3 bisher nur
 *      nicht, weil ohne laufende Engine nie die Job-State-Datei entstand.
 *   2) INSTALL-KERN: der 5.4+-Install nutzt $app->bootComponent('com_installer')->getMVCFactory()
 *      ->createModel(...) - reine J4+-API. Auf J3 gibt es das nicht; dort ist der Weg das
 *      klassische MVC-Legacy-Modell (JModelLegacy 'Update' aus com_installer). Genau das
 *      liefert installUpdateById().
 *
 * WAS WIRD WIEDERVERWENDET (bewusst NICHT dupliziert)?
 *   Die gesamte sicherheitskritische State-Machine bleibt gemeinsam: Kandidatenwahl,
 *   Datei+DB-Backup, Health-Delta-Probe, AUTO-ROLLBACK, Malware-Scan des Update-Inhalts,
 *   Key-/Quellen-Backoff, Recall-/Hold-Liste, Notify. HtpAutoUpdate ruft aus installUpdate()
 *   fuer J3 nur diesen einen Adapter statt der J4+-Fabrik; der Erfolg wird ohnehin ehrlich
 *   an der TATSAECHLICHEN On-Disk-Version + 'gone' + Health verifiziert (installOutcome),
 *   nicht am Rueckgabewert des Modells - deshalb ist ein konservatives Ergebnis hier sicher.
 *
 * Strikt fail-open: nichts hier darf je die Seite beeintraechtigen.
 *
 * @copyright   (C) HTProtect
 * @license     GNU General Public License version 2 or later
 */

defined('_JEXEC') or die;

class HtpAutoUpdateLegacy
{
    /**
     * Genau der Joomla-3-Ast (3.0 <= v < 4.0)? J2.5 sowie J4-<5.4 sind bewusst
     * ausgespart (dort erscheint stattdessen der Upgrade-Hinweis; ab 5.4 laeuft der
     * regulaere Task-Scheduler-Pfad). Reine Policy - ohne FS/DB/HTTP, unit-testbar.
     *
     * @param   string|null  $jversion  Joomla-Version; null = selbst ermitteln.
     *
     * @return boolean
     */
    public static function isJ3($jversion = null)
    {
        if ($jversion === null) {
            $jversion = function_exists('htp_jversion')
                ? htp_jversion()
                : (defined('JVERSION') ? JVERSION : '');
        }

        $jversion = (string) $jversion;

        if ($jversion === '' || !preg_match('~^\d+\.\d+~', $jversion)) {
            return false;
        }

        return version_compare($jversion, '3.0', '>=')
            && version_compare($jversion, '4.0', '<');
    }

    /**
     * JOOMLA-3-ANSTOSS (Ersatz fuer das Task-Scheduler-Plugin): ein Schritt der
     * Auto-Update-State-Machine, aufgerufen aus dem gedrosselten Waechter-Herzschlag
     * (htpRun, alle 15 Min). Auf allem AUSSER J3 kehrt die Methode sofort zurueck - der
     * 5.4+-Pfad bleibt dadurch voellig unberuehrt, selbst wenn der Aufruf dort steht.
     *
     * HtpLink::tick() ruft der Waechter unmittelbar davor bereits selbst; der erneute
     * Aufruf via runStep() ist idempotent + gedrosselt (fuer ungekoppelte Sites ohnehin
     * ein sofortiges Config-Gate) und damit unschaedlich.
     *
     * @param   array|null  $cfg  bereits geladene Konfiguration (nur Zukunfts-Reserve).
     *
     * @return void
     */
    public static function heartbeat($cfg = null)
    {
        try {
            if (!self::isJ3()) {
                return; // NUR Joomla 3
            }

            if (!class_exists('HtpAutoUpdate')) {
                return;
            }

            // Volle, getestete State-Machine (Backup -> Install -> Verify -> Auto-Rollback).
            // Der zuegige Mehrphasen-Durchlauf laeuft danach ueber den J-neutralen
            // com_ajax-Self-Retrigger (htpAutoUpdateTick), sobald der Job aktiv ist.
            //
            // GESCHLOSSENE SESSION UNKRITISCH: der Waechter ruft dies NACH backgroundDetach()
            // (Session geschlossen). Der J3-Install laeuft ueber das MVC-Legacy-'Update'-Modell
            // (installUpdateById) - reine JUpdater/JInstaller-Datei-/DB-Operationen ohne
            // Session-/User-/ACL-Abhaengigkeit (an J3.10-Quelle verifiziert). Der Erfolg wird
            // ohnehin am ECHTEN On-Disk-Versionssprung gemessen (installOutcome), und das
            // Backup->Verify->Rollback-Netz sichert jeden Fall ab. Meist fuehrt aber ohnehin
            // der session-offene com_ajax-/FFR-Pfad die UPDATE-Phase aus (er tickt alle 15 s).
            HtpAutoUpdate::runStep('j3legacy');
        } catch (Exception $e) {
            // fail-open
        } catch (Throwable $e) {
            // fail-open
        }
    }

    /**
     * JOOMLA-3-INSTALL-ADAPTER: genau EIN Erweiterungs-Update anwenden ueber das
     * klassische com_installer-'Update'-Modell (JModelLegacy). Ersetzt auf J3 den
     * J4+-Weg (bootComponent/createModel) - aufgerufen aus HtpAutoUpdate::installUpdate().
     *
     * Contract: liefert NUR bool (Joomla meldete Erfolg). 'gone' (Update-Zeile weg) und die
     * On-Disk-Versionspruefung macht der Aufrufer einheitlich weiter - deshalb ist ein
     * konservatives false hier ungefaehrlich (installOutcome erkennt den Erfolg ueber den
     * echten Versionssprung). Ein falsches true dagegen waere gefaehrlich -> wird vermieden.
     *
     * KEINE Identitaets-Elevation noetig: das 'Update'-Modell prueft in update() selbst keine
     * ACL (der core.admin-Gate sitzt im Controller/View, den wir NICHT aufrufen); JInstaller
     * und der Download (extra_query/Download-Key aus #__update_sites) brauchen keinen
     * angemeldeten Nutzer.
     *
     * @param   int  $updateId  Datensatz-ID aus #__updates.
     *
     * @return boolean  true = Joomla meldete Erfolg (Aufrufer verifiziert zusaetzlich On-Disk).
     */
    public static function installUpdateById($updateId)
    {
        $updateId = (int) $updateId;

        if ($updateId <= 0 || !class_exists('JModelLegacy')) {
            return false;
        }

        $ok = false;

        try {
            // Installer-Kontext (option=com_installer) + Sperr-Datei-Cleanup VOR dem Plugin-Import -
            // verhindert den Regular-Labs-Selbst-Deadlock (deren System-Plugin haelt sonst eine
            // shared-flock ueber den ganzen Request). Identisch zum 5.4+-Pfad, versionsneutral ueber
            // HtpAutoUpdate::prepareInstallerContext (auf J3 greift dort der ->input-Zweig).
            if (class_exists('JFactory') && class_exists('HtpAutoUpdate')
                && method_exists('HtpAutoUpdate', 'prepareInstallerContext')) {
                HtpAutoUpdate::prepareInstallerContext(JFactory::getApplication());
            }

            // Installer-/System-Plugins laden (Installer-Skripte/Libraries erwarten sie).
            if (class_exists('JPluginHelper')) {
                JPluginHelper::importPlugin('system');
                JPluginHelper::importPlugin('installer');
            }

            // com_installer-Modell-/Tabellenpfade registrieren, dann das 'Update'-Modell holen.
            $base = (defined('JPATH_ADMINISTRATOR') ? JPATH_ADMINISTRATOR : JPATH_ROOT . '/administrator')
                . '/components/com_installer';
            JModelLegacy::addIncludePath($base . '/models');

            if (class_exists('JTable')) {
                JTable::addIncludePath($base . '/tables');
            }

            $model = JModelLegacy::getInstance('Update', 'InstallerModel', array('ignore_request' => true));

            if ($model && method_exists($model, 'update')) {
                if (method_exists($model, 'setState')) {
                    $model->setState('list.start', 0);
                }

                // Nur GENAU dieses eine Update anwenden. J3s InstallerModelUpdate::update()
                // liefert KEINEN Rueckgabewert (null) und legt das Ergebnis in
                // getState('result') ab - als int aus bitweisem UND ueber die Ziele (Erfolg
                // je Ziel = 1, ein Fehlschlag zieht auf 0). Konservativ nur ein ECHTES Positiv
                // als Erfolg werten; ein false-Negativ ist unschaedlich, weil der Aufrufer
                // zusaetzlich den TATSAECHLICHEN On-Disk-Versionssprung prueft (installOutcome).
                $model->update(array($updateId));

                if (method_exists($model, 'getState')) {
                    $st = $model->getState('result');

                    if ($st === true || $st === 1 || $st === '1') {
                        $ok = true;
                    }
                }

                self::cleanCache();
            }
        } catch (Exception $e) {
            $ok = false;
        } catch (Throwable $e) {
            $ok = false;
        }

        return $ok;
    }

    /**
     * Joomla-Cache (J3) nach einem Install leeren - damit Menue/Module/System den neuen
     * Stand ziehen. Self-contained + strikt fail-soft (Cache-Fehler duerfen nie stoeren).
     *
     * @return void
     */
    private static function cleanCache()
    {
        try {
            if (!class_exists('JFactory')) {
                return;
            }

            $groups = array('_system', 'com_modules', 'mod_menu', 'com_plugins', 'com_installer');

            foreach (array_unique($groups) as $group) {
                try {
                    $cache = JFactory::getCache($group);

                    if ($cache && method_exists($cache, 'clean')) {
                        $cache->clean();
                    }
                } catch (Exception $e) {
                    // still
                } catch (Throwable $e) {
                    // still
                }
            }
        } catch (Exception $e) {
            // still
        } catch (Throwable $e) {
            // still
        }
    }
}
