(() => { const PLUGIN_ID = "regedit"; const params = new URLSearchParams(window.location.search); const clientInput = document.getElementById("client-id"); const pathInput = document.getElementById("path-input"); const keyTree = document.getElementById("key-tree"); const valueBody = document.getElementById("value-body"); const currentKey = document.getElementById("current-key"); const statusPill = document.getElementById("status-pill"); const logEl = document.getElementById("log"); const navButtons = [ document.getElementById("btn-refresh"), document.getElementById("btn-up"), document.getElementById("btn-go"), ].filter(Boolean); const valueDialog = document.getElementById("value-dialog"); const valueTitle = document.getElementById("value-dialog-title"); const valueName = document.getElementById("value-name"); const valueType = document.getElementById("value-type"); const valueData = document.getElementById("value-data"); const saveValueBtn = document.getElementById("btn-save-value"); const regDialog = document.getElementById("reg-dialog"); const regTitle = document.getElementById("reg-dialog-title"); const regText = document.getElementById("reg-text"); const runImportBtn = document.getElementById("btn-run-import"); const ROOT_PATH = "Computer"; const HIVES = [ ["HKEY_CLASSES_ROOT", "HKCR"], ["HKEY_CURRENT_USER", "HKCU"], ["HKEY_LOCAL_MACHINE", "HKLM"], ["HKEY_USERS", "HKU"], ["HKEY_CURRENT_CONFIG", "HKCC"], ]; let activePath = ROOT_PATH; let lastGoodPath = ROOT_PATH; let pendingBrowsePath = null; let isBrowsing = false; let editingOriginalName = null; let pollTimer = null; clientInput.value = params.get("clientId") || ""; function log(line) { const ts = new Date().toLocaleTimeString(); logEl.textContent = `${ts} ${line}\n` + logEl.textContent; } function setStatus(text) { statusPill.textContent = text; statusPill.classList.toggle("loading", text === "Loading"); } function setBrowsing(browsing) { isBrowsing = browsing; document.body.classList.toggle("regedit-browsing", browsing); for (const btn of navButtons) btn.disabled = browsing; } function getClientId() { return clientInput.value.trim(); } async function sendEvent(event, payload = {}) { const clientId = getClientId(); if (!clientId) { log("Missing clientId"); throw new Error("Missing clientId"); } setStatus("Working"); const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${PLUGIN_ID}/event`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ event, payload }), }); if (!res.ok) { const text = await res.text(); setStatus("Error"); log(`Send failed: ${res.status} ${text}`); throw new Error(text); } log(`Sent ${event}`); } async function pollEvents() { const clientId = getClientId(); if (!clientId) return; try { const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/plugins/${PLUGIN_ID}/events`); if (!res.ok) return; const data = await res.json(); for (const item of data.events || []) handlePluginEvent(item.event, item.payload); } catch (_) {} } function handlePluginEvent(event, payload) { if (event === "ready") { log(payload?.message || "Registry editor ready"); return; } if (event !== "registry_result") return; setStatus(payload?.ok ? "Ready" : "Error"); if (!payload?.ok) { const action = payload?.action || "operation"; const error = payload?.error || "unknown error"; log(`${action} failed: ${error}`); if (action === "list_key") { const failedPath = pendingBrowsePath || pathInput.value || activePath; activePath = lastGoodPath; pendingBrowsePath = null; setBrowsing(false); pathInput.value = activePath; currentKey.textContent = activePath; currentKey.title = activePath; setStatus("Blocked"); alert(`Cannot open ${failedPath}.\n\n${error}`); } return; } if (payload.action === "list_key") { activePath = payload.path || activePath; lastGoodPath = activePath; pendingBrowsePath = null; setBrowsing(false); pathInput.value = activePath; currentKey.textContent = activePath; currentKey.title = activePath; renderKeys(payload.subkeys || []); renderValues(payload.values || []); log(`Loaded ${activePath}`); } else if (payload.action === "export_key") { regTitle.textContent = `Export ${payload.path || activePath}`; regText.value = payload.content || ""; runImportBtn.style.display = "none"; regDialog.showModal(); log(`Exported ${payload.path || activePath}`); } else { log(`${payload.action || "operation"} completed`); listKey(activePath); } } function renderKeys(keys) { keyTree.innerHTML = ""; if (activePath === ROOT_PATH) { for (const [label, path] of HIVES) { keyTree.appendChild(keyButton(label, path, "hive")); } return; } const up = parentPath(activePath); if (up) { const row = keyButton("..", up, "up"); keyTree.appendChild(row); } for (const key of keys) { keyTree.appendChild(keyButton(key, `${activePath}\\${key}`, "key")); } } function keyButton(label, path, iconType) { const btn = document.createElement("button"); btn.className = "key-row"; btn.innerHTML = ``; btn.querySelector(".name").textContent = label; btn.title = path; btn.addEventListener("click", () => listKey(path)); return btn; } function renderValues(values) { valueBody.innerHTML = ""; if (!values.length) { const tr = document.createElement("tr"); tr.innerHTML = `No values in this key.`; valueBody.appendChild(tr); return; } for (const value of values) { const tr = document.createElement("tr"); const displayName = value.name || "(Default)"; const type = value.type || "REG_NONE"; tr.innerHTML = `
`; tr.children[0].title = displayName; tr.querySelector(".value-label").textContent = displayName; tr.querySelector(".value-icon").classList.add(valueIconClass(type)); tr.children[1].textContent = type; tr.querySelector(".value-data").textContent = value.data ?? ""; tr.querySelector(".value-data").title = value.data ?? ""; const actions = tr.querySelector(".row-actions"); const edit = document.createElement("button"); edit.className = "btn"; edit.innerHTML = ``; edit.title = "Edit value"; edit.addEventListener("click", () => openValueDialog(value)); const del = document.createElement("button"); del.className = "btn danger"; del.innerHTML = ``; del.title = "Delete value"; del.addEventListener("click", () => deleteValue(value.name || "")); actions.append(edit, del); valueBody.appendChild(tr); } } function renderLoading(path) { keyTree.innerHTML = `
Opening ${escapeHtml(path)}
`; valueBody.innerHTML = `
Enumerating values and subkeys...
`; } function escapeHtml(text) { return String(text).replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }[ch])); } function valueIconClass(type) { if (type === "REG_SZ" || type === "REG_EXPAND_SZ" || type === "REG_MULTI_SZ") { return "string"; } if (type === "REG_DWORD" || type === "REG_QWORD") return "number"; if (type === "REG_BINARY") return "binary"; return "unknown"; } function parentPath(path) { if (path === ROOT_PATH) return null; const i = path.lastIndexOf("\\"); return i > 0 ? path.slice(0, i) : ROOT_PATH; } function listKey(path) { if (isBrowsing) return Promise.resolve(); const targetPath = normalizePath(path); if (targetPath === ROOT_PATH) { activePath = ROOT_PATH; lastGoodPath = ROOT_PATH; pendingBrowsePath = null; pathInput.value = activePath; currentKey.textContent = activePath; currentKey.title = activePath; setStatus("Ready"); setBrowsing(false); renderKeys([]); renderValues([]); log("Loaded Computer"); return Promise.resolve(); } pendingBrowsePath = targetPath; pathInput.value = targetPath; currentKey.textContent = targetPath; currentKey.title = targetPath; setStatus("Loading"); setBrowsing(true); renderLoading(targetPath); log(`Opening ${targetPath}`); return sendEvent("list_key", { path: targetPath }); } function normalizePath(path) { const raw = (path || "").trim(); if (!raw || raw.toLowerCase() === "computer" || raw === "\\") return ROOT_PATH; return raw.replace(/^Computer\\/i, ""); } function openValueDialog(value) { editingOriginalName = value ? value.name || "" : null; valueTitle.textContent = value ? "Edit Value" : "New Value"; valueName.value = value ? value.name || "" : ""; valueType.value = value ? value.type || "REG_SZ" : "REG_SZ"; valueData.value = value ? value.data || "" : ""; valueDialog.showModal(); } function deleteValue(name) { const display = name || "(Default)"; if (!confirm(`Delete value ${display}?`)) return; sendEvent("delete_value", { path: activePath, name }); } document.getElementById("btn-refresh").addEventListener("click", () => listKey(pathInput.value)); document.getElementById("btn-go").addEventListener("click", () => listKey(pathInput.value)); document.getElementById("btn-up").addEventListener("click", () => { const up = parentPath(activePath); if (up) listKey(up); }); document.getElementById("btn-new-key").addEventListener("click", () => { if (activePath === ROOT_PATH) { alert("Select a hive first."); return; } const name = prompt("New key name"); if (!name) return; sendEvent("create_key", { path: `${activePath}\\${name}` }); }); document.getElementById("btn-delete-key").addEventListener("click", () => { if (activePath === ROOT_PATH || parentPath(activePath) === ROOT_PATH) { alert("Root hives cannot be deleted."); return; } if (!confirm(`Delete key and all subkeys?\n${activePath}`)) return; const next = parentPath(activePath); sendEvent("delete_key", { path: activePath, nextPath: next }); activePath = next; }); document.getElementById("btn-new-value").addEventListener("click", () => { if (activePath === ROOT_PATH) { alert("Select a registry key first."); return; } openValueDialog(null); }); saveValueBtn.addEventListener("click", (event) => { event.preventDefault(); const payload = { path: activePath, name: valueName.value, oldName: editingOriginalName, type: valueType.value, data: valueData.value, }; valueDialog.close(); sendEvent("set_value", payload); }); document.getElementById("btn-export").addEventListener("click", () => { if (activePath === ROOT_PATH) { alert("Select a hive or key to export."); return; } sendEvent("export_key", { path: activePath }); }); document.getElementById("btn-import").addEventListener("click", () => { regTitle.textContent = "Import .reg"; regText.value = "Windows Registry Editor Version 5.00\n\n"; runImportBtn.style.display = ""; regDialog.showModal(); }); runImportBtn.addEventListener("click", (event) => { event.preventDefault(); if (!confirm("Import this .reg data on the remote client?")) return; const content = regText.value; regDialog.close(); sendEvent("import_reg", { content, refreshPath: activePath }); }); pathInput.addEventListener("keydown", (event) => { if (event.key === "Enter") listKey(pathInput.value); }); clientInput.addEventListener("change", () => { if (pollTimer) clearInterval(pollTimer); pollTimer = setInterval(pollEvents, 900); listKey(pathInput.value || ROOT_PATH).catch(() => {}); }); pollTimer = setInterval(pollEvents, 900); listKey(activePath).catch(() => {}); })();