256 lines
11 KiB
JavaScript
256 lines
11 KiB
JavaScript
'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:<salt>" — 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);
|
|
});
|
|
}
|
|
});
|