// 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('Datei ist kein gültiges 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('CSV-Spalten nicht erkannt (erwartet u. a. 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); }; })();