305 lines
9.6 KiB
JavaScript
305 lines
9.6 KiB
JavaScript
(() => {
|
||
const t = (k) => window.WrappedI18n.t(k);
|
||
const files = [];
|
||
let settings = null;
|
||
let captchaWidgetId = null;
|
||
|
||
const els = {
|
||
text: document.getElementById("payload-text"),
|
||
lang: document.getElementById("text-language"),
|
||
ttl: document.getElementById("ttl-seconds"),
|
||
password: document.getElementById("password"),
|
||
generatePassword: document.getElementById("generate-password"),
|
||
dropzone: document.getElementById("dropzone"),
|
||
fileInput: document.getElementById("file-input"),
|
||
fileList: document.getElementById("file-list"),
|
||
wrapBtn: document.getElementById("wrap-btn"),
|
||
error: document.getElementById("form-error"),
|
||
success: document.getElementById("success-panel"),
|
||
composer: document.querySelector(".composer"),
|
||
shareLink: document.getElementById("share-link"),
|
||
shareToken: document.getElementById("share-token"),
|
||
expiresMeta: document.getElementById("expires-meta"),
|
||
captchaSlot: document.getElementById("captcha-slot"),
|
||
};
|
||
|
||
function showError(msg) {
|
||
els.error.textContent = msg;
|
||
els.error.classList.remove("hidden");
|
||
}
|
||
|
||
function clearError() {
|
||
els.error.classList.add("hidden");
|
||
els.error.textContent = "";
|
||
}
|
||
|
||
function mimeAllowed(ct) {
|
||
const allow = settings.mime_allowlist || [];
|
||
const base = (ct || "").split(";")[0].trim().toLowerCase();
|
||
return allow.some((p) => {
|
||
const pat = p.toLowerCase();
|
||
if (pat.endsWith("/*")) return base.startsWith(pat.slice(0, -1));
|
||
return base === pat;
|
||
});
|
||
}
|
||
|
||
function renderFiles() {
|
||
els.fileList.innerHTML = "";
|
||
files.forEach((f, idx) => {
|
||
const li = document.createElement("li");
|
||
li.innerHTML = `<span>${f.name} <small>(${f.type || "file"} · ${f.size} B)</small></span>`;
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.textContent = "×";
|
||
btn.addEventListener("click", () => {
|
||
files.splice(idx, 1);
|
||
renderFiles();
|
||
});
|
||
li.appendChild(btn);
|
||
els.fileList.appendChild(li);
|
||
});
|
||
}
|
||
|
||
function addFiles(list) {
|
||
for (const f of list) {
|
||
if (!mimeAllowed(f.type || "application/octet-stream")) {
|
||
showError(t("create.mimeDenied") + `: ${f.type || f.name}`);
|
||
continue;
|
||
}
|
||
files.push(f);
|
||
}
|
||
renderFiles();
|
||
}
|
||
|
||
let lastExpiresAt = null;
|
||
|
||
function fillTtl() {
|
||
if (!settings || !els.ttl) return;
|
||
const max = settings.max_ttl_seconds;
|
||
const def = settings.default_ttl_seconds;
|
||
const selected = els.ttl.value ? Number(els.ttl.value) : def;
|
||
const options = [
|
||
{ key: "create.ttl.1h", value: 3600 },
|
||
{ key: "create.ttl.6h", value: 6 * 3600 },
|
||
{ key: "create.ttl.24h", value: 24 * 3600 },
|
||
{ key: "create.ttl.3d", value: 3 * 24 * 3600 },
|
||
{ key: "create.ttl.7d", value: 7 * 24 * 3600 },
|
||
].filter((o) => o.value <= max);
|
||
if (!options.find((o) => o.value === def) && def <= max) {
|
||
options.unshift({ key: "create.ttl.default", value: def, seconds: def });
|
||
}
|
||
const pick = options.some((o) => o.value === selected) ? selected : def;
|
||
els.ttl.innerHTML = options
|
||
.map((o) => {
|
||
const label =
|
||
o.key === "create.ttl.default"
|
||
? t("create.ttl.default", { seconds: o.seconds })
|
||
: t(o.key);
|
||
return `<option value="${o.value}" ${o.value === pick ? "selected" : ""}>${label}</option>`;
|
||
})
|
||
.join("");
|
||
}
|
||
|
||
function refreshExpiresMeta() {
|
||
if (lastExpiresAt && els.expiresMeta) {
|
||
els.expiresMeta.textContent = window.WrappedI18n.formatExpires(lastExpiresAt);
|
||
}
|
||
}
|
||
|
||
function loadCaptcha() {
|
||
els.captchaSlot.innerHTML = "";
|
||
captchaWidgetId = null;
|
||
const provider = settings.captcha_provider;
|
||
if (provider === "turnstile" && settings.turnstile_site_key) {
|
||
const div = document.createElement("div");
|
||
div.className = "cf-turnstile";
|
||
div.dataset.sitekey = settings.turnstile_site_key;
|
||
els.captchaSlot.appendChild(div);
|
||
const s = document.createElement("script");
|
||
s.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
||
s.async = true;
|
||
document.body.appendChild(s);
|
||
} else if (provider === "hcaptcha" && settings.hcaptcha_site_key) {
|
||
const div = document.createElement("div");
|
||
div.className = "h-captcha";
|
||
div.dataset.sitekey = settings.hcaptcha_site_key;
|
||
els.captchaSlot.appendChild(div);
|
||
const s = document.createElement("script");
|
||
s.src = "https://js.hcaptcha.com/1/api.js";
|
||
s.async = true;
|
||
document.body.appendChild(s);
|
||
}
|
||
}
|
||
|
||
function captchaToken() {
|
||
if (settings.captcha_provider === "turnstile" && window.turnstile) {
|
||
return window.turnstile.getResponse?.() || "";
|
||
}
|
||
if (settings.captcha_provider === "hcaptcha" && window.hcaptcha) {
|
||
return window.hcaptcha.getResponse?.(captchaWidgetId) || window.hcaptcha.getResponse?.() || "";
|
||
}
|
||
return "";
|
||
}
|
||
|
||
async function init() {
|
||
const resp = await fetch("/api/v1/settings");
|
||
settings = await resp.json();
|
||
fillTtl();
|
||
loadCaptcha();
|
||
}
|
||
|
||
els.dropzone.addEventListener("click", () => els.fileInput.click());
|
||
els.fileInput.addEventListener("change", () => addFiles(els.fileInput.files));
|
||
["dragenter", "dragover"].forEach((ev) => {
|
||
els.dropzone.addEventListener(ev, (e) => {
|
||
e.preventDefault();
|
||
els.dropzone.classList.add("dragover");
|
||
});
|
||
});
|
||
["dragleave", "drop"].forEach((ev) => {
|
||
els.dropzone.addEventListener(ev, (e) => {
|
||
e.preventDefault();
|
||
els.dropzone.classList.remove("dragover");
|
||
});
|
||
});
|
||
els.dropzone.addEventListener("drop", (e) => addFiles(e.dataTransfer.files));
|
||
|
||
document.addEventListener("paste", (e) => {
|
||
const items = e.clipboardData?.items;
|
||
if (!items) return;
|
||
const pasted = [];
|
||
for (const item of items) {
|
||
if (item.kind === "file") {
|
||
const f = item.getAsFile();
|
||
if (f) pasted.push(f);
|
||
}
|
||
}
|
||
if (pasted.length) {
|
||
e.preventDefault();
|
||
addFiles(pasted);
|
||
}
|
||
});
|
||
|
||
async function copyFrom(input, btn) {
|
||
await navigator.clipboard.writeText(input.value);
|
||
const old = btn.textContent;
|
||
btn.textContent = t("common.copied");
|
||
setTimeout(() => (btn.textContent = old), 1200);
|
||
}
|
||
|
||
document.getElementById("copy-link").addEventListener("click", () => {
|
||
copyFrom(els.shareLink, document.getElementById("copy-link"));
|
||
});
|
||
document.getElementById("copy-token").addEventListener("click", () => {
|
||
copyFrom(els.shareToken, document.getElementById("copy-token"));
|
||
});
|
||
document.getElementById("create-another").addEventListener("click", () => {
|
||
location.reload();
|
||
});
|
||
|
||
els.wrapBtn.addEventListener("click", async () => {
|
||
clearError();
|
||
const text = els.text.value;
|
||
if (!text.trim() && files.length === 0) {
|
||
showError(t("create.needPayload"));
|
||
return;
|
||
}
|
||
|
||
const items = [];
|
||
const contentTypes = [];
|
||
if (text.trim()) {
|
||
items.push({
|
||
type: "text",
|
||
language: els.lang.value,
|
||
content: text,
|
||
});
|
||
contentTypes.push("text/plain");
|
||
}
|
||
for (const f of files) {
|
||
const item = await window.WrappedCrypto.fileToItem(f);
|
||
items.push(item);
|
||
contentTypes.push(item.mime);
|
||
}
|
||
|
||
for (const ct of contentTypes) {
|
||
if (!mimeAllowed(ct)) {
|
||
showError(t("create.mimeDenied"));
|
||
return;
|
||
}
|
||
}
|
||
|
||
const password = els.password.value || "";
|
||
const pack = { version: 1, items };
|
||
const { ciphertext, keyB64url } = await window.WrappedCrypto.encryptPackage(
|
||
pack,
|
||
password || null
|
||
);
|
||
|
||
if (ciphertext.byteLength > settings.max_upload_bytes) {
|
||
showError(t("create.tooLarge"));
|
||
return;
|
||
}
|
||
|
||
els.wrapBtn.disabled = true;
|
||
try {
|
||
const body = {
|
||
ciphertext_b64: window.WrappedCrypto.bytesToBase64(ciphertext),
|
||
ttl_seconds: Number(els.ttl.value),
|
||
content_types: contentTypes,
|
||
item_count: items.length,
|
||
has_password: Boolean(password),
|
||
password: settings.password_mode === "server_gate" && password ? password : null,
|
||
captcha_token: captchaToken() || null,
|
||
};
|
||
const resp = await fetch("/api/v1/wraps", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(body),
|
||
});
|
||
if (!resp.ok) {
|
||
const err = await resp.json().catch(() => ({}));
|
||
showError(err.detail || t("common.error"));
|
||
return;
|
||
}
|
||
const data = await resp.json();
|
||
const token = window.WrappedCrypto.buildToken(data.wrap_id, keyB64url);
|
||
const link = `${location.origin}${data.share_path}#${keyB64url}`;
|
||
els.shareLink.value = link;
|
||
els.shareToken.value = token;
|
||
lastExpiresAt = data.expires_at;
|
||
refreshExpiresMeta();
|
||
els.composer.classList.add("hidden");
|
||
els.success.classList.remove("hidden");
|
||
} catch {
|
||
showError(t("common.error"));
|
||
} finally {
|
||
els.wrapBtn.disabled = false;
|
||
}
|
||
});
|
||
|
||
function generatePassword(length = 20) {
|
||
const alphabet =
|
||
"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*-_";
|
||
const bytes = new Uint8Array(length);
|
||
crypto.getRandomValues(bytes);
|
||
return Array.from(bytes, (b) => alphabet[b % alphabet.length]).join("");
|
||
}
|
||
|
||
els.generatePassword?.addEventListener("click", () => {
|
||
const pwd = generatePassword();
|
||
els.password.type = "text";
|
||
els.password.value = pwd;
|
||
els.password.focus();
|
||
els.password.select();
|
||
});
|
||
|
||
if (window.WrappedI18n.onChange) {
|
||
window.WrappedI18n.onChange(() => {
|
||
fillTtl();
|
||
refreshExpiresMeta();
|
||
});
|
||
}
|
||
|
||
init().catch(() => showError(t("common.error")));
|
||
})();
|