diff --git a/src/js/app.js b/src/js/app.js index 32967a4..3dc6c5c 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -250,10 +250,19 @@ async function drainQuickSavePending() { const drained = pending.slice(); const drainedIds = new Set(drained.map(e => e && e.id).filter(Boolean)); const inbox = await ensureInboxBoard(); // legt die Inbox an, falls noetig; gibt das Board zurueck + // Idempotenz gegen den Worker/Drain-Race auf 'quicksave_pending': jede eingespielte Inbox- + // Bookmark traegt die Pending-id ihres Ursprungs als srcId. Taucht ein bereits gedrainter + // Eintrag durch einen gleichzeitigen Worker-Append erneut in der Queue auf, wird er hier + // uebersprungen statt doppelt eingefuegt — kein Duplikat, und kein Verlust (boards-Write + // bleibt vor der Queue-Bereinigung, daher keine umgekehrte Verlustgefahr). + const seenSrc = new Set(inbox.bookmarks.map(b => b && b.srcId).filter(Boolean)); for (const e of drained) { - if (e && typeof e.url === 'string' && e.url) { - inbox.bookmarks.push(normalizeBookmark({ title: e.title, url: e.url })); - } + if (!e || !e.id || seenSrc.has(e.id)) continue; // schon eingespielt + if (typeof e.url !== 'string' || !e.url || !isSafeUrl(e.url)) continue; // leeres/unsicheres Protokoll verwerfen + const bm = normalizeBookmark({ title: e.title, url: e.url }); + bm.srcId = e.id; // Herkunft fuer kuenftige Dedup + inbox.bookmarks.push(bm); + seenSrc.add(e.id); } await saveBoards(); // NUR die verarbeiteten Eintraege entfernen — ein gleichzeitiger Worker-Append bleibt erhalten. diff --git a/src/js/quicksave-core.js b/src/js/quicksave-core.js index 00706a1..296e505 100644 --- a/src/js/quicksave-core.js +++ b/src/js/quicksave-core.js @@ -34,6 +34,17 @@ return inbox; } + // Sicheres URL-Protokoll (http/https/ftp). Inhaltlich identisch zur data.js-Variante, aber + // DOM-frei und auf globalThis, damit der Quick-Save-Drain (app.js) dieselbe Validierung nutzt + // wie jeder andere Bookmark-Schreibpfad. URL ist in Worker UND Seite verfuegbar. + function isSafeUrl(url) { + try { + return ['http:', 'https:', 'ftp:'].includes(new URL(url).protocol); + } catch (_) { + return false; + } + } + // Normalisiert eine Bookmark in die kanonische Form { id, title, url, desc }. // title-Fallback auf url, desc auf ''. Begrenzt Laengen wie data.js (200/500), // damit Quick-Save-Eintraege das gleiche Schema wie Import/Manuell haben. @@ -52,6 +63,7 @@ root.INBOX_ID = INBOX_ID; root.uid = uid; + root.isSafeUrl = isSafeUrl; root.ensureInbox = ensureInbox; root.normalizeBookmark = normalizeBookmark; })(typeof globalThis !== 'undefined' ? globalThis : self);