<?php
/**
 * Demo Oracle — portefeuilles Oracle Cloud (wallets).
 *
 * Une base OCI Autonomous ne se joint pas par host:port/service : elle se
 * joint par un alias TNS, resolu grace au portefeuille telecharge depuis la
 * console OCI. Le portefeuille est une archive ZIP contenant tnsnames.ora,
 * sqlnet.ora et les fichiers de cles.
 *
 * Portage des deux helpers de modules/oracle/databases.php, avec trois
 * durcissements — le module de demonstration est fait pour etre expose :
 *
 *   1. l'archive est INSPECTEE avant extraction. Une entree dont le nom
 *      contient « .. » ou commence par « / » sort du repertoire cible : c'est
 *      la traversee de chemin classique du ZIP, et extractTo() ne s'en protege
 *      pas seul ;
 *   2. seuls les fichiers attendus d'un portefeuille sont extraits. Un ZIP qui
 *      contiendrait un .php serait sinon depose sous securite/, hors
 *      arborescence web mais tout de meme sur le disque ;
 *   3. la taille decompressee est plafonnee, contre l'archive qui gonfle.
 *
 * Le repertoire d'extraction vit sous securite/, c'est-a-dire hors du
 * DocumentRoot : les cles ne sont jamais servies par Apache.
 *
 * Safe to include multiple times (guard sur class_exists).
 */
if (class_exists('DemoOracleWallet')) return;

class DemoOracleWallet
{
    /** Extensions et noms admis dans un portefeuille. */
    private const ALLOWED = [
        'tnsnames.ora', 'sqlnet.ora', 'ojdbc.properties', 'keystore.jks',
        'truststore.jks', 'cwallet.sso', 'ewallet.p12', 'ewallet.pem',
        'cwallet.sso.lck', 'README',
    ];

    /** Plafond de taille decompressee, tous fichiers confondus. */
    private const MAX_TOTAL_BYTES = 5 * 1024 * 1024;

    /** Repertoire d'un portefeuille, par identifiant de base. */
    public static function pathFor(int $dbId): string
    {
        return SECURITE_PATH . '/demo_oracle_wallets/' . $dbId;
    }

    /**
     * Extrait une archive de portefeuille dans son repertoire.
     *
     * @return array{ok: bool, msg: string}
     */
    public static function extract(string $zipPath, string $targetDir): array
    {
        if (!class_exists('ZipArchive')) {
            return ['ok' => false, 'msg' => t('demo_oracle.wallet_no_zip_ext')];
        }

        $zip = new ZipArchive();
        if ($zip->open($zipPath) !== true) {
            return ['ok' => false, 'msg' => t('demo_oracle.wallet_bad_zip')];
        }

        /* Inspection prealable : on refuse l'archive entiere plutot que d'en
           extraire la moitie puis de constater le probleme. */
        $total = 0;
        $keep  = [];
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $st   = $zip->statIndex($i);
            $name = $st['name'];

            if (str_ends_with($name, '/')) continue;           // repertoire
            if (str_contains($name, '..') || str_starts_with($name, '/')) {
                $zip->close();
                return ['ok' => false, 'msg' => t('demo_oracle.wallet_unsafe_entry')];
            }

            $base = basename($name);
            if (!in_array($base, self::ALLOWED, true)) continue;  // ignore en silence

            $total += (int)$st['size'];
            if ($total > self::MAX_TOTAL_BYTES) {
                $zip->close();
                return ['ok' => false, 'msg' => t('demo_oracle.wallet_too_big')];
            }
            $keep[$name] = $base;
        }

        if (!isset(array_flip($keep)['tnsnames.ora'])) {
            $zip->close();
            return ['ok' => false, 'msg' => t('demo_oracle.wallet_no_tnsnames')];
        }

        if (!is_dir($targetDir) && !@mkdir($targetDir, 0750, true)) {
            $zip->close();
            return ['ok' => false, 'msg' => t('demo_oracle.wallet_mkdir_failed')];
        }

        /* Extraction a plat : le nom d'origine peut etre imbrique, la
           destination ne l'est jamais. */
        foreach ($keep as $name => $base) {
            $content = $zip->getFromName($name);
            if ($content === false) continue;
            @file_put_contents($targetDir . '/' . $base, $content);
            @chmod($targetDir . '/' . $base, 0640);
        }
        $zip->close();

        self::patchSqlnet($targetDir);
        return ['ok' => true, 'msg' => t('demo_oracle.wallet_extracted')];
    }

    /**
     * Reecrit DIRECTORY dans sqlnet.ora vers le repertoire reel.
     *
     * Le fichier livre par OCI pointe vers le chemin de la machine qui a
     * telecharge l'archive ; sans cette correction, OCI8 chercherait les cles
     * la-bas et echouerait avec un message peu parlant.
     */
    private static function patchSqlnet(string $targetDir): void
    {
        $path = $targetDir . '/sqlnet.ora';
        if (!is_file($path)) return;

        $content = (string)file_get_contents($path);
        $escaped = str_replace('\\', '/', $targetDir);
        $content = preg_replace(
            '/DIRECTORY\s*=\s*["\']?[^"\')\s]+["\']?/i',
            'DIRECTORY="' . $escaped . '"',
            $content
        );
        @file_put_contents($path, $content);
    }

    /** Supprime le repertoire d'un portefeuille, s'il existe. */
    public static function delete(string $walletPath): void
    {
        if ($walletPath === '' || !is_dir($walletPath)) return;

        /* Garde-fou : ne supprimer que sous le repertoire des portefeuilles.
           Le chemin vient de la base ; une valeur corrompue ne doit pas
           pouvoir faire effacer autre chose. */
        $root = SECURITE_PATH . '/demo_oracle_wallets/';
        $real = realpath($walletPath);
        if ($real === false || !str_starts_with($real . '/', realpath(SECURITE_PATH) . '/demo_oracle_wallets/')) {
            return;
        }

        $it = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($real, FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST
        );
        foreach ($it as $f) {
            $f->isDir() ? @rmdir($f->getPathname()) : @unlink($f->getPathname());
        }
        @rmdir($real);
    }

    /**
     * Alias TNS declares dans le tnsnames.ora d'un portefeuille.
     *
     * Sert a proposer une liste plutot qu'une saisie libre : les alias d'une
     * base Autonomous sont normes (…_high, _medium, _low) mais prefixes du nom
     * de la base, que l'administrateur n'a aucune raison de connaitre par
     * coeur.
     *
     * @return string[] vide si le fichier est absent ou illisible
     */
    public static function aliases(string $walletPath): array
    {
        $path = rtrim($walletPath, '/') . '/tnsnames.ora';
        if ($walletPath === '' || !is_file($path)) return [];

        $out = [];
        foreach (preg_split('/\r?\n/', (string)file_get_contents($path)) as $line) {
            if (preg_match('/^([A-Za-z0-9_$#.]+)\s*=/', $line, $m)) {
                $out[] = $m[1];
            }
        }
        sort($out);
        return array_values(array_unique($out));
    }
}
