fix(quick-save): Drain idempotent (srcId-Dedup) + isSafeUrl-Gate
Zwei Befunde aus der Integrations-Review: - Race: der Page-Drain und der Worker machen je ein read-modify-write auf 'quicksave_pending' ohne kontextuebergreifende Atomizitaet. Ein Worker-Append im await-Fenster des Drains konnte einen bereits gedrainten Eintrag in der Queue belassen, den ein Folge-Drain erneut in die Inbox schrieb (Duplikat). Jede eingespielte Bookmark traegt jetzt die Pending-id als srcId; ein erneut auftauchender Eintrag wird uebersprungen statt doppelt eingefuegt. boards-Write bleibt vor der Queue-Bereinigung -> kein Verlust. - Validierung: der Drain hat e.url ohne isSafeUrl gepusht, anders als jeder andere Bookmark-Schreibpfad. isSafeUrl (jetzt im DOM-freien quicksave-core, http/https/ftp) filtert unsichere/leere Protokolle vor dem Schreiben ins Board.
This commit is contained in:
+12
-3
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user