OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

import.php

10 KB
<?php $this->layout('layouts/main', ['page_title' => $page_title, 'current_route' => $current_route, 'full_width' => true]);
$isTeam    = isset($team_id) && $team_id;
$backUrl   = $isTeam ? '/vault/teams/' . (int)$team_id : '/vault';
$importUrl = $isTeam ? '/vault/teams/' . (int)$team_id . '/import' : '/vault/import';
$exportUrl = $isTeam ? '/vault/teams/' . (int)$team_id . '/export' : '/vault/export';
$scopeLabel = $isTeam ? ($team['name'] ?? 'Team') : 'persönliche Einträge';
?>
<style>
a, a:hover { text-decoration: none; }
.vi-wrap { max-width:600px; margin:0 auto; padding:1.75rem 1.5rem 3rem; }
.vi-card { background:var(--color-bg); border:1px solid var(--gray-200); border-radius:0.75rem; padding:1.5rem; margin-bottom:1.25rem; }
.vi-card h2 { font-size:0.95rem; font-weight:700; margin:0 0 0.75rem; }
.vi-dropzone { border:2px dashed var(--gray-300); border-radius:0.5rem; padding:2rem 1rem;
    text-align:center; color:var(--gray-400); cursor:pointer; transition:border-color 0.15s, background 0.15s; }
.vi-dropzone:hover, .vi-dropzone.dragover { border-color:var(--blue-600); background:var(--blue-50); color:var(--blue-600); }
.vi-dropzone input[type=file] { display:none; }
.vi-dropzone-icon { font-size:2rem; margin-bottom:0.5rem; }
.vi-dropzone-text { font-size:0.875rem; }
.vi-file-selected { font-size:0.8rem; color:var(--emerald-600); margin-top:0.5rem; font-weight:600; }
.vi-format-table { width:100%; border-collapse:collapse; font-size:0.82rem; }
.vi-format-table th { text-align:left; padding:0.4rem 0.5rem; background:var(--gray-50);
    border-bottom:1px solid var(--gray-200); color:var(--gray-500); font-weight:600; }
.vi-format-table td { padding:0.4rem 0.5rem; border-bottom:1px solid var(--gray-100); font-family:monospace; font-size:0.78rem; color:var(--gray-700); }
.vi-warning { background:var(--amber-100); border:1px solid var(--amber-300); border-radius:0.5rem;
    padding:0.75rem 1rem; font-size:0.82rem; color:var(--amber-800); margin-bottom:1.25rem; }
</style>

<div class="content-header">
    <div>
        <h1><?= $this->te('Passwörter importieren / exportieren') ?></h1>
        <ol class="breadcrumb">
            <li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
            <li class="breadcrumb-item"><a href="<?= $backUrl ?>"><?= $isTeam ? $this->e($team['name'] ?? 'Team-Tresor') : 'Passwörter' ?></a></li>
            <li class="breadcrumb-item active"><?= $this->te('Import / Export') ?></li>
        </ol>
    </div>
</div>

<div class="vi-wrap">
    <div class="vi-warning">
        <?= $this->t('<strong>&#9888; Hinweis:</strong> Importierte Passwörter werden sofort verschlüsselt gespeichert. Stellen Sie sicher, dass die Datei korrekte Daten enthält, bevor Sie den Import starten.') ?>
    </div>

    <div class="vi-card">
        <h2><?= $this->te('Datei hochladen (CSV oder KeePass .kdbx)') ?></h2>
        <form method="POST" action="<?= $importUrl ?>" enctype="multipart/form-data" id="importForm">
            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
            <?php if (!$isTeam): ?>
            <div class="form-group" style="margin-bottom:1rem;">
                <label for="targetTeam"><?= $this->te('Importieren in') ?></label>
                <select id="targetTeam" name="target_team_id" class="form-control">
                    <option value=""><?= $this->te('Mein persönlicher Tresor') ?></option>
                    <?php foreach (($writable_teams ?? []) as $wt): ?>
                        <option value="<?= (int)$wt['id'] ?>" <?= (int)($preselect_team ?? 0) === (int)$wt['id'] ? 'selected' : '' ?>>
                            Team: <?= $this->e($wt['name']) ?>
                        </option>
                    <?php endforeach; ?>
                </select>
                <small style="color:var(--gray-500);"><?= $this->te('Es werden nur Team-Tresore angezeigt, in denen du Schreibrechte hast.') ?></small>
            </div>
            <?php endif; ?>
            <div class="vi-dropzone" id="dropzone" data-act="viOeffneDateiwahl">
                <div class="vi-dropzone-icon">&#128190;</div>
                <div class="vi-dropzone-text"><?= $this->t('CSV- oder KDBX-Datei hierher ziehen oder <strong>klicken</strong> zum Auswählen') ?></div>
                <input type="file" id="csvFile" name="import_file" accept=".csv,.kdbx,text/csv" data-act-change="fileSelected">
                <div id="fileLabel" class="vi-file-selected" style="display:none;"></div>
            </div>
            <div class="form-group" id="kdbxPwGroup" style="display:none;margin-top:1rem;margin-bottom:0;">
                <label for="kdbxPassword"><?= $this->te('Master-Passwort der KeePass-Datei') ?> <span class="text-danger">*</span></label>
                <input type="password" id="kdbxPassword" name="kdbx_password" class="form-control"
                       autocomplete="off" placeholder="<?= $this->te('Master-Passwort') ?>">
                <small style="color:var(--gray-500);"><?= $this->te('Wird nur zum Entschlüsseln der Datei verwendet und nicht gespeichert.') ?></small>
            </div>
            <div style="margin-top:1rem;display:flex;gap:0.5rem;">
                <button type="submit" class="btn btn-primary" id="importBtn" disabled>
                    <i class="fas fa-file-import"></i> <?= $this->te('Importieren') ?>
                </button>
                <a href="<?= $backUrl ?>" class="btn btn-default"><?= $this->te('Abbrechen') ?></a>
            </div>
        </form>
    </div>

    <div class="vi-card">
        <h2><?= $this->te('KeePass (KDBX)') ?></h2>
        <p style="font-size:0.82rem;color:var(--gray-500);margin:0 0 0.5rem;">
            <?= $this->t('KeePass-Datenbanken (<code>.kdbx</code>, Format 3.1 und 4) werden direkt mit dem Master-Passwort entschlüsselt und importiert – inklusive Ordnerstruktur (Gruppen), Notizen, Zusatzfeldern und TOTP-Secrets (KeePassXC-Format). Der Papierkorb wird übersprungen.') ?>
        </p>
        <p style="font-size:0.78rem;color:var(--gray-400);margin:0;">
            <?= $this->t('Nicht unterstützt: Schlüsseldateien sowie die Schlüsselableitung <strong>Argon2</strong> (KeePass-Standard bei neuen KDBX4-Dateien). In dem Fall in KeePass unter „Datei&nbsp;→&nbsp;Datenbankeinstellungen&nbsp;→&nbsp;Sicherheit" auf <strong>AES-KDF</strong> umstellen und erneut speichern – oder als CSV exportieren.') ?>
        </p>
    </div>

    <div class="vi-card">
        <h2><?= $this->te('Unterstützte Formate') ?></h2>
        <p style="font-size:0.82rem;color:var(--gray-500);margin:0 0 0.75rem;">
            <?= $this->te('Die CSV-Datei muss eine Header-Zeile haben. Folgende Spalten werden erkannt:') ?>
        </p>
        <table class="vi-format-table">
            <thead>
                <tr>
                    <th scope="col"><?= $this->te('Feld') ?></th>
                    <th scope="col"><?= $this->te('Erkannte Spaltenbezeichnungen') ?></th>
                </tr>
            </thead>
            <tbody>
                <tr><td><?= $this->te('Titel') ?></td><td><?= $this->te('Title, Name, Account, SiteName') ?></td></tr>
                <tr><td><?= $this->te('Benutzername') ?></td><td><?= $this->te('Username, Login Name, Login, Email') ?></td></tr>
                <tr><td><?= $this->te('Passwort') ?></td><td><?= $this->te('Password, Pass') ?></td></tr>
                <tr><td>URL</td><td><?= $this->te('URL, Web Site, Website, URI, Address') ?></td></tr>
                <tr><td><?= $this->te('Notizen') ?></td><td><?= $this->te('Notes, Comments, Extra') ?></td></tr>
                <tr><td>TOTP</td><td><?= $this->te('TOTP Secret, login_totp, OTP') ?></td></tr>
                <tr><td><?= $this->te('Ordner') ?></td><td><?= $this->te('Folder, Group, Category') ?></td></tr>
            </tbody>
        </table>
        <p style="font-size:0.78rem;color:var(--gray-400);margin-top:0.75rem;">
            <?= $this->te('Kompatibel mit: KeePass CSV-Export, Bitwarden CSV, 1Password CSV und eigenem Format.') ?>
        </p>
    </div>

    <div class="vi-card">
        <h2><?= $this->te('Export') ?></h2>
        <p style="font-size:0.82rem;color:var(--gray-500);margin:0 0 0.35rem;">
            <?= $this->t('Alle Einträge aus {bereich} herunterladen (für Backup oder Migration).', ['bereich' => $this->e($scopeLabel)]) ?>
        </p>
        <p style="font-size:0.78rem;color:var(--orange-700);margin:0 0 0.75rem;">
            <?= $this->t('<strong>Hinweis:</strong> Die CSV-Datei ist <strong>unverschlüsselter Klartext</strong> und enthält <strong>keine</strong> Zusatzfelder und Dateianhänge. Für ein sicheres, vollständiges Backup den verschlüsselten KeePass-Export (KDBX) verwenden.') ?>
        </p>
        <a href="<?= $exportUrl ?>" class="btn btn-default"><i class="fas fa-file-export"></i> <?= $this->te('Als CSV exportieren') ?></a>
    </div>
</div>

<script<?= $this->scriptNonce() ?>>
// Klick auf die Ablagefläche reicht an das verborgene Dateifeld durch.
function viOeffneDateiwahl() {
    var feld = document.getElementById('csvFile');
    if (feld) { feld.click(); }
}

function fileSelected(input) {
    const label   = document.getElementById('fileLabel');
    const btn     = document.getElementById('importBtn');
    const pwGroup = document.getElementById('kdbxPwGroup');
    if (input.files.length > 0) {
        label.textContent = '✓ ' + input.files[0].name + ' (' + Math.round(input.files[0].size / 1024) + ' KB)';
        label.style.display = 'block';
        btn.disabled = false;
        const isKdbx = /\.kdbx$/i.test(input.files[0].name);
        pwGroup.style.display = isKdbx ? 'block' : 'none';
        document.getElementById('kdbxPassword').required = isKdbx;
        if (isKdbx) document.getElementById('kdbxPassword').focus();
    } else {
        label.style.display = 'none';
        btn.disabled = true;
        pwGroup.style.display = 'none';
        document.getElementById('kdbxPassword').required = false;
    }
}

// Drag & Drop
const dz = document.getElementById('dropzone');
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('dragover'); });
dz.addEventListener('dragleave', () => dz.classList.remove('dragover'));
dz.addEventListener('drop', e => {
    e.preventDefault();
    dz.classList.remove('dragover');
    const input = document.getElementById('csvFile');
    if (e.dataTransfer.files.length > 0) {
        input.files = e.dataTransfer.files;
        fileSelected(input);
    }
});
</script>