Line data Source code
1 : // SPDX-FileCopyrightText: 2015 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief Dialog for creating or editing one password entry, including the
6 : * generator, template fields, OTP handling and the asynchronous save.
7 : */
8 : #ifndef SRC_PASSWORDDIALOG_H_
9 : #define SRC_PASSWORDDIALOG_H_
10 :
11 : #include "appsettings.h"
12 :
13 : #include <QDialog>
14 : #include <QPointer>
15 : #include <QScopedPointer>
16 :
17 : namespace Ui {
18 : class PasswordDialog;
19 : }
20 :
21 : class Pass;
22 : class FieldLabel;
23 : class QAction;
24 : class QLineEdit;
25 : class SecretLineEdit;
26 : class QShortcut;
27 : class QWidget;
28 :
29 : /**
30 : * @class PasswordDialog
31 : * @ingroup ui
32 : * @brief Dialog for creating a password entry or editing an existing one.
33 : *
34 : * The first line is the password, with a generator beside it; below it are
35 : * one field per template line when templates are in use, and a free text
36 : * box for everything else. For an existing entry the dialog asks the
37 : * backend to Show() it and fills the fields when the content arrives. On OK
38 : * it puts the entry together again and calls Pass::Insert(), then stays
39 : * open with its fields locked until the backend reports: it closes on
40 : * Pass::finishedInsert() and gives the fields back with the reason on
41 : * Pass::insertFailed(), so a failed save never loses what was typed.
42 : */
43 : class PasswordDialog : public QDialog {
44 242 : Q_OBJECT
45 :
46 : public:
47 : /**
48 : * @brief Construct a PasswordDialog for entering a new password.
49 : * @param passConfig Password generation configuration.
50 : * @param parent Optional parent widget.
51 : */
52 : explicit PasswordDialog(PasswordConfiguration passConfig,
53 : QWidget *parent = nullptr);
54 : /**
55 : * @brief Construct a PasswordDialog for editing an existing password file.
56 : * @param pass Backend used to show, insert, and generate passwords.
57 : * @param s Application settings snapshot (password config, template, pwgen).
58 : * @param file Path to the password file being edited.
59 : * @param isNew true if creating a new entry, false if editing existing.
60 : * @param parent Optional parent widget.
61 : */
62 : PasswordDialog(Pass *pass, const AppSettings &s, QString file,
63 : const bool &isNew, QWidget *parent = nullptr);
64 : /**
65 : * @brief Hides the window before QDialog tears it down, so the focus-out
66 : * emitted on hiding cannot reach slots of a half-destroyed dialog.
67 : */
68 : ~PasswordDialog() override;
69 :
70 : /**
71 : * @brief OK: for a new entry, resolve and check the name first and stay
72 : * open when it is not usable; then insert the entry and close as accepted
73 : * only once the insert has succeeded.
74 : *
75 : * The insert is asynchronous. Until it reports back the dialog stays open
76 : * with its fields locked; when it fails (a missing recipient key, an
77 : * unusable `.gpg-id`, a refused path) the fields come back with what was
78 : * typed and the reason is shown, instead of the entry being lost with the
79 : * closed dialog.
80 : */
81 : void accept() override;
82 :
83 : /**
84 : * @brief Cancel, Esc and the window's close button. While an insert is in
85 : * flight they ask first: closing then means a failure can no longer hand
86 : * back what was typed, but a save that never reports must not leave
87 : * quitting as the only way out.
88 : */
89 : void reject() override;
90 :
91 : /**
92 : * @brief Let a new entry be named inside the dialog: a folder picker over
93 : * the store's folders and a name field, instead of a separate prompt.
94 : *
95 : * The dialog validates as the user types (empty, escaping the store,
96 : * already taken) and keeps OK off until the name is good; accept() then
97 : * creates any subfolder the name needs and inserts the entry at
98 : * entryPath(). Only meaningful for a dialog constructed with isNew.
99 : * @param storeRoot Absolute path of the password store.
100 : * @param folders Folders relative to the store, "" for the root; shown in
101 : * the picker in this order.
102 : * @param currentFolder The folder to preselect (relative, may be "").
103 : */
104 : void setNewEntryLocation(const QString &storeRoot, const QStringList &folders,
105 : const QString ¤tFolder);
106 :
107 : /**
108 : * @brief The entry the dialog writes to, relative to the store and
109 : * without the `.gpg` suffix (for example `work/vpn`).
110 : * @return The path; for an existing entry the one given at construction.
111 : */
112 : auto entryPath() const -> QString { return m_file; }
113 :
114 : /**
115 : * @brief Populate the dialog's password field with the given text.
116 : * @param password Password text to display.
117 : * @sa getPassword
118 : */
119 : void setPassword(const QString &password);
120 :
121 : /**
122 : * @brief Retrieve the current text from the dialog's password field.
123 : * @return Current password as a QString.
124 : * @sa setPassword
125 : */
126 : auto getPassword() -> QString;
127 :
128 : /**
129 : * @brief Set the template fields and whether templating is enabled.
130 : * @param rawFields Template text containing field definitions.
131 : * @param useTemplate true to enable the template, false to disable.
132 : */
133 : void setTemplate(const QString &rawFields, bool useTemplate);
134 :
135 : /**
136 : * @brief Whether every `key: value` line becomes a field of its own.
137 : *
138 : * Mirrors the "Template all fields" setting. Off, only the template's
139 : * fields get widgets and every other line stays in the free-text body,
140 : * where the user can edit key and value alike.
141 : * @param templateAll true to split every tokenisable line into a field.
142 : */
143 : void templateAll(bool templateAll);
144 :
145 : /**
146 : * @brief Set the desired password length shown in the dialog.
147 : * @param length Desired password length.
148 : */
149 : void setLength(int length);
150 :
151 : /**
152 : * @brief Set the password character template index.
153 : * @param templateIndex Index identifying the character template to use.
154 : */
155 : void setPasswordCharTemplate(int templateIndex);
156 :
157 : /**
158 : * @brief Enable or disable pwgen-style password generation mode.
159 : * @param usePwgen true to enable pwgen mode, false to disable.
160 : */
161 : void usePwgen(bool usePwgen);
162 :
163 : /**
164 : * @brief Set available templates from file and select default.
165 : * @param templates Hash of template name to field list.
166 : * @param defaultTemplate Name of default template.
167 : */
168 : void setAvailableTemplates(const QHash<QString, QStringList> &templates,
169 : const QString &defaultTemplate);
170 :
171 : signals:
172 : /**
173 : * @brief Text was copied out of the password, a field or the body, marked
174 : * as a secret: for ClipboardManager::trackCopied(), so autoclear clears it.
175 : * @param text The text copied.
176 : */
177 : void copied(const QString &text);
178 :
179 : public slots:
180 : /**
181 : * @brief Cycle to next template (Ctrl+T).
182 : */
183 : void cycleTemplate();
184 :
185 : /**
186 : * @brief Populate the dialog from a decrypted entry (Pass::finishedShow).
187 : *
188 : * Entries with more than FileContent::kMaxDisplayLines lines are not loaded
189 : * at all; the status label says so and the editor stays locked.
190 : * @param output Decrypted content of the entry.
191 : * @param file The entry the output belongs to; output for any other entry
192 : * than the one being edited is ignored. Empty means unspecified.
193 : */
194 : void setPass(const QString &output, const QString &file = QString());
195 :
196 : private slots:
197 : /**
198 : * @brief Show or mask the password field.
199 : *
200 : * Connected explicitly to QCheckBox::toggled in both constructors; the box
201 : * is two-state, so the deprecated stateChanged(int) is not needed.
202 : * @param show true to display the password in clear text.
203 : */
204 : void setPasswordVisible(bool show);
205 : /**
206 : * @brief "Generate" button: asks the backend for a password of the chosen
207 : * length from the selected character set (Pass::generatePassword, which
208 : * runs pwgen when that mode is on) and puts it in the password field. An
209 : * empty result leaves the field alone.
210 : */
211 : void on_createPasswordButton_clicked();
212 : /**
213 : * @brief Connected to QDialog::rejected: blanks the password and field
214 : * widgets so the decrypted secret does not linger in a dialog that is
215 : * being discarded.
216 : */
217 : void on_rejected();
218 :
219 : /**
220 : * @brief The insert started by accept() succeeded: close as accepted.
221 : */
222 : void onInsertFinished();
223 : /**
224 : * @brief The insert started by accept() failed: unlock the fields, which
225 : * still hold what was typed, and show why.
226 : * @param err The reason reported by the backend.
227 : * @param written true when the entry was written and only a later step
228 : * (git) failed; a new entry then closes the dialog.
229 : */
230 : void onInsertFailed(const QString &err, bool written);
231 :
232 : /**
233 : * @brief Handle a process error while waiting for an entry's decrypt.
234 : *
235 : * When the asynchronous Show() fails, keep the dialog open and surface the
236 : * reason in statusLabel instead of closing silently: Ok stays disabled and
237 : * the editor keeps its lock so the user can only Cancel/withdraw.
238 : * @param exitCode Exit code of the failed process (unused).
239 : * @param err Error output to present to the user.
240 : */
241 : void onShowError(int exitCode, const QString &err);
242 :
243 : private:
244 : /**
245 : * @brief Forwards the line edit's SecretLineEdit::copied to this dialog's
246 : * copied(), so every copy out of any field reaches ClipboardManager.
247 : * @param line A password or field editor just created.
248 : */
249 : void reportCopies(SecretLineEdit *line);
250 : /**
251 : * @brief Locate the field that holds the one-time password configuration.
252 : * @return The matching QLineEdit, or nullptr when the entry has no OTP field.
253 : */
254 : [[nodiscard]] auto otpLineEdit() const -> QLineEdit *;
255 : /**
256 : * @brief Enabled or grey out every editable field of the dialog.
257 : *
258 : * Used while an existing entry's decrypted content is being fetched, so an
259 : * early Ok cannot report success while writing nothing and a late setPass()
260 : * cannot clobber edits made on the still-empty fields.
261 : * @param enabled true to restore the editor, false to lock it.
262 : */
263 : void setEditorEnabled(bool enabled);
264 : /**
265 : * @brief Start inserting what the dialog holds and wait for the result.
266 : */
267 : void save();
268 : /**
269 : * @brief Lock or unlock the dialog while an insert is in flight.
270 : * @param saving true while waiting for the backend.
271 : */
272 : void setSaving(bool saving);
273 : /**
274 : * @brief Whether the insert report now arriving is this dialog's own.
275 : *
276 : * False when the dialog is not saving. Otherwise it lets the reports of the
277 : * inserts that were already pending when save() ran pass first (one per
278 : * call), so an earlier dialog's result does not close or fail this one.
279 : * @return true when the arriving report belongs to this dialog's insert.
280 : */
281 : auto isOwnInsertReport() -> bool;
282 : /**
283 : * @brief Say why the entry was not saved and let the user try again; the
284 : * fields keep what was typed.
285 : * @param err The reason reported by the backend.
286 : */
287 : void showNotSaved(const QString &err);
288 : /**
289 : * @brief Connect validation and normalisation to the OTP field, if present.
290 : *
291 : * Called whenever the field widgets are rebuilt, since both setTemplate()
292 : * and setPassword() recreate them.
293 : */
294 : void hookOtpField();
295 : /**
296 : * @brief Flag an OTP value that is neither a URI nor valid base32.
297 : */
298 : void validateOtpField();
299 : /**
300 : * @brief Remember, on the line edit that sent textEdited(), that the user
301 : * typed in it: only such a field has a bare secret canonicalised, a loaded
302 : * value is kept as it was. The mark lives on the widget, not on the dialog,
303 : * so typing into a field that used to be the OTP one (renamed away) says
304 : * nothing about the field that is the OTP one now. A member function:
305 : * Qt::UniqueConnection, which hookOtpField() needs because it runs more
306 : * than once for the same field, is refused for a lambda (a debug build
307 : * asserts, a release build makes no connection).
308 : */
309 : void markOtpFieldEdited();
310 : /**
311 : * @brief Rewrite the OTP field as a canonical otpauth URI.
312 : *
313 : * A value that cannot be parsed is left exactly as the user typed it, so
314 : * nothing is silently destroyed.
315 : */
316 : void normalizeOtpField();
317 :
318 : /** @brief Generated form (`passworddialog.ui`); owned. */
319 : QScopedPointer<Ui::PasswordDialog> ui;
320 : /** @brief Generator settings: character sets and default length. */
321 : PasswordConfiguration m_passConfig;
322 : /** @brief Backend for Show, Insert and generatePassword; not owned. */
323 : Pass *m_pass{nullptr};
324 : /** @brief Field names of the current template, in template order. */
325 : QStringList m_fields;
326 : /** @brief Entry path relative to the store, without `.gpg`; empty for a
327 : * new entry until accept() resolves the typed name. */
328 : QString m_file;
329 : /// Absolute store root while a new entry is being named; empty otherwise.
330 : QString m_storeRoot;
331 : /// Folder chosen when the dialog opened; the template default follows the
332 : /// picker, so this is what setAvailableTemplates() was computed for.
333 : QString m_newEntryFolder;
334 :
335 : /**
336 : * @brief The name the user typed, resolved against the picked folder.
337 : * @param problem Receives a translated reason when the name is unusable.
338 : * @return The relative entry path, or an empty string on a problem.
339 : */
340 : auto resolveNewEntry(QString *problem) const -> QString;
341 : /**
342 : * @brief Re-runs resolveNewEntry() as the user types or picks a folder:
343 : * shows the problem (if any) in the status label and enables OK only for a
344 : * usable name.
345 : */
346 : void validateNewEntry();
347 : /**
348 : * @brief Give a `key: value` field a new key (#132). Refused, with the
349 : * reason in the status label, when another field has that name.
350 : * Re-runs hookOtpField(), as the field may have become or stopped
351 : * being the OTP one.
352 : * @param line The field's editor; its object name is the key.
353 : * @param label The field's label, updated to show the new key.
354 : * @param to The new key.
355 : */
356 : void renameField(QLineEdit *line, FieldLabel *label, const QString &to);
357 : /**
358 : * @brief Drop a `key: value` field and its row from the form. The widgets
359 : * are deleted once control is back in the event loop: the request comes
360 : * from the line's own trailing action or its label's context menu, both
361 : * still on the stack.
362 : * @param line The field's editor, one of m_otherLines.
363 : */
364 : void removeField(QLineEdit *line);
365 : /** @brief Whether template fields get widgets of their own (the
366 : * "Use template" setting). */
367 : bool m_templating{};
368 : /** @brief Whether every `key: value` line becomes a field (the "Template
369 : * all fields" setting), as given to FileContent::parse(). */
370 : bool m_allFields{};
371 : /** @brief True when creating an entry, false when editing one; decides
372 : * whether the name row and the decrypt-before-edit lock apply. */
373 : bool m_isNew{};
374 : /// True once the existing entry's decrypted content has been loaded, so
375 : /// save() can refuse to overwrite it with empty fields before the
376 : /// asynchronous Show completes.
377 : bool m_contentLoaded{};
378 : /// True from accept() starting an insert until the backend reports back.
379 : bool m_saving{};
380 : /// Inserts already pending when save() ran, whose reports must pass before
381 : /// one is taken as this dialog's own (counted down in isOwnInsertReport()).
382 : int m_insertsAhead{};
383 : /// Last pwgen mode passed to usePwgen(), so the policy can be re-applied
384 : /// after setEditorEnabled() re-enables the character-set widgets.
385 : bool m_usePwgen{};
386 : /// Editors for the template's fields, in template order; rebuilt by
387 : /// setTemplate(). Their labels are plain QLabels: the names are fixed.
388 : QList<QLineEdit *> m_templateLines;
389 : /// Fields that came from the entry rather than the template; their names
390 : /// are the user's, so their labels are FieldLabels that can be renamed.
391 : QList<QLineEdit *> m_otherLines;
392 : /// Named templates from `.templates`, name to field list, as given to
393 : /// setAvailableTemplates(); empty when the store has none.
394 : QHash<QString, QStringList> m_availableTemplates;
395 : /// Name of the template currently applied; cycleTemplate() steps from it.
396 : QString m_currentTemplateName;
397 : /// Ctrl+T, created once templates are available; owned by the dialog.
398 : QShortcut *m_templateShortcut{nullptr};
399 : /// Warning indicator shown inside the OTP field; owned by that field.
400 : ///
401 : /// QPointer, not a raw pointer: the QAction is parented to the QLineEdit, and
402 : /// setPassword() deletes the m_otherLines widgets before calling
403 : /// hookOtpField(), so a raw pointer would already dangle there.
404 : QPointer<QAction> m_otpWarning;
405 : /// The line edit hookOtpField() last wired up, so its signals can be
406 : /// disconnected when another field becomes the OTP one (a rename, a
407 : /// removal): a stale editingFinished from the old field must not rewrite
408 : /// the new one.
409 : QPointer<QLineEdit> m_otpLine;
410 :
411 : /**
412 : * @brief Switch to a named template: records it, selects it in the combo
413 : * box and rebuilds the field rows through setTemplate(). Unknown names
414 : * are ignored, and nothing changes while a save is in flight.
415 : * @param templateName A key of m_availableTemplates.
416 : */
417 : void applyTemplate(const QString &templateName);
418 : /**
419 : * @brief Hides the template row until setAvailableTemplates() fills it and
420 : * wires the combo box so that a user selection calls applyTemplate().
421 : */
422 : void setupTemplateBox();
423 : };
424 :
425 : #endif // SRC_PASSWORDDIALOG_H_
|