Files
hadrian/handlers/playground-script.js
2023-12-10 00:23:24 -05:00

492 lines
13 KiB
JavaScript

const modifierContainer = document.getElementById("modifierContainer");
const modalContainer = document.getElementById("modalContainer");
const modalBody = document.getElementById("modal-body");
const modalContent = document.getElementById("modal-content");
const modalSubmitButton = document.getElementById("modal-submit");
const modalClose = document.getElementById("modal-close");
let hasFetched = false;
let payload = {
requestmodifications: [],
responsemodifications: [],
};
let ninjaData = [];
initialize();
// Rerun handleThemeChange() so style is applied to Ninja Keys
handleThemeChange();
// Add event listener to the iframe so it closes dropdown when clicked
closeDropdownOnClickWithinIframe();
async function initialize() {
if (!hasFetched) {
try {
await fetchPayload();
hasFetched = true;
} catch (error) {
console.error("Fetch error:", error);
}
}
}
function closeDropdownOnClickWithinIframe() {
const iframe = document.getElementById("resultIframe");
iframe.contentWindow.document.addEventListener(
"click",
() => {
if (
!document.getElementById("dropdown_panel").classList.contains("hidden")
) {
toggleDropdown();
}
},
true
);
}
async function fetchPayload() {
try {
const response = await fetch("/api/modifiers");
const data = await response.json();
Object.entries(data.result.requestmodifiers ?? []).forEach(([_, value]) => {
addModifierToNinjaData(
value.name,
value.description,
value.params,
"requestmodifications"
);
});
Object.entries(data.result.responsemodifiers ?? []).forEach(
([_, value]) => {
addModifierToNinjaData(
value.name,
value.description,
value.params,
"responsemodifications"
);
}
);
return data;
} catch (error) {
console.error("Fetch error:", error);
throw error;
}
}
async function submitForm() {
if (!document.getElementById("inputForm").checkValidity()) {
return;
}
try {
const response = await fetch("/playground/" + inputField.value, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error("Request failed");
}
const result = await response.text();
updateResultIframe(result);
} catch (error) {
console.error(error);
}
}
function updateResultIframe(result) {
const resultIframe = parent.document.getElementById("resultIframe");
resultIframe.contentDocument.open();
resultIframe.contentDocument.write(result);
closeDropdownOnClickWithinIframe();
resultIframe.contentDocument.close();
}
document.getElementById("inputForm").addEventListener("submit", function (e) {
e.preventDefault();
submitForm();
});
if (navigator.userAgent.includes("Mac")) {
document.getElementById("ninjaKey").textContent = "⌘";
} else {
document.getElementById("ninjaKey").textContent = "Ctrl";
}
function downloadYaml() {
function jsonToYaml(payload) {
const jsonObject = {
rules: [
{
domains: [hostname],
responsemodifications: [],
requestmodifications: [],
...payload,
},
],
};
return jsyaml.dump(jsonObject);
}
if (!document.getElementById("inputForm").checkValidity()) {
alert("Please enter a valid URL.");
return;
}
const hostname = new URL(inputField.value).hostname;
const ruleHostname = hostname.replace(/^www\./, "").replace(/\./g, "-");
const yamlString = jsonToYaml(payload);
const blob = new Blob([yamlString], { type: "text/yaml;charset=utf-8" });
const href = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = href;
link.download = `${ruleHostname}.yaml`;
link.click();
URL.revokeObjectURL(href);
}
function getValues(type, id, description, params) {
const focusTrap = trap(modalBody);
let values = [];
let existingValues = [];
const inputs = [];
const inputEventListeners = [];
function closeModal() {
focusTrap.destroy();
modalBody.removeEventListener("keydown", handleKeyboardEvents);
modalContainer.removeEventListener("click", handleClickOutside);
modalSubmitButton.removeEventListener("click", closeModal);
modalClose.removeEventListener("click", closeModal);
inputEventListeners.forEach((listener, index) => {
if (listener !== undefined && inputs[index] !== undefined)
inputs[index].removeEventListener("input", listener);
});
modalContent.classList.remove("relative", "h-[220px]");
inputEventListeners.length = 0;
inputs.length = 0;
modalContainer.classList.add("hidden");
modalContent.innerHTML = "";
}
function handleClickOutside(e) {
if (modalBody !== null && !modalBody.contains(e.target)) {
closeModal();
}
}
function handleKeyboardEvents(e) {
if (e.key === "Escape") {
closeModal();
}
if (e.key === "Enter") {
if (e.target.tagName.toLowerCase() === "textarea") {
return;
} else {
modalSubmitButton.click();
}
}
}
document.getElementById("modal-title").textContent = id;
document.getElementById("modal-description").textContent = description;
existingValues =
payload[type].find(
(modifier) => modifier.name === id && modifier.params !== undefined
)?.params ?? [];
params.map((param, i) => {
function textareaEventListener(e) {
const codeElement = document.querySelector("code");
let text = e.target.value;
if (text[text.length - 1] == "\n") {
text += " ";
}
codeElement.innerHTML = text
.replace(new RegExp("&", "g"), "&")
.replace(new RegExp("<", "g"), "&lt;");
Prism.highlightElement(codeElement);
values[i] = text;
syncScroll(e.target);
}
function textareaKeyEventListener(e) {
if (e.key === "Tab" && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
let text = e.target.value;
const start = e.target.selectionStart;
const end = e.target.selectionEnd;
e.target.value = text.substring(0, start) + "\t" + text.substring(end);
e.target.setSelectionRange(start + 1, start + 1);
e.target.dispatchEvent(new Event("input"));
}
syncScroll(e.target);
}
function syncScroll(el) {
const codeElement = document.querySelector("code");
codeElement.scrollTop = el.scrollTop;
codeElement.scrollLeft = el.scrollLeft;
}
function inputEventListener(e) {
if (e.key !== "Enter") {
values[i] = e.target.value;
}
}
const label = document.createElement("label");
label.textContent = param.name;
label.setAttribute("for", `input-${i}`);
let input;
if (param.name === "js") {
input = document.createElement("textarea");
input.type = "textarea";
input.setAttribute("spellcheck", "false");
input.placeholder = "Enter your JavaScript injection code ...";
input.classList.add(
"h-[200px]",
"w-full",
"font-mono",
"whitespace-nowrap",
"font-semibold",
"absolute",
"text-base",
"leading-6",
"rounded-md",
"ring-1",
"ring-slate-900/10",
"shadow-sm",
"z-10",
"p-4",
"m-0",
"my-2",
"bg-transparent",
"dark:bg-transparent",
"text-transparent",
"overflow-auto",
"resize-none",
"caret-white",
"hover:ring-slate-300",
"hyphens-none"
);
input.style.tabSize = "4";
} else {
input = document.createElement("input");
input.type = "text";
input.classList.add(
"w-full",
"text-sm",
"leading-6",
"text-slate-400",
"rounded-md",
"ring-1",
"ring-slate-900/10",
"shadow-sm",
"py-1.5",
"pl-2",
"pr-3",
"mt-0",
"hover:ring-slate-300",
"dark:bg-slate-800",
"dark:highlight-white/5",
"overflow-auto"
);
}
input.id = `input-${i}`;
input.value = existingValues[i] ?? "";
modalContent.appendChild(label);
modalContent.appendChild(input);
if (input.type === "textarea") {
label.classList.add("sr-only", "hidden");
preElement = document.createElement("pre");
codeElement = document.createElement("code");
preElement.setAttribute("aria-hidden", "true");
preElement.classList.add(
"bg-[#2d2d2d]",
"dark:bg-[#2d2d2d]",
"h-[200px]",
"w-full",
"rounded-md",
"ring-1",
"ring-slate-900/10",
"shadow-sm",
"p-0",
"m-0",
"my-2",
"font-mono",
"text-base",
"leading-6",
"overflow-auto",
"whitespace-nowrap",
"font-semibold",
"absolute",
"z-0",
"hyphens-none"
);
modalContent.classList.add("relative", "h-[220px]");
preElement.setAttribute("tabindex", "-1");
codeElement.classList.add(
"language-javascript",
"absolute",
"w-full",
"font-mono",
"text-base",
"leading-6",
"z-0",
"p-4",
"-mx-4",
"-my-4",
"h-full",
"whitespace-nowrap",
"overflow-auto",
"hyphens-none"
);
preElement.appendChild(codeElement);
modalContent.appendChild(preElement);
codeElement.innerHTML = input.value
.replace(new RegExp("&", "g"), "&amp;")
.replace(new RegExp("<", "g"), "&lt;");
Prism.highlightElement(codeElement);
input.addEventListener("input", textareaEventListener);
input.addEventListener("keydown", textareaKeyEventListener);
input.addEventListener("scroll", () => syncScroll(input));
inputEventListeners.push(
textareaEventListener,
textareaKeyEventListener,
syncScroll
);
} else {
input.addEventListener("input", inputEventListener);
inputEventListeners.push(inputEventListener);
}
inputs.push(input);
});
modalContainer.classList.remove("hidden");
document.getElementById("input-0").focus();
return new Promise((resolve) => {
modalBody.addEventListener("keydown", handleKeyboardEvents);
modalContainer.addEventListener("click", handleClickOutside);
modalClose.addEventListener("click", () => {
closeModal();
});
modalSubmitButton.addEventListener("click", (e) => {
inputs.forEach((input, i) => {
values[i] = input.value;
});
resolve(values);
closeModal();
});
});
}
function toggleModifier(type, id, params = []) {
function pillClickHandler(pill) {
toggleModifier(pill.getAttribute("type"), pill.id);
pill.removeEventListener("click", () => pillClickHandler(pill));
pill.remove();
}
function createPill(type, id) {
const pill = document.createElement("span");
pill.classList.add(
"inline-flex",
"items-center",
"rounded-md",
"bg-slate-100",
"dark:bg-slate-800",
"px-2",
"py-1",
"h-4",
"text-xs",
"font-medium",
"border",
"border-slate-400",
"dark:border-slate-700",
"cursor-pointer"
);
pill.id = id;
pill.setAttribute("type", type);
pill.textContent = id;
modifierContainer.appendChild(pill);
pill.addEventListener("click", () => pillClickHandler(pill));
}
if (
params === undefined &&
payload[type].some((modifier) => modifier.name === id)
) {
payload[type] = payload[type].filter((modifier) => modifier.name !== id);
const existingPill = document.getElementById(id);
if (existingPill !== null) {
existingPill.removeEventListener("click", () => pillClickHandler(pill));
existingPill.remove();
}
} else {
const existingModifier = payload[type].find(
(modifier) => modifier.name === id
);
if (existingModifier) {
existingModifier.params = params;
} else {
payload[type].push({ name: id, params: params });
}
const existingPill = document.getElementById(id);
if (existingPill === null) {
createPill(type, id);
}
}
submitForm();
}
function addModifierToNinjaData(id, description, params, type) {
const section =
type === "requestmodifications"
? "Request Modifiers"
: "Response Modifiers";
const modifier = {
id: id,
title: id,
section: section,
handler: () => {
if (Object.keys(params).length === 0) {
toggleModifier(type, id);
} else {
if (params[0].name === "_") {
toggleModifier(type, id, (params = [""]));
} else {
getValues(type, id, description, params).then((values) => {
if (Object.keys(values).length === 0) return;
toggleModifier(type, id, values);
});
}
}
},
};
ninjaData.push(modifier);
}
const ninja = document.querySelector("ninja-keys");
ninja.data = ninjaData;
document.getElementById("btnNinja").addEventListener("click", () => {
ninja.open();
});