404 lines
9.2 KiB
Go
404 lines
9.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.julianfamily.org/keepassgo/clipboard"
|
|
"git.julianfamily.org/keepassgo/passwords"
|
|
"git.julianfamily.org/keepassgo/vault"
|
|
)
|
|
|
|
func (u *ui) attachmentInput() (string, []byte, error) {
|
|
name := strings.TrimSpace(u.attachmentName.Text())
|
|
if name == "" {
|
|
return "", nil, fmt.Errorf("attachment name is required")
|
|
}
|
|
|
|
path := strings.TrimSpace(u.attachmentPath.Text())
|
|
if path == "" {
|
|
return "", nil, fmt.Errorf("attachment path is required")
|
|
}
|
|
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("stat attachment: %w", err)
|
|
}
|
|
if info.Size() > maxAttachmentBytes {
|
|
return "", nil, fmt.Errorf("attachment too large: %d bytes exceeds %d byte limit", info.Size(), maxAttachmentBytes)
|
|
}
|
|
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("read attachment: %w", err)
|
|
}
|
|
return name, content, nil
|
|
}
|
|
|
|
func (u *ui) loadSelectedEntryIntoEditor() {
|
|
u.selectedHistoryIndex = -1
|
|
u.historyIndex.SetText("")
|
|
|
|
item, ok := u.selectedEntry()
|
|
if !ok {
|
|
u.entryID.SetText("")
|
|
u.entryTitle.SetText("")
|
|
u.entryUsername.SetText("")
|
|
u.entryPassword.SetText("")
|
|
u.entryURL.SetText("")
|
|
u.entryNotes.SetText("")
|
|
u.entryTags.SetText("")
|
|
u.entryPath.SetText(strings.Join(u.state.CurrentPath, " / "))
|
|
u.entryFields.SetText("")
|
|
u.attachmentName.SetText("")
|
|
u.attachmentPath.SetText("")
|
|
u.exportAttachmentPath.SetText("")
|
|
return
|
|
}
|
|
|
|
u.entryID.SetText(item.ID)
|
|
u.entryTitle.SetText(item.Title)
|
|
u.entryUsername.SetText(item.Username)
|
|
u.entryPassword.SetText(item.Password)
|
|
u.entryURL.SetText(item.URL)
|
|
u.entryNotes.SetText(item.Notes)
|
|
u.entryTags.SetText(strings.Join(item.Tags, ", "))
|
|
u.entryPath.SetText(strings.Join(item.Path, " / "))
|
|
u.entryFields.SetText(marshalFields(item.Fields))
|
|
u.attachmentName.SetText("")
|
|
u.attachmentPath.SetText("")
|
|
u.exportAttachmentPath.SetText("")
|
|
}
|
|
|
|
func (u *ui) visibleHistory() []vault.Entry {
|
|
item, ok := u.selectedEntry()
|
|
if !ok || len(item.History) == 0 {
|
|
return nil
|
|
}
|
|
return append([]vault.Entry(nil), item.History...)
|
|
}
|
|
|
|
func (u *ui) selectedHistoryEntry() (vault.Entry, bool) {
|
|
history := u.visibleHistory()
|
|
if u.selectedHistoryIndex < 0 || u.selectedHistoryIndex >= len(history) {
|
|
return vault.Entry{}, false
|
|
}
|
|
return history[u.selectedHistoryIndex], true
|
|
}
|
|
|
|
func (u *ui) selectHistoryVersion(index int) error {
|
|
history := u.visibleHistory()
|
|
if index < 0 || index >= len(history) {
|
|
return fmt.Errorf("history index %d out of range", index)
|
|
}
|
|
|
|
u.selectedHistoryIndex = index
|
|
u.historyIndex.SetText(strconv.Itoa(index))
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) saveEntryAction() error {
|
|
entry, err := u.editorEntry()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := u.state.UpsertEntry(entry); err != nil {
|
|
return err
|
|
}
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) duplicateSelectedEntryAction() error {
|
|
baseID := strings.TrimSpace(u.state.SelectedEntryID)
|
|
if baseID == "" {
|
|
return fmt.Errorf("no entry selected")
|
|
}
|
|
|
|
duplicateID := baseID + "-copy"
|
|
if _, err := u.state.DuplicateSelectedEntry(duplicateID); err != nil {
|
|
return err
|
|
}
|
|
|
|
u.loadSelectedEntryIntoEditor()
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) deleteSelectedEntryAction() error {
|
|
if err := u.state.DeleteSelectedEntry(); err != nil {
|
|
return err
|
|
}
|
|
|
|
u.loadSelectedEntryIntoEditor()
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) restoreSelectedRecycleEntryAction() error {
|
|
id := strings.TrimSpace(u.state.SelectedEntryID)
|
|
if id == "" {
|
|
return fmt.Errorf("no recycle-bin entry selected")
|
|
}
|
|
if err := u.state.RestoreEntry(id); err != nil {
|
|
return err
|
|
}
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) saveTemplateAction() error {
|
|
entry, err := u.editorEntry()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(entry.Path) == 0 {
|
|
entry.Path = []string{"Templates"}
|
|
}
|
|
if err := u.state.UpsertTemplate(entry); err != nil {
|
|
return err
|
|
}
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) deleteSelectedTemplateAction() error {
|
|
id := strings.TrimSpace(u.state.SelectedEntryID)
|
|
if id == "" {
|
|
return fmt.Errorf("no template selected")
|
|
}
|
|
if err := u.state.DeleteTemplate(id); err != nil {
|
|
return err
|
|
}
|
|
u.loadSelectedEntryIntoEditor()
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) createGroupAction() error {
|
|
return u.state.CreateGroup(strings.TrimSpace(u.groupName.Text()))
|
|
}
|
|
|
|
func (u *ui) renameGroupAction() error {
|
|
return u.state.RenameCurrentGroup(strings.TrimSpace(u.groupName.Text()))
|
|
}
|
|
|
|
func (u *ui) deleteCurrentGroupAction() error {
|
|
if err := u.state.DeleteCurrentGroup(); err != nil {
|
|
return err
|
|
}
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) instantiateSelectedTemplateAction() error {
|
|
templateID := strings.TrimSpace(u.state.SelectedEntryID)
|
|
if templateID == "" {
|
|
return fmt.Errorf("no template selected")
|
|
}
|
|
|
|
entry, err := u.editorEntry()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := u.state.InstantiateTemplate(templateID, entry); err != nil {
|
|
return err
|
|
}
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) addAttachmentAction() error {
|
|
name, content, err := u.attachmentInput()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := u.state.AddAttachmentToSelectedEntry(name, content); err != nil {
|
|
return err
|
|
}
|
|
u.loadSelectedEntryIntoEditor()
|
|
u.attachmentName.SetText(name)
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) replaceAttachmentAction() error {
|
|
name, content, err := u.attachmentInput()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := u.state.ReplaceAttachmentOnSelectedEntry(name, content); err != nil {
|
|
return err
|
|
}
|
|
u.loadSelectedEntryIntoEditor()
|
|
u.attachmentName.SetText(name)
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) exportAttachmentAction() error {
|
|
item, ok := u.selectedEntry()
|
|
if !ok {
|
|
return fmt.Errorf("no entry selected")
|
|
}
|
|
|
|
name := strings.TrimSpace(u.attachmentName.Text())
|
|
content, ok := item.Attachments[name]
|
|
if !ok {
|
|
return fmt.Errorf("attachment not found")
|
|
}
|
|
|
|
exportPath := strings.TrimSpace(u.exportAttachmentPath.Text())
|
|
if exportPath == "" {
|
|
return fmt.Errorf("export attachment path is required")
|
|
}
|
|
if err := os.WriteFile(exportPath, content, 0o600); err != nil {
|
|
return fmt.Errorf("write attachment export: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) removeAttachmentAction() error {
|
|
name := strings.TrimSpace(u.attachmentName.Text())
|
|
if name == "" {
|
|
return fmt.Errorf("attachment name is required")
|
|
}
|
|
|
|
if err := u.state.DeleteAttachmentFromSelectedEntry(name); err != nil {
|
|
return err
|
|
}
|
|
u.loadSelectedEntryIntoEditor()
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) restoreSelectedHistoryAction() error {
|
|
index, err := u.selectedHistoryVersionIndex()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := u.state.RestoreSelectedEntryVersion(index); err != nil {
|
|
return err
|
|
}
|
|
u.loadSelectedEntryIntoEditor()
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) selectedHistoryVersionIndex() (int, error) {
|
|
text := strings.TrimSpace(u.historyIndex.Text())
|
|
if text != "" {
|
|
index, err := strconv.Atoi(text)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid history index: %w", err)
|
|
}
|
|
return index, nil
|
|
}
|
|
if u.selectedHistoryIndex >= 0 {
|
|
return u.selectedHistoryIndex, nil
|
|
}
|
|
return 0, fmt.Errorf("no history version selected")
|
|
}
|
|
|
|
func (u *ui) copySelectedFieldAction(target clipboard.Target) error {
|
|
model, err := u.state.Session.Current()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
service := clipboard.Service{Writer: u.clipboardWriter}
|
|
return service.Copy(model, u.state.SelectedEntryID, target)
|
|
}
|
|
|
|
func (u *ui) generatePasswordAction() error {
|
|
profile, err := passwords.LookupDefaultProfile(u.passwordProfile.Text())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
password, err := passwords.Generate(profile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
u.entryPassword.SetText(password)
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) editorEntry() (vault.Entry, error) {
|
|
path := parsePath(u.entryPath.Text())
|
|
fields, err := parseFields(u.entryFields.Text())
|
|
if err != nil {
|
|
return vault.Entry{}, err
|
|
}
|
|
|
|
return vault.Entry{
|
|
ID: strings.TrimSpace(u.entryID.Text()),
|
|
Title: strings.TrimSpace(u.entryTitle.Text()),
|
|
Username: strings.TrimSpace(u.entryUsername.Text()),
|
|
Password: u.entryPassword.Text(),
|
|
URL: strings.TrimSpace(u.entryURL.Text()),
|
|
Notes: strings.TrimSpace(u.entryNotes.Text()),
|
|
Tags: parseTags(u.entryTags.Text()),
|
|
Path: path,
|
|
Fields: fields,
|
|
}, nil
|
|
}
|
|
|
|
func parsePath(text string) []string {
|
|
var out []string
|
|
for _, part := range strings.Split(text, "/") {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
out = append(out, part)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseTags(text string) []string {
|
|
var out []string
|
|
for _, part := range strings.Split(text, ",") {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
out = append(out, part)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseFields(text string) (map[string]string, error) {
|
|
if strings.TrimSpace(text) == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
fields := map[string]string{}
|
|
for _, line := range strings.Split(text, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
key, value, ok := strings.Cut(line, "=")
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid field line %q", line)
|
|
}
|
|
fields[strings.TrimSpace(key)] = strings.TrimSpace(value)
|
|
}
|
|
return fields, nil
|
|
}
|
|
|
|
func marshalFields(fields map[string]string) string {
|
|
if len(fields) == 0 {
|
|
return ""
|
|
}
|
|
|
|
lines := make([]string, 0, len(fields))
|
|
for key, value := range fields {
|
|
lines = append(lines, key+"="+value)
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|