'use strict'; /** * paste_create.js — Logic for the paste creation page. * Depends on: crypto.js (PasteCrypto), app.js (clearDraft, window.PBCFG) */ document.addEventListener('DOMContentLoaded', function () { const textarea = document.getElementById('content'); const submitBtn = document.getElementById('submitBtn'); const langSelect = document.getElementById('language'); // ── Password modal state ───────────────────────────────────────────────── let _pendingPassword = null; // set when user locks paste const passwordBtn = document.getElementById('passwordBtn'); const passwordModal = document.getElementById('passwordModal'); const ppInput = document.getElementById('pastePassword'); const ppConfirm = document.getElementById('pastePasswordConfirm'); const ppError = document.getElementById('passwordModalError'); const ppSave = document.getElementById('passwordModalSave'); const ppCancel = document.getElementById('passwordModalCancel'); const ppClear = document.getElementById('passwordModalClear'); function openPasswordModal() { ppInput.value = ''; ppConfirm.value = ''; ppError.style.display = 'none'; passwordModal.style.display = 'flex'; ppInput.focus(); } function closePasswordModal() { passwordModal.style.display = 'none'; } function updatePasswordBtn() { if (_pendingPassword) { passwordBtn.textContent = '\uD83D\uDD13 Locked'; passwordBtn.classList.add('nav-btn-locked'); } else { passwordBtn.textContent = '\uD83D\uDD12 Lock'; passwordBtn.classList.remove('nav-btn-locked'); } } if (passwordBtn) passwordBtn.addEventListener('click', openPasswordModal); if (ppCancel) ppCancel.addEventListener('click', closePasswordModal); if (ppClear) { ppClear.addEventListener('click', () => { _pendingPassword = null; updatePasswordBtn(); closePasswordModal(); }); } if (ppSave) { ppSave.addEventListener('click', () => { const pw = ppInput.value; const pw2 = ppConfirm.value; if (!pw) { ppError.textContent = 'Password cannot be empty.'; ppError.style.display = 'block'; return; } if (pw !== pw2) { ppError.textContent = 'Passwords do not match.'; ppError.style.display = 'block'; return; } _pendingPassword = pw; updatePasswordBtn(); closePasswordModal(); }); } // Close modal on overlay click if (passwordModal) { passwordModal.addEventListener('click', (e) => { if (e.target === passwordModal) closePasswordModal(); }); } // Allow Enter to confirm password modal [ppInput, ppConfirm].forEach(el => { if (el) el.addEventListener('keydown', e => { if (e.key === 'Enter') ppSave?.click(); }); }); // ── Load languages from API ────────────────────────────────────────────── fetch('/api/languages') .then(r => r.json()) .then(langs => { langSelect.innerHTML = ''; langs.forEach(l => { const o = document.createElement('option'); o.value = l.value; o.textContent = l.name; langSelect.appendChild(o); }); // Restore last-used language preference const saved = localStorage.getItem('preferred_language'); if (saved) langSelect.value = saved; }) .catch(() => {}); langSelect.addEventListener('change', () => localStorage.setItem('preferred_language', langSelect.value)); // ── Restore expiry preference ──────────────────────────────────────────── const expirySelect = document.getElementById('expires_in'); const savedExpiry = localStorage.getItem('preferred_expiry'); if (savedExpiry) expirySelect.value = savedExpiry; expirySelect.addEventListener('change', () => localStorage.setItem('preferred_expiry', expirySelect.value)); // ── Restore discussions preference ─────────────────────────────────────── const discussCheck = document.getElementById('allowDiscussions'); if (discussCheck) { discussCheck.checked = localStorage.getItem('preferred_discussions') === 'true'; discussCheck.addEventListener('change', () => localStorage.setItem('preferred_discussions', discussCheck.checked)); } // Burn-after-read — intentionally NOT persisted to localStorage. const burnCheck = document.getElementById('burnAfterRead'); // ── Ctrl/Cmd+S shortcut ────────────────────────────────────────────────── document.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); submitPaste(); } }); submitBtn.addEventListener('click', submitPaste); // ── Clear Draft ────────────────────────────────────────────────────────── const clearBtn = document.getElementById('clearBtn'); if (clearBtn) { clearBtn.addEventListener('click', () => { console.log('[Clear] Manual clear requested.'); // Clear the editor and title textarea.value = ''; const title = document.getElementById('title'); if (title) title.value = ''; // Wipe the localStorage draft via the global helper in app.js if (typeof window.clearDraft === 'function') { window.clearDraft(); } else { localStorage.removeItem('paste_draft'); } textarea.focus(); console.log('[Clear] Draft wiped.'); }); } // ── Submit ─────────────────────────────────────────────────────────────── async function submitPaste() { const content = textarea.value; const title = document.getElementById('title').value || 'Untitled'; const language = langSelect.value; const expires_in = expirySelect.value; if (!content.trim()) { textarea.focus(); return; } // Read E2E flag from the already-loaded config (fetched by app.js at startup). // By the time the user clicks Save, window.PBCFG is guaranteed to be populated. const E2E = window.PBCFG?.features?.encrypt_pastes ?? true; const discussions = document.getElementById('allowDiscussions')?.checked ?? false; const burn_after_read = burnCheck?.checked ?? false; submitBtn.disabled = true; submitBtn.textContent = '…'; try { let postBody, keyBase64 = null; if (E2E) { let key, fragmentValue; if (_pendingPassword) { // Password-based: derive key from password via PBKDF2. // Fragment is "p:" — no raw key in the URL. const { key: derivedKey, saltBase64url } = await PasteCrypto.deriveKeyFromPassword(_pendingPassword); key = derivedKey; fragmentValue = 'p:' + saltBase64url; } else { // Normal: random key, fragment is the key itself. const keyLen = window.PBCFG?.pastes?.encryption_key_bits ?? 128; key = await PasteCrypto.generateKey(keyLen); keyBase64 = await PasteCrypto.exportKey(key); fragmentValue = keyBase64; } const plain = JSON.stringify({ title, content, language, discussions, password_protected: !!_pendingPassword }); postBody = { encrypted_data: await PasteCrypto.encrypt(plain, key), expires_in, discussions, burn_after_read }; keyBase64 = fragmentValue; } else { postBody = { title, content, language, expires_in, discussions, burn_after_read }; } const resp = await fetch('/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(postBody), }); const result = await resp.json(); if (result.error) { alert('Error: ' + result.error); submitBtn.disabled = false; submitBtn.textContent = 'Save'; } else { if (typeof window.clearDraft === 'function') { window.clearDraft(); } if (result.deletion_token) { localStorage.setItem('del_' + result.paste_id, result.deletion_token); } const shareUrl = window.location.origin + result.url + (keyBase64 ? '#' + keyBase64 : ''); if (burn_after_read) { // Don't navigate — creator must NOT open the paste or it burns. // Show a share panel so they can copy the link and hand it to the recipient. showBurnSharePanel(shareUrl); } else { window.location.href = shareUrl; } } } catch (err) { console.error(err); alert('Failed to create paste. Try again.'); submitBtn.disabled = false; submitBtn.textContent = 'Save'; } } function showBurnSharePanel(url) { // Hide the editor so the user can't accidentally re-submit const editorEl = document.getElementById('content'); if (editorEl) editorEl.style.display = 'none'; // Hide nav controls (keep theme toggle visible) document.querySelectorAll('.nav-links > *:not(.theme-toggle)').forEach(el => { el.style.visibility = 'hidden'; }); // Populate and show the panel const panel = document.getElementById('burnSharePanel'); const urlEl = document.getElementById('burnShareUrl'); const copyEl = document.getElementById('burnShareCopy'); if (!panel || !urlEl || !copyEl) return; urlEl.value = url; panel.style.display = 'flex'; // Auto-select the URL for easy manual copying urlEl.focus(); urlEl.select(); copyEl.addEventListener('click', async () => { const ok = typeof copyToClipboard === 'function' ? await copyToClipboard(url) : await navigator.clipboard?.writeText(url).then(() => true).catch(() => false); const prev = copyEl.textContent; copyEl.textContent = ok ? 'Copied!' : 'Failed'; setTimeout(() => { copyEl.textContent = prev; }, 1800); }); } });