1package termio
2
3import (
4	"context"
5	"fmt"
6	"io"
7	"os"
8	"strconv"
9	"strings"
10
11	"github.com/gopasspw/gopass/internal/out"
12	"github.com/gopasspw/gopass/pkg/ctxutil"
13)
14
15var (
16	// Stderr is exported for tests
17	Stderr io.Writer = os.Stderr
18	// Stdin is exported for tests
19	Stdin io.Reader = os.Stdin
20	// ErrAborted is returned if the user aborts an action
21	ErrAborted = fmt.Errorf("user aborted")
22)
23
24const (
25	maxTries = 42
26)
27
28// AskForString asks for a string once, using the default if the
29// answer is empty. Errors are only returned on I/O errors
30func AskForString(ctx context.Context, text, def string) (string, error) {
31	if ctxutil.IsAlwaysYes(ctx) || !ctxutil.IsInteractive(ctx) {
32		return def, nil
33	}
34
35	// check for context cancelation
36	select {
37	case <-ctx.Done():
38		return def, ErrAborted
39	default:
40	}
41
42	fmt.Fprintf(Stderr, "%s [%s]: ", text, def)
43	input, err := NewReader(ctx, Stdin).ReadLine()
44	if err != nil {
45		return "", fmt.Errorf("failed to read user input: %w", err)
46	}
47	input = strings.TrimSpace(input)
48	if input == "" {
49		input = def
50	}
51	return input, nil
52}
53
54// AskForBool ask for a bool (yes or no) exactly once.
55// The empty answer uses the specified default, any other answer
56// is an error.
57func AskForBool(ctx context.Context, text string, def bool) (bool, error) {
58	if ctxutil.IsAlwaysYes(ctx) {
59		return def, nil
60	}
61
62	choices := "y/N/q"
63	if def {
64		choices = "Y/n/q"
65	}
66
67	str, err := AskForString(ctx, text, choices)
68	if err != nil {
69		return false, fmt.Errorf("failed to read user input: %w", err)
70	}
71	switch str {
72	case "Y/n/q":
73		return true, nil
74	case "y/N/q":
75		return false, nil
76	}
77
78	str = strings.ToLower(string(str[0]))
79	switch str {
80	case "y":
81		return true, nil
82	case "n":
83		return false, nil
84	case "q":
85		return false, ErrAborted
86	default:
87		return false, fmt.Errorf("unknown answer: %s", str)
88	}
89}
90
91// AskForInt asks for an valid interger once. If the input
92// can not be converted to an int it returns an error
93func AskForInt(ctx context.Context, text string, def int) (int, error) {
94	if ctxutil.IsAlwaysYes(ctx) {
95		return def, nil
96	}
97
98	str, err := AskForString(ctx, text+" (q to abort)", strconv.Itoa(def))
99	if err != nil {
100		return 0, err
101	}
102	if str == "q" {
103		return 0, ErrAborted
104	}
105	intVal, err := strconv.Atoi(str)
106	if err != nil {
107		return 0, fmt.Errorf("failed to convert to number: %w", err)
108	}
109	return intVal, nil
110}
111
112// AskForConfirmation asks a yes/no question until the user
113// replies yes or no
114func AskForConfirmation(ctx context.Context, text string) bool {
115	if ctxutil.IsAlwaysYes(ctx) {
116		return true
117	}
118
119	for i := 0; i < maxTries; i++ {
120		choice, err := AskForBool(ctx, text, false)
121		if err == nil {
122			return choice
123		}
124		if err == ErrAborted {
125			return false
126		}
127	}
128	return false
129}
130
131// AskForKeyImport asks for permissions to import the named key
132func AskForKeyImport(ctx context.Context, key string, names []string) bool {
133	if ctxutil.IsAlwaysYes(ctx) {
134		return true
135	}
136	if !ctxutil.IsInteractive(ctx) {
137		return false
138	}
139
140	ok, err := AskForBool(ctx, fmt.Sprintf("Do you want to import the public key %q (Names: %+v) into your keyring?", key, names), false)
141	if err != nil {
142		return false
143	}
144
145	return ok
146}
147
148// AskForPassword prompts for a password, optionally prompting twice until both match
149func AskForPassword(ctx context.Context, name string, repeat bool) (string, error) {
150	if ctxutil.IsAlwaysYes(ctx) {
151		return "", nil
152	}
153
154	askFn := GetPassPromptFunc(ctx)
155	for i := 0; i < maxTries; i++ {
156		// check for context cancellation
157		select {
158		case <-ctx.Done():
159			return "", ErrAborted
160		default:
161		}
162
163		pass, err := askFn(ctx, fmt.Sprintf("Enter %s", name))
164		if !repeat {
165			return pass, err
166		}
167		if err != nil {
168			return "", err
169		}
170
171		passAgain, err := askFn(ctx, fmt.Sprintf("Retype %s", name))
172		if err != nil {
173			return "", err
174		}
175
176		if pass == passAgain {
177			return pass, nil
178		}
179
180		out.Errorf(ctx, "Error: the entered password do not match")
181	}
182	return "", fmt.Errorf("no valid user input")
183}
184