import * as API from "./api.js"; import * as Notifications from "./notifications.js"; import * as Spinner from "./spinner.js"; import * as Animation from "./animation.js"; import * as Encryption from "./encryption.js"; const CODE_ELEMENT = document.getElementById("code"); const LINE_NUMBERS_ELEMENT = document.getElementById("linenos"); const INPUT_ELEMENT = document.getElementById("input"); const BUTTONS_DEFAULT_ELEMENT = document.getElementById("buttons_default"); const BUTTON_NEW_ELEMENT = document.getElementById("btn_new"); const BUTTON_SAVE_ELEMENT = document.getElementById("btn_save"); const BUTTON_EDIT_ELEMENT = document.getElementById("btn_edit"); const BUTTON_DELETE_ELEMENT = document.getElementById("btn_delete"); const BUTTON_COPY_ELEMENT = document.getElementById("btn_copy"); const BUTTON_REPORT_ELEMENT = document.getElementById("btn_report"); const BUTTONS_EDIT_ELEMENT = document.getElementById("buttons_edit"); const BUTTON_EDIT_CANCEL_ELEMENT = document.getElementById("btn_edit_cancel"); const BUTTON_EDIT_APPLY_ELEMENT = document.getElementById("btn_edit_apply"); const BUTTON_TOGGLE_ENCRYPTION_ELEMENT = document.getElementById("btn_toggle_encryption"); let PASTE_ID; let LANGUAGE; let CODE; let ENCRYPTION_KEY; let ENCRYPTION_IV; let EDIT_MODE = false; let API_INFORMATION = { version: "error", modificationTokens: false, reports: false }; // Initializes the state system export async function initialize() { loadAPIInformation(); setupButtonFunctionality(); setupKeybinds(); // When embedded inside an iframe, add "embedded" // class to body element. if (window != window.parent) { document.body.classList += " embedded"; } // Enable encryption if enabled from last session if (localStorage.getItem("encryption") === "true") { BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.add("active"); } if (location.pathname !== "/") { // Extract the paste data (ID and language) const split = location.pathname.replace("/", "").split("."); const pasteID = split[0]; const language = split[1]; // Try to retrieve the paste data from the API const response = await API.getPaste(pasteID); if (!response.ok) { Notifications.error("Could not load paste: " + await response.text() + ""); setTimeout(() => location.replace(location.protocol + "//" + location.host), 3000); return; } // Set the persistent paste data PASTE_ID = pasteID; LANGUAGE = language; // Decode the response and decrypt the content if needed const json = await response.json(); CODE = json.content; if (json.metadata.pf_encryption) { ENCRYPTION_KEY = location.hash.replace("#", ""); while (ENCRYPTION_KEY.length == 0) { ENCRYPTION_KEY = prompt("Your decryption key:"); } try { CODE = await Encryption.decrypt(ENCRYPTION_KEY, json.metadata.pf_encryption.iv, CODE); ENCRYPTION_IV = json.metadata.pf_encryption.iv; } catch (error) { console.log(error); Notifications.error("Could not decrrypt paste; make sure the decryption key is correct."); setTimeout(() => location.replace(location.protocol + "//" + location.host), 3000); return; } } // Fill the code block with the just received data updateCode(); } else { // Give the user the opportunity to paste his code INPUT_ELEMENT.classList.remove("hidden"); INPUT_ELEMENT.focus(); } // Update the state of the buttons to match the current state updateButtonState(); INPUT_ELEMENT.addEventListener("input", () => { updateLineNumbers(INPUT_ELEMENT.value); }); } // Loads the API information async function loadAPIInformation() { // try to retrieve the API information const response = await API.getAPIInformation(); if (response.ok) { API_INFORMATION = await response.json(); } else { Notifications.error("Failed loading API information: " + await response.text() + ""); } // Display the API version document.getElementById("version").innerText = API_INFORMATION.version; } // Sets the current persistent code to the code block, highlights it and updates the line numbers function updateCode() { CODE_ELEMENT.innerHTML = LANGUAGE ? hljs.highlight(LANGUAGE, CODE).value : hljs.highlightAuto(CODE).value; updateLineNumbers(CODE); } function updateLineNumbers(content) { if (content == "") { LINE_NUMBERS_ELEMENT.innerHTML = ">"; return; } LINE_NUMBERS_ELEMENT.innerHTML = content.split(/\n/).map((_, index) => `${index + 1}`).join(""); } // Updates the button state according to the current state function updateButtonState() { if (PASTE_ID) { BUTTON_SAVE_ELEMENT.setAttribute("disabled", true); BUTTON_EDIT_ELEMENT.removeAttribute("disabled"); BUTTON_DELETE_ELEMENT.removeAttribute("disabled"); BUTTON_COPY_ELEMENT.removeAttribute("disabled"); if (API_INFORMATION.reports) { BUTTON_REPORT_ELEMENT.classList.remove("hidden"); } } else { BUTTON_SAVE_ELEMENT.removeAttribute("disabled"); BUTTON_EDIT_ELEMENT.setAttribute("disabled", true); BUTTON_DELETE_ELEMENT.setAttribute("disabled", true); BUTTON_COPY_ELEMENT.setAttribute("disabled", true); if (API_INFORMATION.reports) { BUTTON_REPORT_ELEMENT.classList.add("hidden"); } } } // Toggles the edit mode function toggleEditMode() { if (EDIT_MODE) { EDIT_MODE = false; INPUT_ELEMENT.classList.add("hidden"); CODE_ELEMENT.classList.remove("hidden"); updateLineNumbers(CODE); Animation.animate(BUTTONS_EDIT_ELEMENT, "animate__fadeOutDown", "0.3s", () => { BUTTONS_EDIT_ELEMENT.classList.add("hidden"); BUTTONS_DEFAULT_ELEMENT.classList.remove("hidden"); Animation.animate(BUTTONS_DEFAULT_ELEMENT, "animate__fadeInDown", "0.3s"); }); } else { EDIT_MODE = true; CODE_ELEMENT.classList.add("hidden"); INPUT_ELEMENT.classList.remove("hidden"); INPUT_ELEMENT.value = CODE; INPUT_ELEMENT.focus(); Animation.animate(BUTTONS_DEFAULT_ELEMENT, "animate__fadeOutUp", "0.3s", () => { BUTTONS_DEFAULT_ELEMENT.classList.add("hidden"); BUTTONS_EDIT_ELEMENT.classList.remove("hidden"); Animation.animate(BUTTONS_EDIT_ELEMENT, "animate__fadeInUp", "0.3s"); }); } } // Sets up the keybinds for the buttons function setupKeybinds() { window.addEventListener("keydown", (event) => { // All keybinds in the default button set include the CTRL key if ((EDIT_MODE && !event.ctrlKey && event.code !== "Escape") || (!EDIT_MODE && !event.ctrlKey)) { return; } // Find the DOM element of the button to trigger let element; if (EDIT_MODE) { switch (event.code) { case "Escape": { element = BUTTON_EDIT_CANCEL_ELEMENT; break } case "KeyS": { element = BUTTON_EDIT_APPLY_ELEMENT; break; } } } else { switch (event.code) { case "KeyQ": { element = BUTTON_NEW_ELEMENT; break; } case "KeyS": { element = BUTTON_SAVE_ELEMENT; break; } case "KeyO": { element = BUTTON_EDIT_ELEMENT; break; } case "KeyX": { element = BUTTON_DELETE_ELEMENT; break; } case "KeyB": { element = BUTTON_COPY_ELEMENT; break; } } } // Trigger the found button if (element) { event.preventDefault(); if (element.hasAttribute("disabled")) { return; } element.click(); } }); // Additionally fix the behaviour of the Tab key window.addEventListener("keydown", (event) => { if (event.code != "Tab") { return; } event.preventDefault(); insertTextAtCursor(inputElement, " "); }); } // Sets up the different button functionalities function setupButtonFunctionality() { BUTTON_NEW_ELEMENT.addEventListener("click", () => location.replace(location.protocol + "//" + location.host)); BUTTON_SAVE_ELEMENT.addEventListener("click", () => { Spinner.surround(async () => { // Only proceed if the input is not empty if (!INPUT_ELEMENT.value) { return; } // Encrypt the paste if needed let value = INPUT_ELEMENT.value; let metadata; let key; if (BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.contains("active")) { const encrypted = await Encryption.encrypt(await Encryption.generateEncryptionData(), value); value = encrypted.result; metadata = { pf_encryption: { alg: "AES-CBC", iv: encrypted.iv } }; key = encrypted.key; } // Try to create the paste const response = await API.createPaste(value, metadata); if (!response.ok) { Notifications.error("Error while creating paste: " + await response.text() + ""); return; } const data = await response.json(); // Display the modification token if provided if (data.modificationToken) { prompt("The modification token for your paste is:", data.modificationToken); } // Redirect the user to his newly created paste location.replace(location.protocol + "//" + location.host + "/" + data.id + (key ? "#" + key : "")); }); }); BUTTON_EDIT_ELEMENT.addEventListener("click", toggleEditMode); BUTTON_DELETE_ELEMENT.addEventListener("click", () => { Spinner.surround(async () => { // Ask for the modification token const modificationToken = prompt("Modification token:"); if (!modificationToken) { return; } // Try to delete the paste const response = await API.deletePaste(PASTE_ID, modificationToken); if (!response.ok) { Notifications.error("Error while deleting paste: " + await response.text() + ""); return; } // Redirect the user to the start page location.replace(location.protocol + "//" + location.host); }); }); BUTTON_COPY_ELEMENT.addEventListener("click", async () => { // Ask for clipboard permissions if (!(await askForClipboardPermission())) { Notifications.error("Clipboard permission denied."); return; } // Copy the current code await navigator.clipboard.writeText(CODE); Notifications.success("Successfully copied the code."); }); BUTTON_EDIT_CANCEL_ELEMENT.addEventListener("click", toggleEditMode); BUTTON_EDIT_APPLY_ELEMENT.addEventListener("click", async () => { // Only proceed if the input is not empty if (!INPUT_ELEMENT.value) { return; } // Ask for the modification token const modificationToken = prompt("Modification token:"); if (!modificationToken) { return; } // Re-encrypt the paste data if needed let value = INPUT_ELEMENT.value; if (ENCRYPTION_KEY && ENCRYPTION_IV) { const encrypted = await Encryption.encrypt(await Encryption.encryptionDataFromHex(ENCRYPTION_KEY, ENCRYPTION_IV), value); value = encrypted.result; } // Try to edit the paste const response = await API.editPaste(PASTE_ID, modificationToken, value); if (!response.ok) { Notifications.error("Error while editing paste: " + await response.text() + ""); return; } // Update the code and leave the edit mode CODE = INPUT_ELEMENT.value; updateCode(); toggleEditMode(); Notifications.success("Successfully edited paste."); }); BUTTON_TOGGLE_ENCRYPTION_ELEMENT.addEventListener("click", () => { const active = BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.toggle("active"); localStorage.setItem("encryption", active); Notifications.success((active ? "Enabled" : "Disabled") + " automatic paste encryption."); }); BUTTON_REPORT_ELEMENT.addEventListener("click", async () => { // Ask the user for a reason const reason = prompt("Reason:"); if (!reason) { return; } // Try to report the paste const response = await API.reportPaste(PASTE_ID, reason); if (!response.ok) { Notifications.error("Error while reporting paste: " + await response.text() + ""); return; } // Show the response message const data = await response.json(); if (!data.success) { Notifications.error("Error while reporting paste: " + data.message + ""); return; } Notifications.success(data.message); }); } // Asks for clipboard write permission async function askForClipboardPermission() { try { const state = await navigator.permissions.query({ name: "clipboard-write" }); return state.state === "granted" || state.state === "prompt"; } catch (error) { return false; } } // 1:1 skid from https://stackoverflow.com/questions/7404366/how-do-i-insert-some-text-where-the-cursor-is function insertTextAtCursor(element, text) { let value = element.value, endIndex, range, doc = element.ownerDocument; if (typeof element.selectionStart == "number" && typeof element.selectionEnd == "number") { endIndex = element.selectionEnd; element.value = value.slice(0, endIndex) + text + value.slice(endIndex); element.selectionStart = element.selectionEnd = endIndex + text.length; } else if (doc.selection != "undefined" && doc.selection.createRange) { element.focus(); range = doc.selection.createRange(); range.collapse(false); range.text = text; range.select(); } }