diff --git a/src/html.gleam b/src/html.gleam
index 45cae9b..581f7c1 100644
--- a/src/html.gleam
+++ b/src/html.gleam
@@ -121,6 +121,11 @@ fn decrypt_js(encrypted_content: String) -> String {
content: document.getElementById('content-display')
};
+ if (!els.loading || !els.error || !els.content) {
+ console.error('Required DOM elements not found');
+ return;
+ }
+
if (!hash) {
els.loading.classList.add('hidden');
els.error.classList.remove('hidden');
@@ -128,7 +133,9 @@ fn decrypt_js(encrypted_content: String) -> String {
}
try {
- const keyBase64 = hash.replace(/-/g, '+').replace(/_/g, '/');
+ let keyBase64 = hash.replace(/-/g, '+').replace(/_/g, '/');
+ while (keyBase64.length % 4) keyBase64 += '=';
+
const encryptedBytes = Uint8Array.from(atob(encryptedContent), c => c.charCodeAt(0));
const keyBytes = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
const iv = encryptedBytes.slice(0, 12);
@@ -150,70 +157,106 @@ fn decrypt_js(encrypted_content: String) -> String {
}
fn escape_js_string(s: String) -> String {
- s
- |> string.replace("\\", "\\\\")
- |> string.replace("'", "\\'")
- |> string.replace("\"", "\\\"")
- |> string.replace("\n", "\\n")
- |> string.replace("\r", "\\r")
+ let escaped =
+ s
+ |> string.replace("\\", "\\\\")
+ |> string.replace("'", "\\'")
+ |> string.replace("\"", "\\\"")
+ |> string.replace("\n", "\\n")
+ |> string.replace("\r", "\\r")
+ |> string.replace("<", "\\u003c")
+ |> string.replace(">", "\\u003e")
+ |> string.replace("&", "\\u0026")
+
+ escaped
}
fn crypto_js() -> String {
"
async function encryptContent(content) {
- const encoder = new TextEncoder();
- const data = encoder.encode(content);
- const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
- const iv = crypto.getRandomValues(new Uint8Array(12));
- const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);
- const keyData = await crypto.subtle.exportKey('raw', key);
- return {
- encrypted: btoa(String.fromCharCode(...new Uint8Array([...iv, ...new Uint8Array(encrypted)]))),
- key: btoa(String.fromCharCode(...new Uint8Array(keyData)))
- };
+ try {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(content);
+ const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
+ const iv = crypto.getRandomValues(new Uint8Array(12));
+ const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);
+ const keyData = await crypto.subtle.exportKey('raw', key);
+ return {
+ encrypted: btoa(String.fromCharCode(...iv, ...new Uint8Array(encrypted))),
+ key: btoa(String.fromCharCode(...new Uint8Array(keyData)))
+ };
+ } catch (e) {
+ console.error('Encryption failed:', e);
+ throw new Error('Failed to encrypt content');
+ }
}
function base64ToUrlSafe(base64) {
- return base64.replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
+ return base64.split('+').join('-').split('/').join('_').replace(/=+$/g, '');
}
function showShareUrl(url) {
const card = document.querySelector('.card');
+ if (!card) {
+ console.error('Card element not found');
+ return;
+ }
const div = document.createElement('div');
div.className = 'share-url';
- div.innerHTML = '';
+ const label = document.createElement('label');
+ label.textContent = 'Share this URL';
+ div.appendChild(label);
const input = document.createElement('input');
input.type = 'text';
input.readOnly = true;
input.value = url;
- input.onclick = () => input.select();
+ input.addEventListener('click', () => input.select());
div.appendChild(input);
card.insertBefore(div, card.firstChild);
input.select();
}
-document.getElementById('paste-form').addEventListener('submit', async (e) => {
+document.getElementById('paste-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
- const content = document.getElementById('content').value;
- const { encrypted, key } = await encryptContent(content);
- document.getElementById('encrypted-content').value = encrypted;
- const csrfToken = document.getElementById('csrf-token').value;
+
+ const submitButton = e.target.querySelector('button[type=\"submit\"]');
+ const contentInput = document.getElementById('content');
+ const encryptedInput = document.getElementById('encrypted-content');
+ const csrfInput = document.getElementById('csrf-token');
+
+ if (!contentInput || !encryptedInput || !csrfInput) {
+ console.error('Required form elements not found');
+ return;
+ }
+
+ if (submitButton) submitButton.disabled = true;
+
+ try {
+ const content = contentInput.value;
+ const { encrypted, key } = await encryptContent(content);
+ encryptedInput.value = encrypted;
+ const csrfToken = csrfInput.value;
- const res = await fetch('/', {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: `encrypted_content=${encodeURIComponent(encrypted)}&csrf_token=${encodeURIComponent(csrfToken)}`
- });
+ const res = await fetch('/', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: `encrypted_content=${encodeURIComponent(encrypted)}&csrf_token=${encodeURIComponent(csrfToken)}`
+ });
- const html = await res.text();
- const doc = new DOMParser().parseFromString(html, 'text/html');
- const pasteId = doc.querySelector('[data-paste-id]')?.getAttribute('data-paste-id');
- if (!pasteId) return;
+ const html = await res.text();
+ const doc = new DOMParser().parseFromString(html, 'text/html');
+ const pasteId = doc.querySelector('[data-paste-id]')?.getAttribute('data-paste-id');
+ if (!pasteId) throw new Error('No paste ID returned');
- const pasteUrl = `${location.origin}/paste/${pasteId}#${base64ToUrlSafe(key)}`;
- document.body.replaceChildren(...doc.body.childNodes);
- showShareUrl(pasteUrl);
- history.replaceState({}, '', pasteUrl);
+ const pasteUrl = `${location.origin}/paste/${pasteId}#${base64ToUrlSafe(key)}`;
+ document.body.replaceChildren(...doc.body.childNodes);
+ showShareUrl(pasteUrl);
+ history.replaceState({}, '', pasteUrl);
+ } catch (err) {
+ console.error('Form submission failed:', err);
+ alert('Failed to create paste. Please try again.');
+ if (submitButton) submitButton.disabled = false;
+ }
});
"
}