Files
MrTerm/src/renderer/rdm-import.js
T

143 lines
6.8 KiB
JavaScript

// Parser für Exporte aus Devolutions Remote Desktop Manager.
// Unterstützt: .rdm / .xml (Export "Remote Desktop Manager-Datei"), .json und .csv.
// Liefert [{ folder: ['A','B'], host?: {...} }] für vault:bulkImport.
'use strict';
(function () {
// Numerische ConnectionType-Werte aus RDM-JSON (die gängigsten)
const NUMERIC_TYPES = { 1: 'RDPConfigured', 25: 'Group', 77: 'SSHShell', 24: 'Putty', 108: 'SSHShell' };
function classify(type) {
const t = String(NUMERIC_TYPES[type] || type || '').toLowerCase();
if (!t) return null;
if (t === 'group' || t === 'folder' || t === 'repository') return 'folder';
if (t.startsWith('rdp') || t === 'remotedesktop' || t === 'microsoftremotedesktop') return 'rdp';
if (t.includes('ssh') || t === 'putty' || t === 'terminal' || t === 'sshshell') return 'ssh';
return null; // VNC, Web, Credentials, Dokumente … werden übersprungen
}
const splitPath = (p) => String(p || '').split(/[\\/]/).map((s) => s.trim()).filter(Boolean);
function splitHostPort(url, defPort) {
let address = String(url || '').trim().replace(/^(rdp|ssh|https?):\/\//i, '').replace(/\/.*$/, '');
let port;
const m = address.match(/^\[(.+)\](?::(\d+))?$/) || address.match(/^([^:]+):(\d+)$/);
if (m) { address = m[1]; port = m[2] ? Number(m[2]) : undefined; }
return { address, port: port && port !== defPort ? port : undefined };
}
function makeEntry(r) {
const kind = classify(r.type);
if (!kind) return null;
let folder = splitPath(r.group);
if (kind === 'folder') {
// RDM speichert bei Ordnern meist den vollständigen Pfad inkl. eigenem Namen im Feld "Group"
if (r.name && folder[folder.length - 1] !== r.name) folder.push(r.name);
return folder.length ? { folder } : null;
}
const defPort = kind === 'rdp' ? 3389 : 22;
const hp = splitHostPort(r.host || r.url, defPort);
if (!hp.address) return null;
const port = Number(r.port) && Number(r.port) !== defPort ? Number(r.port) : hp.port;
const host = {
protocol: kind,
label: r.name || hp.address,
address: hp.address,
port,
username: r.username || undefined,
domain: kind === 'rdp' ? r.domain || undefined : undefined,
tags: ['rdm'],
};
if (r.password) host.password = r.password;
if (r.description) host.notes = r.description;
return { folder, host };
}
// ---------- XML ----------
function child(el, path) {
let cur = el;
for (const name of path.split('/')) {
cur = [...(cur?.children || [])].find((c) => c.localName.toLowerCase() === name.toLowerCase());
if (!cur) return '';
}
return cur.textContent.trim();
}
const firstOf = (el, paths) => { for (const p of paths) { const v = child(el, p); if (v) return v; } return ''; };
function parseXml(text) {
const doc = new DOMParser().parseFromString(text, 'application/xml');
if (doc.querySelector('parsererror')) throw new Error(I18N.t('File is not valid XML.'));
const conns = [...doc.getElementsByTagName('Connection')].filter((c) => child(c, 'ConnectionType'));
return conns.map((c) => makeEntry({
type: child(c, 'ConnectionType'),
name: child(c, 'Name'),
group: firstOf(c, ['Group', 'GroupName', 'Folder']),
url: firstOf(c, ['Url', 'Host', 'HostName']),
host: firstOf(c, ['Terminal/Host', 'Putty/Host', 'SSHTunnel/Host', 'RDP/Host']),
port: firstOf(c, ['Terminal/HostPort', 'Putty/Port', 'RDP/Port', 'Port', 'HostPort']),
username: firstOf(c, ['RDP/UserName', 'Terminal/Username', 'Terminal/UserName', 'Putty/UserName', 'Putty/Username', 'UserName', 'Username']),
domain: firstOf(c, ['RDP/Domain', 'Domain']),
description: child(c, 'Description'),
})).filter(Boolean);
}
// ---------- JSON ----------
function parseJson(text) {
const data = JSON.parse(text);
const list = Array.isArray(data) ? data : data.Connections || data.connections || data.Entries || [];
const g = (o, ...keys) => { for (const k of keys) { const v = k.split('.').reduce((a, p) => a?.[p], o); if (v != null && v !== '') return v; } return ''; };
return list.map((c) => makeEntry({
type: g(c, 'ConnectionType', 'Type'),
name: g(c, 'Name'),
group: g(c, 'Group', 'GroupName', 'Folder'),
url: g(c, 'Url', 'Host'),
host: g(c, 'Terminal.Host', 'Putty.Host', 'RDP.Host'),
port: g(c, 'Terminal.HostPort', 'Putty.Port', 'RDP.Port', 'Port'),
username: g(c, 'RDP.UserName', 'Terminal.Username', 'Putty.UserName', 'UserName', 'Username'),
domain: g(c, 'RDP.Domain', 'Domain'),
description: g(c, 'Description'),
})).filter(Boolean);
}
// ---------- CSV ----------
function csvRows(text) {
const delim = (text.split('\n')[0].match(/;/g) || []).length > (text.split('\n')[0].match(/,/g) || []).length ? ';' : ',';
const rows = []; let row = [], cur = '', q = false;
for (let i = 0; i < text.length; i++) {
const ch = text[i];
if (q) {
if (ch === '"' && text[i + 1] === '"') { cur += '"'; i++; }
else if (ch === '"') q = false;
else cur += ch;
} else if (ch === '"') q = true;
else if (ch === delim) { row.push(cur); cur = ''; }
else if (ch === '\n' || ch === '\r') { if (ch === '\r' && text[i + 1] === '\n') i++; row.push(cur); rows.push(row); row = []; cur = ''; }
else cur += ch;
}
if (cur || row.length) { row.push(cur); rows.push(row); }
return rows.filter((r) => r.some((c) => c.trim()));
}
function parseCsv(text) {
const [head, ...rows] = csvRows(text);
const idx = (...names) => head.findIndex((h) => names.includes(h.trim().toLowerCase().replace(/[\s_]/g, '')));
const col = {
type: idx('connectiontype', 'type', 'protocol'), name: idx('name', 'displayname'), group: idx('group', 'folder', 'groupname'),
host: idx('host', 'hostname', 'url', 'computername', 'address'), port: idx('port', 'hostport'),
username: idx('username', 'user', 'login'), domain: idx('domain'), password: idx('password'), description: idx('description'),
};
if (col.host < 0 && col.type < 0) throw new Error(I18N.t('CSV columns not recognized (expected e.g. Name, Host, ConnectionType, Group).'));
return rows.map((r) => {
const v = (k) => (col[k] >= 0 ? (r[col[k]] || '').trim() : '');
return makeEntry({ type: v('type') || 'RDPConfigured', name: v('name'), group: v('group'), url: v('host'), port: v('port'), username: v('username'), domain: v('domain'), password: v('password'), description: v('description') });
}).filter(Boolean);
}
window.parseRdmExport = function (name, content) {
const t = content.trimStart();
if (/\.json$/i.test(name) || t.startsWith('{') || t.startsWith('[')) return parseJson(content);
if (/\.csv$/i.test(name)) return parseCsv(content);
if (t.startsWith('<')) return parseXml(content);
return parseCsv(content);
};
})();