Add autofill privacy settings controls
This commit is contained in:
@@ -127,6 +127,7 @@ type statePaths struct {
|
|||||||
DefaultSaveAsPath string
|
DefaultSaveAsPath string
|
||||||
RecentVaultsPath string
|
RecentVaultsPath string
|
||||||
RecentRemotesPath string
|
RecentRemotesPath string
|
||||||
|
SettingsPath string
|
||||||
UIPreferencesPath string
|
UIPreferencesPath string
|
||||||
AutofillCachePath string
|
AutofillCachePath string
|
||||||
}
|
}
|
||||||
@@ -150,6 +151,8 @@ type uiPreferences struct {
|
|||||||
GroupControlsHidden bool `json:"groupControlsHidden"`
|
GroupControlsHidden bool `json:"groupControlsHidden"`
|
||||||
LifecycleAdvancedHidden bool `json:"lifecycleAdvancedHidden"`
|
LifecycleAdvancedHidden bool `json:"lifecycleAdvancedHidden"`
|
||||||
HistoryHidden bool `json:"historyHidden"`
|
HistoryHidden bool `json:"historyHidden"`
|
||||||
|
SyncSourceDefault string `json:"syncSourceDefault,omitempty"`
|
||||||
|
SyncDirectionDefault string `json:"syncDirectionDefault,omitempty"`
|
||||||
DenseLayout bool `json:"denseLayout"`
|
DenseLayout bool `json:"denseLayout"`
|
||||||
StatusBannerMillis int `json:"statusBannerMillis,omitempty"`
|
StatusBannerMillis int `json:"statusBannerMillis,omitempty"`
|
||||||
AutofillNoticeMode string `json:"autofillNoticeMode,omitempty"`
|
AutofillNoticeMode string `json:"autofillNoticeMode,omitempty"`
|
||||||
@@ -271,6 +274,10 @@ type ui struct {
|
|||||||
settingsReducedMotionOn widget.Clickable
|
settingsReducedMotionOn widget.Clickable
|
||||||
settingsKeyboardFocusStandard widget.Clickable
|
settingsKeyboardFocusStandard widget.Clickable
|
||||||
settingsKeyboardFocusProminent widget.Clickable
|
settingsKeyboardFocusProminent widget.Clickable
|
||||||
|
showSettingsSyncLocal widget.Clickable
|
||||||
|
showSettingsSyncRemote widget.Clickable
|
||||||
|
showSettingsSyncPull widget.Clickable
|
||||||
|
showSettingsSyncPush widget.Clickable
|
||||||
editEntry widget.Clickable
|
editEntry widget.Clickable
|
||||||
cancelEdit widget.Clickable
|
cancelEdit widget.Clickable
|
||||||
pickVaultPath widget.Clickable
|
pickVaultPath widget.Clickable
|
||||||
@@ -402,10 +409,13 @@ type ui struct {
|
|||||||
keyboardFocus focusID
|
keyboardFocus focusID
|
||||||
defaultSaveAsPath string
|
defaultSaveAsPath string
|
||||||
recentVaultsPath string
|
recentVaultsPath string
|
||||||
|
settingsPath string
|
||||||
uiPreferencesPath string
|
uiPreferencesPath string
|
||||||
recentRemotesPath string
|
recentRemotesPath string
|
||||||
autofillCachePath string
|
autofillCachePath string
|
||||||
editingEntry bool
|
editingEntry bool
|
||||||
|
syncDefaultSourceMode syncSourceMode
|
||||||
|
syncDefaultDirection syncDirection
|
||||||
groupControlsHidden bool
|
groupControlsHidden bool
|
||||||
lifecycleAdvancedHidden bool
|
lifecycleAdvancedHidden bool
|
||||||
historyHidden bool
|
historyHidden bool
|
||||||
@@ -547,6 +557,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
|||||||
lifecycleMode: "local",
|
lifecycleMode: "local",
|
||||||
defaultSaveAsPath: paths.DefaultSaveAsPath,
|
defaultSaveAsPath: paths.DefaultSaveAsPath,
|
||||||
recentVaultsPath: paths.RecentVaultsPath,
|
recentVaultsPath: paths.RecentVaultsPath,
|
||||||
|
settingsPath: paths.SettingsPath,
|
||||||
uiPreferencesPath: paths.UIPreferencesPath,
|
uiPreferencesPath: paths.UIPreferencesPath,
|
||||||
recentRemotesPath: paths.RecentRemotesPath,
|
recentRemotesPath: paths.RecentRemotesPath,
|
||||||
autofillCachePath: paths.AutofillCachePath,
|
autofillCachePath: paths.AutofillCachePath,
|
||||||
@@ -560,6 +571,8 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
|||||||
now: time.Now,
|
now: time.Now,
|
||||||
syncSourceMode: syncSourceLocal,
|
syncSourceMode: syncSourceLocal,
|
||||||
syncDirection: syncDirectionPull,
|
syncDirection: syncDirectionPull,
|
||||||
|
syncDefaultSourceMode: syncSourceLocal,
|
||||||
|
syncDefaultDirection: syncDirectionPull,
|
||||||
apiPolicyGroupScope: true,
|
apiPolicyGroupScope: true,
|
||||||
autofillNoticePreference: autofillNoticeAll,
|
autofillNoticePreference: autofillNoticeAll,
|
||||||
backgroundResults: make(chan backgroundActionResult, 8),
|
backgroundResults: make(chan backgroundActionResult, 8),
|
||||||
@@ -585,6 +598,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
|||||||
u.loadRecentRemotes()
|
u.loadRecentRemotes()
|
||||||
u.restoreStartupLifecycleTarget()
|
u.restoreStartupLifecycleTarget()
|
||||||
u.loadUIPreferences()
|
u.loadUIPreferences()
|
||||||
|
u.loadSettings()
|
||||||
u.loadSettingsFormFromPreferences()
|
u.loadSettingsFormFromPreferences()
|
||||||
u.loadSettingsDraft()
|
u.loadSettingsDraft()
|
||||||
u.filter()
|
u.filter()
|
||||||
@@ -621,6 +635,7 @@ func defaultStatePaths(stateDir string) statePaths {
|
|||||||
DefaultSaveAsPath: filepath.Join(baseDir, "vault.kdbx"),
|
DefaultSaveAsPath: filepath.Join(baseDir, "vault.kdbx"),
|
||||||
RecentVaultsPath: filepath.Join(baseDir, "recent-vaults.json"),
|
RecentVaultsPath: filepath.Join(baseDir, "recent-vaults.json"),
|
||||||
RecentRemotesPath: filepath.Join(baseDir, "recent-remotes.json"),
|
RecentRemotesPath: filepath.Join(baseDir, "recent-remotes.json"),
|
||||||
|
SettingsPath: filepath.Join(baseDir, "settings.json"),
|
||||||
UIPreferencesPath: filepath.Join(baseDir, "ui-prefs.json"),
|
UIPreferencesPath: filepath.Join(baseDir, "ui-prefs.json"),
|
||||||
AutofillCachePath: filepath.Join(baseDir, "autofill-cache.json"),
|
AutofillCachePath: filepath.Join(baseDir, "autofill-cache.json"),
|
||||||
}
|
}
|
||||||
@@ -1093,11 +1108,31 @@ func (u *ui) openAdvancedSyncDialog() {
|
|||||||
u.syncDialogOpen = true
|
u.syncDialogOpen = true
|
||||||
u.syncMenuOpen = false
|
u.syncMenuOpen = false
|
||||||
u.showSyncPassword = false
|
u.showSyncPassword = false
|
||||||
|
u.syncSourceMode = u.syncDefaultSourceMode
|
||||||
|
u.syncDirection = u.syncDefaultDirection
|
||||||
if strings.TrimSpace(u.syncLocalPath.Text()) == "" {
|
if strings.TrimSpace(u.syncLocalPath.Text()) == "" {
|
||||||
u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text()))
|
u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sanitizeSyncSourceMode(mode syncSourceMode) syncSourceMode {
|
||||||
|
switch mode {
|
||||||
|
case syncSourceRemote:
|
||||||
|
return syncSourceRemote
|
||||||
|
default:
|
||||||
|
return syncSourceLocal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeSyncDirection(direction syncDirection) syncDirection {
|
||||||
|
switch direction {
|
||||||
|
case syncDirectionPush:
|
||||||
|
return syncDirectionPush
|
||||||
|
default:
|
||||||
|
return syncDirectionPull
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) advancedSyncAction() error {
|
func (u *ui) advancedSyncAction() error {
|
||||||
switch u.syncDirection {
|
switch u.syncDirection {
|
||||||
case syncDirectionPush:
|
case syncDirectionPush:
|
||||||
@@ -2728,6 +2763,18 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
|||||||
for u.showSyncPush.Clicked(gtx) {
|
for u.showSyncPush.Clicked(gtx) {
|
||||||
u.syncDirection = syncDirectionPush
|
u.syncDirection = syncDirectionPush
|
||||||
}
|
}
|
||||||
|
for u.showSettingsSyncLocal.Clicked(gtx) {
|
||||||
|
u.settingsDraft.Sync.SourceDefault = syncSourceLocal
|
||||||
|
}
|
||||||
|
for u.showSettingsSyncRemote.Clicked(gtx) {
|
||||||
|
u.settingsDraft.Sync.SourceDefault = syncSourceRemote
|
||||||
|
}
|
||||||
|
for u.showSettingsSyncPull.Clicked(gtx) {
|
||||||
|
u.settingsDraft.Sync.DirectionDefault = syncDirectionPull
|
||||||
|
}
|
||||||
|
for u.showSettingsSyncPush.Clicked(gtx) {
|
||||||
|
u.settingsDraft.Sync.DirectionDefault = syncDirectionPush
|
||||||
|
}
|
||||||
for u.showAutofillApprovalAsk.Clicked(gtx) {
|
for u.showAutofillApprovalAsk.Clicked(gtx) {
|
||||||
u.autofillFirstFillApprovalMode = autofillFirstFillApprovalAsk
|
u.autofillFirstFillApprovalMode = autofillFirstFillApprovalAsk
|
||||||
}
|
}
|
||||||
@@ -3210,7 +3257,7 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
|
|||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
lbl := material.Label(u.theme, unit.Sp(14), "Choose how KeePassGO remembers UI layout behavior, tunes noncritical feedback, and sets the KDBX security defaults used for new or future saves.")
|
lbl := material.Label(u.theme, unit.Sp(14), "Choose how KeePassGO remembers UI layout behavior, sync defaults, and KDBX security defaults without crowding the main vault flow.")
|
||||||
lbl.Color = mutedColor
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
@@ -3248,6 +3295,56 @@ func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
|
|||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(labeledEditorHelp(u.theme, "KDF", "Supported values: "+strings.Join([]string{vault.KDFAES, vault.KDFArgon2}, ", "), &u.securityKDF, false)),
|
layout.Rigid(labeledEditorHelp(u.theme, "KDF", "Supported values: "+strings.Join([]string{vault.KDFAES, vault.KDFArgon2}, ", "), &u.securityKDF, false)),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||||
|
layout.Rigid(syncDialogSectionLabel(u.theme, "Sync Defaults")),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(13), "Advanced Sync starts from these defaults. You can still change the source or direction before a single run.")
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return syncChoiceButton(gtx, u.theme, &u.showSettingsSyncPull, "Pull Into Current Vault", u.settingsDraft.Sync.DirectionDefault == syncDirectionPull)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return syncChoiceButton(gtx, u.theme, &u.showSettingsSyncPush, "Push Current Vault Out", u.settingsDraft.Sync.DirectionDefault == syncDirectionPush)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return syncChoiceButton(gtx, u.theme, &u.showSettingsSyncLocal, "Local File", u.settingsDraft.Sync.SourceDefault == syncSourceLocal)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return syncChoiceButton(gtx, u.theme, &u.showSettingsSyncRemote, "Remote WebDAV", u.settingsDraft.Sync.SourceDefault == syncSourceRemote)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return syncDialogSummaryCard(gtx, u.theme, u.settingsDraft.Sync.SourceDefault, u.settingsDraft.Sync.DirectionDefault)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(12), "Conflict handling stays retry-safe: merged entry changes keep history, while remote save conflicts still require reopening the vault and retrying the save.")
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||||
|
layout.Rigid(syncDialogSectionLabel(u.theme, "Background Sync")),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(12), "Future background sync controls belong here so source, direction, and unattended behavior stay in one settings surface.")
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||||
layout.Rigid(syncDialogSectionLabel(u.theme, "Feedback")),
|
layout.Rigid(syncDialogSectionLabel(u.theme, "Feedback")),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -3451,7 +3548,7 @@ func (u *ui) syncDialogContent(gtx layout.Context) layout.Dimensions {
|
|||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
lbl := material.Label(u.theme, unit.Sp(14), "Pick direction, choose the other vault, and then run the merge. The quick sync button keeps using the current source.")
|
lbl := material.Label(u.theme, unit.Sp(14), "Pick direction, choose the other vault, and then run the merge. Saved source and direction defaults now live in Settings.")
|
||||||
lbl.Color = mutedColor
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
|
|||||||
+190
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
@@ -3558,6 +3559,118 @@ func TestUIDenseLayoutPreferencePersists(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUISyncDefaultsPersistInSettings(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
configPath := filepath.Join(t.TempDir(), "settings.json")
|
||||||
|
|
||||||
|
first := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
first.settingsPath = configPath
|
||||||
|
first.syncDefaultSourceMode = syncSourceRemote
|
||||||
|
first.syncDefaultDirection = syncDirectionPush
|
||||||
|
first.saveSettings()
|
||||||
|
|
||||||
|
second := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
second.settingsPath = configPath
|
||||||
|
second.syncDefaultSourceMode = syncSourceLocal
|
||||||
|
second.syncDefaultDirection = syncDirectionPull
|
||||||
|
second.loadSettings()
|
||||||
|
|
||||||
|
if got := second.syncDefaultSourceMode; got != syncSourceRemote {
|
||||||
|
t.Fatalf("syncDefaultSourceMode = %q, want remote", got)
|
||||||
|
}
|
||||||
|
if got := second.syncDefaultDirection; got != syncDirectionPush {
|
||||||
|
t.Fatalf("syncDefaultDirection = %q, want push", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUILoadSettingsFallsBackToLegacySyncDefaultsInUIPreferences(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
legacyPath := filepath.Join(dir, "ui-prefs.json")
|
||||||
|
content, err := json.MarshalIndent(uiPreferences{
|
||||||
|
SyncSourceDefault: string(syncSourceRemote),
|
||||||
|
SyncDirectionDefault: string(syncDirectionPush),
|
||||||
|
}, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("json.MarshalIndent() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(legacyPath, content, 0o600); err != nil {
|
||||||
|
t.Fatalf("os.WriteFile() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reloaded := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
reloaded.uiPreferencesPath = legacyPath
|
||||||
|
reloaded.settingsPath = filepath.Join(dir, "settings.json")
|
||||||
|
reloaded.syncDefaultSourceMode = syncSourceLocal
|
||||||
|
reloaded.syncDefaultDirection = syncDirectionPull
|
||||||
|
reloaded.loadSettings()
|
||||||
|
|
||||||
|
if got := reloaded.syncDefaultSourceMode; got != syncSourceRemote {
|
||||||
|
t.Fatalf("syncDefaultSourceMode = %q after legacy load, want remote", got)
|
||||||
|
}
|
||||||
|
if got := reloaded.syncDefaultDirection; got != syncDirectionPush {
|
||||||
|
t.Fatalf("syncDefaultDirection = %q after legacy load, want push", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIOpenAdvancedSyncDialogUsesSavedSyncDefaults(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
u.syncDefaultSourceMode = syncSourceRemote
|
||||||
|
u.syncDefaultDirection = syncDirectionPush
|
||||||
|
u.syncSourceMode = syncSourceLocal
|
||||||
|
u.syncDirection = syncDirectionPull
|
||||||
|
u.vaultPath.SetText("/vaults/current.kdbx")
|
||||||
|
|
||||||
|
u.openAdvancedSyncDialog()
|
||||||
|
|
||||||
|
if got := u.syncSourceMode; got != syncSourceRemote {
|
||||||
|
t.Fatalf("syncSourceMode = %q after open, want remote default", got)
|
||||||
|
}
|
||||||
|
if got := u.syncDirection; got != syncDirectionPush {
|
||||||
|
t.Fatalf("syncDirection = %q after open, want push default", got)
|
||||||
|
}
|
||||||
|
if got := u.syncLocalPath.Text(); got != "/vaults/current.kdbx" {
|
||||||
|
t.Fatalf("syncLocalPath = %q after open, want current vault path", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUISaveSecuritySettingsPersistsSyncDefaults(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
manager := &session.Manager{}
|
||||||
|
u := newUIWithSession("desktop", manager)
|
||||||
|
u.masterPassword.SetText("correct horse battery staple")
|
||||||
|
if err := u.createVaultAction(); err != nil {
|
||||||
|
t.Fatalf("createVaultAction() error = %v", err)
|
||||||
|
}
|
||||||
|
u.securityCipher.SetText(vault.CipherAES256)
|
||||||
|
u.securityKDF.SetText(vault.KDFAES)
|
||||||
|
u.loadSettingsDraft()
|
||||||
|
u.settingsDraft.Sync.SourceDefault = syncSourceRemote
|
||||||
|
u.settingsDraft.Sync.DirectionDefault = syncDirectionPush
|
||||||
|
u.settingsPath = filepath.Join(t.TempDir(), "settings.json")
|
||||||
|
u.uiPreferencesPath = filepath.Join(t.TempDir(), "ui-prefs.json")
|
||||||
|
|
||||||
|
if err := u.saveSecuritySettingsAction(); err != nil {
|
||||||
|
t.Fatalf("saveSecuritySettingsAction() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reloaded := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
reloaded.settingsPath = u.settingsPath
|
||||||
|
reloaded.loadSettings()
|
||||||
|
|
||||||
|
if got := reloaded.syncDefaultSourceMode; got != syncSourceRemote {
|
||||||
|
t.Fatalf("reloaded syncDefaultSourceMode = %q, want remote", got)
|
||||||
|
}
|
||||||
|
if got := reloaded.syncDefaultDirection; got != syncDirectionPush {
|
||||||
|
t.Fatalf("reloaded syncDefaultDirection = %q, want push", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUIAccessibilityPreferencesPersist(t *testing.T) {
|
func TestUIAccessibilityPreferencesPersist(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -3657,6 +3770,83 @@ func TestUINotificationPreferencesPersist(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAutofillPrivacyLinesNormalizesEntries(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got := autofillPrivacyLines(" com.android.chrome \n\ncom.example.app\ncom.android.chrome\n org.keepassgo.browser ")
|
||||||
|
want := []string{"com.android.chrome", "com.example.app", "org.keepassgo.browser"}
|
||||||
|
if !slices.Equal(got, want) {
|
||||||
|
t.Fatalf("autofillPrivacyLines() = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJoinAutofillPrivacyLines(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got := joinAutofillPrivacyLines([]string{"com.android.chrome", "com.example.app"})
|
||||||
|
if got != "com.android.chrome\ncom.example.app" {
|
||||||
|
t.Fatalf("joinAutofillPrivacyLines() = %q, want %q", got, "com.android.chrome\ncom.example.app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIAutofillPrivacyPreferencesPersist(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
configPath := filepath.Join(t.TempDir(), "ui-prefs.json")
|
||||||
|
|
||||||
|
first := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
first.uiPreferencesPath = configPath
|
||||||
|
first.autofillFirstFillApprovalMode = autofillFirstFillApprovalBlock
|
||||||
|
first.autofillBrowserAllowlist.SetText("https://accounts.example.com\nhttps://login.example.org\nhttps://accounts.example.com")
|
||||||
|
first.autofillAppAllowlist.SetText("org.mozilla.firefox\ncom.android.chrome")
|
||||||
|
first.autofillPackageRules.SetText("com.android.chrome=hostname\norg.keepassgo.browser=view-id")
|
||||||
|
first.saveUIPreferences()
|
||||||
|
|
||||||
|
second := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
second.uiPreferencesPath = configPath
|
||||||
|
second.autofillFirstFillApprovalMode = autofillFirstFillApprovalAsk
|
||||||
|
second.loadUIPreferences()
|
||||||
|
|
||||||
|
if got := second.autofillFirstFillApprovalMode; got != autofillFirstFillApprovalBlock {
|
||||||
|
t.Fatalf("autofillFirstFillApprovalMode = %q, want %q", got, autofillFirstFillApprovalBlock)
|
||||||
|
}
|
||||||
|
if got := second.autofillBrowserAllowlist.Text(); got != "https://accounts.example.com\nhttps://login.example.org" {
|
||||||
|
t.Fatalf("autofillBrowserAllowlist = %q, want normalized browser allowlist", got)
|
||||||
|
}
|
||||||
|
if got := second.autofillAppAllowlist.Text(); got != "org.mozilla.firefox\ncom.android.chrome" {
|
||||||
|
t.Fatalf("autofillAppAllowlist = %q, want preserved allowlist entries", got)
|
||||||
|
}
|
||||||
|
if got := second.autofillPackageRules.Text(); got != "com.android.chrome=hostname\norg.keepassgo.browser=view-id" {
|
||||||
|
t.Fatalf("autofillPackageRules = %q, want persisted package rules", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUILoadUIPreferencesKeepsDefaultAutofillApprovalWhenMissing(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
configPath := filepath.Join(t.TempDir(), "ui-prefs.json")
|
||||||
|
content, err := json.Marshal(uiPreferences{
|
||||||
|
GroupControlsHidden: true,
|
||||||
|
LifecycleAdvancedHidden: true,
|
||||||
|
HistoryHidden: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal(uiPreferences) error = %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(configPath, content, 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile(uiPreferences) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
u.uiPreferencesPath = configPath
|
||||||
|
u.autofillFirstFillApprovalMode = autofillFirstFillApprovalAsk
|
||||||
|
u.loadUIPreferences()
|
||||||
|
|
||||||
|
if got := u.autofillFirstFillApprovalMode; got != autofillFirstFillApprovalAsk {
|
||||||
|
t.Fatalf("autofillFirstFillApprovalMode = %q, want %q when preference missing", got, autofillFirstFillApprovalAsk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSelectingRecentRemoteConnectionKeepsPasswordMasked(t *testing.T) {
|
func TestSelectingRecentRemoteConnectionKeepsPasswordMasked(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -30,8 +33,23 @@ type accessibilityPreferences struct {
|
|||||||
KeyboardFocus string
|
KeyboardFocus string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type settingsFile struct {
|
||||||
|
Sync syncSettings `json:"sync,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type syncSettings struct {
|
||||||
|
SourceDefault string `json:"sourceDefault,omitempty"`
|
||||||
|
DirectionDefault string `json:"directionDefault,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type syncSettingsDraft struct {
|
||||||
|
SourceDefault syncSourceMode
|
||||||
|
DirectionDefault syncDirection
|
||||||
|
}
|
||||||
|
|
||||||
type settingsDraft struct {
|
type settingsDraft struct {
|
||||||
Accessibility accessibilityPreferences
|
Accessibility accessibilityPreferences
|
||||||
|
Sync syncSettingsDraft
|
||||||
}
|
}
|
||||||
|
|
||||||
type choiceSpec struct {
|
type choiceSpec struct {
|
||||||
@@ -87,6 +105,10 @@ func (u *ui) loadSettingsDraft() {
|
|||||||
ReducedMotion: u.accessibilityPrefs.ReducedMotion,
|
ReducedMotion: u.accessibilityPrefs.ReducedMotion,
|
||||||
KeyboardFocus: u.accessibilityPrefs.KeyboardFocus,
|
KeyboardFocus: u.accessibilityPrefs.KeyboardFocus,
|
||||||
},
|
},
|
||||||
|
Sync: syncSettingsDraft{
|
||||||
|
SourceDefault: u.syncDefaultSourceMode,
|
||||||
|
DirectionDefault: u.syncDefaultDirection,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,13 +124,70 @@ func (u *ui) saveSecuritySettingsAction() error {
|
|||||||
u.settingsDraft.Accessibility.DisplayDensity = displayDensityForDenseLayout(u.settingsDenseLayout.Value)
|
u.settingsDraft.Accessibility.DisplayDensity = displayDensityForDenseLayout(u.settingsDenseLayout.Value)
|
||||||
}
|
}
|
||||||
u.settingsDenseLayout.Value = u.settingsDraft.Accessibility.DisplayDensity == displayDensityDense
|
u.settingsDenseLayout.Value = u.settingsDraft.Accessibility.DisplayDensity == displayDensityDense
|
||||||
|
u.syncDefaultSourceMode = sanitizeSyncSourceMode(u.settingsDraft.Sync.SourceDefault)
|
||||||
|
u.syncDefaultDirection = sanitizeSyncDirection(u.settingsDraft.Sync.DirectionDefault)
|
||||||
u.applySettingsFormToPreferences()
|
u.applySettingsFormToPreferences()
|
||||||
u.applyAccessibilityPreferences(u.settingsDraft.Accessibility)
|
u.applyAccessibilityPreferences(u.settingsDraft.Accessibility)
|
||||||
|
u.saveSettings()
|
||||||
u.saveUIPreferences()
|
u.saveUIPreferences()
|
||||||
u.securityDialogOpen = false
|
u.securityDialogOpen = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) loadSettings() {
|
||||||
|
u.syncDefaultSourceMode = syncSourceLocal
|
||||||
|
u.syncDefaultDirection = syncDirectionPull
|
||||||
|
|
||||||
|
if strings.TrimSpace(u.settingsPath) != "" {
|
||||||
|
content, err := os.ReadFile(u.settingsPath)
|
||||||
|
if err == nil {
|
||||||
|
var settings settingsFile
|
||||||
|
if json.Unmarshal(content, &settings) == nil {
|
||||||
|
u.syncDefaultSourceMode = sanitizeSyncSourceMode(syncSourceMode(settings.Sync.SourceDefault))
|
||||||
|
u.syncDefaultDirection = sanitizeSyncDirection(syncDirection(settings.Sync.DirectionDefault))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u.loadLegacySyncDefaultsFromUIPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) loadLegacySyncDefaultsFromUIPreferences() {
|
||||||
|
if strings.TrimSpace(u.uiPreferencesPath) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content, err := os.ReadFile(u.uiPreferencesPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var prefs uiPreferences
|
||||||
|
if err := json.Unmarshal(content, &prefs); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.syncDefaultSourceMode = sanitizeSyncSourceMode(syncSourceMode(prefs.SyncSourceDefault))
|
||||||
|
u.syncDefaultDirection = sanitizeSyncDirection(syncDirection(prefs.SyncDirectionDefault))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) saveSettings() {
|
||||||
|
if strings.TrimSpace(u.settingsPath) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(u.settingsPath), 0o700); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content, err := json.MarshalIndent(settingsFile{
|
||||||
|
Sync: syncSettings{
|
||||||
|
SourceDefault: string(u.syncDefaultSourceMode),
|
||||||
|
DirectionDefault: string(u.syncDefaultDirection),
|
||||||
|
},
|
||||||
|
}, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = os.WriteFile(u.settingsPath, content, 0o600)
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) showStatusMessage(message string) {
|
func (u *ui) showStatusMessage(message string) {
|
||||||
u.state.StatusMessage = message
|
u.state.StatusMessage = message
|
||||||
if u.accessibilityPrefs.ReducedMotion {
|
if u.accessibilityPrefs.ReducedMotion {
|
||||||
|
|||||||
Reference in New Issue
Block a user