fix(editor): warn before overwriting an entry changed elsewhere (A11)
The editor re-ships the payload it loaded on open, so a sync or import that rewrote the same entry meanwhile was silently clobbered by Save — the last hole editorAbsorbChange doesn't cover, since those paths rewrite every field including the ciphertext. Save now compares the live entry against the baseline soState already keeps and asks before overwriting. Ciphertext comparison, so no decryption; changes the editor absorbed read as equal (editorAbsorbChange updates `original` too), so local tag/folder actions never trip it; the post-save reload re-baselines, so a second save is clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4712,6 +4712,45 @@ function soDirtyCheck() {
|
|||||||
if (btn) btn.style.display = isSoDirty() ? '' : 'none';
|
if (btn) btn.style.display = isSoDirty() ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The editor holds the payload it loaded on open and re-ships it whole, so a
|
||||||
|
// sync / import that rewrote the same entry meanwhile would be clobbered by
|
||||||
|
// Save. Compare the live entry against the baseline the editor loaded: any
|
||||||
|
// difference means someone else wrote it, and the user gets to decide.
|
||||||
|
//
|
||||||
|
// Compares ciphertext, not plaintext — no decryption needed, and a re-encrypt
|
||||||
|
// of identical content yields a different blob only if the writer actually
|
||||||
|
// re-encrypted, which is exactly the case worth flagging. Changes the editor
|
||||||
|
// already absorbed (editorAbsorbChange updates `original` too) read as equal,
|
||||||
|
// so local tag/folder actions never trip this.
|
||||||
|
// Returns false when the user backs out — the caller then saves nothing.
|
||||||
|
async function confirmOverwriteIfChangedElsewhere() {
|
||||||
|
const live = state.entries.find(x => x.id === soState.id);
|
||||||
|
if (!live) return true; // trashed/purged elsewhere — the delete gate owns that
|
||||||
|
const o = soState.original;
|
||||||
|
const same =
|
||||||
|
live.encrypted_password === soState.originalEncrypted &&
|
||||||
|
live.iv === soState.originalIV &&
|
||||||
|
(live.totp_secret || null) === (soState.originalTotpEncrypted || null) &&
|
||||||
|
(live.custom_fields|| null) === (soState.originalCustomEncrypted || null) &&
|
||||||
|
(live.title || '') === o.title &&
|
||||||
|
(live.site || '') === o.site &&
|
||||||
|
(live.username || '') === o.username &&
|
||||||
|
(live.folder || 'All') === o.folder &&
|
||||||
|
parseTags(live.tags).join(',') === o.tags;
|
||||||
|
if (same) return true;
|
||||||
|
return await confirmDialog({
|
||||||
|
title: 'Entry changed elsewhere',
|
||||||
|
message: 'This entry was updated after you opened it — by a sync, an ' +
|
||||||
|
'import, or another device. Saving replaces that version with ' +
|
||||||
|
'what is in the editor.' +
|
||||||
|
'<p style="margin:14px 0 0;font-size:11px;color:var(--text-faint);line-height:1.5">' +
|
||||||
|
'Cancel to keep the other version — copy anything you need out ' +
|
||||||
|
'of the editor first, then reopen the entry to see it.</p>',
|
||||||
|
okText: 'Overwrite',
|
||||||
|
danger: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Re-entrancy latch: a double-click on Save (or Enter+click) while the first
|
// Re-entrancy latch: a double-click on Save (or Enter+click) while the first
|
||||||
// run awaits encryption/POST would fire a SECOND full save → duplicate entry.
|
// run awaits encryption/POST would fire a SECOND full save → duplicate entry.
|
||||||
// Same async-race class as the sync-button guard.
|
// Same async-race class as the sync-button guard.
|
||||||
@@ -4759,6 +4798,8 @@ async function soSaveInner() {
|
|||||||
|
|
||||||
const isNew = (soState.id == null);
|
const isNew = (soState.id == null);
|
||||||
|
|
||||||
|
if (!isNew && !(await confirmOverwriteIfChangedElsewhere())) return;
|
||||||
|
|
||||||
// Only re-encrypt if password changed; otherwise reuse stored ciphertext.
|
// Only re-encrypt if password changed; otherwise reuse stored ciphertext.
|
||||||
// For new entries there's nothing stored, always encrypt fresh.
|
// For new entries there's nothing stored, always encrypt fresh.
|
||||||
let enc;
|
let enc;
|
||||||
|
|||||||
Reference in New Issue
Block a user