package appstate import ( "fmt" "slices" "strings" "git.julianfamily.org/keepassgo/vault" "git.julianfamily.org/keepassgo/webdav" ) type Section string const ( SectionEntries Section = "" SectionTemplates Section = "templates" SectionRecycleBin Section = "recycle-bin" ) type CurrentSession interface { Current() (vault.Model, error) } type MutableSession interface { CurrentSession Replace(vault.Model) } type LockableSession interface { CurrentSession Lock() error Unlock(vault.MasterKey) error } type SaveableSession interface { CurrentSession Save() error } type CreateableSession interface { CurrentSession Create(vault.Model, vault.MasterKey) error } type OpenableSession interface { CurrentSession Open(string, vault.MasterKey) error } type SaveAsSession interface { CurrentSession SaveAs(string) error } type RemoteOpenableSession interface { CurrentSession OpenRemote(webdav.Client, string, vault.MasterKey) error } type State struct { Session CurrentSession Section Section CurrentPath []string SearchQuery string SelectedEntryID string Dirty bool } func (s *State) VisibleEntries() ([]vault.Entry, error) { model, err := s.currentModel() if err != nil { return nil, err } entries := s.entriesForSection(model) if strings.TrimSpace(s.SearchQuery) != "" { return filterEntries(entries, s.SearchQuery), nil } if s.Section == SectionEntries { return model.EntriesInPath(s.CurrentPath), nil } if s.Section == SectionRecycleBin || len(s.CurrentPath) == 0 { return entries, nil } return entriesInPath(entries, s.CurrentPath), nil } func (s *State) ChildGroups() ([]string, error) { if strings.TrimSpace(s.SearchQuery) != "" { return nil, nil } model, err := s.currentModel() if err != nil { return nil, err } if s.Section != SectionEntries { if s.Section == SectionTemplates && len(s.CurrentPath) == 0 { return childGroups(s.entriesForSection(model), []string{"Templates"}), nil } return childGroups(s.entriesForSection(model), s.CurrentPath), nil } return model.ChildGroups(s.CurrentPath), nil } func (s *State) SelectVisibleIndex(index int) error { entries, err := s.VisibleEntries() if err != nil { return err } if index < 0 || index >= len(entries) { return fmt.Errorf("visible index %d out of range", index) } s.SelectedEntryID = entries[index].ID return nil } func (s *State) ToggleVisibleIndex(index int) error { entries, err := s.VisibleEntries() if err != nil { return err } if index < 0 || index >= len(entries) { return fmt.Errorf("visible index %d out of range", index) } if s.SelectedEntryID == entries[index].ID { s.SelectedEntryID = "" return nil } s.SelectedEntryID = entries[index].ID return nil } func (s *State) currentModel() (vault.Model, error) { if s.Session == nil { return vault.Model{}, nil } return s.Session.Current() } func (s *State) entriesForSection(model vault.Model) []vault.Entry { switch s.Section { case SectionTemplates: return slices.Clone(model.Templates) case SectionRecycleBin: return slices.Clone(model.RecycleBin) default: return slices.Clone(model.Entries) } } func entriesInPath(entries []vault.Entry, path []string) []vault.Entry { var out []vault.Entry for _, entry := range entries { if slices.Equal(entry.Path, path) { out = append(out, entry) } } slices.SortFunc(out, func(a, b vault.Entry) int { switch { case a.Title < b.Title: return -1 case a.Title > b.Title: return 1 default: return 0 } }) return out } func filterEntries(entries []vault.Entry, query string) []vault.Entry { query = strings.TrimSpace(strings.ToLower(query)) if query == "" { return nil } var out []vault.Entry for _, entry := range entries { haystack := strings.ToLower( entry.Title + " " + entry.Username + " " + entry.URL + " " + strings.Join(entry.Path, " "), ) if !strings.Contains(haystack, query) { continue } out = append(out, entry) } slices.SortFunc(out, func(a, b vault.Entry) int { switch { case a.Title < b.Title: return -1 case a.Title > b.Title: return 1 default: return 0 } }) return out } func childGroups(entries []vault.Entry, path []string) []string { seen := map[string]bool{} var groups []string for _, entry := range entries { if len(path) > len(entry.Path) { continue } if !slices.Equal(entry.Path[:len(path)], path) { continue } if len(entry.Path) == len(path) { continue } group := entry.Path[len(path)] if seen[group] { continue } seen[group] = true groups = append(groups, group) } slices.Sort(groups) return groups } func (s *State) DeleteSelectedEntry() error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } if err := model.DeleteEntry(s.SelectedEntryID); err != nil { return err } session.Replace(model) s.SelectedEntryID = "" s.Dirty = true return nil } func (s *State) RestoreEntry(id string) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } if err := model.RestoreEntry(id); err != nil { return err } session.Replace(model) s.Dirty = true return nil } func (s *State) UpsertEntry(entry vault.Entry) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } model.UpsertEntry(entry) session.Replace(model) s.SelectedEntryID = entry.ID s.Dirty = true return nil } func (s *State) UpsertTemplate(entry vault.Entry) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } model.UpsertTemplate(entry) session.Replace(model) s.SelectedEntryID = entry.ID s.Dirty = true return nil } func (s *State) InstantiateTemplate(templateID string, overrides vault.Entry) (vault.Entry, error) { session, ok := s.Session.(MutableSession) if !ok { return vault.Entry{}, fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return vault.Entry{}, err } entry, err := model.InstantiateTemplate(templateID, overrides) if err != nil { return vault.Entry{}, err } session.Replace(model) s.SelectedEntryID = entry.ID s.Dirty = true return entry, nil } func (s *State) DeleteTemplate(id string) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } if err := model.DeleteTemplate(id); err != nil { return err } session.Replace(model) if s.SelectedEntryID == id { s.SelectedEntryID = "" } s.Dirty = true return nil } func (s *State) DuplicateSelectedEntry(duplicateID string) (vault.Entry, error) { session, ok := s.Session.(MutableSession) if !ok { return vault.Entry{}, fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return vault.Entry{}, err } duplicate, err := model.DuplicateEntry(s.SelectedEntryID, duplicateID) if err != nil { return vault.Entry{}, err } session.Replace(model) s.SelectedEntryID = duplicate.ID s.Dirty = true return duplicate, nil } func (s *State) RestoreSelectedEntryVersion(historyIndex int) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } if err := model.RestoreEntryVersion(s.SelectedEntryID, historyIndex); err != nil { return err } session.Replace(model) s.Dirty = true return nil } func (s *State) Lock() error { session, ok := s.Session.(LockableSession) if !ok { return fmt.Errorf("session is not lockable") } if err := session.Lock(); err != nil { return err } s.SelectedEntryID = "" return nil } func (s *State) Unlock(key vault.MasterKey) error { session, ok := s.Session.(LockableSession) if !ok { return fmt.Errorf("session is not lockable") } return session.Unlock(key) } func (s *State) EnterGroup(name string) { s.CurrentPath = append(append([]string(nil), s.CurrentPath...), name) s.SelectedEntryID = "" } func (s *State) NavigateToPath(path []string) { s.CurrentPath = append([]string(nil), path...) s.SelectedEntryID = "" } func (s *State) Save() error { session, ok := s.Session.(SaveableSession) if !ok { return fmt.Errorf("session is not saveable") } if err := session.Save(); err != nil { return err } s.Dirty = false return nil } func (s *State) CreateVault(key vault.MasterKey) error { session, ok := s.Session.(CreateableSession) if !ok { return fmt.Errorf("session is not createable") } if err := session.Create(vault.Model{}, key); err != nil { return err } s.CurrentPath = nil s.SelectedEntryID = "" s.Dirty = false return nil } func (s *State) OpenVault(path string, key vault.MasterKey) error { session, ok := s.Session.(OpenableSession) if !ok { return fmt.Errorf("session is not openable") } if err := session.Open(path, key); err != nil { return err } s.CurrentPath = nil s.SelectedEntryID = "" s.Dirty = false return nil } func (s *State) SaveAs(path string) error { session, ok := s.Session.(SaveAsSession) if !ok { return fmt.Errorf("session is not save-as capable") } if err := session.SaveAs(path); err != nil { return err } s.Dirty = false return nil } func (s *State) OpenRemoteVault(client webdav.Client, path string, key vault.MasterKey) error { session, ok := s.Session.(RemoteOpenableSession) if !ok { return fmt.Errorf("session is not remote-openable") } if err := session.OpenRemote(client, path, key); err != nil { return err } s.CurrentPath = nil s.SelectedEntryID = "" s.Dirty = false return nil } func (s *State) CreateGroup(name string) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } model.CreateGroup(s.CurrentPath, name) session.Replace(model) s.Dirty = true return nil } func (s *State) RenameCurrentGroup(newName string) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } if err := model.RenameGroup(s.CurrentPath, newName); err != nil { return err } session.Replace(model) if len(s.CurrentPath) > 0 { s.CurrentPath = append(append([]string(nil), s.CurrentPath[:len(s.CurrentPath)-1]...), newName) } s.Dirty = true return nil } func (s *State) MoveSelectedEntry(path []string) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } if err := model.MoveEntry(s.SelectedEntryID, path); err != nil { return err } session.Replace(model) s.Dirty = true return nil } func (s *State) AddAttachmentToSelectedEntry(name string, content []byte) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } for i := range model.Entries { if model.Entries[i].ID != s.SelectedEntryID { continue } if model.Entries[i].Attachments == nil { model.Entries[i].Attachments = map[string][]byte{} } model.Entries[i].Attachments[name] = append([]byte(nil), content...) session.Replace(model) s.Dirty = true return nil } return vault.ErrEntryNotFound } func (s *State) DeleteAttachmentFromSelectedEntry(name string) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } for i := range model.Entries { if model.Entries[i].ID != s.SelectedEntryID { continue } delete(model.Entries[i].Attachments, name) if len(model.Entries[i].Attachments) == 0 { model.Entries[i].Attachments = nil } session.Replace(model) s.Dirty = true return nil } return vault.ErrEntryNotFound }