fix(import): restore favorite + pinned on import/backup restore

A pinned entry came back unpinned after export→import. Wider than pinned:
`favorite` was WRITTEN into the export/backup/sync containers but nothing
ever read it back — not parseEntriesFromJSON, not encryptImportEntry, not
the server INSERTs. So it was a write-only field, and a restore silently
dropped both flags. `pinned` wasn't in any container at all.

Wired the full round-trip:
- containers: export + auto-backup now carry `pinned` (favorite was there).
- parseEntriesFromJSON reads both back (absent → 0, e.g. foreign CSV).
- encryptImportEntry passes them through to the body.
- server create + bulk-import INSERTs write favorite/pinned
  (GetValue<Boolean>(..., False) → 0 default). PUT still ignores them, so a
  sync update can't clobber local flags.

Sync stays device-local as decided 2026-07-11: buildSyncSnapshot now OMITS
favorite explicitly (it was dead weight before; with the passthrough live,
leaving it in would have started propagating it). Commented so the intent
isn't re-broken.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-25 16:06:05 +01:00
parent a6ba0100ee
commit 30e37b3fbd
6 changed files with 47 additions and 14 deletions
+19 -6
View File
@@ -333,7 +333,7 @@ var
LTotpSec, LTotpIv, LKind, LCf, LCfIv, LIcon, LUuid,
LTemplateEnc, LTemplateIv, LCreatedAt, LUpdatedAt,
LSiteEnc, LSiteIv, LTitleEnc, LTitleIv, LTagsEnc, LTagsIv: string;
LIconCustom: Boolean;
LIconCustom, LFavorite, LPinned: Boolean;
LQ: TFDQuery;
begin
try
@@ -374,6 +374,9 @@ begin
LIcon := LBody.GetValue<string>('icon_b64', '');
// Icon provenance (0 = fetched cache, 1 = user upload). Absent → 0.
LIconCustom := LBody.GetValue<Boolean>('icon_custom', False);
// Restore flags (import/backup carry them; sync omits them → 0).
LFavorite := LBody.GetValue<Boolean>('favorite', False);
LPinned := LBody.GetValue<Boolean>('pinned', False);
LTemplateEnc := LBody.GetValue<string>('template_enc', '');
LTemplateIv := LBody.GetValue<string>('template_iv', '');
// Caller may bring its own UUID (sync restore / import preserving
@@ -415,10 +418,10 @@ begin
' site_enc, site_iv, title_enc, title_iv, tags_enc, tags_iv, ' +
' encrypted_password, iv, encryption_method, ' +
' folder, totp_secret, totp_iv, kind, custom_fields, custom_fields_iv,' +
' icon_b64, icon_custom, template_enc, template_iv, uuid, created_at, updated_at, password_changed_at) ' +
' icon_b64, icon_custom, favorite, pinned, template_enc, template_iv, uuid, created_at, updated_at, password_changed_at) ' +
'VALUES (:uid, :uenc, :uiv, :senc, :siv, :tenc, :tiv2, :genc, :giv, ' +
' :e, :i, ''client'', :f, :ts, :tiv, :k, ' +
' :cf, :cfiv, :ic, :iconcust, :tplenc, :tpliv, :uuid, :c, :c2, :c)';
' :cf, :cfiv, :ic, :iconcust, :fav, :pin, :tplenc, :tpliv, :uuid, :c, :c2, :c)';
LQ.ParamByName('uid').AsInteger := LUserId;
// site/username/title/tags cleartext columns dropped — only *_enc written.
BindNullable(LQ, 'uenc', LUserEnc);
@@ -457,6 +460,10 @@ begin
if LIcon = '' then LQ.ParamByName('ic').Clear else LQ.ParamByName('ic').Value := LIcon;
if (LIcon <> '') and LIconCustom then LQ.ParamByName('iconcust').AsInteger := 1
else LQ.ParamByName('iconcust').AsInteger := 0;
if LFavorite then LQ.ParamByName('fav').AsInteger := 1
else LQ.ParamByName('fav').AsInteger := 0;
if LPinned then LQ.ParamByName('pin').AsInteger := 1
else LQ.ParamByName('pin').AsInteger := 0;
BindNullable(LQ, 'tplenc', LTemplateEnc);
BindNullable(LQ, 'tpliv', LTemplateIv);
LQ.ParamByName('uuid').AsString := LUuid;
@@ -1208,7 +1215,7 @@ var
LTotpIv, LNow, LKind, LCf, LCfIv, LIcon, LTemplateEnc, LTemplateIv, LUuid,
LCreatedAt, LUpdatedAt,
LSiteEnc, LSiteIv, LTitleEnc, LTitleIv, LTagsEnc, LTagsIv: string;
LIconCustom: Boolean;
LIconCustom, LFavorite, LPinned: Boolean;
LQ, LTomb: TFDQuery;
begin
try
@@ -1265,10 +1272,10 @@ begin
' site_enc, site_iv, title_enc, title_iv, tags_enc, tags_iv, ' +
' encrypted_password, iv, encryption_method, ' +
' folder, totp_secret, totp_iv, kind, custom_fields, custom_fields_iv,' +
' icon_b64, icon_custom, template_enc, template_iv, uuid, created_at, updated_at) ' +
' icon_b64, icon_custom, favorite, pinned, template_enc, template_iv, uuid, created_at, updated_at) ' +
'VALUES (:uid, :uenc, :uiv, :senc, :siv, :tenc, :tiv2, :genc, :giv, ' +
' :e, :i, ''client'', :f, :ts, :tiv, :k, ' +
' :cf, :cfiv, :ic, :iconcust, :tplenc, :tpliv, :uuid, :c, :c2)';
' :cf, :cfiv, :ic, :iconcust, :fav, :pin, :tplenc, :tpliv, :uuid, :c, :c2)';
// Declare optional param types ONCE — the prepared statement is
// reused across every imported entry, and FireDAC needs the
// type set before the first .Clear call would otherwise fail
@@ -1317,6 +1324,8 @@ begin
LCfIv := LEntry.GetValue<string>('custom_fields_iv', '');
LIcon := LEntry.GetValue<string>('icon_b64', '');
LIconCustom := LEntry.GetValue<Boolean>('icon_custom', False);
LFavorite := LEntry.GetValue<Boolean>('favorite', False);
LPinned := LEntry.GetValue<Boolean>('pinned', False);
LTemplateEnc := LEntry.GetValue<string>('template_enc', '');
LTemplateIv := LEntry.GetValue<string>('template_iv', '');
LUuid := Trim(LEntry.GetValue<string>('uuid', ''));
@@ -1362,6 +1371,10 @@ begin
if LIcon = '' then LQ.ParamByName('ic').Clear else LQ.ParamByName('ic').Value := LIcon;
if (LIcon <> '') and LIconCustom then LQ.ParamByName('iconcust').AsInteger := 1
else LQ.ParamByName('iconcust').AsInteger := 0;
if LFavorite then LQ.ParamByName('fav').AsInteger := 1
else LQ.ParamByName('fav').AsInteger := 0;
if LPinned then LQ.ParamByName('pin').AsInteger := 1
else LQ.ParamByName('pin').AsInteger := 0;
BindNullable(LQ, 'tplenc', LTemplateEnc);
BindNullable(LQ, 'tpliv', LTemplateIv);
LQ.ParamByName('uuid').AsString := LUuid;