Fix hidden root navigation and browser fill matching
This commit is contained in:
@@ -13,10 +13,24 @@ function isVisibleInput(input) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dispatchFillEvents(input) {
|
function dispatchFillEvents(input) {
|
||||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
if (typeof InputEvent === "function") {
|
||||||
|
input.dispatchEvent(new InputEvent("input", { bubbles: true, data: input.value, inputType: "insertText" }));
|
||||||
|
} else {
|
||||||
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
}
|
||||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setInputValue(input, value) {
|
||||||
|
const prototype = Object.getPrototypeOf(input);
|
||||||
|
const descriptor = prototype ? Object.getOwnPropertyDescriptor(prototype, "value") : null;
|
||||||
|
if (descriptor?.set) {
|
||||||
|
descriptor.set.call(input, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
function findPasswordInput() {
|
function findPasswordInput() {
|
||||||
return Array.from(document.querySelectorAll('input[type="password"]')).find(isVisibleInput) || null;
|
return Array.from(document.querySelectorAll('input[type="password"]')).find(isVisibleInput) || null;
|
||||||
}
|
}
|
||||||
@@ -24,12 +38,16 @@ function findPasswordInput() {
|
|||||||
function findUsernameInput(passwordInput) {
|
function findUsernameInput(passwordInput) {
|
||||||
const form = passwordInput?.form || null;
|
const form = passwordInput?.form || null;
|
||||||
const scope = form || document;
|
const scope = form || document;
|
||||||
const candidates = Array.from(scope.querySelectorAll('input[type="text"], input[type="email"], input:not([type])'))
|
const candidates = Array.from(scope.querySelectorAll('input[type="text"], input[type="email"], input:not([type]), input[autocomplete="username"], input[autocomplete="email"]'))
|
||||||
.filter(isVisibleInput);
|
.filter(isVisibleInput);
|
||||||
if (passwordInput) {
|
if (passwordInput) {
|
||||||
const sameForm = candidates.find((input) => input.form === passwordInput.form);
|
const sameForm = candidates.filter((input) => input.form === passwordInput.form);
|
||||||
if (sameForm) {
|
if (sameForm.length !== 0) {
|
||||||
return sameForm;
|
const priorSibling = sameForm.find((input) =>
|
||||||
|
typeof input.compareDocumentPosition === "function" &&
|
||||||
|
Boolean(input.compareDocumentPosition(passwordInput) & Node.DOCUMENT_POSITION_FOLLOWING)
|
||||||
|
);
|
||||||
|
return priorSibling || sameForm[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return candidates[0] || null;
|
return candidates[0] || null;
|
||||||
@@ -41,12 +59,12 @@ function fillCredential(credential) {
|
|||||||
|
|
||||||
if (usernameInput && credential.username) {
|
if (usernameInput && credential.username) {
|
||||||
usernameInput.focus();
|
usernameInput.focus();
|
||||||
usernameInput.value = credential.username;
|
setInputValue(usernameInput, credential.username);
|
||||||
dispatchFillEvents(usernameInput);
|
dispatchFillEvents(usernameInput);
|
||||||
}
|
}
|
||||||
if (passwordInput && credential.password) {
|
if (passwordInput && credential.password) {
|
||||||
passwordInput.focus();
|
passwordInput.focus();
|
||||||
passwordInput.value = credential.password;
|
setInputValue(passwordInput, credential.password);
|
||||||
dispatchFillEvents(passwordInput);
|
dispatchFillEvents(passwordInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"permissions": ["activeTab", "nativeMessaging", "storage", "tabs"],
|
"permissions": ["activeTab", "nativeMessaging", "storage", "tabs"],
|
||||||
"host_permissions": ["http://*/*", "https://*/*"],
|
"host_permissions": ["http://*/*", "https://*/*"],
|
||||||
"background": {
|
"background": {
|
||||||
|
"scripts": ["background.js"],
|
||||||
"service_worker": "background.js"
|
"service_worker": "background.js"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
|
|||||||
+21
-5
@@ -1027,12 +1027,28 @@ func normalizedBrowserHost(raw string) (string, error) {
|
|||||||
return host, nil
|
return host, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func classifyBrowserEntryMatch(pageHost, rawEntryURL string) (string, int) {
|
func normalizedBrowserEntryHost(raw string) string {
|
||||||
parsed, err := url.Parse(strings.TrimSpace(rawEntryURL))
|
raw = strings.TrimSpace(raw)
|
||||||
if err != nil {
|
if raw == "" {
|
||||||
return "", 0
|
return ""
|
||||||
}
|
}
|
||||||
entryHost := strings.ToLower(parsed.Hostname())
|
parsed, err := url.Parse(raw)
|
||||||
|
if err == nil {
|
||||||
|
if host := strings.ToLower(parsed.Hostname()); host != "" {
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(raw, "://") {
|
||||||
|
parsed, err = url.Parse("https://" + raw)
|
||||||
|
if err == nil {
|
||||||
|
return strings.ToLower(parsed.Hostname())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func classifyBrowserEntryMatch(pageHost, rawEntryURL string) (string, int) {
|
||||||
|
entryHost := normalizedBrowserEntryHost(rawEntryURL)
|
||||||
if entryHost == "" {
|
if entryHost == "" {
|
||||||
return "", 0
|
return "", 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,6 +184,40 @@ func TestVaultServiceFindsBrowserLoginsForAuthorizedClients(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVaultServiceFindsBrowserLoginsForSchemeLessEntryURLs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client, _, cleanup := newTestClientForModel(t, vault.Model{
|
||||||
|
Entries: []vault.Entry{
|
||||||
|
{
|
||||||
|
ID: "gitlab",
|
||||||
|
Title: "GitLab",
|
||||||
|
Username: "jjulian",
|
||||||
|
Password: "secret",
|
||||||
|
URL: "gitlab.com",
|
||||||
|
Path: []string{"Root", "Internet"},
|
||||||
|
},
|
||||||
|
testAPITokenEntry(t,
|
||||||
|
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListEntries, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
resp, err := client.FindBrowserLogins(tokenContext(defaultTestTokenSecret), &keepassgov1.FindBrowserLoginsRequest{
|
||||||
|
PageUrl: "https://gitlab.com/users/sign_in",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FindBrowserLogins() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(resp.Matches) != 1 {
|
||||||
|
t.Fatalf("len(FindBrowserLogins().Matches) = %d, want 1", len(resp.Matches))
|
||||||
|
}
|
||||||
|
if resp.Matches[0].Id != "gitlab" {
|
||||||
|
t.Fatalf("FindBrowserLogins().Matches[0].Id = %q, want gitlab", resp.Matches[0].Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVaultServiceFindsBrowserLoginsWithinAuthorizedGroupScope(t *testing.T) {
|
func TestVaultServiceFindsBrowserLoginsWithinAuthorizedGroupScope(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -2871,7 +2871,7 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
|
|||||||
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
for u.groupClicks[idx].Clicked(gtx) {
|
for u.groupClicks[idx].Clicked(gtx) {
|
||||||
u.state.EnterGroup(name)
|
u.state.EnterGroup(name)
|
||||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
u.adoptStateCurrentPath()
|
||||||
u.filter()
|
u.filter()
|
||||||
}
|
}
|
||||||
return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
|
return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
|
||||||
@@ -2902,7 +2902,7 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
|
|||||||
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
for u.groupClicks[idx].Clicked(gtx) {
|
for u.groupClicks[idx].Clicked(gtx) {
|
||||||
u.state.EnterGroup(name)
|
u.state.EnterGroup(name)
|
||||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
u.adoptStateCurrentPath()
|
||||||
u.filter()
|
u.filter()
|
||||||
}
|
}
|
||||||
return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
|
return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
|
||||||
|
|||||||
@@ -275,8 +275,7 @@ func (u *ui) deleteCurrentGroupAction() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.clearDeleteGroupConfirmation()
|
u.clearDeleteGroupConfirmation()
|
||||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
u.adoptStateCurrentPath()
|
||||||
u.syncedPath = append([]string(nil), u.state.CurrentPath...)
|
|
||||||
u.filter()
|
u.filter()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-3
@@ -23,6 +23,7 @@ import (
|
|||||||
detaillayout "git.julianfamily.org/keepassgo/internal/appui/detail/layout"
|
detaillayout "git.julianfamily.org/keepassgo/internal/appui/detail/layout"
|
||||||
"git.julianfamily.org/keepassgo/internal/clipboard"
|
"git.julianfamily.org/keepassgo/internal/clipboard"
|
||||||
"git.julianfamily.org/keepassgo/internal/session"
|
"git.julianfamily.org/keepassgo/internal/session"
|
||||||
|
"git.julianfamily.org/keepassgo/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (u *ui) bannerSurface() uiBanner {
|
func (u *ui) bannerSurface() uiBanner {
|
||||||
@@ -552,10 +553,72 @@ func (u *ui) setCurrentPath(path []string) {
|
|||||||
u.clearDeleteGroupConfirmation()
|
u.clearDeleteGroupConfirmation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyPath(path []string) []string {
|
||||||
|
return append([]string(nil), path...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathExistsInModel(model vault.Model, path []string) bool {
|
||||||
|
return len(model.EntriesInPath(path)) > 0 || len(model.ChildGroups(path)) > 0 || hasExactGroup(model, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeEntriesPathWithoutModel(path []string, root string) []string {
|
||||||
|
if root == "" {
|
||||||
|
return copyPath(path)
|
||||||
|
}
|
||||||
|
if len(path) == 0 {
|
||||||
|
return []string{root}
|
||||||
|
}
|
||||||
|
if path[0] == "Root" {
|
||||||
|
return append([]string{root}, path[1:]...)
|
||||||
|
}
|
||||||
|
return copyPath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) normalizedEntriesPath(path []string) []string {
|
||||||
|
if u.state.Section != appstate.SectionEntries {
|
||||||
|
return copyPath(path)
|
||||||
|
}
|
||||||
|
root := u.hiddenVaultRoot()
|
||||||
|
model, err := u.state.Session.Current()
|
||||||
|
if err != nil {
|
||||||
|
return normalizeEntriesPathWithoutModel(path, root)
|
||||||
|
}
|
||||||
|
if len(path) == 0 {
|
||||||
|
if root == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{root}
|
||||||
|
}
|
||||||
|
if path[0] == "Root" && root != "" {
|
||||||
|
candidate := append([]string{root}, path[1:]...)
|
||||||
|
if pathExistsInModel(model, candidate) {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (len(path) == 1 && root != "" && path[0] == root) || pathExistsInModel(model, path) {
|
||||||
|
return copyPath(path)
|
||||||
|
}
|
||||||
|
if root == "" {
|
||||||
|
return copyPath(path)
|
||||||
|
}
|
||||||
|
return []string{root}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) adoptStateCurrentPath() {
|
||||||
|
path := u.normalizedEntriesPath(u.state.CurrentPath)
|
||||||
|
u.currentPath = append([]string(nil), path...)
|
||||||
|
u.state.CurrentPath = append([]string(nil), path...)
|
||||||
|
u.syncedPath = append([]string(nil), path...)
|
||||||
|
u.syncPhoneGroupBrowser(path)
|
||||||
|
if len(u.deleteGroupPath) > 0 && !slices.Equal(u.deleteGroupPath, u.currentPath) {
|
||||||
|
u.clearDeleteGroupConfirmation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) syncCurrentPath() {
|
func (u *ui) syncCurrentPath() {
|
||||||
switch {
|
switch {
|
||||||
case slices.Equal(u.currentPath, u.syncedPath) && !slices.Equal(u.state.CurrentPath, u.syncedPath):
|
case slices.Equal(u.currentPath, u.syncedPath) && !slices.Equal(u.state.CurrentPath, u.syncedPath):
|
||||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
u.adoptStateCurrentPath()
|
||||||
case !slices.Equal(u.currentPath, u.syncedPath) && slices.Equal(u.state.CurrentPath, u.syncedPath):
|
case !slices.Equal(u.currentPath, u.syncedPath) && slices.Equal(u.state.CurrentPath, u.syncedPath):
|
||||||
u.state.CurrentPath = append([]string(nil), u.currentPath...)
|
u.state.CurrentPath = append([]string(nil), u.currentPath...)
|
||||||
case !slices.Equal(u.currentPath, u.syncedPath) && !slices.Equal(u.state.CurrentPath, u.syncedPath):
|
case !slices.Equal(u.currentPath, u.syncedPath) && !slices.Equal(u.state.CurrentPath, u.syncedPath):
|
||||||
@@ -1217,8 +1280,7 @@ func (u *ui) handleGroupClicks(gtx layout.Context) {
|
|||||||
for u.moveGroup.Clicked(gtx) {
|
for u.moveGroup.Clicked(gtx) {
|
||||||
u.clearDeleteGroupConfirmation()
|
u.clearDeleteGroupConfirmation()
|
||||||
u.runAction("move group", u.moveCurrentGroupAction)
|
u.runAction("move group", u.moveCurrentGroupAction)
|
||||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
u.adoptStateCurrentPath()
|
||||||
u.syncedPath = append([]string(nil), u.state.CurrentPath...)
|
|
||||||
u.filter()
|
u.filter()
|
||||||
}
|
}
|
||||||
for u.toggleGroupControls.Clicked(gtx) {
|
for u.toggleGroupControls.Clicked(gtx) {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func (u *ui) createVaultAction() error {
|
|||||||
u.noteRecentVault(u.saveAsTargetPath())
|
u.noteRecentVault(u.saveAsTargetPath())
|
||||||
}
|
}
|
||||||
u.resetPasswordPeek()
|
u.resetPasswordPeek()
|
||||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
u.adoptStateCurrentPath()
|
||||||
u.loadSecuritySettingsFromSession()
|
u.loadSecuritySettingsFromSession()
|
||||||
u.editingEntry = false
|
u.editingEntry = false
|
||||||
u.filter()
|
u.filter()
|
||||||
@@ -69,7 +69,7 @@ func (u *ui) openVaultAction() error {
|
|||||||
}
|
}
|
||||||
u.noteRecentVault(path)
|
u.noteRecentVault(path)
|
||||||
u.resetPasswordPeek()
|
u.resetPasswordPeek()
|
||||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
u.adoptStateCurrentPath()
|
||||||
u.restoreRecentVaultGroup(path)
|
u.restoreRecentVaultGroup(path)
|
||||||
u.syncSavedRemoteBindingSelection()
|
u.syncSavedRemoteBindingSelection()
|
||||||
if err := u.synchronizeSelectedRemoteBindingOnOpen(); err != nil {
|
if err := u.synchronizeSelectedRemoteBindingOnOpen(); err != nil {
|
||||||
@@ -111,7 +111,7 @@ func (u *ui) startOpenVaultAction() {
|
|||||||
manager.ApplyPreparedLocalOpen(prepared)
|
manager.ApplyPreparedLocalOpen(prepared)
|
||||||
u.noteRecentVault(path)
|
u.noteRecentVault(path)
|
||||||
u.resetPasswordPeek()
|
u.resetPasswordPeek()
|
||||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
u.adoptStateCurrentPath()
|
||||||
u.restoreRecentVaultGroup(path)
|
u.restoreRecentVaultGroup(path)
|
||||||
u.syncSavedRemoteBindingSelection()
|
u.syncSavedRemoteBindingSelection()
|
||||||
if err := u.synchronizeSelectedRemoteBindingOnOpen(); err != nil {
|
if err := u.synchronizeSelectedRemoteBindingOnOpen(); err != nil {
|
||||||
@@ -329,7 +329,7 @@ func (u *ui) lockAction() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.requestMasterPassFocus = true
|
u.requestMasterPassFocus = true
|
||||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
u.adoptStateCurrentPath()
|
||||||
u.resetPasswordPeek()
|
u.resetPasswordPeek()
|
||||||
u.editingEntry = false
|
u.editingEntry = false
|
||||||
u.filter()
|
u.filter()
|
||||||
@@ -346,7 +346,7 @@ func (u *ui) unlockAction() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.resetPasswordPeek()
|
u.resetPasswordPeek()
|
||||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
u.adoptStateCurrentPath()
|
||||||
u.loadSecuritySettingsFromSession()
|
u.loadSecuritySettingsFromSession()
|
||||||
u.editingEntry = false
|
u.editingEntry = false
|
||||||
u.filter()
|
u.filter()
|
||||||
@@ -375,7 +375,7 @@ func (u *ui) startUnlockAction() {
|
|||||||
return func() error {
|
return func() error {
|
||||||
manager.ApplyPreparedUnlock(prepared)
|
manager.ApplyPreparedUnlock(prepared)
|
||||||
u.resetPasswordPeek()
|
u.resetPasswordPeek()
|
||||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
u.adoptStateCurrentPath()
|
||||||
u.loadSecuritySettingsFromSession()
|
u.loadSecuritySettingsFromSession()
|
||||||
u.editingEntry = false
|
u.editingEntry = false
|
||||||
u.filter()
|
u.filter()
|
||||||
|
|||||||
@@ -5192,6 +5192,37 @@ func TestUIShowEntriesSectionRestoresHiddenRootAfterLeavingEntries(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUISyncCurrentPathNormalizesHiddenRootAfterSectionSwitch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithModel("desktop", vault.Model{
|
||||||
|
Entries: []vault.Entry{
|
||||||
|
{ID: "1", Title: "Vault Console", Path: []string{"keepass", "Crew", "Internet"}},
|
||||||
|
},
|
||||||
|
Groups: [][]string{
|
||||||
|
{"keepass"},
|
||||||
|
{"keepass", "Crew"},
|
||||||
|
{"Recycle Bin"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
u.showEntriesSection()
|
||||||
|
u.showAPITokensSection()
|
||||||
|
u.state.Section = appstate.SectionEntries
|
||||||
|
u.state.CurrentPath = []string{"Root"}
|
||||||
|
u.currentPath = nil
|
||||||
|
u.syncedPath = nil
|
||||||
|
|
||||||
|
u.syncCurrentPath()
|
||||||
|
|
||||||
|
if got := u.currentPath; !slices.Equal(got, []string{"keepass"}) {
|
||||||
|
t.Fatalf("currentPath after syncCurrentPath() = %v, want [keepass]", got)
|
||||||
|
}
|
||||||
|
if got := u.displayPath(); len(got) != 0 {
|
||||||
|
t.Fatalf("displayPath() after syncCurrentPath() = %v, want root slash path", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUIShowEntriesSectionRestoresEntriesViewState(t *testing.T) {
|
func TestUIShowEntriesSectionRestoresEntriesViewState(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user