<?php
/**
 * @package     com_htprotect
 * @copyright   (C) 2026 Pascal Lohmann / website-bereinigung.de
 * @license     GNU General Public License version 2 or later
 *
 * Installer-Script: wird vom Joomla Extension Manager ausgefuehrt.
 * Zeigt nach der Installation den Direkteinstieg zur Einrichtung.
 * Diese Edition deinstalliert sich NICHT selbst.
 */

defined('_JEXEC') or die;

class Com_HtprotectInstallerScript
{
    /**
     * Sprachdatei (.sys) einmalig laden - versionsneutral (J2.5-6).
     *
     * @return void
     */
    private function loadLang()
    {
        static $done = false;

        if ($done) {
            return;
        }

        $done = true;

        try {
            $lang = null;

            if (class_exists('Joomla\\CMS\\Factory')) {
                $lang = Joomla\CMS\Factory::getLanguage();
            } elseif (class_exists('JFactory')) {
                $lang = JFactory::getLanguage();
            }

            if ($lang && method_exists($lang, 'load') && defined('JPATH_ADMINISTRATOR')) {
                // Jede deutsche Sprachvariante (de-AT, de-CH, de-LI, de-LU ...) bekommt
                // die deutschen Installer-Texte - nicht nur de-DE. Ohne das faellt z. B.
                // de-AT mangels eigener .sys.ini auf en-GB (t() zeigt dann Englisch, nicht
                // den deutschen Fallback). Also Tag ermitteln und bei Deutsch de-DE laden.
                $load = (method_exists($lang, 'getTag')
                    && strncasecmp((string) $lang->getTag(), 'de', 2) === 0) ? 'de-DE' : null;
                $lang->load('com_htprotect.sys', JPATH_ADMINISTRATOR, $load, true, true);
            }
        } catch (Exception $e) {
            // Ohne Sprachsystem greift der Fallback unten.
        }
    }

    /**
     * Uebersetzten Text liefern; faellt auf den deutschen Originaltext zurueck,
     * wenn das Sprachsystem (noch) nicht verfuegbar ist (z. B. im preflight,
     * bevor die Dateien kopiert wurden).
     *
     * @param   string  $key       Sprachschluessel (.sys)
     * @param   string  $fallback  Rohtext, falls keine Uebersetzung
     *
     * @return string
     */
    private function t($key, $fallback)
    {
        $this->loadLang();

        $text = null;

        if (class_exists('Joomla\\CMS\\Language\\Text')) {
            $text = Joomla\CMS\Language\Text::_($key);
        } elseif (class_exists('JText')) {
            $text = JText::_($key);
        }

        return (!is_string($text) || $text === '' || $text === $key) ? $fallback : $text;
    }

    public function preflight($type, $parent)
    {
        if (version_compare(PHP_VERSION, '5.4.0', '<')) {
            $msg = $this->t(
                'COM_HTPROTECT_INSTALL_PHP_TOOLOW',
                'HTProtect Server-Schild ben&ouml;tigt PHP 5.4 oder neuer. Gefunden: PHP %s'
            );
            echo '<p style="color:#b00">' . sprintf($msg, htmlspecialchars(PHP_VERSION, ENT_QUOTES, 'UTF-8')) . '</p>';

            return false;
        }

        return true;
    }

    public function install($parent)
    {
        return true;
    }

    public function update($parent)
    {
        return true;
    }

    public function uninstall($parent)
    {
        // ALLE automatisch mitinstallierten Plugins restlos entfernen (Dateien +
        // #__extensions-Eintrag) - sonst bleiben plg_system_htprotectwatch und
        // plg_quickicon_htprotect verwaist in der Datenbank zurueck.
        $this->removeBundledPlugins();
        $this->removeWatchTempFiles();
        $this->dropStoreTable();

        echo '<p>' . $this->t(
            'COM_HTPROTECT_UNINSTALL_NOTE',
            'HTProtect Server-Schild wurde entfernt. Eine bereits geschriebene <code>.htaccess</code> und ein '
            . 'eingerichteter <code>/administrator</code>-Passwortschutz bleiben bewusst bestehen und k&ouml;nnen '
            . 'bei Bedarf per FTP angepasst werden.'
        ) . '</p>';

        return true;
    }

    /**
     * Alle mitgelieferten Plugins restlos entfernen: das Hintergrund-Plugin
     * (plg_system_htprotectwatch) UND die Dashboard-Statuskachel
     * (plg_quickicon_htprotect). Sonst bleiben verwaiste #__extensions-Eintraege
     * zurueck.
     *
     * @return void
     */
    private function removeBundledPlugins()
    {
        // IDs, die Joomla in DIESEM Request (Erweiterungen -> Verwalten) ohnehin selbst
        // entfernt: die lassen wir in Ruhe, sonst entfernt unsere Kaskade sie ZUERST und
        // Joomlas anschliessender Versuch scheitert -> "Error uninstalling . (plugin)".
        $batch = $this->currentUninstallBatchIds();

        $this->removePlugin('htprotectwatch', 'system', $batch);
        $this->removePlugin('htprotect', 'quickicon', $batch);
        $this->removePlugin('htpautoupdate', 'task', $batch);
        $this->removeSchedulerTask();
        $this->removeWebRescue();
    }

    /**
     * Die im WEB-Docroot bereitgestellte Kopie des crash-festen Rescue-Endpunkts entfernen. Im
     * Public-Folder-Modus liegt rescue.php unter public/plugins/system/htprotectwatch/rescue.php -
     * GETRENNT vom Code-Plugin-Ordner, den removePlugin() raeumt. In klassischen Installs ist der
     * Web-Docroot == JPATH_ROOT, also identisch mit dem bereits geraeumten Plugin-Ordner -> No-Op.
     * Best effort, nie hart. Prunt nur die evtl. von uns selbst angelegten, jetzt leeren Ordner.
     *
     * @return void
     */
    private function removeWebRescue()
    {
        try {
            $webBase = (defined('JPATH_PUBLIC') && JPATH_PUBLIC)
                ? JPATH_PUBLIC
                : (defined('JPATH_ROOT') ? JPATH_ROOT : '');

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

            $dir  = rtrim(str_replace('\\', '/', (string) $webBase), '/') . '/plugins/system/htprotectwatch';
            $file = $dir . '/rescue.php';

            if (@is_file($file)) {
                @unlink($file);
            }

            @rmdir($dir);              // .../plugins/system/htprotectwatch (nur wenn leer)
            @rmdir(dirname($dir));     // .../plugins/system                (nur wenn leer)
            @rmdir(dirname($dir, 2));  // .../plugins                       (nur wenn leer)
        } catch (\Throwable $e) {
        } catch (\Exception $e) {
        }
    }

    /**
     * Extension-IDs, die im aktuellen Request gemeinsam deinstalliert werden (die
     * cid-Auswahl aus com_installer / manage.remove). Leer, wenn die Deinstallation
     * NICHT ueber die Erweiterungs-Verwaltung laeuft (z. B. Self-Uninstall ueber den
     * HTProtect-Assistenten) -> dann kaskadiert die Komponente die Plugins wie gehabt.
     * Versionsneutral (J4+ getInput(), J3 ->input) und strikt fail-safe.
     *
     * @return array
     */
    private function currentUninstallBatchIds()
    {
        try {
            if (!class_exists('Joomla\\CMS\\Factory')) {
                return array();
            }

            $app = Joomla\CMS\Factory::getApplication();

            if (!$app) {
                return array();
            }

            $input = null;

            if (method_exists($app, 'getInput')) {
                $input = $app->getInput();
            } elseif (isset($app->input)) {
                $input = $app->input;
            }

            if (!is_object($input)) {
                return array();
            }

            // Nur der Sammel-Uninstall aus com_installer traegt eine cid-Auswahl.
            if ($input->getCmd('option', '') !== 'com_installer') {
                return array();
            }

            $cid = $input->get('cid', array(), 'array');

            if (!is_array($cid)) {
                return array();
            }

            return array_filter(array_map('intval', $cid));
        } catch (Exception $e) {
            return array();
        } catch (Throwable $e) {
            return array();
        }
    }

    /**
     * Die Auto-Update-Scheduler-Zeile (#__scheduler_tasks, Routine htp.autoupdate)
     * bei der Deinstallation entfernen. Best effort; darf nie hart fehlschlagen.
     *
     * @return void
     */
    private function removeSchedulerTask()
    {
        try {
            $db = $this->db();

            if (!$db) {
                return;
            }

            $del = $db->getQuery(true)
                ->delete($db->quoteName('#__scheduler_tasks'))
                ->where($db->quoteName('type') . ' = ' . $db->quote('htp.autoupdate'));
            $db->setQuery($del);
            $db->execute();
        } catch (Exception $e) {
            // egal
        } catch (Throwable $e) {
            // egal
        }
    }

    /**
     * Die eigene Blob-Tabelle (#__htprotect_store, ausgelagerte Feed-Caches) bei der
     * Deinstallation entfernen - "spurlos". Best effort; darf nie hart fehlschlagen.
     *
     * @return void
     */
    private function dropStoreTable()
    {
        try {
            $db = $this->db();

            if (!$db) {
                return;
            }

            $db->setQuery('DROP TABLE IF EXISTS ' . $db->quoteName('#__htprotect_store'));
            $db->execute();
        } catch (Exception $e) {
            // egal
        } catch (Throwable $e) {
            // egal
        }
    }

    /**
     * Ein mitgeliefertes Plugin restlos entfernen: bevorzugt sauber ueber den
     * Joomla-Installer (Dateien + Eintrag), sonst manuell (DB-Zeile +
     * Plugin-Verzeichnis). Faellt nie hart aus.
     *
     * @param   string  $element  Plugin-Element (z. B. htprotectwatch, htprotect)
     * @param   string  $folder   Plugin-Gruppe (z. B. system, quickicon)
     *
     * @return void
     */
    private function removePlugin($element, $folder, $batch = array())
    {
        try {
            $db = $this->db();

            if (!$db) {
                return;
            }

            $query = $db->getQuery(true)
                ->select($db->quoteName('extension_id'))
                ->from($db->quoteName('#__extensions'))
                ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
                ->where($db->quoteName('element') . ' = ' . $db->quote($element))
                ->where($db->quoteName('folder') . ' = ' . $db->quote($folder));
            $db->setQuery($query);
            $id = (int) $db->loadResult();

            // Steht dieses Plugin in DERSELBEN com_installer-Auswahl -> Joomla entfernt es
            // selbst; unsere Kaskade laesst es dann in Ruhe (kein "Error uninstalling .").
            if ($id > 0 && in_array($id, $batch, true)) {
                return;
            }

            $removed = false;

            // 1) Sauber ueber den Installer (frische Instanz -> keine Re-Entrancy
            //    mit dem laufenden Komponenten-Uninstall).
            if ($id > 0) {
                try {
                    $installer = null;

                    if (class_exists('Joomla\\CMS\\Installer\\Installer')) {
                        $installer = new Joomla\CMS\Installer\Installer();

                        // J4+: Installer ist DatabaseAwareInterface -> DB explizit
                        // setzen (ein blankes new Installer hat keine; uninstall()
                        // wuerde sonst mangels DB werfen). getInstance scheidet hier
                        // aus: es laeuft mitten im Komponenten-Uninstall (Re-Entrancy).
                        if ($db && method_exists($installer, 'setDatabase')) {
                            $installer->setDatabase($db);
                        }
                    } elseif (class_exists('JInstaller')) {
                        $installer = new JInstaller();
                    }

                    if ($installer) {
                        $removed = (bool) $installer->uninstall('plugin', $id);
                    }
                } catch (Exception $e) {
                    $removed = false;
                } catch (Throwable $e) {
                    $removed = false;
                }
            }

            // 2) Fallback: verwaisten Eintrag + Dateien manuell entfernen.
            if (!$removed) {
                $del = $db->getQuery(true)
                    ->delete($db->quoteName('#__extensions'))
                    ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
                    ->where($db->quoteName('element') . ' = ' . $db->quote($element))
                    ->where($db->quoteName('folder') . ' = ' . $db->quote($folder));
                $db->setQuery($del);
                $db->execute();

                $this->deleteDir($this->siteRoot() . '/plugins/' . $folder . '/' . $element);
            }
        } catch (Exception $e) {
            // Uninstall darf nie hart fehlschlagen.
        } catch (Throwable $e) {
            // PHP 7+ (Error/TypeError)
        }
    }

    /**
     * Reste des Hintergrund-Waechters im Temp-Ordner entfernen (best effort).
     *
     * @return void
     */
    private function removeWatchTempFiles()
    {
        try {
            $tmp = '';

            if (class_exists('Joomla\\CMS\\Factory')) {
                $app = Joomla\CMS\Factory::getApplication();
                $tmp = ($app && method_exists($app, 'get')) ? (string) $app->get('tmp_path') : '';
            } elseif (class_exists('JFactory')) {
                $cfg = JFactory::getConfig();
                $tmp = $cfg ? (string) $cfg->get('tmp_path') : '';
            }

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

            $tmp = rtrim($tmp, '/\\');

            foreach (array('htprotect_watch.state', 'htprotect_watch.lock', 'htprotect_waf.log', 'htprotect_waf.triggers', 'htprotect_status.json') as $name) {
                $file = $tmp . '/' . $name;

                if (is_file($file)) {
                    @unlink($file);
                }
            }
        } catch (Exception $e) {
            // egal
        } catch (Throwable $e) {
            // egal
        }
    }

    /**
     * Datenbank-Objekt versionsneutral (J2.5-6).
     *
     * @return object|null
     */
    private function db()
    {
        try {
            if (class_exists('Joomla\\CMS\\Factory')) {
                return Joomla\CMS\Factory::getDbo();
            }

            if (class_exists('JFactory')) {
                return JFactory::getDbo();
            }
        } catch (Exception $e) {
            // null
        }

        return null;
    }

    /**
     * Version aus dem GERADE installierten Komponenten-Manifest (on-disk htprotect.xml)
     * lesen - die Dateien liegen im postflight bereits an Ort und Stelle.
     *
     * @return string  '' wenn nicht lesbar
     */
    private function htpInstalledVersion()
    {
        $xml = JPATH_ADMINISTRATOR . '/components/com_htprotect/htprotect.xml';

        if (@is_file($xml)
            && preg_match('~<version>\s*([0-9][0-9.]*)\s*</version>~', (string) @file_get_contents($xml), $m)) {
            return $m[1];
        }

        return '';
    }

    /**
     * manifest_cache-Versionsfeld einer Erweiterung direkt in #__extensions nachziehen -
     * reiner Metadaten-Fix (KEIN Installer, keine Re-Entrancy). Schreibt nur bei echter
     * Abweichung. folder=null -> Bedingung entfaellt (Komponenten). Fail-silent.
     *
     * @param   string       $type     extensions.type
     * @param   string       $element  extensions.element
     * @param   string|null  $folder   extensions.folder (null = ignorieren)
     * @param   string       $newVer   Zielversion
     *
     * @return void
     */
    private function htpPatchExtVersion($type, $element, $folder, $newVer)
    {
        try {
            $db = $this->db();

            if (!$db) {
                return;
            }

            $q = $db->getQuery(true)
                ->select($db->quoteName(array('extension_id', 'manifest_cache')))
                ->from($db->quoteName('#__extensions'))
                ->where($db->quoteName('type') . ' = ' . $db->quote($type))
                ->where($db->quoteName('element') . ' = ' . $db->quote($element));

            if ($folder !== null && $folder !== '') {
                $q->where($db->quoteName('folder') . ' = ' . $db->quote($folder));
            }

            $db->setQuery($q);
            $row = $db->loadAssoc();

            if (!is_array($row) || empty($row['extension_id']) || empty($row['manifest_cache'])) {
                return;
            }

            $mc = json_decode((string) $row['manifest_cache'], true);

            if (!is_array($mc) || !isset($mc['version']) || (string) $mc['version'] === (string) $newVer) {
                return;
            }

            $mc['version'] = (string) $newVer;

            $u = $db->getQuery(true)
                ->update($db->quoteName('#__extensions'))
                ->set($db->quoteName('manifest_cache') . ' = ' . $db->quote(json_encode($mc)))
                ->where($db->quoteName('extension_id') . ' = ' . (int) $row['extension_id']);
            $db->setQuery($u);
            $db->execute();
        } catch (Exception $e) {
            // egal
        } catch (Throwable $e) {
            // egal
        }
    }

    /**
     * Joomla-Stammverzeichnis. script.php liegt unter
     * administrator/components/com_htprotect/.
     *
     * @return string
     */
    private function siteRoot()
    {
        if (defined('JPATH_ROOT')) {
            return JPATH_ROOT;
        }

        if (defined('JPATH_SITE')) {
            return JPATH_SITE;
        }

        return dirname(dirname(dirname(__DIR__)));
    }

    /**
     * Verzeichnis rekursiv entfernen (Joomla-Folder bevorzugt, sonst manuell).
     *
     * @param   string  $path  Pfad
     *
     * @return void
     */
    private function deleteDir($path)
    {
        if (!is_string($path) || $path === '' || !is_dir($path)) {
            return;
        }

        try {
            if (class_exists('Joomla\\CMS\\Filesystem\\Folder') && Joomla\CMS\Filesystem\Folder::delete($path)) {
                return;
            }

            if (class_exists('JFolder') && JFolder::delete($path)) {
                return;
            }
        } catch (Exception $e) {
            // manueller Fallback unten
        } catch (Throwable $e) {
            // manueller Fallback unten
        }

        $items = @scandir($path);

        if (is_array($items)) {
            foreach ($items as $item) {
                if ($item === '.' || $item === '..') {
                    continue;
                }

                $sub = $path . '/' . $item;

                // Symlinks NICHT hineinlaufen (nur den Link selbst entfernen) - sonst wuerde ein
                // in den eigenen Plugin-Ordner geschmuggelter Verzeichnis-Symlink beim
                // Deinstallieren fremde Inhalte loeschen (wie HtpUpdateBackup::rmTree es macht).
                if (is_link($sub)) {
                    @unlink($sub);
                } elseif (is_dir($sub)) {
                    $this->deleteDir($sub);
                } else {
                    @unlink($sub);
                }
            }
        }

        @rmdir($path);
    }

    /**
     * Joomla-Menu-Table versionsneutral (J3 JTable / J4-6 namespaced) beschaffen.
     * Die Table pflegt den Nested-Set (lft/rgt) selbst - nie roh per SQL anfassen.
     *
     * @param   object  $db  Datenbank
     *
     * @return object|null
     */
    private function menuTable($db)
    {
        try {
            if (class_exists('Joomla\\CMS\\Table\\Menu')) {
                return new Joomla\CMS\Table\Menu($db);
            }

            if (class_exists('JTable')) {
                return JTable::getInstance('menu');
            }
        } catch (Exception $e) {
            // null
        } catch (Throwable $e) {
            // null
        }

        return null;
    }

    /**
     * Backend-Komponenten-Unterpunkte mit der aktuellen Joomla-Version abgleichen:
     * Site-Scan + Hilfe & Support immer, Backups & Updates erst ab Joomla 5.4
     * (darunter bleibt es beim alten "Sicherungen"-Verhalten ohne eigenen Punkt).
     * Idempotent: legt fehlende an, entfernt unerwuenschte. Best effort.
     *
     * @return void
     */
    private function syncSubmenu()
    {
        try {
            $db = $this->db();

            if (!$db) {
                return;
            }

            // Haupt-Menuepunkt (aus dem Manifest <menu>) + dessen component_id finden.
            $query = $db->getQuery(true)
                ->select($db->quoteName(array('id', 'component_id')))
                ->from($db->quoteName('#__menu'))
                ->where($db->quoteName('client_id') . ' = 1')
                ->where($db->quoteName('menutype') . ' = ' . $db->quote('main'))
                ->where($db->quoteName('link') . ' = ' . $db->quote('index.php?option=com_htprotect'))
                ->where($db->quoteName('parent_id') . ' > 0');
            $db->setQuery($query);
            $parent = $db->loadAssoc();

            if (!is_array($parent) || (int) $parent['id'] <= 0) {
                return;
            }

            $parentId    = (int) $parent['id'];
            $componentId = (int) $parent['component_id'];

            // "Backups & Updates" bekommt ab Joomla 3.0 einen eigenen Komponenten-Unterpunkt:
            //  - 5.4+ und J3 (Legacy-Pfad): mit voller Auto-Update-Oberflaeche,
            //  - J4.0-<5.4: gleicher Name, aber statt der Auto-Update-UI ein Upgrade-auf-J5-Hinweis
            //    (vgl. HtpAutoUpdate::execEligible / upgradeRecommended, HtpUi::backupsSectionNamed).
            // Nur darunter (J2.5) bleibt es der interne "Sicherungen"-Bereich ohne eigenen Unterpunkt.
            $withBackups = defined('JVERSION') && version_compare(JVERSION, '3.0', '>=');

            $items = array(
                array('link' => 'index.php?option=com_htprotect&sec=overview', 'title' => 'COM_HTPROTECT_MENU_OVERVIEW', 'alias' => 'htprotect-overview',        'on' => true),
                // Notfall-Offline: Direkt-Sprung zum Notfall-Modus (Backend-Zugang, Anker #htp-emergency).
                // BEWUSST an ZWEITER Stelle - direkt hinter "Uebersicht" (before => guard sortiert ihn bei
                // Bestandsinstallationen vor "Backend-Zugang"; bei Neuinstallation greift die Array-Reihenfolge).
                array('link' => 'index.php?option=com_htprotect&sec=guard#htp-emergency', 'title' => 'COM_HTPROTECT_MENU_EMERGENCY', 'alias' => 'htprotect-emergency-offline', 'on' => true, 'before' => 'index.php?option=com_htprotect&sec=guard'),
                array('link' => 'index.php?option=com_htprotect&sec=guard',   'title' => 'COM_HTPROTECT_MENU_GUARD',   'alias' => 'htprotect-backend-access',   'on' => true, 'before' => 'index.php?option=com_htprotect&sec=shield'),
                array('link' => 'index.php?option=com_htprotect&sec=shield',   'title' => 'COM_HTPROTECT_MENU_SHIELD',   'alias' => 'htprotect-protection-shield', 'on' => true, 'before' => 'index.php?option=com_htprotect&sec=scan'),
                array('link' => 'index.php?option=com_htprotect&sec=scan',    'title' => 'COM_HTPROTECT_MENU_SCAN',    'alias' => 'htprotect-site-scan',       'on' => true),
                array('link' => 'index.php?option=com_htprotect&sec=backups', 'title' => 'COM_HTPROTECT_MENU_BACKUPS', 'alias' => 'htprotect-backups-updates',  'on' => $withBackups),
                array('link' => 'index.php?option=com_htprotect&sec=help',    'title' => 'COM_HTPROTECT_MENU_HELP',    'alias' => 'htprotect-help-support',     'on' => true),
                // Fernverwaltung: Einstieg in die Dashboard-Kopplung (?sec=connect). Bewusst als LETZTER
                // Komponenten-Unterpunkt (last-child). Der interne app.php-Nav zeigt 'connect' weiterhin
                // NICHT als Reiter - der Joomla-Menuepunkt ist der offizielle Einstieg.
                array('link' => 'index.php?option=com_htprotect&sec=connect', 'title' => 'COM_HTPROTECT_MENU_REMOTE',  'alias' => 'htprotect-remote-management', 'on' => true),
            );

            foreach ($items as $item) {
                $this->ensureSubmenuItem($db, $parentId, $componentId, $item);
            }
        } catch (Exception $e) {
            // Installation darf daran nie scheitern.
        } catch (Throwable $e) {
            // dito
        }
    }

    /**
     * Einen Unterpunkt sicherstellen (anlegen, falls fehlend) bzw. entfernen
     * (falls unerwuenscht). Nutzt ausschliesslich die Joomla-Menu-Table.
     *
     * @param   object  $db           Datenbank
     * @param   int     $parentId     Haupt-Menuepunkt
     * @param   int     $componentId  Komponenten-ID
     * @param   array   $item         ['link','title','alias','on']
     *
     * @return void
     */
    private function ensureSubmenuItem($db, $parentId, $componentId, $item)
    {
        try {
            $query = $db->getQuery(true)
                ->select($db->quoteName('id'))
                ->from($db->quoteName('#__menu'))
                ->where($db->quoteName('client_id') . ' = 1')
                ->where($db->quoteName('menutype') . ' = ' . $db->quote('main'))
                ->where($db->quoteName('link') . ' = ' . $db->quote($item['link']));
            $db->setQuery($query);
            $existingId = (int) $db->loadResult();

            // Unerwuenscht -> vorhandenen Punkt entfernen (z. B. Backups & Updates < 5.4).
            if (empty($item['on'])) {
                if ($existingId > 0) {
                    $table = $this->menuTable($db);

                    if ($table && $table->load($existingId)) {
                        $table->delete($existingId);
                    }
                }

                return;
            }

            // Schon vorhanden -> nichts tun (idempotent).
            if ($existingId > 0) {
                return;
            }

            $table = $this->menuTable($db);

            if (!$table) {
                return;
            }

            // Einsortieren: standardmaessig als letztes Kind. Ist 'before' gesetzt und der
            // Bezugs-Geschwisterpunkt existiert bereits (Bestandsinstallation), DAVOR
            // einsortieren -> korrekte Reihenfolge (Schutzschild VOR Site-Scan). Bei
            // Neuinstallation existiert der Nachbar noch nicht -> last-child; da die Punkte
            // in Array-Reihenfolge angelegt werden, stimmt die Reihenfolge trotzdem.
            $refId = 0;

            if (!empty($item['before'])) {
                $rq = $db->getQuery(true)
                    ->select($db->quoteName('id'))
                    ->from($db->quoteName('#__menu'))
                    ->where($db->quoteName('client_id') . ' = 1')
                    ->where($db->quoteName('menutype') . ' = ' . $db->quote('main'))
                    ->where($db->quoteName('link') . ' = ' . $db->quote($item['before']));
                $db->setQuery($rq);
                $refId = (int) $db->loadResult();
            }

            if ($refId > 0) {
                $table->setLocation($refId, 'before');
            } else {
                $table->setLocation($parentId, 'last-child');
            }
            $table->bind(array(
                'menutype'     => 'main',
                'title'        => $item['title'],
                'alias'        => $item['alias'],
                'link'         => $item['link'],
                'type'         => 'component',
                'published'    => 1,
                'parent_id'    => $parentId,
                'component_id' => $componentId,
                'client_id'    => 1,
                'language'     => '*',
                'access'       => 1,
                'home'         => 0,
                'params'       => '{}',
            ));

            if ($table->check()) {
                $table->store();
            }
        } catch (Exception $e) {
            // best effort
        } catch (Throwable $e) {
            // best effort
        }
    }

    /**
     * Deutsche Sprachvarianten (de-CH/AT/LI/LU/BE ...): Joomla laedt fuer das Backend-
     * Menue, den Erweiterungs-Manager und den Installations-Bildschirm die .sys.ini der
     * EXAKTEN Admin-Sprache. HTProtect liefert nur de-DE + en-GB; auf z. B. de-CH faellt
     * daher GENAU das Komponenten-Menue auf Englisch zurueck (die Komponente selbst bleibt
     * deutsch, weil sie de-* -> de-DE selbst mappt). Fix: die de-DE-.sys.ini in die
     * KANONISCHEN deutschen Varianten (de-AT/CH/LI/LU - Verzeichnis wird bei Bedarf
     * angelegt) SOWIE jede weitere bereits installierte de-*-Variante spiegeln (identischer
     * Inhalt, nur anderer Tag im Dateinamen). Idempotent + best effort - die Installation
     * scheitert nie daran.
     *
     * @return void
     */
    private function mirrorGermanSysIni()
    {
        try {
            $base = defined('JPATH_ADMINISTRATOR') ? JPATH_ADMINISTRATOR
                : (defined('JPATH_ROOT') ? JPATH_ROOT . '/administrator' : '');

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

            $src = $base . '/language/de-DE/de-DE.com_htprotect.sys.ini';

            if (!@is_file($src)) {
                return;
            }

            $content = @file_get_contents($src);

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

            $langDir = $base . '/language';

            // Ziel-Tags: die KANONISCHEN deutschen Varianten IMMER (Verzeichnis wird bei
            // Bedarf erzeugt -> das Menue ist sofort deutsch, sobald jemand darauf umstellt)
            // PLUS jede weitere bereits INSTALLIERTE de-*-Variante.
            $tags = array('de-AT', 'de-CH', 'de-LI', 'de-LU');
            $dirs = @scandir($langDir);

            if (is_array($dirs)) {
                foreach ($dirs as $d) {
                    if (preg_match('~^de-[A-Z]{2}$~', (string) $d) && strcasecmp((string) $d, 'de-DE') !== 0) {
                        $tags[] = (string) $d;
                    }
                }
            }

            foreach (array_unique($tags) as $tag) {
                $dir = $langDir . '/' . $tag;

                if (!@is_dir($dir)) {
                    @mkdir($dir, 0755); // Varianten-Verzeichnis anlegen (falls Sprache (noch) fehlt)
                    @file_put_contents($dir . '/index.html', '<!DOCTYPE html><title></title>');
                }

                if (!@is_dir($dir)) {
                    continue; // nicht anlegbar -> ueberspringen
                }

                $dest = $dir . '/' . $tag . '.com_htprotect.sys.ini';

                // Nur schreiben, wenn fehlend oder inhaltlich veraltet (idempotent).
                if (!@is_file($dest) || @file_get_contents($dest) !== $content) {
                    @file_put_contents($dest, $content);
                }
            }
        } catch (Exception $e) {
            // best effort - die Installation nie gefaehrden
        } catch (Throwable $e) {
            // best effort
        }
    }

    /**
     * Sicherstellen, dass die Erweiterungen aktiviert sind.
     *
     * - Komponente + Quickicon-Statuskachel werden IMMER aktiviert (deaktivierte
     *   Komponente = "Component not found" im Backend; die Kachel hat keinen Schalter).
     * - War die KOMPONENTE SELBST deaktiviert (jemand hat HTProtect komplett abgeschaltet,
     *   z. B. alle Teile unter Erweiterungen -> Verwalten), werden bei einem Update auch
     *   der Waechter + Auto-Update-Task WIEDER aktiviert (falls installiert) - voller
     *   Schutz wird hergestellt.
     * - War die Komponente hingegen AN und nur ein Plugin AUS, ist das ein BEWUSSTER
     *   Feature-Schalter (Ueberwachung / Auto-Update, per HTProtect-UI) -> NICHT anfassen.
     *
     * Best effort, faellt nie hart aus.
     *
     * @param   string  $type  install | update | discover_install
     *
     * @return  void
     */
    private function ensureExtensionsEnabled($type)
    {
        try {
            $db = $this->db();

            if (!$db) {
                return;
            }

            // War die Komponente vor diesem Lauf deaktiviert? (= "HTProtect komplett aus")
            $compWasOff = $this->extIsDisabled($db, 'component', 'com_htprotect', null);

            // Komponente + Quickicon-Kachel: IMMER aktiv.
            $this->enableExtension($db, 'component', 'com_htprotect', null);
            $this->enableExtension($db, 'plugin', 'htprotect', 'quickicon');

            // Nur wenn die Komponente breit abgeschaltet war: auch Waechter + Task zurueck
            // (falls vorhanden). Sonst respektieren wir die bewussten Feature-Schalter.
            if ($compWasOff) {
                $this->enableExtension($db, 'plugin', 'htprotectwatch', 'system');
                $this->enableExtension($db, 'plugin', 'htpautoupdate', 'task'); // erst ab J5.4 vorhanden
            }
        } catch (Exception $e) {
            // best effort
        } catch (Throwable $e) {
            // best effort
        }
    }

    /**
     * Ist die Erweiterung vorhanden UND deaktiviert (enabled=0)? Fehlt sie, false. Rein lesend.
     *
     * @return boolean
     */
    private function extIsDisabled($db, $type, $element, $folder)
    {
        try {
            $q = $db->getQuery(true)
                ->select($db->quoteName('enabled'))
                ->from($db->quoteName('#__extensions'))
                ->where($db->quoteName('type') . ' = ' . $db->quote($type))
                ->where($db->quoteName('element') . ' = ' . $db->quote($element));

            if ($folder !== null) {
                $q->where($db->quoteName('folder') . ' = ' . $db->quote($folder));
            }

            $db->setQuery($q, 0, 1);
            $enabled = $db->loadResult();

            return $enabled !== null && (int) $enabled === 0;
        } catch (Exception $e) {
            return false;
        } catch (Throwable $e) {
            return false;
        }
    }

    /**
     * Eine Erweiterung in #__extensions aktivieren (nur wenn aktuell deaktiviert -
     * kein unnoetiger Schreibvorgang). Best effort.
     *
     * @return void
     */
    private function enableExtension($db, $type, $element, $folder)
    {
        try {
            $q = $db->getQuery(true)
                ->update($db->quoteName('#__extensions'))
                ->set($db->quoteName('enabled') . ' = 1')
                ->where($db->quoteName('type') . ' = ' . $db->quote($type))
                ->where($db->quoteName('element') . ' = ' . $db->quote($element))
                ->where($db->quoteName('enabled') . ' = 0');

            if ($folder !== null) {
                $q->where($db->quoteName('folder') . ' = ' . $db->quote($folder));
            }

            $db->setQuery($q);

            if (method_exists($db, 'execute')) {
                $db->execute();
            } else {
                $db->query();
            }
        } catch (Exception $e) {
            // best effort
        } catch (Throwable $e) {
            // best effort
        }
    }

    /**
     * Gebuendelte Plugins (Echtzeit-Waechter, Quickicon-Kachel, Task-Scheduler) schon im
     * INSTALLER einrichten - damit eine frisch remote-installierte Site OHNE jeden Backend-
     * Aufruf laeuft und sich selbst im Dashboard registriert (der Selbst-Enroll-Tick lebt im
     * Waechter; ohne aktives Plugin feuert nichts, bis zufaellig ein Super-User die Komponente
     * oeffnet). Zwei harte Invarianten fuer die Re-Ausrollung ueber BESTEHENDE Installs:
     *  - NIE JInstaller (verschachtelter Installer im laufenden = Re-Entrancy/Absturz) -> reiner
     *    atomarer Datei-Copy + EINE #__extensions-Zeile ueber die Joomla-Table-API (kennt ihre
     *    Spalten J3..J6 selbst).
     *  - KEINE Duplikate: eingefuegt wird NUR, wenn noch keine Zeile existiert; der Existenz-Check
     *    ist fail-CLOSED (bei DB-Zweifel NICHT einfuegen). Auf schon installierten Sites = No-op.
     *
     * @return void
     */
    private function htpBootstrapBundledPlugins()
    {
        try {
            $db = $this->db();

            if (!$db) {
                return;
            }

            // Waechter (Echtzeit-Schutz + Selbst-Enroll-Tick) und Quickicon-Kachel: ALLE Versionen (J3-J6).
            $this->htpBootstrapOnePlugin($db, 'plg_htprotectwatch', 'system', 'htprotectwatch', 'htprotectwatch.php', 'plg_system_htprotectwatch');
            $this->htpBootstrapOnePlugin($db, 'plg_quickicon_htprotect', 'quickicon', 'htprotect', 'htprotect.php', 'plg_quickicon_htprotect');

            // Task-Scheduler-Plugin: erst ab Joomla 5.4 (davor gibt es die Task-Plugin-Gruppe /
            // com_scheduler NICHT - auf J3/J4 waere die Zeile tot). Auf J3 bewusst uebersprungen.
            if (defined('JVERSION') && version_compare(JVERSION, '5.4.0', '>=')) {
                $this->htpBootstrapOnePlugin($db, 'plg_task_htpautoupdate', 'task', 'htpautoupdate', 'htpautoupdate.php', 'plg_task_htpautoupdate');
            }
        } catch (Exception $e) {
            // best effort - der normale Sync beim ersten Komponenten-Aufruf faengt es sonst ab
        } catch (Throwable $e) {
            // best effort
        }
    }

    /**
     * Ein gebuendeltes Plugin engine-los einrichten - NUR wenn noch keine #__extensions-Zeile
     * existiert (sonst No-op, kein Duplikat). Dann Dateien aus den Komponenten-Assets in den
     * Plugin-Ordner spiegeln und GENAU EINE Zeile (enabled=1) anlegen. Fail-safe.
     *
     * @return void
     */
    private function htpBootstrapOnePlugin($db, $assetDir, $folder, $element, $mainFile, $name)
    {
        try {
            if (!defined('JPATH_ADMINISTRATOR')) {
                return;
            }

            $src = JPATH_ADMINISTRATOR . '/components/com_htprotect/assets/' . $assetDir;

            // Asset nicht mitgeliefert/vorhanden -> nichts tun.
            if (!@is_dir($src) || !@is_file($src . '/' . $mainFile)) {
                return;
            }

            // IDEMPOTENZ: existiert bereits eine Zeile (installiert, deaktiviert ODER nur entdeckt),
            // NICHTS einfuegen. Fail-closed: bei DB-Unsicherheit ebenfalls abbrechen (nie Duplikat).
            if ($this->htpPluginRowExists($db, $element, $folder) !== false) {
                return;
            }

            $pluginsDir = defined('JPATH_PLUGINS') ? JPATH_PLUGINS : JPATH_ROOT . '/plugins';
            $dst        = $pluginsDir . '/' . $folder . '/' . $element;

            // Dateien spiegeln (atomar je Datei, KEIN JInstaller).
            $this->htpCopyTree($src, $dst, 0);

            // Klassendatei nicht angekommen -> KEINE Zeile eintragen (sonst Eintrag ohne Dateien).
            if (!@is_file($dst . '/' . $mainFile)) {
                return;
            }

            $this->htpInsertPluginRow($element, $folder, $name);
        } catch (Exception $e) {
            // best effort
        } catch (Throwable $e) {
            // best effort
        }
    }

    /**
     * Existiert bereits eine #__extensions-Zeile fuer dieses Plugin (irgendein Zustand)?
     * true = ja ODER DB-Unsicherheit (fail-closed) ; false = definitiv keine.
     *
     * @return boolean
     */
    private function htpPluginRowExists($db, $element, $folder)
    {
        try {
            $q = $db->getQuery(true)
                ->select('COUNT(*)')
                ->from($db->quoteName('#__extensions'))
                ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
                ->where($db->quoteName('element') . ' = ' . $db->quote($element))
                ->where($db->quoteName('folder') . ' = ' . $db->quote($folder));

            $db->setQuery($q);

            return ((int) $db->loadResult()) > 0;
        } catch (Exception $e) {
            return true;  // unsicher -> als vorhanden behandeln (NICHT einfuegen)
        } catch (Throwable $e) {
            return true;
        }
    }

    /**
     * GENAU EINE Plugin-Zeile in #__extensions anlegen - versions-robust ueber die Joomla-Table-
     * API (J3 JTable / J4+ Table; die Table kennt ihre Spalten je Version selbst, ein ueber-
     * zaehliges Feld im bind() wird ignoriert). Nur aufrufen, wenn KEINE Zeile existiert.
     *
     * @return boolean
     */
    private function htpInsertPluginRow($element, $folder, $name)
    {
        try {
            $table = null;

            if (class_exists('Joomla\\CMS\\Table\\Table')) {
                $table = Joomla\CMS\Table\Table::getInstance('extension');
            } elseif (class_exists('JTable')) {
                $table = JTable::getInstance('extension');
            }

            if (!$table) {
                return false;
            }

            $data = array(
                'name'           => $name,
                'type'           => 'plugin',
                'element'        => $element,
                'folder'         => $folder,
                'client_id'      => 0,
                'enabled'        => 1,
                'access'         => 1,
                'protected'      => 0,
                'locked'         => 0,   // J4+ (auf J3 vom bind() ignoriert)
                'manifest_cache' => '',
                'params'         => '{}',
                'custom_data'    => '',
                'system_data'    => '',  // J4+ (auf J3 ignoriert)
                'state'          => 0,
                'ordering'       => 0,
                'package_id'     => 0,
            );

            // Frischer Datensatz (kein load) -> bind + store = INSERT.
            if (!$table->bind($data)) {
                return false;
            }

            return (bool) $table->store();
        } catch (Exception $e) {
            return false;
        } catch (Throwable $e) {
            return false;
        }
    }

    /**
     * Verzeichnisbaum rekursiv (Tiefen-Deckel) spiegeln - je Datei ATOMAR (temp + rename), damit
     * nie eine halbe Datei liegt. Fail-safe, versionsneutral (kein JFolder noetig).
     *
     * @return void
     */
    private function htpCopyTree($src, $dst, $depth)
    {
        if ($depth > 8 || !@is_dir($src)) {
            return;
        }

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

        $items = @scandir($src);

        if (!is_array($items)) {
            return;
        }

        foreach ($items as $entry) {
            if ($entry === '.' || $entry === '..') {
                continue;
            }

            $s = $src . '/' . $entry;
            $d = $dst . '/' . $entry;

            if (@is_dir($s)) {
                $this->htpCopyTree($s, $d, $depth + 1);
            } elseif (@is_file($s)) {
                $tmp = $d . '.htpnew';

                if (@copy($s, $tmp) && @filesize($tmp) === @filesize($s)) {
                    @rename($tmp, $d);
                } else {
                    @unlink($tmp);
                }
            }
        }
    }

    /**
     * OPcache fuer HTProtects eigene PHP-Dateien gezielt invalidieren, nachdem der
     * Installer sie ueberschrieben hat. Ohne das serviert PHP nach einem manuellen
     * "Installieren von URL"/Update weiter das ALTE Bytecode (z. B. bootstrap.php ->
     * Versionsanzeige haengt auf der alten Version), bis OPcache die geaenderten Dateien
     * von selbst neu einliest (revalidate_freq). GEZIELT je Datei (force=true), NIE
     * opcache_reset() - das wuerde den Cache FREMDER Sites/Extensions mit leeren.
     * Fail-safe (OPcache aus/unvollstaendig -> stiller No-op).
     *
     * @return void
     */
    private function htpInvalidateOpcache()
    {
        if (!function_exists('opcache_invalidate') || !defined('JPATH_ADMINISTRATOR')) {
            return;
        }

        $dirs       = array(JPATH_ADMINISTRATOR . '/components/com_htprotect');
        $pluginsDir = defined('JPATH_PLUGINS') ? JPATH_PLUGINS : (defined('JPATH_ROOT') ? JPATH_ROOT . '/plugins' : '');

        if (defined('JPATH_ROOT')) {
            $dirs[] = JPATH_ROOT . '/components/com_htprotect';
        }

        if ($pluginsDir !== '') {
            $dirs[] = $pluginsDir . '/system/htprotectwatch';
            $dirs[] = $pluginsDir . '/quickicon/htprotect';
            $dirs[] = $pluginsDir . '/task/htpautoupdate';
        }

        foreach ($dirs as $dir) {
            $this->htpInvalidateOpcacheDir($dir, 0);
        }
    }

    /**
     * Rekursiv (mit Tiefen-Deckel) alle *.php eines Ordners aus dem OPcache werfen.
     *
     * @param   string   $dir
     * @param   integer  $depth
     *
     * @return void
     */
    private function htpInvalidateOpcacheDir($dir, $depth)
    {
        if ($depth > 8 || !@is_dir($dir)) {
            return;
        }

        $items = @scandir($dir);

        if (!is_array($items)) {
            return;
        }

        foreach ($items as $name) {
            if ($name === '.' || $name === '..') {
                continue;
            }

            $path = $dir . '/' . $name;

            if (@is_dir($path)) {
                $this->htpInvalidateOpcacheDir($path, $depth + 1);
            } elseif (substr($name, -4) === '.php') {
                @opcache_invalidate($path, true);
            }
        }
    }

    public function postflight($type, $parent)
    {
        if ($type !== 'install' && $type !== 'update' && $type !== 'discover_install') {
            return true;
        }

        // Komponente (und bei Erstinstallation die Plugins) verlaesslich aktivieren -
        // eine deaktivierte Komponente liefert im Backend "Component not found".
        $this->ensureExtensionsEnabled($type);

        // Gebuendelte Plugins (Waechter/Quickicon/Task) SCHON HIER einrichten, damit eine frisch
        // remote-installierte Site OHNE jeden Backend-Aufruf laeuft UND sich selbst im Dashboard
        // registriert (der Selbst-Enroll-Tick lebt im Waechter). Raw (Datei-Copy + EINE Zeile via
        // Table-API), NIE JInstaller (Re-Entrancy) und strikt idempotent - eingefuegt wird NUR,
        // wenn keine Zeile existiert (Duplikat waere fatal). Auf Bestandsinstallationen = No-op.
        $this->htpBootstrapBundledPlugins();

        // Versionsabhaengige Backend-Komponenten-Unterpunkte anlegen/auffrischen
        // (Site-Scan + Hilfe immer; Backups & Updates erst ab Joomla 5.4). Faellt
        // nie hart aus - die Installation darf daran nicht scheitern.
        $this->syncSubmenu();

        // Deutsche Sprachvarianten (de-CH/AT/LI/LU ...): das Joomla-verwaltete Backend-
        // Menue + der Erweiterungs-Manager laden die .sys.ini der EXAKTEN Admin-Sprache;
        // HTProtect liefert nur de-DE -> auf de-CH & Co. waere genau das Komponenten-Menue
        // englisch. Darum die de-DE-.sys.ini in jede installierte de-*-Variante spiegeln.
        $this->mirrorGermanSysIni();

        // Das Hintergrund-Plugin wird hier NICHT via Installer (re)installiert -
        // ein verschachtelter JInstaller-Aufruf waehrend der laufenden Installation
        // fuehrt zu Re-Entrancy (Stack-Overflow). Die DB-Registrierung/Version macht
        // der normale Sync beim ersten Komponenten-Aufruf. ABER: ist das Plugin
        // BEREITS installiert, frischen wir seine CODE-Datei jetzt mit einem REINEN,
        // ATOMAREN Datei-Copy auf (kein Installer) -> Bugfixes am Plugin greifen SOFORT
        // ab dem naechsten Request, nicht erst beim naechsten HTProtect-Aufruf. Fail-safe.
        try {
            $watchSrc   = JPATH_ADMINISTRATOR . '/components/com_htprotect/assets/plg_htprotectwatch/htprotectwatch.php';
            $pluginsDir = defined('JPATH_PLUGINS') ? JPATH_PLUGINS : JPATH_ROOT . '/plugins';
            $watchDst   = $pluginsDir . '/system/htprotectwatch/htprotectwatch.php';

            if (@is_file($watchSrc) && @is_file($watchDst) && @md5_file($watchSrc) !== @md5_file($watchDst)) {
                $tmp = $watchDst . '.htpnew';

                if (@copy($watchSrc, $tmp) && @filesize($tmp) === @filesize($watchSrc)) {
                    @rename($tmp, $watchDst); // atomar - es liegt nie eine halbe Datei
                } else {
                    @unlink($tmp);
                }
            }
        } catch (\Throwable $e) {
            // egal - der normale Sync holt es beim naechsten Komponenten-Aufruf nach
        } catch (\Exception $e) {
            // egal
        }

        // Crash-festen Rescue-Endpunkt (2.6.6) bereitstellen. Er wird DIREKT per HTTP abgerufen und
        // laeuft OHNE Joomla-Boot - genau dann, wenn der normale com_ajax-Weg im Crash nicht mehr
        // greift. Deshalb MUSS er im WEB-Docroot liegen: im Public-Folder-Modus (Joomla 5.1+) ist das
        // public/ (JPATH_PUBLIC), NICHT JPATH_PLUGINS (das liegt dort OBERHALB des Webroots und wird
        // fuer die URL /plugins/system/htprotectwatch/rescue.php nie ausgeliefert -> sonst 404).
        // In klassischen Installs ist JPATH_PUBLIC == JPATH_ROOT -> exakt derselbe Ort wie bisher
        // (JPATH_ROOT/plugins/system/htprotectwatch). Auf BESTANDSSITES ist die Datei NEU -> das Ziel
        // fehlt (im PF-Modus existiert public/plugins noch gar nicht); daher Ordner bei Bedarf anlegen
        // und immer kopieren, nicht nur bei md5-Abweichung. Reiner atomarer Datei-Copy (kein Installer).
        // Die Waechter-CODE-Datei (htprotectwatch.php) bleibt bewusst in JPATH_PLUGINS - Joomla laedt
        // sie ueber das Dateisystem, nicht per HTTP.
        try {
            $rescueSrc = JPATH_ADMINISTRATOR . '/components/com_htprotect/assets/plg_htprotectwatch/rescue.php';
            $webBase   = (defined('JPATH_PUBLIC') && JPATH_PUBLIC)
                ? JPATH_PUBLIC
                : (defined('JPATH_ROOT') ? JPATH_ROOT : dirname(defined('JPATH_PLUGINS') ? JPATH_PLUGINS : ''));
            $rescueDir = rtrim(str_replace('\\', '/', (string) $webBase), '/') . '/plugins/system/htprotectwatch';
            $rescueDst = $rescueDir . '/rescue.php';

            if (@is_file($rescueSrc)) {
                if (!@is_dir($rescueDir)) {
                    @mkdir($rescueDir, 0755, true); // im PF-Modus existiert public/plugins/... noch nicht
                }

                if (@is_dir($rescueDir)
                    && (!@is_file($rescueDst) || @md5_file($rescueSrc) !== @md5_file($rescueDst))) {
                    $tmp = $rescueDst . '.htpnew';

                    if (@copy($rescueSrc, $tmp) && @filesize($tmp) === @filesize($rescueSrc)) {
                        @rename($tmp, $rescueDst); // atomar
                    } else {
                        @unlink($tmp);
                    }
                }
            }
        } catch (\Throwable $e) {
            // egal - htpBootstrapBundledPlugins/htpCopyTree holt es sonst beim naechsten Lauf nach
        } catch (\Exception $e) {
            // egal
        }

        // Sprachdateien des Task-Plugins (Erweiterungs-Auto-Update) frisch halten: der
        // Joomla-Scheduler zeigt die Task-Beschreibung aus diesen .ini-Dateien. Ein
        // reines Component-Update aktualisiert das separat installierte Plugin NICHT,
        // daher hier reiner atomarer Datei-Copy aus den Komponenten-Assets in BEIDE
        // moeglichen Lade-Orte (Plugin-Ordner + administrator/language). Fail-safe.
        try {
            $pluginsDir = defined('JPATH_PLUGINS') ? JPATH_PLUGINS : JPATH_ROOT . '/plugins';
            $langSrc    = JPATH_ADMINISTRATOR . '/components/com_htprotect/assets/plg_task_htpautoupdate/language';
            $langDsts   = array(
                $pluginsDir . '/task/htpautoupdate/language',
                JPATH_ADMINISTRATOR . '/language',
            );
            $langFiles  = array(
                'de-DE/de-DE.plg_task_htpautoupdate.ini',
                'de-DE/de-DE.plg_task_htpautoupdate.sys.ini',
                'en-GB/en-GB.plg_task_htpautoupdate.ini',
                'en-GB/en-GB.plg_task_htpautoupdate.sys.ini',
            );

            foreach ($langDsts as $dstBase) {
                foreach ($langFiles as $rel) {
                    $src = $langSrc . '/' . $rel;
                    $dst = $dstBase . '/' . $rel;

                    if (@is_file($src) && @is_file($dst) && @md5_file($src) !== @md5_file($dst)) {
                        $tmp = $dst . '.htpnew';

                        if (@copy($src, $tmp) && @filesize($tmp) === @filesize($src)) {
                            @rename($tmp, $dst); // atomar
                        } else {
                            @unlink($tmp);
                        }
                    }
                }
            }
        } catch (\Throwable $e) {
            // egal
        } catch (\Exception $e) {
            // egal
        }

        // ── Versions-Registrierung ZUVERLAESSIG am Ende der Installation finalisieren ──
        // Bei einem Self-Update (HTProtect aktualisiert sich selbst) schreibt Joomla die
        // eigene Komponenten-Version nicht immer sauber nach #__extensions -> manifest_cache
        // haengt auf alt: "Verwalten" zeigt alt UND Joomla bietet dasselbe Update endlos
        // erneut an. Und das separat installierte Task-Plugin zieht ein Component-Update
        // NICHT mit. Daher hier - als letzter Install-Schritt, verlaesslich bei JEDEM Update,
        // OHNE dass jemand spaeter das Backend oeffnen muss - beides direkt nachziehen.
        // Reiner Metadaten-/Datei-Sync (KEIN verschachtelter Installer -> keine Re-Entrancy).
        try {
            $newVer     = $this->htpInstalledVersion();
            $pluginsDir = defined('JPATH_PLUGINS') ? JPATH_PLUGINS : JPATH_ROOT . '/plugins';

            if ($newVer !== '') {
                // a) Komponenten-Version finalisieren.
                $this->htpPatchExtVersion('component', 'com_htprotect', null, $newVer);

                // b) Task-Plugin: Code-/Manifest atomar auffrischen + Version nachziehen.
                $taskSrc = JPATH_ADMINISTRATOR . '/components/com_htprotect/assets/plg_task_htpautoupdate';
                $taskDst = $pluginsDir . '/task/htpautoupdate';

                if (@is_dir($taskSrc) && @is_dir($taskDst)) {
                    foreach (array('htpautoupdate.php', 'htpautoupdate.xml') as $rel) {
                        $s = $taskSrc . '/' . $rel;
                        $d = $taskDst . '/' . $rel;

                        if (@is_file($s) && @md5_file($s) !== @md5_file($d)) {
                            $tmp = $d . '.htpnew';

                            if (@copy($s, $tmp) && @filesize($tmp) === @filesize($s)) {
                                @rename($tmp, $d); // atomar - nie eine halbe Datei
                            } else {
                                @unlink($tmp);
                            }
                        }
                    }

                    $this->htpPatchExtVersion('plugin', 'htpautoupdate', 'task', $newVer);
                }

                // c) Quickicon-Statuskachel: Code + Manifest atomar auffrischen + Version nachziehen
                //    (analog Waechter/Task), damit ALLE gebuendelten Plugins DIREKT beim Update auf
                //    dem aktuellen Stand sind - auch auf headless/remote-installierten Fleets, ohne
                //    dass jemand erst das Backend oeffnen muss (bisher zog nur der Runtime-Sync die
                //    Kachel beim naechsten Backend-Aufruf nach). Streng sicher: nur wenn das Plugin
                //    BEREITS installiert ist (Zielordner existiert) -> kein Duplikat; reiner atomarer
                //    Datei-Copy (Temp+rename), KEIN JInstaller -> keine Re-Entrancy; der Enabled-/
                //    Deaktiviert-Zustand bleibt unberuehrt (nur Dateiinhalt + manifest_cache-Version).
                $qiSrc = JPATH_ADMINISTRATOR . '/components/com_htprotect/assets/plg_quickicon_htprotect';
                $qiDst = $pluginsDir . '/quickicon/htprotect';

                if (@is_dir($qiSrc) && @is_dir($qiDst)) {
                    foreach (array('htprotect.php', 'htprotect.xml') as $rel) {
                        $s = $qiSrc . '/' . $rel;
                        $d = $qiDst . '/' . $rel;

                        if (@is_file($s) && @md5_file($s) !== @md5_file($d)) {
                            $tmp = $d . '.htpnew';

                            if (@copy($s, $tmp) && @filesize($tmp) === @filesize($s)) {
                                @rename($tmp, $d); // atomar - nie eine halbe Datei
                            } else {
                                @unlink($tmp);
                            }
                        }
                    }

                    $this->htpPatchExtVersion('plugin', 'htprotect', 'quickicon', $newVer);
                }
            }
        } catch (\Throwable $e) {
            // egal - der Backstop (HtpAutoUpdate::runStep / Admin-Aufruf) holt es nach
        } catch (\Exception $e) {
            // egal
        }

        // ── Einmalige Migration (2.4.15): Selbst-Update + Schild-Selbstheilung fuer BESTAND ──
        // Neuinstallationen haben beide Schalter per Default AN. Bestehende Installs bekommen sie
        // mit 2.4.15 EINMALIG aktiviert (Nutzerwunsch: Schutz bleibt ohne Zutun aktuell). Idempotent
        // per Marker mig_selfon_2415 -> danach freie Wahl des Admins (eine spaetere Abschaltung
        // bleibt bestehen). Reiner Metadaten-Sync in #__extensions.params (kein Installer). Fail-safe.
        try {
            $db = null;

            if (class_exists('Joomla\\CMS\\Factory') && method_exists('Joomla\\CMS\\Factory', 'getDbo')) {
                $db = Joomla\CMS\Factory::getDbo();
            } elseif (class_exists('JFactory')) {
                $db = JFactory::getDbo();
            }

            if ($db) {
                $q = $db->getQuery(true)
                    ->select($db->quoteName('params'))
                    ->from($db->quoteName('#__extensions'))
                    ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
                    ->where($db->quoteName('element') . ' = ' . $db->quote('com_htprotect'));
                $db->setQuery($q);
                $raw    = (string) $db->loadResult();
                $params = ($raw !== '') ? json_decode($raw, true) : null;

                // Nur Bestand (htp_shield existiert) und nur EINMAL (Marker noch nicht gesetzt).
                if (is_array($params) && isset($params['htp_shield']) && is_array($params['htp_shield'])
                    && empty($params['htp_shield']['mig_selfon_2415'])) {
                    $params['htp_shield']['autoupdate_self'] = 1;
                    $params['htp_shield']['shield_autoheal'] = 1;
                    $params['htp_shield']['mig_selfon_2415'] = 1;

                    $u = $db->getQuery(true)
                        ->update($db->quoteName('#__extensions'))
                        ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
                        ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
                        ->where($db->quoteName('element') . ' = ' . $db->quote('com_htprotect'));
                    $db->setQuery($u);
                    $db->execute();
                }
            }
        } catch (\Throwable $e) {
            // egal - der Admin kann beide Schalter jederzeit selbst setzen
        } catch (\Exception $e) {
            // egal
        }

        // ── "Nur fuer Super-User" (su_only) am com_htprotect-Asset durchsetzen ──
        // Standard (frische Installation ohne gesetzten Wert) = AN: core.manage fuer die Gruppe
        // "Oeffentlich" (1) auf Verboten -> der Komponenten-Menuepunkt und die Statuskachel
        // verschwinden fuer ALLE Nicht-Super-User (der Deny vererbt sich an jede Gruppe; Super-User
        // umgehen ACL via core.admin -> nie Selbst-Aussperren). Bei jedem Update den gespeicherten
        // Stand erneut anwenden (idempotent). Reiner #__assets-Metadaten-Sync, versionssicher
        // (J2.5-6), fail-safe. Deckungsgleich mit HtpApp::syncSuOnlyAcl (dort fuer den Live-Schalter).
        try {
            $db = null;

            if (class_exists('Joomla\\CMS\\Factory') && method_exists('Joomla\\CMS\\Factory', 'getDbo')) {
                $db = Joomla\CMS\Factory::getDbo();
            } elseif (class_exists('JFactory')) {
                $db = JFactory::getDbo();
            }

            if ($db) {
                // Gespeicherten su_only-Wert lesen (Default AN, wenn nicht gesetzt = frische Installation).
                $q = $db->getQuery(true)
                    ->select($db->quoteName('params'))
                    ->from($db->quoteName('#__extensions'))
                    ->where($db->quoteName('type') . ' = ' . $db->quote('component'))
                    ->where($db->quoteName('element') . ' = ' . $db->quote('com_htprotect'));
                $db->setQuery($q);
                $raw    = (string) $db->loadResult();
                $params = ($raw !== '') ? json_decode($raw, true) : null;
                $shield = (is_array($params) && isset($params['htp_shield']) && is_array($params['htp_shield'])) ? $params['htp_shield'] : array();
                $enable = !array_key_exists('su_only', $shield) || !empty($shield['su_only']);

                // Asset-Regeln lesen + core.manage fuer "Oeffentlich" (Gruppe 1) setzen/entfernen.
                $q = $db->getQuery(true)
                    ->select($db->quoteName(array('id', 'rules')))
                    ->from($db->quoteName('#__assets'))
                    ->where($db->quoteName('name') . ' = ' . $db->quote('com_htprotect'));
                $db->setQuery($q, 0, 1);
                $asset = $db->loadObject();

                if ($asset) {
                    $rules = json_decode((string) $asset->rules, true);

                    if (!is_array($rules)) {
                        $rules = array();
                    }

                    $manage = (isset($rules['core.manage']) && is_array($rules['core.manage'])) ? $rules['core.manage'] : array();

                    if ($enable) {
                        $manage['1'] = 0; // Public = Verboten
                    } else {
                        unset($manage['1']); // zurueck auf Vererbung
                    }

                    if (empty($manage)) {
                        unset($rules['core.manage']);
                    } else {
                        $rules['core.manage'] = $manage;
                    }

                    $u = $db->getQuery(true)
                        ->update($db->quoteName('#__assets'))
                        ->set($db->quoteName('rules') . ' = ' . $db->quote(json_encode($rules)))
                        ->where($db->quoteName('id') . ' = ' . (int) $asset->id);
                    $db->setQuery($u);
                    $db->execute();
                }
            }
        } catch (\Throwable $e) {
            // egal - der Live-Schalter (HtpApp::syncSuOnlyAcl) zieht es beim naechsten Speichern nach
        } catch (\Exception $e) {
            // egal
        }

        // ── Masseninstallation: vorgemerkten Selbst-Enroll aus der beigelegten Enroll-Datei uebernehmen ──
        // Das Download-Paket (Dashboard) legt admin/defaults/htprotect_enroll.json bei (workspace-weites,
        // widerrufbares Enroll-Token). Der Installer geht hier NICHT ans Netz (Engine nicht geladen, wuerde
        // blockieren): das Token wandert in den geschuetzten data/-Ordner (0600) als Pending-Marker, die
        // web-nahe Enroll-Datei wird SOFORT geloescht. Den eigentlichen Enroll holt der Hintergrund-
        // Waechter-Tick / Backend-Aufruf nach (HtpLink::enrollRetry). Laeuft bei Install UND Update.
        // Idempotent (Marker), fail-safe (die Installation darf daran nie scheitern).
        try {
            if (defined('JPATH_ADMINISTRATOR')) {
                $htpBase   = JPATH_ADMINISTRATOR . '/components/com_htprotect';
                $htpEnroll = $htpBase . '/defaults/htprotect_enroll.json';

                if (is_file($htpEnroll)) {
                    $ej  = json_decode((string) @file_get_contents($htpEnroll), true);
                    $tok = (is_array($ej) && !empty($ej['token'])) ? (string) $ej['token'] : '';

                    if ($tok !== '' && preg_match('~^[A-Za-z0-9_\-]{16,200}$~', $tok)) {
                        $htpData = $htpBase . '/data';

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

                        if (is_dir($htpData)) {
                            // Datenordner gegen Web-Zugriff haerten (falls frisch angelegt).
                            if (!is_file($htpData . '/.htaccess')) {
                                @file_put_contents($htpData . '/.htaccess', "<IfModule mod_authz_core.c>\nRequire all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\nOrder allow,deny\nDeny from all\n</IfModule>\n");
                            }

                            if (!is_file($htpData . '/index.html')) {
                                @file_put_contents($htpData . '/index.html', '<!DOCTYPE html><title></title>');
                            }

                            // BEWUSST KEIN 'relay' aus der (unsignierten) Enroll-Datei uebernehmen: der
                            // Relay ist im Connector fest hinterlegt (HtpLink::RELAY). Ein extern gesetztes
                            // relay-Feld waere reine Angriffsflaeche (Umlenkung auf fremden Relay -> Pinnen
                            // eines fremden ws_root_pub -> Fernuebernahme).
                            $marker = json_encode(array(
                                'token' => $tok,
                                'ts'    => time(),
                            ));

                            if (@file_put_contents($htpData . '/htprotect_enroll_pending.json', $marker, LOCK_EX) !== false) {
                                @chmod($htpData . '/htprotect_enroll_pending.json', 0600);
                            }

                            // Naechsten Waechter-Tick vorziehen (State-Datei auf "faellig", 901 = INTERVAL+1),
                            // damit der Enroll moeglichst beim naechsten Request laeuft statt erst in ~15 Min.
                            // Bei Frischinstallation existiert die Datei noch nicht: touch() legt sie leer an UND
                            // altert sie zugleich -> der erste Tick feuert sofort statt erst nach dem vollen
                            // Intervall (ein leerer State ist gueltig - der Waechter liest ihn als frisch). Auf
                            // Bestandsinstallationen wird nur der ohnehin faellige Tick vorgezogen.
                            @touch($htpData . '/htprotect_watch.state', time() - 901);
                        }
                    }

                    // Web-nahe Enroll-Datei IMMER entfernen - kein Klartext-Token unter administrator/
                    // liegen lassen (auch bei ungueltigem Token).
                    @unlink($htpEnroll);
                }
            }
        } catch (\Throwable $e) {
            // egal - ohne Marker passiert nichts; die Site bleibt normal koppelbar
        } catch (\Exception $e) {
            // egal
        }

        // OPcache fuer HTProtects gerade ueberschriebene PHP-Dateien gezielt leeren,
        // damit die neue Version SOFORT laeuft. Sonst serviert PHP nach einer manuellen
        // "Installation von URL"/einem Update weiter das ALTE Bytecode (z. B. bootstrap.php
        // -> Versionsanzeige oben rechts haengt auf der alten Version), bis OPcache die
        // geaenderten Dateien von selbst neu einliest (revalidate_freq; auf manchen Servern
        // spuerbar verzoegert). Der Auto-Update-Pfad macht das bereits; hier ziehen wir den
        // MANUELLEN Weg nach. Fail-safe.
        $this->htpInvalidateOpcache();

        $url = 'index.php?option=com_htprotect';

        // Handbuch-PDF sprachrichtig (Deutsch -> .../htprotect-dokumentation.pdf,
        // sonst die englische Fassung). Echter Download via htpDlDoc() unten.
        $htpDe = false;

        try {
            $lg = class_exists('Joomla\\CMS\\Factory') ? Joomla\CMS\Factory::getLanguage()
                : (class_exists('JFactory') ? JFactory::getLanguage() : null);

            if ($lg && method_exists($lg, 'getTag')) {
                $htpDe = strncasecmp((string) $lg->getTag(), 'de', 2) === 0;
            }
        } catch (\Throwable $e) {
        } catch (\Exception $e) {
        }

        $docUrl  = $htpDe ? 'https://htprotect.org/htprotect-dokumentation.pdf' : 'https://htprotect.org/htprotect-documentation-en.pdf';
        $docFile = $htpDe ? 'htprotect-dokumentation.pdf' : 'htprotect-documentation-en.pdf';

        echo '<style>'
            // Bootstrap 2 (Joomla 3 / isis) setzt box-sizing NICHT global auf border-box wie
            // J4+ (Atum). Ohne das rechnet der mobile width:100%-Button sein Padding oben drauf
            // und laeuft aus der Karte -> nur in J3 sichtbar. Fuer die eigene Karte erzwingen
            // (in J4+ ohnehin so -> No-op, keine Aenderung dort).
            . '.htpx-postflight,.htpx-postflight *{box-sizing:border-box}'
            . '.htpx-postflight{background:#10131c;color:#e6e8ee;padding:26px;border-radius:10px;margin:16px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;line-height:1.55;border:1px solid #262b3a}'
            . '.htpx-postflight h2{color:#34d27b;margin:0 0 12px;font-size:1.2rem;display:flex;align-items:center;gap:10px}'
            . '.htpx-postflight h2 .htpx-i{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;background:#34d27b;border-radius:8px;color:#08120c;font-weight:700}'
            . '.htpx-postflight p{margin:0 0 12px;color:#aab0c0}'
            . '.htpx-postflight strong{color:#e6e8ee}'
            . '.htpx-postflight a.htpx-go{display:inline-block;background:#34d27b;color:#08120c !important;padding:12px 26px;border-radius:8px;text-decoration:none !important;font-weight:700;margin-top:6px;font-size:1rem}'
            . '.htpx-postflight a.htpx-go:hover{background:#2bbf6e;text-decoration:none}'
            . '.htpx-postflight code{background:#070a10;color:#34d27b;padding:2px 8px;border-radius:4px;font-size:.85em;font-family:Consolas,Menlo,monospace}'
            . '.htpx-postflight .htpx-foot{color:#6b7185;font-size:.85rem;margin-top:16px;border-top:1px solid #262b3a;padding-top:12px}'
            . '.htpx-postflight a.htpx-fl{color:#34d27b;text-decoration:none}'
            . '.htpx-postflight a.htpx-fl:hover{text-decoration:underline}'
            . '.htpx-postflight a.htpx-doc{display:flex;align-items:center;gap:16px;text-decoration:none !important;background:#f4f8ff;color:#10131c !important;padding:18px 20px;border-radius:12px;margin:0 0 22px;box-shadow:0 8px 26px rgba(0,0,0,.35);border:1px solid #d9e2f2;transition:transform .12s ease,box-shadow .12s ease}'
            . '.htpx-postflight a.htpx-doc:hover{transform:translateY(-2px);box-shadow:0 12px 34px rgba(0,0,0,.5);text-decoration:none}'
            . '.htpx-postflight .htpx-doc-ic{flex:0 0 auto;display:inline-flex;width:54px;height:54px;align-items:center;justify-content:center;background:#eafaf1;border-radius:12px}'
            . '.htpx-postflight .htpx-doc-ic svg{width:34px;height:34px;display:block}'
            . '.htpx-postflight .htpx-doc-tx{flex:1 1 auto;min-width:0}'
            . '.htpx-postflight .htpx-doc-h{display:block;font-weight:800;font-size:1.12rem;color:#10131c;line-height:1.25}'
            . '.htpx-postflight .htpx-doc-s{display:block;font-size:.9rem;color:#5b6472;margin-top:2px}'
            . '.htpx-postflight .htpx-doc-btn{flex:0 0 auto;background:#34d27b;color:#08120c !important;padding:11px 20px;border-radius:9px;font-weight:800;font-size:.95rem;white-space:nowrap;box-shadow:0 2px 8px rgba(52,210,123,.4)}'
            . '@media(max-width:520px){.htpx-postflight a.htpx-doc{flex-wrap:wrap}.htpx-postflight .htpx-doc-tx{flex-basis:60%}.htpx-postflight .htpx-doc-btn{width:100%;text-align:center;margin-top:4px}}'
            . '</style>';

        // Echter Datei-Download (kein Inline-Oeffnen): laedt das PDF per fetch als Blob und
        // stoesst den Download an. Faellt sauber auf normales Oeffnen zurueck, falls fetch/
        // CORS/CSP das verhindert - so bricht der Link nie.
        echo '<script>function htpDlDoc(a,e){try{e.preventDefault();fetch(a.href).then(function(r){if(!r.ok){throw 0}return r.blob()}).then(function(b){var u=URL.createObjectURL(b);var l=document.createElement("a");l.href=u;l.download=a.getAttribute("download")||"";document.body.appendChild(l);l.click();l.remove();setTimeout(function(){URL.revokeObjectURL(u)},5000)}).catch(function(){window.open(a.href,"_blank","noopener")})}catch(x){window.open(a.href,"_blank","noopener")}return false}</script>';

        echo '<div class="htpx-postflight">'
            . '<a href="' . htmlspecialchars($docUrl, ENT_QUOTES, 'UTF-8') . '" download="' . $docFile . '" target="_blank" rel="noopener" class="htpx-doc" onclick="return htpDlDoc(this,event)">'
            . '<span class="htpx-doc-ic"><svg viewBox="0 0 24 24" width="34" height="34" fill="none" aria-hidden="true"><path d="M5 4.6A1.6 1.6 0 0 1 6.6 3H18a1 1 0 0 1 1 1v13.4a1 1 0 0 1-1 1H6.6A1.6 1.6 0 0 0 5 20V4.6Z" fill="#34d27b" fill-opacity=".16" stroke="#1ea862" stroke-width="1.5" stroke-linejoin="round"/><path d="M5 17.4A1.6 1.6 0 0 1 6.6 15.8H19" stroke="#1ea862" stroke-width="1.5" stroke-linejoin="round"/><path d="M8 7.2h7M8 10.2h7M8 13.2h4.5" stroke="#1ea862" stroke-width="1.5" stroke-linecap="round"/></svg></span>'
            . '<span class="htpx-doc-tx">'
            . '<span class="htpx-doc-h">' . $this->t('COM_HTPROTECT_INSTALL_DOC_H', 'Handbuch herunterladen') . '</span>'
            . '<span class="htpx-doc-s">' . $this->t('COM_HTPROTECT_INSTALL_DOC_S', 'Alles Wichtige Schritt f&uuml;r Schritt &ndash; als PDF. Am besten jetzt zuerst lesen.') . '</span>'
            . '</span>'
            . '<span class="htpx-doc-btn">' . $this->t('COM_HTPROTECT_INSTALL_DOC_BTN', 'PDF laden &#8595;') . '</span>'
            . '</a>'
            . '<h2><span class="htpx-i">&#10003;</span> ' . $this->t('COM_HTPROTECT_INSTALL_TITLE', 'HTProtect &middot; Server-Schild installiert') . '</h2>'
            . '<p>' . $this->t(
                'COM_HTPROTECT_INSTALL_INTRO',
                'In wenigen Schritten rundum abgesichert:'
            ) . '</p>'
            . '<p>' . $this->t(
                'COM_HTPROTECT_INSTALL_STEP1',
                '<strong>Schritt 1 &ndash; Ein Klick:</strong> Klicke auf dem &bdquo;&Uuml;berblick&ldquo; auf <strong>&bdquo;Jetzt absichern&ldquo;</strong>. Das setzt die empfohlenen Schutzregeln, schreibt eine geh&auml;rtete <code>.htaccess</code>, riegelt die Upload-Ordner ab und testet alles automatisch.'
            ) . '</p>'
            . '<p>' . $this->t(
                'COM_HTPROTECT_INSTALL_STEP2',
                '<strong>Schritt 2 &ndash; Backend sch&uuml;tzen:</strong> Richte unter &bdquo;Backend-Zugang&ldquo; die zus&auml;tzliche Passwortabfrage f&uuml;r <code>/administrator</code> ein &ndash; eine zweite H&uuml;rde noch vor dem Joomla-Login.'
            ) . '</p>'
            . '<p>' . $this->t(
                'COM_HTPROTECT_INSTALL_SCAN',
                '<strong>Bei Hack-Verdacht:</strong> unter &bdquo;Site-Scan&ldquo; die Seite auf Schadcode pr&uuml;fen.'
            ) . '</p>'
            . '<p>' . $this->t(
                'COM_HTPROTECT_INSTALL_MONITOR',
                'Sobald du HTProtect &ouml;ffnest, startet die <strong>automatische &Uuml;berwachung</strong> von selbst und meldet verwundbare Erweiterungen, neue Super-User oder ver&auml;nderte Schutzdateien an deine E-Mail (Adresse ist vorausgef&uuml;llt, im &bdquo;&Uuml;berblick&ldquo; anpassbar).'
            ) . '</p>'
            . '<p>' . $this->t(
                'COM_HTPROTECT_INSTALL_PERSIST',
                'Die Komponente bleibt dauerhaft installiert und ist jederzeit unter <strong>Komponenten &rarr; HTProtect</strong> erreichbar.'
            ) . '</p>'
            . '<a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" class="htpx-go">' . $this->t('COM_HTPROTECT_INSTALL_OPEN', '&rarr; HTProtect jetzt &ouml;ffnen') . '</a>'
            . '<div class="htpx-foot">'
            . $this->t('COM_HTPROTECT_INSTALL_TOOLBY', 'Tool von') . ' <a href="https://website-bereinigung.de" target="_blank" class="htpx-fl">website-bereinigung.de</a>'
            . '</div>'
            . '</div>';

        return true;
    }
}
