Add files via upload
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
let pendingKey = null;
|
||||
let pendingData = null;
|
||||
|
||||
const FONT_STACK_SANS = "'Vazirmatn Local', 'Vazirmatn', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
||||
const FONT_STACK_MONO = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace";
|
||||
|
||||
const MAX_CUSTOM_EXPIRY_SECONDS = 31536000;
|
||||
const MIN_CUSTOM_EXPIRY_SECONDS = 300;
|
||||
|
||||
function showError(msg) {
|
||||
document.getElementById('errorDisplay').style.display = 'flex';
|
||||
document.getElementById('errorMessage').textContent = msg;
|
||||
@@ -12,34 +18,229 @@ function clearError() {
|
||||
|
||||
function isRTL(text) {
|
||||
const rtlRegex = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
|
||||
return rtlRegex.test(text);
|
||||
return rtlRegex.test(text || '');
|
||||
}
|
||||
|
||||
function applyDirectionalStyles(element, text, rtlFont, ltrFont) {
|
||||
if (!element) return;
|
||||
|
||||
if (isRTL(text)) {
|
||||
element.style.direction = 'rtl';
|
||||
element.style.fontFamily = rtlFont;
|
||||
} else {
|
||||
element.style.direction = 'ltr';
|
||||
element.style.fontFamily = ltrFont;
|
||||
}
|
||||
}
|
||||
|
||||
function updateTextareaDirection(e) {
|
||||
applyDirectionalStyles(e.target, e.target.value, FONT_STACK_SANS, FONT_STACK_MONO);
|
||||
}
|
||||
|
||||
const contentTextarea = document.getElementById('content');
|
||||
if (contentTextarea) {
|
||||
contentTextarea.addEventListener('input', function(e) {
|
||||
const text = e.target.value;
|
||||
if (isRTL(text)) {
|
||||
e.target.style.direction = 'rtl';
|
||||
e.target.style.fontFamily = "'Vazirmatn', sans-serif";
|
||||
} else {
|
||||
e.target.style.direction = 'ltr';
|
||||
e.target.style.fontFamily = "'JetBrains Mono', monospace";
|
||||
}
|
||||
contentTextarea.addEventListener('input', updateTextareaDirection);
|
||||
}
|
||||
|
||||
const subjectInput = document.getElementById('subject');
|
||||
if (subjectInput) {
|
||||
subjectInput.addEventListener('input', function(e) {
|
||||
applyDirectionalStyles(e.target, e.target.value, FONT_STACK_SANS, FONT_STACK_SANS);
|
||||
});
|
||||
}
|
||||
|
||||
const passwordInput = document.getElementById('passwordInput');
|
||||
if (passwordInput) {
|
||||
passwordInput.addEventListener('input', updatePasswordStrength);
|
||||
}
|
||||
|
||||
const expiresInSelect = document.getElementById('expiresIn');
|
||||
if (expiresInSelect) {
|
||||
expiresInSelect.addEventListener('change', toggleCustomExpiry);
|
||||
}
|
||||
|
||||
const editorToolbar = document.querySelector('.editor-toolbar');
|
||||
if (editorToolbar && contentTextarea) {
|
||||
editorToolbar.addEventListener('click', handleToolbarClick);
|
||||
}
|
||||
|
||||
|
||||
function handleToolbarClick(event) {
|
||||
const button = event.target.closest('[data-action]');
|
||||
if (!button || !contentTextarea) return;
|
||||
|
||||
const action = button.dataset.action;
|
||||
applyToolbarAction(action);
|
||||
}
|
||||
|
||||
function applyToolbarAction(action) {
|
||||
switch (action) {
|
||||
case 'bold':
|
||||
wrapSelection('**', '**', 'bold text');
|
||||
break;
|
||||
case 'italic':
|
||||
wrapSelection('*', '*', 'italic text');
|
||||
break;
|
||||
case 'strike':
|
||||
wrapSelection('~~', '~~', 'strikethrough');
|
||||
break;
|
||||
case 'heading':
|
||||
transformSelectedLines(function(line) {
|
||||
return line ? `# ${line.replace(/^#{1,6}\s+/, '')}` : '# Heading';
|
||||
}, { fallback: '# Heading' });
|
||||
break;
|
||||
case 'quote':
|
||||
transformSelectedLines(function(line) {
|
||||
return line ? `> ${line.replace(/^>\s?/, '')}` : '> Quote';
|
||||
}, { fallback: '> Quote' });
|
||||
break;
|
||||
case 'bullet':
|
||||
transformSelectedLines(function(line) {
|
||||
return line ? `- ${line.replace(/^[-*+]\s+/, '')}` : '- List item';
|
||||
}, { fallback: '- List item' });
|
||||
break;
|
||||
case 'numbered':
|
||||
transformSelectedLines(function(line, index) {
|
||||
return `${index + 1}. ${line.replace(/^\d+\.\s+/, '') || 'List item'}`;
|
||||
}, { fallback: '1. List item' });
|
||||
break;
|
||||
case 'link': {
|
||||
const selectedText = getSelectedText(contentTextarea) || 'link text';
|
||||
const url = window.prompt('Enter the URL for this link:', 'https://');
|
||||
if (url === null) {
|
||||
focusEditor();
|
||||
return;
|
||||
}
|
||||
const trimmedUrl = url.trim() || 'https://';
|
||||
replaceSelection(`[${selectedText}](${trimmedUrl})`, { selectInserted: false });
|
||||
break;
|
||||
}
|
||||
case 'code':
|
||||
wrapSelection('`', '`', 'code');
|
||||
break;
|
||||
case 'codeblock':
|
||||
wrapSelection('```\n', '\n```', 'your code here');
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
updateTextareaDirection({ target: contentTextarea });
|
||||
}
|
||||
|
||||
function getSelectedText(textarea) {
|
||||
return textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
|
||||
}
|
||||
|
||||
function replaceSelection(replacement, options) {
|
||||
if (!contentTextarea) return;
|
||||
|
||||
const settings = Object.assign({ selectInserted: true, selectionStartOffset: 0, selectionEndOffset: 0 }, options || {});
|
||||
const start = contentTextarea.selectionStart;
|
||||
const end = contentTextarea.selectionEnd;
|
||||
const current = contentTextarea.value;
|
||||
|
||||
contentTextarea.value = current.slice(0, start) + replacement + current.slice(end);
|
||||
|
||||
if (settings.selectInserted) {
|
||||
contentTextarea.setSelectionRange(start + settings.selectionStartOffset, start + replacement.length - settings.selectionEndOffset);
|
||||
} else {
|
||||
const caret = start + replacement.length;
|
||||
contentTextarea.setSelectionRange(caret, caret);
|
||||
}
|
||||
|
||||
focusEditor();
|
||||
}
|
||||
|
||||
function wrapSelection(prefix, suffix, placeholder) {
|
||||
if (!contentTextarea) return;
|
||||
|
||||
const selected = getSelectedText(contentTextarea);
|
||||
const inner = selected || placeholder;
|
||||
const replacement = `${prefix}${inner}${suffix}`;
|
||||
const selectionStartOffset = prefix.length;
|
||||
const selectionEndOffset = suffix.length;
|
||||
|
||||
replaceSelection(replacement, {
|
||||
selectInserted: true,
|
||||
selectionStartOffset,
|
||||
selectionEndOffset
|
||||
});
|
||||
}
|
||||
|
||||
function transformSelectedLines(transformer, options) {
|
||||
if (!contentTextarea) return;
|
||||
|
||||
const settings = Object.assign({ fallback: '' }, options || {});
|
||||
const value = contentTextarea.value;
|
||||
const start = contentTextarea.selectionStart;
|
||||
const end = contentTextarea.selectionEnd;
|
||||
const lineStart = value.lastIndexOf('\n', Math.max(0, start - 1)) + 1;
|
||||
const nextNewlineIndex = value.indexOf('\n', end);
|
||||
const lineEnd = nextNewlineIndex === -1 ? value.length : nextNewlineIndex;
|
||||
const selectedBlock = value.slice(lineStart, lineEnd);
|
||||
const lines = selectedBlock ? selectedBlock.split('\n') : [settings.fallback];
|
||||
const transformed = lines.map(function(line, index) {
|
||||
return transformer(line, index);
|
||||
}).join('\n');
|
||||
|
||||
contentTextarea.value = value.slice(0, lineStart) + transformed + value.slice(lineEnd);
|
||||
contentTextarea.setSelectionRange(lineStart, lineStart + transformed.length);
|
||||
focusEditor();
|
||||
}
|
||||
|
||||
function focusEditor() {
|
||||
if (!contentTextarea) return;
|
||||
contentTextarea.focus();
|
||||
}
|
||||
|
||||
function togglePassword() {
|
||||
const enabled = document.getElementById('enablePassword').checked;
|
||||
const wrapper = document.getElementById('passwordWrapper');
|
||||
wrapper.classList.toggle('show', enabled);
|
||||
|
||||
if (!enabled) {
|
||||
document.getElementById('passwordInput').value = '';
|
||||
resetPasswordStrength();
|
||||
} else {
|
||||
updatePasswordStrength();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCustomExpiry() {
|
||||
const isCustom = document.getElementById('expiresIn').value === 'custom';
|
||||
const wrapper = document.getElementById('customExpiryWrapper');
|
||||
wrapper.classList.toggle('show', isCustom);
|
||||
|
||||
if (isCustom) {
|
||||
setCustomExpiryBounds();
|
||||
const input = document.getElementById('customExpiry');
|
||||
if (!input.value) {
|
||||
const defaultDate = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
input.value = toLocalDateTimeValue(defaultDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setCustomExpiryBounds() {
|
||||
const input = document.getElementById('customExpiry');
|
||||
if (!input) return;
|
||||
|
||||
input.min = toLocalDateTimeValue(new Date(Date.now() + MIN_CUSTOM_EXPIRY_SECONDS * 1000));
|
||||
input.max = toLocalDateTimeValue(new Date(Date.now() + MAX_CUSTOM_EXPIRY_SECONDS * 1000));
|
||||
}
|
||||
|
||||
function toLocalDateTimeValue(date) {
|
||||
const offset = date.getTimezoneOffset();
|
||||
const local = new Date(date.getTime() - offset * 60000);
|
||||
return local.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
const Base64 = {
|
||||
encode(buf) {
|
||||
const bytes = new Uint8Array(buf);
|
||||
let bin = '';
|
||||
for (let b of bytes) bin += String.fromCharCode(b);
|
||||
for (const b of bytes) bin += String.fromCharCode(b);
|
||||
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
},
|
||||
decode(str) {
|
||||
@@ -67,19 +268,294 @@ async function deriveKeyFromPassword(password, salt) {
|
||||
}
|
||||
|
||||
function genId() {
|
||||
const arr = new Uint8Array(16);
|
||||
const arr = new Uint8Array(12);
|
||||
crypto.getRandomValues(arr);
|
||||
return Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');
|
||||
return Base64.encode(arr);
|
||||
}
|
||||
|
||||
function buildEncryptedPayload(subject, content) {
|
||||
return JSON.stringify({
|
||||
subject: subject || '',
|
||||
content: content
|
||||
});
|
||||
}
|
||||
|
||||
function parseDecryptedPayload(text) {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed && typeof parsed === 'object' && typeof parsed.content === 'string') {
|
||||
return {
|
||||
subject: typeof parsed.subject === 'string' ? parsed.subject : '',
|
||||
content: parsed.content
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Backward compatibility with old plain-text payloads.
|
||||
}
|
||||
|
||||
return {
|
||||
subject: '',
|
||||
content: text
|
||||
};
|
||||
}
|
||||
|
||||
function resetCreateForm() {
|
||||
document.getElementById('content').value = '';
|
||||
document.getElementById('subject').value = '';
|
||||
document.getElementById('passwordInput').value = '';
|
||||
document.getElementById('enablePassword').checked = false;
|
||||
document.getElementById('burnAfterRead').checked = false;
|
||||
document.getElementById('expiresIn').value = '86400';
|
||||
document.getElementById('customExpiry').value = '';
|
||||
document.getElementById('customExpiryWrapper').classList.remove('show');
|
||||
document.getElementById('passwordWrapper').classList.remove('show');
|
||||
resetPasswordStrength();
|
||||
applyDirectionalStyles(document.getElementById('content'), '', FONT_STACK_SANS, FONT_STACK_MONO);
|
||||
applyDirectionalStyles(document.getElementById('subject'), '', FONT_STACK_SANS, FONT_STACK_SANS);
|
||||
setCustomExpiryBounds();
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return (text || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function escapeAttribute(text) {
|
||||
return escapeHtml(text).replace(/`/g, '`');
|
||||
}
|
||||
|
||||
function sanitizeUrl(url) {
|
||||
const trimmed = (url || '').trim();
|
||||
if (/^(https?:\/\/|mailto:)/i.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderInlineMarkdown(text) {
|
||||
if (!text) return '';
|
||||
|
||||
const placeholders = [];
|
||||
let work = text.replace(/\r\n/g, '\n');
|
||||
|
||||
work = work.replace(/`([^`\n]+)`/g, function(_, code) {
|
||||
const token = `@@MDTOKEN${placeholders.length}@@`;
|
||||
placeholders.push({ token, html: `<code>${escapeHtml(code)}</code>` });
|
||||
return token;
|
||||
});
|
||||
|
||||
work = work.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, function(_, label, url, title) {
|
||||
const safeUrl = sanitizeUrl(url);
|
||||
if (!safeUrl) {
|
||||
return label;
|
||||
}
|
||||
const attrs = [`href="${escapeAttribute(safeUrl)}"`, 'target="_blank"', 'rel="noopener noreferrer"'];
|
||||
if (title) {
|
||||
attrs.push(`title="${escapeAttribute(title)}"`);
|
||||
}
|
||||
const token = `@@MDTOKEN${placeholders.length}@@`;
|
||||
placeholders.push({ token, html: `<a ${attrs.join(' ')}>${escapeHtml(label)}</a>` });
|
||||
return token;
|
||||
});
|
||||
|
||||
let html = escapeHtml(work);
|
||||
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
||||
html = html.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1<em>$2</em>');
|
||||
html = html.replace(/(^|[^_])_([^_\n]+)_(?!_)/g, '$1<em>$2</em>');
|
||||
html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
||||
|
||||
placeholders.forEach(function(entry) {
|
||||
html = html.replace(entry.token, entry.html);
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderMarkdown(content) {
|
||||
const normalized = (content || '').replace(/\r\n/g, '\n');
|
||||
const blocks = [];
|
||||
const lines = normalized.split('\n');
|
||||
let i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
if (!lines[i].trim()) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lines[i].startsWith('```')) {
|
||||
const lang = lines[i].slice(3).trim();
|
||||
i += 1;
|
||||
const codeLines = [];
|
||||
while (i < lines.length && !lines[i].startsWith('```')) {
|
||||
codeLines.push(lines[i]);
|
||||
i += 1;
|
||||
}
|
||||
if (i < lines.length && lines[i].startsWith('```')) {
|
||||
i += 1;
|
||||
}
|
||||
const langAttr = lang ? ` data-lang="${escapeAttribute(lang)}"` : '';
|
||||
blocks.push(`<pre class="markdown-code"><code${langAttr}>${escapeHtml(codeLines.join('\n'))}</code></pre>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^#{1,6}\s+/.test(lines[i])) {
|
||||
const match = lines[i].match(/^(#{1,6})\s+(.*)$/);
|
||||
const level = match[1].length;
|
||||
blocks.push(`<h${level}>${renderInlineMarkdown(match[2].trim())}</h${level}>`);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^>\s?/.test(lines[i])) {
|
||||
const quoteLines = [];
|
||||
while (i < lines.length && /^>\s?/.test(lines[i])) {
|
||||
quoteLines.push(lines[i].replace(/^>\s?/, ''));
|
||||
i += 1;
|
||||
}
|
||||
blocks.push(`<blockquote>${quoteLines.map(line => renderInlineMarkdown(line)).join('<br>')}</blockquote>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^[-*+]\s+/.test(lines[i])) {
|
||||
const items = [];
|
||||
while (i < lines.length && /^[-*+]\s+/.test(lines[i])) {
|
||||
items.push(`<li>${renderInlineMarkdown(lines[i].replace(/^[-*+]\s+/, ''))}</li>`);
|
||||
i += 1;
|
||||
}
|
||||
blocks.push(`<ul>${items.join('')}</ul>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^\d+\.\s+/.test(lines[i])) {
|
||||
const items = [];
|
||||
while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
|
||||
items.push(`<li>${renderInlineMarkdown(lines[i].replace(/^\d+\.\s+/, ''))}</li>`);
|
||||
i += 1;
|
||||
}
|
||||
blocks.push(`<ol>${items.join('')}</ol>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const paragraphLines = [];
|
||||
while (
|
||||
i < lines.length &&
|
||||
lines[i].trim() &&
|
||||
!/^#{1,6}\s+/.test(lines[i]) &&
|
||||
!/^>\s?/.test(lines[i]) &&
|
||||
!/^[-*+]\s+/.test(lines[i]) &&
|
||||
!/^\d+\.\s+/.test(lines[i]) &&
|
||||
!lines[i].startsWith('```')
|
||||
) {
|
||||
paragraphLines.push(lines[i]);
|
||||
i += 1;
|
||||
}
|
||||
blocks.push(`<p>${paragraphLines.map(line => renderInlineMarkdown(line)).join('<br>')}</p>`);
|
||||
}
|
||||
|
||||
return blocks.join('');
|
||||
}
|
||||
|
||||
function renderContent(content) {
|
||||
const contentBox = document.getElementById('decryptedContent');
|
||||
contentBox.dataset.rawContent = content || '';
|
||||
contentBox.innerHTML = renderMarkdown(content || '');
|
||||
}
|
||||
|
||||
function showDecryptedContent(payload, burnAfterRead) {
|
||||
const subjectWrapper = document.getElementById('decryptedSubjectWrapper');
|
||||
const subjectBox = document.getElementById('decryptedSubject');
|
||||
const contentBox = document.getElementById('decryptedContent');
|
||||
|
||||
document.getElementById('passwordPrompt').classList.remove('show');
|
||||
document.getElementById('decryptView').classList.add('show');
|
||||
document.getElementById('createView').style.display = 'none';
|
||||
|
||||
if (burnAfterRead) {
|
||||
document.getElementById('burnNotice').style.display = 'flex';
|
||||
} else {
|
||||
document.getElementById('burnNotice').style.display = 'none';
|
||||
}
|
||||
|
||||
if (payload.subject && payload.subject.trim()) {
|
||||
subjectWrapper.style.display = 'block';
|
||||
subjectBox.textContent = payload.subject;
|
||||
applyDirectionalStyles(subjectBox, payload.subject, FONT_STACK_SANS, FONT_STACK_SANS);
|
||||
document.title = `${payload.subject} - Secure Pastebin`;
|
||||
} else {
|
||||
subjectWrapper.style.display = 'none';
|
||||
subjectBox.textContent = '';
|
||||
document.title = 'Secure Pastebin - End-to-End Encrypted Message Sharing';
|
||||
}
|
||||
|
||||
renderContent(payload.content);
|
||||
applyDirectionalStyles(contentBox, payload.content, FONT_STACK_SANS, FONT_STACK_MONO);
|
||||
}
|
||||
|
||||
function getExpiryPayload() {
|
||||
const selected = document.getElementById('expiresIn').value;
|
||||
|
||||
if (selected !== 'custom') {
|
||||
const expiresIn = parseInt(selected, 10);
|
||||
return {
|
||||
expiresIn,
|
||||
expiresAt: null,
|
||||
displayDate: new Date(Date.now() + expiresIn * 1000)
|
||||
};
|
||||
}
|
||||
|
||||
const customValue = document.getElementById('customExpiry').value;
|
||||
if (!customValue) {
|
||||
throw new Error('Please choose a custom expiration date and time');
|
||||
}
|
||||
|
||||
const customDate = new Date(customValue);
|
||||
if (Number.isNaN(customDate.getTime())) {
|
||||
throw new Error('Custom expiration date is invalid');
|
||||
}
|
||||
|
||||
const expiresAt = Math.floor(customDate.getTime() / 1000);
|
||||
const deltaSeconds = expiresAt - Math.floor(Date.now() / 1000);
|
||||
|
||||
if (deltaSeconds < MIN_CUSTOM_EXPIRY_SECONDS) {
|
||||
throw new Error('Custom expiration must be at least 5 minutes from now');
|
||||
}
|
||||
|
||||
if (deltaSeconds > MAX_CUSTOM_EXPIRY_SECONDS) {
|
||||
throw new Error('Custom expiration cannot be more than 365 days from now');
|
||||
}
|
||||
|
||||
return {
|
||||
expiresIn: deltaSeconds,
|
||||
expiresAt,
|
||||
displayDate: customDate
|
||||
};
|
||||
}
|
||||
|
||||
function buildFullShareUrl(id, keyData, hasPassword) {
|
||||
const suffix = hasPassword ? `${keyData}:pwd` : keyData;
|
||||
return `${location.origin}/p/${encodeURIComponent(id)}#${suffix}`;
|
||||
}
|
||||
|
||||
function buildShortShareUrl(id, keyData, hasPassword) {
|
||||
const suffix = hasPassword ? `${keyData}:pwd` : keyData;
|
||||
return `${location.origin}/#${encodeURIComponent(id)}:${suffix}`;
|
||||
}
|
||||
|
||||
async function createPaste() {
|
||||
clearError();
|
||||
const subject = document.getElementById('subject').value.trim();
|
||||
const content = document.getElementById('content').value.trim();
|
||||
if (!content) return showError('Please enter content to encrypt');
|
||||
|
||||
const hasPassword = document.getElementById('enablePassword').checked;
|
||||
const password = document.getElementById('passwordInput').value;
|
||||
|
||||
|
||||
if (hasPassword && password.length < 4) {
|
||||
return showError('Password must be at least 4 characters');
|
||||
}
|
||||
@@ -90,8 +566,10 @@ async function createPaste() {
|
||||
btnText.innerHTML = '<span class="loading"></span> Encrypting...';
|
||||
|
||||
try {
|
||||
let key, keyToExport;
|
||||
|
||||
const expiry = getExpiryPayload();
|
||||
let key;
|
||||
let keyToExport;
|
||||
|
||||
if (hasPassword) {
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
key = await deriveKeyFromPassword(password, salt);
|
||||
@@ -100,48 +578,47 @@ async function createPaste() {
|
||||
key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
||||
keyToExport = await crypto.subtle.exportKey('raw', key);
|
||||
}
|
||||
|
||||
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
new TextEncoder().encode(content)
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
new TextEncoder().encode(buildEncryptedPayload(subject, content))
|
||||
);
|
||||
|
||||
|
||||
const id = genId();
|
||||
const payload = {
|
||||
iv: Array.from(iv),
|
||||
data: Array.from(new Uint8Array(encrypted))
|
||||
const payload = {
|
||||
iv: Array.from(iv),
|
||||
data: Array.from(new Uint8Array(encrypted))
|
||||
};
|
||||
|
||||
const res = await fetch('/api/create', {
|
||||
|
||||
const res = await fetch('/api/pastes', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
encryptedData: payload,
|
||||
expiresIn: parseInt(document.getElementById('expiresIn').value),
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
encryptedData: payload,
|
||||
expiresIn: expiry.expiresIn,
|
||||
customExpiresAt: expiry.expiresAt,
|
||||
burnAfterRead: document.getElementById('burnAfterRead').checked,
|
||||
hasPassword: hasPassword
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
const result = await res.json();
|
||||
if (!res.ok) throw new Error(result.error || 'Server error');
|
||||
|
||||
let url;
|
||||
if (hasPassword) {
|
||||
url = `${location.origin}/#${id}:${Base64.encode(keyToExport)}:pwd`;
|
||||
} else {
|
||||
url = `${location.origin}/#${id}:${Base64.encode(keyToExport)}`;
|
||||
}
|
||||
|
||||
document.getElementById('shareUrl').value = url;
|
||||
document.getElementById('expiryTime').textContent = new Date(Date.now() + parseInt(document.getElementById('expiresIn').value) * 1000).toLocaleString();
|
||||
const keyFragment = Base64.encode(keyToExport);
|
||||
const fullShareUrl = buildFullShareUrl(id, keyFragment, hasPassword);
|
||||
const shortShareUrl = buildShortShareUrl(id, keyFragment, hasPassword);
|
||||
document.getElementById('shareUrl').value = fullShareUrl;
|
||||
document.getElementById('shareUrl').dataset.shortUrl = shortShareUrl;
|
||||
document.getElementById('expiryTime').textContent = expiry.displayDate.toLocaleString();
|
||||
document.getElementById('passwordNotice').style.display = hasPassword ? 'flex' : 'none';
|
||||
document.getElementById('resultBox').classList.add('show');
|
||||
document.getElementById('content').value = '';
|
||||
|
||||
document.getElementById('resultBox').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
resetCreateForm();
|
||||
|
||||
} catch (err) {
|
||||
showError(err.message);
|
||||
} finally {
|
||||
@@ -150,49 +627,76 @@ async function createPaste() {
|
||||
}
|
||||
}
|
||||
|
||||
async function decryptPaste() {
|
||||
function parseLocationForPaste() {
|
||||
const hash = location.hash.slice(1);
|
||||
if (!hash.includes(':')) return;
|
||||
|
||||
const parts = hash.split(':');
|
||||
const id = parts[0];
|
||||
const keyData = parts[1];
|
||||
const isPasswordProtected = parts[2] === 'pwd';
|
||||
|
||||
const pathMatch = location.pathname.match(/^\/p\/([^/]+)\/?$/);
|
||||
|
||||
if (pathMatch && hash) {
|
||||
const id = decodeURIComponent(pathMatch[1]);
|
||||
const isPasswordProtected = hash.endsWith(':pwd');
|
||||
const keyData = isPasswordProtected ? hash.slice(0, -4) : hash;
|
||||
|
||||
if (id && keyData) {
|
||||
return { id, keyData, isPasswordProtected };
|
||||
}
|
||||
}
|
||||
|
||||
if (hash.includes(':')) {
|
||||
const parts = hash.split(':');
|
||||
const id = parts[0];
|
||||
const keyData = parts[1];
|
||||
const isPasswordProtected = parts[2] === 'pwd';
|
||||
|
||||
if (id && keyData) {
|
||||
return { id, keyData, isPasswordProtected };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function decryptPaste() {
|
||||
const pasteRef = parseLocationForPaste();
|
||||
if (!pasteRef) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/get/${id}`);
|
||||
const res = await fetch(`/api/get/${encodeURIComponent(pasteRef.id)}`);
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
|
||||
if (data.hasPassword || isPasswordProtected) {
|
||||
pendingKey = keyData;
|
||||
if (data.hasPassword || pasteRef.isPasswordProtected) {
|
||||
pendingKey = pasteRef.keyData;
|
||||
pendingData = data;
|
||||
document.getElementById('createView').style.display = 'none';
|
||||
document.getElementById('passwordPrompt').classList.add('show');
|
||||
document.getElementById('passwordPrompt').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
return;
|
||||
}
|
||||
|
||||
await performDecryption(keyData, data);
|
||||
|
||||
|
||||
await performDecryption(pasteRef.keyData, data);
|
||||
|
||||
} catch (err) {
|
||||
showError('Failed: ' + err.message);
|
||||
setTimeout(() => location.href = '/', 3000);
|
||||
setTimeout(() => {
|
||||
location.href = '/';
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
async function decryptWithPassword() {
|
||||
const password = document.getElementById('decryptPassword').value;
|
||||
if (!password) return;
|
||||
|
||||
|
||||
const btn = document.querySelector('#passwordPrompt .btn');
|
||||
const btnText = document.getElementById('decryptBtnText');
|
||||
btn.disabled = true;
|
||||
btnText.innerHTML = '<span class="loading"></span> Decrypting...';
|
||||
|
||||
|
||||
try {
|
||||
const salt = Base64.decode(pendingKey);
|
||||
const key = await deriveKeyFromPassword(password, salt);
|
||||
await performDecryption(key, pendingData, true);
|
||||
document.getElementById('passwordError').style.display = 'none';
|
||||
} catch (err) {
|
||||
document.getElementById('passwordError').style.display = 'block';
|
||||
btn.disabled = false;
|
||||
@@ -202,7 +706,7 @@ async function decryptWithPassword() {
|
||||
|
||||
async function performDecryption(keyOrData, data, isKeyObject = false) {
|
||||
let key;
|
||||
|
||||
|
||||
if (isKeyObject) {
|
||||
key = keyOrData;
|
||||
} else {
|
||||
@@ -211,45 +715,159 @@ async function performDecryption(keyOrData, data, isKeyObject = false) {
|
||||
'raw', keyRaw, { name: 'AES-GCM', length: 256 }, false, ['decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: new Uint8Array(data.data.iv) },
|
||||
key,
|
||||
{ name: 'AES-GCM', iv: new Uint8Array(data.data.iv) },
|
||||
key,
|
||||
new Uint8Array(data.data.data)
|
||||
);
|
||||
|
||||
const text = new TextDecoder().decode(decrypted);
|
||||
|
||||
document.getElementById('passwordPrompt').classList.remove('show');
|
||||
document.getElementById('decryptView').classList.add('show');
|
||||
|
||||
if (data.burnAfterRead) {
|
||||
document.getElementById('burnNotice').style.display = 'flex';
|
||||
}
|
||||
|
||||
const contentBox = document.getElementById('decryptedContent');
|
||||
contentBox.textContent = text;
|
||||
|
||||
if (isRTL(text)) {
|
||||
contentBox.style.direction = 'rtl';
|
||||
contentBox.style.fontFamily = "'Vazirmatn', sans-serif";
|
||||
} else {
|
||||
contentBox.style.direction = 'ltr';
|
||||
contentBox.style.fontFamily = "'JetBrains Mono', monospace";
|
||||
}
|
||||
|
||||
history.replaceState(null, null, ' ');
|
||||
|
||||
const decoded = new TextDecoder().decode(decrypted);
|
||||
const payload = parseDecryptedPayload(decoded);
|
||||
showDecryptedContent(payload, data.burnAfterRead);
|
||||
|
||||
history.replaceState(null, '', location.pathname + location.search);
|
||||
}
|
||||
|
||||
function copyUrl() {
|
||||
const inp = document.getElementById('shareUrl');
|
||||
inp.select();
|
||||
async function copyTextToClipboard(text) {
|
||||
if (!text) return;
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
|
||||
const temp = document.createElement('textarea');
|
||||
temp.value = text;
|
||||
document.body.appendChild(temp);
|
||||
temp.select();
|
||||
document.execCommand('copy');
|
||||
const btn = document.querySelector('.btn-copy');
|
||||
btn.textContent = '✅ Copied!';
|
||||
setTimeout(() => btn.textContent = '📋 Copy', 2000);
|
||||
document.body.removeChild(temp);
|
||||
}
|
||||
|
||||
async function copyUrl() {
|
||||
const btn = document.getElementById('copyUrlBtn');
|
||||
|
||||
try {
|
||||
await copyTextToClipboard(document.getElementById('shareUrl').value);
|
||||
btn.textContent = '✅ Copied!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = '📋 Copy';
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
showError('Unable to copy the full share URL');
|
||||
}
|
||||
}
|
||||
|
||||
async function copyShortUrl() {
|
||||
const btn = document.getElementById('copyShortUrlBtn');
|
||||
const shareUrl = document.getElementById('shareUrl');
|
||||
const shortUrl = shareUrl ? shareUrl.dataset.shortUrl : '';
|
||||
|
||||
if (!shortUrl) {
|
||||
showError('Short link is not available yet');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await copyTextToClipboard(shortUrl);
|
||||
btn.textContent = '✅ Copied!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = '⚡ Copy short';
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
showError('Unable to copy the short share URL');
|
||||
}
|
||||
}
|
||||
|
||||
async function copyDecryptedContent() {
|
||||
const btn = document.getElementById('copyDecryptedBtn');
|
||||
const subject = document.getElementById('decryptedSubject').textContent.trim();
|
||||
const content = document.getElementById('decryptedContent').dataset.rawContent || '';
|
||||
const textToCopy = subject ? `Subject: ${subject}\n\n${content}` : content;
|
||||
|
||||
try {
|
||||
await copyTextToClipboard(textToCopy);
|
||||
btn.textContent = '✅ Copied!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = '📋 Copy Text';
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
showError('Unable to copy decrypted content');
|
||||
}
|
||||
}
|
||||
|
||||
function resetPasswordStrength() {
|
||||
const meterFill = document.getElementById('passwordStrengthFill');
|
||||
const meterLabel = document.getElementById('passwordStrengthLabel');
|
||||
const wrapper = document.getElementById('passwordStrength');
|
||||
if (!meterFill || !meterLabel || !wrapper) return;
|
||||
|
||||
wrapper.dataset.strength = 'empty';
|
||||
meterFill.style.width = '0%';
|
||||
meterLabel.textContent = 'Password strength will appear here';
|
||||
}
|
||||
|
||||
function calculatePasswordStrength(password) {
|
||||
let score = 0;
|
||||
|
||||
if (password.length >= 8) score += 1;
|
||||
if (password.length >= 12) score += 1;
|
||||
if (password.length >= 16) score += 1;
|
||||
if (/[a-z]/.test(password)) score += 1;
|
||||
if (/[A-Z]/.test(password)) score += 1;
|
||||
if (/\d/.test(password)) score += 1;
|
||||
if (/[^A-Za-z0-9]/.test(password)) score += 1;
|
||||
|
||||
if (password.length > 0 && password.length < 6) {
|
||||
score = Math.min(score, 1);
|
||||
}
|
||||
|
||||
if (score <= 1) return { label: 'Weak', width: 25, level: 'weak' };
|
||||
if (score <= 3) return { label: 'Fair', width: 50, level: 'fair' };
|
||||
if (score <= 5) return { label: 'Good', width: 75, level: 'good' };
|
||||
return { label: 'Strong', width: 100, level: 'strong' };
|
||||
}
|
||||
|
||||
function updatePasswordStrength() {
|
||||
const wrapper = document.getElementById('passwordStrength');
|
||||
const input = document.getElementById('passwordInput');
|
||||
const meterFill = document.getElementById('passwordStrengthFill');
|
||||
const meterLabel = document.getElementById('passwordStrengthLabel');
|
||||
if (!wrapper || !input || !meterFill || !meterLabel) return;
|
||||
|
||||
const password = input.value;
|
||||
if (!password) {
|
||||
resetPasswordStrength();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = calculatePasswordStrength(password);
|
||||
wrapper.dataset.strength = result.level;
|
||||
meterFill.style.width = `${result.width}%`;
|
||||
meterLabel.textContent = `${result.label} password`;
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (!contentTextarea) return;
|
||||
if (document.activeElement !== contentTextarea) return;
|
||||
|
||||
const key = event.key.toLowerCase();
|
||||
if ((event.ctrlKey || event.metaKey) && !event.altKey) {
|
||||
if (key === 'b') {
|
||||
event.preventDefault();
|
||||
applyToolbarAction('bold');
|
||||
} else if (key === 'i') {
|
||||
event.preventDefault();
|
||||
applyToolbarAction('italic');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
if (location.hash.length > 1) decryptPaste();
|
||||
setCustomExpiryBounds();
|
||||
resetPasswordStrength();
|
||||
if (location.hash.length > 0 || /^\/p\//.test(location.pathname)) {
|
||||
decryptPaste();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user