RDM-Import, App-Designs, Auto-Update über Gitea, Logo; SFTP-Namensspalte repariert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
// 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);
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user